OLD | NEW |
| (Empty) |
1 # Copyright (c) 2007 Twisted Matrix Laboratories. | |
2 # See LICENSE for details. | |
3 | |
4 """ | |
5 Test for L{twisted.web.proxy}. | |
6 """ | |
7 | |
8 from twisted.trial.unittest import TestCase | |
9 from twisted.test.proto_helpers import StringTransportWithDisconnection | |
10 from twisted.internet.error import ConnectionDone | |
11 | |
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 | |
17 | |
18 | |
19 class FakeReactor(object): | |
20 """ | |
21 A fake reactor to be used in tests. | |
22 | |
23 @ivar connect: a list that keeps track of connection attempts (ie, calls | |
24 to C{connectTCP}). | |
25 @type connect: C{list} | |
26 """ | |
27 | |
28 def __init__(self): | |
29 """ | |
30 Initialize the C{connect} list. | |
31 """ | |
32 self.connect = [] | |
33 | |
34 | |
35 def connectTCP(self, host, port, factory): | |
36 """ | |
37 Fake L{reactor.connectTCP}, that does nothing but log the call. | |
38 """ | |
39 self.connect.append([host, port, factory]) | |
40 | |
41 | |
42 | |
43 class ReverseProxyResourceTestCase(TestCase): | |
44 """ | |
45 Tests for L{ReverseProxyResource}. | |
46 """ | |
47 | |
48 def _testRender(self, uri, expectedURI): | |
49 """ | |
50 Check that a request pointing at C{uri} produce a new proxy connection, | |
51 with the path of this request pointing at C{expectedURI}. | |
52 """ | |
53 root = Resource() | |
54 reactor = FakeReactor() | |
55 resource = ReverseProxyResource("127.0.0.1", 1234, "/path", reactor) | |
56 root.putChild('index', resource) | |
57 site = Site(root) | |
58 | |
59 transport = StringTransportWithDisconnection() | |
60 channel = site.buildProtocol(None) | |
61 channel.makeConnection(transport) | |
62 # Clear the timeout if the tests failed | |
63 self.addCleanup(channel.connectionLost, None) | |
64 | |
65 channel.dataReceived("GET %s HTTP/1.1\r\nAccept: text/html\r\n\r\n" % | |
66 (uri,)) | |
67 | |
68 # Check that one connection has been created, to the good host/port | |
69 self.assertEquals(len(reactor.connect), 1) | |
70 self.assertEquals(reactor.connect[0][0], "127.0.0.1") | |
71 self.assertEquals(reactor.connect[0][1], 1234) | |
72 | |
73 # Check the factory passed to the connect, and its given path | |
74 factory = reactor.connect[0][2] | |
75 self.assertIsInstance(factory, ProxyClientFactory) | |
76 self.assertEquals(factory.rest, expectedURI) | |
77 self.assertEquals(factory.headers["host"], "127.0.0.1:1234") | |
78 | |
79 | |
80 def test_render(self): | |
81 """ | |
82 Test that L{ReverseProxyResource.render} initiates a connection to the | |
83 given server with a L{ProxyClientFactory} as parameter. | |
84 """ | |
85 return self._testRender("/index", "/path") | |
86 | |
87 | |
88 def test_renderWithQuery(self): | |
89 """ | |
90 Test that L{ReverseProxyResource.render} passes query parameters to the | |
91 created factory. | |
92 """ | |
93 return self._testRender("/index?foo=bar", "/path?foo=bar") | |
94 | |
95 | |
96 def test_getChild(self): | |
97 """ | |
98 The L{ReverseProxyResource.getChild} method should return a resource | |
99 instance with the same class as the originating resource, forward port | |
100 and host values, and update the path value with the value passed. | |
101 """ | |
102 resource = ReverseProxyResource("127.0.0.1", 1234, "/path") | |
103 child = resource.getChild('foo', None) | |
104 # The child should keep the same class | |
105 self.assertIsInstance(child, ReverseProxyResource) | |
106 self.assertEquals(child.path, "/path/foo") | |
107 self.assertEquals(child.port, 1234) | |
108 self.assertEquals(child.host, "127.0.0.1") | |
109 | |
110 | |
111 def test_getChildWithSpecial(self): | |
112 """ | |
113 The L{ReverseProxyResource} return by C{getChild} has a path which has | |
114 already been quoted. | |
115 """ | |
116 resource = ReverseProxyResource("127.0.0.1", 1234, "/path") | |
117 child = resource.getChild(' /%', None) | |
118 self.assertEqual(child.path, "/path/%20%2F%25") | |
119 | |
120 | |
121 | |
122 class DummyParent(object): | |
123 """ | |
124 A dummy parent request that holds a channel and its transport. | |
125 | |
126 @ivar channel: the request channel. | |
127 @ivar transport: the transport of the channel. | |
128 """ | |
129 | |
130 def __init__(self, channel): | |
131 """ | |
132 Hold a reference to the channel and its transport. | |
133 """ | |
134 self.channel = channel | |
135 self.transport = channel.transport | |
136 | |
137 | |
138 | |
139 class DummyChannel(object): | |
140 """ | |
141 A dummy HTTP channel, that does nothing but holds a transport and saves | |
142 connection lost. | |
143 | |
144 @ivar transport: the transport used by the client. | |
145 @ivar lostReason: the reason saved at connection lost. | |
146 """ | |
147 | |
148 def __init__(self, transport): | |
149 """ | |
150 Hold a reference to the transport. | |
151 """ | |
152 self.transport = transport | |
153 self.lostReason = None | |
154 | |
155 | |
156 def connectionLost(self, reason): | |
157 """ | |
158 Keep track of the connection lost reason. | |
159 """ | |
160 self.lostReason = reason | |
161 | |
162 | |
163 | |
164 class ProxyClientTestCase(TestCase): | |
165 """ | |
166 Tests for L{ProxyClient}. | |
167 """ | |
168 | |
169 def _testDataForward(self, data, method="GET", body=""): | |
170 """ | |
171 Build a fake proxy connection, and send C{data} over it, checking that | |
172 it's forwarded to the originating request. | |
173 """ | |
174 # Connect everything | |
175 clientTransport = StringTransportWithDisconnection() | |
176 serverTransport = StringTransportWithDisconnection() | |
177 channel = DummyChannel(serverTransport) | |
178 parent = DummyParent(channel) | |
179 serverTransport.protocol = channel | |
180 | |
181 client = ProxyClient(method, '/foo', 'HTTP/1.0', | |
182 {"accept": "text/html"}, body, parent) | |
183 clientTransport.protocol = client | |
184 client.makeConnection(clientTransport) | |
185 | |
186 # Check data sent | |
187 self.assertEquals(clientTransport.value(), | |
188 "%s /foo HTTP/1.0\r\n" | |
189 "connection: close\r\n" | |
190 "accept: text/html\r\n\r\n%s" % (method, body)) | |
191 | |
192 # Fake an answer | |
193 client.dataReceived(data) | |
194 | |
195 # Check that the data has been forwarded | |
196 self.assertEquals(serverTransport.value(), data) | |
197 | |
198 clientTransport.loseConnection() | |
199 self.assertIsInstance(channel.lostReason, ConnectionDone) | |
200 | |
201 | |
202 def test_forward(self): | |
203 """ | |
204 When connected to the server, L{ProxyClient} should send the saved | |
205 request, with modifications of the headers, and then forward the result | |
206 to the parent request. | |
207 """ | |
208 return self._testDataForward("200 OK\r\nFoo: bar\r\n\r\nSome data\r\n") | |
209 | |
210 | |
211 def test_postData(self): | |
212 """ | |
213 Try to post content in the request, and check that the proxy client | |
214 forward the body of the request. | |
215 """ | |
216 return self._testDataForward( | |
217 "200 OK\r\nFoo: bar\r\n\r\nSome data\r\n", "POST", "Some content") | |
218 | |
219 | |
220 def test_statusWithMessage(self): | |
221 """ | |
222 If the response contains a status with a message, it should be | |
223 forwarded to the parent request with all the information. | |
224 """ | |
225 return self._testDataForward("404 Not Found\r\n") | |
226 | |
227 | |
228 def test_headersCleanups(self): | |
229 """ | |
230 The headers given at initialization should be modified: | |
231 B{proxy-connection} should be removed if present, and B{connection} | |
232 should be added. | |
233 """ | |
234 client = ProxyClient('GET', '/foo', 'HTTP/1.0', | |
235 {"accept": "text/html", "proxy-connection": "foo"}, '', None) | |
236 self.assertEquals(client.headers, | |
237 {"accept": "text/html", "connection": "close"}) | |
238 | |
239 | |
240 | |
241 class ProxyClientFactoryTestCase(TestCase): | |
242 """ | |
243 Tests for L{ProxyClientFactory}. | |
244 """ | |
245 | |
246 def test_connectionFailed(self): | |
247 """ | |
248 Check that L{ProxyClientFactory.clientConnectionFailed} produces | |
249 a B{501} response to the parent request. | |
250 """ | |
251 serverTransport = StringTransportWithDisconnection() | |
252 channel = DummyChannel(serverTransport) | |
253 parent = DummyParent(channel) | |
254 serverTransport.protocol = channel | |
255 factory = ProxyClientFactory('GET', '/foo', 'HTTP/1.0', | |
256 {"accept": "text/html"}, '', parent) | |
257 | |
258 factory.clientConnectionFailed(None, None) | |
259 self.assertEquals(serverTransport.value(), | |
260 "HTTP/1.0 501 Gateway error\r\n" | |
261 "Content-Type: text/html\r\n\r\n" | |
262 "<H1>Could not connect</H1>") | |
263 self.assertIsInstance(channel.lostReason, ConnectionDone) | |
264 | |
265 | |
266 def test_buildProtocol(self): | |
267 """ | |
268 L{ProxyClientFactory.buildProtocol} should produce a L{ProxyClient} | |
269 with the same values of attributes (with updates on the headers). | |
270 """ | |
271 factory = ProxyClientFactory('GET', '/foo', 'HTTP/1.0', | |
272 {"accept": "text/html"}, 'Some data', | |
273 None) | |
274 proto = factory.buildProtocol(None) | |
275 self.assertIsInstance(proto, ProxyClient) | |
276 self.assertEquals(proto.command, 'GET') | |
277 self.assertEquals(proto.rest, '/foo') | |
278 self.assertEquals(proto.data, 'Some data') | |
279 self.assertEquals(proto.headers, | |
280 {"accept": "text/html", "connection": "close"}) | |
281 | |
282 | |
283 | |
284 class ProxyRequestTestCase(TestCase): | |
285 """ | |
286 Tests for L{ProxyRequest}. | |
287 """ | |
288 | |
289 def _testProcess(self, uri, expectedURI, method="GET", data=""): | |
290 """ | |
291 Build a request pointing at C{uri}, and check that a proxied request | |
292 is created, pointing a C{expectedURI}. | |
293 """ | |
294 transport = StringTransportWithDisconnection() | |
295 channel = DummyChannel(transport) | |
296 reactor = FakeReactor() | |
297 request = ProxyRequest(channel, False, reactor) | |
298 request.gotLength(len(data)) | |
299 request.handleContentChunk(data) | |
300 request.requestReceived(method, 'http://example.com%s' % (uri,), | |
301 'HTTP/1.0') | |
302 | |
303 self.assertEquals(len(reactor.connect), 1) | |
304 self.assertEquals(reactor.connect[0][0], "example.com") | |
305 self.assertEquals(reactor.connect[0][1], 80) | |
306 | |
307 factory = reactor.connect[0][2] | |
308 self.assertIsInstance(factory, ProxyClientFactory) | |
309 self.assertEquals(factory.command, method) | |
310 self.assertEquals(factory.version, 'HTTP/1.0') | |
311 self.assertEquals(factory.headers, {'host': 'example.com'}) | |
312 self.assertEquals(factory.data, data) | |
313 self.assertEquals(factory.rest, expectedURI) | |
314 self.assertEquals(factory.father, request) | |
315 | |
316 | |
317 def test_process(self): | |
318 """ | |
319 L{ProxyRequest.process} should create a connection to the given server, | |
320 with a L{ProxyClientFactory} as connection factory, with the correct | |
321 parameters: | |
322 - forward comment, version and data values | |
323 - update headers with the B{host} value | |
324 - remove the host from the URL | |
325 - pass the request as parent request | |
326 """ | |
327 return self._testProcess("/foo/bar", "/foo/bar") | |
328 | |
329 | |
330 def test_processWithoutTrailingSlash(self): | |
331 """ | |
332 If the incoming request doesn't contain a slash, | |
333 L{ProxyRequest.process} should add one when instantiating | |
334 L{ProxyClientFactory}. | |
335 """ | |
336 return self._testProcess("", "/") | |
337 | |
338 | |
339 def test_processWithData(self): | |
340 """ | |
341 L{ProxyRequest.process} should be able to retrieve request body and | |
342 to forward it. | |
343 """ | |
344 return self._testProcess( | |
345 "/foo/bar", "/foo/bar", "POST", "Some content") | |
346 | |
347 | |
348 def test_processWithPort(self): | |
349 """ | |
350 Check that L{ProxyRequest.process} correctly parse port in the incoming | |
351 URL, and create a outgoing connection with this port. | |
352 """ | |
353 transport = StringTransportWithDisconnection() | |
354 channel = DummyChannel(transport) | |
355 reactor = FakeReactor() | |
356 request = ProxyRequest(channel, False, reactor) | |
357 request.gotLength(0) | |
358 request.requestReceived('GET', 'http://example.com:1234/foo/bar', | |
359 'HTTP/1.0') | |
360 | |
361 # That should create one connection, with the port parsed from the URL | |
362 self.assertEquals(len(reactor.connect), 1) | |
363 self.assertEquals(reactor.connect[0][0], "example.com") | |
364 self.assertEquals(reactor.connect[0][1], 1234) | |
365 | |
366 | |
367 | |
368 class DummyFactory(object): | |
369 """ | |
370 A simple holder for C{host} and C{port} information. | |
371 """ | |
372 | |
373 def __init__(self, host, port): | |
374 self.host = host | |
375 self.port = port | |
376 | |
377 | |
378 | |
379 class ReverseProxyRequestTestCase(TestCase): | |
380 """ | |
381 Tests for L{ReverseProxyRequest}. | |
382 """ | |
383 | |
384 def test_process(self): | |
385 """ | |
386 L{ReverseProxyRequest.process} should create a connection to its | |
387 factory host/port, using a L{ProxyClientFactory} instantiated with the | |
388 correct parameters, and particulary set the B{host} header to the | |
389 factory host. | |
390 """ | |
391 transport = StringTransportWithDisconnection() | |
392 channel = DummyChannel(transport) | |
393 reactor = FakeReactor() | |
394 request = ReverseProxyRequest(channel, False, reactor) | |
395 request.factory = DummyFactory("example.com", 1234) | |
396 request.gotLength(0) | |
397 request.requestReceived('GET', '/foo/bar', 'HTTP/1.0') | |
398 | |
399 # Check that one connection has been created, to the good host/port | |
400 self.assertEquals(len(reactor.connect), 1) | |
401 self.assertEquals(reactor.connect[0][0], "example.com") | |
402 self.assertEquals(reactor.connect[0][1], 1234) | |
403 | |
404 # Check the factory passed to the connect, and its headers | |
405 factory = reactor.connect[0][2] | |
406 self.assertIsInstance(factory, ProxyClientFactory) | |
407 self.assertEquals(factory.headers, {'host': 'example.com'}) | |
OLD | NEW |