Skip to content

Commit 5b26a52

Browse files
committed
Allow header support in Router, MethodNotFoundHandler (405) and CORS middleware
1 parent 4fffee2 commit 5b26a52

File tree

7 files changed

+441
-119
lines changed

7 files changed

+441
-119
lines changed

context.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,13 @@ type (
210210
}
211211
)
212212

213+
const (
214+
// ContextKeyHeaderAllow is set by Router for getting value for `Allow` header in later stages of handler call chain.
215+
// Allow header is mandatory for status 405 (method not found) and useful for OPTIONS method requests.
216+
// It is added to context only when Router does not find matching method handler for request.
217+
ContextKeyHeaderAllow = "____echo____header_allow"
218+
)
219+
213220
const (
214221
defaultMemory = 32 << 20 // 32 MB
215222
indexPage = "index.html"

echo.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,11 @@ const (
190190

191191
// Headers
192192
const (
193-
HeaderAccept = "Accept"
194-
HeaderAcceptEncoding = "Accept-Encoding"
193+
HeaderAccept = "Accept"
194+
HeaderAcceptEncoding = "Accept-Encoding"
195+
// HeaderAllow is header field that lists the set of methods advertised as supported by the target resource.
196+
// Allow header is mandatory for status 405 (method not found) and useful OPTIONS method responses.
197+
// See: https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1
195198
HeaderAllow = "Allow"
196199
HeaderAuthorization = "Authorization"
197200
HeaderContentDisposition = "Content-Disposition"
@@ -302,6 +305,13 @@ var (
302305
}
303306

304307
MethodNotAllowedHandler = func(c Context) error {
308+
// 'Allow' header RFC: https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1
309+
// >> An origin server MUST generate an Allow field in a 405 (Method Not Allowed) response
310+
// and MAY do so in any other response.
311+
routerAllowMethods, ok := c.Get(ContextKeyHeaderAllow).(string)
312+
if ok && routerAllowMethods != "" {
313+
c.Response().Header().Set(HeaderAllow, routerAllowMethods)
314+
}
305315
return ErrMethodNotAllowed
306316
}
307317
)

echo_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,13 +716,16 @@ func TestEchoNotFound(t *testing.T) {
716716

717717
func TestEchoMethodNotAllowed(t *testing.T) {
718718
e := New()
719+
719720
e.GET("/", func(c Context) error {
720721
return c.String(http.StatusOK, "Echo!")
721722
})
722723
req := httptest.NewRequest(http.MethodPost, "/", nil)
723724
rec := httptest.NewRecorder()
724725
e.ServeHTTP(rec, req)
726+
725727
assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
728+
assert.Equal(t, "OPTIONS, GET", rec.Header().Get(HeaderAllow))
726729
}
727730

728731
func TestEchoContext(t *testing.T) {

middleware/cors.go

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ type (
2929
// AllowMethods defines a list methods allowed when accessing the resource.
3030
// This is used in response to a preflight request.
3131
// Optional. Default value DefaultCORSConfig.AllowMethods.
32+
// If `allowMethods` is left empty will fill for preflight request `Access-Control-Allow-Methods` header value
33+
// from `Allow` header that echo.Router set into context.
3234
AllowMethods []string `yaml:"allow_methods"`
3335

3436
// AllowHeaders defines a list of request headers that can be used when
@@ -41,6 +43,8 @@ type (
4143
// a response to a preflight request, this indicates whether or not the
4244
// actual request can be made using credentials.
4345
// Optional. Default value false.
46+
// Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`.
47+
// See http://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
4448
AllowCredentials bool `yaml:"allow_credentials"`
4549

4650
// ExposeHeaders defines a whitelist headers that clients are allowed to
@@ -80,7 +84,9 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
8084
if len(config.AllowOrigins) == 0 {
8185
config.AllowOrigins = DefaultCORSConfig.AllowOrigins
8286
}
87+
hasCustomAllowMethods := true
8388
if len(config.AllowMethods) == 0 {
89+
hasCustomAllowMethods = false
8490
config.AllowMethods = DefaultCORSConfig.AllowMethods
8591
}
8692

@@ -109,10 +115,28 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
109115
origin := req.Header.Get(echo.HeaderOrigin)
110116
allowOrigin := ""
111117

112-
preflight := req.Method == http.MethodOptions
113118
res.Header().Add(echo.HeaderVary, echo.HeaderOrigin)
114119

115-
// No Origin provided
120+
// Preflight request is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method,
121+
// Access-Control-Request-Headers, and the Origin header. See: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
122+
// For simplicity we just consider method type and later `Origin` header.
123+
preflight := req.Method == http.MethodOptions
124+
125+
// Although router adds special handler in case of OPTIONS method we avoid calling next for OPTIONS in this middleware
126+
// as CORS requests do not have cookies / authentication headers by default, so we could get stuck in auth
127+
// middlewares by calling next(c).
128+
// But we still want to send `Allow` header as response in case of Non-CORS OPTIONS request as router default
129+
// handler does.
130+
routerAllowMethods := ""
131+
if preflight {
132+
tmpAllowMethods, ok := c.Get(echo.ContextKeyHeaderAllow).(string)
133+
if ok && tmpAllowMethods != "" {
134+
routerAllowMethods = tmpAllowMethods
135+
c.Response().Header().Set(echo.HeaderAllow, routerAllowMethods)
136+
}
137+
}
138+
139+
// No Origin provided. This is (probably) not request from actual browser - proceed executing middleware chain
116140
if origin == "" {
117141
if !preflight {
118142
return next(c)
@@ -145,19 +169,15 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
145169
}
146170
}
147171

148-
// Check allowed origin patterns
149-
for _, re := range allowOriginPatterns {
150-
if allowOrigin == "" {
151-
didx := strings.Index(origin, "://")
152-
if didx == -1 {
153-
continue
154-
}
155-
domAuth := origin[didx+3:]
156-
// to avoid regex cost by invalid long domain
157-
if len(domAuth) > 253 {
158-
break
159-
}
160-
172+
checkPatterns := false
173+
if allowOrigin == "" {
174+
// to avoid regex cost by invalid (long) domains (253 is domain name max limit)
175+
if len(origin) <= (253+3+4) && strings.Contains(origin, "://") {
176+
checkPatterns = true
177+
}
178+
}
179+
if checkPatterns {
180+
for _, re := range allowOriginPatterns {
161181
if match, _ := regexp.MatchString(re, origin); match {
162182
allowOrigin = origin
163183
break
@@ -174,12 +194,13 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
174194
return c.NoContent(http.StatusNoContent)
175195
}
176196

197+
res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin)
198+
if config.AllowCredentials {
199+
res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
200+
}
201+
177202
// Simple request
178203
if !preflight {
179-
res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin)
180-
if config.AllowCredentials {
181-
res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
182-
}
183204
if exposeHeaders != "" {
184205
res.Header().Set(echo.HeaderAccessControlExposeHeaders, exposeHeaders)
185206
}
@@ -189,11 +210,13 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
189210
// Preflight request
190211
res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestMethod)
191212
res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestHeaders)
192-
res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin)
193-
res.Header().Set(echo.HeaderAccessControlAllowMethods, allowMethods)
194-
if config.AllowCredentials {
195-
res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
213+
214+
if !hasCustomAllowMethods && routerAllowMethods != "" {
215+
res.Header().Set(echo.HeaderAccessControlAllowMethods, routerAllowMethods)
216+
} else {
217+
res.Header().Set(echo.HeaderAccessControlAllowMethods, allowMethods)
196218
}
219+
197220
if allowHeaders != "" {
198221
res.Header().Set(echo.HeaderAccessControlAllowHeaders, allowHeaders)
199222
} else {

0 commit comments

Comments
 (0)