feat(Response): Add an option for disabling secure cookies for testing (#991)

This commit is contained in:
Kurt Griffiths
2017-01-26 20:32:05 -07:00
committed by John Vrbanac
parent 8be7ff7057
commit 673bb2e136
7 changed files with 75 additions and 17 deletions

View File

@@ -18,4 +18,6 @@ standard-compliant WSGI server.
.. autoclass:: falcon.RequestOptions
:members:
.. autoclass:: falcon.ResponseOptions
:members:

View File

@@ -98,14 +98,15 @@ the request.
.. warning::
For this attribute to be effective, your application will need to
enforce HTTPS when setting the cookie, as well as in all
subsequent requests that require the cookie to be sent back from
the client.
For this attribute to be effective, your web server or load
balancer will need to enforce HTTPS when setting the cookie, as
well as in all subsequent requests that require the cookie to be
sent back from the client.
When running your application in a development environment, you can
disable this behavior by passing `secure=False` to
:py:meth:`~.Response.set_cookie`. This lets you test your app locally
disable this default behavior by setting
:py:attr:`~.ResponseOptions.secure_cookies_by_default` to ``False``
via :any:`API.resp_options`. This lets you test your app locally
without having to set up TLS. You can make this option configurable to
easily switch between development and production environments.

View File

@@ -57,4 +57,4 @@ from falcon.util import * # NOQA
from falcon.hooks import before, after # NOQA
from falcon.request import Request, RequestOptions # NOQA
from falcon.response import Response # NOQA
from falcon.response import Response, ResponseOptions # NOQA

View File

@@ -23,7 +23,7 @@ from falcon.http_error import HTTPError
from falcon.http_status import HTTPStatus
from falcon.request import Request, RequestOptions
import falcon.responders
from falcon.response import Response
from falcon.response import Response, ResponseOptions
import falcon.status_codes as status
from falcon.util.misc import get_argnames
@@ -110,6 +110,8 @@ class API(object):
Attributes:
req_options: A set of behavioral options related to incoming
requests. See also: :py:class:`~.RequestOptions`
resp_options: A set of behavioral options related to outgoing
responses. See also: :py:class:`~.ResponseOptions`
"""
# PERF(kgriffs): Reference via self since that is faster than
@@ -125,7 +127,7 @@ class API(object):
__slots__ = ('_request_type', '_response_type',
'_error_handlers', '_media_type', '_router', '_sinks',
'_serialize_error', 'req_options',
'_serialize_error', 'req_options', 'resp_options',
'_middleware', '_independent_middleware')
def __init__(self, media_type=DEFAULT_MEDIA_TYPE,
@@ -147,7 +149,9 @@ class API(object):
self._error_handlers = []
self._serialize_error = helpers.default_serialize_error
self.req_options = RequestOptions()
self.resp_options = ResponseOptions()
# NOTE(kgriffs): Add default error handlers
self.add_error_handler(falcon.HTTPError, self._http_error_handler)
@@ -170,7 +174,7 @@ class API(object):
"""
req = self._request_type(env, options=self.req_options)
resp = self._response_type()
resp = self._response_type(options=self.resp_options)
resource = None
params = {}

View File

@@ -286,12 +286,11 @@ class Request(object):
string, the value mapped to that parameter key will be a list of
all the values in the order seen.
options (dict): Set of global options passed from the API handler.
cookies (dict):
A dict of name/value cookie pairs.
See also: :ref:`Getting Cookies <getting-cookies>`
options (dict): Set of global options passed from the API handler.
"""
__slots__ = (

View File

@@ -44,6 +44,9 @@ class Response(object):
Note:
`Response` is not meant to be instantiated directly by responders.
Keyword Arguments:
options (dict): Set of global options passed from the API handler.
Attributes:
status (str): HTTP status line (e.g., '200 OK'). Falcon requires the
full status line, not just the code (e.g., 200). This design
@@ -109,6 +112,8 @@ class Response(object):
opposed to a class), the function is called like a method of
the current Response instance. Therefore the first argument is
the Response instance itself (self).
options (dict): Set of global options passed from the API handler.
"""
__slots__ = (
@@ -120,16 +125,19 @@ class Response(object):
'stream',
'stream_len',
'context',
'options',
'__dict__',
)
# Child classes may override this
context_type = None
def __init__(self):
def __init__(self, options=None):
self.status = '200 OK'
self._headers = {}
self.options = ResponseOptions() if options is None else options
# NOTE(tbug): will be set to a SimpleCookie object
# when cookie is set via set_cookie
self._cookies = None
@@ -164,7 +172,7 @@ class Response(object):
self.stream_len = stream_len
def set_cookie(self, name, value, expires=None, max_age=None,
domain=None, path=None, secure=True, http_only=True):
domain=None, path=None, secure=None, http_only=True):
"""Set a response cookie.
Note:
@@ -223,6 +231,12 @@ class Response(object):
(default: ``True``). This prevents attackers from
reading sensitive cookie data.
Note:
The default value for this argument is normally
``True``, but can be modified by setting
:py:attr:`~.ResponseOptions.secure_cookies_by_default`
via :any:`API.resp_options`.
Warning:
For the `secure` cookie attribute to be effective,
your application will need to enforce HTTPS.
@@ -295,8 +309,13 @@ class Response(object):
if path:
self._cookies[name]['path'] = path
if secure:
self._cookies[name]['secure'] = secure
if secure is None:
is_secure = self.options.secure_cookies_by_default
else:
is_secure = secure
if is_secure:
self._cookies[name]['secure'] = True
if http_only:
self._cookies[name]['httponly'] = http_only
@@ -716,3 +735,24 @@ class Response(object):
items += [('set-cookie', c.OutputString())
for c in self._cookies.values()]
return items
class ResponseOptions(object):
"""Defines a set of configurable response options.
An instance of this class is exposed via :any:`API.resp_options` for
configuring certain :py:class:`~.Response` behaviors.
Attributes:
secure_cookies_by_default (bool): Set to ``False`` in development
environments to make the `secure` attribute for all cookies
default to ``False``. This can make testing easier by
not requiring HTTPS. Note, however, that this setting can
be overridden via `set_cookie()`'s `secure` kwarg.
"""
__slots__ = (
'secure_cookies_by_default',
)
def __init__(self):
self.secure_cookies_by_default = True

View File

@@ -58,7 +58,7 @@ class CookieResourceMaxAgeFloatString:
'foostring', 'bar', max_age='15', secure=False, http_only=False)
@pytest.fixture(scope='module')
@pytest.fixture()
def client():
app = falcon.API()
app.add_route('/', CookieResource())
@@ -92,6 +92,18 @@ def test_response_base_case(client):
assert cookie.secure
def test_response_disable_secure_globally(client):
client.app.resp_options.secure_cookies_by_default = False
result = client.simulate_get('/')
cookie = result.cookies['foo']
assert not cookie.secure
client.app.resp_options.secure_cookies_by_default = True
result = client.simulate_get('/')
cookie = result.cookies['foo']
assert cookie.secure
def test_response_complex_case(client):
result = client.simulate_head('/')