/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/http_utils.py

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2019-02-14 03:30:18 UTC
  • mfrom: (6745.1.3 test-file-ids)
  • Revision ID: breezy.the.bot@gmail.com-20190214033018-4mhv416kiuozgned
Fix a commonly typoed word: compatibility.

Merged from https://code.launchpad.net/~jelmer/brz/compatibility-typos/+merge/363008

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2011 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
import base64
 
18
import re
 
19
try:
 
20
    from urllib.request import (
 
21
        parse_http_list,
 
22
        parse_keqv_list,
 
23
        )
 
24
except ImportError:  # python < 3
 
25
    from urllib2 import (
 
26
        parse_http_list,
 
27
        parse_keqv_list,
 
28
        )
 
29
 
 
30
 
 
31
from .. import (
 
32
    errors,
 
33
    osutils,
 
34
    tests,
 
35
    transport,
 
36
    )
 
37
from ..sixish import (
 
38
    BytesIO,
 
39
    )
 
40
from ..bzr.smart import (
 
41
    medium,
 
42
    )
 
43
from . import http_server
 
44
from ..transport import chroot
 
45
 
 
46
 
 
47
class HTTPServerWithSmarts(http_server.HttpServer):
 
48
    """HTTPServerWithSmarts extends the HttpServer with POST methods that will
 
49
    trigger a smart server to execute with a transport rooted at the rootdir of
 
50
    the HTTP server.
 
51
    """
 
52
 
 
53
    def __init__(self, protocol_version=None):
 
54
        http_server.HttpServer.__init__(self, SmartRequestHandler,
 
55
                                        protocol_version=protocol_version)
 
56
 
 
57
 
 
58
class SmartRequestHandler(http_server.TestingHTTPRequestHandler):
 
59
    """Extend TestingHTTPRequestHandler to support smart client POSTs.
 
60
 
 
61
    XXX: This duplicates a fair bit of the logic in breezy.transport.http.wsgi.
 
62
    """
 
63
 
 
64
    def do_POST(self):
 
65
        """Hand the request off to a smart server instance."""
 
66
        backing = transport.get_transport_from_path(
 
67
            self.server.test_case_server._home_dir)
 
68
        chroot_server = chroot.ChrootServer(backing)
 
69
        chroot_server.start_server()
 
70
        try:
 
71
            t = transport.get_transport_from_url(chroot_server.get_url())
 
72
            self.do_POST_inner(t)
 
73
        finally:
 
74
            chroot_server.stop_server()
 
75
 
 
76
    def do_POST_inner(self, chrooted_transport):
 
77
        self.send_response(200)
 
78
        self.send_header("Content-type", "application/octet-stream")
 
79
        if not self.path.endswith('.bzr/smart'):
 
80
            raise AssertionError(
 
81
                'POST to path not ending in .bzr/smart: %r' % (self.path,))
 
82
        t = chrooted_transport.clone(self.path[:-len('.bzr/smart')])
 
83
        # if this fails, we should return 400 bad request, but failure is
 
84
        # failure for now - RBC 20060919
 
85
        data_length = int(self.headers['Content-Length'])
 
86
        # TODO: We might like to support streaming responses.  1.0 allows no
 
87
        # Content-length in this case, so for integrity we should perform our
 
88
        # own chunking within the stream.
 
89
        # 1.1 allows chunked responses, and in this case we could chunk using
 
90
        # the HTTP chunking as this will allow HTTP persistence safely, even if
 
91
        # we have to stop early due to error, but we would also have to use the
 
92
        # HTTP trailer facility which may not be widely available.
 
93
        request_bytes = self.rfile.read(data_length)
 
94
        protocol_factory, unused_bytes = (
 
95
            medium._get_protocol_factory_for_bytes(request_bytes))
 
96
        out_buffer = BytesIO()
 
97
        smart_protocol_request = protocol_factory(t, out_buffer.write, '/')
 
98
        # Perhaps there should be a SmartServerHTTPMedium that takes care of
 
99
        # feeding the bytes in the http request to the smart_protocol_request,
 
100
        # but for now it's simpler to just feed the bytes directly.
 
101
        smart_protocol_request.accept_bytes(unused_bytes)
 
102
        if not (smart_protocol_request.next_read_size() == 0):
 
103
            raise errors.SmartProtocolError(
 
104
                "not finished reading, but all data sent to protocol.")
 
105
        self.send_header("Content-Length", str(len(out_buffer.getvalue())))
 
106
        self.end_headers()
 
107
        self.wfile.write(out_buffer.getvalue())
 
108
 
 
109
 
 
110
class TestCaseWithWebserver(tests.TestCaseWithTransport):
 
111
    """A support class that provides readonly urls that are http://.
 
112
 
 
113
    This is done by forcing the readonly server to be an http
 
114
    one. This will currently fail if the primary transport is not
 
115
    backed by regular disk files.
 
116
    """
 
117
 
 
118
    # These attributes can be overriden or parametrized by daughter clasess if
 
119
    # needed, but must exist so that the create_transport_readonly_server()
 
120
    # method (or any method creating an http(s) server) can propagate it.
 
121
    _protocol_version = None
 
122
    _url_protocol = 'http'
 
123
 
 
124
    def setUp(self):
 
125
        super(TestCaseWithWebserver, self).setUp()
 
126
        self.transport_readonly_server = http_server.HttpServer
 
127
 
 
128
    def create_transport_readonly_server(self):
 
129
        server = self.transport_readonly_server(
 
130
            protocol_version=self._protocol_version)
 
131
        server._url_protocol = self._url_protocol
 
132
        return server
 
133
 
 
134
 
 
135
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
 
136
    """A support class providing readonly urls on two servers that are http://.
 
137
 
 
138
    We set up two webservers to allows various tests involving
 
139
    proxies or redirections from one server to the other.
 
140
    """
 
141
 
 
142
    def setUp(self):
 
143
        super(TestCaseWithTwoWebservers, self).setUp()
 
144
        self.transport_secondary_server = http_server.HttpServer
 
145
        self.__secondary_server = None
 
146
 
 
147
    def create_transport_secondary_server(self):
 
148
        """Create a transport server from class defined at init.
 
149
 
 
150
        This is mostly a hook for daughter classes.
 
151
        """
 
152
        server = self.transport_secondary_server(
 
153
            protocol_version=self._protocol_version)
 
154
        server._url_protocol = self._url_protocol
 
155
        return server
 
156
 
 
157
    def get_secondary_server(self):
 
158
        """Get the server instance for the secondary transport."""
 
159
        if self.__secondary_server is None:
 
160
            self.__secondary_server = self.create_transport_secondary_server()
 
161
            self.start_server(self.__secondary_server)
 
162
        return self.__secondary_server
 
163
 
 
164
    def get_secondary_url(self, relpath=None):
 
165
        base = self.get_secondary_server().get_url()
 
166
        return self._adjust_url(base, relpath)
 
167
 
 
168
    def get_secondary_transport(self, relpath=None):
 
169
        t = transport.get_transport_from_url(self.get_secondary_url(relpath))
 
170
        self.assertTrue(t.is_readonly())
 
171
        return t
 
172
 
 
173
 
 
174
class ProxyServer(http_server.HttpServer):
 
175
    """A proxy test server for http transports."""
 
176
 
 
177
    proxy_requests = True
 
178
 
 
179
 
 
180
class RedirectRequestHandler(http_server.TestingHTTPRequestHandler):
 
181
    """Redirect all request to the specified server"""
 
182
 
 
183
    def parse_request(self):
 
184
        """Redirect a single HTTP request to another host"""
 
185
        valid = http_server.TestingHTTPRequestHandler.parse_request(self)
 
186
        if valid:
 
187
            tcs = self.server.test_case_server
 
188
            code, target = tcs.is_redirected(self.path)
 
189
            if code is not None and target is not None:
 
190
                # Redirect as instructed
 
191
                self.send_response(code)
 
192
                self.send_header('Location', target)
 
193
                # We do not send a body
 
194
                self.send_header('Content-Length', '0')
 
195
                self.end_headers()
 
196
                return False  # The job is done
 
197
            else:
 
198
                # We leave the parent class serve the request
 
199
                pass
 
200
        return valid
 
201
 
 
202
 
 
203
class HTTPServerRedirecting(http_server.HttpServer):
 
204
    """An HttpServer redirecting to another server """
 
205
 
 
206
    def __init__(self, request_handler=RedirectRequestHandler,
 
207
                 protocol_version=None):
 
208
        http_server.HttpServer.__init__(self, request_handler,
 
209
                                        protocol_version=protocol_version)
 
210
        # redirections is a list of tuples (source, target, code)
 
211
        # - source is a regexp for the paths requested
 
212
        # - target is a replacement for re.sub describing where
 
213
        #   the request will be redirected
 
214
        # - code is the http error code associated to the
 
215
        #   redirection (301 permanent, 302 temporarry, etc
 
216
        self.redirections = []
 
217
 
 
218
    def redirect_to(self, host, port):
 
219
        """Redirect all requests to a specific host:port"""
 
220
        self.redirections = [('(.*)',
 
221
                              r'http://%s:%s\1' % (host, port),
 
222
                              301)]
 
223
 
 
224
    def is_redirected(self, path):
 
225
        """Is the path redirected by this server.
 
226
 
 
227
        :param path: the requested relative path
 
228
 
 
229
        :returns: a tuple (code, target) if a matching
 
230
             redirection is found, (None, None) otherwise.
 
231
        """
 
232
        code = None
 
233
        target = None
 
234
        for (rsource, rtarget, rcode) in self.redirections:
 
235
            target, match = re.subn(rsource, rtarget, path, count=1)
 
236
            if match:
 
237
                code = rcode
 
238
                break  # The first match wins
 
239
            else:
 
240
                target = None
 
241
        return code, target
 
242
 
 
243
 
 
244
class TestCaseWithRedirectedWebserver(TestCaseWithTwoWebservers):
 
245
    """A support class providing redirections from one server to another.
 
246
 
 
247
    We set up two webservers to allows various tests involving
 
248
    redirections.
 
249
    The 'old' server is redirected to the 'new' server.
 
250
    """
 
251
 
 
252
    def setUp(self):
 
253
        super(TestCaseWithRedirectedWebserver, self).setUp()
 
254
        # The redirections will point to the new server
 
255
        self.new_server = self.get_readonly_server()
 
256
        # The requests to the old server will be redirected to the new server
 
257
        self.old_server = self.get_secondary_server()
 
258
 
 
259
    def create_transport_secondary_server(self):
 
260
        """Create the secondary server redirecting to the primary server"""
 
261
        new = self.get_readonly_server()
 
262
        redirecting = HTTPServerRedirecting(
 
263
            protocol_version=self._protocol_version)
 
264
        redirecting.redirect_to(new.host, new.port)
 
265
        redirecting._url_protocol = self._url_protocol
 
266
        return redirecting
 
267
 
 
268
    def get_old_url(self, relpath=None):
 
269
        base = self.old_server.get_url()
 
270
        return self._adjust_url(base, relpath)
 
271
 
 
272
    def get_old_transport(self, relpath=None):
 
273
        t = transport.get_transport_from_url(self.get_old_url(relpath))
 
274
        self.assertTrue(t.is_readonly())
 
275
        return t
 
276
 
 
277
    def get_new_url(self, relpath=None):
 
278
        base = self.new_server.get_url()
 
279
        return self._adjust_url(base, relpath)
 
280
 
 
281
    def get_new_transport(self, relpath=None):
 
282
        t = transport.get_transport_from_url(self.get_new_url(relpath))
 
283
        self.assertTrue(t.is_readonly())
 
284
        return t
 
285
 
 
286
 
 
287
class AuthRequestHandler(http_server.TestingHTTPRequestHandler):
 
288
    """Requires an authentication to process requests.
 
289
 
 
290
    This is intended to be used with a server that always and
 
291
    only use one authentication scheme (implemented by daughter
 
292
    classes).
 
293
    """
 
294
 
 
295
    # The following attributes should be defined in the server
 
296
    # - auth_header_sent: the header name sent to require auth
 
297
    # - auth_header_recv: the header received containing auth
 
298
    # - auth_error_code: the error code to indicate auth required
 
299
 
 
300
    def _require_authentication(self):
 
301
        # Note that we must update test_case_server *before*
 
302
        # sending the error or the client may try to read it
 
303
        # before we have sent the whole error back.
 
304
        tcs = self.server.test_case_server
 
305
        tcs.auth_required_errors += 1
 
306
        self.send_response(tcs.auth_error_code)
 
307
        self.send_header_auth_reqed()
 
308
        # We do not send a body
 
309
        self.send_header('Content-Length', '0')
 
310
        self.end_headers()
 
311
        return
 
312
 
 
313
    def do_GET(self):
 
314
        if self.authorized():
 
315
            return http_server.TestingHTTPRequestHandler.do_GET(self)
 
316
        else:
 
317
            return self._require_authentication()
 
318
 
 
319
    def do_HEAD(self):
 
320
        if self.authorized():
 
321
            return http_server.TestingHTTPRequestHandler.do_HEAD(self)
 
322
        else:
 
323
            return self._require_authentication()
 
324
 
 
325
 
 
326
class BasicAuthRequestHandler(AuthRequestHandler):
 
327
    """Implements the basic authentication of a request"""
 
328
 
 
329
    def authorized(self):
 
330
        tcs = self.server.test_case_server
 
331
        if tcs.auth_scheme != 'basic':
 
332
            return False
 
333
 
 
334
        auth_header = self.headers.get(tcs.auth_header_recv, None)
 
335
        if auth_header:
 
336
            scheme, raw_auth = auth_header.split(' ', 1)
 
337
            if scheme.lower() == tcs.auth_scheme:
 
338
                user, password = base64.b64decode(raw_auth).split(b':')
 
339
                return tcs.authorized(user.decode('ascii'),
 
340
                                      password.decode('ascii'))
 
341
 
 
342
        return False
 
343
 
 
344
    def send_header_auth_reqed(self):
 
345
        tcs = self.server.test_case_server
 
346
        self.send_header(tcs.auth_header_sent,
 
347
                         'Basic realm="%s"' % tcs.auth_realm)
 
348
 
 
349
 
 
350
# FIXME: We could send an Authentication-Info header too when
 
351
# the authentication is succesful
 
352
 
 
353
class DigestAuthRequestHandler(AuthRequestHandler):
 
354
    """Implements the digest authentication of a request.
 
355
 
 
356
    We need persistence for some attributes and that can't be
 
357
    achieved here since we get instantiated for each request. We
 
358
    rely on the DigestAuthServer to take care of them.
 
359
    """
 
360
 
 
361
    def authorized(self):
 
362
        tcs = self.server.test_case_server
 
363
 
 
364
        auth_header = self.headers.get(tcs.auth_header_recv, None)
 
365
        if auth_header is None:
 
366
            return False
 
367
        scheme, auth = auth_header.split(None, 1)
 
368
        if scheme.lower() == tcs.auth_scheme:
 
369
            auth_dict = parse_keqv_list(parse_http_list(auth))
 
370
 
 
371
            return tcs.digest_authorized(auth_dict, self.command)
 
372
 
 
373
        return False
 
374
 
 
375
    def send_header_auth_reqed(self):
 
376
        tcs = self.server.test_case_server
 
377
        header = 'Digest realm="%s", ' % tcs.auth_realm
 
378
        header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
 
379
                                                              'MD5')
 
380
        self.send_header(tcs.auth_header_sent, header)
 
381
 
 
382
 
 
383
class DigestAndBasicAuthRequestHandler(DigestAuthRequestHandler):
 
384
    """Implements a digest and basic authentication of a request.
 
385
 
 
386
    I.e. the server proposes both schemes and the client should choose the best
 
387
    one it can handle, which, in that case, should be digest, the only scheme
 
388
    accepted here.
 
389
    """
 
390
 
 
391
    def send_header_auth_reqed(self):
 
392
        tcs = self.server.test_case_server
 
393
        self.send_header(tcs.auth_header_sent,
 
394
                         'Basic realm="%s"' % tcs.auth_realm)
 
395
        header = 'Digest realm="%s", ' % tcs.auth_realm
 
396
        header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
 
397
                                                              'MD5')
 
398
        self.send_header(tcs.auth_header_sent, header)
 
399
 
 
400
 
 
401
class AuthServer(http_server.HttpServer):
 
402
    """Extends HttpServer with a dictionary of passwords.
 
403
 
 
404
    This is used as a base class for various schemes which should
 
405
    all use or redefined the associated AuthRequestHandler.
 
406
 
 
407
    Note that no users are defined by default, so add_user should
 
408
    be called before issuing the first request.
 
409
    """
 
410
 
 
411
    # The following attributes should be set dy daughter classes
 
412
    # and are used by AuthRequestHandler.
 
413
    auth_header_sent = None
 
414
    auth_header_recv = None
 
415
    auth_error_code = None
 
416
    auth_realm = u"Thou should not pass"
 
417
 
 
418
    def __init__(self, request_handler, auth_scheme,
 
419
                 protocol_version=None):
 
420
        http_server.HttpServer.__init__(self, request_handler,
 
421
                                        protocol_version=protocol_version)
 
422
        self.auth_scheme = auth_scheme
 
423
        self.password_of = {}
 
424
        self.auth_required_errors = 0
 
425
 
 
426
    def add_user(self, user, password):
 
427
        """Declare a user with an associated password.
 
428
 
 
429
        password can be empty, use an empty string ('') in that
 
430
        case, not None.
 
431
        """
 
432
        self.password_of[user] = password
 
433
 
 
434
    def authorized(self, user, password):
 
435
        """Check that the given user provided the right password"""
 
436
        expected_password = self.password_of.get(user, None)
 
437
        return expected_password is not None and password == expected_password
 
438
 
 
439
 
 
440
# FIXME: There is some code duplication with
 
441
# _urllib2_wrappers.py.DigestAuthHandler. If that duplication
 
442
# grows, it may require a refactoring. Also, we don't implement
 
443
# SHA algorithm nor MD5-sess here, but that does not seem worth
 
444
# it.
 
445
class DigestAuthServer(AuthServer):
 
446
    """A digest authentication server"""
 
447
 
 
448
    auth_nonce = 'now!'
 
449
 
 
450
    def __init__(self, request_handler, auth_scheme,
 
451
                 protocol_version=None):
 
452
        AuthServer.__init__(self, request_handler, auth_scheme,
 
453
                            protocol_version=protocol_version)
 
454
 
 
455
    def digest_authorized(self, auth, command):
 
456
        nonce = auth['nonce']
 
457
        if nonce != self.auth_nonce:
 
458
            return False
 
459
        realm = auth['realm']
 
460
        if realm != self.auth_realm:
 
461
            return False
 
462
        user = auth['username']
 
463
        if user not in self.password_of:
 
464
            return False
 
465
        algorithm = auth['algorithm']
 
466
        if algorithm != 'MD5':
 
467
            return False
 
468
        qop = auth['qop']
 
469
        if qop != 'auth':
 
470
            return False
 
471
 
 
472
        password = self.password_of[user]
 
473
 
 
474
        # Recalculate the response_digest to compare with the one
 
475
        # sent by the client
 
476
        A1 = ('%s:%s:%s' % (user, realm, password)).encode('utf-8')
 
477
        A2 = ('%s:%s' % (command, auth['uri'])).encode('utf-8')
 
478
 
 
479
        def H(x):
 
480
            return osutils.md5(x).hexdigest()
 
481
 
 
482
        def KD(secret, data):
 
483
            return H(("%s:%s" % (secret, data)).encode('utf-8'))
 
484
 
 
485
        nonce_count = int(auth['nc'], 16)
 
486
 
 
487
        ncvalue = '%08x' % nonce_count
 
488
 
 
489
        cnonce = auth['cnonce']
 
490
        noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
 
491
        response_digest = KD(H(A1), noncebit)
 
492
 
 
493
        return response_digest == auth['response']
 
494
 
 
495
 
 
496
class HTTPAuthServer(AuthServer):
 
497
    """An HTTP server requiring authentication"""
 
498
 
 
499
    def init_http_auth(self):
 
500
        self.auth_header_sent = 'WWW-Authenticate'
 
501
        self.auth_header_recv = 'Authorization'
 
502
        self.auth_error_code = 401
 
503
 
 
504
 
 
505
class ProxyAuthServer(AuthServer):
 
506
    """A proxy server requiring authentication"""
 
507
 
 
508
    def init_proxy_auth(self):
 
509
        self.proxy_requests = True
 
510
        self.auth_header_sent = 'Proxy-Authenticate'
 
511
        self.auth_header_recv = 'Proxy-Authorization'
 
512
        self.auth_error_code = 407
 
513
 
 
514
 
 
515
class HTTPBasicAuthServer(HTTPAuthServer):
 
516
    """An HTTP server requiring basic authentication"""
 
517
 
 
518
    def __init__(self, protocol_version=None):
 
519
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
 
520
                                protocol_version=protocol_version)
 
521
        self.init_http_auth()
 
522
 
 
523
 
 
524
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
 
525
    """An HTTP server requiring digest authentication"""
 
526
 
 
527
    def __init__(self, protocol_version=None):
 
528
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
 
529
                                  protocol_version=protocol_version)
 
530
        self.init_http_auth()
 
531
 
 
532
 
 
533
class HTTPBasicAndDigestAuthServer(DigestAuthServer, HTTPAuthServer):
 
534
    """An HTTP server requiring basic or digest authentication"""
 
535
 
 
536
    def __init__(self, protocol_version=None):
 
537
        DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
 
538
                                  'basicdigest',
 
539
                                  protocol_version=protocol_version)
 
540
        self.init_http_auth()
 
541
        # We really accept Digest only
 
542
        self.auth_scheme = 'digest'
 
543
 
 
544
 
 
545
class ProxyBasicAuthServer(ProxyAuthServer):
 
546
    """A proxy server requiring basic authentication"""
 
547
 
 
548
    def __init__(self, protocol_version=None):
 
549
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
 
550
                                 protocol_version=protocol_version)
 
551
        self.init_proxy_auth()
 
552
 
 
553
 
 
554
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
 
555
    """A proxy server requiring basic authentication"""
 
556
 
 
557
    def __init__(self, protocol_version=None):
 
558
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
 
559
                                 protocol_version=protocol_version)
 
560
        self.init_proxy_auth()
 
561
 
 
562
 
 
563
class ProxyBasicAndDigestAuthServer(DigestAuthServer, ProxyAuthServer):
 
564
    """An proxy server requiring basic or digest authentication"""
 
565
 
 
566
    def __init__(self, protocol_version=None):
 
567
        DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
 
568
                                  'basicdigest',
 
569
                                  protocol_version=protocol_version)
 
570
        self.init_proxy_auth()
 
571
        # We really accept Digest only
 
572
        self.auth_scheme = 'digest'