/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 bzrlib/tests/HTTPTestUtil.py

Merge from bzr.dev, resolving conflicts.

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