diff --git a/HISTORY.md b/HISTORY.md index aa728107..d0fe7105 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -184,6 +184,8 @@ Other Improvements: - Hero Handlers (and `app.ConfigureContainer().Handle`) do not have to require `iris.Context` just to call `ctx.Next()` anymore, this is done automatically now. +- Improve Remote Address parsing as requested at: https://github.com/kataras/iris/issues/1453. Add `Configuration.RemoteAddrPrivateSubnets` to exclude those addresses when fetched by `Configuration.RemoteAddrHeaders` through `context.RemoteAddr() string`. + New Context Methods: - `context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too diff --git a/_examples/configuration/README.md b/_examples/configuration/README.md index 443f0fc1..a018c6f6 100644 --- a/_examples/configuration/README.md +++ b/_examples/configuration/README.md @@ -137,6 +137,21 @@ func main() { ## Builtin Configurators ```go + +// WithGlobalConfiguration will load the global yaml configuration file +// from the home directory and it will set/override the whole app's configuration +// to that file's contents. The global configuration file can be modified by user +// and be used by multiple iris instances. +// +// This is useful when we run multiple iris servers that share the same +// configuration, even with custom values at its "Other" field. +// +// Usage: `app.Configure(iris.WithGlobalConfiguration)` or `app.Run([iris.Runner], iris.WithGlobalConfiguration)`. +WithGlobalConfiguration + +// variables for configurators don't need any receivers, functions +// for them that need (helps code editors to recognise as variables without parenthesis completion). + // WithoutServerError will cause to ignore the matched "errors" // from the main application's `Run` function. // @@ -147,81 +162,128 @@ func main() { // See `Configuration#IgnoreServerErrors []string` too. // // Example: https://github.com/kataras/iris/tree/master/_examples/http-listening/listen-addr/omit-server-errors -func WithoutServerError(errors ...error) Configurator +WithoutServerError(errors ...error) Configurator // WithoutStartupLog turns off the information send, once, to the terminal when the main server is open. -var WithoutStartupLog +WithoutStartupLog // WithoutInterruptHandler disables the automatic graceful server shutdown // when control/cmd+C pressed. -var WithoutInterruptHandler +WithoutInterruptHandle // WithoutPathCorrection disables the PathCorrection setting. // // See `Configuration`. -var WithoutPathCorrection +WithoutPathCorrectio + +// WithoutPathCorrectionRedirection disables the PathCorrectionRedirection setting. +// +// See `Configuration`. +WithoutPathCorrectionRedirection // WithoutBodyConsumptionOnUnmarshal disables BodyConsumptionOnUnmarshal setting. // // See `Configuration`. -var WithoutBodyConsumptionOnUnmarshal +WithoutBodyConsumptionOnUnmarshal // WithoutAutoFireStatusCode disables the AutoFireStatusCode setting. // // See `Configuration`. -var WithoutAutoFireStatusCode +WithoutAutoFireStatusCode -// WithPathEscape enanbles the PathEscape setting. +// WithPathEscape enables the PathEscape setting. // // See `Configuration`. -var WithPathEscape +WithPathEscape // WithOptimizations can force the application to optimize for the best performance where is possible. // // See `Configuration`. -var WithOptimizations +WithOptimizations -// WithFireMethodNotAllowed enanbles the FireMethodNotAllowed setting. +// WithFireMethodNotAllowed enables the FireMethodNotAllowed setting. // // See `Configuration`. -var WithFireMethodNotAllowed +WithFireMethodNotAllowed // WithTimeFormat sets the TimeFormat setting. // // See `Configuration`. -func WithTimeFormat(timeformat string) Configurator +WithTimeFormat(timeformat string) Configurator // WithCharset sets the Charset setting. // // See `Configuration`. -func WithCharset(charset string) Configurator +WithCharset(charset string) Configurator + +// WithPostMaxMemory sets the maximum post data size +// that a client can send to the server, this differs +// from the overral request body size which can be modified +// by the `context#SetMaxRequestBodySize` or `iris#LimitRequestBodySize`. +// +// Defaults to 32MB or 32 << 20 if you prefer. +WithPostMaxMemory(limit int64) Configurator // WithRemoteAddrHeader enables or adds a new or existing request header name // that can be used to validate the client's real IP. // -// Existing values are: -// "X-Real-Ip": false, -// "X-Forwarded-For": false, -// "CF-Connecting-IP": false +// 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") // // Look `context.RemoteAddr()` for more. -func WithRemoteAddrHeader(headerName string) Configurator +WithRemoteAddrHeader(headerName string) Configurator // WithoutRemoteAddrHeader disables an existing request header name -// that can be used to validate the client's real IP. +// that can be used to validate and parse the client's real IP. // -// Existing values are: -// "X-Real-Ip": false, -// "X-Forwarded-For": false, -// "CF-Connecting-IP": false +// +// 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. -func WithoutRemoteAddrHeader(headerName string) Configurator +WithoutRemoteAddrHeader(headerName string) Configurator + +// WithRemoteAddrPrivateSubnet adds a new private sub-net to be excluded from `context.RemoteAddr`. +// See `WithRemoteAddrHeader` too. +WithRemoteAddrPrivateSubnet(startIP, endIP string) Configurator // WithOtherValue adds a value based on a key to the Other setting. // -// See `Configuration`. -func WithOtherValue(key string, val interface{}) Configurator +// See `Configuration.Other`. +WithOtherValue(key string, val interface{}) Configurator + +// WithSitemap enables the sitemap generator. +// Use the Route's `SetLastMod`, `SetChangeFreq` and `SetPriority` to modify +// the sitemap's URL child element properties. +// +// It accepts a "startURL" input argument which +// is the prefix for the registered routes that will be included in the sitemap. +// +// If more than 50,000 static routes are registered then sitemaps will be splitted and a sitemap index will be served in +// /sitemap.xml. +// +// If `Application.I18n.Load/LoadAssets` is called then the sitemap will contain translated links for each static route. +// +// If the result does not complete your needs you can take control +// and use the github.com/kataras/sitemap package to generate a customized one instead. +// +// Example: https://github.com/kataras/iris/tree/master/_examples/sitemap. +WithSitemap(startURL string) Configurator + +// WithTunneling is the `iris.Configurator` for the `iris.Configuration.Tunneling` field. +// It's used to enable http tunneling for an Iris Application, per registered host +// +// Alternatively use the `iris.WithConfiguration(iris.Configuration{Tunneling: iris.TunnelingConfiguration{ ...}}}`. +WithTunneling ``` ## Custom Configurator diff --git a/configuration.go b/configuration.go index f46a4b2c..0ae32744 100644 --- a/configuration.go +++ b/configuration.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io/ioutil" + "net" "net/http" "os" "os/exec" @@ -350,6 +351,17 @@ func WithoutRemoteAddrHeader(headerName string) Configurator { } } +// WithRemoteAddrPrivateSubnet adds a new private sub-net to be excluded from `context.RemoteAddr`. +// See `WithRemoteAddrHeader` too. +func WithRemoteAddrPrivateSubnet(startIP, endIP string) Configurator { + return func(app *Application) { + app.config.RemoteAddrPrivateSubnets = append(app.config.RemoteAddrPrivateSubnets, netutil.IPRange{ + Start: net.IP(startIP), + End: net.IP(endIP), + }) + } +} + // WithOtherValue adds a value based on a key to the Other setting. // // See `Configuration.Other`. @@ -857,6 +869,19 @@ type Configuration struct { // Look `context.RemoteAddr()` for more. RemoteAddrHeaders map[string]bool `json:"remoteAddrHeaders,omitempty" yaml:"RemoteAddrHeaders" toml:"RemoteAddrHeaders"` + // RemoteAddrPrivateSubnets defines the private sub-networks. + // They are used to be compared against + // IP Addresses fetched through `RemoteAddrHeaders` or `Request.RemoteAddr`. + // For details please navigate through: https://github.com/kataras/iris/issues/1453 + // Defaults to an empty slice, usage: + // + // RemoteAddrPrivateSubnets { + // {Start: "10.0.0.0", End: "10.255.255.255"}, + // {Start: "100.64.0.0", End: "100.127.255.255"}, + // } + // + // Look `context.RemoteAddr()` for more. + RemoteAddrPrivateSubnets []netutil.IPRange `json:"remoteAddrPrivateSubnets" yaml:"RemoteAddrPrivateSubnets" toml:"RemoteAddrPrivateSubnets"` // Other are the custom, dynamic options, can be empty. // This field used only by you to set any app's options you want. // @@ -993,6 +1018,22 @@ func (c Configuration) GetRemoteAddrHeaders() map[string]bool { return c.RemoteAddrHeaders } +// GetRemoteAddrPrivateSubnets returns the configuration's private sub-networks. +// They are used to be compared against +// IP Addresses fetched through `RemoteAddrHeaders` or `Request.RemoteAddr`. +// For details please navigate through: https://github.com/kataras/iris/issues/1453 +// Defaults to an empty slice, usage: +// +// RemoteAddrPrivateSubnets { +// {Start: "10.0.0.0", End: "10.255.255.255"}, +// {Start: "100.64.0.0", End: "100.127.255.255"}, +// } +// +// Look `context.RemoteAddr()` for more. +func (c Configuration) GetRemoteAddrPrivateSubnets() []netutil.IPRange { + return c.RemoteAddrPrivateSubnets +} + // GetOther returns the Configuration#Other map. func (c Configuration) GetOther() map[string]interface{} { return c.Other @@ -1087,6 +1128,10 @@ func WithConfiguration(c Configuration) Configurator { } } + if v := c.RemoteAddrPrivateSubnets; len(v) > 0 { + main.RemoteAddrPrivateSubnets = v + } + if v := c.Other; len(v) > 0 { if main.Other == nil { main.Other = make(map[string]interface{}, len(v)) @@ -1116,12 +1161,13 @@ func DefaultConfiguration() Configuration { // The request body the size limit // can be set by the middleware `LimitRequestBodySize` // or `context#SetMaxRequestBodySize`. - PostMaxMemory: 32 << 20, // 32MB - LocaleContextKey: "iris.locale", - ViewLayoutContextKey: "iris.viewLayout", - ViewDataContextKey: "iris.viewData", - RemoteAddrHeaders: make(map[string]bool), - EnableOptimizations: false, - Other: make(map[string]interface{}), + PostMaxMemory: 32 << 20, // 32MB + LocaleContextKey: "iris.locale", + ViewLayoutContextKey: "iris.viewLayout", + ViewDataContextKey: "iris.viewData", + RemoteAddrHeaders: make(map[string]bool), + RemoteAddrPrivateSubnets: []netutil.IPRange{}, + EnableOptimizations: false, + Other: make(map[string]interface{}), } } diff --git a/configuration_test.go b/configuration_test.go index 3ceae6eb..f9df9dc7 100644 --- a/configuration_test.go +++ b/configuration_test.go @@ -112,10 +112,13 @@ func createGlobalConfiguration(t *testing.T) { } func TestConfigurationGlobal(t *testing.T) { + t.Cleanup(func() { + os.Remove(homeConfigurationFilename(".yml")) + }) + createGlobalConfiguration(t) testConfigurationGlobal(t, WithGlobalConfiguration) - // globalConfigurationKeyword = "~"" testConfigurationGlobal(t, WithConfiguration(YAML(globalConfigurationKeyword))) } diff --git a/context/configuration.go b/context/configuration.go index 2c9cd76e..bd5a65a2 100644 --- a/context/configuration.go +++ b/context/configuration.go @@ -1,5 +1,9 @@ package context +import ( + "github.com/kataras/iris/v12/core/netutil" +) + // ConfigurationReadOnly can be implemented // by Configuration, it's being used inside the Context. // All methods that it contains should be "safe" to be called by the context @@ -91,7 +95,19 @@ type ConfigurationReadOnly interface { // // Look `context.RemoteAddr()` for more. GetRemoteAddrHeaders() map[string]bool - + // GetRemoteAddrPrivateSubnets returns the configuration's private sub-networks. + // They are used to be compared against + // IP Addresses fetched through `RemoteAddrHeaders` or `Request.RemoteAddr`. + // For details please navigate through: https://github.com/kataras/iris/issues/1453 + // Defaults to an empty slice, usage: + // + // RemoteAddrPrivateSubnets { + // {Start: "10.0.0.0", End: "10.255.255.255"}, + // {Start: "100.64.0.0", End: "100.127.255.255"}, + // } + // + // Look `context.RemoteAddr()` for more. + GetRemoteAddrPrivateSubnets() []netutil.IPRange // GetOther returns the configuration.Other map. GetOther() map[string]interface{} } diff --git a/context/context.go b/context/context.go index d7cc1963..ab270b70 100644 --- a/context/context.go +++ b/context/context.go @@ -25,6 +25,7 @@ import ( "unsafe" "github.com/kataras/iris/v12/core/memstore" + "github.com/kataras/iris/v12/core/netutil" "github.com/Shopify/goreferrer" "github.com/fatih/structs" @@ -358,7 +359,8 @@ type Context interface { // // Look `Configuration.RemoteAddrHeaders`, // `Configuration.WithRemoteAddrHeader(...)`, - // `Configuration.WithoutRemoteAddrHeader(...)` for more. + // `Configuration.WithoutRemoteAddrHeader(...)`and + // `Configuration.RemoteAddrPrivateSubnets` for more. RemoteAddr() string // GetHeader returns the request header's value based on its name. GetHeader(name string) string @@ -1753,25 +1755,20 @@ const xForwardedForHeaderKey = "X-Forwarded-For" // // Look `Configuration.RemoteAddrHeaders`, // `Configuration.WithRemoteAddrHeader(...)`, -// `Configuration.WithoutRemoteAddrHeader(...)` for more. +// `Configuration.WithoutRemoteAddrHeader(...)` and +// `Configuration.RemoteAddrPrivateSubnets` for more. func (ctx *context) RemoteAddr() string { remoteHeaders := ctx.Application().ConfigurationReadOnly().GetRemoteAddrHeaders() + privateSubnets := ctx.Application().ConfigurationReadOnly().GetRemoteAddrPrivateSubnets() for headerName, enabled := range remoteHeaders { - if enabled { - headerValue := ctx.GetHeader(headerName) - // exception needed for 'X-Forwarded-For' only , if enabled. - if headerName == xForwardedForHeaderKey { - idx := strings.IndexByte(headerValue, ',') - if idx >= 0 { - headerValue = headerValue[0:idx] - } - } + if !enabled { + continue + } - realIP := strings.TrimSpace(headerValue) - if realIP != "" { - return realIP - } + ipAddresses := strings.Split(ctx.GetHeader(headerName), ",") + if ip, ok := netutil.GetIPAddress(ipAddresses, privateSubnets); ok { + return ip } } diff --git a/core/netutil/ip.go b/core/netutil/ip.go new file mode 100644 index 00000000..7ae395f7 --- /dev/null +++ b/core/netutil/ip.go @@ -0,0 +1,59 @@ +package netutil + +import ( + "bytes" + "net" + "strings" +) + +/* Based on: +https://husobee.github.io/golang/ip-address/2015/12/17/remote-ip-go.html requested at: +https://github.com/kataras/iris/issues/1453 +*/ + +//IPRange is a structure that holds the start and end of a range of IP Addresses. +type IPRange struct { + Start net.IP `json:"start" yaml:"Start" toml"Start"` + End net.IP `json:"end" yaml:"End" toml"End"` +} + +// IPInRange reports whether a given IP Address is within a range given. +func IPInRange(r IPRange, ipAddress net.IP) bool { + return bytes.Compare(ipAddress, r.Start) >= 0 && bytes.Compare(ipAddress, r.End) < 0 +} + +// IPIsPrivateSubnet reports whether this "ipAddress" is in a private subnet. +func IPIsPrivateSubnet(ipAddress net.IP, privateRanges []IPRange) bool { + // IPv4 for now. + if ipCheck := ipAddress.To4(); ipCheck != nil { + // iterate over all our ranges. + for _, r := range privateRanges { + // check if this ip is in a private range. + if IPInRange(r, ipAddress) { + return true + } + } + } + return false +} + +// GetIPAddress returns a valid public IP Address from a collection of IP Addresses +// and a range of private subnets. +// +// Reports whether a valid IP was found. +func GetIPAddress(ipAddresses []string, privateRanges []IPRange) (string, bool) { + // march from right to left until we get a public address + // that will be the address right before our proxy. + for i := len(ipAddresses) - 1; i >= 0; i-- { + ip := strings.TrimSpace(ipAddresses[i]) + realIP := net.ParseIP(ip) + if !realIP.IsGlobalUnicast() || IPIsPrivateSubnet(realIP, privateRanges) { + // bad address, go to next + continue + } + return ip, true + + } + + return "", false +} diff --git a/core/netutil/ip_test.go b/core/netutil/ip_test.go new file mode 100644 index 00000000..4451b4fa --- /dev/null +++ b/core/netutil/ip_test.go @@ -0,0 +1,62 @@ +package netutil + +import ( + "net" + "testing" +) + +func TestIP(t *testing.T) { + privateRanges := []IPRange{ + { + Start: net.ParseIP("10.0.0.0"), + End: net.ParseIP("10.255.255.255"), + }, + { + Start: net.ParseIP("100.64.0.0"), + End: net.ParseIP("100.127.255.255"), + }, + { + Start: net.ParseIP("172.16.0.0"), + End: net.ParseIP("172.31.255.255"), + }, + { + Start: net.ParseIP("192.0.0.0"), + End: net.ParseIP("192.0.0.255"), + }, + { + Start: net.ParseIP("192.168.0.0"), + End: net.ParseIP("192.168.255.255"), + }, + { + Start: net.ParseIP("198.18.0.0"), + End: net.ParseIP("198.19.255.255"), + }, + } + + addresses := []string{ + "201.37.138.59", + "159.117.3.153", + "166.192.97.84", + "225.181.213.210", + "124.50.84.134", + "87.53.250.102", + "106.79.33.62", + "242.120.17.144", + "131.179.101.254", + "103.11.11.174", + "115.97.0.114", + "219.202.120.251", + "37.72.123.120", + "154.94.78.101", + "126.105.144.250", + } + + got, ok := GetIPAddress(addresses, privateRanges) + if !ok { + t.Logf("expected addr to be matched") + } + + if expected := "126.105.144.250"; expected != got { + t.Logf("expected addr to be found: %s but got: %s", expected, got) + } +}