Repository: labstack/echo
Pull request #1301 (2019-03-04 14:59:58)

Set subdomains to AllowOrigins with wildcard
close: #1300 

codecov[bot]: # [Codecov](https://codecov.io/gh/labstack/echo/pull/1301?src=pr&el=h1) Report
> Merging [#1301](https://codecov.io/gh/labstack/echo/pull/1301?src=pr&el=desc) into [master](https://codecov.io/gh/labstack/echo/commit/775b2eefec49e0a4d224b3f38f2c5640b509c3cf?src=pr&el=desc) will **increase** coverage by `0.03%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/labstack/echo/pull/1301/graphs/tree.svg?width=650&token=vqnT76l79i&height=150&src=pr)](https://codecov.io/gh/labstack/echo/pull/1301?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1301      +/-   ##
==========================================
+ Coverage   84.21%   84.24%   +0.03%     
==========================================
  Files          26       26              
  Lines        1951     1955       +4     
==========================================
+ Hits         1643     1647       +4     
  Misses        202      202              
  Partials      106      106
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/1301?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [middleware/cors.go](https://codecov.io/gh/labstack/echo/pull/1301/diff?src=pr&el=tree#diff-bWlkZGxld2FyZS9jb3JzLmdv) | `76.78% <100%> (+1.78%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/labstack/echo/pull/1301?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/labstack/echo/pull/1301?src=pr&el=footer). Last update [775b2ee...751ecd7](https://codecov.io/gh/labstack/echo/pull/1301?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/echo.go b/echo.go
index 409dfad..bd4ac2f 100644
--- a/echo.go
+++ b/echo.go
@@ -575,21 +575,15 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	if e.premiddleware == nil {
 		e.router.Find(r.Method, getPath(r), c)
 		h = c.Handler()
-		for i := len(e.middleware) - 1; i >= 0; i-- {
-			h = e.middleware[i](h)
-		}
+		h = applyMiddleware(h, e.middleware...)
 	} else {
 		h = func(c Context) error {
 			e.router.Find(r.Method, getPath(r), c)
 			h := c.Handler()
-			for i := len(e.middleware) - 1; i >= 0; i-- {
-				h = e.middleware[i](h)
-			}
+			h = applyMiddleware(h, e.middleware...)
 			return h(c)
 		}
-		for i := len(e.premiddleware) - 1; i >= 0; i-- {
-			h = e.premiddleware[i](h)
-		}
+		h = applyMiddleware(h, e.premiddleware...)
 	}
 
 	// Execute chain
@@ -804,3 +798,10 @@ func newListener(address string) (*tcpKeepAliveListener, error) {
 	}
 	return &tcpKeepAliveListener{l.(*net.TCPListener)}, nil
 }
+
+func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc {
+	for i := len(middleware) - 1; i >= 0; i-- {
+		h = middleware[i](h)
+	}
+	return h
+}
diff --git a/middleware/cors.go b/middleware/cors.go
index c61c712..5dfe31f 100644
--- a/middleware/cors.go
+++ b/middleware/cors.go
@@ -102,6 +102,10 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
 					allowOrigin = o
 					break
 				}
+				if matchSubdomain(origin, o) {
+					allowOrigin = origin
+					break
+				}
 			}
 
 			// Simple request
diff --git a/middleware/secure.go b/middleware/secure.go
index eded54f..77a1487 100644
--- a/middleware/secure.go
+++ b/middleware/secure.go
@@ -60,6 +60,12 @@ type (
 		// have occurred instead of blocking the resource.
 		// Optional. Default value false.
 		CSPReportOnly bool `yaml:"csp_report_only"`
+
+		// HSTSPreloadEnabled will add the preload tag in the `Strict Transport Security`
+		// header, which enables the domain to be included in the HSTS preload list
+		// maintained by Chrome (and used by Firefox and Safari): https://hstspreload.org/
+		// Optional.  Default value false.
+		HSTSPreloadEnabled bool `yaml:"hsts_preload_enabled"`
 	}
 )
 
@@ -70,6 +76,7 @@ var (
 		XSSProtection:      "1; mode=block",
 		ContentTypeNosniff: "nosniff",
 		XFrameOptions:      "SAMEORIGIN",
+		HSTSPreloadEnabled: false,
 	}
 )
 
@@ -112,6 +119,9 @@ func SecureWithConfig(config SecureConfig) echo.MiddlewareFunc {
 				if !config.HSTSExcludeSubdomains {
 					subdomains = "; includeSubdomains"
 				}
+				if config.HSTSPreloadEnabled {
+					subdomains = fmt.Sprintf("%s; preload", subdomains)
+				}
 				res.Header().Set(echo.HeaderStrictTransportSecurity, fmt.Sprintf("max-age=%d%s", config.HSTSMaxAge, subdomains))
 			}
 			if config.ContentSecurityPolicy != "" {
diff --git a/middleware/util.go b/middleware/util.go
new file mode 100644
index 0000000..ab951a0
--- /dev/null
+++ b/middleware/util.go
@@ -0,0 +1,54 @@
+package middleware
+
+import (
+	"strings"
+)
+
+func matchScheme(domain, pattern string) bool {
+	didx := strings.Index(domain, ":")
+	pidx := strings.Index(pattern, ":")
+	return didx != -1 && pidx != -1 && domain[:didx] == pattern[:pidx]
+}
+
+// matchSubdomain compares authority with wildcard
+func matchSubdomain(domain, pattern string) bool {
+	if !matchScheme(domain, pattern) {
+		return false
+	}
+	didx := strings.Index(domain, "://")
+	pidx := strings.Index(pattern, "://")
+	if didx == -1 || pidx == -1 {
+		return false
+	}
+	domAuth := domain[didx+3:]
+	// to avoid long loop by invalid long domain
+	if len(domAuth) > 253 {
+		return false
+	}
+	patAuth := pattern[pidx+3:]
+
+	domComp := strings.Split(domAuth, ".")
+	patComp := strings.Split(patAuth, ".")
+	for i := len(domComp)/2 - 1; i >= 0; i-- {
+		opp := len(domComp) - 1 - i
+		domComp[i], domComp[opp] = domComp[opp], domComp[i]
+	}
+	for i := len(patComp)/2 - 1; i >= 0; i-- {
+		opp := len(patComp) - 1 - i
+		patComp[i], patComp[opp] = patComp[opp], patComp[i]
+	}
+
+	for i, v := range domComp {
+		if len(patComp) <= i {
+			return false
+		}
+		p := patComp[i]
+		if p == "*" {
+			return true
+		}
+		if p != v {
+			return false
+		}
+	}
+	return false
+}


Test patch:
diff --git a/middleware/cors_test.go b/middleware/cors_test.go
index 7a71e73..acfdf47 100644
--- a/middleware/cors_test.go
+++ b/middleware/cors_test.go
@@ -66,4 +66,20 @@ func TestCORS(t *testing.T) {
 	assert.NotEmpty(t, rec.Header().Get(echo.HeaderAccessControlAllowMethods))
 	assert.Equal(t, "true", rec.Header().Get(echo.HeaderAccessControlAllowCredentials))
 	assert.Equal(t, "3600", rec.Header().Get(echo.HeaderAccessControlMaxAge))
+
+	// Preflight request with `AllowOrigins` which allow all subdomains with *
+	req = httptest.NewRequest(http.MethodOptions, "/", nil)
+	rec = httptest.NewRecorder()
+	c = e.NewContext(req, rec)
+	req.Header.Set(echo.HeaderOrigin, "http://aaa.example.com")
+	cors = CORSWithConfig(CORSConfig{
+		AllowOrigins:     []string{"http://*.example.com"},
+	})
+	h = cors(echo.NotFoundHandler)
+	h(c)
+	assert.Equal(t, "http://aaa.example.com", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
+
+	req.Header.Set(echo.HeaderOrigin, "http://bbb.example.com")
+	h(c)
+	assert.Equal(t, "http://bbb.example.com", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
 }
diff --git a/middleware/secure_test.go b/middleware/secure_test.go
index 47121fb..96245fe 100644
--- a/middleware/secure_test.go
+++ b/middleware/secure_test.go
@@ -62,4 +62,25 @@ func TestSecure(t *testing.T) {
 	assert.Equal(t, "max-age=3600; includeSubdomains", rec.Header().Get(echo.HeaderStrictTransportSecurity))
 	assert.Equal(t, "default-src 'self'", rec.Header().Get(echo.HeaderContentSecurityPolicyReportOnly))
 	assert.Equal(t, "", rec.Header().Get(echo.HeaderContentSecurityPolicy))
+
+	// Custom, with preload option enabled
+	req.Header.Set(echo.HeaderXForwardedProto, "https")
+	rec = httptest.NewRecorder()
+	c = e.NewContext(req, rec)
+	SecureWithConfig(SecureConfig{
+		HSTSMaxAge:         3600,
+		HSTSPreloadEnabled: true,
+	})(h)(c)
+	assert.Equal(t, "max-age=3600; includeSubdomains; preload", rec.Header().Get(echo.HeaderStrictTransportSecurity))
+
+	// Custom, with preload option enabled and subdomains excluded
+	req.Header.Set(echo.HeaderXForwardedProto, "https")
+	rec = httptest.NewRecorder()
+	c = e.NewContext(req, rec)
+	SecureWithConfig(SecureConfig{
+		HSTSMaxAge:            3600,
+		HSTSPreloadEnabled:    true,
+		HSTSExcludeSubdomains: true,
+	})(h)(c)
+	assert.Equal(t, "max-age=3600; preload", rec.Header().Get(echo.HeaderStrictTransportSecurity))
 }
diff --git a/middleware/util_test.go b/middleware/util_test.go
new file mode 100644
index 0000000..df1d262
--- /dev/null
+++ b/middleware/util_test.go
@@ -0,0 +1,95 @@
+package middleware
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_matchScheme(t *testing.T) {
+	tests := []struct {
+		domain, pattern string
+		expected        bool
+	}{
+		{
+			domain:   "http://example.com",
+			pattern:  "http://example.com",
+			expected: true,
+		},
+		{
+			domain:   "https://example.com",
+			pattern:  "https://example.com",
+			expected: true,
+		},
+		{
+			domain:   "http://example.com",
+			pattern:  "https://example.com",
+			expected: false,
+		},
+		{
+			domain:   "https://example.com",
+			pattern:  "http://example.com",
+			expected: false,
+		},
+	}
+
+	for _, v := range tests {
+		assert.Equal(t, v.expected, matchScheme(v.domain, v.pattern))
+	}
+}
+
+func Test_matchSubdomain(t *testing.T) {
+	tests := []struct {
+		domain, pattern string
+		expected        bool
+	}{
+		{
+			domain:   "http://aaa.example.com",
+			pattern:  "http://*.example.com",
+			expected: true,
+		},
+		{
+			domain:   "http://bbb.aaa.example.com",
+			pattern:  "http://*.example.com",
+			expected: true,
+		},
+		{
+			domain:   "http://bbb.aaa.example.com",
+			pattern:  "http://*.aaa.example.com",
+			expected: true,
+		},
+		{
+			domain:   "http://aaa.example.com:8080",
+			pattern:  "http://*.example.com:8080",
+			expected: true,
+		},
+
+		{
+			domain:   "http://fuga.hoge.com",
+			pattern:  "http://*.example.com",
+			expected: false,
+		},
+		{
+			domain:   "http://ccc.bbb.example.com",
+			pattern:  "http://*.aaa.example.com",
+			expected: false,
+		},
+		{
+			domain: `http://1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\
+      .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\
+      .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\
+      .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.example.com`,
+			pattern:  "http://*.example.com",
+			expected: false,
+		},
+		{
+			domain:   "http://ccc.bbb.example.com",
+			pattern:  "http://example.com",
+			expected: false,
+		},
+	}
+
+	for _, v := range tests {
+		assert.Equal(t, v.expected, matchSubdomain(v.domain, v.pattern))
+	}
+}