~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_http.py

  • Committer: Robert Collins
  • Date: 2009-03-03 03:27:51 UTC
  • mto: (4070.2.5 integration)
  • mto: This revision was merged to the branch mainline in revision 4075.
  • Revision ID: robertc@robertcollins.net-20090303032751-ubyfhezgjul6y5ic
Get BzrDir.cloning_metadir working.

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