~bzr-pqm/bzr/bzr.dev

2052.3.1 by John Arbash Meinel
Add tests to cleanup the copyright of all source files
1
# Copyright (C) 2005, 2006 Canonical Ltd
1540.3.24 by Martin Pool
Add new protocol 'http+pycurl' that always uses PyCurl.
2
#
1540.3.15 by Martin Pool
[merge] large merge to sync with bzr.dev
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
1540.3.24 by Martin Pool
Add new protocol 'http+pycurl' that always uses PyCurl.
7
#
1540.3.15 by Martin Pool
[merge] large merge to sync with bzr.dev
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
1540.3.24 by Martin Pool
Add new protocol 'http+pycurl' that always uses PyCurl.
12
#
1540.3.15 by Martin Pool
[merge] large merge to sync with bzr.dev
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
1185.16.68 by Martin Pool
- http url fixes suggested by Robey Pointer, and tests
16
3111.1.30 by Vincent Ladeuil
Update NEWS. Some cosmetic changes.
17
"""Tests for HTTP implementations.
3111.1.10 by Vincent Ladeuil
Finish http parameterization, 24 auth tests failing for pycurl (not
18
3111.1.30 by Vincent Ladeuil
Update NEWS. Some cosmetic changes.
19
This module defines a load_tests() method that parametrize tests classes for
20
transport implementation, http protocol versions and authentication schemes.
3111.1.10 by Vincent Ladeuil
Finish http parameterization, 24 auth tests failing for pycurl (not
21
"""
1540.3.3 by Martin Pool
Review updates of pycurl transport
22
1540.3.22 by Martin Pool
[patch] Add TestCase.assertIsInstance
23
# TODO: Should be renamed to bzrlib.transport.http.tests?
2167.3.5 by v.ladeuil+lp at free
Tests for proxies, covering #74759.
24
# TODO: What about renaming to bzrlib.tests.transport.http ?
1540.3.22 by Martin Pool
[patch] Add TestCase.assertIsInstance
25
2363.4.12 by Vincent Ladeuil
Take jam's review comments into account. Fix typos, give better
26
from cStringIO import StringIO
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
27
import httplib
2167.3.5 by v.ladeuil+lp at free
Tests for proxies, covering #74759.
28
import os
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
29
import select
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
30
import SimpleHTTPServer
2000.2.2 by John Arbash Meinel
Update the urllib.has test.
31
import socket
2420.1.20 by Vincent Ladeuil
Fix test failure on pqm.
32
import sys
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
33
import threading
2000.2.2 by John Arbash Meinel
Update the urllib.has test.
34
1553.1.2 by James Henstridge
Add a test to make sure the user-agent header is being sent correctly.
35
import bzrlib
2164.2.22 by Vincent Ladeuil
Take Aaron's review comments into account.
36
from bzrlib import (
2900.2.6 by Vincent Ladeuil
Make http aware of authentication config.
37
    config,
2164.2.22 by Vincent Ladeuil
Take Aaron's review comments into account.
38
    errors,
39
    osutils,
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
40
    tests,
3111.1.10 by Vincent Ladeuil
Finish http parameterization, 24 auth tests failing for pycurl (not
41
    transport,
2363.4.12 by Vincent Ladeuil
Take jam's review comments into account. Fix typos, give better
42
    ui,
2164.2.22 by Vincent Ladeuil
Take Aaron's review comments into account.
43
    urlutils,
44
    )
3102.1.1 by Vincent Ladeuil
Rename bzrlib/test/HTTPTestUtils.py to bzrlib/tests/http_utils.py and fix
45
from bzrlib.tests import (
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
46
    http_server,
3111.1.7 by Vincent Ladeuil
Further refactoring.
47
    http_utils,
3102.1.1 by Vincent Ladeuil
Rename bzrlib/test/HTTPTestUtils.py to bzrlib/tests/http_utils.py and fix
48
    )
3111.1.25 by Vincent Ladeuil
Fix the smart server failing test and use it against protocol combinations.
49
from bzrlib.transport import (
50
    http,
51
    remote,
52
    )
2004.3.3 by vila
Better (but still incomplete) design for bogus servers.
53
from bzrlib.transport.http import (
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
54
    _urllib,
2164.2.29 by Vincent Ladeuil
Test the http redirection at the request level even if it's not
55
    _urllib2_wrappers,
2004.3.3 by vila
Better (but still incomplete) design for bogus servers.
56
    )
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
57
58
59
try:
60
    from bzrlib.transport.http._pycurl import PyCurlTransport
61
    pycurl_present = True
62
except errors.DependencyNotPresent:
63
    pycurl_present = False
64
65
66
class TransportAdapter(tests.TestScenarioApplier):
67
    """Generate the same test for each transport implementation."""
68
69
    def __init__(self):
70
        transport_scenarios = [
71
            ('urllib', dict(_transport=_urllib.HttpTransport_urllib,
72
                            _server=http_server.HttpServer_urllib,
73
                            _qualified_prefix='http+urllib',)),
74
            ]
75
        if pycurl_present:
76
            transport_scenarios.append(
77
                ('pycurl', dict(_transport=PyCurlTransport,
78
                                _server=http_server.HttpServer_PyCurl,
79
                                _qualified_prefix='http+pycurl',)))
80
        self.scenarios = transport_scenarios
81
82
83
class TransportProtocolAdapter(TransportAdapter):
84
    """Generate the same test for each protocol implementation.
85
86
    In addition to the transport adaptatation that we inherit from.
87
    """
88
89
    def __init__(self):
90
        super(TransportProtocolAdapter, self).__init__()
91
        protocol_scenarios = [
92
            ('HTTP/1.0',  dict(_protocol_version='HTTP/1.0')),
93
            ('HTTP/1.1',  dict(_protocol_version='HTTP/1.1')),
94
            ]
95
        self.scenarios = tests.multiply_scenarios(self.scenarios,
96
                                                  protocol_scenarios)
97
98
99
class TransportProtocolAuthenticationAdapter(TransportProtocolAdapter):
100
    """Generate the same test for each authentication scheme implementation.
101
102
    In addition to the protocol adaptatation that we inherit from.
103
    """
104
105
    def __init__(self):
106
        super(TransportProtocolAuthenticationAdapter, self).__init__()
107
        auth_scheme_scenarios = [
108
            ('basic', dict(_auth_scheme='basic')),
109
            ('digest', dict(_auth_scheme='digest')),
110
            ]
111
112
        self.scenarios = tests.multiply_scenarios(self.scenarios,
113
                                                  auth_scheme_scenarios)
114
115
def load_tests(standard_tests, module, loader):
116
    """Multiply tests for http clients and protocol versions."""
117
    # one for each transport
118
    t_adapter = TransportAdapter()
119
    t_classes= (TestHttpTransportRegistration,
120
                TestHttpTransportUrls,
121
                )
122
    is_testing_for_transports = tests.condition_isinstance(t_classes)
123
124
    # multiplied by one for each protocol version
125
    tp_adapter = TransportProtocolAdapter()
3111.1.25 by Vincent Ladeuil
Fix the smart server failing test and use it against protocol combinations.
126
    tp_classes= (SmartHTTPTunnellingTest,
127
                 TestDoCatchRedirections,
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
128
                 TestHTTPConnections,
129
                 TestHTTPRedirections,
130
                 TestHTTPSilentRedirections,
131
                 TestLimitedRangeRequestServer,
132
                 TestPost,
133
                 TestProxyHttpServer,
134
                 TestRanges,
135
                 TestSpecificRequestHandler,
136
                 )
137
    is_also_testing_for_protocols = tests.condition_isinstance(tp_classes)
138
139
    # multiplied by one for each authentication scheme
140
    tpa_adapter = TransportProtocolAuthenticationAdapter()
141
    tpa_classes = (TestAuth,
142
                   )
3111.1.30 by Vincent Ladeuil
Update NEWS. Some cosmetic changes.
143
    is_also_testing_for_authentication = tests.condition_isinstance(
144
        tpa_classes)
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
145
146
    result = loader.suiteClass()
3111.1.24 by Vincent Ladeuil
Cleanups.
147
    for test_class in tests.iter_suite_tests(standard_tests):
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
148
        # Each test class is either standalone or testing for some combination
149
        # of transport, protocol version, authentication scheme. Use the right
150
        # adpater (or none) depending on the class.
3111.1.24 by Vincent Ladeuil
Cleanups.
151
        if is_testing_for_transports(test_class):
152
            result.addTests(t_adapter.adapt(test_class))
153
        elif is_also_testing_for_protocols(test_class):
154
            result.addTests(tp_adapter.adapt(test_class))
155
        elif is_also_testing_for_authentication(test_class):
156
            result.addTests(tpa_adapter.adapt(test_class))
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
157
        else:
3111.1.24 by Vincent Ladeuil
Cleanups.
158
            result.addTest(test_class)
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
159
    return result
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
160
1786.1.8 by John Arbash Meinel
[merge] Johan Rydberg test updates
161
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
162
class FakeManager(object):
1786.1.8 by John Arbash Meinel
[merge] Johan Rydberg test updates
163
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
164
    def __init__(self):
165
        self.credentials = []
2004.3.1 by vila
Test ConnectionError exceptions.
166
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
167
    def add_password(self, realm, host, username, password):
168
        self.credentials.append([realm, host, username, password])
169
1553.1.2 by James Henstridge
Add a test to make sure the user-agent header is being sent correctly.
170
3111.1.29 by Vincent Ladeuil
Cancel RecordingServer move, that was useless.
171
class RecordingServer(object):
172
    """A fake HTTP server.
173
    
174
    It records the bytes sent to it, and replies with a 200.
175
    """
176
177
    def __init__(self, expect_body_tail=None):
178
        """Constructor.
179
180
        :type expect_body_tail: str
181
        :param expect_body_tail: a reply won't be sent until this string is
182
            received.
183
        """
184
        self._expect_body_tail = expect_body_tail
185
        self.host = None
186
        self.port = None
187
        self.received_bytes = ''
188
189
    def setUp(self):
190
        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
191
        self._sock.bind(('127.0.0.1', 0))
192
        self.host, self.port = self._sock.getsockname()
193
        self._ready = threading.Event()
194
        self._thread = threading.Thread(target=self._accept_read_and_reply)
195
        self._thread.setDaemon(True)
196
        self._thread.start()
197
        self._ready.wait(5)
198
199
    def _accept_read_and_reply(self):
200
        self._sock.listen(1)
201
        self._ready.set()
202
        self._sock.settimeout(5)
203
        try:
204
            conn, address = self._sock.accept()
205
            # On win32, the accepted connection will be non-blocking to start
206
            # with because we're using settimeout.
207
            conn.setblocking(True)
208
            while not self.received_bytes.endswith(self._expect_body_tail):
209
                self.received_bytes += conn.recv(4096)
210
            conn.sendall('HTTP/1.1 200 OK\r\n')
211
        except socket.timeout:
212
            # Make sure the client isn't stuck waiting for us to e.g. accept.
213
            self._sock.close()
214
        except socket.error:
215
            # The client may have already closed the socket.
216
            pass
217
218
    def tearDown(self):
219
        try:
220
            self._sock.close()
221
        except socket.error:
222
            # We might have already closed it.  We don't care.
223
            pass
224
        self.host = None
225
        self.port = None
226
227
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
228
class TestHTTPServer(tests.TestCase):
229
    """Test the HTTP servers implementations."""
230
231
    def test_invalid_protocol(self):
232
        class BogusRequestHandler(http_server.TestingHTTPRequestHandler):
233
234
            protocol_version = 'HTTP/0.1'
235
236
        server = http_server.HttpServer(BogusRequestHandler)
237
        try:
238
            self.assertRaises(httplib.UnknownProtocol,server.setUp)
239
        except:
240
            server.tearDown()
241
            self.fail('HTTP Server creation did not raise UnknownProtocol')
242
3111.1.17 by Vincent Ladeuil
Add tests for the protocol version parameter.
243
    def test_force_invalid_protocol(self):
244
        server = http_server.HttpServer(protocol_version='HTTP/0.1')
245
        try:
246
            self.assertRaises(httplib.UnknownProtocol,server.setUp)
247
        except:
248
            server.tearDown()
249
            self.fail('HTTP Server creation did not raise UnknownProtocol')
250
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
251
    def test_server_start_and_stop(self):
252
        server = http_server.HttpServer()
253
        server.setUp()
254
        self.assertTrue(server._http_running)
255
        server.tearDown()
256
        self.assertFalse(server._http_running)
257
258
    def test_create_http_server_one_zero(self):
259
        class RequestHandlerOneZero(http_server.TestingHTTPRequestHandler):
260
261
            protocol_version = 'HTTP/1.0'
262
263
        server = http_server.HttpServer(RequestHandlerOneZero)
264
        server.setUp()
3111.1.17 by Vincent Ladeuil
Add tests for the protocol version parameter.
265
        self.addCleanup(server.tearDown)
266
        self.assertIsInstance(server._httpd, http_server.TestingHTTPServer)
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
267
268
    def test_create_http_server_one_one(self):
269
        class RequestHandlerOneOne(http_server.TestingHTTPRequestHandler):
270
271
            protocol_version = 'HTTP/1.1'
272
273
        server = http_server.HttpServer(RequestHandlerOneOne)
274
        server.setUp()
3111.1.17 by Vincent Ladeuil
Add tests for the protocol version parameter.
275
        self.addCleanup(server.tearDown)
276
        self.assertIsInstance(server._httpd,
277
                              http_server.TestingThreadingHTTPServer)
278
279
    def test_create_http_server_force_one_one(self):
280
        class RequestHandlerOneZero(http_server.TestingHTTPRequestHandler):
281
282
            protocol_version = 'HTTP/1.0'
283
284
        server = http_server.HttpServer(RequestHandlerOneZero,
285
                                        protocol_version='HTTP/1.1')
286
        server.setUp()
287
        self.addCleanup(server.tearDown)
288
        self.assertIsInstance(server._httpd,
289
                              http_server.TestingThreadingHTTPServer)
290
291
    def test_create_http_server_force_one_zero(self):
292
        class RequestHandlerOneOne(http_server.TestingHTTPRequestHandler):
293
294
            protocol_version = 'HTTP/1.1'
295
296
        server = http_server.HttpServer(RequestHandlerOneOne,
297
                                        protocol_version='HTTP/1.0')
298
        server.setUp()
299
        self.addCleanup(server.tearDown)
300
        self.assertIsInstance(server._httpd,
301
                              http_server.TestingHTTPServer)
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
302
303
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
304
class TestWithTransport_pycurl(object):
305
    """Test case to inherit from if pycurl is present"""
306
307
    def _get_pycurl_maybe(self):
308
        try:
309
            from bzrlib.transport.http._pycurl import PyCurlTransport
310
            return PyCurlTransport
311
        except errors.DependencyNotPresent:
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
312
            raise tests.TestSkipped('pycurl not present')
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
313
314
    _transport = property(_get_pycurl_maybe)
315
316
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
317
class TestHttpUrls(tests.TestCase):
1786.1.8 by John Arbash Meinel
[merge] Johan Rydberg test updates
318
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
319
    # TODO: This should be moved to authorization tests once they
320
    # are written.
2004.1.40 by v.ladeuil+lp at free
Fix the race condition again and correct some small typos to be in
321
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
322
    def test_url_parsing(self):
323
        f = FakeManager()
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
324
        url = http.extract_auth('http://example.com', f)
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
325
        self.assertEquals('http://example.com', url)
326
        self.assertEquals(0, len(f.credentials))
3111.1.30 by Vincent Ladeuil
Update NEWS. Some cosmetic changes.
327
        url = http.extract_auth(
328
            'http://user:pass@www.bazaar-vcs.org/bzr/bzr.dev', f)
1185.50.94 by John Arbash Meinel
Updated web page url to http://bazaar-vcs.org
329
        self.assertEquals('http://www.bazaar-vcs.org/bzr/bzr.dev', url)
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
330
        self.assertEquals(1, len(f.credentials))
2004.3.1 by vila
Test ConnectionError exceptions.
331
        self.assertEquals([None, 'www.bazaar-vcs.org', 'user', 'pass'],
332
                          f.credentials[0])
333
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
334
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
335
class TestHttpTransportUrls(tests.TestCase):
336
    """Test the http urls."""
337
338
    def test_abs_url(self):
339
        """Construction of absolute http URLs"""
340
        t = self._transport('http://bazaar-vcs.org/bzr/bzr.dev/')
341
        eq = self.assertEqualDiff
342
        eq(t.abspath('.'), 'http://bazaar-vcs.org/bzr/bzr.dev')
343
        eq(t.abspath('foo/bar'), 'http://bazaar-vcs.org/bzr/bzr.dev/foo/bar')
344
        eq(t.abspath('.bzr'), 'http://bazaar-vcs.org/bzr/bzr.dev/.bzr')
345
        eq(t.abspath('.bzr/1//2/./3'),
346
           'http://bazaar-vcs.org/bzr/bzr.dev/.bzr/1/2/3')
347
348
    def test_invalid_http_urls(self):
349
        """Trap invalid construction of urls"""
350
        t = self._transport('http://bazaar-vcs.org/bzr/bzr.dev/')
351
        self.assertRaises(errors.InvalidURL,
352
                          self._transport,
353
                          'http://http://bazaar-vcs.org/bzr/bzr.dev/')
354
355
    def test_http_root_urls(self):
356
        """Construction of URLs from server root"""
357
        t = self._transport('http://bzr.ozlabs.org/')
358
        eq = self.assertEqualDiff
359
        eq(t.abspath('.bzr/tree-version'),
360
           'http://bzr.ozlabs.org/.bzr/tree-version')
361
362
    def test_http_impl_urls(self):
363
        """There are servers which ask for particular clients to connect"""
364
        server = self._server()
365
        try:
366
            server.setUp()
367
            url = server.get_url()
368
            self.assertTrue(url.startswith('%s://' % self._qualified_prefix))
369
        finally:
370
            server.tearDown()
371
372
3111.1.9 by Vincent Ladeuil
Most refactoring regarding parameterization for urllib/pycurl and custom
373
class TestHttps_pycurl(TestWithTransport_pycurl, tests.TestCase):
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
374
375
    # TODO: This should really be moved into another pycurl
376
    # specific test. When https tests will be implemented, take
377
    # this one into account.
378
    def test_pycurl_without_https_support(self):
379
        """Test that pycurl without SSL do not fail with a traceback.
380
381
        For the purpose of the test, we force pycurl to ignore
382
        https by supplying a fake version_info that do not
383
        support it.
384
        """
385
        try:
386
            import pycurl
387
        except ImportError:
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
388
            raise tests.TestSkipped('pycurl not present')
3111.1.14 by Vincent Ladeuil
Fix test leakage.
389
390
        version_info_orig = pycurl.version_info
391
        try:
392
            # Now that we have pycurl imported, we can fake its version_info
393
            # This was taken from a windows pycurl without SSL
394
            # (thanks to bialix)
395
            pycurl.version_info = lambda : (2,
396
                                            '7.13.2',
397
                                            462082,
398
                                            'i386-pc-win32',
399
                                            2576,
400
                                            None,
401
                                            0,
402
                                            None,
403
                                            ('ftp', 'gopher', 'telnet',
404
                                             'dict', 'ldap', 'http', 'file'),
405
                                            None,
406
                                            0,
407
                                            None)
408
            self.assertRaises(errors.DependencyNotPresent, self._transport,
409
                              'https://launchpad.net')
410
        finally:
411
            # Restore the right function
412
            pycurl.version_info = version_info_orig
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
413
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
414
415
class TestHTTPConnections(http_utils.TestCaseWithWebserver):
416
    """Test the http connections."""
417
418
    def setUp(self):
419
        http_utils.TestCaseWithWebserver.setUp(self)
420
        self.build_tree(['foo/', 'foo/bar'], line_endings='binary',
421
                        transport=self.get_transport())
422
423
    def test_http_has(self):
424
        server = self.get_readonly_server()
425
        t = self._transport(server.get_url())
426
        self.assertEqual(t.has('foo/bar'), True)
427
        self.assertEqual(len(server.logs), 1)
428
        self.assertContainsRe(server.logs[0],
429
            r'"HEAD /foo/bar HTTP/1.." (200|302) - "-" "bzr/')
430
431
    def test_http_has_not_found(self):
432
        server = self.get_readonly_server()
433
        t = self._transport(server.get_url())
434
        self.assertEqual(t.has('not-found'), False)
435
        self.assertContainsRe(server.logs[1],
436
            r'"HEAD /not-found HTTP/1.." 404 - "-" "bzr/')
437
438
    def test_http_get(self):
439
        server = self.get_readonly_server()
440
        t = self._transport(server.get_url())
441
        fp = t.get('foo/bar')
442
        self.assertEqualDiff(
443
            fp.read(),
444
            'contents of foo/bar\n')
445
        self.assertEqual(len(server.logs), 1)
446
        self.assertTrue(server.logs[0].find(
447
            '"GET /foo/bar HTTP/1.1" 200 - "-" "bzr/%s'
448
            % bzrlib.__version__) > -1)
449
450
    def test_get_smart_medium(self):
451
        # For HTTP, get_smart_medium should return the transport object.
452
        server = self.get_readonly_server()
453
        http_transport = self._transport(server.get_url())
454
        medium = http_transport.get_smart_medium()
455
        self.assertIs(medium, http_transport)
456
457
    def test_has_on_bogus_host(self):
458
        # Get a free address and don't 'accept' on it, so that we
459
        # can be sure there is no http handler there, but set a
460
        # reasonable timeout to not slow down tests too much.
461
        default_timeout = socket.getdefaulttimeout()
462
        try:
463
            socket.setdefaulttimeout(2)
464
            s = socket.socket()
465
            s.bind(('localhost', 0))
466
            t = self._transport('http://%s:%s/' % s.getsockname())
467
            self.assertRaises(errors.ConnectionError, t.has, 'foo/bar')
468
        finally:
469
            socket.setdefaulttimeout(default_timeout)
470
471
472
class TestHttpTransportRegistration(tests.TestCase):
473
    """Test registrations of various http implementations"""
474
475
    def test_http_registered(self):
476
        t = transport.get_transport('%s://foo.com/' % self._qualified_prefix)
477
        self.assertIsInstance(t, transport.Transport)
478
        self.assertIsInstance(t, self._transport)
479
480
481
class TestPost(tests.TestCase):
482
483
    def test_post_body_is_received(self):
3111.1.29 by Vincent Ladeuil
Cancel RecordingServer move, that was useless.
484
        server = RecordingServer(expect_body_tail='end-of-body')
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
485
        server.setUp()
486
        self.addCleanup(server.tearDown)
487
        scheme = self._qualified_prefix
488
        url = '%s://%s:%s/' % (scheme, server.host, server.port)
489
        http_transport = self._transport(url)
490
        code, response = http_transport._post('abc def end-of-body')
491
        self.assertTrue(
492
            server.received_bytes.startswith('POST /.bzr/smart HTTP/1.'))
493
        self.assertTrue('content-length: 19\r' in server.received_bytes.lower())
494
        # The transport should not be assuming that the server can accept
495
        # chunked encoding the first time it connects, because HTTP/1.1, so we
496
        # check for the literal string.
497
        self.assertTrue(
498
            server.received_bytes.endswith('\r\n\r\nabc def end-of-body'))
499
500
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
501
class TestRangeHeader(tests.TestCase):
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
502
    """Test range_header method"""
503
504
    def check_header(self, value, ranges=[], tail=0):
2520.2.1 by Vincent Ladeuil
First step to fix #115209 use _coalesce_offsets like other transports.
505
        offsets = [ (start, end - start + 1) for start, end in ranges]
3111.1.10 by Vincent Ladeuil
Finish http parameterization, 24 auth tests failing for pycurl (not
506
        coalesce = transport.Transport._coalesce_offsets
2520.2.1 by Vincent Ladeuil
First step to fix #115209 use _coalesce_offsets like other transports.
507
        coalesced = list(coalesce(offsets, limit=0, fudge_factor=0))
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
508
        range_header = http.HttpTransportBase._range_header
2520.2.1 by Vincent Ladeuil
First step to fix #115209 use _coalesce_offsets like other transports.
509
        self.assertEqual(value, range_header(coalesced, tail))
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
510
511
    def test_range_header_single(self):
2520.2.1 by Vincent Ladeuil
First step to fix #115209 use _coalesce_offsets like other transports.
512
        self.check_header('0-9', ranges=[(0,9)])
513
        self.check_header('100-109', ranges=[(100,109)])
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
514
515
    def test_range_header_tail(self):
1786.1.36 by John Arbash Meinel
pycurl expects us to just set the range of bytes, not including bytes=
516
        self.check_header('-10', tail=10)
517
        self.check_header('-50', tail=50)
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
518
519
    def test_range_header_multi(self):
1786.1.36 by John Arbash Meinel
pycurl expects us to just set the range of bytes, not including bytes=
520
        self.check_header('0-9,100-200,300-5000',
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
521
                          ranges=[(0,9), (100, 200), (300,5000)])
522
523
    def test_range_header_mixed(self):
1786.1.36 by John Arbash Meinel
pycurl expects us to just set the range of bytes, not including bytes=
524
        self.check_header('0-9,300-5000,-50',
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
525
                          ranges=[(0,9), (300,5000)],
526
                          tail=50)
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
527
2004.1.15 by v.ladeuil+lp at free
Better design for bogus servers. Both urllib and pycurl pass tests.
528
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
529
class TestSpecificRequestHandler(http_utils.TestCaseWithWebserver):
530
    """Tests a specific request handler.
531
3111.1.31 by Vincent Ladeuil
Review feeback.
532
    Daughter classes are expected to override _req_handler_class
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
533
    """
534
535
    # Provide a useful default
536
    _req_handler_class = http_server.TestingHTTPRequestHandler
537
538
    def create_transport_readonly_server(self):
539
        return http_server.HttpServer(self._req_handler_class,
540
                                      protocol_version=self._protocol_version)
541
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
542
    def _testing_pycurl(self):
543
        return pycurl_present and self._transport == PyCurlTransport
544
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
545
546
class WallRequestHandler(http_server.TestingHTTPRequestHandler):
547
    """Whatever request comes in, close the connection"""
548
549
    def handle_one_request(self):
550
        """Handle a single HTTP request, by abruptly closing the connection"""
551
        self.close_connection = 1
552
553
554
class TestWallServer(TestSpecificRequestHandler):
555
    """Tests exceptions during the connection phase"""
556
557
    _req_handler_class = WallRequestHandler
558
559
    def test_http_has(self):
560
        server = self.get_readonly_server()
561
        t = self._transport(server.get_url())
562
        # Unfortunately httplib (see HTTPResponse._read_status
563
        # for details) make no distinction between a closed
564
        # socket and badly formatted status line, so we can't
565
        # just test for ConnectionError, we have to test
566
        # InvalidHttpResponse too.
567
        self.assertRaises((errors.ConnectionError, errors.InvalidHttpResponse),
568
                          t.has, 'foo/bar')
569
570
    def test_http_get(self):
571
        server = self.get_readonly_server()
572
        t = self._transport(server.get_url())
573
        self.assertRaises((errors.ConnectionError, errors.InvalidHttpResponse),
574
                          t.get, 'foo/bar')
575
576
577
class BadStatusRequestHandler(http_server.TestingHTTPRequestHandler):
578
    """Whatever request comes in, returns a bad status"""
579
580
    def parse_request(self):
581
        """Fakes handling a single HTTP request, returns a bad status"""
582
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
583
        self.send_response(0, "Bad status")
584
        self.close_connection = 1
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
585
        return False
586
587
588
class TestBadStatusServer(TestSpecificRequestHandler):
589
    """Tests bad status from server."""
590
591
    _req_handler_class = BadStatusRequestHandler
592
593
    def test_http_has(self):
594
        server = self.get_readonly_server()
595
        t = self._transport(server.get_url())
596
        self.assertRaises(errors.InvalidHttpResponse, t.has, 'foo/bar')
597
598
    def test_http_get(self):
599
        server = self.get_readonly_server()
600
        t = self._transport(server.get_url())
601
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'foo/bar')
602
603
604
class InvalidStatusRequestHandler(http_server.TestingHTTPRequestHandler):
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
605
    """Whatever request comes in, returns an invalid status"""
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
606
607
    def parse_request(self):
608
        """Fakes handling a single HTTP request, returns a bad status"""
609
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
610
        self.wfile.write("Invalid status line\r\n")
611
        return False
612
613
614
class TestInvalidStatusServer(TestBadStatusServer):
615
    """Tests invalid status from server.
616
617
    Both implementations raises the same error as for a bad status.
618
    """
619
620
    _req_handler_class = InvalidStatusRequestHandler
621
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
622
    def test_http_has(self):
623
        if self._testing_pycurl() and self._protocol_version == 'HTTP/1.1':
3111.1.22 by Vincent Ladeuil
Rework TestingHTTPServer classes, fix test bug.
624
            raise tests.KnownFailure(
625
                'pycurl hangs if the server send back garbage')
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
626
        super(TestInvalidStatusServer, self).test_http_has()
627
628
    def test_http_get(self):
629
        if self._testing_pycurl() and self._protocol_version == 'HTTP/1.1':
3111.1.22 by Vincent Ladeuil
Rework TestingHTTPServer classes, fix test bug.
630
            raise tests.KnownFailure(
631
                'pycurl hangs if the server send back garbage')
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
632
        super(TestInvalidStatusServer, self).test_http_get()
633
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
634
635
class BadProtocolRequestHandler(http_server.TestingHTTPRequestHandler):
636
    """Whatever request comes in, returns a bad protocol version"""
637
638
    def parse_request(self):
639
        """Fakes handling a single HTTP request, returns a bad status"""
640
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
641
        # Returns an invalid protocol version, but curl just
642
        # ignores it and those cannot be tested.
643
        self.wfile.write("%s %d %s\r\n" % ('HTTP/0.0',
644
                                           404,
645
                                           'Look at my protocol version'))
646
        return False
647
648
649
class TestBadProtocolServer(TestSpecificRequestHandler):
650
    """Tests bad protocol from server."""
651
652
    _req_handler_class = BadProtocolRequestHandler
653
654
    def setUp(self):
655
        if pycurl_present and self._transport == PyCurlTransport:
656
            raise tests.TestNotApplicable(
657
                "pycurl doesn't check the protocol version")
658
        super(TestBadProtocolServer, self).setUp()
659
660
    def test_http_has(self):
661
        server = self.get_readonly_server()
662
        t = self._transport(server.get_url())
663
        self.assertRaises(errors.InvalidHttpResponse, t.has, 'foo/bar')
664
665
    def test_http_get(self):
666
        server = self.get_readonly_server()
667
        t = self._transport(server.get_url())
668
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'foo/bar')
669
670
671
class ForbiddenRequestHandler(http_server.TestingHTTPRequestHandler):
672
    """Whatever request comes in, returns a 403 code"""
673
674
    def parse_request(self):
675
        """Handle a single HTTP request, by replying we cannot handle it"""
676
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
677
        self.send_error(403)
678
        return False
679
680
681
class TestForbiddenServer(TestSpecificRequestHandler):
682
    """Tests forbidden server"""
683
684
    _req_handler_class = ForbiddenRequestHandler
685
686
    def test_http_has(self):
687
        server = self.get_readonly_server()
688
        t = self._transport(server.get_url())
689
        self.assertRaises(errors.TransportError, t.has, 'foo/bar')
690
691
    def test_http_get(self):
692
        server = self.get_readonly_server()
693
        t = self._transport(server.get_url())
694
        self.assertRaises(errors.TransportError, t.get, 'foo/bar')
695
696
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
697
class TestRecordingServer(tests.TestCase):
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
698
699
    def test_create(self):
3111.1.29 by Vincent Ladeuil
Cancel RecordingServer move, that was useless.
700
        server = RecordingServer(expect_body_tail=None)
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
701
        self.assertEqual('', server.received_bytes)
702
        self.assertEqual(None, server.host)
703
        self.assertEqual(None, server.port)
704
705
    def test_setUp_and_tearDown(self):
3111.1.29 by Vincent Ladeuil
Cancel RecordingServer move, that was useless.
706
        server = RecordingServer(expect_body_tail=None)
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
707
        server.setUp()
708
        try:
709
            self.assertNotEqual(None, server.host)
710
            self.assertNotEqual(None, server.port)
711
        finally:
712
            server.tearDown()
713
        self.assertEqual(None, server.host)
714
        self.assertEqual(None, server.port)
715
716
    def test_send_receive_bytes(self):
3111.1.29 by Vincent Ladeuil
Cancel RecordingServer move, that was useless.
717
        server = RecordingServer(expect_body_tail='c')
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
718
        server.setUp()
719
        self.addCleanup(server.tearDown)
720
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
721
        sock.connect((server.host, server.port))
722
        sock.sendall('abc')
723
        self.assertEqual('HTTP/1.1 200 OK\r\n',
2091.1.1 by Martin Pool
Avoid MSG_WAITALL as it doesn't work on Windows
724
                         osutils.recv_all(sock, 4096))
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
725
        self.assertEqual('abc', server.received_bytes)
2004.1.29 by v.ladeuil+lp at free
New tests for http range requests handling.
726
727
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
728
class TestRangeRequestServer(TestSpecificRequestHandler):
729
    """Tests readv requests against server.
730
731
    We test against default "normal" server.
732
    """
733
734
    def setUp(self):
735
        super(TestRangeRequestServer, self).setUp()
736
        self.build_tree_contents([('a', '0123456789')],)
737
738
    def test_readv(self):
739
        server = self.get_readonly_server()
740
        t = self._transport(server.get_url())
741
        l = list(t.readv('a', ((0, 1), (1, 1), (3, 2), (9, 1))))
742
        self.assertEqual(l[0], (0, '0'))
743
        self.assertEqual(l[1], (1, '1'))
744
        self.assertEqual(l[2], (3, '34'))
745
        self.assertEqual(l[3], (9, '9'))
746
747
    def test_readv_out_of_order(self):
748
        server = self.get_readonly_server()
749
        t = self._transport(server.get_url())
750
        l = list(t.readv('a', ((1, 1), (9, 1), (0, 1), (3, 2))))
751
        self.assertEqual(l[0], (1, '1'))
752
        self.assertEqual(l[1], (9, '9'))
753
        self.assertEqual(l[2], (0, '0'))
754
        self.assertEqual(l[3], (3, '34'))
755
756
    def test_readv_invalid_ranges(self):
757
        server = self.get_readonly_server()
758
        t = self._transport(server.get_url())
759
760
        # This is intentionally reading off the end of the file
761
        # since we are sure that it cannot get there
762
        self.assertListRaises((errors.InvalidRange, errors.ShortReadvError,),
763
                              t.readv, 'a', [(1,1), (8,10)])
764
765
        # This is trying to seek past the end of the file, it should
766
        # also raise a special error
767
        self.assertListRaises((errors.InvalidRange, errors.ShortReadvError,),
768
                              t.readv, 'a', [(12,2)])
769
770
    def test_readv_multiple_get_requests(self):
771
        server = self.get_readonly_server()
772
        t = self._transport(server.get_url())
773
        # force transport to issue multiple requests
774
        t._max_readv_combine = 1
775
        t._max_get_ranges = 1
776
        l = list(t.readv('a', ((0, 1), (1, 1), (3, 2), (9, 1))))
777
        self.assertEqual(l[0], (0, '0'))
778
        self.assertEqual(l[1], (1, '1'))
779
        self.assertEqual(l[2], (3, '34'))
780
        self.assertEqual(l[3], (9, '9'))
781
        # The server should have issued 4 requests
782
        self.assertEqual(4, server.GET_request_nb)
783
784
    def test_readv_get_max_size(self):
785
        server = self.get_readonly_server()
786
        t = self._transport(server.get_url())
787
        # force transport to issue multiple requests by limiting the number of
788
        # bytes by request. Note that this apply to coalesced offsets only, a
3111.1.28 by Vincent Ladeuil
Fix the multi-ranges http server and add tests.
789
        # single range will keep its size even if bigger than the limit.
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
790
        t._get_max_size = 2
791
        l = list(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4))))
792
        self.assertEqual(l[0], (0, '0'))
793
        self.assertEqual(l[1], (1, '1'))
794
        self.assertEqual(l[2], (2, '2345'))
795
        self.assertEqual(l[3], (6, '6789'))
796
        # The server should have issued 3 requests
797
        self.assertEqual(3, server.GET_request_nb)
798
3111.1.28 by Vincent Ladeuil
Fix the multi-ranges http server and add tests.
799
    def test_complete_readv_leave_pipe_clean(self):
800
        server = self.get_readonly_server()
801
        t = self._transport(server.get_url())
802
        # force transport to issue multiple requests
803
        t._get_max_size = 2
804
        l = list(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4))))
805
        # The server should have issued 3 requests
806
        self.assertEqual(3, server.GET_request_nb)
807
        self.assertEqual('0123456789', t.get_bytes('a'))
808
        self.assertEqual(4, server.GET_request_nb)
809
810
    def test_incomplete_readv_leave_pipe_clean(self):
811
        server = self.get_readonly_server()
812
        t = self._transport(server.get_url())
813
        # force transport to issue multiple requests
814
        t._get_max_size = 2
815
        # Don't collapse readv results into a list so that we leave unread
816
        # bytes on the socket
817
        ireadv = iter(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4))))
818
        self.assertEqual((0, '0'), ireadv.next())
819
        # The server should have issued one request so far 
820
        self.assertEqual(1, server.GET_request_nb)
821
        self.assertEqual('0123456789', t.get_bytes('a'))
822
        # get_bytes issued an additional request, the readv pending ones are
823
        # lost
824
        self.assertEqual(2, server.GET_request_nb)
825
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
826
827
class SingleRangeRequestHandler(http_server.TestingHTTPRequestHandler):
828
    """Always reply to range request as if they were single.
829
830
    Don't be explicit about it, just to annoy the clients.
831
    """
832
833
    def get_multiple_ranges(self, file, file_size, ranges):
834
        """Answer as if it was a single range request and ignores the rest"""
835
        (start, end) = ranges[0]
836
        return self.get_single_range(file, file_size, start, end)
837
838
839
class TestSingleRangeRequestServer(TestRangeRequestServer):
840
    """Test readv against a server which accept only single range requests"""
841
842
    _req_handler_class = SingleRangeRequestHandler
843
844
845
class SingleOnlyRangeRequestHandler(http_server.TestingHTTPRequestHandler):
846
    """Only reply to simple range requests, errors out on multiple"""
847
848
    def get_multiple_ranges(self, file, file_size, ranges):
849
        """Refuses the multiple ranges request"""
850
        if len(ranges) > 1:
851
            file.close()
852
            self.send_error(416, "Requested range not satisfiable")
853
            return
854
        (start, end) = ranges[0]
855
        return self.get_single_range(file, file_size, start, end)
856
857
858
class TestSingleOnlyRangeRequestServer(TestRangeRequestServer):
859
    """Test readv against a server which only accept single range requests"""
860
861
    _req_handler_class = SingleOnlyRangeRequestHandler
862
863
864
class NoRangeRequestHandler(http_server.TestingHTTPRequestHandler):
865
    """Ignore range requests without notice"""
866
867
    def do_GET(self):
868
        # Update the statistics
869
        self.server.test_case_server.GET_request_nb += 1
870
        # Just bypass the range handling done by TestingHTTPRequestHandler
871
        return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
872
873
874
class TestNoRangeRequestServer(TestRangeRequestServer):
875
    """Test readv against a server which do not accept range requests"""
876
877
    _req_handler_class = NoRangeRequestHandler
878
879
3111.1.28 by Vincent Ladeuil
Fix the multi-ranges http server and add tests.
880
class MultipleRangeWithoutContentLengthRequestHandler(
881
    http_server.TestingHTTPRequestHandler):
882
    """Reply to multiple range requests without content length header."""
883
884
    def get_multiple_ranges(self, file, file_size, ranges):
885
        self.send_response(206)
886
        self.send_header('Accept-Ranges', 'bytes')
887
        boundary = "%d" % random.randint(0,0x7FFFFFFF)
888
        self.send_header("Content-Type",
889
                         "multipart/byteranges; boundary=%s" % boundary)
890
        self.end_headers()
891
        for (start, end) in ranges:
892
            self.wfile.write("--%s\r\n" % boundary)
893
            self.send_header("Content-type", 'application/octet-stream')
894
            self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
895
                                                                  end,
896
                                                                  file_size))
897
            self.end_headers()
898
            self.send_range_content(file, start, end - start + 1)
899
        # Final boundary
900
        self.wfile.write("--%s\r\n" % boundary)
901
902
903
class TestMultipleRangeWithoutContentLengthServer(TestRangeRequestServer):
904
905
    _req_handler_class = MultipleRangeWithoutContentLengthRequestHandler
906
3146.3.2 by Vincent Ladeuil
Fix #179368 by keeping the current range hint on ShortReadvErrors.
907
908
class TruncatedMultipleRangeRequestHandler(
909
    http_server.TestingHTTPRequestHandler):
910
    """Reply to multiple range requests truncating the last ones.
911
912
    This server generates responses whose Content-Length describes all the
913
    ranges, but fail to include the last ones leading to client short reads.
914
    This has been observed randomly with lighttpd (bug #179368).
915
    """
916
917
    _truncated_ranges = 2
918
919
    def get_multiple_ranges(self, file, file_size, ranges):
920
        self.send_response(206)
921
        self.send_header('Accept-Ranges', 'bytes')
922
        boundary = 'tagada'
923
        self.send_header('Content-Type',
924
                         'multipart/byteranges; boundary=%s' % boundary)
925
        boundary_line = '--%s\r\n' % boundary
926
        # Calculate the Content-Length
927
        content_length = 0
928
        for (start, end) in ranges:
929
            content_length += len(boundary_line)
930
            content_length += self._header_line_length(
931
                'Content-type', 'application/octet-stream')
932
            content_length += self._header_line_length(
933
                'Content-Range', 'bytes %d-%d/%d' % (start, end, file_size))
934
            content_length += len('\r\n') # end headers
935
            content_length += end - start # + 1
936
        content_length += len(boundary_line)
937
        self.send_header('Content-length', content_length)
938
        self.end_headers()
939
940
        # Send the multipart body
941
        cur = 0
942
        for (start, end) in ranges:
943
            self.wfile.write(boundary_line)
944
            self.send_header('Content-type', 'application/octet-stream')
945
            self.send_header('Content-Range', 'bytes %d-%d/%d'
946
                             % (start, end, file_size))
947
            self.end_headers()
948
            if cur + self._truncated_ranges >= len(ranges):
949
                # Abruptly ends the response and close the connection
950
                self.close_connection = 1
951
                return
952
            self.send_range_content(file, start, end - start + 1)
953
            cur += 1
954
        # No final boundary
955
        self.wfile.write(boundary_line)
956
957
958
class TestTruncatedMultipleRangeServer(TestSpecificRequestHandler):
959
960
    _req_handler_class = TruncatedMultipleRangeRequestHandler
961
962
    def setUp(self):
963
        super(TestTruncatedMultipleRangeServer, self).setUp()
964
        self.build_tree_contents([('a', '0123456789')],)
965
966
    def test_readv_with_short_reads(self):
967
        server = self.get_readonly_server()
968
        t = self._transport(server.get_url())
969
        # Force separate ranges for each offset
970
        t._bytes_to_read_before_seek = 0
971
        ireadv = iter(t.readv('a', ((0, 1), (2, 1), (4, 2), (9, 1))))
972
        self.assertEqual((0, '0'), ireadv.next())
973
        self.assertEqual((2, '2'), ireadv.next())
974
        if not self._testing_pycurl():
975
            # Only one request have been issued so far (except for pycurl that
976
            # try to read the whole response at once)
977
            self.assertEqual(1, server.GET_request_nb)
978
        self.assertEqual((4, '45'), ireadv.next())
979
        self.assertEqual((9, '9'), ireadv.next())
980
        # Both implementations issue 3 requests but:
981
        # - urllib does two multiple (4 ranges, then 2 ranges) then a single
982
        #   range,
983
        # - pycurl does two multiple (4 ranges, 4 ranges) then a single range
984
        self.assertEqual(3, server.GET_request_nb)
985
        # Finally the client have tried a single range request and stays in
986
        # that mode
987
        self.assertEqual('single', t._range_hint)
988
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
989
class LimitedRangeRequestHandler(http_server.TestingHTTPRequestHandler):
990
    """Errors out when range specifiers exceed the limit"""
991
992
    def get_multiple_ranges(self, file, file_size, ranges):
993
        """Refuses the multiple ranges request"""
994
        tcs = self.server.test_case_server
995
        if tcs.range_limit is not None and len(ranges) > tcs.range_limit:
996
            file.close()
997
            # Emulate apache behavior
998
            self.send_error(400, "Bad Request")
999
            return
1000
        return http_server.TestingHTTPRequestHandler.get_multiple_ranges(
1001
            self, file, file_size, ranges)
1002
1003
1004
class LimitedRangeHTTPServer(http_server.HttpServer):
1005
    """An HttpServer erroring out on requests with too much range specifiers"""
1006
1007
    def __init__(self, request_handler=LimitedRangeRequestHandler,
1008
                 protocol_version=None,
1009
                 range_limit=None):
1010
        http_server.HttpServer.__init__(self, request_handler,
1011
                                        protocol_version=protocol_version)
1012
        self.range_limit = range_limit
1013
1014
1015
class TestLimitedRangeRequestServer(http_utils.TestCaseWithWebserver):
1016
    """Tests readv requests against a server erroring out on too much ranges."""
1017
3111.1.22 by Vincent Ladeuil
Rework TestingHTTPServer classes, fix test bug.
1018
    # Requests with more range specifiers will error out
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1019
    range_limit = 3
1020
1021
    def create_transport_readonly_server(self):
1022
        return LimitedRangeHTTPServer(range_limit=self.range_limit,
1023
                                      protocol_version=self._protocol_version)
1024
1025
    def get_transport(self):
1026
        return self._transport(self.get_readonly_server().get_url())
1027
1028
    def setUp(self):
1029
        http_utils.TestCaseWithWebserver.setUp(self)
1030
        # We need to manipulate ranges that correspond to real chunks in the
1031
        # response, so we build a content appropriately.
1032
        filler = ''.join(['abcdefghij' for x in range(102)])
1033
        content = ''.join(['%04d' % v + filler for v in range(16)])
1034
        self.build_tree_contents([('a', content)],)
1035
1036
    def test_few_ranges(self):
1037
        t = self.get_transport()
1038
        l = list(t.readv('a', ((0, 4), (1024, 4), )))
1039
        self.assertEqual(l[0], (0, '0000'))
1040
        self.assertEqual(l[1], (1024, '0001'))
1041
        self.assertEqual(1, self.get_readonly_server().GET_request_nb)
1042
1043
    def test_more_ranges(self):
1044
        t = self.get_transport()
1045
        l = list(t.readv('a', ((0, 4), (1024, 4), (4096, 4), (8192, 4))))
1046
        self.assertEqual(l[0], (0, '0000'))
1047
        self.assertEqual(l[1], (1024, '0001'))
1048
        self.assertEqual(l[2], (4096, '0004'))
1049
        self.assertEqual(l[3], (8192, '0008'))
1050
        # The server will refuse to serve the first request (too much ranges),
3199.1.2 by Vincent Ladeuil
Fix two more leaked log files.
1051
        # a second request will succeed.
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1052
        self.assertEqual(2, self.get_readonly_server().GET_request_nb)
1053
1054
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
1055
class TestHttpProxyWhiteBox(tests.TestCase):
2298.7.1 by Vincent Ladeuil
Fix bug #87765: proxy env variables without scheme should cause
1056
    """Whitebox test proxy http authorization.
1057
2420.1.3 by Vincent Ladeuil
Implement http proxy basic authentication.
1058
    Only the urllib implementation is tested here.
2298.7.1 by Vincent Ladeuil
Fix bug #87765: proxy env variables without scheme should cause
1059
    """
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1060
1061
    def setUp(self):
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
1062
        tests.TestCase.setUp(self)
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1063
        self._old_env = {}
1064
1065
    def tearDown(self):
1066
        self._restore_env()
3199.1.2 by Vincent Ladeuil
Fix two more leaked log files.
1067
        tests.TestCase.tearDown(self)
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1068
1069
    def _install_env(self, env):
1070
        for name, value in env.iteritems():
2420.1.2 by Vincent Ladeuil
Define tests for http proxy basic authentication. They fail.
1071
            self._old_env[name] = osutils.set_or_unset_env(name, value)
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1072
1073
    def _restore_env(self):
1074
        for name, value in self._old_env.iteritems():
1075
            osutils.set_or_unset_env(name, value)
1076
1077
    def _proxied_request(self):
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
1078
        handler = _urllib2_wrappers.ProxyHandler()
1079
        request = _urllib2_wrappers.Request('GET','http://baz/buzzle')
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1080
        handler.set_proxy(request, 'http')
1081
        return request
1082
1083
    def test_empty_user(self):
1084
        self._install_env({'http_proxy': 'http://bar.com'})
1085
        request = self._proxied_request()
1086
        self.assertFalse(request.headers.has_key('Proxy-authorization'))
1087
2298.7.1 by Vincent Ladeuil
Fix bug #87765: proxy env variables without scheme should cause
1088
    def test_invalid_proxy(self):
1089
        """A proxy env variable without scheme"""
1090
        self._install_env({'http_proxy': 'host:1234'})
1091
        self.assertRaises(errors.InvalidURL, self._proxied_request)
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1092
1093
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1094
class TestProxyHttpServer(http_utils.TestCaseWithTwoWebservers):
1095
    """Tests proxy server.
1096
1097
    Be aware that we do not setup a real proxy here. Instead, we
1098
    check that the *connection* goes through the proxy by serving
1099
    different content (the faked proxy server append '-proxied'
1100
    to the file names).
1101
    """
1102
1103
    # FIXME: We don't have an https server available, so we don't
1104
    # test https connections.
1105
1106
    def setUp(self):
1107
        super(TestProxyHttpServer, self).setUp()
1108
        self.build_tree_contents([('foo', 'contents of foo\n'),
1109
                                  ('foo-proxied', 'proxied contents of foo\n')])
1110
        # Let's setup some attributes for tests
1111
        self.server = self.get_readonly_server()
1112
        self.proxy_address = '%s:%d' % (self.server.host, self.server.port)
1113
        if self._testing_pycurl():
1114
            # Oh my ! pycurl does not check for the port as part of
1115
            # no_proxy :-( So we just test the host part
1116
            self.no_proxy_host = 'localhost'
1117
        else:
1118
            self.no_proxy_host = self.proxy_address
1119
        # The secondary server is the proxy
1120
        self.proxy = self.get_secondary_server()
1121
        self.proxy_url = self.proxy.get_url()
1122
        self._old_env = {}
1123
1124
    def _testing_pycurl(self):
1125
        return pycurl_present and self._transport == PyCurlTransport
1126
1127
    def create_transport_secondary_server(self):
1128
        """Creates an http server that will serve files with
1129
        '-proxied' appended to their names.
1130
        """
1131
        return http_utils.ProxyServer(protocol_version=self._protocol_version)
1132
1133
    def _install_env(self, env):
1134
        for name, value in env.iteritems():
1135
            self._old_env[name] = osutils.set_or_unset_env(name, value)
1136
1137
    def _restore_env(self):
1138
        for name, value in self._old_env.iteritems():
1139
            osutils.set_or_unset_env(name, value)
1140
1141
    def proxied_in_env(self, env):
1142
        self._install_env(env)
1143
        url = self.server.get_url()
1144
        t = self._transport(url)
1145
        try:
1146
            self.assertEqual(t.get('foo').read(), 'proxied contents of foo\n')
1147
        finally:
1148
            self._restore_env()
1149
1150
    def not_proxied_in_env(self, env):
1151
        self._install_env(env)
1152
        url = self.server.get_url()
1153
        t = self._transport(url)
1154
        try:
1155
            self.assertEqual(t.get('foo').read(), 'contents of foo\n')
1156
        finally:
1157
            self._restore_env()
1158
1159
    def test_http_proxy(self):
1160
        self.proxied_in_env({'http_proxy': self.proxy_url})
1161
1162
    def test_HTTP_PROXY(self):
1163
        if self._testing_pycurl():
1164
            # pycurl does not check HTTP_PROXY for security reasons
1165
            # (for use in a CGI context that we do not care
1166
            # about. Should we ?)
1167
            raise tests.TestNotApplicable(
1168
                'pycurl does not check HTTP_PROXY for security reasons')
1169
        self.proxied_in_env({'HTTP_PROXY': self.proxy_url})
1170
1171
    def test_all_proxy(self):
1172
        self.proxied_in_env({'all_proxy': self.proxy_url})
1173
1174
    def test_ALL_PROXY(self):
1175
        self.proxied_in_env({'ALL_PROXY': self.proxy_url})
1176
1177
    def test_http_proxy_with_no_proxy(self):
1178
        self.not_proxied_in_env({'http_proxy': self.proxy_url,
1179
                                 'no_proxy': self.no_proxy_host})
1180
1181
    def test_HTTP_PROXY_with_NO_PROXY(self):
1182
        if self._testing_pycurl():
1183
            raise tests.TestNotApplicable(
1184
                'pycurl does not check HTTP_PROXY for security reasons')
1185
        self.not_proxied_in_env({'HTTP_PROXY': self.proxy_url,
1186
                                 'NO_PROXY': self.no_proxy_host})
1187
1188
    def test_all_proxy_with_no_proxy(self):
1189
        self.not_proxied_in_env({'all_proxy': self.proxy_url,
1190
                                 'no_proxy': self.no_proxy_host})
1191
1192
    def test_ALL_PROXY_with_NO_PROXY(self):
1193
        self.not_proxied_in_env({'ALL_PROXY': self.proxy_url,
1194
                                 'NO_PROXY': self.no_proxy_host})
1195
1196
    def test_http_proxy_without_scheme(self):
1197
        if self._testing_pycurl():
1198
            # pycurl *ignores* invalid proxy env variables. If that ever change
1199
            # in the future, this test will fail indicating that pycurl do not
1200
            # ignore anymore such variables.
1201
            self.not_proxied_in_env({'http_proxy': self.proxy_address})
1202
        else:
1203
            self.assertRaises(errors.InvalidURL,
1204
                              self.proxied_in_env,
1205
                              {'http_proxy': self.proxy_address})
1206
1207
1208
class TestRanges(http_utils.TestCaseWithWebserver):
1209
    """Test the Range header in GET methods."""
1210
1211
    def setUp(self):
1212
        http_utils.TestCaseWithWebserver.setUp(self)
1213
        self.build_tree_contents([('a', '0123456789')],)
1214
        server = self.get_readonly_server()
1215
        self.transport = self._transport(server.get_url())
1216
3111.1.22 by Vincent Ladeuil
Rework TestingHTTPServer classes, fix test bug.
1217
    def create_transport_readonly_server(self):
1218
        return http_server.HttpServer(protocol_version=self._protocol_version)
1219
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1220
    def _file_contents(self, relpath, ranges):
1221
        offsets = [ (start, end - start + 1) for start, end in ranges]
1222
        coalesce = self.transport._coalesce_offsets
1223
        coalesced = list(coalesce(offsets, limit=0, fudge_factor=0))
1224
        code, data = self.transport._get(relpath, coalesced)
1225
        self.assertTrue(code in (200, 206),'_get returns: %d' % code)
1226
        for start, end in ranges:
1227
            data.seek(start)
1228
            yield data.read(end - start + 1)
1229
1230
    def _file_tail(self, relpath, tail_amount):
1231
        code, data = self.transport._get(relpath, [], tail_amount)
1232
        self.assertTrue(code in (200, 206),'_get returns: %d' % code)
1233
        data.seek(-tail_amount, 2)
1234
        return data.read(tail_amount)
1235
1236
    def test_range_header(self):
1237
        # Valid ranges
1238
        map(self.assertEqual,['0', '234'],
1239
            list(self._file_contents('a', [(0,0), (2,4)])),)
1240
1241
    def test_range_header_tail(self):
1242
        self.assertEqual('789', self._file_tail('a', 3))
1243
1244
    def test_syntactically_invalid_range_header(self):
1245
        self.assertListRaises(errors.InvalidHttpRange,
1246
                          self._file_contents, 'a', [(4, 3)])
1247
1248
    def test_semantically_invalid_range_header(self):
1249
        self.assertListRaises(errors.InvalidHttpRange,
1250
                          self._file_contents, 'a', [(42, 128)])
1251
1252
1253
class TestHTTPRedirections(http_utils.TestCaseWithRedirectedWebserver):
1254
    """Test redirection between http servers."""
1255
1256
    def create_transport_secondary_server(self):
1257
        """Create the secondary server redirecting to the primary server"""
1258
        new = self.get_readonly_server()
1259
1260
        redirecting = http_utils.HTTPServerRedirecting(
1261
            protocol_version=self._protocol_version)
1262
        redirecting.redirect_to(new.host, new.port)
1263
        return redirecting
1264
1265
    def setUp(self):
1266
        super(TestHTTPRedirections, self).setUp()
1267
        self.build_tree_contents([('a', '0123456789'),
1268
                                  ('bundle',
1269
                                  '# Bazaar revision bundle v0.9\n#\n')
1270
                                  ],)
1271
1272
        self.old_transport = self._transport(self.old_server.get_url())
1273
1274
    def test_redirected(self):
1275
        self.assertRaises(errors.RedirectRequested, self.old_transport.get, 'a')
1276
        t = self._transport(self.new_server.get_url())
1277
        self.assertEqual('0123456789', t.get('a').read())
1278
1279
    def test_read_redirected_bundle_from_url(self):
1280
        from bzrlib.bundle import read_bundle_from_url
1281
        url = self.old_transport.abspath('bundle')
1282
        bundle = read_bundle_from_url(url)
1283
        # If read_bundle_from_url was successful we get an empty bundle
1284
        self.assertEqual([], bundle.revisions)
1285
1286
1287
class RedirectedRequest(_urllib2_wrappers.Request):
1288
    """Request following redirections. """
1289
1290
    init_orig = _urllib2_wrappers.Request.__init__
1291
1292
    def __init__(self, method, url, *args, **kwargs):
1293
        """Constructor.
1294
1295
        """
1296
        # Since the tests using this class will replace
1297
        # _urllib2_wrappers.Request, we can't just call the base class __init__
1298
        # or we'll loop.
1299
        RedirectedRequest.init_orig(self, method, url, args, kwargs)
1300
        self.follow_redirections = True
1301
1302
1303
class TestHTTPSilentRedirections(http_utils.TestCaseWithRedirectedWebserver):
1304
    """Test redirections.
1305
1306
    http implementations do not redirect silently anymore (they
1307
    do not redirect at all in fact). The mechanism is still in
1308
    place at the _urllib2_wrappers.Request level and these tests
1309
    exercise it.
1310
1311
    For the pycurl implementation
1312
    the redirection have been deleted as we may deprecate pycurl
1313
    and I have no place to keep a working implementation.
1314
    -- vila 20070212
1315
    """
1316
1317
    def setUp(self):
1318
        if pycurl_present and self._transport == PyCurlTransport:
1319
            raise tests.TestNotApplicable(
1320
                "pycurl doesn't redirect silently annymore")
1321
        super(TestHTTPSilentRedirections, self).setUp()
1322
        self.setup_redirected_request()
1323
        self.addCleanup(self.cleanup_redirected_request)
1324
        self.build_tree_contents([('a','a'),
1325
                                  ('1/',),
1326
                                  ('1/a', 'redirected once'),
1327
                                  ('2/',),
1328
                                  ('2/a', 'redirected twice'),
1329
                                  ('3/',),
1330
                                  ('3/a', 'redirected thrice'),
1331
                                  ('4/',),
1332
                                  ('4/a', 'redirected 4 times'),
1333
                                  ('5/',),
1334
                                  ('5/a', 'redirected 5 times'),
1335
                                  ],)
1336
1337
        self.old_transport = self._transport(self.old_server.get_url())
1338
1339
    def setup_redirected_request(self):
1340
        self.original_class = _urllib2_wrappers.Request
1341
        _urllib2_wrappers.Request = RedirectedRequest
1342
1343
    def cleanup_redirected_request(self):
1344
        _urllib2_wrappers.Request = self.original_class
1345
1346
    def create_transport_secondary_server(self):
1347
        """Create the secondary server, redirections are defined in the tests"""
1348
        return http_utils.HTTPServerRedirecting(
1349
            protocol_version=self._protocol_version)
1350
1351
    def test_one_redirection(self):
1352
        t = self.old_transport
1353
1354
        req = RedirectedRequest('GET', t.abspath('a'))
1355
        req.follow_redirections = True
1356
        new_prefix = 'http://%s:%s' % (self.new_server.host,
1357
                                       self.new_server.port)
1358
        self.old_server.redirections = \
1359
            [('(.*)', r'%s/1\1' % (new_prefix), 301),]
1360
        self.assertEquals('redirected once',t._perform(req).read())
1361
1362
    def test_five_redirections(self):
1363
        t = self.old_transport
1364
1365
        req = RedirectedRequest('GET', t.abspath('a'))
1366
        req.follow_redirections = True
1367
        old_prefix = 'http://%s:%s' % (self.old_server.host,
1368
                                       self.old_server.port)
1369
        new_prefix = 'http://%s:%s' % (self.new_server.host,
1370
                                       self.new_server.port)
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
1371
        self.old_server.redirections = [
1372
            ('/1(.*)', r'%s/2\1' % (old_prefix), 302),
1373
            ('/2(.*)', r'%s/3\1' % (old_prefix), 303),
1374
            ('/3(.*)', r'%s/4\1' % (old_prefix), 307),
1375
            ('/4(.*)', r'%s/5\1' % (new_prefix), 301),
1376
            ('(/[^/]+)', r'%s/1\1' % (old_prefix), 301),
1377
            ]
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1378
        self.assertEquals('redirected 5 times',t._perform(req).read())
1379
1380
1381
class TestDoCatchRedirections(http_utils.TestCaseWithRedirectedWebserver):
1382
    """Test transport.do_catching_redirections."""
1383
1384
    def setUp(self):
1385
        super(TestDoCatchRedirections, self).setUp()
1386
        self.build_tree_contents([('a', '0123456789'),],)
1387
1388
        self.old_transport = self._transport(self.old_server.get_url())
1389
1390
    def get_a(self, transport):
1391
        return transport.get('a')
1392
1393
    def test_no_redirection(self):
1394
        t = self._transport(self.new_server.get_url())
1395
1396
        # We use None for redirected so that we fail if redirected
1397
        self.assertEquals('0123456789',
1398
                          transport.do_catching_redirections(
1399
                self.get_a, t, None).read())
1400
1401
    def test_one_redirection(self):
1402
        self.redirections = 0
1403
1404
        def redirected(transport, exception, redirection_notice):
1405
            self.redirections += 1
1406
            dir, file = urlutils.split(exception.target)
1407
            return self._transport(dir)
1408
1409
        self.assertEquals('0123456789',
1410
                          transport.do_catching_redirections(
1411
                self.get_a, self.old_transport, redirected).read())
1412
        self.assertEquals(1, self.redirections)
1413
1414
    def test_redirection_loop(self):
1415
1416
        def redirected(transport, exception, redirection_notice):
1417
            # By using the redirected url as a base dir for the
1418
            # *old* transport, we create a loop: a => a/a =>
1419
            # a/a/a
1420
            return self.old_transport.clone(exception.target)
1421
1422
        self.assertRaises(errors.TooManyRedirections,
1423
                          transport.do_catching_redirections,
1424
                          self.get_a, self.old_transport, redirected)
1425
1426
1427
class TestAuth(http_utils.TestCaseWithWebserver):
1428
    """Test authentication scheme"""
1429
1430
    _auth_header = 'Authorization'
1431
    _password_prompt_prefix = ''
1432
1433
    def setUp(self):
1434
        super(TestAuth, self).setUp()
1435
        self.server = self.get_readonly_server()
1436
        self.build_tree_contents([('a', 'contents of a\n'),
1437
                                  ('b', 'contents of b\n'),])
1438
1439
    def create_transport_readonly_server(self):
1440
        if self._auth_scheme == 'basic':
1441
            server = http_utils.HTTPBasicAuthServer(
1442
                protocol_version=self._protocol_version)
1443
        else:
1444
            if self._auth_scheme != 'digest':
1445
                raise AssertionError('Unknown auth scheme: %r'
1446
                                     % self._auth_scheme)
1447
            server = http_utils.HTTPDigestAuthServer(
1448
                protocol_version=self._protocol_version)
1449
        return server
1450
1451
    def _testing_pycurl(self):
1452
        return pycurl_present and self._transport == PyCurlTransport
1453
1454
    def get_user_url(self, user=None, password=None):
1455
        """Build an url embedding user and password"""
1456
        url = '%s://' % self.server._url_protocol
1457
        if user is not None:
1458
            url += user
1459
            if password is not None:
1460
                url += ':' + password
1461
            url += '@'
1462
        url += '%s:%s/' % (self.server.host, self.server.port)
1463
        return url
1464
1465
    def get_user_transport(self, user=None, password=None):
1466
        return self._transport(self.get_user_url(user, password))
1467
1468
    def test_no_user(self):
1469
        self.server.add_user('joe', 'foo')
1470
        t = self.get_user_transport()
1471
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
1472
        # Only one 'Authentication Required' error should occur
1473
        self.assertEqual(1, self.server.auth_required_errors)
1474
1475
    def test_empty_pass(self):
1476
        self.server.add_user('joe', '')
1477
        t = self.get_user_transport('joe', '')
1478
        self.assertEqual('contents of a\n', t.get('a').read())
1479
        # Only one 'Authentication Required' error should occur
1480
        self.assertEqual(1, self.server.auth_required_errors)
1481
1482
    def test_user_pass(self):
1483
        self.server.add_user('joe', 'foo')
1484
        t = self.get_user_transport('joe', 'foo')
1485
        self.assertEqual('contents of a\n', t.get('a').read())
1486
        # Only one 'Authentication Required' error should occur
1487
        self.assertEqual(1, self.server.auth_required_errors)
1488
1489
    def test_unknown_user(self):
1490
        self.server.add_user('joe', 'foo')
1491
        t = self.get_user_transport('bill', 'foo')
1492
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
1493
        # Two 'Authentication Required' errors should occur (the
1494
        # initial 'who are you' and 'I don't know you, who are
1495
        # you').
1496
        self.assertEqual(2, self.server.auth_required_errors)
1497
1498
    def test_wrong_pass(self):
1499
        self.server.add_user('joe', 'foo')
1500
        t = self.get_user_transport('joe', 'bar')
1501
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
1502
        # Two 'Authentication Required' errors should occur (the
1503
        # initial 'who are you' and 'this is not you, who are you')
1504
        self.assertEqual(2, self.server.auth_required_errors)
1505
1506
    def test_prompt_for_password(self):
1507
        if self._testing_pycurl():
1508
            raise tests.TestNotApplicable(
1509
                'pycurl cannot prompt, it handles auth by embedding'
1510
                ' user:pass in urls only')
1511
1512
        self.server.add_user('joe', 'foo')
1513
        t = self.get_user_transport('joe', None)
1514
        stdout = tests.StringIOWrapper()
1515
        ui.ui_factory = tests.TestUIFactory(stdin='foo\n', stdout=stdout)
1516
        self.assertEqual('contents of a\n',t.get('a').read())
1517
        # stdin should be empty
1518
        self.assertEqual('', ui.ui_factory.stdin.readline())
1519
        self._check_password_prompt(t._unqualified_scheme, 'joe',
1520
                                    stdout.getvalue())
1521
        # And we shouldn't prompt again for a different request
1522
        # against the same transport.
1523
        self.assertEqual('contents of b\n',t.get('b').read())
1524
        t2 = t.clone()
1525
        # And neither against a clone
1526
        self.assertEqual('contents of b\n',t2.get('b').read())
1527
        # Only one 'Authentication Required' error should occur
1528
        self.assertEqual(1, self.server.auth_required_errors)
1529
1530
    def _check_password_prompt(self, scheme, user, actual_prompt):
1531
        expected_prompt = (self._password_prompt_prefix
1532
                           + ("%s %s@%s:%d, Realm: '%s' password: "
1533
                              % (scheme.upper(),
1534
                                 user, self.server.host, self.server.port,
1535
                                 self.server.auth_realm)))
1536
        self.assertEquals(expected_prompt, actual_prompt)
1537
1538
    def test_no_prompt_for_password_when_using_auth_config(self):
1539
        if self._testing_pycurl():
1540
            raise tests.TestNotApplicable(
1541
                'pycurl does not support authentication.conf'
1542
                ' since it cannot prompt')
1543
1544
        user =' joe'
1545
        password = 'foo'
1546
        stdin_content = 'bar\n'  # Not the right password
1547
        self.server.add_user(user, password)
1548
        t = self.get_user_transport(user, None)
1549
        ui.ui_factory = tests.TestUIFactory(stdin=stdin_content,
1550
                                            stdout=tests.StringIOWrapper())
1551
        # Create a minimal config file with the right password
1552
        conf = config.AuthenticationConfig()
1553
        conf._get_config().update(
1554
            {'httptest': {'scheme': 'http', 'port': self.server.port,
1555
                          'user': user, 'password': password}})
1556
        conf._save()
1557
        # Issue a request to the server to connect
1558
        self.assertEqual('contents of a\n',t.get('a').read())
1559
        # stdin should have  been left untouched
1560
        self.assertEqual(stdin_content, ui.ui_factory.stdin.readline())
1561
        # Only one 'Authentication Required' error should occur
1562
        self.assertEqual(1, self.server.auth_required_errors)
1563
3111.1.26 by Vincent Ladeuil
Re-add a test lost in refactoring.
1564
    def test_changing_nonce(self):
1565
        if self._auth_scheme != 'digest':
1566
            raise tests.TestNotApplicable('HTTP auth digest only test')
1567
        if self._testing_pycurl():
1568
            raise tests.KnownFailure(
1569
                'pycurl does not handle a nonce change')
1570
        self.server.add_user('joe', 'foo')
1571
        t = self.get_user_transport('joe', 'foo')
1572
        self.assertEqual('contents of a\n', t.get('a').read())
1573
        self.assertEqual('contents of b\n', t.get('b').read())
1574
        # Only one 'Authentication Required' error should have
1575
        # occured so far
1576
        self.assertEqual(1, self.server.auth_required_errors)
1577
        # The server invalidates the current nonce
1578
        self.server.auth_nonce = self.server.auth_nonce + '. No, now!'
1579
        self.assertEqual('contents of a\n', t.get('a').read())
1580
        # Two 'Authentication Required' errors should occur (the
1581
        # initial 'who are you' and a second 'who are you' with the new nonce)
1582
        self.assertEqual(2, self.server.auth_required_errors)
1583
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1584
1585
1586
class TestProxyAuth(TestAuth):
1587
    """Test proxy authentication schemes."""
1588
1589
    _auth_header = 'Proxy-authorization'
1590
    _password_prompt_prefix='Proxy '
1591
1592
    def setUp(self):
1593
        super(TestProxyAuth, self).setUp()
1594
        self._old_env = {}
1595
        self.addCleanup(self._restore_env)
1596
        # Override the contents to avoid false positives
1597
        self.build_tree_contents([('a', 'not proxied contents of a\n'),
1598
                                  ('b', 'not proxied contents of b\n'),
1599
                                  ('a-proxied', 'contents of a\n'),
1600
                                  ('b-proxied', 'contents of b\n'),
1601
                                  ])
1602
1603
    def create_transport_readonly_server(self):
1604
        if self._auth_scheme == 'basic':
1605
            server = http_utils.ProxyBasicAuthServer(
1606
                protocol_version=self._protocol_version)
1607
        else:
1608
            if self._auth_scheme != 'digest':
1609
                raise AssertionError('Unknown auth scheme: %r'
1610
                                     % self._auth_scheme)
1611
            server = http_utils.ProxyDigestAuthServer(
1612
                protocol_version=self._protocol_version)
1613
        return server
1614
1615
    def get_user_transport(self, user=None, password=None):
1616
        self._install_env({'all_proxy': self.get_user_url(user, password)})
1617
        return self._transport(self.server.get_url())
1618
1619
    def _install_env(self, env):
1620
        for name, value in env.iteritems():
1621
            self._old_env[name] = osutils.set_or_unset_env(name, value)
1622
1623
    def _restore_env(self):
1624
        for name, value in self._old_env.iteritems():
1625
            osutils.set_or_unset_env(name, value)
1626
1627
    def test_empty_pass(self):
1628
        if self._testing_pycurl():
1629
            import pycurl
1630
            if pycurl.version_info()[1] < '7.16.0':
1631
                raise tests.KnownFailure(
1632
                    'pycurl < 7.16.0 does not handle empty proxy passwords')
1633
        super(TestProxyAuth, self).test_empty_pass()
1634
3111.1.25 by Vincent Ladeuil
Fix the smart server failing test and use it against protocol combinations.
1635
1636
class SampleSocket(object):
1637
    """A socket-like object for use in testing the HTTP request handler."""
1638
1639
    def __init__(self, socket_read_content):
1640
        """Constructs a sample socket.
1641
1642
        :param socket_read_content: a byte sequence
1643
        """
1644
        # Use plain python StringIO so we can monkey-patch the close method to
1645
        # not discard the contents.
1646
        from StringIO import StringIO
1647
        self.readfile = StringIO(socket_read_content)
1648
        self.writefile = StringIO()
1649
        self.writefile.close = lambda: None
1650
1651
    def makefile(self, mode='r', bufsize=None):
1652
        if 'r' in mode:
1653
            return self.readfile
1654
        else:
1655
            return self.writefile
1656
1657
1658
class SmartHTTPTunnellingTest(tests.TestCaseWithTransport):
1659
1660
    def setUp(self):
1661
        super(SmartHTTPTunnellingTest, self).setUp()
1662
        # We use the VFS layer as part of HTTP tunnelling tests.
1663
        self._captureVar('BZR_NO_SMART_VFS', None)
1664
        self.transport_readonly_server = http_utils.HTTPServerWithSmarts
1665
1666
    def create_transport_readonly_server(self):
1667
        return http_utils.HTTPServerWithSmarts(
1668
            protocol_version=self._protocol_version)
1669
1670
    def test_bulk_data(self):
1671
        # We should be able to send and receive bulk data in a single message.
1672
        # The 'readv' command in the smart protocol both sends and receives
1673
        # bulk data, so we use that.
1674
        self.build_tree(['data-file'])
1675
        http_server = self.get_readonly_server()
1676
        http_transport = self._transport(http_server.get_url())
1677
        medium = http_transport.get_smart_medium()
1678
        # Since we provide the medium, the url below will be mostly ignored
1679
        # during the test, as long as the path is '/'.
1680
        remote_transport = remote.RemoteTransport('bzr://fake_host/',
1681
                                                  medium=medium)
1682
        self.assertEqual(
1683
            [(0, "c")], list(remote_transport.readv("data-file", [(0,1)])))
1684
1685
    def test_http_send_smart_request(self):
1686
1687
        post_body = 'hello\n'
1688
        expected_reply_body = 'ok\x012\n'
1689
1690
        http_server = self.get_readonly_server()
1691
        http_transport = self._transport(http_server.get_url())
1692
        medium = http_transport.get_smart_medium()
1693
        response = medium.send_http_smart_request(post_body)
1694
        reply_body = response.read()
1695
        self.assertEqual(expected_reply_body, reply_body)
1696
1697
    def test_smart_http_server_post_request_handler(self):
1698
        httpd = self.get_readonly_server()._get_httpd()
1699
1700
        socket = SampleSocket(
1701
            'POST /.bzr/smart %s \r\n' % self._protocol_version
1702
            # HTTP/1.1 posts must have a Content-Length (but it doesn't hurt
1703
            # for 1.0)
1704
            + 'Content-Length: 6\r\n'
1705
            '\r\n'
1706
            'hello\n')
1707
        # Beware: the ('localhost', 80) below is the
1708
        # client_address parameter, but we don't have one because
1709
        # we have defined a socket which is not bound to an
1710
        # address. The test framework never uses this client
1711
        # address, so far...
1712
        request_handler = http_utils.SmartRequestHandler(socket,
1713
                                                         ('localhost', 80),
1714
                                                         httpd)
1715
        response = socket.writefile.getvalue()
1716
        self.assertStartsWith(response, '%s 200 ' % self._protocol_version)
1717
        # This includes the end of the HTTP headers, and all the body.
1718
        expected_end_of_response = '\r\n\r\nok\x012\n'
1719
        self.assertEndsWith(response, expected_end_of_response)
1720
1721