1
# Copyright (C) 2005 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
from cStringIO import StringIO
33
from bzrlib.smart import protocol
34
from bzrlib.tests import http_server
37
class HTTPServerWithSmarts(http_server.HttpServer):
38
"""HTTPServerWithSmarts extends the HttpServer with POST methods that will
39
trigger a smart server to execute with a transport rooted at the rootdir of
43
def __init__(self, protocol_version=None):
44
http_server.HttpServer.__init__(self, SmartRequestHandler,
45
protocol_version=protocol_version)
48
class SmartRequestHandler(http_server.TestingHTTPRequestHandler):
49
"""Extend TestingHTTPRequestHandler to support smart client POSTs."""
52
"""Hand the request off to a smart server instance."""
53
self.send_response(200)
54
self.send_header("Content-type", "application/octet-stream")
55
t = transport.get_transport(self.server.test_case_server._home_dir)
56
# TODO: We might like to support streaming responses. 1.0 allows no
57
# Content-length in this case, so for integrity we should perform our
58
# own chunking within the stream.
59
# 1.1 allows chunked responses, and in this case we could chunk using
60
# the HTTP chunking as this will allow HTTP persistence safely, even if
61
# we have to stop early due to error, but we would also have to use the
62
# HTTP trailer facility which may not be widely available.
63
out_buffer = StringIO()
64
smart_protocol_request = protocol.SmartServerRequestProtocolOne(
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'])
69
# Perhaps there should be a SmartServerHTTPMedium that takes care of
70
# feeding the bytes in the http request to the smart_protocol_request,
71
# but for now it's simpler to just feed the bytes directly.
72
smart_protocol_request.accept_bytes(self.rfile.read(data_length))
73
if not (smart_protocol_request.next_read_size() == 0):
74
raise errors.SmartProtocolError(
75
"not finished reading, but all data sent to protocol.")
76
self.send_header("Content-Length", str(len(out_buffer.getvalue())))
78
self.wfile.write(out_buffer.getvalue())
81
class TestCaseWithWebserver(tests.TestCaseWithTransport):
82
"""A support class that provides readonly urls that are http://.
84
This is done by forcing the readonly server to be an http
85
one. This will currently fail if the primary transport is not
86
backed by regular disk files.
89
super(TestCaseWithWebserver, self).setUp()
90
self.transport_readonly_server = http_server.HttpServer
93
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
94
"""A support class providing readonly urls on two servers that are http://.
96
We set up two webservers to allows various tests involving
97
proxies or redirections from one server to the other.
100
super(TestCaseWithTwoWebservers, self).setUp()
101
self.transport_secondary_server = http_server.HttpServer
102
self.__secondary_server = None
104
def create_transport_secondary_server(self):
105
"""Create a transport server from class defined at init.
107
This is mostly a hook for daughter classes.
109
return self.transport_secondary_server()
111
def get_secondary_server(self):
112
"""Get the server instance for the secondary transport."""
113
if self.__secondary_server is None:
114
self.__secondary_server = self.create_transport_secondary_server()
115
self.__secondary_server.setUp()
116
self.addCleanup(self.__secondary_server.tearDown)
117
return self.__secondary_server
120
class ProxyServer(http_server.HttpServer):
121
"""A proxy test server for http transports."""
123
proxy_requests = True
126
class RedirectRequestHandler(http_server.TestingHTTPRequestHandler):
127
"""Redirect all request to the specified server"""
129
def parse_request(self):
130
"""Redirect a single HTTP request to another host"""
131
valid = http_server.TestingHTTPRequestHandler.parse_request(self)
133
tcs = self.server.test_case_server
134
code, target = tcs.is_redirected(self.path)
135
if code is not None and target is not None:
136
# Redirect as instructed
137
self.send_response(code)
138
self.send_header('Location', target)
139
# We do not send a body
140
self.send_header('Content-Length', '0')
142
return False # The job is done
144
# We leave the parent class serve the request
149
class HTTPServerRedirecting(http_server.HttpServer):
150
"""An HttpServer redirecting to another server """
152
def __init__(self, request_handler=RedirectRequestHandler,
153
protocol_version=None):
154
http_server.HttpServer.__init__(self, request_handler,
155
protocol_version=protocol_version)
156
# redirections is a list of tuples (source, target, code)
157
# - source is a regexp for the paths requested
158
# - target is a replacement for re.sub describing where
159
# the request will be redirected
160
# - code is the http error code associated to the
161
# redirection (301 permanent, 302 temporarry, etc
162
self.redirections = []
164
def redirect_to(self, host, port):
165
"""Redirect all requests to a specific host:port"""
166
self.redirections = [('(.*)',
167
r'http://%s:%s\1' % (host, port) ,
170
def is_redirected(self, path):
171
"""Is the path redirected by this server.
173
:param path: the requested relative path
175
:returns: a tuple (code, target) if a matching
176
redirection is found, (None, None) otherwise.
180
for (rsource, rtarget, rcode) in self.redirections:
181
target, match = re.subn(rsource, rtarget, path)
184
break # The first match wins
190
class TestCaseWithRedirectedWebserver(TestCaseWithTwoWebservers):
191
"""A support class providing redirections from one server to another.
193
We set up two webservers to allows various tests involving
195
The 'old' server is redirected to the 'new' server.
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)
206
super(TestCaseWithRedirectedWebserver, self).setUp()
207
# The redirections will point to the new server
208
self.new_server = self.get_readonly_server()
209
# The requests to the old server will be redirected
210
self.old_server = self.get_secondary_server()
213
class AuthRequestHandler(http_server.TestingHTTPRequestHandler):
214
"""Requires an authentication to process requests.
216
This is intended to be used with a server that always and
217
only use one authentication scheme (implemented by daughter
221
# The following attributes should be defined in the server
222
# - auth_header_sent: the header name sent to require auth
223
# - auth_header_recv: the header received containing auth
224
# - auth_error_code: the error code to indicate auth required
227
if self.authorized():
228
return http_server.TestingHTTPRequestHandler.do_GET(self)
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')
243
class BasicAuthRequestHandler(AuthRequestHandler):
244
"""Implements the basic authentication of a request"""
246
def authorized(self):
247
tcs = self.server.test_case_server
248
if tcs.auth_scheme != 'basic':
251
auth_header = self.headers.get(tcs.auth_header_recv, None)
253
scheme, raw_auth = auth_header.split(' ', 1)
254
if scheme.lower() == tcs.auth_scheme:
255
user, password = raw_auth.decode('base64').split(':')
256
return tcs.authorized(user, password)
260
def send_header_auth_reqed(self):
261
tcs = self.server.test_case_server
262
self.send_header(tcs.auth_header_sent,
263
'Basic realm="%s"' % tcs.auth_realm)
266
# FIXME: We could send an Authentication-Info header too when
267
# the authentication is succesful
269
class DigestAuthRequestHandler(AuthRequestHandler):
270
"""Implements the digest authentication of a request.
272
We need persistence for some attributes and that can't be
273
achieved here since we get instantiated for each request. We
274
rely on the DigestAuthServer to take care of them.
277
def authorized(self):
278
tcs = self.server.test_case_server
279
if tcs.auth_scheme != 'digest':
282
auth_header = self.headers.get(tcs.auth_header_recv, None)
283
if auth_header is None:
285
scheme, auth = auth_header.split(None, 1)
286
if scheme.lower() == tcs.auth_scheme:
287
auth_dict = urllib2.parse_keqv_list(urllib2.parse_http_list(auth))
289
return tcs.digest_authorized(auth_dict, self.command)
293
def send_header_auth_reqed(self):
294
tcs = self.server.test_case_server
295
header = 'Digest realm="%s", ' % tcs.auth_realm
296
header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
298
self.send_header(tcs.auth_header_sent,header)
301
class AuthServer(http_server.HttpServer):
302
"""Extends HttpServer with a dictionary of passwords.
304
This is used as a base class for various schemes which should
305
all use or redefined the associated AuthRequestHandler.
307
Note that no users are defined by default, so add_user should
308
be called before issuing the first request.
311
# The following attributes should be set dy daughter classes
312
# and are used by AuthRequestHandler.
313
auth_header_sent = None
314
auth_header_recv = None
315
auth_error_code = None
316
auth_realm = "Thou should not pass"
318
def __init__(self, request_handler, auth_scheme,
319
protocol_version=None):
320
http_server.HttpServer.__init__(self, request_handler,
321
protocol_version=protocol_version)
322
self.auth_scheme = auth_scheme
323
self.password_of = {}
324
self.auth_required_errors = 0
326
def add_user(self, user, password):
327
"""Declare a user with an associated password.
329
password can be empty, use an empty string ('') in that
332
self.password_of[user] = password
334
def authorized(self, user, password):
335
"""Check that the given user provided the right password"""
336
expected_password = self.password_of.get(user, None)
337
return expected_password is not None and password == expected_password
340
# FIXME: There is some code duplication with
341
# _urllib2_wrappers.py.DigestAuthHandler. If that duplication
342
# grows, it may require a refactoring. Also, we don't implement
343
# SHA algorithm nor MD5-sess here, but that does not seem worth
345
class DigestAuthServer(AuthServer):
346
"""A digest authentication server"""
350
def __init__(self, request_handler, auth_scheme,
351
protocol_version=None):
352
AuthServer.__init__(self, request_handler, auth_scheme,
353
protocol_version=protocol_version)
355
def digest_authorized(self, auth, command):
356
nonce = auth['nonce']
357
if nonce != self.auth_nonce:
359
realm = auth['realm']
360
if realm != self.auth_realm:
362
user = auth['username']
363
if not self.password_of.has_key(user):
365
algorithm= auth['algorithm']
366
if algorithm != 'MD5':
372
password = self.password_of[user]
374
# Recalculate the response_digest to compare with the one
376
A1 = '%s:%s:%s' % (user, realm, password)
377
A2 = '%s:%s' % (command, auth['uri'])
379
H = lambda x: md5.new(x).hexdigest()
380
KD = lambda secret, data: H("%s:%s" % (secret, data))
382
nonce_count = int(auth['nc'], 16)
384
ncvalue = '%08x' % nonce_count
386
cnonce = auth['cnonce']
387
noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
388
response_digest = KD(H(A1), noncebit)
390
return response_digest == auth['response']
392
class HTTPAuthServer(AuthServer):
393
"""An HTTP server requiring authentication"""
395
def init_http_auth(self):
396
self.auth_header_sent = 'WWW-Authenticate'
397
self.auth_header_recv = 'Authorization'
398
self.auth_error_code = 401
401
class ProxyAuthServer(AuthServer):
402
"""A proxy server requiring authentication"""
404
def init_proxy_auth(self):
405
self.proxy_requests = True
406
self.auth_header_sent = 'Proxy-Authenticate'
407
self.auth_header_recv = 'Proxy-Authorization'
408
self.auth_error_code = 407
411
class HTTPBasicAuthServer(HTTPAuthServer):
412
"""An HTTP server requiring basic authentication"""
414
def __init__(self, protocol_version=None):
415
HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
416
protocol_version=protocol_version)
417
self.init_http_auth()
420
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
421
"""An HTTP server requiring digest authentication"""
423
def __init__(self, protocol_version=None):
424
DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
425
protocol_version=protocol_version)
426
self.init_http_auth()
429
class ProxyBasicAuthServer(ProxyAuthServer):
430
"""A proxy server requiring basic authentication"""
432
def __init__(self, protocol_version=None):
433
ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
434
protocol_version=protocol_version)
435
self.init_proxy_auth()
438
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
439
"""A proxy server requiring basic authentication"""
441
def __init__(self, protocol_version=None):
442
ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
443
protocol_version=protocol_version)
444
self.init_proxy_auth()