1
# Copyright (c) 2007-2010 Twisted Matrix Laboratories.
2
# See LICENSE for details.
5
Test for L{twisted.web.proxy}.
8
from twisted.trial.unittest import TestCase
9
from twisted.test.proto_helpers import StringTransportWithDisconnection
10
from twisted.test.proto_helpers import MemoryReactor
12
from twisted.web.resource import Resource
13
from twisted.web.server import Site
14
from twisted.web.proxy import ReverseProxyResource, ProxyClientFactory
15
from twisted.web.proxy import ProxyClient, ProxyRequest, ReverseProxyRequest
16
from twisted.web.test.test_web import DummyRequest
19
class ReverseProxyResourceTestCase(TestCase):
21
Tests for L{ReverseProxyResource}.
24
def _testRender(self, uri, expectedURI):
26
Check that a request pointing at C{uri} produce a new proxy connection,
27
with the path of this request pointing at C{expectedURI}.
30
reactor = MemoryReactor()
31
resource = ReverseProxyResource("127.0.0.1", 1234, "/path", reactor)
32
root.putChild('index', resource)
35
transport = StringTransportWithDisconnection()
36
channel = site.buildProtocol(None)
37
channel.makeConnection(transport)
38
# Clear the timeout if the tests failed
39
self.addCleanup(channel.connectionLost, None)
41
channel.dataReceived("GET %s HTTP/1.1\r\nAccept: text/html\r\n\r\n" %
44
# Check that one connection has been created, to the good host/port
45
self.assertEquals(len(reactor.tcpClients), 1)
46
self.assertEquals(reactor.tcpClients[0][0], "127.0.0.1")
47
self.assertEquals(reactor.tcpClients[0][1], 1234)
49
# Check the factory passed to the connect, and its given path
50
factory = reactor.tcpClients[0][2]
51
self.assertIsInstance(factory, ProxyClientFactory)
52
self.assertEquals(factory.rest, expectedURI)
53
self.assertEquals(factory.headers["host"], "127.0.0.1:1234")
56
def test_render(self):
58
Test that L{ReverseProxyResource.render} initiates a connection to the
59
given server with a L{ProxyClientFactory} as parameter.
61
return self._testRender("/index", "/path")
64
def test_renderWithQuery(self):
66
Test that L{ReverseProxyResource.render} passes query parameters to the
69
return self._testRender("/index?foo=bar", "/path?foo=bar")
72
def test_getChild(self):
74
The L{ReverseProxyResource.getChild} method should return a resource
75
instance with the same class as the originating resource, forward port
76
and host values, and update the path value with the value passed.
78
resource = ReverseProxyResource("127.0.0.1", 1234, "/path")
79
child = resource.getChild('foo', None)
80
# The child should keep the same class
81
self.assertIsInstance(child, ReverseProxyResource)
82
self.assertEquals(child.path, "/path/foo")
83
self.assertEquals(child.port, 1234)
84
self.assertEquals(child.host, "127.0.0.1")
87
def test_getChildWithSpecial(self):
89
The L{ReverseProxyResource} return by C{getChild} has a path which has
92
resource = ReverseProxyResource("127.0.0.1", 1234, "/path")
93
child = resource.getChild(' /%', None)
94
self.assertEqual(child.path, "/path/%20%2F%25")
98
class DummyChannel(object):
100
A dummy HTTP channel, that does nothing but holds a transport and saves
103
@ivar transport: the transport used by the client.
104
@ivar lostReason: the reason saved at connection lost.
107
def __init__(self, transport):
109
Hold a reference to the transport.
111
self.transport = transport
112
self.lostReason = None
115
def connectionLost(self, reason):
117
Keep track of the connection lost reason.
119
self.lostReason = reason
123
class ProxyClientTestCase(TestCase):
125
Tests for L{ProxyClient}.
128
def _parseOutHeaders(self, content):
130
Parse the headers out of some web content.
132
@param content: Bytes received from a web server.
133
@return: A tuple of (requestLine, headers, body). C{headers} is a dict
134
of headers, C{requestLine} is the first line (e.g. "POST /foo ...")
135
and C{body} is whatever is left.
137
headers, body = content.split('\r\n\r\n')
138
headers = headers.split('\r\n')
139
requestLine = headers.pop(0)
141
requestLine, dict(header.split(': ') for header in headers), body)
144
def makeRequest(self, path):
146
Make a dummy request object for the URL path.
148
@param path: A URL path, beginning with a slash.
149
@return: A L{DummyRequest}.
151
return DummyRequest(path)
154
def makeProxyClient(self, request, method="GET", headers=None,
157
Make a L{ProxyClient} object used for testing.
159
@param request: The request to use.
160
@param method: The HTTP method to use, GET by default.
161
@param headers: The HTTP headers to use expressed as a dict. If not
162
provided, defaults to {'accept': 'text/html'}.
163
@param requestBody: The body of the request. Defaults to the empty
165
@return: A L{ProxyClient}
168
headers = {"accept": "text/html"}
169
path = '/' + request.postpath
171
method, path, 'HTTP/1.0', headers, requestBody, request)
174
def connectProxy(self, proxyClient):
176
Connect a proxy client to a L{StringTransportWithDisconnection}.
178
@param proxyClient: A L{ProxyClient}.
179
@return: The L{StringTransportWithDisconnection}.
181
clientTransport = StringTransportWithDisconnection()
182
clientTransport.protocol = proxyClient
183
proxyClient.makeConnection(clientTransport)
184
return clientTransport
187
def assertForwardsHeaders(self, proxyClient, requestLine, headers):
189
Assert that C{proxyClient} sends C{headers} when it connects.
191
@param proxyClient: A L{ProxyClient}.
192
@param requestLine: The request line we expect to be sent.
193
@param headers: A dict of headers we expect to be sent.
194
@return: If the assertion is successful, return the request body as
197
self.connectProxy(proxyClient)
198
requestContent = proxyClient.transport.value()
199
receivedLine, receivedHeaders, body = self._parseOutHeaders(
201
self.assertEquals(receivedLine, requestLine)
202
self.assertEquals(receivedHeaders, headers)
206
def makeResponseBytes(self, code, message, headers, body):
207
lines = ["HTTP/1.0 %d %s" % (code, message)]
208
for header, values in headers:
210
lines.append("%s: %s" % (header, value))
211
lines.extend(['', body])
212
return '\r\n'.join(lines)
215
def assertForwardsResponse(self, request, code, message, headers, body):
217
Assert that C{request} has forwarded a response from the server.
219
@param request: A L{DummyRequest}.
220
@param code: The expected HTTP response code.
221
@param message: The expected HTTP message.
222
@param headers: The expected HTTP headers.
223
@param body: The expected response body.
225
self.assertEquals(request.responseCode, code)
226
self.assertEquals(request.responseMessage, message)
227
receivedHeaders = list(request.responseHeaders.getAllRawHeaders())
228
receivedHeaders.sort()
229
expectedHeaders = headers[:]
230
expectedHeaders.sort()
231
self.assertEquals(receivedHeaders, expectedHeaders)
232
self.assertEquals(''.join(request.written), body)
235
def _testDataForward(self, code, message, headers, body, method="GET",
236
requestBody="", loseConnection=True):
238
Build a fake proxy connection, and send C{data} over it, checking that
239
it's forwarded to the originating request.
241
request = self.makeRequest('foo')
242
client = self.makeProxyClient(
243
request, method, {'accept': 'text/html'}, requestBody)
245
receivedBody = self.assertForwardsHeaders(
246
client, '%s /foo HTTP/1.0' % (method,),
247
{'connection': 'close', 'accept': 'text/html'})
249
self.assertEquals(receivedBody, requestBody)
253
self.makeResponseBytes(code, message, headers, body))
255
# Check that the response data has been forwarded back to the original
257
self.assertForwardsResponse(request, code, message, headers, body)
259
# Check that when the response is done, the request is finished.
261
client.transport.loseConnection()
263
# Even if we didn't call loseConnection, the transport should be
264
# disconnected. This lets us not rely on the server to close our
266
self.assertFalse(client.transport.connected)
267
self.assertEquals(request.finished, 1)
270
def test_forward(self):
272
When connected to the server, L{ProxyClient} should send the saved
273
request, with modifications of the headers, and then forward the result
274
to the parent request.
276
return self._testDataForward(
277
200, "OK", [("Foo", ["bar", "baz"])], "Some data\r\n")
280
def test_postData(self):
282
Try to post content in the request, and check that the proxy client
283
forward the body of the request.
285
return self._testDataForward(
286
200, "OK", [("Foo", ["bar"])], "Some data\r\n", "POST", "Some content")
289
def test_statusWithMessage(self):
291
If the response contains a status with a message, it should be
292
forwarded to the parent request with all the information.
294
return self._testDataForward(
295
404, "Not Found", [], "")
298
def test_contentLength(self):
300
If the response contains a I{Content-Length} header, the inbound
301
request object should still only have C{finish} called on it once.
304
return self._testDataForward(
305
200, "OK", [("Content-Length", [str(len(data))])], data)
308
def test_losesConnection(self):
310
If the response contains a I{Content-Length} header, the outgoing
311
connection is closed when all response body data has been received.
314
return self._testDataForward(
315
200, "OK", [("Content-Length", [str(len(data))])], data,
316
loseConnection=False)
319
def test_headersCleanups(self):
321
The headers given at initialization should be modified:
322
B{proxy-connection} should be removed if present, and B{connection}
325
client = ProxyClient('GET', '/foo', 'HTTP/1.0',
326
{"accept": "text/html", "proxy-connection": "foo"}, '', None)
327
self.assertEquals(client.headers,
328
{"accept": "text/html", "connection": "close"})
331
def test_keepaliveNotForwarded(self):
333
The proxy doesn't really know what to do with keepalive things from
334
the remote server, so we stomp over any keepalive header we get from
338
"accept": "text/html",
340
'connection': 'keep-alive',
342
expectedHeaders = headers.copy()
343
expectedHeaders['connection'] = 'close'
344
del expectedHeaders['keep-alive']
345
client = ProxyClient('GET', '/foo', 'HTTP/1.0', headers, '', None)
346
self.assertForwardsHeaders(
347
client, 'GET /foo HTTP/1.0', expectedHeaders)
350
def test_defaultHeadersOverridden(self):
352
L{server.Request} within the proxy sets certain response headers by
353
default. When we get these headers back from the remote server, the
354
defaults are overridden rather than simply appended.
356
request = self.makeRequest('foo')
357
request.responseHeaders.setRawHeaders('server', ['old-bar'])
358
request.responseHeaders.setRawHeaders('date', ['old-baz'])
359
request.responseHeaders.setRawHeaders('content-type', ["old/qux"])
360
client = self.makeProxyClient(request, headers={'accept': 'text/html'})
361
self.connectProxy(client)
364
'Date': ['2010-01-01'],
365
'Content-Type': ['application/x-baz'],
368
self.makeResponseBytes(200, "OK", headers.items(), ''))
369
self.assertForwardsResponse(
370
request, 200, 'OK', headers.items(), '')
374
class ProxyClientFactoryTestCase(TestCase):
376
Tests for L{ProxyClientFactory}.
379
def test_connectionFailed(self):
381
Check that L{ProxyClientFactory.clientConnectionFailed} produces
382
a B{501} response to the parent request.
384
request = DummyRequest(['foo'])
385
factory = ProxyClientFactory('GET', '/foo', 'HTTP/1.0',
386
{"accept": "text/html"}, '', request)
388
factory.clientConnectionFailed(None, None)
389
self.assertEquals(request.responseCode, 501)
390
self.assertEquals(request.responseMessage, "Gateway error")
392
list(request.responseHeaders.getAllRawHeaders()),
393
[("Content-Type", ["text/html"])])
395
''.join(request.written),
396
"<H1>Could not connect</H1>")
397
self.assertEquals(request.finished, 1)
400
def test_buildProtocol(self):
402
L{ProxyClientFactory.buildProtocol} should produce a L{ProxyClient}
403
with the same values of attributes (with updates on the headers).
405
factory = ProxyClientFactory('GET', '/foo', 'HTTP/1.0',
406
{"accept": "text/html"}, 'Some data',
408
proto = factory.buildProtocol(None)
409
self.assertIsInstance(proto, ProxyClient)
410
self.assertEquals(proto.command, 'GET')
411
self.assertEquals(proto.rest, '/foo')
412
self.assertEquals(proto.data, 'Some data')
413
self.assertEquals(proto.headers,
414
{"accept": "text/html", "connection": "close"})
418
class ProxyRequestTestCase(TestCase):
420
Tests for L{ProxyRequest}.
423
def _testProcess(self, uri, expectedURI, method="GET", data=""):
425
Build a request pointing at C{uri}, and check that a proxied request
426
is created, pointing a C{expectedURI}.
428
transport = StringTransportWithDisconnection()
429
channel = DummyChannel(transport)
430
reactor = MemoryReactor()
431
request = ProxyRequest(channel, False, reactor)
432
request.gotLength(len(data))
433
request.handleContentChunk(data)
434
request.requestReceived(method, 'http://example.com%s' % (uri,),
437
self.assertEquals(len(reactor.tcpClients), 1)
438
self.assertEquals(reactor.tcpClients[0][0], "example.com")
439
self.assertEquals(reactor.tcpClients[0][1], 80)
441
factory = reactor.tcpClients[0][2]
442
self.assertIsInstance(factory, ProxyClientFactory)
443
self.assertEquals(factory.command, method)
444
self.assertEquals(factory.version, 'HTTP/1.0')
445
self.assertEquals(factory.headers, {'host': 'example.com'})
446
self.assertEquals(factory.data, data)
447
self.assertEquals(factory.rest, expectedURI)
448
self.assertEquals(factory.father, request)
451
def test_process(self):
453
L{ProxyRequest.process} should create a connection to the given server,
454
with a L{ProxyClientFactory} as connection factory, with the correct
456
- forward comment, version and data values
457
- update headers with the B{host} value
458
- remove the host from the URL
459
- pass the request as parent request
461
return self._testProcess("/foo/bar", "/foo/bar")
464
def test_processWithoutTrailingSlash(self):
466
If the incoming request doesn't contain a slash,
467
L{ProxyRequest.process} should add one when instantiating
468
L{ProxyClientFactory}.
470
return self._testProcess("", "/")
473
def test_processWithData(self):
475
L{ProxyRequest.process} should be able to retrieve request body and
478
return self._testProcess(
479
"/foo/bar", "/foo/bar", "POST", "Some content")
482
def test_processWithPort(self):
484
Check that L{ProxyRequest.process} correctly parse port in the incoming
485
URL, and create a outgoing connection with this port.
487
transport = StringTransportWithDisconnection()
488
channel = DummyChannel(transport)
489
reactor = MemoryReactor()
490
request = ProxyRequest(channel, False, reactor)
492
request.requestReceived('GET', 'http://example.com:1234/foo/bar',
495
# That should create one connection, with the port parsed from the URL
496
self.assertEquals(len(reactor.tcpClients), 1)
497
self.assertEquals(reactor.tcpClients[0][0], "example.com")
498
self.assertEquals(reactor.tcpClients[0][1], 1234)
502
class DummyFactory(object):
504
A simple holder for C{host} and C{port} information.
507
def __init__(self, host, port):
513
class ReverseProxyRequestTestCase(TestCase):
515
Tests for L{ReverseProxyRequest}.
518
def test_process(self):
520
L{ReverseProxyRequest.process} should create a connection to its
521
factory host/port, using a L{ProxyClientFactory} instantiated with the
522
correct parameters, and particulary set the B{host} header to the
525
transport = StringTransportWithDisconnection()
526
channel = DummyChannel(transport)
527
reactor = MemoryReactor()
528
request = ReverseProxyRequest(channel, False, reactor)
529
request.factory = DummyFactory("example.com", 1234)
531
request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
533
# Check that one connection has been created, to the good host/port
534
self.assertEquals(len(reactor.tcpClients), 1)
535
self.assertEquals(reactor.tcpClients[0][0], "example.com")
536
self.assertEquals(reactor.tcpClients[0][1], 1234)
538
# Check the factory passed to the connect, and its headers
539
factory = reactor.tcpClients[0][2]
540
self.assertIsInstance(factory, ProxyClientFactory)
541
self.assertEquals(factory.headers, {'host': 'example.com'})