From 9d0c0ea76d446de41d923de95df4f55bc12342e5 Mon Sep 17 00:00:00 2001 From: "simon.mittag" Date: Mon, 5 Jun 2023 22:54:10 +1000 Subject: [PATCH 1/2] added test for TLS config on downstream connection --- config_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/config_test.go b/config_test.go index f04247e..9fb4a04 100644 --- a/config_test.go +++ b/config_test.go @@ -317,6 +317,20 @@ func TestValidateConfigHasHttpAndTLS(t *testing.T) { config = config.validateHTTPConfig() } +func TestValidateConfigHasTLS(t *testing.T) { + config := &Config{ + Connection: Connection{ + Downstream: Downstream{ + Tls: Tls{ + Port: 443, + }, + }, + }, + } + + config = config.validateHTTPConfig() +} + func TestValidateConfigNoHttpAndNoTLSFails(t *testing.T) { defer func() { if r := recover(); r == nil { From 5911452f3411fed0027e894d2ea245bcb1b7a288 Mon Sep 17 00:00:00 2001 From: "simon.mittag" Date: Sun, 11 Jun 2023 15:23:28 +1000 Subject: [PATCH 2/2] fixed broken test --- config.go | 100 +++++++++++++++++ config_test.go | 258 +++++++++++++++++++++++++++++++++++++++++++ integration/j8a6.yml | 1 + route_test.go | 36 ++++-- server.go | 1 + 5 files changed, 388 insertions(+), 8 deletions(-) diff --git a/config.go b/config.go index 119e9fe..cc2c38c 100644 --- a/config.go +++ b/config.go @@ -3,8 +3,11 @@ package j8a import ( "bytes" "encoding/json" + "errors" "fmt" + "golang.org/x/net/idna" "io/ioutil" + "net" "os" "sort" "strings" @@ -164,10 +167,92 @@ func (config Config) validateResources() *Config { if len(resourceMappings) == 0 { config.panic("resource needs to have at least one url, see https://j8a.io/docs") } + for _, r := range resourceMappings { + if r.URL.Port <= 1 || r.URL.Port > 65535 { + config.panic("resource needs to have port between 0 and 65535") + } + if len(r.URL.Host) == 0 { + config.panic("resource needs to have host") + } else { + ie := validIpAddress(r) + he := validHostName(r) + if ie != nil && he != nil { + config.panic(fmt.Sprintf("resource host needs to be valid DNS name or IP address: %v", r.URL.Host)) + } + } + + sm := "resource needs to have valid scheme: " + if len(r.URL.Scheme) == 0 { + config.panic(sm + r.URL.Scheme) + } else if !validScheme(r.URL.Scheme) { + config.panic(sm + r.URL.Scheme) + } + } } return &config } +func validScheme(s string) bool { + s = strings.TrimSpace(s) + s = strings.TrimSuffix(s, "://") + s = strings.ToLower(s) + + schemes := [4]string{"http", "https", "ws", "wss"} + for _, v := range schemes { + if v == s { + return true + } + } + return false +} + +func validIpAddress(r ResourceMapping) error { + defaultErr := errors.New(fmt.Sprintf("invalid ipv4 or ipv6 address: %v", r.URL.Host)) + + h := r.URL.Host + h = strings.TrimPrefix(h, "[") + h = strings.TrimSuffix(h, "]") + hasBrackets := h != r.URL.Host + + p := net.ParseIP(h) + if p == nil || len(p) == 0 { + return defaultErr + } else if p.To4() != nil && hasBrackets { + return defaultErr + } + return nil +} + +func validHostName(r ResourceMapping) error { + invalidErr := errors.New(fmt.Sprintf("resource host needs a valid DNS name: %v", r.URL.Host)) + p := idna.New( + idna.ValidateLabels(true), + //this has to be off it disallows * for registration + //idna.ValidateForRegistration(), + idna.StrictDomainName(true)) + _, err := p.ToUnicode(r.URL.Host) + if err != nil { + return invalidErr + } + a, err := p.ToASCII(r.URL.Host) + if err != nil { + return invalidErr + } + if a != r.URL.Host { + return invalidErr + } + if strings.Contains(a, "*") { + return errors.New(fmt.Sprintf("resource host cannot be a wildcard DNS name: %v", r.URL.Host)) + } + if strings.Contains(a, ":") { + return errors.New(fmt.Sprintf("resource host cannot contain port: %v", r.URL.Host)) + } + if !govalidator.IsDNSName(a) { + return invalidErr + } + return nil +} + func (config Config) reApplyResourceNames() *Config { for name := range config.Resources { resourceMappings := config.Resources[name] @@ -259,6 +344,20 @@ func (config Config) compileRouteTransforms() *Config { return &config } +func (config Config) reformatResourceUrlSchemes() *Config { + for name := range config.Resources { + resourceMappings := config.Resources[name] + for i, resourceMapping := range resourceMappings { + scheme := resourceMapping.URL.Scheme + scheme = strings.TrimSpace(scheme) + scheme = strings.ToLower(scheme) + scheme = strings.TrimSuffix(scheme, "://") + resourceMappings[i].URL.Scheme = scheme + } + } + return &config +} + func (config Config) reApplyResourceURLDefaults() *Config { const http = "http" const https = "https" @@ -475,6 +574,7 @@ func (config Config) validateJwt() *Config { func (config Config) getDownstreamRoundTripTimeoutDuration() time.Duration { return time.Duration(time.Second * time.Duration(config.Connection.Downstream.RoundTripTimeoutSeconds)) } + func envToMap() map[string]string { envMap := make(map[string]string) diff --git a/config_test.go b/config_test.go index 9fb4a04..e9cc8d6 100644 --- a/config_test.go +++ b/config_test.go @@ -349,6 +349,264 @@ func TestValidateConfigNoHttpAndNoTLSFails(t *testing.T) { config = config.validateHTTPConfig() } +func TestValidateNonWildcardHostName(t *testing.T) { + var tests = []struct { + n string + h string + v bool + }{ + {"valid host", "host.com", true}, + {"invalid wildcard host", "*.host.com", false}, + {"invalid unicode host", "höst.com", false}, + {"invalid character host", "h/st.com", false}, + {"invalid port spec host", "host.com:80", false}, + } + + tests = append(tests, DNSNameHostTestFactory()...) + + for _, tt := range tests { + t.Run(tt.n, func(t *testing.T) { + rm := ResourceMapping{ + URL: URL{ + Scheme: "http://", + Host: tt.h, + Port: 80, + }} + got := validHostName(rm) == nil + want := tt.v + if got != want { + t.Errorf("%v got: %v want %v", tt.n, got, want) + } + }) + } +} + +func TestValidateIpv4AndIpv6(t *testing.T) { + var tests = []struct { + n string + h string + v bool + }{ + {"valid ipv4", "10.1.1.1", true}, + {"invalid ipv4 with ipv6 brackets", "[10.1.1.1]", false}, + {"invalid short ipv4", "10.0.0", false}, + {"invalid ipv4 with CIDR range", "10.0.0/1/24", false}, + {"invalid ipv4", "300.0.0.0", false}, + {"invalid ipv4 with port", "10.1.1.1:80", false}, + {"valid ipv6", "::1", true}, + {"valid ipv6", "[::1]", true}, + {"invalid port spec host", "host.com:80", false}, + } + for i, ipv4 := range ipv4s { + tests = append(tests, struct { + n string + h string + v bool + }{fmt.Sprintf("valid ipv4 %v", i), ipv4, true}) + } + for i, ipv6 := range ipv4s { + tests = append(tests, struct { + n string + h string + v bool + }{fmt.Sprintf("valid ipv6 %v", i), ipv6, true}) + } + + for _, tt := range tests { + t.Run(tt.n, func(t *testing.T) { + rm := ResourceMapping{ + URL: URL{ + Scheme: "http://", + Host: tt.h, + Port: 80, + }} + got := validIpAddress(rm) == nil + want := tt.v + if got != want { + t.Errorf("%v got: %v want %v", tt.n, got, want) + } + }) + } +} + +func TestResourceMappingValidUpstreamResource(t *testing.T) { + var tests = []struct { + n string + s string + h string + p uint16 + v bool + }{ + //ports + {"invalid port 0", "http", "host.com", 0, false}, + {"valid port", "http", "host.com", 80, true}, + {"invalid port 65535", "http", "host.com", 65535, true}, + + //schemes + {"valid scheme http", "Http", "host.com", 80, true}, + {"valid scheme http", "http", "host.com", 80, true}, + {"valid scheme http", "http://", "host.com", 80, true}, + {"valid scheme http", "http:// ", "host.com", 80, true}, + {"valid scheme https", "Https", "host.com", 443, true}, + {"valid scheme https", "https", "host.com", 443, true}, + {"valid scheme https", "https://", "host.com", 443, true}, + {"valid scheme https", "https:// ", "host.com", 443, true}, + {"valid scheme ws", "Ws", "host.com", 80, true}, + {"valid scheme ws", "ws", "host.com", 80, true}, + {"valid scheme ws", "ws://", "host.com", 80, true}, + {"valid scheme ws", "ws:// ", "host.com", 80, true}, + {"valid scheme wss", "WsS", "host.com", 80, true}, + {"valid scheme wss", "wss", "host.com", 80, true}, + {"valid scheme wss", "wsS://", "host.com", 80, true}, + {"valid scheme wss", "wsS:// ", "host.com", 80, true}, + + //bad schemes + {"invalid scheme blah", "blah:// ", "host.com", 80, false}, + {"invalid scheme gopher", "gopher:// ", "host.com", 80, false}, + {"invalid scheme ftp", "ftp:// ", "host.com", 80, false}, + + //hosts + {"valid host", "http", "host.com", 80, true}, + {"invalid wildcard host", "http", "*.host.com", 80, false}, + {"invalid unicode host", "http", "höst.com", 80, false}, + {"invalid character host", "http", "h/st.com", 80, false}, + {"invalid port spec host", "http", "host.com:80", 80, false}, + + //ips + {"valid ipv4", "http", "10.1.1.1", 80, true}, + {"invalid ipv4 with ipv6 brackets", "http", "[10.1.1.1]", 80, false}, + {"invalid short ipv4", "http", "10.0.0", 80, false}, + {"invalid ipv4 with CIDR range", "http", "10.0.0.1/24", 80, false}, + {"invalid ipv4", "http", "300.0.0.0", 80, false}, + {"invalid ipv4 with port", "http", "10.1.1.1:80", 80, false}, + {"valid ipv6", "http", "::1", 80, true}, + {"valid ipv6", "http", "[::1]", 80, true}, + {"invalid port spec host", "http", "host.com:80", 80, false}, + } + //more ipv4 + for i, ipv4 := range ipv4s { + tests = append(tests, struct { + n string + s string + h string + p uint16 + v bool + }{fmt.Sprintf("valid ipv4 %v", i), "http", ipv4, 80, true}) + } + //more ipv6 + for i, ipv6 := range ipv4s { + tests = append(tests, struct { + n string + s string + h string + p uint16 + v bool + }{fmt.Sprintf("valid ipv6 %v", i), "https", ipv6, 443, true}) + } + + for _, tt := range tests { + t.Run(tt.n, func(t *testing.T) { + //for those tests that are failing. + if !tt.v { + defer func() { + if err := recover(); err != nil { + t.Logf("normal. recovered from config panic as expected") + } + }() + } + + //validate resource mapping. for those that don't work, a config panic will fire that this test execution recovers. + rm := ResourceMapping{ + URL: URL{ + Scheme: tt.s, + Host: tt.h, + Port: tt.p, + }} + cfg := Config{Resources: map[string][]ResourceMapping{tt.n: []ResourceMapping{rm}}} + cfg.reApplyResourceNames(). + reformatResourceUrlSchemes(). + validateResources() + }) + } +} + +func TestReformatResourceUrlSchemes(t *testing.T) { + var tests = []struct { + n string + r string + a string + }{ + //schemes for reformatting + {"valid http", "httP:// ", "http"}, + {"valid http", "http ", "http"}, + {"valid https", "httPs:// ", "https"}, + {"valid https", "Https", "https"}, + {"valid ws", "ws:// ", "ws"}, + {"valid ws", "Ws ", "ws"}, + {"valid wss", "wss ", "wss"}, + } + + for _, tt := range tests { + t.Run(tt.n, func(t *testing.T) { + //validate resource mapping. for those that don't work, a config panic will fire that this test execution recovers. + rm := ResourceMapping{ + URL: URL{ + Scheme: tt.r, + Host: "host.com", + Port: 80, + }} + cfg := Config{Resources: map[string][]ResourceMapping{tt.n: []ResourceMapping{rm}}} + cfg.reApplyResourceNames(). + reformatResourceUrlSchemes() + + got := cfg.Resources[tt.n][0].URL.Scheme + want := tt.a + if got != want { + t.Errorf("test %v got %v want %v", tt.n, got, want) + } + }) + } +} + +func TestScheme(t *testing.T) { + var tests = []struct { + n string + s string + v bool + }{ + //hosts + {"valid http", "http", true}, + {"valid http", "http://", true}, + {"valid http", "HTTP", true}, + {"valid http", "HTTP://", true}, + {"valid https", "https", true}, + {"valid https", "https://", true}, + {"valid https", "HTTPS", true}, + {"valid https", "HTTPS://", true}, + {"valid https", "HtTPs://", true}, + {"valid https", "HtTPs:// ", true}, + {"valid ws", "ws", true}, + {"valid ws", "ws://", true}, + {"valid ws", "wS", true}, + {"valid wss", "wss://", true}, + {"valid wss", "wsS://", true}, + {"invalid gopher", "gopher://", false}, + {"invalid ftp", "ftp://", false}, + {"invalid blah", "blah://", false}, + {"invalid blah", "blah", false}, + } + + for _, tt := range tests { + t.Run(tt.n, func(t *testing.T) { + got := validScheme(tt.s) + want := tt.v + if got != want { + t.Errorf("test %v got %v want %v", tt.n, got, want) + } + }) + } +} + // TestValidateAcmeEmail func TestValidateAcmeEmail(t *testing.T) { config := &Config{ diff --git a/integration/j8a6.yml b/integration/j8a6.yml index 05abee9..51f6a61 100644 --- a/integration/j8a6.yml +++ b/integration/j8a6.yml @@ -1,4 +1,5 @@ --- +logLevel: TRACE connection: downstream: readTimeoutSeconds: 30 diff --git a/route_test.go b/route_test.go index ae469ed..ba9cc4d 100644 --- a/route_test.go +++ b/route_test.go @@ -337,7 +337,7 @@ func TestRoutePathsValid(t *testing.T) { } func TestHostDNSNamePatternValid(t *testing.T) { - for _, tt := range HostTestFactory() { + for _, tt := range WildCardIdnaHostTestFactory() { t.Run(tt.n, func(t *testing.T) { r := Route{Host: tt.h} v, e := r.validHostPattern() @@ -350,7 +350,7 @@ func TestHostDNSNamePatternValid(t *testing.T) { } -func HostTestFactory() []struct { +func WildCardIdnaHostTestFactory() []struct { n string h string v bool @@ -360,19 +360,38 @@ func HostTestFactory() []struct { h string v bool }{ - {n: "valid host", h: "blah", v: true}, - {n: "valid host unicode", h: "六书", v: true}, + //wildcard valid and syntax invalid {n: "valid wildcard dns umlaut", h: "*.faß.com", v: true}, + {n: "valid wildcard dns other unicode", h: "*.😀😀😀.com", v: true}, + {n: "valid wildcard dns with punycode", h: "*.xn--bbb-yi33baa.com", v: true}, {n: "invalid wildcard pattern with double dot", h: "*..faß.com", v: false}, {n: "invalid wildcard pattern with asterisk inside pattern", h: "a.*.faß.com", v: false}, {n: "invalid wildcard pattern with valid regex style asterisk", h: "a*.faß.com", v: false}, {n: "invalid wildcard pattern with invalid regex style asterisk", h: "*a.faß.com", v: false}, - {n: "valid wildcard dns other unicode", h: "*.😀😀😀.com", v: true}, {n: "invalid asterisk in the middle of domain", h: "a.*.😀😀😀.com", v: false}, - {n: "valid punycode", h: "xn--bbb-yi33baa.com", v: true}, - {n: "valid wildcard dns with punycode", h: "*.xn--bbb-yi33baa.com", v: true}, + + //IDNA {n: "valid cyrillic", h: "ԛәлп.com", v: true}, + {n: "valid host unicode", h: "六书", v: true}, {n: "invalid latin with stroke, case mapping is not part of IDNA 2008. We pass this anyway because go does", h: "Ⱥbby.com", v: true}, + } + tests = append(tests, DNSNameHostTestFactory()...) + + return tests +} + +func DNSNameHostTestFactory() []struct { + n string + h string + v bool +} { + tests := []struct { + n string + h string + v bool + }{ + {n: "valid host", h: "blah", v: true}, + {n: "valid punycode", h: "xn--bbb-yi33baa.com", v: true}, {n: "DNS name can start with number (RFC-1123)", h: "1aaa.com", v: true}, {n: "invalid ascii dollar sign as part of DNS name", h: "$1.a.com", v: false}, {n: "invalid contains illegal ascii exclamation mark !", h: "!1.abc.com", v: false}, @@ -394,11 +413,12 @@ func HostTestFactory() []struct { {n: "invalid contains illegal ascii <", h: "<1.abc.com", v: false}, {n: "invalid contains illegal ascii >", h: ">1.abc.com", v: false}, } + return tests } func TestHostDNSNamePatternCompiles(t *testing.T) { - for _, tt := range HostTestFactory() { + for _, tt := range WildCardIdnaHostTestFactory() { t.Run(tt.n, func(t *testing.T) { r := Route{Host: tt.h} e := r.compileHostPattern() diff --git a/server.go b/server.go index 54b236e..ef048bc 100644 --- a/server.go +++ b/server.go @@ -199,6 +199,7 @@ func processConfig() *Config { load(). validateTimeZone(). validateLogLevel(). + reformatResourceUrlSchemes(). reApplyResourceURLDefaults(). validateResources(). reApplyResourceNames().