~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/http_utils.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-10-31 04:39:04 UTC
  • mfrom: (3565.6.16 switch_nick)
  • Revision ID: pqm@pqm.ubuntu.com-20081031043904-52fnbfrloojemvcc
(mbp) branch nickname documentation

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 by Canonical Ltd
2
 
 
 
1
# Copyright (C) 2005 Canonical Ltd
 
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
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
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
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
import BaseHTTPServer, SimpleHTTPServer, socket, errno, time
18
 
from bzrlib.tests import TestCaseInTempDir
19
 
 
20
 
 
21
 
class WebserverNotAvailable(Exception):
22
 
    pass
23
 
 
24
 
class BadWebserverPath(ValueError):
25
 
    def __str__(self):
26
 
        return 'path %s is not in %s' % self.args
27
 
 
28
 
class TestingHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
29
 
    def log_message(self, format, *args):
30
 
        self.server.test_case.log("webserver - %s - - [%s] %s",
31
 
                                  self.address_string(),
32
 
                                  self.log_date_time_string(),
33
 
                                  format%args)
34
 
 
35
 
    def handle_one_request(self):
36
 
        """Handle a single HTTP request.
37
 
 
38
 
        You normally don't need to override this method; see the class
39
 
        __doc__ string for information on how to handle specific HTTP
40
 
        commands such as GET and POST.
41
 
 
 
17
from cStringIO import StringIO
 
18
import errno
 
19
import re
 
20
import socket
 
21
import threading
 
22
import time
 
23
import urllib2
 
24
import urlparse
 
25
 
 
26
 
 
27
from bzrlib import (
 
28
    errors,
 
29
    osutils,
 
30
    tests,
 
31
    )
 
32
from bzrlib.smart import medium, protocol
 
33
from bzrlib.tests import http_server
 
34
from bzrlib.transport import (
 
35
    chroot,
 
36
    get_transport,
 
37
    )
 
38
 
 
39
 
 
40
class HTTPServerWithSmarts(http_server.HttpServer):
 
41
    """HTTPServerWithSmarts extends the HttpServer with POST methods that will
 
42
    trigger a smart server to execute with a transport rooted at the rootdir of
 
43
    the HTTP server.
 
44
    """
 
45
 
 
46
    def __init__(self, protocol_version=None):
 
47
        http_server.HttpServer.__init__(self, SmartRequestHandler,
 
48
                                        protocol_version=protocol_version)
 
49
 
 
50
 
 
51
class SmartRequestHandler(http_server.TestingHTTPRequestHandler):
 
52
    """Extend TestingHTTPRequestHandler to support smart client POSTs.
 
53
    
 
54
    XXX: This duplicates a fair bit of the logic in bzrlib.transport.http.wsgi.
 
55
    """
 
56
 
 
57
    def do_POST(self):
 
58
        """Hand the request off to a smart server instance."""
 
59
        backing = get_transport(self.server.test_case_server._home_dir)
 
60
        chroot_server = chroot.ChrootServer(backing)
 
61
        chroot_server.setUp()
 
62
        try:
 
63
            t = get_transport(chroot_server.get_url())
 
64
            self.do_POST_inner(t)
 
65
        finally:
 
66
            chroot_server.tearDown()
 
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 = medium._get_protocol_factory_for_bytes(
 
87
            request_bytes)
 
88
        out_buffer = StringIO()
 
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
    def setUp(self):
 
110
        super(TestCaseWithWebserver, self).setUp()
 
111
        self.transport_readonly_server = http_server.HttpServer
 
112
 
 
113
 
 
114
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
 
115
    """A support class providing readonly urls on two servers that are http://.
 
116
 
 
117
    We set up two webservers to allows various tests involving
 
118
    proxies or redirections from one server to the other.
 
119
    """
 
120
    def setUp(self):
 
121
        super(TestCaseWithTwoWebservers, self).setUp()
 
122
        self.transport_secondary_server = http_server.HttpServer
 
123
        self.__secondary_server = None
 
124
 
 
125
    def create_transport_secondary_server(self):
 
126
        """Create a transport server from class defined at init.
 
127
 
 
128
        This is mostly a hook for daughter classes.
42
129
        """
43
 
        for i in xrange(1,11): # Don't try more than 10 times
44
 
            try:
45
 
                self.raw_requestline = self.rfile.readline()
46
 
            except socket.error, e:
47
 
                if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
48
 
                    # omitted for now because some tests look at the log of
49
 
                    # the server and expect to see no errors.  see recent
50
 
                    # email thread. -- mbp 20051021. 
51
 
                    ## self.log_message('EAGAIN (%d) while reading from raw_requestline' % i)
52
 
                    time.sleep(0.01)
53
 
                    continue
54
 
                raise
55
 
            else:
56
 
                break
57
 
        if not self.raw_requestline:
58
 
            self.close_connection = 1
59
 
            return
60
 
        if not self.parse_request(): # An error code has been sent, just exit
61
 
            return
62
 
        mname = 'do_' + self.command
63
 
        if not hasattr(self, mname):
64
 
            self.send_error(501, "Unsupported method (%r)" % self.command)
65
 
            return
66
 
        method = getattr(self, mname)
67
 
        method()
68
 
 
69
 
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
70
 
    def __init__(self, server_address, RequestHandlerClass, test_case):
71
 
        BaseHTTPServer.HTTPServer.__init__(self, server_address,
72
 
                                                RequestHandlerClass)
73
 
        self.test_case = test_case
74
 
 
75
 
 
76
 
class TestCaseWithWebserver(TestCaseInTempDir):
77
 
    """Derived class that starts a localhost-only webserver
78
 
    (in addition to what TestCaseInTempDir does).
79
 
 
80
 
    This is useful for testing RemoteBranch.
81
 
    """
82
 
 
83
 
    _HTTP_PORTS = range(13000, 0x8000)
84
 
 
85
 
    def _http_start(self):
86
 
        import SimpleHTTPServer, BaseHTTPServer, socket, errno
87
 
        httpd = None
88
 
        for port in self._HTTP_PORTS:
89
 
            try:
90
 
                httpd = TestingHTTPServer(('localhost', port),
91
 
                                          TestingHTTPRequestHandler,
92
 
                                          self)
93
 
            except socket.error, e:
94
 
                if e.args[0] == errno.EADDRINUSE:
95
 
                    continue
96
 
                print >>sys.stderr, "Cannot run webserver :-("
97
 
                raise
98
 
            else:
99
 
                break
100
 
 
101
 
        if httpd is None:
102
 
            raise WebserverNotAvailable("Cannot run webserver :-( "
103
 
                                        "no free ports in range %s..%s" %
104
 
                                        (_HTTP_PORTS[0], _HTTP_PORTS[-1]))
105
 
 
106
 
        self._http_base_url = 'http://localhost:%s/' % port
107
 
        self._http_starting.release()
108
 
        httpd.socket.settimeout(0.1)
109
 
 
110
 
        while self._http_running:
111
 
            try:
112
 
                httpd.handle_request()
113
 
            except socket.timeout:
 
130
        return self.transport_secondary_server()
 
131
 
 
132
    def get_secondary_server(self):
 
133
        """Get the server instance for the secondary transport."""
 
134
        if self.__secondary_server is None:
 
135
            self.__secondary_server = self.create_transport_secondary_server()
 
136
            self.__secondary_server.setUp()
 
137
            self.addCleanup(self.__secondary_server.tearDown)
 
138
        return self.__secondary_server
 
139
 
 
140
 
 
141
class ProxyServer(http_server.HttpServer):
 
142
    """A proxy test server for http transports."""
 
143
 
 
144
    proxy_requests = True
 
145
 
 
146
 
 
147
class RedirectRequestHandler(http_server.TestingHTTPRequestHandler):
 
148
    """Redirect all request to the specified server"""
 
149
 
 
150
    def parse_request(self):
 
151
        """Redirect a single HTTP request to another host"""
 
152
        valid = http_server.TestingHTTPRequestHandler.parse_request(self)
 
153
        if valid:
 
154
            tcs = self.server.test_case_server
 
155
            code, target = tcs.is_redirected(self.path)
 
156
            if code is not None and target is not None:
 
157
                # Redirect as instructed
 
158
                self.send_response(code)
 
159
                self.send_header('Location', target)
 
160
                # We do not send a body
 
161
                self.send_header('Content-Length', '0')
 
162
                self.end_headers()
 
163
                return False # The job is done
 
164
            else:
 
165
                # We leave the parent class serve the request
114
166
                pass
115
 
 
116
 
    def get_remote_url(self, path):
117
 
        import os
118
 
 
119
 
        path_parts = path.split(os.path.sep)
120
 
        if os.path.isabs(path):
121
 
            if path_parts[:len(self._local_path_parts)] != \
122
 
                   self._local_path_parts:
123
 
                raise BadWebserverPath(path, self.test_dir)
124
 
            remote_path = '/'.join(path_parts[len(self._local_path_parts):])
 
167
        return valid
 
168
 
 
169
 
 
170
class HTTPServerRedirecting(http_server.HttpServer):
 
171
    """An HttpServer redirecting to another server """
 
172
 
 
173
    def __init__(self, request_handler=RedirectRequestHandler,
 
174
                 protocol_version=None):
 
175
        http_server.HttpServer.__init__(self, request_handler,
 
176
                                        protocol_version=protocol_version)
 
177
        # redirections is a list of tuples (source, target, code)
 
178
        # - source is a regexp for the paths requested
 
179
        # - target is a replacement for re.sub describing where
 
180
        #   the request will be redirected
 
181
        # - code is the http error code associated to the
 
182
        #   redirection (301 permanent, 302 temporarry, etc
 
183
        self.redirections = []
 
184
 
 
185
    def redirect_to(self, host, port):
 
186
        """Redirect all requests to a specific host:port"""
 
187
        self.redirections = [('(.*)',
 
188
                              r'http://%s:%s\1' % (host, port) ,
 
189
                              301)]
 
190
 
 
191
    def is_redirected(self, path):
 
192
        """Is the path redirected by this server.
 
193
 
 
194
        :param path: the requested relative path
 
195
 
 
196
        :returns: a tuple (code, target) if a matching
 
197
             redirection is found, (None, None) otherwise.
 
198
        """
 
199
        code = None
 
200
        target = None
 
201
        for (rsource, rtarget, rcode) in self.redirections:
 
202
            target, match = re.subn(rsource, rtarget, path)
 
203
            if match:
 
204
                code = rcode
 
205
                break # The first match wins
 
206
            else:
 
207
                target = None
 
208
        return code, target
 
209
 
 
210
 
 
211
class TestCaseWithRedirectedWebserver(TestCaseWithTwoWebservers):
 
212
   """A support class providing redirections from one server to another.
 
213
 
 
214
   We set up two webservers to allows various tests involving
 
215
   redirections.
 
216
   The 'old' server is redirected to the 'new' server.
 
217
   """
 
218
 
 
219
   def create_transport_secondary_server(self):
 
220
       """Create the secondary server redirecting to the primary server"""
 
221
       new = self.get_readonly_server()
 
222
       redirecting = HTTPServerRedirecting()
 
223
       redirecting.redirect_to(new.host, new.port)
 
224
       return redirecting
 
225
 
 
226
   def setUp(self):
 
227
       super(TestCaseWithRedirectedWebserver, self).setUp()
 
228
       # The redirections will point to the new server
 
229
       self.new_server = self.get_readonly_server()
 
230
       # The requests to the old server will be redirected
 
231
       self.old_server = self.get_secondary_server()
 
232
 
 
233
 
 
234
class AuthRequestHandler(http_server.TestingHTTPRequestHandler):
 
235
    """Requires an authentication to process requests.
 
236
 
 
237
    This is intended to be used with a server that always and
 
238
    only use one authentication scheme (implemented by daughter
 
239
    classes).
 
240
    """
 
241
 
 
242
    # The following attributes should be defined in the server
 
243
    # - auth_header_sent: the header name sent to require auth
 
244
    # - auth_header_recv: the header received containing auth
 
245
    # - auth_error_code: the error code to indicate auth required
 
246
 
 
247
    def do_GET(self):
 
248
        if self.authorized():
 
249
            return http_server.TestingHTTPRequestHandler.do_GET(self)
125
250
        else:
126
 
            remote_path = '/'.join(path_parts)
127
 
 
128
 
        self._http_starting.acquire()
129
 
        self._http_starting.release()
130
 
        return self._http_base_url + remote_path
131
 
 
132
 
    def setUp(self):
133
 
        TestCaseInTempDir.setUp(self)
134
 
        import threading, os
135
 
        self._local_path_parts = self.test_dir.split(os.path.sep)
136
 
        self._http_starting = threading.Lock()
137
 
        self._http_starting.acquire()
138
 
        self._http_running = True
139
 
        self._http_base_url = None
140
 
        self._http_thread = threading.Thread(target=self._http_start)
141
 
        self._http_thread.setDaemon(True)
142
 
        self._http_thread.start()
143
 
        self._http_proxy = os.environ.get("http_proxy")
144
 
        if self._http_proxy is not None:
145
 
            del os.environ["http_proxy"]
146
 
 
147
 
    def tearDown(self):
148
 
        self._http_running = False
149
 
        self._http_thread.join()
150
 
        if self._http_proxy is not None:
151
 
            import os
152
 
            os.environ["http_proxy"] = self._http_proxy
153
 
        TestCaseInTempDir.tearDown(self)
 
251
            # Note that we must update test_case_server *before*
 
252
            # sending the error or the client may try to read it
 
253
            # before we have sent the whole error back.
 
254
            tcs = self.server.test_case_server
 
255
            tcs.auth_required_errors += 1
 
256
            self.send_response(tcs.auth_error_code)
 
257
            self.send_header_auth_reqed()
 
258
            # We do not send a body
 
259
            self.send_header('Content-Length', '0')
 
260
            self.end_headers()
 
261
            return
 
262
 
 
263
 
 
264
class BasicAuthRequestHandler(AuthRequestHandler):
 
265
    """Implements the basic authentication of a request"""
 
266
 
 
267
    def authorized(self):
 
268
        tcs = self.server.test_case_server
 
269
        if tcs.auth_scheme != 'basic':
 
270
            return False
 
271
 
 
272
        auth_header = self.headers.get(tcs.auth_header_recv, None)
 
273
        if auth_header:
 
274
            scheme, raw_auth = auth_header.split(' ', 1)
 
275
            if scheme.lower() == tcs.auth_scheme:
 
276
                user, password = raw_auth.decode('base64').split(':')
 
277
                return tcs.authorized(user, password)
 
278
 
 
279
        return False
 
280
 
 
281
    def send_header_auth_reqed(self):
 
282
        tcs = self.server.test_case_server
 
283
        self.send_header(tcs.auth_header_sent,
 
284
                         'Basic realm="%s"' % tcs.auth_realm)
 
285
 
 
286
 
 
287
# FIXME: We could send an Authentication-Info header too when
 
288
# the authentication is succesful
 
289
 
 
290
class DigestAuthRequestHandler(AuthRequestHandler):
 
291
    """Implements the digest authentication of a request.
 
292
 
 
293
    We need persistence for some attributes and that can't be
 
294
    achieved here since we get instantiated for each request. We
 
295
    rely on the DigestAuthServer to take care of them.
 
296
    """
 
297
 
 
298
    def authorized(self):
 
299
        tcs = self.server.test_case_server
 
300
        if tcs.auth_scheme != 'digest':
 
301
            return False
 
302
 
 
303
        auth_header = self.headers.get(tcs.auth_header_recv, None)
 
304
        if auth_header is None:
 
305
            return False
 
306
        scheme, auth = auth_header.split(None, 1)
 
307
        if scheme.lower() == tcs.auth_scheme:
 
308
            auth_dict = urllib2.parse_keqv_list(urllib2.parse_http_list(auth))
 
309
 
 
310
            return tcs.digest_authorized(auth_dict, self.command)
 
311
 
 
312
        return False
 
313
 
 
314
    def send_header_auth_reqed(self):
 
315
        tcs = self.server.test_case_server
 
316
        header = 'Digest realm="%s", ' % tcs.auth_realm
 
317
        header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
 
318
                                                              'MD5')
 
319
        self.send_header(tcs.auth_header_sent,header)
 
320
 
 
321
 
 
322
class AuthServer(http_server.HttpServer):
 
323
    """Extends HttpServer with a dictionary of passwords.
 
324
 
 
325
    This is used as a base class for various schemes which should
 
326
    all use or redefined the associated AuthRequestHandler.
 
327
 
 
328
    Note that no users are defined by default, so add_user should
 
329
    be called before issuing the first request.
 
330
    """
 
331
 
 
332
    # The following attributes should be set dy daughter classes
 
333
    # and are used by AuthRequestHandler.
 
334
    auth_header_sent = None
 
335
    auth_header_recv = None
 
336
    auth_error_code = None
 
337
    auth_realm = "Thou should not pass"
 
338
 
 
339
    def __init__(self, request_handler, auth_scheme,
 
340
                 protocol_version=None):
 
341
        http_server.HttpServer.__init__(self, request_handler,
 
342
                                        protocol_version=protocol_version)
 
343
        self.auth_scheme = auth_scheme
 
344
        self.password_of = {}
 
345
        self.auth_required_errors = 0
 
346
 
 
347
    def add_user(self, user, password):
 
348
        """Declare a user with an associated password.
 
349
 
 
350
        password can be empty, use an empty string ('') in that
 
351
        case, not None.
 
352
        """
 
353
        self.password_of[user] = password
 
354
 
 
355
    def authorized(self, user, password):
 
356
        """Check that the given user provided the right password"""
 
357
        expected_password = self.password_of.get(user, None)
 
358
        return expected_password is not None and password == expected_password
 
359
 
 
360
 
 
361
# FIXME: There is some code duplication with
 
362
# _urllib2_wrappers.py.DigestAuthHandler. If that duplication
 
363
# grows, it may require a refactoring. Also, we don't implement
 
364
# SHA algorithm nor MD5-sess here, but that does not seem worth
 
365
# it.
 
366
class DigestAuthServer(AuthServer):
 
367
    """A digest authentication server"""
 
368
 
 
369
    auth_nonce = 'now!'
 
370
 
 
371
    def __init__(self, request_handler, auth_scheme,
 
372
                 protocol_version=None):
 
373
        AuthServer.__init__(self, request_handler, auth_scheme,
 
374
                            protocol_version=protocol_version)
 
375
 
 
376
    def digest_authorized(self, auth, command):
 
377
        nonce = auth['nonce']
 
378
        if nonce != self.auth_nonce:
 
379
            return False
 
380
        realm = auth['realm']
 
381
        if realm != self.auth_realm:
 
382
            return False
 
383
        user = auth['username']
 
384
        if not self.password_of.has_key(user):
 
385
            return False
 
386
        algorithm= auth['algorithm']
 
387
        if algorithm != 'MD5':
 
388
            return False
 
389
        qop = auth['qop']
 
390
        if qop != 'auth':
 
391
            return False
 
392
 
 
393
        password = self.password_of[user]
 
394
 
 
395
        # Recalculate the response_digest to compare with the one
 
396
        # sent by the client
 
397
        A1 = '%s:%s:%s' % (user, realm, password)
 
398
        A2 = '%s:%s' % (command, auth['uri'])
 
399
 
 
400
        H = lambda x: osutils.md5(x).hexdigest()
 
401
        KD = lambda secret, data: H("%s:%s" % (secret, data))
 
402
 
 
403
        nonce_count = int(auth['nc'], 16)
 
404
 
 
405
        ncvalue = '%08x' % nonce_count
 
406
 
 
407
        cnonce = auth['cnonce']
 
408
        noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
 
409
        response_digest = KD(H(A1), noncebit)
 
410
 
 
411
        return response_digest == auth['response']
 
412
 
 
413
class HTTPAuthServer(AuthServer):
 
414
    """An HTTP server requiring authentication"""
 
415
 
 
416
    def init_http_auth(self):
 
417
        self.auth_header_sent = 'WWW-Authenticate'
 
418
        self.auth_header_recv = 'Authorization'
 
419
        self.auth_error_code = 401
 
420
 
 
421
 
 
422
class ProxyAuthServer(AuthServer):
 
423
    """A proxy server requiring authentication"""
 
424
 
 
425
    def init_proxy_auth(self):
 
426
        self.proxy_requests = True
 
427
        self.auth_header_sent = 'Proxy-Authenticate'
 
428
        self.auth_header_recv = 'Proxy-Authorization'
 
429
        self.auth_error_code = 407
 
430
 
 
431
 
 
432
class HTTPBasicAuthServer(HTTPAuthServer):
 
433
    """An HTTP server requiring basic authentication"""
 
434
 
 
435
    def __init__(self, protocol_version=None):
 
436
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
 
437
                                protocol_version=protocol_version)
 
438
        self.init_http_auth()
 
439
 
 
440
 
 
441
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
 
442
    """An HTTP server requiring digest authentication"""
 
443
 
 
444
    def __init__(self, protocol_version=None):
 
445
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
 
446
                                  protocol_version=protocol_version)
 
447
        self.init_http_auth()
 
448
 
 
449
 
 
450
class ProxyBasicAuthServer(ProxyAuthServer):
 
451
    """A proxy server requiring basic authentication"""
 
452
 
 
453
    def __init__(self, protocol_version=None):
 
454
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
 
455
                                 protocol_version=protocol_version)
 
456
        self.init_proxy_auth()
 
457
 
 
458
 
 
459
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
 
460
    """A proxy server requiring basic authentication"""
 
461
 
 
462
    def __init__(self, protocol_version=None):
 
463
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
 
464
                                 protocol_version=protocol_version)
 
465
        self.init_proxy_auth()
 
466
 
154
467