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
17
from cStringIO import StringIO
19
from SimpleHTTPServer import SimpleHTTPRequestHandler
19
from urllib.request import (
23
except ImportError: # python < 3
36
from ..sixish import (
39
from ..bzr.smart import (
42
from . import http_server
43
from ..transport import chroot
46
class HTTPServerWithSmarts(http_server.HttpServer):
24
from bzrlib import smart
25
import bzrlib.smart.request
26
from bzrlib.tests import TestCaseWithTransport
27
from bzrlib.tests.HttpServer import (
29
TestingHTTPRequestHandler,
31
from bzrlib.transport import (
34
from bzrlib.smart import protocol
37
class WallRequestHandler(TestingHTTPRequestHandler):
38
"""Whatever request comes in, close the connection"""
40
def handle_one_request(self):
41
"""Handle a single HTTP request, by abruptly closing the connection"""
42
self.close_connection = 1
45
class BadStatusRequestHandler(TestingHTTPRequestHandler):
46
"""Whatever request comes in, returns a bad status"""
48
def parse_request(self):
49
"""Fakes handling a single HTTP request, returns a bad status"""
50
ignored = TestingHTTPRequestHandler.parse_request(self)
52
self.send_response(0, "Bad status")
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,
67
if (len(e.args) > 0) and (e.args[0] in spurious):
68
self.close_connection = 1
75
class InvalidStatusRequestHandler(TestingHTTPRequestHandler):
76
"""Whatever request comes in, returns am invalid status"""
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")
85
class BadProtocolRequestHandler(TestingHTTPRequestHandler):
86
"""Whatever request comes in, returns a bad protocol version"""
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',
95
'Look at my protocol version'))
99
class ForbiddenRequestHandler(TestingHTTPRequestHandler):
100
"""Whatever request comes in, returns a 403 code"""
102
def parse_request(self):
103
"""Handle a single HTTP request, by replying we cannot handle it"""
104
ignored = TestingHTTPRequestHandler.parse_request(self)
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
52
def __init__(self, protocol_version=None):
53
http_server.HttpServer.__init__(self, SmartRequestHandler,
54
protocol_version=protocol_version)
57
class SmartRequestHandler(http_server.TestingHTTPRequestHandler):
58
"""Extend TestingHTTPRequestHandler to support smart client POSTs.
60
XXX: This duplicates a fair bit of the logic in breezy.transport.http.wsgi.
116
HttpServer.__init__(self, SmartRequestHandler)
119
class SmartRequestHandler(TestingHTTPRequestHandler):
120
"""Extend TestingHTTPRequestHandler to support smart client POSTs."""
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()
70
t = transport.get_transport_from_url(chroot_server.get_url())
73
chroot_server.stop_server()
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'):
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(
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())
109
class TestCaseWithWebserver(tests.TestCaseWithTransport):
151
class SingleRangeRequestHandler(TestingHTTPRequestHandler):
152
"""Always reply to range request as if they were single.
154
Don't be explicit about it, just to annoy the clients.
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)
163
class NoRangeRequestHandler(TestingHTTPRequestHandler):
164
"""Ignore range requests without notice"""
166
# Just bypass the range handling done by TestingHTTPRequestHandler
167
do_GET = SimpleHTTPRequestHandler.do_GET
170
class TestCaseWithWebserver(TestCaseWithTransport):
110
171
"""A support class that provides readonly urls that are http://.
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.
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'
124
178
super(TestCaseWithWebserver, self).setUp()
125
self.transport_readonly_server = http_server.HttpServer
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
179
self.transport_readonly_server = HttpServer
134
182
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
148
196
This is mostly a hook for daughter classes.
150
server = self.transport_secondary_server(
151
protocol_version=self._protocol_version)
152
server._url_protocol = self._url_protocol
198
return self.transport_secondary_server()
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
162
def get_secondary_url(self, relpath=None):
163
base = self.get_secondary_server().get_url()
164
return self._adjust_url(base, relpath)
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())
172
class ProxyServer(http_server.HttpServer):
173
"""A proxy test server for http transports."""
175
proxy_requests = True
178
class RedirectRequestHandler(http_server.TestingHTTPRequestHandler):
209
class FakeProxyRequestHandler(TestingHTTPRequestHandler):
210
"""Append a '-proxied' suffix to file served"""
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).
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)
231
class RedirectRequestHandler(TestingHTTPRequestHandler):
179
232
"""Redirect all request to the specified server"""
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)
185
238
tcs = self.server.test_case_server
186
239
code, target = tcs.is_redirected(self.path)
247
296
The 'old' server is redirected to the 'new' server.
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)
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()
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
266
def get_old_url(self, relpath=None):
267
base = self.old_server.get_url()
268
return self._adjust_url(base, relpath)
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())
275
def get_new_url(self, relpath=None):
276
base = self.new_server.get_url()
277
return self._adjust_url(base, relpath)
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())
285
class AuthRequestHandler(http_server.TestingHTTPRequestHandler):
286
"""Requires an authentication to process requests.
288
This is intended to be used with a server that always and
289
only use one authentication scheme (implemented by daughter
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
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')
312
if self.authorized():
313
return http_server.TestingHTTPRequestHandler.do_GET(self)
315
return self._require_authentication()
318
if self.authorized():
319
return http_server.TestingHTTPRequestHandler.do_HEAD(self)
321
return self._require_authentication()
324
class BasicAuthRequestHandler(AuthRequestHandler):
325
"""Implements the basic authentication of a request"""
327
def authorized(self):
328
tcs = self.server.test_case_server
329
if tcs.auth_scheme != 'basic':
332
auth_header = self.headers.get(tcs.auth_header_recv, None)
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)
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)
347
# FIXME: We could send an Authentication-Info header too when
348
# the authentication is succesful
350
class DigestAuthRequestHandler(AuthRequestHandler):
351
"""Implements the digest authentication of a request.
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.
358
def authorized(self):
359
tcs = self.server.test_case_server
361
auth_header = self.headers.get(tcs.auth_header_recv, None)
362
if auth_header is None:
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))
368
return tcs.digest_authorized(auth_dict, self.command)
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,
377
self.send_header(tcs.auth_header_sent, header)
380
class DigestAndBasicAuthRequestHandler(DigestAuthRequestHandler):
381
"""Implements a digest and basic authentication of a request.
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
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,
395
self.send_header(tcs.auth_header_sent, header)
398
class AuthServer(http_server.HttpServer):
399
"""Extends HttpServer with a dictionary of passwords.
401
This is used as a base class for various schemes which should
402
all use or redefined the associated AuthRequestHandler.
404
Note that no users are defined by default, so add_user should
405
be called before issuing the first request.
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"
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
423
def add_user(self, user, password):
424
"""Declare a user with an associated password.
426
password can be empty, use an empty string ('') in that
429
self.password_of[user] = password
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
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
442
class DigestAuthServer(AuthServer):
443
"""A digest authentication server"""
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)
452
def digest_authorized(self, auth, command):
453
nonce = auth['nonce']
454
if nonce != self.auth_nonce:
456
realm = auth['realm']
457
if realm != self.auth_realm:
459
user = auth['username']
460
if user not in self.password_of:
462
algorithm= auth['algorithm']
463
if algorithm != 'MD5':
469
password = self.password_of[user]
471
# Recalculate the response_digest to compare with the one
473
A1 = '%s:%s:%s' % (user, realm, password)
474
A2 = '%s:%s' % (command, auth['uri'])
476
H = lambda x: osutils.md5(x).hexdigest()
477
KD = lambda secret, data: H("%s:%s" % (secret, data))
479
nonce_count = int(auth['nc'], 16)
481
ncvalue = '%08x' % nonce_count
483
cnonce = auth['cnonce']
484
noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
485
response_digest = KD(H(A1), noncebit)
487
return response_digest == auth['response']
490
class HTTPAuthServer(AuthServer):
491
"""An HTTP server requiring authentication"""
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
499
class ProxyAuthServer(AuthServer):
500
"""A proxy server requiring authentication"""
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
509
class HTTPBasicAuthServer(HTTPAuthServer):
510
"""An HTTP server requiring basic authentication"""
512
def __init__(self, protocol_version=None):
513
HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
514
protocol_version=protocol_version)
515
self.init_http_auth()
518
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
519
"""An HTTP server requiring digest authentication"""
521
def __init__(self, protocol_version=None):
522
DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
523
protocol_version=protocol_version)
524
self.init_http_auth()
527
class HTTPBasicAndDigestAuthServer(DigestAuthServer, HTTPAuthServer):
528
"""An HTTP server requiring basic or digest authentication"""
530
def __init__(self, protocol_version=None):
531
DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
533
protocol_version=protocol_version)
534
self.init_http_auth()
535
# We really accept Digest only
536
self.auth_scheme = 'digest'
539
class ProxyBasicAuthServer(ProxyAuthServer):
540
"""A proxy server requiring basic authentication"""
542
def __init__(self, protocol_version=None):
543
ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
544
protocol_version=protocol_version)
545
self.init_proxy_auth()
548
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
549
"""A proxy server requiring basic authentication"""
551
def __init__(self, protocol_version=None):
552
ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
553
protocol_version=protocol_version)
554
self.init_proxy_auth()
557
class ProxyBasicAndDigestAuthServer(DigestAuthServer, ProxyAuthServer):
558
"""An proxy server requiring basic or digest authentication"""
560
def __init__(self, protocol_version=None):
561
DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
563
protocol_version=protocol_version)
564
self.init_proxy_auth()
565
# We really accept Digest only
566
self.auth_scheme = 'digest'