~ntt-pf-lab/nova/monkey_patch_notification

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/web/test/test_proxy.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2007-2010 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.test.proto_helpers import MemoryReactor
 
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
from twisted.web.test.test_web import DummyRequest
 
17
 
 
18
 
 
19
class ReverseProxyResourceTestCase(TestCase):
 
20
    """
 
21
    Tests for L{ReverseProxyResource}.
 
22
    """
 
23
 
 
24
    def _testRender(self, uri, expectedURI):
 
25
        """
 
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}.
 
28
        """
 
29
        root = Resource()
 
30
        reactor = MemoryReactor()
 
31
        resource = ReverseProxyResource("127.0.0.1", 1234, "/path", reactor)
 
32
        root.putChild('index', resource)
 
33
        site = Site(root)
 
34
 
 
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)
 
40
 
 
41
        channel.dataReceived("GET %s HTTP/1.1\r\nAccept: text/html\r\n\r\n" %
 
42
                             (uri,))
 
43
 
 
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)
 
48
 
 
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")
 
54
 
 
55
 
 
56
    def test_render(self):
 
57
        """
 
58
        Test that L{ReverseProxyResource.render} initiates a connection to the
 
59
        given server with a L{ProxyClientFactory} as parameter.
 
60
        """
 
61
        return self._testRender("/index", "/path")
 
62
 
 
63
 
 
64
    def test_renderWithQuery(self):
 
65
        """
 
66
        Test that L{ReverseProxyResource.render} passes query parameters to the
 
67
        created factory.
 
68
        """
 
69
        return self._testRender("/index?foo=bar", "/path?foo=bar")
 
70
 
 
71
 
 
72
    def test_getChild(self):
 
73
        """
 
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.
 
77
        """
 
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")
 
85
 
 
86
 
 
87
    def test_getChildWithSpecial(self):
 
88
        """
 
89
        The L{ReverseProxyResource} return by C{getChild} has a path which has
 
90
        already been quoted.
 
91
        """
 
92
        resource = ReverseProxyResource("127.0.0.1", 1234, "/path")
 
93
        child = resource.getChild(' /%', None)
 
94
        self.assertEqual(child.path, "/path/%20%2F%25")
 
95
 
 
96
 
 
97
 
 
98
class DummyChannel(object):
 
99
    """
 
100
    A dummy HTTP channel, that does nothing but holds a transport and saves
 
101
    connection lost.
 
102
 
 
103
    @ivar transport: the transport used by the client.
 
104
    @ivar lostReason: the reason saved at connection lost.
 
105
    """
 
106
 
 
107
    def __init__(self, transport):
 
108
        """
 
109
        Hold a reference to the transport.
 
110
        """
 
111
        self.transport = transport
 
112
        self.lostReason = None
 
113
 
 
114
 
 
115
    def connectionLost(self, reason):
 
116
        """
 
117
        Keep track of the connection lost reason.
 
118
        """
 
119
        self.lostReason = reason
 
120
 
 
121
 
 
122
 
 
123
class ProxyClientTestCase(TestCase):
 
124
    """
 
125
    Tests for L{ProxyClient}.
 
126
    """
 
127
 
 
128
    def _parseOutHeaders(self, content):
 
129
        """
 
130
        Parse the headers out of some web content.
 
131
 
 
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.
 
136
        """
 
137
        headers, body = content.split('\r\n\r\n')
 
138
        headers = headers.split('\r\n')
 
139
        requestLine = headers.pop(0)
 
140
        return (
 
141
            requestLine, dict(header.split(': ') for header in headers), body)
 
142
 
 
143
 
 
144
    def makeRequest(self, path):
 
145
        """
 
146
        Make a dummy request object for the URL path.
 
147
 
 
148
        @param path: A URL path, beginning with a slash.
 
149
        @return: A L{DummyRequest}.
 
150
        """
 
151
        return DummyRequest(path)
 
152
 
 
153
 
 
154
    def makeProxyClient(self, request, method="GET", headers=None,
 
155
                        requestBody=""):
 
156
        """
 
157
        Make a L{ProxyClient} object used for testing.
 
158
 
 
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
 
164
            string.
 
165
        @return: A L{ProxyClient}
 
166
        """
 
167
        if headers is None:
 
168
            headers = {"accept": "text/html"}
 
169
        path = '/' + request.postpath
 
170
        return ProxyClient(
 
171
            method, path, 'HTTP/1.0', headers, requestBody, request)
 
172
 
 
173
 
 
174
    def connectProxy(self, proxyClient):
 
175
        """
 
176
        Connect a proxy client to a L{StringTransportWithDisconnection}.
 
177
 
 
178
        @param proxyClient: A L{ProxyClient}.
 
179
        @return: The L{StringTransportWithDisconnection}.
 
180
        """
 
181
        clientTransport = StringTransportWithDisconnection()
 
182
        clientTransport.protocol = proxyClient
 
183
        proxyClient.makeConnection(clientTransport)
 
184
        return clientTransport
 
185
 
 
186
 
 
187
    def assertForwardsHeaders(self, proxyClient, requestLine, headers):
 
188
        """
 
189
        Assert that C{proxyClient} sends C{headers} when it connects.
 
190
 
 
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
 
195
            bytes.
 
196
        """
 
197
        self.connectProxy(proxyClient)
 
198
        requestContent = proxyClient.transport.value()
 
199
        receivedLine, receivedHeaders, body = self._parseOutHeaders(
 
200
            requestContent)
 
201
        self.assertEquals(receivedLine, requestLine)
 
202
        self.assertEquals(receivedHeaders, headers)
 
203
        return body
 
204
 
 
205
 
 
206
    def makeResponseBytes(self, code, message, headers, body):
 
207
        lines = ["HTTP/1.0 %d %s" % (code, message)]
 
208
        for header, values in headers:
 
209
            for value in values:
 
210
                lines.append("%s: %s" % (header, value))
 
211
        lines.extend(['', body])
 
212
        return '\r\n'.join(lines)
 
213
 
 
214
 
 
215
    def assertForwardsResponse(self, request, code, message, headers, body):
 
216
        """
 
217
        Assert that C{request} has forwarded a response from the server.
 
218
 
 
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.
 
224
        """
 
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)
 
233
 
 
234
 
 
235
    def _testDataForward(self, code, message, headers, body, method="GET",
 
236
                         requestBody="", loseConnection=True):
 
237
        """
 
238
        Build a fake proxy connection, and send C{data} over it, checking that
 
239
        it's forwarded to the originating request.
 
240
        """
 
241
        request = self.makeRequest('foo')
 
242
        client = self.makeProxyClient(
 
243
            request, method, {'accept': 'text/html'}, requestBody)
 
244
 
 
245
        receivedBody = self.assertForwardsHeaders(
 
246
            client, '%s /foo HTTP/1.0' % (method,),
 
247
            {'connection': 'close', 'accept': 'text/html'})
 
248
 
 
249
        self.assertEquals(receivedBody, requestBody)
 
250
 
 
251
        # Fake an answer
 
252
        client.dataReceived(
 
253
            self.makeResponseBytes(code, message, headers, body))
 
254
 
 
255
        # Check that the response data has been forwarded back to the original
 
256
        # requester.
 
257
        self.assertForwardsResponse(request, code, message, headers, body)
 
258
 
 
259
        # Check that when the response is done, the request is finished.
 
260
        if loseConnection:
 
261
            client.transport.loseConnection()
 
262
 
 
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
 
265
        # sockets for us.
 
266
        self.assertFalse(client.transport.connected)
 
267
        self.assertEquals(request.finished, 1)
 
268
 
 
269
 
 
270
    def test_forward(self):
 
271
        """
 
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.
 
275
        """
 
276
        return self._testDataForward(
 
277
            200, "OK", [("Foo", ["bar", "baz"])], "Some data\r\n")
 
278
 
 
279
 
 
280
    def test_postData(self):
 
281
        """
 
282
        Try to post content in the request, and check that the proxy client
 
283
        forward the body of the request.
 
284
        """
 
285
        return self._testDataForward(
 
286
            200, "OK", [("Foo", ["bar"])], "Some data\r\n", "POST", "Some content")
 
287
 
 
288
 
 
289
    def test_statusWithMessage(self):
 
290
        """
 
291
        If the response contains a status with a message, it should be
 
292
        forwarded to the parent request with all the information.
 
293
        """
 
294
        return self._testDataForward(
 
295
            404, "Not Found", [], "")
 
296
 
 
297
 
 
298
    def test_contentLength(self):
 
299
        """
 
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.
 
302
        """
 
303
        data = "foo bar baz"
 
304
        return self._testDataForward(
 
305
            200, "OK", [("Content-Length", [str(len(data))])], data)
 
306
 
 
307
 
 
308
    def test_losesConnection(self):
 
309
        """
 
310
        If the response contains a I{Content-Length} header, the outgoing
 
311
        connection is closed when all response body data has been received.
 
312
        """
 
313
        data = "foo bar baz"
 
314
        return self._testDataForward(
 
315
            200, "OK", [("Content-Length", [str(len(data))])], data,
 
316
            loseConnection=False)
 
317
 
 
318
 
 
319
    def test_headersCleanups(self):
 
320
        """
 
321
        The headers given at initialization should be modified:
 
322
        B{proxy-connection} should be removed if present, and B{connection}
 
323
        should be added.
 
324
        """
 
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"})
 
329
 
 
330
 
 
331
    def test_keepaliveNotForwarded(self):
 
332
        """
 
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
 
335
        the client.
 
336
        """
 
337
        headers = {
 
338
            "accept": "text/html",
 
339
            'keep-alive': '300',
 
340
            'connection': 'keep-alive',
 
341
            }
 
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)
 
348
 
 
349
 
 
350
    def test_defaultHeadersOverridden(self):
 
351
        """
 
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.
 
355
        """
 
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)
 
362
        headers = {
 
363
            'Server': ['bar'],
 
364
            'Date': ['2010-01-01'],
 
365
            'Content-Type': ['application/x-baz'],
 
366
            }
 
367
        client.dataReceived(
 
368
            self.makeResponseBytes(200, "OK", headers.items(), ''))
 
369
        self.assertForwardsResponse(
 
370
            request, 200, 'OK', headers.items(), '')
 
371
 
 
372
 
 
373
 
 
374
class ProxyClientFactoryTestCase(TestCase):
 
375
    """
 
376
    Tests for L{ProxyClientFactory}.
 
377
    """
 
378
 
 
379
    def test_connectionFailed(self):
 
380
        """
 
381
        Check that L{ProxyClientFactory.clientConnectionFailed} produces
 
382
        a B{501} response to the parent request.
 
383
        """
 
384
        request = DummyRequest(['foo'])
 
385
        factory = ProxyClientFactory('GET', '/foo', 'HTTP/1.0',
 
386
                                     {"accept": "text/html"}, '', request)
 
387
 
 
388
        factory.clientConnectionFailed(None, None)
 
389
        self.assertEquals(request.responseCode, 501)
 
390
        self.assertEquals(request.responseMessage, "Gateway error")
 
391
        self.assertEquals(
 
392
            list(request.responseHeaders.getAllRawHeaders()),
 
393
            [("Content-Type", ["text/html"])])
 
394
        self.assertEquals(
 
395
            ''.join(request.written),
 
396
            "<H1>Could not connect</H1>")
 
397
        self.assertEquals(request.finished, 1)
 
398
 
 
399
 
 
400
    def test_buildProtocol(self):
 
401
        """
 
402
        L{ProxyClientFactory.buildProtocol} should produce a L{ProxyClient}
 
403
        with the same values of attributes (with updates on the headers).
 
404
        """
 
405
        factory = ProxyClientFactory('GET', '/foo', 'HTTP/1.0',
 
406
                                     {"accept": "text/html"}, 'Some data',
 
407
                                     None)
 
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"})
 
415
 
 
416
 
 
417
 
 
418
class ProxyRequestTestCase(TestCase):
 
419
    """
 
420
    Tests for L{ProxyRequest}.
 
421
    """
 
422
 
 
423
    def _testProcess(self, uri, expectedURI, method="GET", data=""):
 
424
        """
 
425
        Build a request pointing at C{uri}, and check that a proxied request
 
426
        is created, pointing a C{expectedURI}.
 
427
        """
 
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,),
 
435
                                'HTTP/1.0')
 
436
 
 
437
        self.assertEquals(len(reactor.tcpClients), 1)
 
438
        self.assertEquals(reactor.tcpClients[0][0], "example.com")
 
439
        self.assertEquals(reactor.tcpClients[0][1], 80)
 
440
 
 
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)
 
449
 
 
450
 
 
451
    def test_process(self):
 
452
        """
 
453
        L{ProxyRequest.process} should create a connection to the given server,
 
454
        with a L{ProxyClientFactory} as connection factory, with the correct
 
455
        parameters:
 
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
 
460
        """
 
461
        return self._testProcess("/foo/bar", "/foo/bar")
 
462
 
 
463
 
 
464
    def test_processWithoutTrailingSlash(self):
 
465
        """
 
466
        If the incoming request doesn't contain a slash,
 
467
        L{ProxyRequest.process} should add one when instantiating
 
468
        L{ProxyClientFactory}.
 
469
        """
 
470
        return self._testProcess("", "/")
 
471
 
 
472
 
 
473
    def test_processWithData(self):
 
474
        """
 
475
        L{ProxyRequest.process} should be able to retrieve request body and
 
476
        to forward it.
 
477
        """
 
478
        return self._testProcess(
 
479
            "/foo/bar", "/foo/bar", "POST", "Some content")
 
480
 
 
481
 
 
482
    def test_processWithPort(self):
 
483
        """
 
484
        Check that L{ProxyRequest.process} correctly parse port in the incoming
 
485
        URL, and create a outgoing connection with this port.
 
486
        """
 
487
        transport = StringTransportWithDisconnection()
 
488
        channel = DummyChannel(transport)
 
489
        reactor = MemoryReactor()
 
490
        request = ProxyRequest(channel, False, reactor)
 
491
        request.gotLength(0)
 
492
        request.requestReceived('GET', 'http://example.com:1234/foo/bar',
 
493
                                'HTTP/1.0')
 
494
 
 
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)
 
499
 
 
500
 
 
501
 
 
502
class DummyFactory(object):
 
503
    """
 
504
    A simple holder for C{host} and C{port} information.
 
505
    """
 
506
 
 
507
    def __init__(self, host, port):
 
508
        self.host = host
 
509
        self.port = port
 
510
 
 
511
 
 
512
 
 
513
class ReverseProxyRequestTestCase(TestCase):
 
514
    """
 
515
    Tests for L{ReverseProxyRequest}.
 
516
    """
 
517
 
 
518
    def test_process(self):
 
519
        """
 
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
 
523
        factory host.
 
524
        """
 
525
        transport = StringTransportWithDisconnection()
 
526
        channel = DummyChannel(transport)
 
527
        reactor = MemoryReactor()
 
528
        request = ReverseProxyRequest(channel, False, reactor)
 
529
        request.factory = DummyFactory("example.com", 1234)
 
530
        request.gotLength(0)
 
531
        request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
 
532
 
 
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)
 
537
 
 
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'})