Handle incoming proxy requests ip, scheme (#5591)
This PR implements functions to get the right ip, scheme from the incoming proxied requests.master
parent
d71b1d25f8
commit
e4f6877c8b
@ -0,0 +1,115 @@ |
|||||||
|
/* |
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package handlers |
||||||
|
|
||||||
|
import ( |
||||||
|
"net" |
||||||
|
"net/http" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
// De-facto standard header keys.
|
||||||
|
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") |
||||||
|
xForwardedHost = http.CanonicalHeaderKey("X-Forwarded-Host") |
||||||
|
xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto") |
||||||
|
xForwardedScheme = http.CanonicalHeaderKey("X-Forwarded-Scheme") |
||||||
|
xRealIP = http.CanonicalHeaderKey("X-Real-IP") |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
// RFC7239 defines a new "Forwarded: " header designed to replace the
|
||||||
|
// existing use of X-Forwarded-* headers.
|
||||||
|
// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
|
||||||
|
forwarded = http.CanonicalHeaderKey("Forwarded") |
||||||
|
// Allows for a sub-match of the first value after 'for=' to the next
|
||||||
|
// comma, semi-colon or space. The match is case-insensitive.
|
||||||
|
forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)(.*)`) |
||||||
|
// Allows for a sub-match for the first instance of scheme (http|https)
|
||||||
|
// prefixed by 'proto='. The match is case-insensitive.
|
||||||
|
protoRegex = regexp.MustCompile(`(?i)^(;|,| )+(?:proto=)(https|http)`) |
||||||
|
) |
||||||
|
|
||||||
|
// GetSourceScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239
|
||||||
|
// Forwarded headers (in that order).
|
||||||
|
func GetSourceScheme(r *http.Request) string { |
||||||
|
var scheme string |
||||||
|
|
||||||
|
// Retrieve the scheme from X-Forwarded-Proto.
|
||||||
|
if proto := r.Header.Get(xForwardedProto); proto != "" { |
||||||
|
scheme = strings.ToLower(proto) |
||||||
|
} else if proto = r.Header.Get(xForwardedScheme); proto != "" { |
||||||
|
scheme = strings.ToLower(proto) |
||||||
|
} else if proto := r.Header.Get(forwarded); proto != "" { |
||||||
|
// match should contain at least two elements if the protocol was
|
||||||
|
// specified in the Forwarded header. The first element will always be
|
||||||
|
// the 'for=', which we ignore, subsequently we proceed to look for
|
||||||
|
// 'proto=' which should precede right after `for=` if not
|
||||||
|
// we simply ignore the values and return empty. This is in line
|
||||||
|
// with the approach we took for returning first ip from multiple
|
||||||
|
// params.
|
||||||
|
if match := forRegex.FindStringSubmatch(proto); len(match) > 1 { |
||||||
|
if match = protoRegex.FindStringSubmatch(match[2]); len(match) > 1 { |
||||||
|
scheme = strings.ToLower(match[2]) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return scheme |
||||||
|
} |
||||||
|
|
||||||
|
// GetSourceIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239
|
||||||
|
// Forwarded headers (in that order), falls back to r.RemoteAddr when all
|
||||||
|
// else fails.
|
||||||
|
func GetSourceIP(r *http.Request) string { |
||||||
|
var addr string |
||||||
|
|
||||||
|
if fwd := r.Header.Get(xForwardedFor); fwd != "" { |
||||||
|
// Only grab the first (client) address. Note that '192.168.0.1,
|
||||||
|
// 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
|
||||||
|
// the first may represent forwarding proxies earlier in the chain.
|
||||||
|
s := strings.Index(fwd, ", ") |
||||||
|
if s == -1 { |
||||||
|
s = len(fwd) |
||||||
|
} |
||||||
|
addr = fwd[:s] |
||||||
|
} else if fwd := r.Header.Get(xRealIP); fwd != "" { |
||||||
|
// X-Real-IP should only contain one IP address (the client making the
|
||||||
|
// request).
|
||||||
|
addr = fwd |
||||||
|
} else if fwd := r.Header.Get(forwarded); fwd != "" { |
||||||
|
// match should contain at least two elements if the protocol was
|
||||||
|
// specified in the Forwarded header. The first element will always be
|
||||||
|
// the 'for=' capture, which we ignore. In the case of multiple IP
|
||||||
|
// addresses (for=8.8.8.8, 8.8.4.4, 172.16.1.20 is valid) we only
|
||||||
|
// extract the first, which should be the client IP.
|
||||||
|
if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 { |
||||||
|
// IPv6 addresses in Forwarded headers are quoted-strings. We strip
|
||||||
|
// these quotes.
|
||||||
|
addr = strings.Trim(match[1], `"`) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if addr != "" { |
||||||
|
return addr |
||||||
|
} |
||||||
|
|
||||||
|
// Default to remote address if headers not set.
|
||||||
|
addr, _, _ = net.SplitHostPort(r.RemoteAddr) |
||||||
|
return addr |
||||||
|
} |
@ -0,0 +1,83 @@ |
|||||||
|
/* |
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package handlers |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
type headerTest struct { |
||||||
|
key string // header key
|
||||||
|
val string // header val
|
||||||
|
expected string // expected result
|
||||||
|
} |
||||||
|
|
||||||
|
func TestGetScheme(t *testing.T) { |
||||||
|
headers := []headerTest{ |
||||||
|
{xForwardedProto, "https", "https"}, |
||||||
|
{xForwardedProto, "http", "http"}, |
||||||
|
{xForwardedProto, "HTTP", "http"}, |
||||||
|
{xForwardedScheme, "https", "https"}, |
||||||
|
{xForwardedScheme, "http", "http"}, |
||||||
|
{xForwardedScheme, "HTTP", "http"}, |
||||||
|
{forwarded, `For="[2001:db8:cafe::17]:4711`, ""}, // No proto
|
||||||
|
{forwarded, `for=192.0.2.43, for=198.51.100.17;proto=https`, ""}, // Multiple params, will be empty.
|
||||||
|
{forwarded, `for=172.32.10.15; proto=https;by=127.0.0.1;`, "https"}, // Space before proto
|
||||||
|
{forwarded, `for=192.0.2.60;proto=http;by=203.0.113.43`, "http"}, // Multiple params
|
||||||
|
} |
||||||
|
for _, v := range headers { |
||||||
|
req := &http.Request{ |
||||||
|
Header: http.Header{ |
||||||
|
v.key: []string{v.val}, |
||||||
|
}} |
||||||
|
res := GetSourceScheme(req) |
||||||
|
if res != v.expected { |
||||||
|
t.Errorf("wrong header for %s: got %s want %s", v.key, res, |
||||||
|
v.expected) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TestGetSourceIP - check the source ip of a request is parsed correctly.
|
||||||
|
func TestGetSourceIP(t *testing.T) { |
||||||
|
headers := []headerTest{ |
||||||
|
{xForwardedFor, "8.8.8.8", "8.8.8.8"}, // Single address
|
||||||
|
{xForwardedFor, "8.8.8.8, 8.8.4.4", "8.8.8.8"}, // Multiple
|
||||||
|
{xForwardedFor, "", ""}, // None
|
||||||
|
{xRealIP, "8.8.8.8", "8.8.8.8"}, // Single address
|
||||||
|
{xRealIP, "[2001:db8:cafe::17]:4711", "[2001:db8:cafe::17]:4711"}, // IPv6 address
|
||||||
|
{xRealIP, "", ""}, // None
|
||||||
|
{forwarded, `for="_gazonk"`, "_gazonk"}, // Hostname
|
||||||
|
{forwarded, `For="[2001:db8:cafe::17]:4711`, `[2001:db8:cafe::17]:4711`}, // IPv6 address
|
||||||
|
{forwarded, `for=192.0.2.60;proto=http;by=203.0.113.43`, `192.0.2.60`}, // Multiple params
|
||||||
|
{forwarded, `for=192.0.2.43, for=198.51.100.17`, "192.0.2.43"}, // Multiple params
|
||||||
|
{forwarded, `for="workstation.local",for=198.51.100.17`, "workstation.local"}, // Hostname
|
||||||
|
} |
||||||
|
|
||||||
|
for _, v := range headers { |
||||||
|
req := &http.Request{ |
||||||
|
Header: http.Header{ |
||||||
|
v.key: []string{v.val}, |
||||||
|
}} |
||||||
|
res := GetSourceIP(req) |
||||||
|
if res != v.expected { |
||||||
|
t.Errorf("wrong header for %s: got %s want %s", v.key, res, |
||||||
|
v.expected) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue