Merge pull request #337 from kgriffs/issues/332
feat(Requests): Add req.host and req.subdomain
This commit is contained in:
@@ -55,6 +55,15 @@ class Request(object):
|
||||
Attributes:
|
||||
protocol (str): Either 'http' or 'https'.
|
||||
method (str): HTTP method requested (e.g., GET, POST, etc.)
|
||||
host (str): Hostname requested by the client
|
||||
subdomain (str): Leftmost (i.e., most specific) subdomain from the
|
||||
hostname. If only a single domain name is given, `subdomain`
|
||||
will be *None*.
|
||||
|
||||
Note:
|
||||
If the hostname in the request is an IP address, the value
|
||||
for `subdomain` is undefined.
|
||||
|
||||
user_agent (str): Value of the User-Agent header, or *None* if the
|
||||
header is missing.
|
||||
app (str): Name of the WSGI app (if using WSGI's notion of virtual
|
||||
@@ -359,11 +368,31 @@ class Request(object):
|
||||
@property
|
||||
def uri(self):
|
||||
if self._cached_uri is None:
|
||||
env = self.env
|
||||
protocol = env['wsgi.url_scheme']
|
||||
|
||||
# NOTE(kgriffs): According to PEP-3333 we should first
|
||||
# try to use the Host header if present.
|
||||
#
|
||||
# PERF(kgriffs): try..except is faster than .get
|
||||
try:
|
||||
host = env['HTTP_HOST']
|
||||
except KeyError:
|
||||
host = env['SERVER_NAME']
|
||||
port = env['SERVER_PORT']
|
||||
|
||||
if protocol == 'https':
|
||||
if port != '443':
|
||||
host += ':' + port
|
||||
else:
|
||||
if port != '80':
|
||||
host += ':' + port
|
||||
|
||||
# PERF: For small numbers of items, '+' is faster
|
||||
# than ''.join(...). Concatenation is also generally
|
||||
# faster than formatting.
|
||||
value = (self.protocol + '://' +
|
||||
self.get_header('host') +
|
||||
value = (protocol + '://' +
|
||||
host +
|
||||
self.app +
|
||||
self.path)
|
||||
|
||||
@@ -376,6 +405,27 @@ class Request(object):
|
||||
|
||||
url = uri
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
try:
|
||||
# NOTE(kgriffs): Prefer the host header; the web server
|
||||
# isn't supposed to mess with it, so it should be what
|
||||
# the client actually sent.
|
||||
host_header = self.env['HTTP_HOST']
|
||||
host, port = uri.parse_host(host_header)
|
||||
except KeyError:
|
||||
# PERF(kgriffs): According to PEP-3333, this header
|
||||
# will always be present.
|
||||
host = self.env['SERVER_NAME']
|
||||
|
||||
return host
|
||||
|
||||
@property
|
||||
def subdomain(self):
|
||||
# PERF(kgriffs): .partition is slightly faster than .split
|
||||
subdomain, sep, remainder = self.host.partition('.')
|
||||
return subdomain if sep else None
|
||||
|
||||
@property
|
||||
def relative_uri(self):
|
||||
if self._cached_relative_uri is None:
|
||||
|
||||
@@ -37,16 +37,6 @@ def normalize_headers(env):
|
||||
if 'CONTENT_LENGTH' in env:
|
||||
env['HTTP_CONTENT_LENGTH'] = env['CONTENT_LENGTH']
|
||||
|
||||
# Fallback to SERVER_* vars if the Host header isn't specified
|
||||
if 'HTTP_HOST' not in env:
|
||||
host = env['SERVER_NAME']
|
||||
port = env['SERVER_PORT']
|
||||
|
||||
if port != '80':
|
||||
host = ''.join([host, ':', port])
|
||||
|
||||
env['HTTP_HOST'] = host
|
||||
|
||||
|
||||
class Body(object):
|
||||
"""Wrap wsgi.input streams to make them more robust.
|
||||
|
||||
@@ -51,7 +51,8 @@ def rand_string(min, max):
|
||||
for i in range(string_length)])
|
||||
|
||||
|
||||
def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
|
||||
def create_environ(path='/', query_string='', protocol='HTTP/1.1',
|
||||
scheme='http', host=DEFAULT_HOST, port=None,
|
||||
headers=None, app='', body='', method='GET',
|
||||
wsgierrors=None, file_wrapper=None):
|
||||
|
||||
@@ -62,8 +63,13 @@ def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
|
||||
query_string (str, optional): The query string to simulate, without a
|
||||
leading '?' (default '')
|
||||
protocol (str, optional): The HTTP protocol to simulate
|
||||
(default 'HTTP/1.1')
|
||||
port (str, optional): The TCP port to simulate (default '80')
|
||||
(default 'HTTP/1.1'). If set 'HTTP/1.0', the Host header
|
||||
will not be added to the environment.
|
||||
scheme (str): URL scheme, either 'http' or 'https' (default 'http')
|
||||
host(str): Hostname for the request (default 'falconframework.org')
|
||||
port (str or int, optional): The TCP port to simulate. Defaults to
|
||||
the standard port used by the given scheme (i.e., 80 for 'http'
|
||||
and 443 for 'https').
|
||||
headers (dict or list, optional): Headers as a dict or an
|
||||
iterable collection of ``(key, value)`` tuples
|
||||
app (str): Value for the SCRIPT_NAME environ variable, described in
|
||||
@@ -88,6 +94,12 @@ def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
|
||||
if six.PY2 and isinstance(path, unicode): # pragma: nocover
|
||||
path = path.encode('utf-8')
|
||||
|
||||
scheme = scheme.lower()
|
||||
if port is None:
|
||||
port = '80' if scheme == 'http' else '443'
|
||||
else:
|
||||
port = str(port)
|
||||
|
||||
env = {
|
||||
'SERVER_PROTOCOL': protocol,
|
||||
'SERVER_SOFTWARE': 'gunicorn/0.17.0',
|
||||
@@ -99,10 +111,10 @@ def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
|
||||
'REMOTE_PORT': '65133',
|
||||
'RAW_URI': '/',
|
||||
'REMOTE_ADDR': '127.0.0.1',
|
||||
'SERVER_NAME': 'localhost',
|
||||
'SERVER_NAME': host,
|
||||
'SERVER_PORT': port,
|
||||
|
||||
'wsgi.url_scheme': 'http',
|
||||
'wsgi.url_scheme': scheme,
|
||||
'wsgi.input': body,
|
||||
'wsgi.errors': wsgierrors or sys.stderr,
|
||||
'wsgi.multithread': False,
|
||||
@@ -114,7 +126,16 @@ def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
|
||||
env['wsgi.file_wrapper'] = file_wrapper
|
||||
|
||||
if protocol != 'HTTP/1.0':
|
||||
env['HTTP_HOST'] = DEFAULT_HOST
|
||||
host_header = host
|
||||
|
||||
if scheme == 'https':
|
||||
if port != '443':
|
||||
host_header += ':' + port
|
||||
else:
|
||||
if port != '80':
|
||||
host_header += ':' + port
|
||||
|
||||
env['HTTP_HOST'] = host_header
|
||||
|
||||
content_length = body.seek(0, 2)
|
||||
body.seek(0)
|
||||
|
||||
@@ -311,3 +311,52 @@ def parse_query_string(query_string):
|
||||
params[k] = v
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def parse_host(host, default_port=None):
|
||||
"""Parse a canonical host:port string into parts.
|
||||
|
||||
Parse a host string (which may or may not contain a port) into
|
||||
parts, taking into account that the string may contain
|
||||
either a domain name or an IP address. In the latter case,
|
||||
both IPv4 and IPv6 addresses are supported.
|
||||
|
||||
Args:
|
||||
host (str): Host string to parse, optionally containing a
|
||||
port number.
|
||||
default_port (int, optional): Port number to return when
|
||||
the host string does not contain one (default ``None``).
|
||||
|
||||
Returns:
|
||||
tuple: A parsed (host, port) tuple from the given
|
||||
host string, with the port converted to an ``int``.
|
||||
If the string does not specify a port, `default_port` is
|
||||
used instead.
|
||||
|
||||
"""
|
||||
|
||||
# NOTE(kgriff): The value from the Host header may
|
||||
# contain a port, so check that and strip it if
|
||||
# necessary. This is complicated by the fact that
|
||||
# a hostname may be specified either as an IP address
|
||||
# or as a domain name, and in the case of IPv6 there
|
||||
# may be multiple colons in the string.
|
||||
|
||||
if host.startswith('['):
|
||||
# IPv6 address with a port
|
||||
pos = host.rfind(']:')
|
||||
if pos != -1:
|
||||
return (host[1:pos], int(host[pos + 2:]))
|
||||
else:
|
||||
return (host[1:-1], default_port)
|
||||
|
||||
pos = host.rfind(':')
|
||||
if (pos == -1) or (pos != host.find(':')):
|
||||
# Bare domain name or IP address
|
||||
return (host, default_port)
|
||||
|
||||
# NOTE(kgriffs): At this point we know that there was
|
||||
# only a single colon, so we should have an IPv4 address
|
||||
# or a domain name plus a port
|
||||
name, _, port = host.partition(':')
|
||||
return (name, int(port))
|
||||
|
||||
@@ -187,30 +187,6 @@ class TestHeaders(testing.TestBase):
|
||||
self.resource.req.get_header, 'X-Not-Found',
|
||||
required=True)
|
||||
|
||||
def test_prefer_host_header(self):
|
||||
self.simulate_request(self.test_route)
|
||||
|
||||
# Make sure we picked up host from HTTP_HOST, not SERVER_NAME
|
||||
host = self.resource.req.get_header('host')
|
||||
self.assertEqual(host, testing.DEFAULT_HOST)
|
||||
|
||||
def test_host_fallback(self):
|
||||
# Set protocol to 1.0 so that we won't get a host header
|
||||
self.simulate_request(self.test_route, protocol='HTTP/1.0')
|
||||
|
||||
# Make sure we picked up host from HTTP_HOST, not SERVER_NAME
|
||||
host = self.resource.req.get_header('host')
|
||||
self.assertEqual(host, 'localhost')
|
||||
|
||||
def test_host_fallback_port8000(self):
|
||||
# Set protocol to 1.0 so that we won't get a host header
|
||||
self.simulate_request(self.test_route, protocol='HTTP/1.0',
|
||||
port='8000')
|
||||
|
||||
# Make sure we picked up host from HTTP_HOST, not SERVER_NAME
|
||||
host = self.resource.req.get_header('host')
|
||||
self.assertEqual(host, 'localhost:8000')
|
||||
|
||||
def test_no_body_on_100(self):
|
||||
self.resource = StatusTestResource(falcon.HTTP_100)
|
||||
self.api.add_route('/1xx', self.resource)
|
||||
|
||||
@@ -11,7 +11,6 @@ class TestReqVars(testing.TestBase):
|
||||
self.qs = 'marker=deadbeef&limit=10'
|
||||
|
||||
self.headers = {
|
||||
'Host': 'falcon.example.com',
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Length': '4829',
|
||||
'Authorization': ''
|
||||
@@ -20,11 +19,10 @@ class TestReqVars(testing.TestBase):
|
||||
self.app = '/test'
|
||||
self.path = '/hello'
|
||||
self.relative_uri = self.path + '?' + self.qs
|
||||
self.uri = 'http://falcon.example.com' + self.app + self.relative_uri
|
||||
self.uri_noqs = 'http://falcon.example.com' + self.app + self.path
|
||||
|
||||
self.req = Request(testing.create_environ(
|
||||
app=self.app,
|
||||
port=8080,
|
||||
path='/hello',
|
||||
query_string=self.qs,
|
||||
headers=self.headers))
|
||||
@@ -45,6 +43,53 @@ class TestReqVars(testing.TestBase):
|
||||
def test_empty(self):
|
||||
self.assertIs(self.req.auth, None)
|
||||
|
||||
def test_host(self):
|
||||
self.assertEqual(self.req.host, testing.DEFAULT_HOST)
|
||||
|
||||
def test_subdomain(self):
|
||||
req = Request(testing.create_environ(
|
||||
host='com',
|
||||
path='/hello',
|
||||
headers=self.headers))
|
||||
self.assertIs(req.subdomain, None)
|
||||
|
||||
req = Request(testing.create_environ(
|
||||
host='example.com',
|
||||
path='/hello',
|
||||
headers=self.headers))
|
||||
self.assertEqual(req.subdomain, 'example')
|
||||
|
||||
req = Request(testing.create_environ(
|
||||
host='highwire.example.com',
|
||||
path='/hello',
|
||||
headers=self.headers))
|
||||
self.assertEqual(req.subdomain, 'highwire')
|
||||
|
||||
req = Request(testing.create_environ(
|
||||
host='lb01.dfw01.example.com',
|
||||
port=8080,
|
||||
path='/hello',
|
||||
headers=self.headers))
|
||||
self.assertEqual(req.subdomain, 'lb01')
|
||||
|
||||
# NOTE(kgriffs): Behavior for IP addresses is undefined,
|
||||
# so just make sure it doesn't blow up.
|
||||
req = Request(testing.create_environ(
|
||||
host='127.0.0.1',
|
||||
path='/hello',
|
||||
headers=self.headers))
|
||||
self.assertEqual(type(req.subdomain), str)
|
||||
|
||||
# NOTE(kgriffs): Test fallback to SERVER_NAME by using
|
||||
# HTTP 1.0, which will cause .create_environ to not set
|
||||
# HTTP_HOST.
|
||||
req = Request(testing.create_environ(
|
||||
protocol='HTTP/1.0',
|
||||
host='example.com',
|
||||
path='/hello',
|
||||
headers=self.headers))
|
||||
self.assertEqual(req.subdomain, 'example')
|
||||
|
||||
def test_reconstruct_url(self):
|
||||
req = self.req
|
||||
|
||||
@@ -54,18 +99,90 @@ class TestReqVars(testing.TestBase):
|
||||
path = req.path
|
||||
query_string = req.query_string
|
||||
|
||||
actual_url = ''.join([scheme, '://', host, app, path,
|
||||
'?', query_string])
|
||||
self.assertEqual(actual_url, self.uri)
|
||||
expected_uri = ''.join([scheme, '://', host, app, path,
|
||||
'?', query_string])
|
||||
|
||||
self.assertEqual(expected_uri, req.uri)
|
||||
|
||||
def test_uri(self):
|
||||
self.assertEqual(self.req.url, self.uri)
|
||||
uri = ('http://' + testing.DEFAULT_HOST + ':8080' +
|
||||
self.app + self.relative_uri)
|
||||
|
||||
self.assertEqual(self.req.url, uri)
|
||||
|
||||
# NOTE(kgriffs): Call twice to check caching works
|
||||
self.assertEqual(self.req.uri, self.uri)
|
||||
self.assertEqual(self.req.uri, self.uri)
|
||||
self.assertEqual(self.req.uri, uri)
|
||||
self.assertEqual(self.req.uri, uri)
|
||||
|
||||
self.assertEqual(self.req_noqs.uri, self.uri_noqs)
|
||||
uri_noqs = ('http://' + testing.DEFAULT_HOST + self.app + self.path)
|
||||
self.assertEqual(self.req_noqs.uri, uri_noqs)
|
||||
|
||||
def test_uri_http_1_0(self):
|
||||
# =======================================================
|
||||
# HTTP, 80
|
||||
# =======================================================
|
||||
req = Request(testing.create_environ(
|
||||
protocol='HTTP/1.0',
|
||||
app=self.app,
|
||||
port=80,
|
||||
path='/hello',
|
||||
query_string=self.qs,
|
||||
headers=self.headers))
|
||||
|
||||
uri = ('http://' + testing.DEFAULT_HOST +
|
||||
self.app + self.relative_uri)
|
||||
|
||||
self.assertEqual(req.uri, uri)
|
||||
|
||||
# =======================================================
|
||||
# HTTP, 80
|
||||
# =======================================================
|
||||
req = Request(testing.create_environ(
|
||||
protocol='HTTP/1.0',
|
||||
app=self.app,
|
||||
port=8080,
|
||||
path='/hello',
|
||||
query_string=self.qs,
|
||||
headers=self.headers))
|
||||
|
||||
uri = ('http://' + testing.DEFAULT_HOST + ':8080' +
|
||||
self.app + self.relative_uri)
|
||||
|
||||
self.assertEqual(req.uri, uri)
|
||||
|
||||
# =======================================================
|
||||
# HTTP, 80
|
||||
# =======================================================
|
||||
req = Request(testing.create_environ(
|
||||
protocol='HTTP/1.0',
|
||||
scheme='https',
|
||||
app=self.app,
|
||||
port=443,
|
||||
path='/hello',
|
||||
query_string=self.qs,
|
||||
headers=self.headers))
|
||||
|
||||
uri = ('https://' + testing.DEFAULT_HOST +
|
||||
self.app + self.relative_uri)
|
||||
|
||||
self.assertEqual(req.uri, uri)
|
||||
|
||||
# =======================================================
|
||||
# HTTP, 80
|
||||
# =======================================================
|
||||
req = Request(testing.create_environ(
|
||||
protocol='HTTP/1.0',
|
||||
scheme='https',
|
||||
app=self.app,
|
||||
port=22,
|
||||
path='/hello',
|
||||
query_string=self.qs,
|
||||
headers=self.headers))
|
||||
|
||||
uri = ('https://' + testing.DEFAULT_HOST + ':22' +
|
||||
self.app + self.relative_uri)
|
||||
|
||||
self.assertEqual(req.uri, uri)
|
||||
|
||||
def test_relative_uri(self):
|
||||
self.assertEqual(self.req.relative_uri, self.app + self.relative_uri)
|
||||
|
||||
@@ -185,6 +185,50 @@ class TestFalconUtils(testtools.TestCase):
|
||||
actual = uri.decode(case)
|
||||
self.assertEqual(expect, actual)
|
||||
|
||||
def test_parse_host(self):
|
||||
self.assertEqual(uri.parse_host('::1'), ('::1', None))
|
||||
self.assertEqual(uri.parse_host('2001:ODB8:AC10:FE01::'),
|
||||
('2001:ODB8:AC10:FE01::', None))
|
||||
self.assertEqual(
|
||||
uri.parse_host('2001:ODB8:AC10:FE01::', default_port=80),
|
||||
('2001:ODB8:AC10:FE01::', 80))
|
||||
|
||||
ipv6_addr = '2001:4801:1221:101:1c10::f5:116'
|
||||
|
||||
self.assertEqual(uri.parse_host(ipv6_addr), (ipv6_addr, None))
|
||||
self.assertEqual(uri.parse_host('[' + ipv6_addr + ']'),
|
||||
(ipv6_addr, None))
|
||||
self.assertEqual(uri.parse_host('[' + ipv6_addr + ']:28080'),
|
||||
(ipv6_addr, 28080))
|
||||
self.assertEqual(uri.parse_host('[' + ipv6_addr + ']:8080'),
|
||||
(ipv6_addr, 8080))
|
||||
self.assertEqual(uri.parse_host('[' + ipv6_addr + ']:123'),
|
||||
(ipv6_addr, 123))
|
||||
self.assertEqual(uri.parse_host('[' + ipv6_addr + ']:42'),
|
||||
(ipv6_addr, 42))
|
||||
|
||||
self.assertEqual(uri.parse_host('173.203.44.122'),
|
||||
('173.203.44.122', None))
|
||||
self.assertEqual(uri.parse_host('173.203.44.122', default_port=80),
|
||||
('173.203.44.122', 80))
|
||||
self.assertEqual(uri.parse_host('173.203.44.122:27070'),
|
||||
('173.203.44.122', 27070))
|
||||
self.assertEqual(uri.parse_host('173.203.44.122:123'),
|
||||
('173.203.44.122', 123))
|
||||
self.assertEqual(uri.parse_host('173.203.44.122:42'),
|
||||
('173.203.44.122', 42))
|
||||
|
||||
self.assertEqual(uri.parse_host('example.com'),
|
||||
('example.com', None))
|
||||
self.assertEqual(uri.parse_host('example.com', default_port=443),
|
||||
('example.com', 443))
|
||||
self.assertEqual(uri.parse_host('falcon.example.com'),
|
||||
('falcon.example.com', None))
|
||||
self.assertEqual(uri.parse_host('falcon.example.com:9876'),
|
||||
('falcon.example.com', 9876))
|
||||
self.assertEqual(uri.parse_host('falcon.example.com:42'),
|
||||
('falcon.example.com', 42))
|
||||
|
||||
|
||||
class TestFalconTesting(falcon.testing.TestBase):
|
||||
"""Catch some uncommon branches not covered elsewhere."""
|
||||
|
||||
Reference in New Issue
Block a user