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 (
33
from bzrlib.smart import (
33
from bzrlib.smart import protocol
37
34
from bzrlib.tests import http_server
38
from bzrlib.transport import chroot
41
37
class HTTPServerWithSmarts(http_server.HttpServer):
52
48
class SmartRequestHandler(http_server.TestingHTTPRequestHandler):
53
"""Extend TestingHTTPRequestHandler to support smart client POSTs.
55
XXX: This duplicates a fair bit of the logic in bzrlib.transport.http.wsgi.
49
"""Extend TestingHTTPRequestHandler to support smart client POSTs."""
59
52
"""Hand the request off to a smart server instance."""
60
backing = transport.get_transport(
61
self.server.test_case_server._home_dir)
62
chroot_server = chroot.ChrootServer(backing)
63
chroot_server.start_server()
65
t = transport.get_transport(chroot_server.get_url())
68
chroot_server.stop_server()
70
def do_POST_inner(self, chrooted_transport):
71
53
self.send_response(200)
72
54
self.send_header("Content-type", "application/octet-stream")
73
if not self.path.endswith('.bzr/smart'):
75
'POST to path not ending in .bzr/smart: %r' % (self.path,))
76
t = chrooted_transport.clone(self.path[:-len('.bzr/smart')])
77
# if this fails, we should return 400 bad request, but failure is
78
# failure for now - RBC 20060919
79
data_length = int(self.headers['Content-Length'])
55
t = transport.get_transport(self.server.test_case_server._home_dir)
80
56
# TODO: We might like to support streaming responses. 1.0 allows no
81
57
# Content-length in this case, so for integrity we should perform our
82
58
# own chunking within the stream.
84
60
# the HTTP chunking as this will allow HTTP persistence safely, even if
85
61
# we have to stop early due to error, but we would also have to use the
86
62
# HTTP trailer facility which may not be widely available.
87
request_bytes = self.rfile.read(data_length)
88
protocol_factory, unused_bytes = medium._get_protocol_factory_for_bytes(
90
63
out_buffer = StringIO()
91
smart_protocol_request = protocol_factory(t, out_buffer.write, '/')
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'])
92
69
# Perhaps there should be a SmartServerHTTPMedium that takes care of
93
70
# feeding the bytes in the http request to the smart_protocol_request,
94
71
# but for now it's simpler to just feed the bytes directly.
95
smart_protocol_request.accept_bytes(unused_bytes)
96
if not (smart_protocol_request.next_read_size() == 0):
97
raise errors.SmartProtocolError(
98
"not finished reading, but all data sent to protocol.")
72
smart_protocol_request.accept_bytes(self.rfile.read(data_length))
73
assert smart_protocol_request.next_read_size() == 0, (
74
"not finished reading, but all data sent to protocol.")
99
75
self.send_header("Content-Length", str(len(out_buffer.getvalue())))
100
76
self.end_headers()
101
77
self.wfile.write(out_buffer.getvalue())
108
84
one. This will currently fail if the primary transport is not
109
85
backed by regular disk files.
112
# These attributes can be overriden or parametrized by daughter clasess if
113
# needed, but must exist so that the create_transport_readonly_server()
114
# method (or any method creating an http(s) server) can propagate it.
115
_protocol_version = None
116
_url_protocol = 'http'
119
88
super(TestCaseWithWebserver, self).setUp()
120
89
self.transport_readonly_server = http_server.HttpServer
122
def create_transport_readonly_server(self):
123
server = self.transport_readonly_server(
124
protocol_version=self._protocol_version)
125
server._url_protocol = self._url_protocol
129
92
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
130
93
"""A support class providing readonly urls on two servers that are http://.
143
106
This is mostly a hook for daughter classes.
145
server = self.transport_secondary_server(
146
protocol_version=self._protocol_version)
147
server._url_protocol = self._url_protocol
108
return self.transport_secondary_server()
150
110
def get_secondary_server(self):
151
111
"""Get the server instance for the secondary transport."""
152
112
if self.__secondary_server is None:
153
113
self.__secondary_server = self.create_transport_secondary_server()
154
self.start_server(self.__secondary_server)
114
self.__secondary_server.setUp()
115
self.addCleanup(self.__secondary_server.tearDown)
155
116
return self.__secondary_server
157
def get_secondary_url(self, relpath=None):
158
base = self.get_secondary_server().get_url()
159
return self._adjust_url(base, relpath)
161
def get_secondary_transport(self, relpath=None):
162
t = transport.get_transport(self.get_secondary_url(relpath))
163
self.assertTrue(t.is_readonly())
167
119
class ProxyServer(http_server.HttpServer):
168
120
"""A proxy test server for http transports."""
242
194
The 'old' server is redirected to the 'new' server.
197
def create_transport_secondary_server(self):
198
"""Create the secondary server redirecting to the primary server"""
199
new = self.get_readonly_server()
200
redirecting = HTTPServerRedirecting()
201
redirecting.redirect_to(new.host, new.port)
246
205
super(TestCaseWithRedirectedWebserver, self).setUp()
247
206
# The redirections will point to the new server
248
207
self.new_server = self.get_readonly_server()
249
# The requests to the old server will be redirected to the new server
208
# The requests to the old server will be redirected
250
209
self.old_server = self.get_secondary_server()
252
def create_transport_secondary_server(self):
253
"""Create the secondary server redirecting to the primary server"""
254
new = self.get_readonly_server()
255
redirecting = HTTPServerRedirecting(
256
protocol_version=self._protocol_version)
257
redirecting.redirect_to(new.host, new.port)
258
redirecting._url_protocol = self._url_protocol
261
def get_old_url(self, relpath=None):
262
base = self.old_server.get_url()
263
return self._adjust_url(base, relpath)
265
def get_old_transport(self, relpath=None):
266
t = transport.get_transport(self.get_old_url(relpath))
267
self.assertTrue(t.is_readonly())
270
def get_new_url(self, relpath=None):
271
base = self.new_server.get_url()
272
return self._adjust_url(base, relpath)
274
def get_new_transport(self, relpath=None):
275
t = transport.get_transport(self.get_new_url(relpath))
276
self.assertTrue(t.is_readonly())
280
212
class AuthRequestHandler(http_server.TestingHTTPRequestHandler):
281
213
"""Requires an authentication to process requests.
363
297
self.send_header(tcs.auth_header_sent,header)
366
class DigestAndBasicAuthRequestHandler(DigestAuthRequestHandler):
367
"""Implements a digest and basic authentication of a request.
369
I.e. the server proposes both schemes and the client should choose the best
370
one it can handle, which, in that case, should be digest, the only scheme
374
def send_header_auth_reqed(self):
375
tcs = self.server.test_case_server
376
self.send_header(tcs.auth_header_sent,
377
'Basic realm="%s"' % tcs.auth_realm)
378
header = 'Digest realm="%s", ' % tcs.auth_realm
379
header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
381
self.send_header(tcs.auth_header_sent,header)
384
300
class AuthServer(http_server.HttpServer):
385
301
"""Extends HttpServer with a dictionary of passwords.
459
375
A1 = '%s:%s:%s' % (user, realm, password)
460
376
A2 = '%s:%s' % (command, auth['uri'])
462
H = lambda x: osutils.md5(x).hexdigest()
378
H = lambda x: md5.new(x).hexdigest()
463
379
KD = lambda secret, data: H("%s:%s" % (secret, data))
465
381
nonce_count = int(auth['nc'], 16)
510
425
self.init_http_auth()
513
class HTTPBasicAndDigestAuthServer(DigestAuthServer, HTTPAuthServer):
514
"""An HTTP server requiring basic or digest authentication"""
516
def __init__(self, protocol_version=None):
517
DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
519
protocol_version=protocol_version)
520
self.init_http_auth()
521
# We really accept Digest only
522
self.auth_scheme = 'digest'
525
428
class ProxyBasicAuthServer(ProxyAuthServer):
526
429
"""A proxy server requiring basic authentication"""
540
443
self.init_proxy_auth()
543
class ProxyBasicAndDigestAuthServer(DigestAuthServer, ProxyAuthServer):
544
"""An proxy server requiring basic or digest authentication"""
546
def __init__(self, protocol_version=None):
547
DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
549
protocol_version=protocol_version)
550
self.init_proxy_auth()
551
# We really accept Digest only
552
self.auth_scheme = 'digest'