Add Configuration.RemoteAddrHeadersForce as requested at #1567 and change RemoteAddrHeaders from map to string slice

Read HISTORY.md entry
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-07-26 14:37:30 +03:00
parent a50f0ed5ba
commit 22a89c12cb
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
7 changed files with 101 additions and 84 deletions

View File

@ -375,6 +375,8 @@ Other Improvements:
![DBUG routes](https://iris-go.com/images/v12.2.0-dbug2.png?v=0) ![DBUG routes](https://iris-go.com/images/v12.2.0-dbug2.png?v=0)
- Add `Configuration.RemoteAddrHeadersForce bool` to force `Context.RemoteAddr() string` to return the first entry of request headers as a fallback instead of the `Request.RemoteAddr` one, as requested at: [1567#issuecomment-663972620](https://github.com/kataras/iris/issues/1567#issuecomment-663972620).
- Fix [#1569#issuecomment-663739177](https://github.com/kataras/iris/issues/1569#issuecomment-663739177). - Fix [#1569#issuecomment-663739177](https://github.com/kataras/iris/issues/1569#issuecomment-663739177).
- Fix [#1564](https://github.com/kataras/iris/issues/1564). - Fix [#1564](https://github.com/kataras/iris/issues/1564).
@ -516,6 +518,7 @@ New Context Methods:
Breaking Changes: Breaking Changes:
- `Configuration.RemoteAddrHeaders` from `map[string]bool` to `[]string`. If you used `With(out)RemoteAddrHeader` then you are ready to proceed without any code changes for that one.
- `ctx.Gzip(boolean)` replaced with `ctx.CompressWriter(boolean) error`. - `ctx.Gzip(boolean)` replaced with `ctx.CompressWriter(boolean) error`.
- `ctx.GzipReader(boolean) error` replaced with `ctx.CompressReader(boolean) error`. - `ctx.GzipReader(boolean) error` replaced with `ctx.CompressReader(boolean) error`.
- `iris.Gzip` and `iris.GzipReader` replaced with `iris.Compression` (middleware). - `iris.Gzip` and `iris.GzipReader` replaced with `iris.Compression` (middleware).

View File

@ -4,6 +4,6 @@ FireMethodNotAllowed = true
DisableBodyConsumptionOnUnmarshal = false DisableBodyConsumptionOnUnmarshal = false
TimeFormat = "Mon, 01 Jan 2006 15:04:05 GMT" TimeFormat = "Mon, 01 Jan 2006 15:04:05 GMT"
Charset = "utf-8" Charset = "utf-8"
RemoteAddrHeaders = ["X-Real-Ip", "X-Forwarded-For", "CF-Connecting-IP"]
[Other] [Other]
MyServerName = "iris" MyServerName = "iris"

View File

@ -9,8 +9,8 @@ SSLProxyHeaders:
HostProxyHeaders: HostProxyHeaders:
X-Host: true X-Host: true
RemoteAddrHeaders: RemoteAddrHeaders:
X-Real-Ip: true - X-Real-Ip
X-Forwarded-For: true - X-Forwarded-For
CF-Connecting-IP: true - CF-Connecting-IP
Other: Other:
Addr: :8080 Addr: :8080

View File

@ -351,48 +351,38 @@ func WithPostMaxMemory(limit int64) Configurator {
} }
} }
// WithRemoteAddrHeader enables or adds a new or existing request header name // WithRemoteAddrHeader adds a new request header name
// that can be used to validate the client's real IP. // that can be used to validate the client's real IP.
//
// By-default no "X-" header is consired safe to be used for retrieving the
// client's IP address, because those headers can manually change by
// the client. But sometimes are useful e.g., when behind a proxy
// you want to enable the "X-Forwarded-For" or when cloudflare
// you want to enable the "CF-Connecting-IP", inneed you
// can allow the `ctx.RemoteAddr()` to use any header
// that the client may sent.
//
// Defaults to an empty map but an example usage is:
// WithRemoteAddrHeader("X-Forwarded-For", "CF-Connecting-IP")
//
// Look `context.RemoteAddr()` for more.
func WithRemoteAddrHeader(header ...string) Configurator { func WithRemoteAddrHeader(header ...string) Configurator {
return func(app *Application) { return func(app *Application) {
if app.config.RemoteAddrHeaders == nil { for _, h := range header {
app.config.RemoteAddrHeaders = make(map[string]bool) exists := false
for _, v := range app.config.RemoteAddrHeaders {
if v == h {
exists = true
}
} }
for _, k := range header { if !exists {
app.config.RemoteAddrHeaders[k] = true app.config.RemoteAddrHeaders = append(app.config.RemoteAddrHeaders, h)
}
} }
} }
} }
// WithoutRemoteAddrHeader disables an existing request header name // WithoutRemoteAddrHeader removes an existing request header name
// that can be used to validate and parse the client's real IP. // that can be used to validate and parse the client's real IP.
// //
//
// Keep note that RemoteAddrHeaders is already defaults to an empty map
// so you don't have to call this Configurator if you didn't
// add allowed headers via configuration or via `WithRemoteAddrHeader` before.
//
// Look `context.RemoteAddr()` for more. // Look `context.RemoteAddr()` for more.
func WithoutRemoteAddrHeader(headerName string) Configurator { func WithoutRemoteAddrHeader(headerName string) Configurator {
return func(app *Application) { return func(app *Application) {
if app.config.RemoteAddrHeaders == nil { tmp := app.config.RemoteAddrHeaders[:0]
app.config.RemoteAddrHeaders = make(map[string]bool) for _, v := range app.config.RemoteAddrHeaders {
if v != headerName {
tmp = append(tmp, v)
} }
app.config.RemoteAddrHeaders[headerName] = false }
app.config.RemoteAddrHeaders = tmp
} }
} }
@ -783,21 +773,27 @@ type Configuration struct {
// that can be valid to parse the client's IP based on. // that can be valid to parse the client's IP based on.
// By-default no "X-" header is consired safe to be used for retrieving the // By-default no "X-" header is consired safe to be used for retrieving the
// client's IP address, because those headers can manually change by // client's IP address, because those headers can manually change by
// the client. But sometimes are useful e.g., when behind a proxy // the client. But sometimes are useful e.g. when behind a proxy
// you want to enable the "X-Forwarded-For" or when cloudflare // you want to enable the "X-Forwarded-For" or when cloudflare
// you want to enable the "CF-Connecting-IP", inneed you // you want to enable the "CF-Connecting-IP", indeed you
// can allow the `ctx.RemoteAddr()` to use any header // can allow the `ctx.RemoteAddr()` to use any header
// that the client may sent. // that the client may sent.
// //
// Defaults to an empty map but an example usage is: // Defaults to an empty slice but an example usage is:
// RemoteAddrHeaders { // RemoteAddrHeaders {
// "X-Real-Ip": true, // "X-Real-Ip",
// "X-Forwarded-For": true, // "X-Forwarded-For",
// "CF-Connecting-IP": true, // "CF-Connecting-IP",
// } // }
// //
// Look `context.RemoteAddr()` for more. // Look `context.RemoteAddr()` for more.
RemoteAddrHeaders map[string]bool `json:"remoteAddrHeaders,omitempty" yaml:"RemoteAddrHeaders" toml:"RemoteAddrHeaders"` RemoteAddrHeaders []string `json:"remoteAddrHeaders,omitempty" yaml:"RemoteAddrHeaders" toml:"RemoteAddrHeaders"`
// RemoteAddrHeadersForce forces the `Context.RemoteAddr()` method
// to return the first entry of a request header as a fallback,
// even if that IP is a part of the `RemoteAddrPrivateSubnets` list.
// The default behavior, if a remote address is part of the `RemoteAddrPrivateSubnets`,
// is to retrieve the IP from the `Request.RemoteAddr` field instead.
RemoteAddrHeadersForce bool `json:"remoteAddrHeadersForce,omitempty" yaml:"RemoteAddrHeadersForce" toml:"RemoteAddrHeadersForce"`
// RemoteAddrPrivateSubnets defines the private sub-networks. // RemoteAddrPrivateSubnets defines the private sub-networks.
// They are used to be compared against // They are used to be compared against
// IP Addresses fetched through `RemoteAddrHeaders` or `Context.Request.RemoteAddr`. // IP Addresses fetched through `RemoteAddrHeaders` or `Context.Request.RemoteAddr`.
@ -960,10 +956,15 @@ func (c Configuration) GetViewDataContextKey() string {
} }
// GetRemoteAddrHeaders returns the RemoteAddrHeaders field. // GetRemoteAddrHeaders returns the RemoteAddrHeaders field.
func (c Configuration) GetRemoteAddrHeaders() map[string]bool { func (c Configuration) GetRemoteAddrHeaders() []string {
return c.RemoteAddrHeaders return c.RemoteAddrHeaders
} }
// GetRemoteAddrHeadersForce returns RemoteAddrHeadersForce field.
func (c Configuration) GetRemoteAddrHeadersForce() bool {
return c.RemoteAddrHeadersForce
}
// GetSSLProxyHeaders returns the SSLProxyHeaders field. // GetSSLProxyHeaders returns the SSLProxyHeaders field.
func (c Configuration) GetSSLProxyHeaders() map[string]string { func (c Configuration) GetSSLProxyHeaders() map[string]string {
return c.SSLProxyHeaders return c.SSLProxyHeaders
@ -1102,12 +1103,11 @@ func WithConfiguration(c Configuration) Configurator {
} }
if v := c.RemoteAddrHeaders; len(v) > 0 { if v := c.RemoteAddrHeaders; len(v) > 0 {
if main.RemoteAddrHeaders == nil { main.RemoteAddrHeaders = v
main.RemoteAddrHeaders = make(map[string]bool, len(v))
}
for key, value := range v {
main.RemoteAddrHeaders[key] = value
} }
if v := c.RemoteAddrHeadersForce; v {
main.RemoteAddrHeadersForce = v
} }
if v := c.RemoteAddrPrivateSubnets; len(v) > 0 { if v := c.RemoteAddrPrivateSubnets; len(v) > 0 {
@ -1171,7 +1171,8 @@ func DefaultConfiguration() Configuration {
VersionContextKey: "iris.api.version", VersionContextKey: "iris.api.version",
ViewLayoutContextKey: "iris.viewLayout", ViewLayoutContextKey: "iris.viewLayout",
ViewDataContextKey: "iris.viewData", ViewDataContextKey: "iris.viewData",
RemoteAddrHeaders: make(map[string]bool), RemoteAddrHeaders: nil,
RemoteAddrHeadersForce: false,
RemoteAddrPrivateSubnets: []netutil.IPRange{ RemoteAddrPrivateSubnets: []netutil.IPRange{
{ {
Start: net.ParseIP("10.0.0.0"), Start: net.ParseIP("10.0.0.0"),

View File

@ -154,9 +154,9 @@ DisableBodyConsumptionOnUnmarshal: true
TimeFormat: "Mon, 02 Jan 2006 15:04:05 GMT" TimeFormat: "Mon, 02 Jan 2006 15:04:05 GMT"
Charset: "utf-8" Charset: "utf-8"
RemoteAddrHeaders: RemoteAddrHeaders:
X-Real-Ip: true - X-Real-Ip
X-Forwarded-For: true - X-Forwarded-For
CF-Connecting-IP: true - CF-Connecting-IP
HostProxyHeaders: HostProxyHeaders:
X-Host: true X-Host: true
SSLProxyHeaders: SSLProxyHeaders:
@ -210,19 +210,19 @@ Other:
t.Fatalf("error on TestConfigurationYAML: Expected RemoteAddrHeaders to be filled") t.Fatalf("error on TestConfigurationYAML: Expected RemoteAddrHeaders to be filled")
} }
expectedRemoteAddrHeaders := map[string]bool{ expectedRemoteAddrHeaders := []string{
"X-Real-Ip": true, "X-Real-Ip",
"X-Forwarded-For": true, "X-Forwarded-For",
"CF-Connecting-IP": true, "CF-Connecting-IP",
} }
if expected, got := len(c.RemoteAddrHeaders), len(expectedRemoteAddrHeaders); expected != got { if expected, got := len(c.RemoteAddrHeaders), len(expectedRemoteAddrHeaders); expected != got {
t.Fatalf("error on TestConfigurationYAML: Expected RemoteAddrHeaders' len(%d) and got(%d), len is not the same", expected, got) t.Fatalf("error on TestConfigurationYAML: Expected RemoteAddrHeaders' len(%d) and got(%d), len is not the same", expected, got)
} }
for k, v := range c.RemoteAddrHeaders { for i, v := range c.RemoteAddrHeaders {
if expected, got := expectedRemoteAddrHeaders[k], v; expected != got { if expected, got := expectedRemoteAddrHeaders[i], v; expected != got {
t.Fatalf("error on TestConfigurationYAML: Expected RemoteAddrHeaders[%s] = %t but got %t", k, expected, got) t.Fatalf("error on TestConfigurationYAML: Expected RemoteAddrHeaders[%d] = %s but got %s", i, expected, got)
} }
} }
@ -285,10 +285,7 @@ DisableBodyConsumptionOnUnmarshal = true
TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
Charset = "utf-8" Charset = "utf-8"
[RemoteAddrHeaders] RemoteAddrHeaders = ["X-Real-Ip", "X-Forwarded-For", "CF-Connecting-IP"]
X-Real-Ip = true
X-Forwarded-For = true
CF-Connecting-IP = true
[Other] [Other]
# Indentation (tabs and/or spaces) is allowed but not required # Indentation (tabs and/or spaces) is allowed but not required
@ -337,19 +334,19 @@ Charset = "utf-8"
t.Fatalf("error on TestConfigurationTOML: Expected RemoteAddrHeaders to be filled") t.Fatalf("error on TestConfigurationTOML: Expected RemoteAddrHeaders to be filled")
} }
expectedRemoteAddrHeaders := map[string]bool{ expectedRemoteAddrHeaders := []string{
"X-Real-Ip": true, "X-Real-Ip",
"X-Forwarded-For": true, "X-Forwarded-For",
"CF-Connecting-IP": true, "CF-Connecting-IP",
} }
if expected, got := len(c.RemoteAddrHeaders), len(expectedRemoteAddrHeaders); expected != got { if expected, got := len(c.RemoteAddrHeaders), len(expectedRemoteAddrHeaders); expected != got {
t.Fatalf("error on TestConfigurationTOML: Expected RemoteAddrHeaders' len(%d) and got(%d), len is not the same", expected, got) t.Fatalf("error on TestConfigurationTOML: Expected RemoteAddrHeaders' len(%d) and got(%d), len is not the same", expected, got)
} }
for k, v := range c.RemoteAddrHeaders { for i, got := range c.RemoteAddrHeaders {
if expected, got := expectedRemoteAddrHeaders[k], v; expected != got { if expected := expectedRemoteAddrHeaders[i]; expected != got {
t.Fatalf("error on TestConfigurationTOML: Expected RemoteAddrHeaders[%s] = %t but got %t", k, expected, got) t.Fatalf("error on TestConfigurationTOML: Expected RemoteAddrHeaders[%d] = %s but got %s", i, expected, got)
} }
} }

View File

@ -57,7 +57,9 @@ type ConfigurationReadOnly interface {
GetViewDataContextKey() string GetViewDataContextKey() string
// GetRemoteAddrHeaders returns RemoteAddrHeaders field. // GetRemoteAddrHeaders returns RemoteAddrHeaders field.
GetRemoteAddrHeaders() map[string]bool GetRemoteAddrHeaders() []string
// GetRemoteAddrHeadersForce returns RemoteAddrHeadersForce field.
GetRemoteAddrHeadersForce() bool
// GetRemoteAddrPrivateSubnets returns the RemoteAddrPrivateSubnets field. // GetRemoteAddrPrivateSubnets returns the RemoteAddrPrivateSubnets field.
GetRemoteAddrPrivateSubnets() []netutil.IPRange GetRemoteAddrPrivateSubnets() []netutil.IPRange
// GetSSLProxyHeaders returns the SSLProxyHeaders field. // GetSSLProxyHeaders returns the SSLProxyHeaders field.

View File

@ -803,27 +803,41 @@ func (ctx *Context) FullRequestURI() string {
// Based on allowed headers names that can be modified from Configuration.RemoteAddrHeaders. // Based on allowed headers names that can be modified from Configuration.RemoteAddrHeaders.
// //
// If parse based on these headers fail then it will return the Request's `RemoteAddr` field // If parse based on these headers fail then it will return the Request's `RemoteAddr` field
// which is filled by the server before the HTTP handler. // which is filled by the server before the HTTP handler,
// unless the Configuration.RemoteAddrHeadersForce was set to true
// which will force this method to return the first IP from RemoteAddrHeaders
// even if it's part of a private network.
// //
// Look `Configuration.RemoteAddrHeaders`, // Look `Configuration.RemoteAddrHeaders`,
// `Configuration.RemoteAddrHeadersForce`,
// `Configuration.WithRemoteAddrHeader(...)`, // `Configuration.WithRemoteAddrHeader(...)`,
// `Configuration.WithoutRemoteAddrHeader(...)` and // `Configuration.WithoutRemoteAddrHeader(...)` and
// `Configuration.RemoteAddrPrivateSubnets` for more. // `Configuration.RemoteAddrPrivateSubnets` for more.
func (ctx *Context) RemoteAddr() string { func (ctx *Context) RemoteAddr() string {
remoteHeaders := ctx.app.ConfigurationReadOnly().GetRemoteAddrHeaders() if remoteHeaders := ctx.app.ConfigurationReadOnly().GetRemoteAddrHeaders(); len(remoteHeaders) > 0 {
privateSubnets := ctx.app.ConfigurationReadOnly().GetRemoteAddrPrivateSubnets() privateSubnets := ctx.app.ConfigurationReadOnly().GetRemoteAddrPrivateSubnets()
for headerName, enabled := range remoteHeaders { for _, headerName := range remoteHeaders {
if !enabled {
continue
}
ipAddresses := strings.Split(ctx.GetHeader(headerName), ",") ipAddresses := strings.Split(ctx.GetHeader(headerName), ",")
if ip, ok := netutil.GetIPAddress(ipAddresses, privateSubnets); ok { if ip, ok := netutil.GetIPAddress(ipAddresses, privateSubnets); ok {
return ip return ip
} }
} }
if ctx.app.ConfigurationReadOnly().GetRemoteAddrHeadersForce() {
for _, headerName := range remoteHeaders {
// return the first valid IP,
// even if it's a part of a private network.
ipAddresses := strings.Split(ctx.GetHeader(headerName), ",")
for _, addr := range ipAddresses {
if ip, _, err := net.SplitHostPort(addr); err == nil {
return ip
}
}
}
}
}
addr := strings.TrimSpace(ctx.request.RemoteAddr) addr := strings.TrimSpace(ctx.request.RemoteAddr)
if addr != "" { if addr != "" {
// if addr has port use the net.SplitHostPort otherwise(error occurs) take as it is // if addr has port use the net.SplitHostPort otherwise(error occurs) take as it is