~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/HTTPTestUtil.py

  • Committer: Robert Collins
  • Date: 2007-04-19 02:27:44 UTC
  • mto: This revision was merged to the branch mainline in revision 2426.
  • Revision ID: robertc@robertcollins.net-20070419022744-pfdqz42kp1wizh43
``make docs`` now creates a man page at ``man1/bzr.1`` fixing bug 107388.
(Robert Collins)

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