Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ Proxy middleware
HttpProxys support custom middleware for preprocessing data from
downstream to be sent to upstream endpoints and for preprocessing
response data before it is sent back downstream. ``X-Forwarded-Host``,
``X-Forwarded-For``, ``X-Forwarded-Proto`` and the ``ProxyPassRevere``
``X-Forwarded-For``, ``X-Forwarded-Proto``, ``ForwardSetCookie`` and the ``ProxyPassRevere``
functionality area all implemented as middleware.

HttProxy views are configured to execute particular middleware by
Expand Down
57 changes: 57 additions & 0 deletions djproxy/proxy_middleware.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import re

# The Cookie module has been renamed to http.cookies in Python 3.0.
# We use its equivalent re_path to maintain compatibility
# with older versions of django
try:
from http.cookies import SimpleCookie
except ImportError as exception:
from Cookie import SimpleCookie

from .util import import_string


Expand Down Expand Up @@ -101,6 +109,55 @@ def process_request(self, proxy, request, **kwargs):
return kwargs


class ForwardSetCookie(object):
"""
Handles the transmission of multiple cookies returned by a server as multiple set-cookie headers.

Request merges the set-cookie headers into one. The default behavior is ok
if the server returns only one cookie per http response.

The browser will receive only one cookie.

{
Set-Cookie: hello=world; Expires=Wed, 21 Oct 2015 07:28:00 GMT, world=hello
}

instead

{
Set-Cookie: hello=world; Expires=Wed, 21 Oct 2015 07:28:00 GMT
Set-Cookie: world=hello
}

more about this behavior

* https://github.com/urllib3/urllib3/commit/d8013cb111644a06eb5cb9bccce174a1a996078d
* https://stackoverflow.com/a/57131320
* https://github.com/psf/requests/issues/3957#issuecomment-1047539652
"""
def process_response(self, proxy, request, upstream_response, response):
if 'set-cookie' in response:
# The set-cookie headers are well present in the headers of urlib3 forwarded by request
# On its own interface, request has merged those cookies in one single header
for header, value in upstream_response.raw.headers.items():
if header.lower() == 'set-cookie':
cookies = SimpleCookie(value)
for key in cookies.keys():
cookie = cookies.get(key)
response.set_cookie(cookie.key,
value=cookie.value,
expires=cookie.get('expires'),
path=cookie.get('path'),
domain=cookie.get('domain'),
secure=True if cookie.get('secure') else False,
httponly=True if cookie.get('httponly') else False)

# remove the default header added by request
del response['set-cookie']

return response


class ProxyPassReverse(object):

"""Applies reverse url rules to location headers like ProxyPassReverse.
Expand Down
72 changes: 71 additions & 1 deletion tests/middleware_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from mock import Mock
from unittest2 import TestCase
from six import iteritems
from urllib3 import HTTPResponse

from djproxy.proxy_middleware import AddXFF, AddXFH, AddXFP, ProxyPassReverse
from djproxy.proxy_middleware import AddXFF, AddXFH, AddXFP, ProxyPassReverse, ForwardSetCookie
from djproxy.request import DownstreamRequest


Expand Down Expand Up @@ -49,6 +50,75 @@ def test_sets_XFP_header_to_http_if_request_is_not_secure(self):
kwargs['headers']['X-Forwarded-Proto'], 'http')


class TestForwardSetCookie(TestCase):

def setUp(self):
self.request = DownstreamRequest(RequestFactory().get('/'))
self.upstream_response = {
'URI': 'http://upstream.tld/go/',
'Location': 'http://upstream.tld/go/',
'Content-Location': 'http://upstream.tld/go/',
'Location-Foo': 'http://upstream.tld/go/'
}
self.proxy = Mock()

self._tested = ForwardSetCookie()

def test_middleware_should_do_nothing_when_there_is_no_header_set_cookie(self):
# Assign
response = HttpResponse()

# Acts
response = self._tested.process_response(self.proxy, self.request, self.upstream_response, response)

# Assert
self.assertNotIn('set-cookie', response)

def test_middleware_should_move_cookie_into_cookies_and_remove_set_cookie_header(self):
# Assign
response = HttpResponse()
response['set-cookie'] = 'key=hello'
upstream_response = Mock()
upstream_response.raw = HTTPResponse()
upstream_response.raw.headers.add('set-cookie', 'key=hello')


# Acts
response = self._tested.process_response(self.proxy, self.request, upstream_response, response)

# Assert
cookie = response.cookies.get('key')
self.assertEqual('hello', cookie.value)
self.assertNotIn('set-cookie', response)

def test_middleware_should_move_multiple_cookies_into_cookies_and_remove_set_cookie_header(self):
# Assign
response = HttpResponse()
response['set-cookie'] = 'hello=world; Expires=Wed, 21 Oct 2015 07:28:00 GMT, world=hello'
upstream_response = Mock()
upstream_response.raw = HTTPResponse()
upstream_response.raw.headers.add('set-cookie', 'hello=world; Expires=Wed, 21 Oct 2015 07:28:00 GMT')
upstream_response.raw.headers.add('set-cookie', 'hello1=world; Domain=somecompany.co.uk')
upstream_response.raw.headers.add('set-cookie', 'hello2=world; Secure; Path=/')
upstream_response.raw.headers.add('set-cookie', 'world=hello')

# Acts
response = self._tested.process_response(self.proxy, self.request, upstream_response, response) # typing: HttpResponse

# Assert
cookie = response.cookies.get('hello')
self.assertEqual('world', cookie.value)

cookie = response.cookies.get('hello1')
self.assertEqual('somecompany.co.uk', cookie.get('domain'))

cookie = response.cookies.get('hello2')
self.assertEqual('/', cookie.get('path'))
self.assertEqual(True, cookie.get('secure'))

self.assertNotIn('set-cookie', response)


class ProxyPassReverseTest(TestCase):
def setUp(self):
self.request = DownstreamRequest(RequestFactory().get('/'))
Expand Down