/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-02-19 23:18:42 UTC
  • mto: (7490.3.4 work)
  • mto: This revision was merged to the branch mainline in revision 7495.
  • Revision ID: jelmer@jelmer.uk-20200219231842-agwjh2db66cpajqg
Consistent return values.

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