-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add Forwarded header parsing and real IP extraction with tests #2744
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,6 +40,8 @@ type Context interface { | |
// Scheme returns the HTTP protocol scheme, `http` or `https`. | ||
Scheme() string | ||
|
||
SchemeForwarded() *Forwarded | ||
|
||
// RealIP returns the client's network address based on `X-Forwarded-For` | ||
// or `X-Real-IP` request header. | ||
// The behavior can be configured using `Echo#IPExtractor`. | ||
|
@@ -234,6 +236,14 @@ const ( | |
ContextKeyHeaderAllow = "echo_header_allow" | ||
) | ||
|
||
// Forwarded represents the structured format of the Forwarded HTTP header. | ||
type Forwarded struct { | ||
By []string | ||
For []string | ||
Host []string | ||
Proto []string | ||
} | ||
|
||
const ( | ||
defaultMemory = 32 << 20 // 32 MB | ||
indexPage = "index.html" | ||
|
@@ -293,24 +303,85 @@ func (c *context) Scheme() string { | |
return "http" | ||
} | ||
|
||
func (c *context) SchemeForwarded() *Forwarded { | ||
// Parse and get "Forwarded" header. | ||
// See : https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded | ||
if scheme := c.request.Header.Get(HeaderForwarded); scheme != "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only parses single Header. From mozilla article I understand that you can have actually multiple RFC says https://datatracker.ietf.org/doc/html/rfc7239
Mozilla rephrases this as
There is Nginx issue https://trac.nginx.org/nginx/ticket/1316 that has example for multiple headers in request |
||
f, err := c.parseForwarded(scheme) | ||
if err != nil { | ||
return nil | ||
} | ||
return &f | ||
} | ||
return nil | ||
} | ||
|
||
func (c *context) parseForwarded(input string) (Forwarded, error) { | ||
forwarded := Forwarded{} | ||
entries := strings.Split(input, ",") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be honest - I do not understand when is semicolon used and when is colon used. Can semicolon and comma be mixed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Additionally - what about quoted strings that contain comma or semicolon? These 2 are defined as
I think rfc7230 section-3.2.6 allows unescaped commas in quoted field value |
||
|
||
for _, entry := range entries { | ||
entry = strings.TrimSpace(entry) | ||
pairs := strings.Split(entry, ";") | ||
|
||
for _, pair := range pairs { | ||
parts := strings.SplitN(pair, "=", 2) | ||
if len(parts) != 2 { | ||
return forwarded, fmt.Errorf("invalid pair: %s", pair) | ||
} | ||
|
||
key := strings.TrimSpace(parts[0]) | ||
value, err := url.QueryUnescape(strings.TrimSpace(parts[1])) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is RFC says that
|
||
if err != nil { | ||
return forwarded, fmt.Errorf("failed to unescape value: %w", err) | ||
} | ||
value = strings.Trim(value, "\"[]") | ||
switch key { | ||
case "by": | ||
forwarded.By = append(forwarded.By, value) | ||
case "for": | ||
forwarded.For = append(forwarded.For, value) | ||
case "host": | ||
forwarded.Host = append(forwarded.Host, value) | ||
case "proto": | ||
forwarded.Proto = append(forwarded.Proto, value) | ||
default: | ||
return forwarded, fmt.Errorf("unknown key: %s", key) | ||
} | ||
} | ||
} | ||
|
||
return forwarded, nil | ||
} | ||
|
||
func (c *context) RealIP() string { | ||
if c.echo != nil && c.echo.IPExtractor != nil { | ||
return c.echo.IPExtractor(c.request) | ||
} | ||
// Check if the "Forwarded" header is present in the request. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After seeing how complex or how less understood |
||
if d := c.request.Header.Get(HeaderForwarded); d != "" { | ||
// Parse the "Forwarded" header. | ||
scheme, err := c.parseForwarded(d) | ||
if err != nil { | ||
return "" // Return an empty string if parsing fails. | ||
} | ||
if len(scheme.For) > 0 { | ||
return scheme.For[0] // Return first for item | ||
} | ||
return "" | ||
} | ||
// Fall back to legacy behavior | ||
if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" { | ||
i := strings.IndexAny(ip, ",") | ||
if i > 0 { | ||
xffip := strings.TrimSpace(ip[:i]) | ||
xffip = strings.TrimPrefix(xffip, "[") | ||
xffip = strings.TrimSuffix(xffip, "]") | ||
xffip = strings.Trim(xffip, "\"[]") | ||
return xffip | ||
} | ||
return ip | ||
} | ||
if ip := c.request.Header.Get(HeaderXRealIP); ip != "" { | ||
ip = strings.TrimPrefix(ip, "[") | ||
ip = strings.TrimSuffix(ip, "]") | ||
ip = strings.Trim(ip, "\"[]") | ||
return ip | ||
} | ||
ra, _, _ := net.SplitHostPort(c.request.RemoteAddr) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding new
SchemeForwarded
method toContext
interface is a backwards incompatible change. It has to be some utility function or specificIPExtractor
implementation