1 # Copyright (c) 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.assertEqual(len(reactor.tcpClients), 1)
46 self.assertEqual(reactor.tcpClients[0][0], "127.0.0.1")
47 self.assertEqual(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.assertEqual(factory.rest, expectedURI)
53 self.assertEqual(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
76 port, host, and reactor values, and update the path value with the
79 reactor = MemoryReactor()
80 resource = ReverseProxyResource("127.0.0.1", 1234, "/path", reactor)
81 child = resource.getChild('foo', None)
82 # The child should keep the same class
83 self.assertIsInstance(child, ReverseProxyResource)
84 self.assertEqual(child.path, "/path/foo")
85 self.assertEqual(child.port, 1234)
86 self.assertEqual(child.host, "127.0.0.1")
87 self.assertIdentical(child.reactor, resource.reactor)
90 def test_getChildWithSpecial(self):
92 The L{ReverseProxyResource} return by C{getChild} has a path which has
95 resource = ReverseProxyResource("127.0.0.1", 1234, "/path")
96 child = resource.getChild(' /%', None)
97 self.assertEqual(child.path, "/path/%20%2F%25")
101 class DummyChannel(object):
103 A dummy HTTP channel, that does nothing but holds a transport and saves
106 @ivar transport: the transport used by the client.
107 @ivar lostReason: the reason saved at connection lost.
110 def __init__(self, transport):
112 Hold a reference to the transport.
114 self.transport = transport
115 self.lostReason = None
118 def connectionLost(self, reason):
120 Keep track of the connection lost reason.
122 self.lostReason = reason
126 class ProxyClientTestCase(TestCase):
128 Tests for L{ProxyClient}.
131 def _parseOutHeaders(self, content):
133 Parse the headers out of some web content.
135 @param content: Bytes received from a web server.
136 @return: A tuple of (requestLine, headers, body). C{headers} is a dict
137 of headers, C{requestLine} is the first line (e.g. "POST /foo ...")
138 and C{body} is whatever is left.
140 headers, body = content.split('\r\n\r\n')
141 headers = headers.split('\r\n')
142 requestLine = headers.pop(0)
144 requestLine, dict(header.split(': ') for header in headers), body)
147 def makeRequest(self, path):
149 Make a dummy request object for the URL path.
151 @param path: A URL path, beginning with a slash.
152 @return: A L{DummyRequest}.
154 return DummyRequest(path)
157 def makeProxyClient(self, request, method="GET", headers=None,
160 Make a L{ProxyClient} object used for testing.
162 @param request: The request to use.
163 @param method: The HTTP method to use, GET by default.
164 @param headers: The HTTP headers to use expressed as a dict. If not
165 provided, defaults to {'accept': 'text/html'}.
166 @param requestBody: The body of the request. Defaults to the empty
168 @return: A L{ProxyClient}
171 headers = {"accept": "text/html"}
172 path = '/' + request.postpath
174 method, path, 'HTTP/1.0', headers, requestBody, request)
177 def connectProxy(self, proxyClient):
179 Connect a proxy client to a L{StringTransportWithDisconnection}.
181 @param proxyClient: A L{ProxyClient}.
182 @return: The L{StringTransportWithDisconnection}.
184 clientTransport = StringTransportWithDisconnection()
185 clientTransport.protocol = proxyClient
186 proxyClient.makeConnection(clientTransport)
187 return clientTransport
190 def assertForwardsHeaders(self, proxyClient, requestLine, headers):
192 Assert that C{proxyClient} sends C{headers} when it connects.
194 @param proxyClient: A L{ProxyClient}.
195 @param requestLine: The request line we expect to be sent.
196 @param headers: A dict of headers we expect to be sent.
197 @return: If the assertion is successful, return the request body as
200 self.connectProxy(proxyClient)
201 requestContent = proxyClient.transport.value()
202 receivedLine, receivedHeaders, body = self._parseOutHeaders(
204 self.assertEqual(receivedLine, requestLine)
205 self.assertEqual(receivedHeaders, headers)
209 def makeResponseBytes(self, code, message, headers, body):
210 lines = ["HTTP/1.0 %d %s" % (code, message)]
211 for header, values in headers:
213 lines.append("%s: %s" % (header, value))
214 lines.extend(['', body])
215 return '\r\n'.join(lines)
218 def assertForwardsResponse(self, request, code, message, headers, body):
220 Assert that C{request} has forwarded a response from the server.
222 @param request: A L{DummyRequest}.
223 @param code: The expected HTTP response code.
224 @param message: The expected HTTP message.
225 @param headers: The expected HTTP headers.
226 @param body: The expected response body.
228 self.assertEqual(request.responseCode, code)
229 self.assertEqual(request.responseMessage, message)
230 receivedHeaders = list(request.responseHeaders.getAllRawHeaders())
231 receivedHeaders.sort()
232 expectedHeaders = headers[:]
233 expectedHeaders.sort()
234 self.assertEqual(receivedHeaders, expectedHeaders)
235 self.assertEqual(''.join(request.written), body)
238 def _testDataForward(self, code, message, headers, body, method="GET",
239 requestBody="", loseConnection=True):
241 Build a fake proxy connection, and send C{data} over it, checking that
242 it's forwarded to the originating request.
244 request = self.makeRequest('foo')
245 client = self.makeProxyClient(
246 request, method, {'accept': 'text/html'}, requestBody)
248 receivedBody = self.assertForwardsHeaders(
249 client, '%s /foo HTTP/1.0' % (method,),
250 {'connection': 'close', 'accept': 'text/html'})
252 self.assertEqual(receivedBody, requestBody)
256 self.makeResponseBytes(code, message, headers, body))
258 # Check that the response data has been forwarded back to the original
260 self.assertForwardsResponse(request, code, message, headers, body)
262 # Check that when the response is done, the request is finished.
264 client.transport.loseConnection()
266 # Even if we didn't call loseConnection, the transport should be
267 # disconnected. This lets us not rely on the server to close our
269 self.assertFalse(client.transport.connected)
270 self.assertEqual(request.finished, 1)
273 def test_forward(self):
275 When connected to the server, L{ProxyClient} should send the saved
276 request, with modifications of the headers, and then forward the result
277 to the parent request.
279 return self._testDataForward(
280 200, "OK", [("Foo", ["bar", "baz"])], "Some data\r\n")
283 def test_postData(self):
285 Try to post content in the request, and check that the proxy client
286 forward the body of the request.
288 return self._testDataForward(
289 200, "OK", [("Foo", ["bar"])], "Some data\r\n", "POST", "Some content")
292 def test_statusWithMessage(self):
294 If the response contains a status with a message, it should be
295 forwarded to the parent request with all the information.
297 return self._testDataForward(
298 404, "Not Found", [], "")
301 def test_contentLength(self):
303 If the response contains a I{Content-Length} header, the inbound
304 request object should still only have C{finish} called on it once.
307 return self._testDataForward(
308 200, "OK", [("Content-Length", [str(len(data))])], data)
311 def test_losesConnection(self):
313 If the response contains a I{Content-Length} header, the outgoing
314 connection is closed when all response body data has been received.
317 return self._testDataForward(
318 200, "OK", [("Content-Length", [str(len(data))])], data,
319 loseConnection=False)
322 def test_headersCleanups(self):
324 The headers given at initialization should be modified:
325 B{proxy-connection} should be removed if present, and B{connection}
328 client = ProxyClient('GET', '/foo', 'HTTP/1.0',
329 {"accept": "text/html", "proxy-connection": "foo"}, '', None)
330 self.assertEqual(client.headers,
331 {"accept": "text/html", "connection": "close"})
334 def test_keepaliveNotForwarded(self):
336 The proxy doesn't really know what to do with keepalive things from
337 the remote server, so we stomp over any keepalive header we get from
341 "accept": "text/html",
343 'connection': 'keep-alive',
345 expectedHeaders = headers.copy()
346 expectedHeaders['connection'] = 'close'
347 del expectedHeaders['keep-alive']
348 client = ProxyClient('GET', '/foo', 'HTTP/1.0', headers, '', None)
349 self.assertForwardsHeaders(
350 client, 'GET /foo HTTP/1.0', expectedHeaders)
353 def test_defaultHeadersOverridden(self):
355 L{server.Request} within the proxy sets certain response headers by
356 default. When we get these headers back from the remote server, the
357 defaults are overridden rather than simply appended.
359 request = self.makeRequest('foo')
360 request.responseHeaders.setRawHeaders('server', ['old-bar'])
361 request.responseHeaders.setRawHeaders('date', ['old-baz'])
362 request.responseHeaders.setRawHeaders('content-type', ["old/qux"])
363 client = self.makeProxyClient(request, headers={'accept': 'text/html'})
364 self.connectProxy(client)
367 'Date': ['2010-01-01'],
368 'Content-Type': ['application/x-baz'],
371 self.makeResponseBytes(200, "OK", headers.items(), ''))
372 self.assertForwardsResponse(
373 request, 200, 'OK', headers.items(), '')
377 class ProxyClientFactoryTestCase(TestCase):
379 Tests for L{ProxyClientFactory}.
382 def test_connectionFailed(self):
384 Check that L{ProxyClientFactory.clientConnectionFailed} produces
385 a B{501} response to the parent request.
387 request = DummyRequest(['foo'])
388 factory = ProxyClientFactory('GET', '/foo', 'HTTP/1.0',
389 {"accept": "text/html"}, '', request)
391 factory.clientConnectionFailed(None, None)
392 self.assertEqual(request.responseCode, 501)
393 self.assertEqual(request.responseMessage, "Gateway error")
395 list(request.responseHeaders.getAllRawHeaders()),
396 [("Content-Type", ["text/html"])])
398 ''.join(request.written),
399 "<H1>Could not connect</H1>")
400 self.assertEqual(request.finished, 1)
403 def test_buildProtocol(self):
405 L{ProxyClientFactory.buildProtocol} should produce a L{ProxyClient}
406 with the same values of attributes (with updates on the headers).
408 factory = ProxyClientFactory('GET', '/foo', 'HTTP/1.0',
409 {"accept": "text/html"}, 'Some data',
411 proto = factory.buildProtocol(None)
412 self.assertIsInstance(proto, ProxyClient)
413 self.assertEqual(proto.command, 'GET')
414 self.assertEqual(proto.rest, '/foo')
415 self.assertEqual(proto.data, 'Some data')
416 self.assertEqual(proto.headers,
417 {"accept": "text/html", "connection": "close"})
421 class ProxyRequestTestCase(TestCase):
423 Tests for L{ProxyRequest}.
426 def _testProcess(self, uri, expectedURI, method="GET", data=""):
428 Build a request pointing at C{uri}, and check that a proxied request
429 is created, pointing a C{expectedURI}.
431 transport = StringTransportWithDisconnection()
432 channel = DummyChannel(transport)
433 reactor = MemoryReactor()
434 request = ProxyRequest(channel, False, reactor)
435 request.gotLength(len(data))
436 request.handleContentChunk(data)
437 request.requestReceived(method, 'http://example.com%s' % (uri,),
440 self.assertEqual(len(reactor.tcpClients), 1)
441 self.assertEqual(reactor.tcpClients[0][0], "example.com")
442 self.assertEqual(reactor.tcpClients[0][1], 80)
444 factory = reactor.tcpClients[0][2]
445 self.assertIsInstance(factory, ProxyClientFactory)
446 self.assertEqual(factory.command, method)
447 self.assertEqual(factory.version, 'HTTP/1.0')
448 self.assertEqual(factory.headers, {'host': 'example.com'})
449 self.assertEqual(factory.data, data)
450 self.assertEqual(factory.rest, expectedURI)
451 self.assertEqual(factory.father, request)
454 def test_process(self):
456 L{ProxyRequest.process} should create a connection to the given server,
457 with a L{ProxyClientFactory} as connection factory, with the correct
459 - forward comment, version and data values
460 - update headers with the B{host} value
461 - remove the host from the URL
462 - pass the request as parent request
464 return self._testProcess("/foo/bar", "/foo/bar")
467 def test_processWithoutTrailingSlash(self):
469 If the incoming request doesn't contain a slash,
470 L{ProxyRequest.process} should add one when instantiating
471 L{ProxyClientFactory}.
473 return self._testProcess("", "/")
476 def test_processWithData(self):
478 L{ProxyRequest.process} should be able to retrieve request body and
481 return self._testProcess(
482 "/foo/bar", "/foo/bar", "POST", "Some content")
485 def test_processWithPort(self):
487 Check that L{ProxyRequest.process} correctly parse port in the incoming
488 URL, and create a outgoing connection with this port.
490 transport = StringTransportWithDisconnection()
491 channel = DummyChannel(transport)
492 reactor = MemoryReactor()
493 request = ProxyRequest(channel, False, reactor)
495 request.requestReceived('GET', 'http://example.com:1234/foo/bar',
498 # That should create one connection, with the port parsed from the URL
499 self.assertEqual(len(reactor.tcpClients), 1)
500 self.assertEqual(reactor.tcpClients[0][0], "example.com")
501 self.assertEqual(reactor.tcpClients[0][1], 1234)
505 class DummyFactory(object):
507 A simple holder for C{host} and C{port} information.
510 def __init__(self, host, port):
516 class ReverseProxyRequestTestCase(TestCase):
518 Tests for L{ReverseProxyRequest}.
521 def test_process(self):
523 L{ReverseProxyRequest.process} should create a connection to its
524 factory host/port, using a L{ProxyClientFactory} instantiated with the
525 correct parameters, and particulary set the B{host} header to the
528 transport = StringTransportWithDisconnection()
529 channel = DummyChannel(transport)
530 reactor = MemoryReactor()
531 request = ReverseProxyRequest(channel, False, reactor)
532 request.factory = DummyFactory("example.com", 1234)
534 request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
536 # Check that one connection has been created, to the good host/port
537 self.assertEqual(len(reactor.tcpClients), 1)
538 self.assertEqual(reactor.tcpClients[0][0], "example.com")
539 self.assertEqual(reactor.tcpClients[0][1], 1234)
541 # Check the factory passed to the connect, and its headers
542 factory = reactor.tcpClients[0][2]
543 self.assertIsInstance(factory, ProxyClientFactory)
544 self.assertEqual(factory.headers, {'host': 'example.com'})