~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/http_utils.py

Merge up bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
from cStringIO import StringIO
 
18
import errno
 
19
import md5
18
20
import re
 
21
import sha
 
22
import socket
 
23
import threading
 
24
import time
19
25
import urllib2
20
 
 
 
26
import urlparse
21
27
 
22
28
from bzrlib import (
23
29
    errors,
24
 
    osutils,
25
30
    tests,
26
31
    transport,
27
32
    )
28
 
from bzrlib.smart import (
29
 
    medium,
30
 
    )
 
33
from bzrlib.smart import protocol
31
34
from bzrlib.tests import http_server
32
 
from bzrlib.transport import chroot
33
35
 
34
36
 
35
37
class HTTPServerWithSmarts(http_server.HttpServer):
44
46
 
45
47
 
46
48
class SmartRequestHandler(http_server.TestingHTTPRequestHandler):
47
 
    """Extend TestingHTTPRequestHandler to support smart client POSTs.
48
 
 
49
 
    XXX: This duplicates a fair bit of the logic in bzrlib.transport.http.wsgi.
50
 
    """
 
49
    """Extend TestingHTTPRequestHandler to support smart client POSTs."""
51
50
 
52
51
    def do_POST(self):
53
52
        """Hand the request off to a smart server instance."""
54
 
        backing = transport.get_transport_from_path(
55
 
            self.server.test_case_server._home_dir)
56
 
        chroot_server = chroot.ChrootServer(backing)
57
 
        chroot_server.start_server()
58
 
        try:
59
 
            t = transport.get_transport_from_url(chroot_server.get_url())
60
 
            self.do_POST_inner(t)
61
 
        finally:
62
 
            chroot_server.stop_server()
63
 
 
64
 
    def do_POST_inner(self, chrooted_transport):
65
53
        self.send_response(200)
66
54
        self.send_header("Content-type", "application/octet-stream")
67
 
        if not self.path.endswith('.bzr/smart'):
68
 
            raise AssertionError(
69
 
                'POST to path not ending in .bzr/smart: %r' % (self.path,))
70
 
        t = chrooted_transport.clone(self.path[:-len('.bzr/smart')])
71
 
        # if this fails, we should return 400 bad request, but failure is
72
 
        # failure for now - RBC 20060919
73
 
        data_length = int(self.headers['Content-Length'])
 
55
        t = transport.get_transport(self.server.test_case_server._home_dir)
74
56
        # TODO: We might like to support streaming responses.  1.0 allows no
75
57
        # Content-length in this case, so for integrity we should perform our
76
58
        # own chunking within the stream.
78
60
        # the HTTP chunking as this will allow HTTP persistence safely, even if
79
61
        # we have to stop early due to error, but we would also have to use the
80
62
        # HTTP trailer facility which may not be widely available.
81
 
        request_bytes = self.rfile.read(data_length)
82
 
        protocol_factory, unused_bytes = medium._get_protocol_factory_for_bytes(
83
 
            request_bytes)
84
63
        out_buffer = StringIO()
85
 
        smart_protocol_request = protocol_factory(t, out_buffer.write, '/')
 
64
        smart_protocol_request = protocol.SmartServerRequestProtocolOne(
 
65
                t, out_buffer.write)
 
66
        # if this fails, we should return 400 bad request, but failure is
 
67
        # failure for now - RBC 20060919
 
68
        data_length = int(self.headers['Content-Length'])
86
69
        # Perhaps there should be a SmartServerHTTPMedium that takes care of
87
70
        # feeding the bytes in the http request to the smart_protocol_request,
88
71
        # but for now it's simpler to just feed the bytes directly.
89
 
        smart_protocol_request.accept_bytes(unused_bytes)
 
72
        smart_protocol_request.accept_bytes(self.rfile.read(data_length))
90
73
        if not (smart_protocol_request.next_read_size() == 0):
91
74
            raise errors.SmartProtocolError(
92
75
                "not finished reading, but all data sent to protocol.")
102
85
    one. This will currently fail if the primary transport is not
103
86
    backed by regular disk files.
104
87
    """
105
 
 
106
 
    # These attributes can be overriden or parametrized by daughter clasess if
107
 
    # needed, but must exist so that the create_transport_readonly_server()
108
 
    # method (or any method creating an http(s) server) can propagate it.
109
 
    _protocol_version = None
110
 
    _url_protocol = 'http'
111
 
 
112
88
    def setUp(self):
113
89
        super(TestCaseWithWebserver, self).setUp()
114
90
        self.transport_readonly_server = http_server.HttpServer
115
91
 
116
 
    def create_transport_readonly_server(self):
117
 
        server = self.transport_readonly_server(
118
 
            protocol_version=self._protocol_version)
119
 
        server._url_protocol = self._url_protocol
120
 
        return server
121
 
 
122
92
 
123
93
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
124
94
    """A support class providing readonly urls on two servers that are http://.
136
106
 
137
107
        This is mostly a hook for daughter classes.
138
108
        """
139
 
        server = self.transport_secondary_server(
140
 
            protocol_version=self._protocol_version)
141
 
        server._url_protocol = self._url_protocol
142
 
        return server
 
109
        return self.transport_secondary_server()
143
110
 
144
111
    def get_secondary_server(self):
145
112
        """Get the server instance for the secondary transport."""
146
113
        if self.__secondary_server is None:
147
114
            self.__secondary_server = self.create_transport_secondary_server()
148
 
            self.start_server(self.__secondary_server)
 
115
            self.__secondary_server.setUp()
 
116
            self.addCleanup(self.__secondary_server.tearDown)
149
117
        return self.__secondary_server
150
118
 
151
 
    def get_secondary_url(self, relpath=None):
152
 
        base = self.get_secondary_server().get_url()
153
 
        return self._adjust_url(base, relpath)
154
 
 
155
 
    def get_secondary_transport(self, relpath=None):
156
 
        t = transport.get_transport_from_url(self.get_secondary_url(relpath))
157
 
        self.assertTrue(t.is_readonly())
158
 
        return t
159
 
 
160
119
 
161
120
class ProxyServer(http_server.HttpServer):
162
121
    """A proxy test server for http transports."""
236
195
   The 'old' server is redirected to the 'new' server.
237
196
   """
238
197
 
 
198
   def create_transport_secondary_server(self):
 
199
       """Create the secondary server redirecting to the primary server"""
 
200
       new = self.get_readonly_server()
 
201
       redirecting = HTTPServerRedirecting()
 
202
       redirecting.redirect_to(new.host, new.port)
 
203
       return redirecting
 
204
 
239
205
   def setUp(self):
240
206
       super(TestCaseWithRedirectedWebserver, self).setUp()
241
207
       # The redirections will point to the new server
242
208
       self.new_server = self.get_readonly_server()
243
 
       # The requests to the old server will be redirected to the new server
 
209
       # The requests to the old server will be redirected
244
210
       self.old_server = self.get_secondary_server()
245
211
 
246
 
   def create_transport_secondary_server(self):
247
 
       """Create the secondary server redirecting to the primary server"""
248
 
       new = self.get_readonly_server()
249
 
       redirecting = HTTPServerRedirecting(
250
 
           protocol_version=self._protocol_version)
251
 
       redirecting.redirect_to(new.host, new.port)
252
 
       redirecting._url_protocol = self._url_protocol
253
 
       return redirecting
254
 
 
255
 
   def get_old_url(self, relpath=None):
256
 
        base = self.old_server.get_url()
257
 
        return self._adjust_url(base, relpath)
258
 
 
259
 
   def get_old_transport(self, relpath=None):
260
 
        t = transport.get_transport_from_url(self.get_old_url(relpath))
261
 
        self.assertTrue(t.is_readonly())
262
 
        return t
263
 
 
264
 
   def get_new_url(self, relpath=None):
265
 
        base = self.new_server.get_url()
266
 
        return self._adjust_url(base, relpath)
267
 
 
268
 
   def get_new_transport(self, relpath=None):
269
 
        t = transport.get_transport_from_url(self.get_new_url(relpath))
270
 
        self.assertTrue(t.is_readonly())
271
 
        return t
272
 
 
273
212
 
274
213
class AuthRequestHandler(http_server.TestingHTTPRequestHandler):
275
214
    """Requires an authentication to process requests.
284
223
    # - auth_header_recv: the header received containing auth
285
224
    # - auth_error_code: the error code to indicate auth required
286
225
 
287
 
    def _require_authentication(self):
288
 
        # Note that we must update test_case_server *before*
289
 
        # sending the error or the client may try to read it
290
 
        # before we have sent the whole error back.
291
 
        tcs = self.server.test_case_server
292
 
        tcs.auth_required_errors += 1
293
 
        self.send_response(tcs.auth_error_code)
294
 
        self.send_header_auth_reqed()
295
 
        # We do not send a body
296
 
        self.send_header('Content-Length', '0')
297
 
        self.end_headers()
298
 
        return
299
 
 
300
226
    def do_GET(self):
301
227
        if self.authorized():
302
228
            return http_server.TestingHTTPRequestHandler.do_GET(self)
303
229
        else:
304
 
            return self._require_authentication()
305
 
 
306
 
    def do_HEAD(self):
307
 
        if self.authorized():
308
 
            return http_server.TestingHTTPRequestHandler.do_HEAD(self)
309
 
        else:
310
 
            return self._require_authentication()
 
230
            # Note that we must update test_case_server *before*
 
231
            # sending the error or the client may try to read it
 
232
            # before we have sent the whole error back.
 
233
            tcs = self.server.test_case_server
 
234
            tcs.auth_required_errors += 1
 
235
            self.send_response(tcs.auth_error_code)
 
236
            self.send_header_auth_reqed()
 
237
            # We do not send a body
 
238
            self.send_header('Content-Length', '0')
 
239
            self.end_headers()
 
240
            return
311
241
 
312
242
 
313
243
class BasicAuthRequestHandler(AuthRequestHandler):
346
276
 
347
277
    def authorized(self):
348
278
        tcs = self.server.test_case_server
 
279
        if tcs.auth_scheme != 'digest':
 
280
            return False
349
281
 
350
282
        auth_header = self.headers.get(tcs.auth_header_recv, None)
351
283
        if auth_header is None:
366
298
        self.send_header(tcs.auth_header_sent,header)
367
299
 
368
300
 
369
 
class DigestAndBasicAuthRequestHandler(DigestAuthRequestHandler):
370
 
    """Implements a digest and basic authentication of a request.
371
 
 
372
 
    I.e. the server proposes both schemes and the client should choose the best
373
 
    one it can handle, which, in that case, should be digest, the only scheme
374
 
    accepted here.
375
 
    """
376
 
 
377
 
    def send_header_auth_reqed(self):
378
 
        tcs = self.server.test_case_server
379
 
        self.send_header(tcs.auth_header_sent,
380
 
                         'Basic realm="%s"' % tcs.auth_realm)
381
 
        header = 'Digest realm="%s", ' % tcs.auth_realm
382
 
        header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
383
 
                                                              'MD5')
384
 
        self.send_header(tcs.auth_header_sent,header)
385
 
 
386
 
 
387
301
class AuthServer(http_server.HttpServer):
388
302
    """Extends HttpServer with a dictionary of passwords.
389
303
 
462
376
        A1 = '%s:%s:%s' % (user, realm, password)
463
377
        A2 = '%s:%s' % (command, auth['uri'])
464
378
 
465
 
        H = lambda x: osutils.md5(x).hexdigest()
 
379
        H = lambda x: md5.new(x).hexdigest()
466
380
        KD = lambda secret, data: H("%s:%s" % (secret, data))
467
381
 
468
382
        nonce_count = int(auth['nc'], 16)
475
389
 
476
390
        return response_digest == auth['response']
477
391
 
478
 
 
479
392
class HTTPAuthServer(AuthServer):
480
393
    """An HTTP server requiring authentication"""
481
394
 
513
426
        self.init_http_auth()
514
427
 
515
428
 
516
 
class HTTPBasicAndDigestAuthServer(DigestAuthServer, HTTPAuthServer):
517
 
    """An HTTP server requiring basic or digest authentication"""
518
 
 
519
 
    def __init__(self, protocol_version=None):
520
 
        DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
521
 
                                  'basicdigest',
522
 
                                  protocol_version=protocol_version)
523
 
        self.init_http_auth()
524
 
        # We really accept Digest only
525
 
        self.auth_scheme = 'digest'
526
 
 
527
 
 
528
429
class ProxyBasicAuthServer(ProxyAuthServer):
529
430
    """A proxy server requiring basic authentication"""
530
431
 
543
444
        self.init_proxy_auth()
544
445
 
545
446
 
546
 
class ProxyBasicAndDigestAuthServer(DigestAuthServer, ProxyAuthServer):
547
 
    """An proxy server requiring basic or digest authentication"""
548
 
 
549
 
    def __init__(self, protocol_version=None):
550
 
        DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
551
 
                                  'basicdigest',
552
 
                                  protocol_version=protocol_version)
553
 
        self.init_proxy_auth()
554
 
        # We really accept Digest only
555
 
        self.auth_scheme = 'digest'
556
 
 
557