1
# Copyright (C) 2005-2010 Canonical Ltd
1
# Copyright (C) 2005 Canonical Ltd
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
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
17
from cStringIO import StringIO
27
29
from bzrlib import (
32
from bzrlib.smart import medium, protocol
33
from bzrlib.smart import protocol
33
34
from bzrlib.tests import http_server
34
from bzrlib.transport import (
40
37
class HTTPServerWithSmarts(http_server.HttpServer):
46
def __init__(self, protocol_version=None):
47
http_server.HttpServer.__init__(self, SmartRequestHandler,
48
protocol_version=protocol_version)
44
http_server.HttpServer.__init__(self, SmartRequestHandler)
51
47
class SmartRequestHandler(http_server.TestingHTTPRequestHandler):
52
"""Extend TestingHTTPRequestHandler to support smart client POSTs.
54
XXX: This duplicates a fair bit of the logic in bzrlib.transport.http.wsgi.
48
"""Extend TestingHTTPRequestHandler to support smart client POSTs."""
58
51
"""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.start_server()
63
t = get_transport(chroot_server.get_url())
66
chroot_server.stop_server()
68
def do_POST_inner(self, chrooted_transport):
69
52
self.send_response(200)
70
53
self.send_header("Content-type", "application/octet-stream")
71
if not self.path.endswith('.bzr/smart'):
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'])
54
t = transport.get_transport(self.server.test_case_server._home_dir)
78
55
# TODO: We might like to support streaming responses. 1.0 allows no
79
56
# Content-length in this case, so for integrity we should perform our
80
57
# own chunking within the stream.
82
59
# the HTTP chunking as this will allow HTTP persistence safely, even if
83
60
# we have to stop early due to error, but we would also have to use the
84
61
# 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(
88
62
out_buffer = StringIO()
89
smart_protocol_request = protocol_factory(t, out_buffer.write, '/')
63
smart_protocol_request = protocol.SmartServerRequestProtocolOne(
65
# if this fails, we should return 400 bad request, but failure is
66
# failure for now - RBC 20060919
67
data_length = int(self.headers['Content-Length'])
90
68
# Perhaps there should be a SmartServerHTTPMedium that takes care of
91
69
# feeding the bytes in the http request to the smart_protocol_request,
92
70
# 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.")
71
smart_protocol_request.accept_bytes(self.rfile.read(data_length))
72
assert smart_protocol_request.next_read_size() == 0, (
73
"not finished reading, but all data sent to protocol.")
97
74
self.send_header("Content-Length", str(len(out_buffer.getvalue())))
99
76
self.wfile.write(out_buffer.getvalue())
133
110
"""Get the server instance for the secondary transport."""
134
111
if self.__secondary_server is None:
135
112
self.__secondary_server = self.create_transport_secondary_server()
136
self.start_server(self.__secondary_server)
113
self.__secondary_server.setUp()
114
self.addCleanup(self.__secondary_server.tearDown)
137
115
return self.__secondary_server
254
230
tcs.auth_required_errors += 1
255
231
self.send_response(tcs.auth_error_code)
256
232
self.send_header_auth_reqed()
257
# We do not send a body
258
self.send_header('Content-Length', '0')
259
233
self.end_headers()
316
292
self.send_header(tcs.auth_header_sent,header)
319
class DigestAndBasicAuthRequestHandler(DigestAuthRequestHandler):
320
"""Implements a digest and basic authentication of a request.
322
I.e. the server proposes both schemes and the client should choose the best
323
one it can handle, which, in that case, should be digest, the only scheme
327
def send_header_auth_reqed(self):
328
tcs = self.server.test_case_server
329
self.send_header(tcs.auth_header_sent,
330
'Basic realm="%s"' % tcs.auth_realm)
331
header = 'Digest realm="%s", ' % tcs.auth_realm
332
header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
334
self.send_header(tcs.auth_header_sent,header)
337
295
class AuthServer(http_server.HttpServer):
338
296
"""Extends HttpServer with a dictionary of passwords.
412
370
A1 = '%s:%s:%s' % (user, realm, password)
413
371
A2 = '%s:%s' % (command, auth['uri'])
415
H = lambda x: osutils.md5(x).hexdigest()
373
H = lambda x: md5.new(x).hexdigest()
416
374
KD = lambda secret, data: H("%s:%s" % (secret, data))
418
376
nonce_count = int(auth['nc'], 16)
463
420
self.init_http_auth()
466
class HTTPBasicAndDigestAuthServer(DigestAuthServer, HTTPAuthServer):
467
"""An HTTP server requiring basic or digest authentication"""
469
def __init__(self, protocol_version=None):
470
DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
472
protocol_version=protocol_version)
473
self.init_http_auth()
474
# We really accept Digest only
475
self.auth_scheme = 'digest'
478
423
class ProxyBasicAuthServer(ProxyAuthServer):
479
424
"""A proxy server requiring basic authentication"""
493
438
self.init_proxy_auth()
496
class ProxyBasicAndDigestAuthServer(DigestAuthServer, ProxyAuthServer):
497
"""An proxy server requiring basic or digest authentication"""
499
def __init__(self, protocol_version=None):
500
DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
502
protocol_version=protocol_version)
503
self.init_proxy_auth()
504
# We really accept Digest only
505
self.auth_scheme = 'digest'
441
class RecordingServer(object):
442
"""A fake HTTP server.
444
It records the bytes sent to it, and replies with a 200.
447
def __init__(self, expect_body_tail=None):
450
:type expect_body_tail: str
451
:param expect_body_tail: a reply won't be sent until this string is
454
self._expect_body_tail = expect_body_tail
457
self.received_bytes = ''
460
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
461
self._sock.bind(('127.0.0.1', 0))
462
self.host, self.port = self._sock.getsockname()
463
self._ready = threading.Event()
464
self._thread = threading.Thread(target=self._accept_read_and_reply)
465
self._thread.setDaemon(True)
469
def _accept_read_and_reply(self):
472
self._sock.settimeout(5)
474
conn, address = self._sock.accept()
475
# On win32, the accepted connection will be non-blocking to start
476
# with because we're using settimeout.
477
conn.setblocking(True)
478
while not self.received_bytes.endswith(self._expect_body_tail):
479
self.received_bytes += conn.recv(4096)
480
conn.sendall('HTTP/1.1 200 OK\r\n')
481
except socket.timeout:
482
# Make sure the client isn't stuck waiting for us to e.g. accept.
485
# The client may have already closed the socket.
492
# We might have already closed it. We don't care.