/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to breezy/tests/test_http.py

  • Committer: Jelmer Vernooij
  • Date: 2020-03-22 01:35:14 UTC
  • mfrom: (7490.7.6 work)
  • mto: This revision was merged to the branch mainline in revision 7499.
  • Revision ID: jelmer@jelmer.uk-20200322013514-7vw1ntwho04rcuj3
merge lp:brz/3.1.

Show diffs side-by-side

added added

removed removed

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