Repository: labstack/echo
Pull request #1878 (2021-05-20 14:54:12)

Fix: split XFF header only by comma
When splitting `X-Forwarded-For` header by `', '` (comma and space) I got an issue when valid value `127.0.0.1,127.0.1.1` can't be parsed correctly. I'm suggesting splitting just by `,` (comma) and trim a result. 


Patch:
diff --git a/bind.go b/bind.go
index dfdf82d..47947ce 100644
--- a/bind.go
+++ b/bind.go
@@ -97,6 +97,14 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
 	return nil
 }
 
+// BindHeaders binds HTTP headers to a bindable object
+func (b *DefaultBinder) BindHeaders(c Context, i interface{}) error {
+	if err := b.bindData(i, c.Request().Header, "header"); err != nil {
+		return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
+	}
+	return nil
+}
+
 // Bind implements the `Binder#Bind` function.
 // Binding is done in following order: 1) path params; 2) query params; 3) request body. Each step COULD override previous
 // step binded values. For single source binding use their own methods BindBody, BindQueryParams, BindPathParams.
@@ -134,7 +142,7 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
 
 	// !struct
 	if typ.Kind() != reflect.Struct {
-		if tag == "param" || tag == "query" {
+		if tag == "param" || tag == "query" || tag == "header" {
 			// incompatible type, data is probably to be found in the body
 			return nil
 		}
diff --git a/context.go b/context.go
index 0cee48c..fad8bf7 100644
--- a/context.go
+++ b/context.go
@@ -276,9 +276,9 @@ func (c *context) RealIP() string {
 	}
 	// Fall back to legacy behavior
 	if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" {
-		i := strings.IndexAny(ip, ", ")
+		i := strings.IndexAny(ip, ",")
 		if i > 0 {
-			return ip[:i]
+			return strings.TrimSpace(ip[:i])
 		}
 		return ip
 	}
diff --git a/middleware/jwt.go b/middleware/jwt.go
index cd35b62..6c8bceb 100644
--- a/middleware/jwt.go
+++ b/middleware/jwt.go
@@ -53,7 +53,7 @@ type (
 		// Optional. Default value jwt.MapClaims
 		Claims jwt.Claims
 
-		// TokenLookup is a string in the form of "<source>:<name>" that is used
+		// TokenLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used
 		// to extract token from the request.
 		// Optional. Default value "header:Authorization".
 		// Possible values:
@@ -62,6 +62,9 @@ type (
 		// - "param:<name>"
 		// - "cookie:<name>"
 		// - "form:<name>"
+		// Multiply sources example:
+		// - "header: Authorization,cookie: myowncookie"
+
 		TokenLookup string
 
 		// AuthScheme to be used in the Authorization header.
diff --git a/middleware/rate_limiter.go b/middleware/rate_limiter.go
index 46a310d..0291eb4 100644
--- a/middleware/rate_limiter.go
+++ b/middleware/rate_limiter.go
@@ -169,7 +169,8 @@ type (
 
 /*
 NewRateLimiterMemoryStore returns an instance of RateLimiterMemoryStore with
-the provided rate (as req/s). Burst and ExpiresIn will be set to default values.
+the provided rate (as req/s). The provided rate less than 1 will be treated as zero.
+Burst and ExpiresIn will be set to default values.
 
 Example (with 20 requests/sec):
 


Test patch:
diff --git a/bind_test.go b/bind_test.go
index ff03370..4ed8dbb 100644
--- a/bind_test.go
+++ b/bind_test.go
@@ -269,6 +269,37 @@ func TestBindQueryParamsCaseSensitivePrioritized(t *testing.T) {
 	}
 }
 
+func TestBindHeaderParam(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	req.Header.Set("Name", "Jon Doe")
+	req.Header.Set("Id", "2")
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+	u := new(user)
+	err := (&DefaultBinder{}).BindHeaders(c, u)
+	if assert.NoError(t, err) {
+		assert.Equal(t, 2, u.ID)
+		assert.Equal(t, "Jon Doe", u.Name)
+	}
+}
+
+func TestBindHeaderParamBadType(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	req.Header.Set("Id", "salamander")
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+	u := new(user)
+	err := (&DefaultBinder{}).BindHeaders(c, u)
+	assert.Error(t, err)
+
+	httpErr, ok := err.(*HTTPError)
+	if assert.True(t, ok) {
+		assert.Equal(t, http.StatusBadRequest, httpErr.Code)
+	}
+}
+
 func TestBindUnmarshalParam(t *testing.T) {
 	e := New()
 	req := httptest.NewRequest(http.MethodGet, "/?ts=2016-12-06T19:09:05Z&sa=one,two,three&ta=2016-12-06T19:09:05Z&ta=2016-12-06T19:09:05Z&ST=baz", nil)
diff --git a/context_test.go b/context_test.go
index 2c61ffb..a8b9a99 100644
--- a/context_test.go
+++ b/context_test.go
@@ -888,6 +888,14 @@ func TestContext_RealIP(t *testing.T) {
 			},
 			"127.0.0.1",
 		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXForwardedFor: []string{"127.0.0.1,127.0.1.1"}},
+				},
+			},
+			"127.0.0.1",
+		},
 		{
 			&context{
 				request: &http.Request{
diff --git a/echo_test.go b/echo_test.go
index ba49883..e5bd371 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -24,8 +24,8 @@ import (
 
 type (
 	user struct {
-		ID   int    `json:"id" xml:"id" form:"id" query:"id" param:"id"`
-		Name string `json:"name" xml:"name" form:"name" query:"name" param:"name"`
+		ID   int    `json:"id" xml:"id" form:"id" query:"id" param:"id" header:"id"`
+		Name string `json:"name" xml:"name" form:"name" query:"name" param:"name" header:"name"`
 	}
 )