| 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 |