/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: Jelmer Vernooij
  • Date: 2020-05-24 00:39:50 UTC
  • mto: This revision was merged to the branch mainline in revision 7504.
  • Revision ID: jelmer@jelmer.uk-20200524003950-bbc545r76vc5yajg
Add github action.

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