feat(Response): Add an option for disabling secure cookies for testing (#991)
This commit is contained in:
committed by
John Vrbanac
parent
8be7ff7057
commit
673bb2e136
@@ -18,4 +18,6 @@ standard-compliant WSGI server.
|
||||
.. autoclass:: falcon.RequestOptions
|
||||
:members:
|
||||
|
||||
.. autoclass:: falcon.ResponseOptions
|
||||
:members:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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__ = (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('/')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user