~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_http.py

  • Committer: Patch Queue Manager
  • Date: 2011-09-22 14:12:18 UTC
  • mfrom: (6155.3.1 jam)
  • Revision ID: pqm@pqm.ubuntu.com-20110922141218-86s4uu6nqvourw4f
(jameinel) Cleanup comments bzrlib/smart/__init__.py (John A Meinel)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
16
 
 
17
 
# FIXME: This test should be repeated for each available http client
18
 
# implementation; at the moment we have urllib and pycurl.
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Tests for HTTP implementations.
 
18
 
 
19
This module defines a load_tests() method that parametrize tests classes for
 
20
transport implementation, http protocol versions and authentication schemes.
 
21
"""
19
22
 
20
23
# TODO: Should be renamed to bzrlib.transport.http.tests?
 
24
# TODO: What about renaming to bzrlib.tests.transport.http ?
 
25
 
 
26
import httplib
 
27
import SimpleHTTPServer
 
28
import socket
 
29
import sys
 
30
import threading
21
31
 
22
32
import bzrlib
23
 
from bzrlib.errors import DependencyNotPresent
24
 
from bzrlib.tests import TestCase, TestSkipped
25
 
from bzrlib.transport import Transport
26
 
from bzrlib.transport.http import extract_auth
27
 
from bzrlib.transport.http._urllib import HttpTransport_urllib
28
 
from bzrlib.tests.HTTPTestUtil import TestCaseWithWebserver
29
 
 
30
 
class FakeManager (object):
 
33
from bzrlib import (
 
34
    bzrdir,
 
35
    cethread,
 
36
    config,
 
37
    debug,
 
38
    errors,
 
39
    osutils,
 
40
    remote as _mod_remote,
 
41
    tests,
 
42
    trace,
 
43
    transport,
 
44
    ui,
 
45
    )
 
46
from bzrlib.tests import (
 
47
    features,
 
48
    http_server,
 
49
    http_utils,
 
50
    test_server,
 
51
    )
 
52
from bzrlib.tests.scenarios import (
 
53
    load_tests_apply_scenarios,
 
54
    multiply_scenarios,
 
55
    )
 
56
from bzrlib.transport import (
 
57
    http,
 
58
    remote,
 
59
    )
 
60
from bzrlib.transport.http import (
 
61
    _urllib,
 
62
    _urllib2_wrappers,
 
63
    )
 
64
 
 
65
 
 
66
if features.pycurl.available():
 
67
    from bzrlib.transport.http._pycurl import PyCurlTransport
 
68
 
 
69
 
 
70
load_tests = load_tests_apply_scenarios
 
71
 
 
72
 
 
73
def vary_by_http_client_implementation():
 
74
    """Test the two libraries we can use, pycurl and urllib."""
 
75
    transport_scenarios = [
 
76
        ('urllib', dict(_transport=_urllib.HttpTransport_urllib,
 
77
                        _server=http_server.HttpServer_urllib,
 
78
                        _url_protocol='http+urllib',)),
 
79
        ]
 
80
    if features.pycurl.available():
 
81
        transport_scenarios.append(
 
82
            ('pycurl', dict(_transport=PyCurlTransport,
 
83
                            _server=http_server.HttpServer_PyCurl,
 
84
                            _url_protocol='http+pycurl',)))
 
85
    return transport_scenarios
 
86
 
 
87
 
 
88
def vary_by_http_protocol_version():
 
89
    """Test on http/1.0 and 1.1"""
 
90
    return [
 
91
        ('HTTP/1.0',  dict(_protocol_version='HTTP/1.0')),
 
92
        ('HTTP/1.1',  dict(_protocol_version='HTTP/1.1')),
 
93
        ]
 
94
 
 
95
 
 
96
def vary_by_http_auth_scheme():
 
97
    scenarios = [
 
98
        ('basic', dict(_auth_server=http_utils.HTTPBasicAuthServer)),
 
99
        ('digest', dict(_auth_server=http_utils.HTTPDigestAuthServer)),
 
100
        ('basicdigest',
 
101
            dict(_auth_server=http_utils.HTTPBasicAndDigestAuthServer)),
 
102
        ]
 
103
    # Add some attributes common to all scenarios
 
104
    for scenario_id, scenario_dict in scenarios:
 
105
        scenario_dict.update(_auth_header='Authorization',
 
106
                             _username_prompt_prefix='',
 
107
                             _password_prompt_prefix='')
 
108
    return scenarios
 
109
 
 
110
 
 
111
def vary_by_http_proxy_auth_scheme():
 
112
    scenarios = [
 
113
        ('proxy-basic', dict(_auth_server=http_utils.ProxyBasicAuthServer)),
 
114
        ('proxy-digest', dict(_auth_server=http_utils.ProxyDigestAuthServer)),
 
115
        ('proxy-basicdigest',
 
116
            dict(_auth_server=http_utils.ProxyBasicAndDigestAuthServer)),
 
117
        ]
 
118
    # Add some attributes common to all scenarios
 
119
    for scenario_id, scenario_dict in scenarios:
 
120
        scenario_dict.update(_auth_header='Proxy-Authorization',
 
121
                             _username_prompt_prefix='Proxy ',
 
122
                             _password_prompt_prefix='Proxy ')
 
123
    return scenarios
 
124
 
 
125
 
 
126
def vary_by_http_activity():
 
127
    activity_scenarios = [
 
128
        ('urllib,http', dict(_activity_server=ActivityHTTPServer,
 
129
                            _transport=_urllib.HttpTransport_urllib,)),
 
130
        ]
 
131
    if features.HTTPSServerFeature.available():
 
132
        activity_scenarios.append(
 
133
            ('urllib,https', dict(_activity_server=ActivityHTTPSServer,
 
134
                                _transport=_urllib.HttpTransport_urllib,)),)
 
135
    if features.pycurl.available():
 
136
        activity_scenarios.append(
 
137
            ('pycurl,http', dict(_activity_server=ActivityHTTPServer,
 
138
                                _transport=PyCurlTransport,)),)
 
139
        if features.HTTPSServerFeature.available():
 
140
            from bzrlib.tests import (
 
141
                ssl_certs,
 
142
                )
 
143
            # FIXME: Until we have a better way to handle self-signed
 
144
            # certificates (like allowing them in a test specific
 
145
            # authentication.conf for example), we need some specialized pycurl
 
146
            # transport for tests.
 
147
            class HTTPS_pycurl_transport(PyCurlTransport):
 
148
 
 
149
                def __init__(self, base, _from_transport=None):
 
150
                    super(HTTPS_pycurl_transport, self).__init__(
 
151
                        base, _from_transport)
 
152
                    self.cabundle = str(ssl_certs.build_path('ca.crt'))
 
153
 
 
154
            activity_scenarios.append(
 
155
                ('pycurl,https', dict(_activity_server=ActivityHTTPSServer,
 
156
                                    _transport=HTTPS_pycurl_transport,)),)
 
157
    return activity_scenarios
 
158
 
 
159
 
 
160
class FakeManager(object):
 
161
 
31
162
    def __init__(self):
32
163
        self.credentials = []
33
 
        
 
164
 
34
165
    def add_password(self, realm, host, username, password):
35
166
        self.credentials.append([realm, host, username, password])
36
167
 
37
168
 
38
 
class TestHttpUrls(TestCase):
 
169
class RecordingServer(object):
 
170
    """A fake HTTP server.
 
171
 
 
172
    It records the bytes sent to it, and replies with a 200.
 
173
    """
 
174
 
 
175
    def __init__(self, expect_body_tail=None, scheme=''):
 
176
        """Constructor.
 
177
 
 
178
        :type expect_body_tail: str
 
179
        :param expect_body_tail: a reply won't be sent until this string is
 
180
            received.
 
181
        """
 
182
        self._expect_body_tail = expect_body_tail
 
183
        self.host = None
 
184
        self.port = None
 
185
        self.received_bytes = ''
 
186
        self.scheme = scheme
 
187
 
 
188
    def get_url(self):
 
189
        return '%s://%s:%s/' % (self.scheme, self.host, self.port)
 
190
 
 
191
    def start_server(self):
 
192
        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
193
        self._sock.bind(('127.0.0.1', 0))
 
194
        self.host, self.port = self._sock.getsockname()
 
195
        self._ready = threading.Event()
 
196
        self._thread = test_server.TestThread(
 
197
            sync_event=self._ready, target=self._accept_read_and_reply)
 
198
        self._thread.start()
 
199
        if 'threads' in tests.selftest_debug_flags:
 
200
            sys.stderr.write('Thread started: %s\n' % (self._thread.ident,))
 
201
        self._ready.wait()
 
202
 
 
203
    def _accept_read_and_reply(self):
 
204
        self._sock.listen(1)
 
205
        self._ready.set()
 
206
        conn, address = self._sock.accept()
 
207
        if self._expect_body_tail is not None:
 
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
        try:
 
212
            self._sock.close()
 
213
        except socket.error:
 
214
            # The client may have already closed the socket.
 
215
            pass
 
216
 
 
217
    def stop_server(self):
 
218
        try:
 
219
            # Issue a fake connection to wake up the server and allow it to
 
220
            # finish quickly
 
221
            fake_conn = osutils.connect_socket((self.host, self.port))
 
222
            fake_conn.close()
 
223
        except socket.error:
 
224
            # We might have already closed it.  We don't care.
 
225
            pass
 
226
        self.host = None
 
227
        self.port = None
 
228
        self._thread.join()
 
229
        if 'threads' in tests.selftest_debug_flags:
 
230
            sys.stderr.write('Thread  joined: %s\n' % (self._thread.ident,))
 
231
 
 
232
 
 
233
class TestAuthHeader(tests.TestCase):
 
234
 
 
235
    def parse_header(self, header, auth_handler_class=None):
 
236
        if auth_handler_class is None:
 
237
            auth_handler_class = _urllib2_wrappers.AbstractAuthHandler
 
238
        self.auth_handler =  auth_handler_class()
 
239
        return self.auth_handler._parse_auth_header(header)
 
240
 
 
241
    def test_empty_header(self):
 
242
        scheme, remainder = self.parse_header('')
 
243
        self.assertEqual('', scheme)
 
244
        self.assertIs(None, remainder)
 
245
 
 
246
    def test_negotiate_header(self):
 
247
        scheme, remainder = self.parse_header('Negotiate')
 
248
        self.assertEqual('negotiate', scheme)
 
249
        self.assertIs(None, remainder)
 
250
 
 
251
    def test_basic_header(self):
 
252
        scheme, remainder = self.parse_header(
 
253
            'Basic realm="Thou should not pass"')
 
254
        self.assertEqual('basic', scheme)
 
255
        self.assertEqual('realm="Thou should not pass"', remainder)
 
256
 
 
257
    def test_basic_extract_realm(self):
 
258
        scheme, remainder = self.parse_header(
 
259
            'Basic realm="Thou should not pass"',
 
260
            _urllib2_wrappers.BasicAuthHandler)
 
261
        match, realm = self.auth_handler.extract_realm(remainder)
 
262
        self.assertTrue(match is not None)
 
263
        self.assertEqual('Thou should not pass', realm)
 
264
 
 
265
    def test_digest_header(self):
 
266
        scheme, remainder = self.parse_header(
 
267
            'Digest realm="Thou should not pass"')
 
268
        self.assertEqual('digest', scheme)
 
269
        self.assertEqual('realm="Thou should not pass"', remainder)
 
270
 
 
271
 
 
272
class TestHTTPRangeParsing(tests.TestCase):
 
273
 
 
274
    def setUp(self):
 
275
        super(TestHTTPRangeParsing, self).setUp()
 
276
        # We focus on range  parsing here and ignore everything else
 
277
        class RequestHandler(http_server.TestingHTTPRequestHandler):
 
278
            def setup(self): pass
 
279
            def handle(self): pass
 
280
            def finish(self): pass
 
281
 
 
282
        self.req_handler = RequestHandler(None, None, None)
 
283
 
 
284
    def assertRanges(self, ranges, header, file_size):
 
285
        self.assertEquals(ranges,
 
286
                          self.req_handler._parse_ranges(header, file_size))
 
287
 
 
288
    def test_simple_range(self):
 
289
        self.assertRanges([(0,2)], 'bytes=0-2', 12)
 
290
 
 
291
    def test_tail(self):
 
292
        self.assertRanges([(8, 11)], 'bytes=-4', 12)
 
293
 
 
294
    def test_tail_bigger_than_file(self):
 
295
        self.assertRanges([(0, 11)], 'bytes=-99', 12)
 
296
 
 
297
    def test_range_without_end(self):
 
298
        self.assertRanges([(4, 11)], 'bytes=4-', 12)
 
299
 
 
300
    def test_invalid_ranges(self):
 
301
        self.assertRanges(None, 'bytes=12-22', 12)
 
302
        self.assertRanges(None, 'bytes=1-3,12-22', 12)
 
303
        self.assertRanges(None, 'bytes=-', 12)
 
304
 
 
305
 
 
306
class TestHTTPServer(tests.TestCase):
 
307
    """Test the HTTP servers implementations."""
 
308
 
 
309
    def test_invalid_protocol(self):
 
310
        class BogusRequestHandler(http_server.TestingHTTPRequestHandler):
 
311
 
 
312
            protocol_version = 'HTTP/0.1'
 
313
 
 
314
        self.assertRaises(httplib.UnknownProtocol,
 
315
                          http_server.HttpServer, BogusRequestHandler)
 
316
 
 
317
    def test_force_invalid_protocol(self):
 
318
        self.assertRaises(httplib.UnknownProtocol,
 
319
                          http_server.HttpServer, protocol_version='HTTP/0.1')
 
320
 
 
321
    def test_server_start_and_stop(self):
 
322
        server = http_server.HttpServer()
 
323
        self.addCleanup(server.stop_server)
 
324
        server.start_server()
 
325
        self.assertTrue(server.server is not None)
 
326
        self.assertTrue(server.server.serving is not None)
 
327
        self.assertTrue(server.server.serving)
 
328
 
 
329
    def test_create_http_server_one_zero(self):
 
330
        class RequestHandlerOneZero(http_server.TestingHTTPRequestHandler):
 
331
 
 
332
            protocol_version = 'HTTP/1.0'
 
333
 
 
334
        server = http_server.HttpServer(RequestHandlerOneZero)
 
335
        self.start_server(server)
 
336
        self.assertIsInstance(server.server, http_server.TestingHTTPServer)
 
337
 
 
338
    def test_create_http_server_one_one(self):
 
339
        class RequestHandlerOneOne(http_server.TestingHTTPRequestHandler):
 
340
 
 
341
            protocol_version = 'HTTP/1.1'
 
342
 
 
343
        server = http_server.HttpServer(RequestHandlerOneOne)
 
344
        self.start_server(server)
 
345
        self.assertIsInstance(server.server,
 
346
                              http_server.TestingThreadingHTTPServer)
 
347
 
 
348
    def test_create_http_server_force_one_one(self):
 
349
        class RequestHandlerOneZero(http_server.TestingHTTPRequestHandler):
 
350
 
 
351
            protocol_version = 'HTTP/1.0'
 
352
 
 
353
        server = http_server.HttpServer(RequestHandlerOneZero,
 
354
                                        protocol_version='HTTP/1.1')
 
355
        self.start_server(server)
 
356
        self.assertIsInstance(server.server,
 
357
                              http_server.TestingThreadingHTTPServer)
 
358
 
 
359
    def test_create_http_server_force_one_zero(self):
 
360
        class RequestHandlerOneOne(http_server.TestingHTTPRequestHandler):
 
361
 
 
362
            protocol_version = 'HTTP/1.1'
 
363
 
 
364
        server = http_server.HttpServer(RequestHandlerOneOne,
 
365
                                        protocol_version='HTTP/1.0')
 
366
        self.start_server(server)
 
367
        self.assertIsInstance(server.server,
 
368
                              http_server.TestingHTTPServer)
 
369
 
 
370
 
 
371
class TestWithTransport_pycurl(object):
 
372
    """Test case to inherit from if pycurl is present"""
 
373
 
 
374
    def _get_pycurl_maybe(self):
 
375
        self.requireFeature(features.pycurl)
 
376
        return PyCurlTransport
 
377
 
 
378
    _transport = property(_get_pycurl_maybe)
 
379
 
 
380
 
 
381
class TestHttpUrls(tests.TestCase):
 
382
 
 
383
    # TODO: This should be moved to authorization tests once they
 
384
    # are written.
 
385
 
39
386
    def test_url_parsing(self):
40
387
        f = FakeManager()
41
 
        url = extract_auth('http://example.com', f)
42
 
        self.assertEquals('http://example.com', url)
43
 
        self.assertEquals(0, len(f.credentials))
44
 
        url = extract_auth('http://user:pass@www.bazaar-ng.org/bzr/bzr.dev', f)
45
 
        self.assertEquals('http://www.bazaar-ng.org/bzr/bzr.dev', url)
46
 
        self.assertEquals(1, len(f.credentials))
47
 
        self.assertEquals([None, 'www.bazaar-ng.org', 'user', 'pass'], f.credentials[0])
48
 
        
 
388
        url = http.extract_auth('http://example.com', f)
 
389
        self.assertEqual('http://example.com', url)
 
390
        self.assertEqual(0, len(f.credentials))
 
391
        url = http.extract_auth(
 
392
            'http://user:pass@example.com/bzr/bzr.dev', f)
 
393
        self.assertEqual('http://example.com/bzr/bzr.dev', url)
 
394
        self.assertEqual(1, len(f.credentials))
 
395
        self.assertEqual([None, 'example.com', 'user', 'pass'],
 
396
                         f.credentials[0])
 
397
 
 
398
 
 
399
class TestHttpTransportUrls(tests.TestCase):
 
400
    """Test the http urls."""
 
401
 
 
402
    scenarios = vary_by_http_client_implementation()
 
403
 
49
404
    def test_abs_url(self):
50
405
        """Construction of absolute http URLs"""
51
 
        t = HttpTransport_urllib('http://bazaar-ng.org/bzr/bzr.dev/')
 
406
        t = self._transport('http://example.com/bzr/bzr.dev/')
52
407
        eq = self.assertEqualDiff
53
 
        eq(t.abspath('.'),
54
 
           'http://bazaar-ng.org/bzr/bzr.dev')
55
 
        eq(t.abspath('foo/bar'), 
56
 
           'http://bazaar-ng.org/bzr/bzr.dev/foo/bar')
57
 
        eq(t.abspath('.bzr'),
58
 
           'http://bazaar-ng.org/bzr/bzr.dev/.bzr')
 
408
        eq(t.abspath('.'), 'http://example.com/bzr/bzr.dev')
 
409
        eq(t.abspath('foo/bar'), 'http://example.com/bzr/bzr.dev/foo/bar')
 
410
        eq(t.abspath('.bzr'), 'http://example.com/bzr/bzr.dev/.bzr')
59
411
        eq(t.abspath('.bzr/1//2/./3'),
60
 
           'http://bazaar-ng.org/bzr/bzr.dev/.bzr/1/2/3')
 
412
           'http://example.com/bzr/bzr.dev/.bzr/1/2/3')
61
413
 
62
414
    def test_invalid_http_urls(self):
63
415
        """Trap invalid construction of urls"""
64
 
        t = HttpTransport_urllib('http://bazaar-ng.org/bzr/bzr.dev/')
65
 
        self.assertRaises(ValueError,
66
 
            t.abspath,
67
 
            '.bzr/')
68
 
        self.assertRaises(ValueError,
69
 
            t.abspath,
70
 
            '/.bzr')
 
416
        self._transport('http://example.com/bzr/bzr.dev/')
 
417
        self.assertRaises(errors.InvalidURL,
 
418
                          self._transport,
 
419
                          'http://http://example.com/bzr/bzr.dev/')
71
420
 
72
421
    def test_http_root_urls(self):
73
422
        """Construction of URLs from server root"""
74
 
        t = HttpTransport_urllib('http://bzr.ozlabs.org/')
 
423
        t = self._transport('http://example.com/')
75
424
        eq = self.assertEqualDiff
76
425
        eq(t.abspath('.bzr/tree-version'),
77
 
           'http://bzr.ozlabs.org/.bzr/tree-version')
 
426
           'http://example.com/.bzr/tree-version')
78
427
 
79
428
    def test_http_impl_urls(self):
80
429
        """There are servers which ask for particular clients to connect"""
 
430
        server = self._server()
 
431
        server.start_server()
81
432
        try:
82
 
            from bzrlib.transport.http._pycurl import HttpServer_PyCurl
83
 
            server = HttpServer_PyCurl()
84
 
            try:
85
 
                server.setUp()
86
 
                url = server.get_url()
87
 
                self.assertTrue(url.startswith('http+pycurl://'))
88
 
            finally:
89
 
                server.tearDown()
90
 
        except DependencyNotPresent:
91
 
            raise TestSkipped('pycurl not present')
92
 
 
93
 
class TestHttpMixins(object):
94
 
 
95
 
    def _prep_tree(self):
96
 
        self.build_tree(['xxx', 'foo/', 'foo/bar'], line_endings='binary',
 
433
            url = server.get_url()
 
434
            self.assertTrue(url.startswith('%s://' % self._url_protocol))
 
435
        finally:
 
436
            server.stop_server()
 
437
 
 
438
 
 
439
class TestHttps_pycurl(TestWithTransport_pycurl, tests.TestCase):
 
440
 
 
441
    # TODO: This should really be moved into another pycurl
 
442
    # specific test. When https tests will be implemented, take
 
443
    # this one into account.
 
444
    def test_pycurl_without_https_support(self):
 
445
        """Test that pycurl without SSL do not fail with a traceback.
 
446
 
 
447
        For the purpose of the test, we force pycurl to ignore
 
448
        https by supplying a fake version_info that do not
 
449
        support it.
 
450
        """
 
451
        self.requireFeature(features.pycurl)
 
452
        # Import the module locally now that we now it's available.
 
453
        pycurl = features.pycurl.module
 
454
 
 
455
        self.overrideAttr(pycurl, 'version_info',
 
456
                          # Fake the pycurl version_info This was taken from
 
457
                          # a windows pycurl without SSL (thanks to bialix)
 
458
                          lambda : (2,
 
459
                                    '7.13.2',
 
460
                                    462082,
 
461
                                    'i386-pc-win32',
 
462
                                    2576,
 
463
                                    None,
 
464
                                    0,
 
465
                                    None,
 
466
                                    ('ftp', 'gopher', 'telnet',
 
467
                                     'dict', 'ldap', 'http', 'file'),
 
468
                                    None,
 
469
                                    0,
 
470
                                    None))
 
471
        self.assertRaises(errors.DependencyNotPresent, self._transport,
 
472
                          'https://launchpad.net')
 
473
 
 
474
 
 
475
class TestHTTPConnections(http_utils.TestCaseWithWebserver):
 
476
    """Test the http connections."""
 
477
 
 
478
    scenarios = multiply_scenarios(
 
479
        vary_by_http_client_implementation(),
 
480
        vary_by_http_protocol_version(),
 
481
        )
 
482
 
 
483
    def setUp(self):
 
484
        http_utils.TestCaseWithWebserver.setUp(self)
 
485
        self.build_tree(['foo/', 'foo/bar'], line_endings='binary',
97
486
                        transport=self.get_transport())
98
487
 
99
488
    def test_http_has(self):
100
489
        server = self.get_readonly_server()
101
 
        t = self._transport(server.get_url())
 
490
        t = self.get_readonly_transport()
102
491
        self.assertEqual(t.has('foo/bar'), True)
103
492
        self.assertEqual(len(server.logs), 1)
104
 
        self.assertContainsRe(server.logs[0], 
 
493
        self.assertContainsRe(server.logs[0],
105
494
            r'"HEAD /foo/bar HTTP/1.." (200|302) - "-" "bzr/')
106
495
 
107
496
    def test_http_has_not_found(self):
108
497
        server = self.get_readonly_server()
109
 
        t = self._transport(server.get_url())
 
498
        t = self.get_readonly_transport()
110
499
        self.assertEqual(t.has('not-found'), False)
111
 
        self.assertContainsRe(server.logs[1], 
 
500
        self.assertContainsRe(server.logs[1],
112
501
            r'"HEAD /not-found HTTP/1.." 404 - "-" "bzr/')
113
502
 
114
503
    def test_http_get(self):
115
504
        server = self.get_readonly_server()
116
 
        t = self._transport(server.get_url())
 
505
        t = self.get_readonly_transport()
117
506
        fp = t.get('foo/bar')
118
507
        self.assertEqualDiff(
119
508
            fp.read(),
120
509
            'contents of foo/bar\n')
121
510
        self.assertEqual(len(server.logs), 1)
122
511
        self.assertTrue(server.logs[0].find(
123
 
            '"GET /foo/bar HTTP/1.1" 200 - "-" "bzr/%s' % bzrlib.__version__) > -1)
124
 
 
125
 
 
126
 
class TestHttpConnections_urllib(TestCaseWithWebserver, TestHttpMixins):
127
 
    _transport = HttpTransport_urllib
128
 
 
129
 
    def setUp(self):
130
 
        TestCaseWithWebserver.setUp(self)
131
 
        self._prep_tree()
132
 
 
133
 
 
134
 
 
135
 
class TestHttpConnections_pycurl(TestCaseWithWebserver, TestHttpMixins):
136
 
 
137
 
    def _get_pycurl_maybe(self):
 
512
            '"GET /foo/bar HTTP/1.1" 200 - "-" "bzr/%s'
 
513
            % bzrlib.__version__) > -1)
 
514
 
 
515
    def test_has_on_bogus_host(self):
 
516
        # Get a free address and don't 'accept' on it, so that we
 
517
        # can be sure there is no http handler there, but set a
 
518
        # reasonable timeout to not slow down tests too much.
 
519
        default_timeout = socket.getdefaulttimeout()
138
520
        try:
139
 
            from bzrlib.transport.http._pycurl import PyCurlTransport
140
 
            return PyCurlTransport
141
 
        except DependencyNotPresent:
142
 
            raise TestSkipped('pycurl not present')
143
 
 
144
 
    _transport = property(_get_pycurl_maybe)
145
 
 
146
 
    def setUp(self):
147
 
        TestCaseWithWebserver.setUp(self)
148
 
        self._prep_tree()
149
 
 
150
 
 
151
 
 
152
 
class TestHttpTransportRegistration(TestCase):
 
521
            socket.setdefaulttimeout(2)
 
522
            s = socket.socket()
 
523
            s.bind(('localhost', 0))
 
524
            t = self._transport('http://%s:%s/' % s.getsockname())
 
525
            self.assertRaises(errors.ConnectionError, t.has, 'foo/bar')
 
526
        finally:
 
527
            socket.setdefaulttimeout(default_timeout)
 
528
 
 
529
 
 
530
class TestHttpTransportRegistration(tests.TestCase):
153
531
    """Test registrations of various http implementations"""
154
532
 
 
533
    scenarios = vary_by_http_client_implementation()
 
534
 
155
535
    def test_http_registered(self):
156
 
        import bzrlib.transport.http._urllib
157
 
        from bzrlib.transport import get_transport
158
 
        # urlllib should always be present
159
 
        t = get_transport('http+urllib://bzr.google.com/')
160
 
        self.assertIsInstance(t, Transport)
161
 
        self.assertIsInstance(t, bzrlib.transport.http._urllib.HttpTransport_urllib)
 
536
        t = transport.get_transport_from_url(
 
537
            '%s://foo.com/' % self._url_protocol)
 
538
        self.assertIsInstance(t, transport.Transport)
 
539
        self.assertIsInstance(t, self._transport)
 
540
 
 
541
 
 
542
class TestPost(tests.TestCase):
 
543
 
 
544
    scenarios = multiply_scenarios(
 
545
        vary_by_http_client_implementation(),
 
546
        vary_by_http_protocol_version(),
 
547
        )
 
548
 
 
549
    def test_post_body_is_received(self):
 
550
        server = RecordingServer(expect_body_tail='end-of-body',
 
551
                                 scheme=self._url_protocol)
 
552
        self.start_server(server)
 
553
        url = server.get_url()
 
554
        # FIXME: needs a cleanup -- vila 20100611
 
555
        http_transport = transport.get_transport_from_url(url)
 
556
        code, response = http_transport._post('abc def end-of-body')
 
557
        self.assertTrue(
 
558
            server.received_bytes.startswith('POST /.bzr/smart HTTP/1.'))
 
559
        self.assertTrue('content-length: 19\r' in server.received_bytes.lower())
 
560
        self.assertTrue('content-type: application/octet-stream\r'
 
561
                        in server.received_bytes.lower())
 
562
        # The transport should not be assuming that the server can accept
 
563
        # chunked encoding the first time it connects, because HTTP/1.1, so we
 
564
        # check for the literal string.
 
565
        self.assertTrue(
 
566
            server.received_bytes.endswith('\r\n\r\nabc def end-of-body'))
 
567
 
 
568
 
 
569
class TestRangeHeader(tests.TestCase):
 
570
    """Test range_header method"""
 
571
 
 
572
    def check_header(self, value, ranges=[], tail=0):
 
573
        offsets = [ (start, end - start + 1) for start, end in ranges]
 
574
        coalesce = transport.Transport._coalesce_offsets
 
575
        coalesced = list(coalesce(offsets, limit=0, fudge_factor=0))
 
576
        range_header = http.HttpTransportBase._range_header
 
577
        self.assertEqual(value, range_header(coalesced, tail))
 
578
 
 
579
    def test_range_header_single(self):
 
580
        self.check_header('0-9', ranges=[(0,9)])
 
581
        self.check_header('100-109', ranges=[(100,109)])
 
582
 
 
583
    def test_range_header_tail(self):
 
584
        self.check_header('-10', tail=10)
 
585
        self.check_header('-50', tail=50)
 
586
 
 
587
    def test_range_header_multi(self):
 
588
        self.check_header('0-9,100-200,300-5000',
 
589
                          ranges=[(0,9), (100, 200), (300,5000)])
 
590
 
 
591
    def test_range_header_mixed(self):
 
592
        self.check_header('0-9,300-5000,-50',
 
593
                          ranges=[(0,9), (300,5000)],
 
594
                          tail=50)
 
595
 
 
596
 
 
597
class TestSpecificRequestHandler(http_utils.TestCaseWithWebserver):
 
598
    """Tests a specific request handler.
 
599
 
 
600
    Daughter classes are expected to override _req_handler_class
 
601
    """
 
602
 
 
603
    scenarios = multiply_scenarios(
 
604
        vary_by_http_client_implementation(),
 
605
        vary_by_http_protocol_version(),
 
606
        )
 
607
 
 
608
    # Provide a useful default
 
609
    _req_handler_class = http_server.TestingHTTPRequestHandler
 
610
 
 
611
    def create_transport_readonly_server(self):
 
612
        server = http_server.HttpServer(self._req_handler_class,
 
613
                                        protocol_version=self._protocol_version)
 
614
        server._url_protocol = self._url_protocol
 
615
        return server
 
616
 
 
617
    def _testing_pycurl(self):
 
618
        # TODO: This is duplicated for lots of the classes in this file
 
619
        return (features.pycurl.available()
 
620
                and self._transport == PyCurlTransport)
 
621
 
 
622
 
 
623
class WallRequestHandler(http_server.TestingHTTPRequestHandler):
 
624
    """Whatever request comes in, close the connection"""
 
625
 
 
626
    def _handle_one_request(self):
 
627
        """Handle a single HTTP request, by abruptly closing the connection"""
 
628
        self.close_connection = 1
 
629
 
 
630
 
 
631
class TestWallServer(TestSpecificRequestHandler):
 
632
    """Tests exceptions during the connection phase"""
 
633
 
 
634
    _req_handler_class = WallRequestHandler
 
635
 
 
636
    def test_http_has(self):
 
637
        t = self.get_readonly_transport()
 
638
        # Unfortunately httplib (see HTTPResponse._read_status
 
639
        # for details) make no distinction between a closed
 
640
        # socket and badly formatted status line, so we can't
 
641
        # just test for ConnectionError, we have to test
 
642
        # InvalidHttpResponse too. And pycurl may raise ConnectionReset
 
643
        # instead of ConnectionError too.
 
644
        self.assertRaises(( errors.ConnectionError, errors.ConnectionReset,
 
645
                            errors.InvalidHttpResponse),
 
646
                          t.has, 'foo/bar')
 
647
 
 
648
    def test_http_get(self):
 
649
        t = self.get_readonly_transport()
 
650
        self.assertRaises((errors.ConnectionError, errors.ConnectionReset,
 
651
                           errors.InvalidHttpResponse),
 
652
                          t.get, 'foo/bar')
 
653
 
 
654
 
 
655
class BadStatusRequestHandler(http_server.TestingHTTPRequestHandler):
 
656
    """Whatever request comes in, returns a bad status"""
 
657
 
 
658
    def parse_request(self):
 
659
        """Fakes handling a single HTTP request, returns a bad status"""
 
660
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
 
661
        self.send_response(0, "Bad status")
 
662
        self.close_connection = 1
 
663
        return False
 
664
 
 
665
 
 
666
class TestBadStatusServer(TestSpecificRequestHandler):
 
667
    """Tests bad status from server."""
 
668
 
 
669
    _req_handler_class = BadStatusRequestHandler
 
670
 
 
671
    def test_http_has(self):
 
672
        t = self.get_readonly_transport()
 
673
        self.assertRaises(errors.InvalidHttpResponse, t.has, 'foo/bar')
 
674
 
 
675
    def test_http_get(self):
 
676
        t = self.get_readonly_transport()
 
677
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'foo/bar')
 
678
 
 
679
 
 
680
class InvalidStatusRequestHandler(http_server.TestingHTTPRequestHandler):
 
681
    """Whatever request comes in, returns an invalid status"""
 
682
 
 
683
    def parse_request(self):
 
684
        """Fakes handling a single HTTP request, returns a bad status"""
 
685
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
 
686
        self.wfile.write("Invalid status line\r\n")
 
687
        # If we don't close the connection pycurl will hang. Since this is a
 
688
        # stress test we don't *have* to respect the protocol, but we don't
 
689
        # have to sabotage it too much either.
 
690
        self.close_connection = True
 
691
        return False
 
692
 
 
693
 
 
694
class TestInvalidStatusServer(TestBadStatusServer):
 
695
    """Tests invalid status from server.
 
696
 
 
697
    Both implementations raises the same error as for a bad status.
 
698
    """
 
699
 
 
700
    _req_handler_class = InvalidStatusRequestHandler
 
701
 
 
702
 
 
703
class BadProtocolRequestHandler(http_server.TestingHTTPRequestHandler):
 
704
    """Whatever request comes in, returns a bad protocol version"""
 
705
 
 
706
    def parse_request(self):
 
707
        """Fakes handling a single HTTP request, returns a bad status"""
 
708
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
 
709
        # Returns an invalid protocol version, but curl just
 
710
        # ignores it and those cannot be tested.
 
711
        self.wfile.write("%s %d %s\r\n" % ('HTTP/0.0',
 
712
                                           404,
 
713
                                           'Look at my protocol version'))
 
714
        return False
 
715
 
 
716
 
 
717
class TestBadProtocolServer(TestSpecificRequestHandler):
 
718
    """Tests bad protocol from server."""
 
719
 
 
720
    _req_handler_class = BadProtocolRequestHandler
 
721
 
 
722
    def setUp(self):
 
723
        if self._testing_pycurl():
 
724
            raise tests.TestNotApplicable(
 
725
                "pycurl doesn't check the protocol version")
 
726
        super(TestBadProtocolServer, self).setUp()
 
727
 
 
728
    def test_http_has(self):
 
729
        t = self.get_readonly_transport()
 
730
        self.assertRaises(errors.InvalidHttpResponse, t.has, 'foo/bar')
 
731
 
 
732
    def test_http_get(self):
 
733
        t = self.get_readonly_transport()
 
734
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'foo/bar')
 
735
 
 
736
 
 
737
class ForbiddenRequestHandler(http_server.TestingHTTPRequestHandler):
 
738
    """Whatever request comes in, returns a 403 code"""
 
739
 
 
740
    def parse_request(self):
 
741
        """Handle a single HTTP request, by replying we cannot handle it"""
 
742
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
 
743
        self.send_error(403)
 
744
        return False
 
745
 
 
746
 
 
747
class TestForbiddenServer(TestSpecificRequestHandler):
 
748
    """Tests forbidden server"""
 
749
 
 
750
    _req_handler_class = ForbiddenRequestHandler
 
751
 
 
752
    def test_http_has(self):
 
753
        t = self.get_readonly_transport()
 
754
        self.assertRaises(errors.TransportError, t.has, 'foo/bar')
 
755
 
 
756
    def test_http_get(self):
 
757
        t = self.get_readonly_transport()
 
758
        self.assertRaises(errors.TransportError, t.get, 'foo/bar')
 
759
 
 
760
 
 
761
class TestRecordingServer(tests.TestCase):
 
762
 
 
763
    def test_create(self):
 
764
        server = RecordingServer(expect_body_tail=None)
 
765
        self.assertEqual('', server.received_bytes)
 
766
        self.assertEqual(None, server.host)
 
767
        self.assertEqual(None, server.port)
 
768
 
 
769
    def test_setUp_and_stop(self):
 
770
        server = RecordingServer(expect_body_tail=None)
 
771
        server.start_server()
 
772
        try:
 
773
            self.assertNotEqual(None, server.host)
 
774
            self.assertNotEqual(None, server.port)
 
775
        finally:
 
776
            server.stop_server()
 
777
        self.assertEqual(None, server.host)
 
778
        self.assertEqual(None, server.port)
 
779
 
 
780
    def test_send_receive_bytes(self):
 
781
        server = RecordingServer(expect_body_tail='c', scheme='http')
 
782
        self.start_server(server)
 
783
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
784
        sock.connect((server.host, server.port))
 
785
        sock.sendall('abc')
 
786
        self.assertEqual('HTTP/1.1 200 OK\r\n',
 
787
                         osutils.recv_all(sock, 4096))
 
788
        self.assertEqual('abc', server.received_bytes)
 
789
 
 
790
 
 
791
class TestRangeRequestServer(TestSpecificRequestHandler):
 
792
    """Tests readv requests against server.
 
793
 
 
794
    We test against default "normal" server.
 
795
    """
 
796
 
 
797
    def setUp(self):
 
798
        super(TestRangeRequestServer, self).setUp()
 
799
        self.build_tree_contents([('a', '0123456789')],)
 
800
 
 
801
    def test_readv(self):
 
802
        t = self.get_readonly_transport()
 
803
        l = list(t.readv('a', ((0, 1), (1, 1), (3, 2), (9, 1))))
 
804
        self.assertEqual(l[0], (0, '0'))
 
805
        self.assertEqual(l[1], (1, '1'))
 
806
        self.assertEqual(l[2], (3, '34'))
 
807
        self.assertEqual(l[3], (9, '9'))
 
808
 
 
809
    def test_readv_out_of_order(self):
 
810
        t = self.get_readonly_transport()
 
811
        l = list(t.readv('a', ((1, 1), (9, 1), (0, 1), (3, 2))))
 
812
        self.assertEqual(l[0], (1, '1'))
 
813
        self.assertEqual(l[1], (9, '9'))
 
814
        self.assertEqual(l[2], (0, '0'))
 
815
        self.assertEqual(l[3], (3, '34'))
 
816
 
 
817
    def test_readv_invalid_ranges(self):
 
818
        t = self.get_readonly_transport()
 
819
 
 
820
        # This is intentionally reading off the end of the file
 
821
        # since we are sure that it cannot get there
 
822
        self.assertListRaises((errors.InvalidRange, errors.ShortReadvError,),
 
823
                              t.readv, 'a', [(1,1), (8,10)])
 
824
 
 
825
        # This is trying to seek past the end of the file, it should
 
826
        # also raise a special error
 
827
        self.assertListRaises((errors.InvalidRange, errors.ShortReadvError,),
 
828
                              t.readv, 'a', [(12,2)])
 
829
 
 
830
    def test_readv_multiple_get_requests(self):
 
831
        server = self.get_readonly_server()
 
832
        t = self.get_readonly_transport()
 
833
        # force transport to issue multiple requests
 
834
        t._max_readv_combine = 1
 
835
        t._max_get_ranges = 1
 
836
        l = list(t.readv('a', ((0, 1), (1, 1), (3, 2), (9, 1))))
 
837
        self.assertEqual(l[0], (0, '0'))
 
838
        self.assertEqual(l[1], (1, '1'))
 
839
        self.assertEqual(l[2], (3, '34'))
 
840
        self.assertEqual(l[3], (9, '9'))
 
841
        # The server should have issued 4 requests
 
842
        self.assertEqual(4, server.GET_request_nb)
 
843
 
 
844
    def test_readv_get_max_size(self):
 
845
        server = self.get_readonly_server()
 
846
        t = self.get_readonly_transport()
 
847
        # force transport to issue multiple requests by limiting the number of
 
848
        # bytes by request. Note that this apply to coalesced offsets only, a
 
849
        # single range will keep its size even if bigger than the limit.
 
850
        t._get_max_size = 2
 
851
        l = list(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4))))
 
852
        self.assertEqual(l[0], (0, '0'))
 
853
        self.assertEqual(l[1], (1, '1'))
 
854
        self.assertEqual(l[2], (2, '2345'))
 
855
        self.assertEqual(l[3], (6, '6789'))
 
856
        # The server should have issued 3 requests
 
857
        self.assertEqual(3, server.GET_request_nb)
 
858
 
 
859
    def test_complete_readv_leave_pipe_clean(self):
 
860
        server = self.get_readonly_server()
 
861
        t = self.get_readonly_transport()
 
862
        # force transport to issue multiple requests
 
863
        t._get_max_size = 2
 
864
        list(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4))))
 
865
        # The server should have issued 3 requests
 
866
        self.assertEqual(3, server.GET_request_nb)
 
867
        self.assertEqual('0123456789', t.get_bytes('a'))
 
868
        self.assertEqual(4, server.GET_request_nb)
 
869
 
 
870
    def test_incomplete_readv_leave_pipe_clean(self):
 
871
        server = self.get_readonly_server()
 
872
        t = self.get_readonly_transport()
 
873
        # force transport to issue multiple requests
 
874
        t._get_max_size = 2
 
875
        # Don't collapse readv results into a list so that we leave unread
 
876
        # bytes on the socket
 
877
        ireadv = iter(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4))))
 
878
        self.assertEqual((0, '0'), ireadv.next())
 
879
        # The server should have issued one request so far
 
880
        self.assertEqual(1, server.GET_request_nb)
 
881
        self.assertEqual('0123456789', t.get_bytes('a'))
 
882
        # get_bytes issued an additional request, the readv pending ones are
 
883
        # lost
 
884
        self.assertEqual(2, server.GET_request_nb)
 
885
 
 
886
 
 
887
class SingleRangeRequestHandler(http_server.TestingHTTPRequestHandler):
 
888
    """Always reply to range request as if they were single.
 
889
 
 
890
    Don't be explicit about it, just to annoy the clients.
 
891
    """
 
892
 
 
893
    def get_multiple_ranges(self, file, file_size, ranges):
 
894
        """Answer as if it was a single range request and ignores the rest"""
 
895
        (start, end) = ranges[0]
 
896
        return self.get_single_range(file, file_size, start, end)
 
897
 
 
898
 
 
899
class TestSingleRangeRequestServer(TestRangeRequestServer):
 
900
    """Test readv against a server which accept only single range requests"""
 
901
 
 
902
    _req_handler_class = SingleRangeRequestHandler
 
903
 
 
904
 
 
905
class SingleOnlyRangeRequestHandler(http_server.TestingHTTPRequestHandler):
 
906
    """Only reply to simple range requests, errors out on multiple"""
 
907
 
 
908
    def get_multiple_ranges(self, file, file_size, ranges):
 
909
        """Refuses the multiple ranges request"""
 
910
        if len(ranges) > 1:
 
911
            file.close()
 
912
            self.send_error(416, "Requested range not satisfiable")
 
913
            return
 
914
        (start, end) = ranges[0]
 
915
        return self.get_single_range(file, file_size, start, end)
 
916
 
 
917
 
 
918
class TestSingleOnlyRangeRequestServer(TestRangeRequestServer):
 
919
    """Test readv against a server which only accept single range requests"""
 
920
 
 
921
    _req_handler_class = SingleOnlyRangeRequestHandler
 
922
 
 
923
 
 
924
class NoRangeRequestHandler(http_server.TestingHTTPRequestHandler):
 
925
    """Ignore range requests without notice"""
 
926
 
 
927
    def do_GET(self):
 
928
        # Update the statistics
 
929
        self.server.test_case_server.GET_request_nb += 1
 
930
        # Just bypass the range handling done by TestingHTTPRequestHandler
 
931
        return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
 
932
 
 
933
 
 
934
class TestNoRangeRequestServer(TestRangeRequestServer):
 
935
    """Test readv against a server which do not accept range requests"""
 
936
 
 
937
    _req_handler_class = NoRangeRequestHandler
 
938
 
 
939
 
 
940
class MultipleRangeWithoutContentLengthRequestHandler(
 
941
    http_server.TestingHTTPRequestHandler):
 
942
    """Reply to multiple range requests without content length header."""
 
943
 
 
944
    def get_multiple_ranges(self, file, file_size, ranges):
 
945
        self.send_response(206)
 
946
        self.send_header('Accept-Ranges', 'bytes')
 
947
        # XXX: this is strange; the 'random' name below seems undefined and
 
948
        # yet the tests pass -- mbp 2010-10-11 bug 658773
 
949
        boundary = "%d" % random.randint(0,0x7FFFFFFF)
 
950
        self.send_header("Content-Type",
 
951
                         "multipart/byteranges; boundary=%s" % boundary)
 
952
        self.end_headers()
 
953
        for (start, end) in ranges:
 
954
            self.wfile.write("--%s\r\n" % boundary)
 
955
            self.send_header("Content-type", 'application/octet-stream')
 
956
            self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
 
957
                                                                  end,
 
958
                                                                  file_size))
 
959
            self.end_headers()
 
960
            self.send_range_content(file, start, end - start + 1)
 
961
        # Final boundary
 
962
        self.wfile.write("--%s\r\n" % boundary)
 
963
 
 
964
 
 
965
class TestMultipleRangeWithoutContentLengthServer(TestRangeRequestServer):
 
966
 
 
967
    _req_handler_class = MultipleRangeWithoutContentLengthRequestHandler
 
968
 
 
969
 
 
970
class TruncatedMultipleRangeRequestHandler(
 
971
    http_server.TestingHTTPRequestHandler):
 
972
    """Reply to multiple range requests truncating the last ones.
 
973
 
 
974
    This server generates responses whose Content-Length describes all the
 
975
    ranges, but fail to include the last ones leading to client short reads.
 
976
    This has been observed randomly with lighttpd (bug #179368).
 
977
    """
 
978
 
 
979
    _truncated_ranges = 2
 
980
 
 
981
    def get_multiple_ranges(self, file, file_size, ranges):
 
982
        self.send_response(206)
 
983
        self.send_header('Accept-Ranges', 'bytes')
 
984
        boundary = 'tagada'
 
985
        self.send_header('Content-Type',
 
986
                         'multipart/byteranges; boundary=%s' % boundary)
 
987
        boundary_line = '--%s\r\n' % boundary
 
988
        # Calculate the Content-Length
 
989
        content_length = 0
 
990
        for (start, end) in ranges:
 
991
            content_length += len(boundary_line)
 
992
            content_length += self._header_line_length(
 
993
                'Content-type', 'application/octet-stream')
 
994
            content_length += self._header_line_length(
 
995
                'Content-Range', 'bytes %d-%d/%d' % (start, end, file_size))
 
996
            content_length += len('\r\n') # end headers
 
997
            content_length += end - start # + 1
 
998
        content_length += len(boundary_line)
 
999
        self.send_header('Content-length', content_length)
 
1000
        self.end_headers()
 
1001
 
 
1002
        # Send the multipart body
 
1003
        cur = 0
 
1004
        for (start, end) in ranges:
 
1005
            self.wfile.write(boundary_line)
 
1006
            self.send_header('Content-type', 'application/octet-stream')
 
1007
            self.send_header('Content-Range', 'bytes %d-%d/%d'
 
1008
                             % (start, end, file_size))
 
1009
            self.end_headers()
 
1010
            if cur + self._truncated_ranges >= len(ranges):
 
1011
                # Abruptly ends the response and close the connection
 
1012
                self.close_connection = 1
 
1013
                return
 
1014
            self.send_range_content(file, start, end - start + 1)
 
1015
            cur += 1
 
1016
        # Final boundary
 
1017
        self.wfile.write(boundary_line)
 
1018
 
 
1019
 
 
1020
class TestTruncatedMultipleRangeServer(TestSpecificRequestHandler):
 
1021
 
 
1022
    _req_handler_class = TruncatedMultipleRangeRequestHandler
 
1023
 
 
1024
    def setUp(self):
 
1025
        super(TestTruncatedMultipleRangeServer, self).setUp()
 
1026
        self.build_tree_contents([('a', '0123456789')],)
 
1027
 
 
1028
    def test_readv_with_short_reads(self):
 
1029
        server = self.get_readonly_server()
 
1030
        t = self.get_readonly_transport()
 
1031
        # Force separate ranges for each offset
 
1032
        t._bytes_to_read_before_seek = 0
 
1033
        ireadv = iter(t.readv('a', ((0, 1), (2, 1), (4, 2), (9, 1))))
 
1034
        self.assertEqual((0, '0'), ireadv.next())
 
1035
        self.assertEqual((2, '2'), ireadv.next())
 
1036
        if not self._testing_pycurl():
 
1037
            # Only one request have been issued so far (except for pycurl that
 
1038
            # try to read the whole response at once)
 
1039
            self.assertEqual(1, server.GET_request_nb)
 
1040
        self.assertEqual((4, '45'), ireadv.next())
 
1041
        self.assertEqual((9, '9'), ireadv.next())
 
1042
        # Both implementations issue 3 requests but:
 
1043
        # - urllib does two multiple (4 ranges, then 2 ranges) then a single
 
1044
        #   range,
 
1045
        # - pycurl does two multiple (4 ranges, 4 ranges) then a single range
 
1046
        self.assertEqual(3, server.GET_request_nb)
 
1047
        # Finally the client have tried a single range request and stays in
 
1048
        # that mode
 
1049
        self.assertEqual('single', t._range_hint)
 
1050
 
 
1051
 
 
1052
class TruncatedBeforeBoundaryRequestHandler(
 
1053
    http_server.TestingHTTPRequestHandler):
 
1054
    """Truncation before a boundary, like in bug 198646"""
 
1055
 
 
1056
    _truncated_ranges = 1
 
1057
 
 
1058
    def get_multiple_ranges(self, file, file_size, ranges):
 
1059
        self.send_response(206)
 
1060
        self.send_header('Accept-Ranges', 'bytes')
 
1061
        boundary = 'tagada'
 
1062
        self.send_header('Content-Type',
 
1063
                         'multipart/byteranges; boundary=%s' % boundary)
 
1064
        boundary_line = '--%s\r\n' % boundary
 
1065
        # Calculate the Content-Length
 
1066
        content_length = 0
 
1067
        for (start, end) in ranges:
 
1068
            content_length += len(boundary_line)
 
1069
            content_length += self._header_line_length(
 
1070
                'Content-type', 'application/octet-stream')
 
1071
            content_length += self._header_line_length(
 
1072
                'Content-Range', 'bytes %d-%d/%d' % (start, end, file_size))
 
1073
            content_length += len('\r\n') # end headers
 
1074
            content_length += end - start # + 1
 
1075
        content_length += len(boundary_line)
 
1076
        self.send_header('Content-length', content_length)
 
1077
        self.end_headers()
 
1078
 
 
1079
        # Send the multipart body
 
1080
        cur = 0
 
1081
        for (start, end) in ranges:
 
1082
            if cur + self._truncated_ranges >= len(ranges):
 
1083
                # Abruptly ends the response and close the connection
 
1084
                self.close_connection = 1
 
1085
                return
 
1086
            self.wfile.write(boundary_line)
 
1087
            self.send_header('Content-type', 'application/octet-stream')
 
1088
            self.send_header('Content-Range', 'bytes %d-%d/%d'
 
1089
                             % (start, end, file_size))
 
1090
            self.end_headers()
 
1091
            self.send_range_content(file, start, end - start + 1)
 
1092
            cur += 1
 
1093
        # Final boundary
 
1094
        self.wfile.write(boundary_line)
 
1095
 
 
1096
 
 
1097
class TestTruncatedBeforeBoundary(TestSpecificRequestHandler):
 
1098
    """Tests the case of bug 198646, disconnecting before a boundary."""
 
1099
 
 
1100
    _req_handler_class = TruncatedBeforeBoundaryRequestHandler
 
1101
 
 
1102
    def setUp(self):
 
1103
        super(TestTruncatedBeforeBoundary, self).setUp()
 
1104
        self.build_tree_contents([('a', '0123456789')],)
 
1105
 
 
1106
    def test_readv_with_short_reads(self):
 
1107
        server = self.get_readonly_server()
 
1108
        t = self.get_readonly_transport()
 
1109
        # Force separate ranges for each offset
 
1110
        t._bytes_to_read_before_seek = 0
 
1111
        ireadv = iter(t.readv('a', ((0, 1), (2, 1), (4, 2), (9, 1))))
 
1112
        self.assertEqual((0, '0'), ireadv.next())
 
1113
        self.assertEqual((2, '2'), ireadv.next())
 
1114
        self.assertEqual((4, '45'), ireadv.next())
 
1115
        self.assertEqual((9, '9'), ireadv.next())
 
1116
 
 
1117
 
 
1118
class LimitedRangeRequestHandler(http_server.TestingHTTPRequestHandler):
 
1119
    """Errors out when range specifiers exceed the limit"""
 
1120
 
 
1121
    def get_multiple_ranges(self, file, file_size, ranges):
 
1122
        """Refuses the multiple ranges request"""
 
1123
        tcs = self.server.test_case_server
 
1124
        if tcs.range_limit is not None and len(ranges) > tcs.range_limit:
 
1125
            file.close()
 
1126
            # Emulate apache behavior
 
1127
            self.send_error(400, "Bad Request")
 
1128
            return
 
1129
        return http_server.TestingHTTPRequestHandler.get_multiple_ranges(
 
1130
            self, file, file_size, ranges)
 
1131
 
 
1132
 
 
1133
class LimitedRangeHTTPServer(http_server.HttpServer):
 
1134
    """An HttpServer erroring out on requests with too much range specifiers"""
 
1135
 
 
1136
    def __init__(self, request_handler=LimitedRangeRequestHandler,
 
1137
                 protocol_version=None,
 
1138
                 range_limit=None):
 
1139
        http_server.HttpServer.__init__(self, request_handler,
 
1140
                                        protocol_version=protocol_version)
 
1141
        self.range_limit = range_limit
 
1142
 
 
1143
 
 
1144
class TestLimitedRangeRequestServer(http_utils.TestCaseWithWebserver):
 
1145
    """Tests readv requests against a server erroring out on too much ranges."""
 
1146
 
 
1147
    scenarios = multiply_scenarios(
 
1148
        vary_by_http_client_implementation(),
 
1149
        vary_by_http_protocol_version(),
 
1150
        )
 
1151
 
 
1152
    # Requests with more range specifiers will error out
 
1153
    range_limit = 3
 
1154
 
 
1155
    def create_transport_readonly_server(self):
 
1156
        return LimitedRangeHTTPServer(range_limit=self.range_limit,
 
1157
                                      protocol_version=self._protocol_version)
 
1158
 
 
1159
    def setUp(self):
 
1160
        http_utils.TestCaseWithWebserver.setUp(self)
 
1161
        # We need to manipulate ranges that correspond to real chunks in the
 
1162
        # response, so we build a content appropriately.
 
1163
        filler = ''.join(['abcdefghij' for x in range(102)])
 
1164
        content = ''.join(['%04d' % v + filler for v in range(16)])
 
1165
        self.build_tree_contents([('a', content)],)
 
1166
 
 
1167
    def test_few_ranges(self):
 
1168
        t = self.get_readonly_transport()
 
1169
        l = list(t.readv('a', ((0, 4), (1024, 4), )))
 
1170
        self.assertEqual(l[0], (0, '0000'))
 
1171
        self.assertEqual(l[1], (1024, '0001'))
 
1172
        self.assertEqual(1, self.get_readonly_server().GET_request_nb)
 
1173
 
 
1174
    def test_more_ranges(self):
 
1175
        t = self.get_readonly_transport()
 
1176
        l = list(t.readv('a', ((0, 4), (1024, 4), (4096, 4), (8192, 4))))
 
1177
        self.assertEqual(l[0], (0, '0000'))
 
1178
        self.assertEqual(l[1], (1024, '0001'))
 
1179
        self.assertEqual(l[2], (4096, '0004'))
 
1180
        self.assertEqual(l[3], (8192, '0008'))
 
1181
        # The server will refuse to serve the first request (too much ranges),
 
1182
        # a second request will succeed.
 
1183
        self.assertEqual(2, self.get_readonly_server().GET_request_nb)
 
1184
 
 
1185
 
 
1186
class TestHttpProxyWhiteBox(tests.TestCase):
 
1187
    """Whitebox test proxy http authorization.
 
1188
 
 
1189
    Only the urllib implementation is tested here.
 
1190
    """
 
1191
 
 
1192
    def _proxied_request(self):
 
1193
        handler = _urllib2_wrappers.ProxyHandler()
 
1194
        request = _urllib2_wrappers.Request('GET', 'http://baz/buzzle')
 
1195
        handler.set_proxy(request, 'http')
 
1196
        return request
 
1197
 
 
1198
    def assertEvaluateProxyBypass(self, expected, host, no_proxy):
 
1199
        handler = _urllib2_wrappers.ProxyHandler()
 
1200
        self.assertEquals(expected,
 
1201
                          handler.evaluate_proxy_bypass(host, no_proxy))
 
1202
 
 
1203
    def test_empty_user(self):
 
1204
        self.overrideEnv('http_proxy', 'http://bar.com')
 
1205
        request = self._proxied_request()
 
1206
        self.assertFalse(request.headers.has_key('Proxy-authorization'))
 
1207
 
 
1208
    def test_user_with_at(self):
 
1209
        self.overrideEnv('http_proxy',
 
1210
                         'http://username@domain:password@proxy_host:1234')
 
1211
        request = self._proxied_request()
 
1212
        self.assertFalse(request.headers.has_key('Proxy-authorization'))
 
1213
 
 
1214
    def test_invalid_proxy(self):
 
1215
        """A proxy env variable without scheme"""
 
1216
        self.overrideEnv('http_proxy', 'host:1234')
 
1217
        self.assertRaises(errors.InvalidURL, self._proxied_request)
 
1218
 
 
1219
    def test_evaluate_proxy_bypass_true(self):
 
1220
        """The host is not proxied"""
 
1221
        self.assertEvaluateProxyBypass(True, 'example.com', 'example.com')
 
1222
        self.assertEvaluateProxyBypass(True, 'bzr.example.com', '*example.com')
 
1223
 
 
1224
    def test_evaluate_proxy_bypass_false(self):
 
1225
        """The host is proxied"""
 
1226
        self.assertEvaluateProxyBypass(False, 'bzr.example.com', None)
 
1227
 
 
1228
    def test_evaluate_proxy_bypass_unknown(self):
 
1229
        """The host is not explicitly proxied"""
 
1230
        self.assertEvaluateProxyBypass(None, 'example.com', 'not.example.com')
 
1231
        self.assertEvaluateProxyBypass(None, 'bzr.example.com', 'example.com')
 
1232
 
 
1233
    def test_evaluate_proxy_bypass_empty_entries(self):
 
1234
        """Ignore empty entries"""
 
1235
        self.assertEvaluateProxyBypass(None, 'example.com', '')
 
1236
        self.assertEvaluateProxyBypass(None, 'example.com', ',')
 
1237
        self.assertEvaluateProxyBypass(None, 'example.com', 'foo,,bar')
 
1238
 
 
1239
 
 
1240
class TestProxyHttpServer(http_utils.TestCaseWithTwoWebservers):
 
1241
    """Tests proxy server.
 
1242
 
 
1243
    Be aware that we do not setup a real proxy here. Instead, we
 
1244
    check that the *connection* goes through the proxy by serving
 
1245
    different content (the faked proxy server append '-proxied'
 
1246
    to the file names).
 
1247
    """
 
1248
 
 
1249
    scenarios = multiply_scenarios(
 
1250
        vary_by_http_client_implementation(),
 
1251
        vary_by_http_protocol_version(),
 
1252
        )
 
1253
 
 
1254
    # FIXME: We don't have an https server available, so we don't
 
1255
    # test https connections. --vila toolongago
 
1256
 
 
1257
    def setUp(self):
 
1258
        super(TestProxyHttpServer, self).setUp()
 
1259
        self.transport_secondary_server = http_utils.ProxyServer
 
1260
        self.build_tree_contents([('foo', 'contents of foo\n'),
 
1261
                                  ('foo-proxied', 'proxied contents of foo\n')])
 
1262
        # Let's setup some attributes for tests
 
1263
        server = self.get_readonly_server()
 
1264
        self.server_host_port = '%s:%d' % (server.host, server.port)
 
1265
        if self._testing_pycurl():
 
1266
            # Oh my ! pycurl does not check for the port as part of
 
1267
            # no_proxy :-( So we just test the host part
 
1268
            self.no_proxy_host = server.host
 
1269
        else:
 
1270
            self.no_proxy_host = self.server_host_port
 
1271
        # The secondary server is the proxy
 
1272
        self.proxy_url = self.get_secondary_url()
 
1273
 
 
1274
    def _testing_pycurl(self):
 
1275
        # TODO: This is duplicated for lots of the classes in this file
 
1276
        return (features.pycurl.available()
 
1277
                and self._transport == PyCurlTransport)
 
1278
 
 
1279
    def assertProxied(self):
 
1280
        t = self.get_readonly_transport()
 
1281
        self.assertEqual('proxied contents of foo\n', t.get('foo').read())
 
1282
 
 
1283
    def assertNotProxied(self):
 
1284
        t = self.get_readonly_transport()
 
1285
        self.assertEqual('contents of foo\n', t.get('foo').read())
 
1286
 
 
1287
    def test_http_proxy(self):
 
1288
        self.overrideEnv('http_proxy', self.proxy_url)
 
1289
        self.assertProxied()
 
1290
 
 
1291
    def test_HTTP_PROXY(self):
 
1292
        if self._testing_pycurl():
 
1293
            # pycurl does not check HTTP_PROXY for security reasons
 
1294
            # (for use in a CGI context that we do not care
 
1295
            # about. Should we ?)
 
1296
            raise tests.TestNotApplicable(
 
1297
                'pycurl does not check HTTP_PROXY for security reasons')
 
1298
        self.overrideEnv('HTTP_PROXY', self.proxy_url)
 
1299
        self.assertProxied()
 
1300
 
 
1301
    def test_all_proxy(self):
 
1302
        self.overrideEnv('all_proxy', self.proxy_url)
 
1303
        self.assertProxied()
 
1304
 
 
1305
    def test_ALL_PROXY(self):
 
1306
        self.overrideEnv('ALL_PROXY', self.proxy_url)
 
1307
        self.assertProxied()
 
1308
 
 
1309
    def test_http_proxy_with_no_proxy(self):
 
1310
        self.overrideEnv('no_proxy', self.no_proxy_host)
 
1311
        self.overrideEnv('http_proxy', self.proxy_url)
 
1312
        self.assertNotProxied()
 
1313
 
 
1314
    def test_HTTP_PROXY_with_NO_PROXY(self):
 
1315
        if self._testing_pycurl():
 
1316
            raise tests.TestNotApplicable(
 
1317
                'pycurl does not check HTTP_PROXY for security reasons')
 
1318
        self.overrideEnv('NO_PROXY', self.no_proxy_host)
 
1319
        self.overrideEnv('HTTP_PROXY', self.proxy_url)
 
1320
        self.assertNotProxied()
 
1321
 
 
1322
    def test_all_proxy_with_no_proxy(self):
 
1323
        self.overrideEnv('no_proxy', self.no_proxy_host)
 
1324
        self.overrideEnv('all_proxy', self.proxy_url)
 
1325
        self.assertNotProxied()
 
1326
 
 
1327
    def test_ALL_PROXY_with_NO_PROXY(self):
 
1328
        self.overrideEnv('NO_PROXY', self.no_proxy_host)
 
1329
        self.overrideEnv('ALL_PROXY', self.proxy_url)
 
1330
        self.assertNotProxied()
 
1331
 
 
1332
    def test_http_proxy_without_scheme(self):
 
1333
        self.overrideEnv('http_proxy', self.server_host_port)
 
1334
        if self._testing_pycurl():
 
1335
            # pycurl *ignores* invalid proxy env variables. If that ever change
 
1336
            # in the future, this test will fail indicating that pycurl do not
 
1337
            # ignore anymore such variables.
 
1338
            self.assertNotProxied()
 
1339
        else:
 
1340
            self.assertRaises(errors.InvalidURL, self.assertProxied)
 
1341
 
 
1342
 
 
1343
class TestRanges(http_utils.TestCaseWithWebserver):
 
1344
    """Test the Range header in GET methods."""
 
1345
 
 
1346
    scenarios = multiply_scenarios(
 
1347
        vary_by_http_client_implementation(),
 
1348
        vary_by_http_protocol_version(),
 
1349
        )
 
1350
 
 
1351
    def setUp(self):
 
1352
        http_utils.TestCaseWithWebserver.setUp(self)
 
1353
        self.build_tree_contents([('a', '0123456789')],)
 
1354
 
 
1355
    def create_transport_readonly_server(self):
 
1356
        return http_server.HttpServer(protocol_version=self._protocol_version)
 
1357
 
 
1358
    def _file_contents(self, relpath, ranges):
 
1359
        t = self.get_readonly_transport()
 
1360
        offsets = [ (start, end - start + 1) for start, end in ranges]
 
1361
        coalesce = t._coalesce_offsets
 
1362
        coalesced = list(coalesce(offsets, limit=0, fudge_factor=0))
 
1363
        code, data = t._get(relpath, coalesced)
 
1364
        self.assertTrue(code in (200, 206),'_get returns: %d' % code)
 
1365
        for start, end in ranges:
 
1366
            data.seek(start)
 
1367
            yield data.read(end - start + 1)
 
1368
 
 
1369
    def _file_tail(self, relpath, tail_amount):
 
1370
        t = self.get_readonly_transport()
 
1371
        code, data = t._get(relpath, [], tail_amount)
 
1372
        self.assertTrue(code in (200, 206),'_get returns: %d' % code)
 
1373
        data.seek(-tail_amount, 2)
 
1374
        return data.read(tail_amount)
 
1375
 
 
1376
    def test_range_header(self):
 
1377
        # Valid ranges
 
1378
        map(self.assertEqual,['0', '234'],
 
1379
            list(self._file_contents('a', [(0,0), (2,4)])),)
 
1380
 
 
1381
    def test_range_header_tail(self):
 
1382
        self.assertEqual('789', self._file_tail('a', 3))
 
1383
 
 
1384
    def test_syntactically_invalid_range_header(self):
 
1385
        self.assertListRaises(errors.InvalidHttpRange,
 
1386
                          self._file_contents, 'a', [(4, 3)])
 
1387
 
 
1388
    def test_semantically_invalid_range_header(self):
 
1389
        self.assertListRaises(errors.InvalidHttpRange,
 
1390
                          self._file_contents, 'a', [(42, 128)])
 
1391
 
 
1392
 
 
1393
class TestHTTPRedirections(http_utils.TestCaseWithRedirectedWebserver):
 
1394
    """Test redirection between http servers."""
 
1395
 
 
1396
    scenarios = multiply_scenarios(
 
1397
        vary_by_http_client_implementation(),
 
1398
        vary_by_http_protocol_version(),
 
1399
        )
 
1400
 
 
1401
    def setUp(self):
 
1402
        super(TestHTTPRedirections, self).setUp()
 
1403
        self.build_tree_contents([('a', '0123456789'),
 
1404
                                  ('bundle',
 
1405
                                  '# Bazaar revision bundle v0.9\n#\n')
 
1406
                                  ],)
 
1407
 
 
1408
    def test_redirected(self):
 
1409
        self.assertRaises(errors.RedirectRequested,
 
1410
                          self.get_old_transport().get, 'a')
 
1411
        self.assertEqual('0123456789', self.get_new_transport().get('a').read())
 
1412
 
 
1413
 
 
1414
class RedirectedRequest(_urllib2_wrappers.Request):
 
1415
    """Request following redirections. """
 
1416
 
 
1417
    init_orig = _urllib2_wrappers.Request.__init__
 
1418
 
 
1419
    def __init__(self, method, url, *args, **kwargs):
 
1420
        """Constructor.
 
1421
 
 
1422
        """
 
1423
        # Since the tests using this class will replace
 
1424
        # _urllib2_wrappers.Request, we can't just call the base class __init__
 
1425
        # or we'll loop.
 
1426
        RedirectedRequest.init_orig(self, method, url, *args, **kwargs)
 
1427
        self.follow_redirections = True
 
1428
 
 
1429
 
 
1430
def install_redirected_request(test):
 
1431
    test.overrideAttr(_urllib2_wrappers, 'Request', RedirectedRequest)
 
1432
 
 
1433
 
 
1434
def cleanup_http_redirection_connections(test):
 
1435
    # Some sockets are opened but never seen by _urllib, so we trap them at
 
1436
    # the _urllib2_wrappers level to be able to clean them up.
 
1437
    def socket_disconnect(sock):
 
1438
        try:
 
1439
            sock.shutdown(socket.SHUT_RDWR)
 
1440
            sock.close()
 
1441
        except socket.error:
 
1442
            pass
 
1443
    def connect(connection):
 
1444
        test.http_connect_orig(connection)
 
1445
        test.addCleanup(socket_disconnect, connection.sock)
 
1446
    test.http_connect_orig = test.overrideAttr(
 
1447
        _urllib2_wrappers.HTTPConnection, 'connect', connect)
 
1448
    def connect(connection):
 
1449
        test.https_connect_orig(connection)
 
1450
        test.addCleanup(socket_disconnect, connection.sock)
 
1451
    test.https_connect_orig = test.overrideAttr(
 
1452
        _urllib2_wrappers.HTTPSConnection, 'connect', connect)
 
1453
 
 
1454
 
 
1455
class TestHTTPSilentRedirections(http_utils.TestCaseWithRedirectedWebserver):
 
1456
    """Test redirections.
 
1457
 
 
1458
    http implementations do not redirect silently anymore (they
 
1459
    do not redirect at all in fact). The mechanism is still in
 
1460
    place at the _urllib2_wrappers.Request level and these tests
 
1461
    exercise it.
 
1462
 
 
1463
    For the pycurl implementation
 
1464
    the redirection have been deleted as we may deprecate pycurl
 
1465
    and I have no place to keep a working implementation.
 
1466
    -- vila 20070212
 
1467
    """
 
1468
 
 
1469
    scenarios = multiply_scenarios(
 
1470
        vary_by_http_client_implementation(),
 
1471
        vary_by_http_protocol_version(),
 
1472
        )
 
1473
 
 
1474
    def setUp(self):
 
1475
        if (features.pycurl.available()
 
1476
            and self._transport == PyCurlTransport):
 
1477
            raise tests.TestNotApplicable(
 
1478
                "pycurl doesn't redirect silently anymore")
 
1479
        super(TestHTTPSilentRedirections, self).setUp()
 
1480
        install_redirected_request(self)
 
1481
        cleanup_http_redirection_connections(self)
 
1482
        self.build_tree_contents([('a','a'),
 
1483
                                  ('1/',),
 
1484
                                  ('1/a', 'redirected once'),
 
1485
                                  ('2/',),
 
1486
                                  ('2/a', 'redirected twice'),
 
1487
                                  ('3/',),
 
1488
                                  ('3/a', 'redirected thrice'),
 
1489
                                  ('4/',),
 
1490
                                  ('4/a', 'redirected 4 times'),
 
1491
                                  ('5/',),
 
1492
                                  ('5/a', 'redirected 5 times'),
 
1493
                                  ],)
 
1494
 
 
1495
    def test_one_redirection(self):
 
1496
        t = self.get_old_transport()
 
1497
        req = RedirectedRequest('GET', t._remote_path('a'))
 
1498
        new_prefix = 'http://%s:%s' % (self.new_server.host,
 
1499
                                       self.new_server.port)
 
1500
        self.old_server.redirections = \
 
1501
            [('(.*)', r'%s/1\1' % (new_prefix), 301),]
 
1502
        self.assertEqual('redirected once', t._perform(req).read())
 
1503
 
 
1504
    def test_five_redirections(self):
 
1505
        t = self.get_old_transport()
 
1506
        req = RedirectedRequest('GET', t._remote_path('a'))
 
1507
        old_prefix = 'http://%s:%s' % (self.old_server.host,
 
1508
                                       self.old_server.port)
 
1509
        new_prefix = 'http://%s:%s' % (self.new_server.host,
 
1510
                                       self.new_server.port)
 
1511
        self.old_server.redirections = [
 
1512
            ('/1(.*)', r'%s/2\1' % (old_prefix), 302),
 
1513
            ('/2(.*)', r'%s/3\1' % (old_prefix), 303),
 
1514
            ('/3(.*)', r'%s/4\1' % (old_prefix), 307),
 
1515
            ('/4(.*)', r'%s/5\1' % (new_prefix), 301),
 
1516
            ('(/[^/]+)', r'%s/1\1' % (old_prefix), 301),
 
1517
            ]
 
1518
        self.assertEqual('redirected 5 times', t._perform(req).read())
 
1519
 
 
1520
 
 
1521
class TestDoCatchRedirections(http_utils.TestCaseWithRedirectedWebserver):
 
1522
    """Test transport.do_catching_redirections."""
 
1523
 
 
1524
    scenarios = multiply_scenarios(
 
1525
        vary_by_http_client_implementation(),
 
1526
        vary_by_http_protocol_version(),
 
1527
        )
 
1528
 
 
1529
    def setUp(self):
 
1530
        super(TestDoCatchRedirections, self).setUp()
 
1531
        self.build_tree_contents([('a', '0123456789'),],)
 
1532
        cleanup_http_redirection_connections(self)
 
1533
 
 
1534
        self.old_transport = self.get_old_transport()
 
1535
 
 
1536
    def get_a(self, t):
 
1537
        return t.get('a')
 
1538
 
 
1539
    def test_no_redirection(self):
 
1540
        t = self.get_new_transport()
 
1541
 
 
1542
        # We use None for redirected so that we fail if redirected
 
1543
        self.assertEqual('0123456789',
 
1544
                         transport.do_catching_redirections(
 
1545
                self.get_a, t, None).read())
 
1546
 
 
1547
    def test_one_redirection(self):
 
1548
        self.redirections = 0
 
1549
 
 
1550
        def redirected(t, exception, redirection_notice):
 
1551
            self.redirections += 1
 
1552
            redirected_t = t._redirected_to(exception.source, exception.target)
 
1553
            return redirected_t
 
1554
 
 
1555
        self.assertEqual('0123456789',
 
1556
                         transport.do_catching_redirections(
 
1557
                self.get_a, self.old_transport, redirected).read())
 
1558
        self.assertEqual(1, self.redirections)
 
1559
 
 
1560
    def test_redirection_loop(self):
 
1561
 
 
1562
        def redirected(transport, exception, redirection_notice):
 
1563
            # By using the redirected url as a base dir for the
 
1564
            # *old* transport, we create a loop: a => a/a =>
 
1565
            # a/a/a
 
1566
            return self.old_transport.clone(exception.target)
 
1567
 
 
1568
        self.assertRaises(errors.TooManyRedirections,
 
1569
                          transport.do_catching_redirections,
 
1570
                          self.get_a, self.old_transport, redirected)
 
1571
 
 
1572
 
 
1573
def _setup_authentication_config(**kwargs):
 
1574
    conf = config.AuthenticationConfig()
 
1575
    conf._get_config().update({'httptest': kwargs})
 
1576
    conf._save()
 
1577
 
 
1578
 
 
1579
class TestUrllib2AuthHandler(tests.TestCaseWithTransport):
 
1580
    """Unit tests for glue by which urllib2 asks us for authentication"""
 
1581
 
 
1582
    def test_get_user_password_without_port(self):
 
1583
        """We cope if urllib2 doesn't tell us the port.
 
1584
 
 
1585
        See https://bugs.launchpad.net/bzr/+bug/654684
 
1586
        """
 
1587
        user = 'joe'
 
1588
        password = 'foo'
 
1589
        _setup_authentication_config(scheme='http', host='localhost',
 
1590
                                     user=user, password=password)
 
1591
        handler = _urllib2_wrappers.HTTPAuthHandler()
 
1592
        got_pass = handler.get_user_password(dict(
 
1593
            user='joe',
 
1594
            protocol='http',
 
1595
            host='localhost',
 
1596
            path='/',
 
1597
            realm='Realm',
 
1598
            ))
 
1599
        self.assertEquals((user, password), got_pass)
 
1600
 
 
1601
 
 
1602
class TestAuth(http_utils.TestCaseWithWebserver):
 
1603
    """Test authentication scheme"""
 
1604
 
 
1605
    scenarios = multiply_scenarios(
 
1606
        vary_by_http_client_implementation(),
 
1607
        vary_by_http_protocol_version(),
 
1608
        vary_by_http_auth_scheme(),
 
1609
        )
 
1610
 
 
1611
    def setUp(self):
 
1612
        super(TestAuth, self).setUp()
 
1613
        self.server = self.get_readonly_server()
 
1614
        self.build_tree_contents([('a', 'contents of a\n'),
 
1615
                                  ('b', 'contents of b\n'),])
 
1616
 
 
1617
    def create_transport_readonly_server(self):
 
1618
        server = self._auth_server(protocol_version=self._protocol_version)
 
1619
        server._url_protocol = self._url_protocol
 
1620
        return server
 
1621
 
 
1622
    def _testing_pycurl(self):
 
1623
        # TODO: This is duplicated for lots of the classes in this file
 
1624
        return (features.pycurl.available()
 
1625
                and self._transport == PyCurlTransport)
 
1626
 
 
1627
    def get_user_url(self, user, password):
 
1628
        """Build an url embedding user and password"""
 
1629
        url = '%s://' % self.server._url_protocol
 
1630
        if user is not None:
 
1631
            url += user
 
1632
            if password is not None:
 
1633
                url += ':' + password
 
1634
            url += '@'
 
1635
        url += '%s:%s/' % (self.server.host, self.server.port)
 
1636
        return url
 
1637
 
 
1638
    def get_user_transport(self, user, password):
 
1639
        t = transport.get_transport_from_url(
 
1640
            self.get_user_url(user, password))
 
1641
        return t
 
1642
 
 
1643
    def test_no_user(self):
 
1644
        self.server.add_user('joe', 'foo')
 
1645
        t = self.get_user_transport(None, None)
 
1646
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
 
1647
        # Only one 'Authentication Required' error should occur
 
1648
        self.assertEqual(1, self.server.auth_required_errors)
 
1649
 
 
1650
    def test_empty_pass(self):
 
1651
        self.server.add_user('joe', '')
 
1652
        t = self.get_user_transport('joe', '')
 
1653
        self.assertEqual('contents of a\n', t.get('a').read())
 
1654
        # Only one 'Authentication Required' error should occur
 
1655
        self.assertEqual(1, self.server.auth_required_errors)
 
1656
 
 
1657
    def test_user_pass(self):
 
1658
        self.server.add_user('joe', 'foo')
 
1659
        t = self.get_user_transport('joe', 'foo')
 
1660
        self.assertEqual('contents of a\n', t.get('a').read())
 
1661
        # Only one 'Authentication Required' error should occur
 
1662
        self.assertEqual(1, self.server.auth_required_errors)
 
1663
 
 
1664
    def test_unknown_user(self):
 
1665
        self.server.add_user('joe', 'foo')
 
1666
        t = self.get_user_transport('bill', 'foo')
 
1667
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
 
1668
        # Two 'Authentication Required' errors should occur (the
 
1669
        # initial 'who are you' and 'I don't know you, who are
 
1670
        # you').
 
1671
        self.assertEqual(2, self.server.auth_required_errors)
 
1672
 
 
1673
    def test_wrong_pass(self):
 
1674
        self.server.add_user('joe', 'foo')
 
1675
        t = self.get_user_transport('joe', 'bar')
 
1676
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
 
1677
        # Two 'Authentication Required' errors should occur (the
 
1678
        # initial 'who are you' and 'this is not you, who are you')
 
1679
        self.assertEqual(2, self.server.auth_required_errors)
 
1680
 
 
1681
    def test_prompt_for_username(self):
 
1682
        if self._testing_pycurl():
 
1683
            raise tests.TestNotApplicable(
 
1684
                'pycurl cannot prompt, it handles auth by embedding'
 
1685
                ' user:pass in urls only')
 
1686
 
 
1687
        self.server.add_user('joe', 'foo')
 
1688
        t = self.get_user_transport(None, None)
 
1689
        stdout = tests.StringIOWrapper()
 
1690
        stderr = tests.StringIOWrapper()
 
1691
        ui.ui_factory = tests.TestUIFactory(stdin='joe\nfoo\n',
 
1692
                                            stdout=stdout, stderr=stderr)
 
1693
        self.assertEqual('contents of a\n',t.get('a').read())
 
1694
        # stdin should be empty
 
1695
        self.assertEqual('', ui.ui_factory.stdin.readline())
 
1696
        stderr.seek(0)
 
1697
        expected_prompt = self._expected_username_prompt(t._unqualified_scheme)
 
1698
        self.assertEqual(expected_prompt, stderr.read(len(expected_prompt)))
 
1699
        self.assertEqual('', stdout.getvalue())
 
1700
        self._check_password_prompt(t._unqualified_scheme, 'joe',
 
1701
                                    stderr.readline())
 
1702
 
 
1703
    def test_prompt_for_password(self):
 
1704
        if self._testing_pycurl():
 
1705
            raise tests.TestNotApplicable(
 
1706
                'pycurl cannot prompt, it handles auth by embedding'
 
1707
                ' user:pass in urls only')
 
1708
 
 
1709
        self.server.add_user('joe', 'foo')
 
1710
        t = self.get_user_transport('joe', None)
 
1711
        stdout = tests.StringIOWrapper()
 
1712
        stderr = tests.StringIOWrapper()
 
1713
        ui.ui_factory = tests.TestUIFactory(stdin='foo\n',
 
1714
                                            stdout=stdout, stderr=stderr)
 
1715
        self.assertEqual('contents of a\n', t.get('a').read())
 
1716
        # stdin should be empty
 
1717
        self.assertEqual('', ui.ui_factory.stdin.readline())
 
1718
        self._check_password_prompt(t._unqualified_scheme, 'joe',
 
1719
                                    stderr.getvalue())
 
1720
        self.assertEqual('', stdout.getvalue())
 
1721
        # And we shouldn't prompt again for a different request
 
1722
        # against the same transport.
 
1723
        self.assertEqual('contents of b\n',t.get('b').read())
 
1724
        t2 = t.clone()
 
1725
        # And neither against a clone
 
1726
        self.assertEqual('contents of b\n',t2.get('b').read())
 
1727
        # Only one 'Authentication Required' error should occur
 
1728
        self.assertEqual(1, self.server.auth_required_errors)
 
1729
 
 
1730
    def _check_password_prompt(self, scheme, user, actual_prompt):
 
1731
        expected_prompt = (self._password_prompt_prefix
 
1732
                           + ("%s %s@%s:%d, Realm: '%s' password: "
 
1733
                              % (scheme.upper(),
 
1734
                                 user, self.server.host, self.server.port,
 
1735
                                 self.server.auth_realm)))
 
1736
        self.assertEqual(expected_prompt, actual_prompt)
 
1737
 
 
1738
    def _expected_username_prompt(self, scheme):
 
1739
        return (self._username_prompt_prefix
 
1740
                + "%s %s:%d, Realm: '%s' username: " % (scheme.upper(),
 
1741
                                 self.server.host, self.server.port,
 
1742
                                 self.server.auth_realm))
 
1743
 
 
1744
    def test_no_prompt_for_password_when_using_auth_config(self):
 
1745
        if self._testing_pycurl():
 
1746
            raise tests.TestNotApplicable(
 
1747
                'pycurl does not support authentication.conf'
 
1748
                ' since it cannot prompt')
 
1749
 
 
1750
        user =' joe'
 
1751
        password = 'foo'
 
1752
        stdin_content = 'bar\n'  # Not the right password
 
1753
        self.server.add_user(user, password)
 
1754
        t = self.get_user_transport(user, None)
 
1755
        ui.ui_factory = tests.TestUIFactory(stdin=stdin_content,
 
1756
                                            stderr=tests.StringIOWrapper())
 
1757
        # Create a minimal config file with the right password
 
1758
        _setup_authentication_config(scheme='http', port=self.server.port,
 
1759
                                     user=user, password=password)
 
1760
        # Issue a request to the server to connect
 
1761
        self.assertEqual('contents of a\n',t.get('a').read())
 
1762
        # stdin should have  been left untouched
 
1763
        self.assertEqual(stdin_content, ui.ui_factory.stdin.readline())
 
1764
        # Only one 'Authentication Required' error should occur
 
1765
        self.assertEqual(1, self.server.auth_required_errors)
 
1766
 
 
1767
    def test_changing_nonce(self):
 
1768
        if self._auth_server not in (http_utils.HTTPDigestAuthServer,
 
1769
                                     http_utils.ProxyDigestAuthServer):
 
1770
            raise tests.TestNotApplicable('HTTP/proxy auth digest only test')
 
1771
        if self._testing_pycurl():
 
1772
            self.knownFailure(
 
1773
                'pycurl does not handle a nonce change')
 
1774
        self.server.add_user('joe', 'foo')
 
1775
        t = self.get_user_transport('joe', 'foo')
 
1776
        self.assertEqual('contents of a\n', t.get('a').read())
 
1777
        self.assertEqual('contents of b\n', t.get('b').read())
 
1778
        # Only one 'Authentication Required' error should have
 
1779
        # occured so far
 
1780
        self.assertEqual(1, self.server.auth_required_errors)
 
1781
        # The server invalidates the current nonce
 
1782
        self.server.auth_nonce = self.server.auth_nonce + '. No, now!'
 
1783
        self.assertEqual('contents of a\n', t.get('a').read())
 
1784
        # Two 'Authentication Required' errors should occur (the
 
1785
        # initial 'who are you' and a second 'who are you' with the new nonce)
 
1786
        self.assertEqual(2, self.server.auth_required_errors)
 
1787
 
 
1788
    def test_user_from_auth_conf(self):
 
1789
        if self._testing_pycurl():
 
1790
            raise tests.TestNotApplicable(
 
1791
                'pycurl does not support authentication.conf')
 
1792
        user = 'joe'
 
1793
        password = 'foo'
 
1794
        self.server.add_user(user, password)
 
1795
        _setup_authentication_config(scheme='http', port=self.server.port,
 
1796
                                     user=user, password=password)
 
1797
        t = self.get_user_transport(None, None)
 
1798
        # Issue a request to the server to connect
 
1799
        self.assertEqual('contents of a\n', t.get('a').read())
 
1800
        # Only one 'Authentication Required' error should occur
 
1801
        self.assertEqual(1, self.server.auth_required_errors)
 
1802
 
 
1803
    def test_no_credential_leaks_in_log(self):
 
1804
        self.overrideAttr(debug, 'debug_flags', set(['http']))
 
1805
        user = 'joe'
 
1806
        password = 'very-sensitive-password'
 
1807
        self.server.add_user(user, password)
 
1808
        t = self.get_user_transport(user, password)
 
1809
        # Capture the debug calls to mutter
 
1810
        self.mutters = []
 
1811
        def mutter(*args):
 
1812
            lines = args[0] % args[1:]
 
1813
            # Some calls output multiple lines, just split them now since we
 
1814
            # care about a single one later.
 
1815
            self.mutters.extend(lines.splitlines())
 
1816
        self.overrideAttr(trace, 'mutter', mutter)
 
1817
        # Issue a request to the server to connect
 
1818
        self.assertEqual(True, t.has('a'))
 
1819
        # Only one 'Authentication Required' error should occur
 
1820
        self.assertEqual(1, self.server.auth_required_errors)
 
1821
        # Since the authentification succeeded, there should be a corresponding
 
1822
        # debug line
 
1823
        sent_auth_headers = [line for line in self.mutters
 
1824
                             if line.startswith('> %s' % (self._auth_header,))]
 
1825
        self.assertLength(1, sent_auth_headers)
 
1826
        self.assertStartsWith(sent_auth_headers[0],
 
1827
                              '> %s: <masked>' % (self._auth_header,))
 
1828
 
 
1829
 
 
1830
class TestProxyAuth(TestAuth):
 
1831
    """Test proxy authentication schemes.
 
1832
 
 
1833
    This inherits from TestAuth to tweak the setUp and filter some failing
 
1834
    tests.
 
1835
    """
 
1836
 
 
1837
    scenarios = multiply_scenarios(
 
1838
        vary_by_http_client_implementation(),
 
1839
        vary_by_http_protocol_version(),
 
1840
        vary_by_http_proxy_auth_scheme(),
 
1841
        )
 
1842
 
 
1843
    def setUp(self):
 
1844
        super(TestProxyAuth, self).setUp()
 
1845
        # Override the contents to avoid false positives
 
1846
        self.build_tree_contents([('a', 'not proxied contents of a\n'),
 
1847
                                  ('b', 'not proxied contents of b\n'),
 
1848
                                  ('a-proxied', 'contents of a\n'),
 
1849
                                  ('b-proxied', 'contents of b\n'),
 
1850
                                  ])
 
1851
 
 
1852
    def get_user_transport(self, user, password):
 
1853
        self.overrideEnv('all_proxy', self.get_user_url(user, password))
 
1854
        return TestAuth.get_user_transport(self, user, password)
 
1855
 
 
1856
    def test_empty_pass(self):
 
1857
        if self._testing_pycurl():
 
1858
            import pycurl
 
1859
            if pycurl.version_info()[1] < '7.16.0':
 
1860
                self.knownFailure(
 
1861
                    'pycurl < 7.16.0 does not handle empty proxy passwords')
 
1862
        super(TestProxyAuth, self).test_empty_pass()
 
1863
 
 
1864
 
 
1865
class SampleSocket(object):
 
1866
    """A socket-like object for use in testing the HTTP request handler."""
 
1867
 
 
1868
    def __init__(self, socket_read_content):
 
1869
        """Constructs a sample socket.
 
1870
 
 
1871
        :param socket_read_content: a byte sequence
 
1872
        """
 
1873
        # Use plain python StringIO so we can monkey-patch the close method to
 
1874
        # not discard the contents.
 
1875
        from StringIO import StringIO
 
1876
        self.readfile = StringIO(socket_read_content)
 
1877
        self.writefile = StringIO()
 
1878
        self.writefile.close = lambda: None
 
1879
        self.close = lambda: None
 
1880
 
 
1881
    def makefile(self, mode='r', bufsize=None):
 
1882
        if 'r' in mode:
 
1883
            return self.readfile
 
1884
        else:
 
1885
            return self.writefile
 
1886
 
 
1887
 
 
1888
class SmartHTTPTunnellingTest(tests.TestCaseWithTransport):
 
1889
 
 
1890
    scenarios = multiply_scenarios(
 
1891
        vary_by_http_client_implementation(),
 
1892
        vary_by_http_protocol_version(),
 
1893
        )
 
1894
 
 
1895
    def setUp(self):
 
1896
        super(SmartHTTPTunnellingTest, self).setUp()
 
1897
        # We use the VFS layer as part of HTTP tunnelling tests.
 
1898
        self.overrideEnv('BZR_NO_SMART_VFS', None)
 
1899
        self.transport_readonly_server = http_utils.HTTPServerWithSmarts
 
1900
        self.http_server = self.get_readonly_server()
 
1901
 
 
1902
    def create_transport_readonly_server(self):
 
1903
        server = http_utils.HTTPServerWithSmarts(
 
1904
            protocol_version=self._protocol_version)
 
1905
        server._url_protocol = self._url_protocol
 
1906
        return server
 
1907
 
 
1908
    def test_open_bzrdir(self):
 
1909
        branch = self.make_branch('relpath')
 
1910
        url = self.http_server.get_url() + 'relpath'
 
1911
        bd = bzrdir.BzrDir.open(url)
 
1912
        self.addCleanup(bd.transport.disconnect)
 
1913
        self.assertIsInstance(bd, _mod_remote.RemoteBzrDir)
 
1914
 
 
1915
    def test_bulk_data(self):
 
1916
        # We should be able to send and receive bulk data in a single message.
 
1917
        # The 'readv' command in the smart protocol both sends and receives
 
1918
        # bulk data, so we use that.
 
1919
        self.build_tree(['data-file'])
 
1920
        http_transport = transport.get_transport_from_url(
 
1921
            self.http_server.get_url())
 
1922
        medium = http_transport.get_smart_medium()
 
1923
        # Since we provide the medium, the url below will be mostly ignored
 
1924
        # during the test, as long as the path is '/'.
 
1925
        remote_transport = remote.RemoteTransport('bzr://fake_host/',
 
1926
                                                  medium=medium)
 
1927
        self.assertEqual(
 
1928
            [(0, "c")], list(remote_transport.readv("data-file", [(0,1)])))
 
1929
 
 
1930
    def test_http_send_smart_request(self):
 
1931
 
 
1932
        post_body = 'hello\n'
 
1933
        expected_reply_body = 'ok\x012\n'
 
1934
 
 
1935
        http_transport = transport.get_transport_from_url(
 
1936
            self.http_server.get_url())
 
1937
        medium = http_transport.get_smart_medium()
 
1938
        response = medium.send_http_smart_request(post_body)
 
1939
        reply_body = response.read()
 
1940
        self.assertEqual(expected_reply_body, reply_body)
 
1941
 
 
1942
    def test_smart_http_server_post_request_handler(self):
 
1943
        httpd = self.http_server.server
 
1944
 
 
1945
        socket = SampleSocket(
 
1946
            'POST /.bzr/smart %s \r\n' % self._protocol_version
 
1947
            # HTTP/1.1 posts must have a Content-Length (but it doesn't hurt
 
1948
            # for 1.0)
 
1949
            + 'Content-Length: 6\r\n'
 
1950
            '\r\n'
 
1951
            'hello\n')
 
1952
        # Beware: the ('localhost', 80) below is the
 
1953
        # client_address parameter, but we don't have one because
 
1954
        # we have defined a socket which is not bound to an
 
1955
        # address. The test framework never uses this client
 
1956
        # address, so far...
 
1957
        request_handler = http_utils.SmartRequestHandler(socket,
 
1958
                                                         ('localhost', 80),
 
1959
                                                         httpd)
 
1960
        response = socket.writefile.getvalue()
 
1961
        self.assertStartsWith(response, '%s 200 ' % self._protocol_version)
 
1962
        # This includes the end of the HTTP headers, and all the body.
 
1963
        expected_end_of_response = '\r\n\r\nok\x012\n'
 
1964
        self.assertEndsWith(response, expected_end_of_response)
 
1965
 
 
1966
 
 
1967
class ForbiddenRequestHandler(http_server.TestingHTTPRequestHandler):
 
1968
    """No smart server here request handler."""
 
1969
 
 
1970
    def do_POST(self):
 
1971
        self.send_error(403, "Forbidden")
 
1972
 
 
1973
 
 
1974
class SmartClientAgainstNotSmartServer(TestSpecificRequestHandler):
 
1975
    """Test smart client behaviour against an http server without smarts."""
 
1976
 
 
1977
    _req_handler_class = ForbiddenRequestHandler
 
1978
 
 
1979
    def test_probe_smart_server(self):
 
1980
        """Test error handling against server refusing smart requests."""
 
1981
        t = self.get_readonly_transport()
 
1982
        # No need to build a valid smart request here, the server will not even
 
1983
        # try to interpret it.
 
1984
        self.assertRaises(errors.SmartProtocolError,
 
1985
                          t.get_smart_medium().send_http_smart_request,
 
1986
                          'whatever')
 
1987
 
 
1988
 
 
1989
class Test_redirected_to(tests.TestCase):
 
1990
 
 
1991
    scenarios = vary_by_http_client_implementation()
 
1992
 
 
1993
    def test_redirected_to_subdir(self):
 
1994
        t = self._transport('http://www.example.com/foo')
 
1995
        r = t._redirected_to('http://www.example.com/foo',
 
1996
                             'http://www.example.com/foo/subdir')
 
1997
        self.assertIsInstance(r, type(t))
 
1998
        # Both transports share the some connection
 
1999
        self.assertEqual(t._get_connection(), r._get_connection())
 
2000
        self.assertEquals('http://www.example.com/foo/subdir/', r.base)
 
2001
 
 
2002
    def test_redirected_to_self_with_slash(self):
 
2003
        t = self._transport('http://www.example.com/foo')
 
2004
        r = t._redirected_to('http://www.example.com/foo',
 
2005
                             'http://www.example.com/foo/')
 
2006
        self.assertIsInstance(r, type(t))
 
2007
        # Both transports share the some connection (one can argue that we
 
2008
        # should return the exact same transport here, but that seems
 
2009
        # overkill).
 
2010
        self.assertEqual(t._get_connection(), r._get_connection())
 
2011
 
 
2012
    def test_redirected_to_host(self):
 
2013
        t = self._transport('http://www.example.com/foo')
 
2014
        r = t._redirected_to('http://www.example.com/foo',
 
2015
                             'http://foo.example.com/foo/subdir')
 
2016
        self.assertIsInstance(r, type(t))
 
2017
        self.assertEquals('http://foo.example.com/foo/subdir/',
 
2018
            r.external_url())
 
2019
 
 
2020
    def test_redirected_to_same_host_sibling_protocol(self):
 
2021
        t = self._transport('http://www.example.com/foo')
 
2022
        r = t._redirected_to('http://www.example.com/foo',
 
2023
                             'https://www.example.com/foo')
 
2024
        self.assertIsInstance(r, type(t))
 
2025
        self.assertEquals('https://www.example.com/foo/',
 
2026
            r.external_url())
 
2027
 
 
2028
    def test_redirected_to_same_host_different_protocol(self):
 
2029
        t = self._transport('http://www.example.com/foo')
 
2030
        r = t._redirected_to('http://www.example.com/foo',
 
2031
                             'ftp://www.example.com/foo')
 
2032
        self.assertNotEquals(type(r), type(t))
 
2033
        self.assertEquals('ftp://www.example.com/foo/', r.external_url())
 
2034
 
 
2035
    def test_redirected_to_same_host_specific_implementation(self):
 
2036
        t = self._transport('http://www.example.com/foo')
 
2037
        r = t._redirected_to('http://www.example.com/foo',
 
2038
                             'https+urllib://www.example.com/foo')
 
2039
        self.assertEquals('https://www.example.com/foo/', r.external_url())
 
2040
 
 
2041
    def test_redirected_to_different_host_same_user(self):
 
2042
        t = self._transport('http://joe@www.example.com/foo')
 
2043
        r = t._redirected_to('http://www.example.com/foo',
 
2044
                             'https://foo.example.com/foo')
 
2045
        self.assertIsInstance(r, type(t))
 
2046
        self.assertEqual(t._parsed_url.user, r._parsed_url.user)
 
2047
        self.assertEquals('https://joe@foo.example.com/foo/', r.external_url())
 
2048
 
 
2049
 
 
2050
class PredefinedRequestHandler(http_server.TestingHTTPRequestHandler):
 
2051
    """Request handler for a unique and pre-defined request.
 
2052
 
 
2053
    The only thing we care about here is how many bytes travel on the wire. But
 
2054
    since we want to measure it for a real http client, we have to send it
 
2055
    correct responses.
 
2056
 
 
2057
    We expect to receive a *single* request nothing more (and we won't even
 
2058
    check what request it is, we just measure the bytes read until an empty
 
2059
    line.
 
2060
    """
 
2061
 
 
2062
    def _handle_one_request(self):
 
2063
        tcs = self.server.test_case_server
 
2064
        requestline = self.rfile.readline()
 
2065
        headers = self.MessageClass(self.rfile, 0)
 
2066
        # We just read: the request, the headers, an empty line indicating the
 
2067
        # end of the headers.
 
2068
        bytes_read = len(requestline)
 
2069
        for line in headers.headers:
 
2070
            bytes_read += len(line)
 
2071
        bytes_read += len('\r\n')
 
2072
        if requestline.startswith('POST'):
 
2073
            # The body should be a single line (or we don't know where it ends
 
2074
            # and we don't want to issue a blocking read)
 
2075
            body = self.rfile.readline()
 
2076
            bytes_read += len(body)
 
2077
        tcs.bytes_read = bytes_read
 
2078
 
 
2079
        # We set the bytes written *before* issuing the write, the client is
 
2080
        # supposed to consume every produced byte *before* checking that value.
 
2081
 
 
2082
        # Doing the oppposite may lead to test failure: we may be interrupted
 
2083
        # after the write but before updating the value. The client can then
 
2084
        # continue and read the value *before* we can update it. And yes,
 
2085
        # this has been observed -- vila 20090129
 
2086
        tcs.bytes_written = len(tcs.canned_response)
 
2087
        self.wfile.write(tcs.canned_response)
 
2088
 
 
2089
 
 
2090
class ActivityServerMixin(object):
 
2091
 
 
2092
    def __init__(self, protocol_version):
 
2093
        super(ActivityServerMixin, self).__init__(
 
2094
            request_handler=PredefinedRequestHandler,
 
2095
            protocol_version=protocol_version)
 
2096
        # Bytes read and written by the server
 
2097
        self.bytes_read = 0
 
2098
        self.bytes_written = 0
 
2099
        self.canned_response = None
 
2100
 
 
2101
 
 
2102
class ActivityHTTPServer(ActivityServerMixin, http_server.HttpServer):
 
2103
    pass
 
2104
 
 
2105
 
 
2106
if features.HTTPSServerFeature.available():
 
2107
    from bzrlib.tests import https_server
 
2108
    class ActivityHTTPSServer(ActivityServerMixin, https_server.HTTPSServer):
 
2109
        pass
 
2110
 
 
2111
 
 
2112
class TestActivityMixin(object):
 
2113
    """Test socket activity reporting.
 
2114
 
 
2115
    We use a special purpose server to control the bytes sent and received and
 
2116
    be able to predict the activity on the client socket.
 
2117
    """
 
2118
 
 
2119
    def setUp(self):
 
2120
        tests.TestCase.setUp(self)
 
2121
        self.server = self._activity_server(self._protocol_version)
 
2122
        self.server.start_server()
 
2123
        _activities = {} # Don't close over self and create a cycle
 
2124
        def report_activity(t, bytes, direction):
 
2125
            count = _activities.get(direction, 0)
 
2126
            count += bytes
 
2127
            _activities[direction] = count
 
2128
        self.activities = _activities
 
2129
 
 
2130
        # We override at class level because constructors may propagate the
 
2131
        # bound method and render instance overriding ineffective (an
 
2132
        # alternative would be to define a specific ui factory instead...)
 
2133
        self.overrideAttr(self._transport, '_report_activity', report_activity)
 
2134
        self.addCleanup(self.server.stop_server)
 
2135
 
 
2136
    def get_transport(self):
 
2137
        t = self._transport(self.server.get_url())
 
2138
        # FIXME: Needs cleanup -- vila 20100611
 
2139
        return t
 
2140
 
 
2141
    def assertActivitiesMatch(self):
 
2142
        self.assertEqual(self.server.bytes_read,
 
2143
                         self.activities.get('write', 0), 'written bytes')
 
2144
        self.assertEqual(self.server.bytes_written,
 
2145
                         self.activities.get('read', 0), 'read bytes')
 
2146
 
 
2147
    def test_get(self):
 
2148
        self.server.canned_response = '''HTTP/1.1 200 OK\r
 
2149
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
 
2150
Server: Apache/2.0.54 (Fedora)\r
 
2151
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
 
2152
ETag: "56691-23-38e9ae00"\r
 
2153
Accept-Ranges: bytes\r
 
2154
Content-Length: 35\r
 
2155
Connection: close\r
 
2156
Content-Type: text/plain; charset=UTF-8\r
 
2157
\r
 
2158
Bazaar-NG meta directory, format 1
 
2159
'''
 
2160
        t = self.get_transport()
 
2161
        self.assertEqual('Bazaar-NG meta directory, format 1\n',
 
2162
                         t.get('foo/bar').read())
 
2163
        self.assertActivitiesMatch()
 
2164
 
 
2165
    def test_has(self):
 
2166
        self.server.canned_response = '''HTTP/1.1 200 OK\r
 
2167
Server: SimpleHTTP/0.6 Python/2.5.2\r
 
2168
Date: Thu, 29 Jan 2009 20:21:47 GMT\r
 
2169
Content-type: application/octet-stream\r
 
2170
Content-Length: 20\r
 
2171
Last-Modified: Thu, 29 Jan 2009 20:21:47 GMT\r
 
2172
\r
 
2173
'''
 
2174
        t = self.get_transport()
 
2175
        self.assertTrue(t.has('foo/bar'))
 
2176
        self.assertActivitiesMatch()
 
2177
 
 
2178
    def test_readv(self):
 
2179
        self.server.canned_response = '''HTTP/1.1 206 Partial Content\r
 
2180
Date: Tue, 11 Jul 2006 04:49:48 GMT\r
 
2181
Server: Apache/2.0.54 (Fedora)\r
 
2182
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
 
2183
ETag: "238a3c-16ec2-805c5540"\r
 
2184
Accept-Ranges: bytes\r
 
2185
Content-Length: 1534\r
 
2186
Connection: close\r
 
2187
Content-Type: multipart/byteranges; boundary=418470f848b63279b\r
 
2188
\r
 
2189
\r
 
2190
--418470f848b63279b\r
 
2191
Content-type: text/plain; charset=UTF-8\r
 
2192
Content-range: bytes 0-254/93890\r
 
2193
\r
 
2194
mbp@sourcefrog.net-20050309040815-13242001617e4a06
 
2195
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e7627
 
2196
mbp@sourcefrog.net-20050309040957-6cad07f466bb0bb8
 
2197
mbp@sourcefrog.net-20050309041501-c840e09071de3b67
 
2198
mbp@sourcefrog.net-20050309044615-c24a3250be83220a
 
2199
\r
 
2200
--418470f848b63279b\r
 
2201
Content-type: text/plain; charset=UTF-8\r
 
2202
Content-range: bytes 1000-2049/93890\r
 
2203
\r
 
2204
40-fd4ec249b6b139ab
 
2205
mbp@sourcefrog.net-20050311063625-07858525021f270b
 
2206
mbp@sourcefrog.net-20050311231934-aa3776aff5200bb9
 
2207
mbp@sourcefrog.net-20050311231953-73aeb3a131c3699a
 
2208
mbp@sourcefrog.net-20050311232353-f5e33da490872c6a
 
2209
mbp@sourcefrog.net-20050312071639-0a8f59a34a024ff0
 
2210
mbp@sourcefrog.net-20050312073432-b2c16a55e0d6e9fb
 
2211
mbp@sourcefrog.net-20050312073831-a47c3335ece1920f
 
2212
mbp@sourcefrog.net-20050312085412-13373aa129ccbad3
 
2213
mbp@sourcefrog.net-20050313052251-2bf004cb96b39933
 
2214
mbp@sourcefrog.net-20050313052856-3edd84094687cb11
 
2215
mbp@sourcefrog.net-20050313053233-e30a4f28aef48f9d
 
2216
mbp@sourcefrog.net-20050313053853-7c64085594ff3072
 
2217
mbp@sourcefrog.net-20050313054757-a86c3f5871069e22
 
2218
mbp@sourcefrog.net-20050313061422-418f1f73b94879b9
 
2219
mbp@sourcefrog.net-20050313120651-497bd231b19df600
 
2220
mbp@sourcefrog.net-20050314024931-eae0170ef25a5d1a
 
2221
mbp@sourcefrog.net-20050314025438-d52099f915fe65fc
 
2222
mbp@sourcefrog.net-20050314025539-637a636692c055cf
 
2223
mbp@sourcefrog.net-20050314025737-55eb441f430ab4ba
 
2224
mbp@sourcefrog.net-20050314025901-d74aa93bb7ee8f62
 
2225
mbp@source\r
 
2226
--418470f848b63279b--\r
 
2227
'''
 
2228
        t = self.get_transport()
 
2229
        # Remember that the request is ignored and that the ranges below
 
2230
        # doesn't have to match the canned response.
 
2231
        l = list(t.readv('/foo/bar', ((0, 255), (1000, 1050))))
 
2232
        self.assertEqual(2, len(l))
 
2233
        self.assertActivitiesMatch()
 
2234
 
 
2235
    def test_post(self):
 
2236
        self.server.canned_response = '''HTTP/1.1 200 OK\r
 
2237
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
 
2238
Server: Apache/2.0.54 (Fedora)\r
 
2239
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
 
2240
ETag: "56691-23-38e9ae00"\r
 
2241
Accept-Ranges: bytes\r
 
2242
Content-Length: 35\r
 
2243
Connection: close\r
 
2244
Content-Type: text/plain; charset=UTF-8\r
 
2245
\r
 
2246
lalala whatever as long as itsssss
 
2247
'''
 
2248
        t = self.get_transport()
 
2249
        # We must send a single line of body bytes, see
 
2250
        # PredefinedRequestHandler._handle_one_request
 
2251
        code, f = t._post('abc def end-of-body\n')
 
2252
        self.assertEqual('lalala whatever as long as itsssss\n', f.read())
 
2253
        self.assertActivitiesMatch()
 
2254
 
 
2255
 
 
2256
class TestActivity(tests.TestCase, TestActivityMixin):
 
2257
 
 
2258
    scenarios = multiply_scenarios(
 
2259
        vary_by_http_activity(),
 
2260
        vary_by_http_protocol_version(),
 
2261
        )
 
2262
 
 
2263
    def setUp(self):
 
2264
        TestActivityMixin.setUp(self)
 
2265
 
 
2266
 
 
2267
class TestNoReportActivity(tests.TestCase, TestActivityMixin):
 
2268
 
 
2269
    # Unlike TestActivity, we are really testing ReportingFileSocket and
 
2270
    # ReportingSocket, so we don't need all the parametrization. Since
 
2271
    # ReportingFileSocket and ReportingSocket are wrappers, it's easier to
 
2272
    # test them through their use by the transport than directly (that's a
 
2273
    # bit less clean but far more simpler and effective).
 
2274
    _activity_server = ActivityHTTPServer
 
2275
    _protocol_version = 'HTTP/1.1'
 
2276
 
 
2277
    def setUp(self):
 
2278
        self._transport =_urllib.HttpTransport_urllib
 
2279
        TestActivityMixin.setUp(self)
 
2280
 
 
2281
    def assertActivitiesMatch(self):
 
2282
        # Nothing to check here
 
2283
        pass
 
2284
 
 
2285
 
 
2286
class TestAuthOnRedirected(http_utils.TestCaseWithRedirectedWebserver):
 
2287
    """Test authentication on the redirected http server."""
 
2288
 
 
2289
    scenarios = vary_by_http_protocol_version()
 
2290
 
 
2291
    _auth_header = 'Authorization'
 
2292
    _password_prompt_prefix = ''
 
2293
    _username_prompt_prefix = ''
 
2294
    _auth_server = http_utils.HTTPBasicAuthServer
 
2295
    _transport = _urllib.HttpTransport_urllib
 
2296
 
 
2297
    def setUp(self):
 
2298
        super(TestAuthOnRedirected, self).setUp()
 
2299
        self.build_tree_contents([('a','a'),
 
2300
                                  ('1/',),
 
2301
                                  ('1/a', 'redirected once'),
 
2302
                                  ],)
 
2303
        new_prefix = 'http://%s:%s' % (self.new_server.host,
 
2304
                                       self.new_server.port)
 
2305
        self.old_server.redirections = [
 
2306
            ('(.*)', r'%s/1\1' % (new_prefix), 301),]
 
2307
        self.old_transport = self.get_old_transport()
 
2308
        self.new_server.add_user('joe', 'foo')
 
2309
        cleanup_http_redirection_connections(self)
 
2310
 
 
2311
    def create_transport_readonly_server(self):
 
2312
        server = self._auth_server(protocol_version=self._protocol_version)
 
2313
        server._url_protocol = self._url_protocol
 
2314
        return server
 
2315
 
 
2316
    def get_a(self, t):
 
2317
        return t.get('a')
 
2318
 
 
2319
    def test_auth_on_redirected_via_do_catching_redirections(self):
 
2320
        self.redirections = 0
 
2321
 
 
2322
        def redirected(t, exception, redirection_notice):
 
2323
            self.redirections += 1
 
2324
            redirected_t = t._redirected_to(exception.source, exception.target)
 
2325
            self.addCleanup(redirected_t.disconnect)
 
2326
            return redirected_t
 
2327
 
 
2328
        stdout = tests.StringIOWrapper()
 
2329
        stderr = tests.StringIOWrapper()
 
2330
        ui.ui_factory = tests.TestUIFactory(stdin='joe\nfoo\n',
 
2331
                                            stdout=stdout, stderr=stderr)
 
2332
        self.assertEqual('redirected once',
 
2333
                         transport.do_catching_redirections(
 
2334
                self.get_a, self.old_transport, redirected).read())
 
2335
        self.assertEqual(1, self.redirections)
 
2336
        # stdin should be empty
 
2337
        self.assertEqual('', ui.ui_factory.stdin.readline())
 
2338
        # stdout should be empty, stderr will contains the prompts
 
2339
        self.assertEqual('', stdout.getvalue())
 
2340
 
 
2341
    def test_auth_on_redirected_via_following_redirections(self):
 
2342
        self.new_server.add_user('joe', 'foo')
 
2343
        stdout = tests.StringIOWrapper()
 
2344
        stderr = tests.StringIOWrapper()
 
2345
        ui.ui_factory = tests.TestUIFactory(stdin='joe\nfoo\n',
 
2346
                                            stdout=stdout, stderr=stderr)
 
2347
        t = self.old_transport
 
2348
        req = RedirectedRequest('GET', t.abspath('a'))
 
2349
        new_prefix = 'http://%s:%s' % (self.new_server.host,
 
2350
                                       self.new_server.port)
 
2351
        self.old_server.redirections = [
 
2352
            ('(.*)', r'%s/1\1' % (new_prefix), 301),]
 
2353
        self.assertEqual('redirected once', t._perform(req).read())
 
2354
        # stdin should be empty
 
2355
        self.assertEqual('', ui.ui_factory.stdin.readline())
 
2356
        # stdout should be empty, stderr will contains the prompts
 
2357
        self.assertEqual('', stdout.getvalue())
 
2358