/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_http.py

  • Committer: Robert Collins
  • Date: 2009-04-20 04:19:45 UTC
  • mto: This revision was merged to the branch mainline in revision 4304.
  • Revision ID: robertc@robertcollins.net-20090420041945-qvim67wg99c3euki
Move directory checking for bzr push options into Branch.create_clone_on_transport.

Show diffs side-by-side

added added

removed removed

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