Add support for PROXY protocol v1 (only)
...to the proxy-server. The point is to allow the Swift proxy server to log accurate client IP addresses when there is a proxy or SSL-terminator between the client and the Swift proxy server. Example servers supporting this PROXY protocol: stud (v1 only) stunnel haproxy hitch (v2 only) varnish See http://www.haproxy.org/download/1.7/doc/proxy-protocol.txt The feature is enabled by adding this to your proxy config file: [app:proxy-server] use = egg:swift#proxy ... require_proxy_protocol = true The protocol specification states: The receiver MUST be configured to only receive the protocol described in this specification and MUST not try to guess whether the protocol header is present or not. so valid deployments are: 1) require_proxy_protocol = false (or missing; default is false) and NOT behind a proxy that adds or proxies existing PROXY lines. 2) require_proxy_protocol = true and IS behind a proxy that adds or proxies existing PROXY lines. Specifically, in the default configuration, one cannot send the swift proxy PROXY lines (no change from before this patch). When this feature is enabled, one _must_ send PROXY lines. Change-Id: Icb88902f0a89b8d980c860be032d5e822845d03a
This commit is contained in:
parent
8403ca3915
commit
661838d968
@ -109,6 +109,15 @@ use = egg:swift#proxy
|
||||
# set log_level = INFO
|
||||
# set log_address = /dev/log
|
||||
#
|
||||
# When deployed behind a proxy, load balancer, or SSL terminator that is
|
||||
# configured to speak the human-readable (v1) PROXY protocol (see
|
||||
# http://www.haproxy.org/download/1.7/doc/proxy-protocol.txt), you should set
|
||||
# this option to true. The proxy-server will populate the client connection
|
||||
# information using the PROXY protocol and reject any connection missing a
|
||||
# valid PROXY line with a 400. Only v1 (human-readable) of the PROXY protocol
|
||||
# is supported.
|
||||
# require_proxy_protocol = false
|
||||
#
|
||||
# log_handoffs = true
|
||||
# recheck_account_existence = 60
|
||||
# recheck_container_existence = 60
|
||||
|
@ -420,18 +420,99 @@ def load_app_config(conf_file):
|
||||
return app_conf
|
||||
|
||||
|
||||
class SwiftHttpProtocol(wsgi.HttpProtocol):
|
||||
default_request_version = "HTTP/1.0"
|
||||
|
||||
def log_request(self, *a):
|
||||
"""
|
||||
Turn off logging requests by the underlying WSGI software.
|
||||
"""
|
||||
pass
|
||||
|
||||
def log_message(self, f, *a):
|
||||
"""
|
||||
Redirect logging other messages by the underlying WSGI software.
|
||||
"""
|
||||
logger = getattr(self.server.app, 'logger', None) or self.server.log
|
||||
logger.error('ERROR WSGI: ' + f, *a)
|
||||
|
||||
|
||||
class SwiftHttpProxiedProtocol(SwiftHttpProtocol):
|
||||
"""
|
||||
Protocol object that speaks HTTP, including multiple requests, but with
|
||||
a single PROXY line as the very first thing coming in over the socket.
|
||||
This is so we can learn what the client's IP address is when Swift is
|
||||
behind a TLS terminator, like hitch, that does not understand HTTP and
|
||||
so cannot add X-Forwarded-For or other similar headers.
|
||||
|
||||
See http://www.haproxy.org/download/1.7/doc/proxy-protocol.txt for
|
||||
protocol details.
|
||||
"""
|
||||
def handle_error(self, connection_line):
|
||||
if not six.PY2:
|
||||
connection_line = connection_line.decode('latin-1')
|
||||
|
||||
# No further processing will proceed on this connection under any
|
||||
# circumstances. We always send the request into the superclass to
|
||||
# handle any cleanup - this ensures that the request will not be
|
||||
# processed.
|
||||
self.rfile.close()
|
||||
# We don't really have any confidence that an HTTP Error will be
|
||||
# processable by the client as our transmission broken down between
|
||||
# ourselves and our gateway proxy before processing the client
|
||||
# protocol request. Hopefully the operator will know what to do!
|
||||
msg = 'Invalid PROXY line %r' % connection_line
|
||||
self.log_message(msg)
|
||||
# Even assuming HTTP we don't even known what version of HTTP the
|
||||
# client is sending? This entire endeavor seems questionable.
|
||||
self.request_version = self.default_request_version
|
||||
# appease http.server
|
||||
self.command = 'PROXY'
|
||||
self.send_error(400, msg)
|
||||
|
||||
def handle(self):
|
||||
"""Handle multiple requests if necessary."""
|
||||
# ensure the opening line for the connection is a valid PROXY protcol
|
||||
# line; this is the only IO we do on this connection before any
|
||||
# additional wrapping further pollutes the raw socket.
|
||||
connection_line = self.rfile.readline(self.server.url_length_limit)
|
||||
|
||||
if connection_line.startswith(b'PROXY'):
|
||||
proxy_parts = connection_line.split(b' ')
|
||||
if len(proxy_parts) >= 2 and proxy_parts[0] == b'PROXY':
|
||||
if proxy_parts[1] in (b'TCP4', b'TCP6') and \
|
||||
len(proxy_parts) == 6:
|
||||
if six.PY2:
|
||||
self.client_address = (proxy_parts[2], proxy_parts[4])
|
||||
else:
|
||||
self.client_address = (
|
||||
proxy_parts[2].decode('latin-1'),
|
||||
proxy_parts[4].decode('latin-1'))
|
||||
elif proxy_parts[1].startswith(b'UNKNOWN'):
|
||||
# "UNKNOWN", in PROXY protocol version 1, means "not
|
||||
# TCP4 or TCP6". This includes completely legitimate
|
||||
# things like QUIC or Unix domain sockets. The PROXY
|
||||
# protocol (section 2.1) states that the receiver
|
||||
# (that's us) MUST ignore anything after "UNKNOWN" and
|
||||
# before the CRLF, essentially discarding the first
|
||||
# line.
|
||||
pass
|
||||
else:
|
||||
self.handle_error(connection_line)
|
||||
else:
|
||||
self.handle_error(connection_line)
|
||||
else:
|
||||
self.handle_error(connection_line)
|
||||
|
||||
return SwiftHttpProtocol.handle(self)
|
||||
|
||||
|
||||
def run_server(conf, logger, sock, global_conf=None):
|
||||
# Ensure TZ environment variable exists to avoid stat('/etc/localtime') on
|
||||
# some platforms. This locks in reported times to UTC.
|
||||
os.environ['TZ'] = 'UTC+0'
|
||||
time.tzset()
|
||||
|
||||
wsgi.HttpProtocol.default_request_version = "HTTP/1.0"
|
||||
# Turn off logging requests by the underlying WSGI software.
|
||||
wsgi.HttpProtocol.log_request = lambda *a: None
|
||||
# Redirect logging other messages by the underlying WSGI software.
|
||||
wsgi.HttpProtocol.log_message = \
|
||||
lambda s, f, *a: logger.error('ERROR WSGI: ' + f % a)
|
||||
wsgi.WRITE_TIMEOUT = int(conf.get('client_timeout') or 60)
|
||||
|
||||
eventlet.hubs.use_hub(get_hub())
|
||||
@ -451,15 +532,25 @@ def run_server(conf, logger, sock, global_conf=None):
|
||||
app = loadapp(conf['__file__'], global_conf=global_conf)
|
||||
max_clients = int(conf.get('max_clients', '1024'))
|
||||
pool = RestrictedGreenPool(size=max_clients)
|
||||
|
||||
# Select which protocol class to use (normal or one expecting PROXY
|
||||
# protocol)
|
||||
if config_true_value(conf.get('require_proxy_protocol', 'no')):
|
||||
protocol_class = SwiftHttpProxiedProtocol
|
||||
else:
|
||||
protocol_class = SwiftHttpProtocol
|
||||
|
||||
server_kwargs = {
|
||||
'custom_pool': pool,
|
||||
'protocol': protocol_class,
|
||||
}
|
||||
# Disable capitalizing headers in Eventlet if possible. This is
|
||||
# necessary for the AWS SDK to work with swift3 middleware.
|
||||
argspec = inspect.getargspec(wsgi.server)
|
||||
if 'capitalize_response_headers' in argspec.args:
|
||||
server_kwargs['capitalize_response_headers'] = False
|
||||
try:
|
||||
# Disable capitalizing headers in Eventlet if possible. This is
|
||||
# necessary for the AWS SDK to work with swift3 middleware.
|
||||
argspec = inspect.getargspec(wsgi.server)
|
||||
if 'capitalize_response_headers' in argspec.args:
|
||||
wsgi.server(sock, app, wsgi_logger, custom_pool=pool,
|
||||
capitalize_response_headers=False)
|
||||
else:
|
||||
wsgi.server(sock, app, wsgi_logger, custom_pool=pool)
|
||||
wsgi.server(sock, app, wsgi_logger, **server_kwargs)
|
||||
except socket.error as err:
|
||||
if err[0] != errno.EINVAL:
|
||||
raise
|
||||
|
@ -53,7 +53,8 @@ from test.unit import SkipTest
|
||||
|
||||
from swift.common import constraints, utils, ring, storage_policy
|
||||
from swift.common.ring import Ring
|
||||
from swift.common.wsgi import monkey_patch_mimetools, loadapp
|
||||
from swift.common.wsgi import (
|
||||
monkey_patch_mimetools, loadapp, SwiftHttpProtocol)
|
||||
from swift.common.utils import config_true_value, split_path
|
||||
from swift.account import server as account_server
|
||||
from swift.container import server as container_server
|
||||
@ -626,13 +627,6 @@ def in_process_setup(the_object_server=object_server):
|
||||
'port': con2lis.getsockname()[1]}], 30),
|
||||
f)
|
||||
|
||||
eventlet.wsgi.HttpProtocol.default_request_version = "HTTP/1.0"
|
||||
# Turn off logging requests by the underlying WSGI software.
|
||||
eventlet.wsgi.HttpProtocol.log_request = lambda *a: None
|
||||
logger = utils.get_logger(config, 'wsgi-server', log_route='wsgi')
|
||||
# Redirect logging other messages by the underlying WSGI software.
|
||||
eventlet.wsgi.HttpProtocol.log_message = \
|
||||
lambda s, f, *a: logger.error('ERROR WSGI: ' + f % a)
|
||||
# Default to only 4 seconds for in-process functional test runs
|
||||
eventlet.wsgi.WRITE_TIMEOUT = 4
|
||||
|
||||
@ -659,7 +653,9 @@ def in_process_setup(the_object_server=object_server):
|
||||
]
|
||||
|
||||
if show_debug_logs:
|
||||
logger = debug_logger('proxy')
|
||||
logger = get_logger_name('proxy')
|
||||
else:
|
||||
logger = utils.get_logger(config, 'wsgi-server', log_route='wsgi')
|
||||
|
||||
def get_logger(name, *args, **kwargs):
|
||||
return logger
|
||||
@ -675,13 +671,19 @@ def in_process_setup(the_object_server=object_server):
|
||||
nl = utils.NullLogger()
|
||||
global proxy_srv
|
||||
proxy_srv = prolis
|
||||
prospa = eventlet.spawn(eventlet.wsgi.server, prolis, app, nl)
|
||||
acc1spa = eventlet.spawn(eventlet.wsgi.server, acc1lis, acc1srv, nl)
|
||||
acc2spa = eventlet.spawn(eventlet.wsgi.server, acc2lis, acc2srv, nl)
|
||||
con1spa = eventlet.spawn(eventlet.wsgi.server, con1lis, con1srv, nl)
|
||||
con2spa = eventlet.spawn(eventlet.wsgi.server, con2lis, con2srv, nl)
|
||||
prospa = eventlet.spawn(eventlet.wsgi.server, prolis, app, nl,
|
||||
protocol=SwiftHttpProtocol)
|
||||
acc1spa = eventlet.spawn(eventlet.wsgi.server, acc1lis, acc1srv, nl,
|
||||
protocol=SwiftHttpProtocol)
|
||||
acc2spa = eventlet.spawn(eventlet.wsgi.server, acc2lis, acc2srv, nl,
|
||||
protocol=SwiftHttpProtocol)
|
||||
con1spa = eventlet.spawn(eventlet.wsgi.server, con1lis, con1srv, nl,
|
||||
protocol=SwiftHttpProtocol)
|
||||
con2spa = eventlet.spawn(eventlet.wsgi.server, con2lis, con2srv, nl,
|
||||
protocol=SwiftHttpProtocol)
|
||||
|
||||
objspa = [eventlet.spawn(eventlet.wsgi.server, objsrv[0], objsrv[1], nl)
|
||||
objspa = [eventlet.spawn(eventlet.wsgi.server, objsrv[0], objsrv[1], nl,
|
||||
protocol=SwiftHttpProtocol)
|
||||
for objsrv in objsrvs]
|
||||
|
||||
global _test_coros
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
"""Tests for swift.common.wsgi"""
|
||||
|
||||
from argparse import Namespace
|
||||
import errno
|
||||
import logging
|
||||
import socket
|
||||
@ -22,6 +23,9 @@ import unittest
|
||||
import os
|
||||
from textwrap import dedent
|
||||
from collections import defaultdict
|
||||
import types
|
||||
|
||||
import eventlet.wsgi
|
||||
|
||||
import six
|
||||
from six import BytesIO
|
||||
@ -480,8 +484,6 @@ class TestWSGI(unittest.TestCase):
|
||||
logger = logging.getLogger('test')
|
||||
sock = listen_zero()
|
||||
wsgi.run_server(conf, logger, sock)
|
||||
self.assertEqual('HTTP/1.0',
|
||||
_wsgi.HttpProtocol.default_request_version)
|
||||
self.assertEqual(30, _wsgi.WRITE_TIMEOUT)
|
||||
_wsgi_evt.hubs.use_hub.assert_called_with(utils.get_hub())
|
||||
_wsgi_evt.debug.hub_exceptions.assert_called_with(False)
|
||||
@ -495,6 +497,61 @@ class TestWSGI(unittest.TestCase):
|
||||
self.assertTrue('custom_pool' in kwargs)
|
||||
self.assertEqual(1000, kwargs['custom_pool'].size)
|
||||
|
||||
proto_class = kwargs['protocol']
|
||||
self.assertEqual(proto_class, wsgi.SwiftHttpProtocol)
|
||||
self.assertEqual('HTTP/1.0', proto_class.default_request_version)
|
||||
|
||||
def test_run_server_proxied(self):
|
||||
config = """
|
||||
[DEFAULT]
|
||||
client_timeout = 30
|
||||
max_clients = 1000
|
||||
swift_dir = TEMPDIR
|
||||
|
||||
[pipeline:main]
|
||||
pipeline = proxy-server
|
||||
|
||||
[app:proxy-server]
|
||||
use = egg:swift#proxy
|
||||
# these "set" values override defaults
|
||||
set client_timeout = 20
|
||||
set max_clients = 10
|
||||
require_proxy_protocol = true
|
||||
"""
|
||||
|
||||
contents = dedent(config)
|
||||
with temptree(['proxy-server.conf']) as t:
|
||||
conf_file = os.path.join(t, 'proxy-server.conf')
|
||||
with open(conf_file, 'w') as f:
|
||||
f.write(contents.replace('TEMPDIR', t))
|
||||
_fake_rings(t)
|
||||
with mock.patch('swift.proxy.server.Application.'
|
||||
'modify_wsgi_pipeline'), \
|
||||
mock.patch('swift.common.wsgi.wsgi') as _wsgi, \
|
||||
mock.patch('swift.common.wsgi.eventlet') as _eventlet, \
|
||||
mock.patch('swift.common.wsgi.inspect'):
|
||||
conf = wsgi.appconfig(conf_file,
|
||||
name='proxy-server')
|
||||
logger = logging.getLogger('test')
|
||||
sock = listen_zero()
|
||||
wsgi.run_server(conf, logger, sock)
|
||||
self.assertEqual(20, _wsgi.WRITE_TIMEOUT)
|
||||
_eventlet.hubs.use_hub.assert_called_with(utils.get_hub())
|
||||
_eventlet.debug.hub_exceptions.assert_called_with(False)
|
||||
self.assertTrue(_wsgi.server.called)
|
||||
args, kwargs = _wsgi.server.call_args
|
||||
server_sock, server_app, server_logger = args
|
||||
self.assertEqual(sock, server_sock)
|
||||
self.assertTrue(isinstance(server_app, swift.proxy.server.Application))
|
||||
self.assertEqual(20, server_app.client_timeout)
|
||||
self.assertTrue(isinstance(server_logger, wsgi.NullLogger))
|
||||
self.assertTrue('custom_pool' in kwargs)
|
||||
self.assertEqual(10, kwargs['custom_pool'].size)
|
||||
|
||||
proto_class = kwargs['protocol']
|
||||
self.assertEqual(proto_class, wsgi.SwiftHttpProxiedProtocol)
|
||||
self.assertEqual('HTTP/1.0', proto_class.default_request_version)
|
||||
|
||||
def test_run_server_with_latest_eventlet(self):
|
||||
config = """
|
||||
[DEFAULT]
|
||||
@ -530,6 +587,9 @@ class TestWSGI(unittest.TestCase):
|
||||
self.assertTrue(_wsgi.server.called)
|
||||
args, kwargs = _wsgi.server.call_args
|
||||
self.assertEqual(kwargs.get('capitalize_response_headers'), False)
|
||||
self.assertTrue('protocol' in kwargs)
|
||||
self.assertEqual('HTTP/1.0',
|
||||
kwargs['protocol'].default_request_version)
|
||||
|
||||
def test_run_server_conf_dir(self):
|
||||
config_dir = {
|
||||
@ -566,8 +626,6 @@ class TestWSGI(unittest.TestCase):
|
||||
wsgi.run_server(conf, logger, sock)
|
||||
self.assertTrue(os.environ['TZ'] is not '')
|
||||
|
||||
self.assertEqual('HTTP/1.0',
|
||||
_wsgi.HttpProtocol.default_request_version)
|
||||
self.assertEqual(30, _wsgi.WRITE_TIMEOUT)
|
||||
_wsgi_evt.hubs.use_hub.assert_called_with(utils.get_hub())
|
||||
_wsgi_evt.debug.hub_exceptions.assert_called_with(False)
|
||||
@ -578,6 +636,9 @@ class TestWSGI(unittest.TestCase):
|
||||
self.assertTrue(isinstance(server_app, swift.proxy.server.Application))
|
||||
self.assertTrue(isinstance(server_logger, wsgi.NullLogger))
|
||||
self.assertTrue('custom_pool' in kwargs)
|
||||
self.assertTrue('protocol' in kwargs)
|
||||
self.assertEqual('HTTP/1.0',
|
||||
kwargs['protocol'].default_request_version)
|
||||
|
||||
def test_run_server_debug(self):
|
||||
config = """
|
||||
@ -615,8 +676,6 @@ class TestWSGI(unittest.TestCase):
|
||||
logger = logging.getLogger('test')
|
||||
sock = listen_zero()
|
||||
wsgi.run_server(conf, logger, sock)
|
||||
self.assertEqual('HTTP/1.0',
|
||||
_wsgi.HttpProtocol.default_request_version)
|
||||
self.assertEqual(30, _wsgi.WRITE_TIMEOUT)
|
||||
_wsgi_evt.hubs.use_hub.assert_called_with(utils.get_hub())
|
||||
_wsgi_evt.debug.hub_exceptions.assert_called_with(True)
|
||||
@ -629,6 +688,9 @@ class TestWSGI(unittest.TestCase):
|
||||
self.assertIsNone(server_logger)
|
||||
self.assertTrue('custom_pool' in kwargs)
|
||||
self.assertEqual(1000, kwargs['custom_pool'].size)
|
||||
self.assertTrue('protocol' in kwargs)
|
||||
self.assertEqual('HTTP/1.0',
|
||||
kwargs['protocol'].default_request_version)
|
||||
|
||||
def test_appconfig_dir_ignores_hidden_files(self):
|
||||
config_dir = {
|
||||
@ -948,6 +1010,193 @@ class TestWSGI(unittest.TestCase):
|
||||
self.assertIs(newenv.get('swift.infocache'), oldenv['swift.infocache'])
|
||||
|
||||
|
||||
class TestSwiftHttpProtocol(unittest.TestCase):
|
||||
def setUp(self):
|
||||
patcher = mock.patch('swift.common.wsgi.wsgi.HttpProtocol')
|
||||
self.mock_super = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def _proto_obj(self):
|
||||
# Make an object we can exercise... note the base class's __init__()
|
||||
# does a bunch of work, so we just new up an object like eventlet.wsgi
|
||||
# does.
|
||||
proto_class = wsgi.SwiftHttpProtocol
|
||||
try:
|
||||
the_obj = types.InstanceType(proto_class)
|
||||
except AttributeError:
|
||||
the_obj = proto_class.__new__(proto_class)
|
||||
# Install some convenience mocks
|
||||
the_obj.server = Namespace(app=Namespace(logger=mock.Mock()),
|
||||
url_length_limit=777,
|
||||
log=mock.Mock())
|
||||
the_obj.send_error = mock.Mock()
|
||||
|
||||
return the_obj
|
||||
|
||||
def test_swift_http_protocol_log_request(self):
|
||||
proto_obj = self._proto_obj()
|
||||
self.assertEqual(None, proto_obj.log_request('ignored'))
|
||||
|
||||
def test_swift_http_protocol_log_message(self):
|
||||
proto_obj = self._proto_obj()
|
||||
|
||||
proto_obj.log_message('a%sc', 'b')
|
||||
self.assertEqual([mock.call.error('ERROR WSGI: a%sc', 'b')],
|
||||
proto_obj.server.app.logger.mock_calls)
|
||||
|
||||
def test_swift_http_protocol_log_message_no_logger(self):
|
||||
# If the app somehow had no logger attribute or it was None, don't blow
|
||||
# up
|
||||
proto_obj = self._proto_obj()
|
||||
delattr(proto_obj.server.app, 'logger')
|
||||
|
||||
proto_obj.log_message('a%sc', 'b')
|
||||
self.assertEqual([mock.call.error('ERROR WSGI: a%sc', 'b')],
|
||||
proto_obj.server.log.mock_calls)
|
||||
|
||||
proto_obj.server.log.reset_mock()
|
||||
proto_obj.server.app.logger = None
|
||||
|
||||
proto_obj.log_message('a%sc', 'b')
|
||||
self.assertEqual([mock.call.error('ERROR WSGI: a%sc', 'b')],
|
||||
proto_obj.server.log.mock_calls)
|
||||
|
||||
def test_swift_http_protocol_parse_request_no_proxy(self):
|
||||
proto_obj = self._proto_obj()
|
||||
proto_obj.raw_requestline = b'jimmy jam'
|
||||
proto_obj.client_address = ('a', '123')
|
||||
|
||||
self.assertEqual(False, proto_obj.parse_request())
|
||||
|
||||
self.assertEqual([], self.mock_super.mock_calls)
|
||||
self.assertEqual([
|
||||
mock.call(400, "Bad HTTP/0.9 request type ('jimmy')"),
|
||||
], proto_obj.send_error.mock_calls)
|
||||
self.assertEqual(('a', '123'), proto_obj.client_address)
|
||||
|
||||
|
||||
class TestProxyProtocol(unittest.TestCase):
|
||||
def _run_bytes_through_protocol(self, bytes_from_client, protocol_class):
|
||||
rfile = BytesIO(bytes_from_client)
|
||||
wfile = BytesIO()
|
||||
|
||||
# All this fakery is needed to make the WSGI server process one
|
||||
# connection, possibly with multiple requests, in the main
|
||||
# greenthread. It doesn't hurt correctness if the function is called
|
||||
# in a separate greenthread, but it makes using the debugger harder.
|
||||
class FakeGreenthread(object):
|
||||
def link(self, a_callable, *args):
|
||||
a_callable(self, *args)
|
||||
|
||||
class FakePool(object):
|
||||
def spawn(self, a_callable, *args, **kwargs):
|
||||
a_callable(*args, **kwargs)
|
||||
return FakeGreenthread()
|
||||
|
||||
def spawn_n(self, a_callable, *args, **kwargs):
|
||||
a_callable(*args, **kwargs)
|
||||
|
||||
def waitall(self):
|
||||
pass
|
||||
|
||||
def dinky_app(env, start_response):
|
||||
start_response("200 OK", [])
|
||||
body = "got addr: %s %s\r\n" % (
|
||||
env.get("REMOTE_ADDR", "<missing>"),
|
||||
env.get("REMOTE_PORT", "<missing>"))
|
||||
return [body.encode("utf-8")]
|
||||
|
||||
fake_tcp_socket = mock.Mock(
|
||||
setsockopt=lambda *a: None,
|
||||
makefile=lambda mode, bufsize: rfile if 'r' in mode else wfile,
|
||||
)
|
||||
fake_listen_socket = mock.Mock(accept=mock.MagicMock(
|
||||
side_effect=[[fake_tcp_socket, ('127.0.0.1', 8359)],
|
||||
# KeyboardInterrupt breaks the WSGI server out of
|
||||
# its infinite accept-process-close loop.
|
||||
KeyboardInterrupt]))
|
||||
|
||||
# If we let the WSGI server close rfile/wfile then we can't access
|
||||
# their contents any more.
|
||||
with mock.patch.object(wfile, 'close', lambda: None), \
|
||||
mock.patch.object(rfile, 'close', lambda: None):
|
||||
eventlet.wsgi.server(
|
||||
fake_listen_socket, dinky_app,
|
||||
protocol=protocol_class,
|
||||
custom_pool=FakePool(),
|
||||
log_output=False, # quiet the test run
|
||||
)
|
||||
return wfile.getvalue()
|
||||
|
||||
def test_request_with_proxy(self):
|
||||
bytes_out = self._run_bytes_through_protocol((
|
||||
b"PROXY TCP4 192.168.0.1 192.168.0.11 56423 443\r\n"
|
||||
b"GET /someurl HTTP/1.0\r\n"
|
||||
b"User-Agent: something or other\r\n"
|
||||
b"\r\n"
|
||||
), wsgi.SwiftHttpProxiedProtocol)
|
||||
|
||||
lines = [l for l in bytes_out.split(b"\r\n") if l]
|
||||
self.assertEqual(lines[0], b"HTTP/1.1 200 OK") # sanity check
|
||||
self.assertEqual(lines[-1], b"got addr: 192.168.0.1 56423")
|
||||
|
||||
def test_multiple_requests_with_proxy(self):
|
||||
bytes_out = self._run_bytes_through_protocol((
|
||||
b"PROXY TCP4 192.168.0.1 192.168.0.11 56423 443\r\n"
|
||||
b"GET /someurl HTTP/1.1\r\n"
|
||||
b"User-Agent: something or other\r\n"
|
||||
b"\r\n"
|
||||
b"GET /otherurl HTTP/1.1\r\n"
|
||||
b"User-Agent: something or other\r\n"
|
||||
b"Connection: close\r\n"
|
||||
b"\r\n"
|
||||
), wsgi.SwiftHttpProxiedProtocol)
|
||||
|
||||
lines = bytes_out.split(b"\r\n")
|
||||
self.assertEqual(lines[0], b"HTTP/1.1 200 OK") # sanity check
|
||||
|
||||
# the address in the PROXY line is applied to every request
|
||||
addr_lines = [l for l in lines if l.startswith(b"got addr")]
|
||||
self.assertEqual(addr_lines, [b"got addr: 192.168.0.1 56423"] * 2)
|
||||
|
||||
def test_missing_proxy_line(self):
|
||||
bytes_out = self._run_bytes_through_protocol((
|
||||
# whoops, no PROXY line here
|
||||
b"GET /someurl HTTP/1.0\r\n"
|
||||
b"User-Agent: something or other\r\n"
|
||||
b"\r\n"
|
||||
), wsgi.SwiftHttpProxiedProtocol)
|
||||
|
||||
lines = [l for l in bytes_out.split(b"\r\n") if l]
|
||||
self.assertIn(b"400 Invalid PROXY line", lines[0])
|
||||
|
||||
def test_malformed_proxy_lines(self):
|
||||
for bad_line in [b'PROXY jojo',
|
||||
b'PROXYjojo a b c d e',
|
||||
b'PROXY a b c d e', # bad INET protocol and family
|
||||
]:
|
||||
bytes_out = self._run_bytes_through_protocol(
|
||||
bad_line, wsgi.SwiftHttpProxiedProtocol)
|
||||
lines = [l for l in bytes_out.split(b"\r\n") if l]
|
||||
self.assertIn(b"400 Invalid PROXY line", lines[0])
|
||||
|
||||
def test_unknown_client_addr(self):
|
||||
# For "UNKNOWN", the rest of the line before the CRLF may be omitted by
|
||||
# the sender, and the receiver must ignore anything presented before
|
||||
# the CRLF is found.
|
||||
for unknown_line in [b'PROXY UNKNOWN', # mimimal valid unknown
|
||||
b'PROXY UNKNOWNblahblah', # also valid
|
||||
b'PROXY UNKNOWN a b c d']:
|
||||
bytes_out = self._run_bytes_through_protocol((
|
||||
unknown_line + (b"\r\n"
|
||||
b"GET /someurl HTTP/1.0\r\n"
|
||||
b"User-Agent: something or other\r\n"
|
||||
b"\r\n")
|
||||
), wsgi.SwiftHttpProxiedProtocol)
|
||||
lines = [l for l in bytes_out.split(b"\r\n") if l]
|
||||
self.assertIn(b"200 OK", lines[0])
|
||||
|
||||
|
||||
class TestServersPerPortStrategy(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.logger = FakeLogger()
|
||||
|
Loading…
Reference in New Issue
Block a user