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
20
from SimpleHTTPServer import SimpleHTTPRequestHandler
28
from bzrlib.smart import protocol
29
from bzrlib.tests import TestCaseWithTransport
30
from bzrlib.tests.HttpServer import (
32
TestingHTTPRequestHandler,
32
from bzrlib.smart import medium, protocol
33
from bzrlib.tests import http_server
34
34
from bzrlib.transport import (
40
class HTTPServerWithSmarts(http_server.HttpServer):
39
class WallRequestHandler(TestingHTTPRequestHandler):
40
"""Whatever request comes in, close the connection"""
42
def handle_one_request(self):
43
"""Handle a single HTTP request, by abruptly closing the connection"""
44
self.close_connection = 1
47
class BadStatusRequestHandler(TestingHTTPRequestHandler):
48
"""Whatever request comes in, returns a bad status"""
50
def parse_request(self):
51
"""Fakes handling a single HTTP request, returns a bad status"""
52
ignored = TestingHTTPRequestHandler.parse_request(self)
54
self.send_response(0, "Bad status")
56
except socket.error, e:
57
# We don't want to pollute the test results with
58
# spurious server errors while test succeed. In our
59
# case, it may occur that the test has already read
60
# the 'Bad Status' and closed the socket while we are
61
# still trying to send some headers... So the test is
62
# ok, but if we raise the exception, the output is
63
# dirty. So we don't raise, but we close the
64
# connection, just to be safe :)
65
spurious = [errno.EPIPE,
69
if (len(e.args) > 0) and (e.args[0] in spurious):
70
self.close_connection = 1
77
class InvalidStatusRequestHandler(TestingHTTPRequestHandler):
78
"""Whatever request comes in, returns am invalid status"""
80
def parse_request(self):
81
"""Fakes handling a single HTTP request, returns a bad status"""
82
ignored = TestingHTTPRequestHandler.parse_request(self)
83
self.wfile.write("Invalid status line\r\n")
87
class BadProtocolRequestHandler(TestingHTTPRequestHandler):
88
"""Whatever request comes in, returns a bad protocol version"""
90
def parse_request(self):
91
"""Fakes handling a single HTTP request, returns a bad status"""
92
ignored = TestingHTTPRequestHandler.parse_request(self)
93
# Returns an invalid protocol version, but curl just
94
# ignores it and those cannot be tested.
95
self.wfile.write("%s %d %s\r\n" % ('HTTP/0.0',
97
'Look at my protocol version'))
101
class ForbiddenRequestHandler(TestingHTTPRequestHandler):
102
"""Whatever request comes in, returns a 403 code"""
104
def parse_request(self):
105
"""Handle a single HTTP request, by replying we cannot handle it"""
106
ignored = TestingHTTPRequestHandler.parse_request(self)
111
class HTTPServerWithSmarts(HttpServer):
41
112
"""HTTPServerWithSmarts extends the HttpServer with POST methods that will
42
113
trigger a smart server to execute with a transport rooted at the rootdir of
46
def __init__(self, protocol_version=None):
47
http_server.HttpServer.__init__(self, SmartRequestHandler,
48
protocol_version=protocol_version)
51
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.
118
HttpServer.__init__(self, SmartRequestHandler)
121
class SmartRequestHandler(TestingHTTPRequestHandler):
122
"""Extend TestingHTTPRequestHandler to support smart client POSTs."""
57
124
def do_POST(self):
58
125
"""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
126
self.send_response(200)
70
127
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'])
128
transport = get_transport(self.server.test_case_server._home_dir)
78
129
# TODO: We might like to support streaming responses. 1.0 allows no
79
130
# Content-length in this case, so for integrity we should perform our
80
131
# own chunking within the stream.
82
133
# the HTTP chunking as this will allow HTTP persistence safely, even if
83
134
# we have to stop early due to error, but we would also have to use the
84
135
# 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
136
out_buffer = StringIO()
89
smart_protocol_request = protocol_factory(t, out_buffer.write, '/')
137
smart_protocol_request = protocol.SmartServerRequestProtocolOne(
138
transport, out_buffer.write)
139
# if this fails, we should return 400 bad request, but failure is
140
# failure for now - RBC 20060919
141
data_length = int(self.headers['Content-Length'])
90
142
# Perhaps there should be a SmartServerHTTPMedium that takes care of
91
143
# feeding the bytes in the http request to the smart_protocol_request,
92
144
# 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.")
145
smart_protocol_request.accept_bytes(self.rfile.read(data_length))
146
assert smart_protocol_request.next_read_size() == 0, (
147
"not finished reading, but all data sent to protocol.")
97
148
self.send_header("Content-Length", str(len(out_buffer.getvalue())))
98
149
self.end_headers()
99
150
self.wfile.write(out_buffer.getvalue())
102
class TestCaseWithWebserver(tests.TestCaseWithTransport):
153
class SingleRangeRequestHandler(TestingHTTPRequestHandler):
154
"""Always reply to range request as if they were single.
156
Don't be explicit about it, just to annoy the clients.
159
def get_multiple_ranges(self, file, file_size, ranges):
160
"""Answer as if it was a single range request and ignores the rest"""
161
(start, end) = ranges[0]
162
return self.get_single_range(file, file_size, start, end)
165
class SingleOnlyRangeRequestHandler(TestingHTTPRequestHandler):
166
"""Only reply to simple range requests, errors out on multiple"""
168
def get_multiple_ranges(self, file, file_size, ranges):
169
"""Refuses the multiple ranges request"""
172
self.send_error(416, "Requested range not satisfiable")
174
(start, end) = ranges[0]
175
return self.get_single_range(file, file_size, start, end)
178
class NoRangeRequestHandler(TestingHTTPRequestHandler):
179
"""Ignore range requests without notice"""
181
# Just bypass the range handling done by TestingHTTPRequestHandler
182
do_GET = SimpleHTTPRequestHandler.do_GET
185
class TestCaseWithWebserver(TestCaseWithTransport):
103
186
"""A support class that provides readonly urls that are http://.
105
188
This is done by forcing the readonly server to be an http
106
189
one. This will currently fail if the primary transport is not
107
190
backed by regular disk files.
110
# This can be overriden or parametrized by daughter clasess if needed, but
111
# it must exist so that the create_transport_readonly_server() method can
113
_protocol_version = None
116
193
super(TestCaseWithWebserver, self).setUp()
117
self.transport_readonly_server = http_server.HttpServer
119
def create_transport_readonly_server(self):
120
return self.transport_readonly_server(
121
protocol_version=self._protocol_version)
194
self.transport_readonly_server = HttpServer
124
197
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
138
211
This is mostly a hook for daughter classes.
140
return self.transport_secondary_server(
141
protocol_version=self._protocol_version)
213
return self.transport_secondary_server()
143
215
def get_secondary_server(self):
144
216
"""Get the server instance for the secondary transport."""
145
217
if self.__secondary_server is None:
146
218
self.__secondary_server = self.create_transport_secondary_server()
147
self.start_server(self.__secondary_server)
219
self.__secondary_server.setUp()
220
self.addCleanup(self.__secondary_server.tearDown)
148
221
return self.__secondary_server
151
class ProxyServer(http_server.HttpServer):
224
class ProxyServer(HttpServer):
152
225
"""A proxy test server for http transports."""
154
227
proxy_requests = True
157
class RedirectRequestHandler(http_server.TestingHTTPRequestHandler):
230
class RedirectRequestHandler(TestingHTTPRequestHandler):
158
231
"""Redirect all request to the specified server"""
160
233
def parse_request(self):
161
234
"""Redirect a single HTTP request to another host"""
162
valid = http_server.TestingHTTPRequestHandler.parse_request(self)
235
valid = TestingHTTPRequestHandler.parse_request(self)
164
237
tcs = self.server.test_case_server
165
238
code, target = tcs.is_redirected(self.path)
323
393
def send_header_auth_reqed(self):
324
394
tcs = self.server.test_case_server
325
395
header = 'Digest realm="%s", ' % tcs.auth_realm
326
header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
328
self.send_header(tcs.auth_header_sent,header)
331
class DigestAndBasicAuthRequestHandler(DigestAuthRequestHandler):
332
"""Implements a digest and basic authentication of a request.
334
I.e. the server proposes both schemes and the client should choose the best
335
one it can handle, which, in that case, should be digest, the only scheme
339
def send_header_auth_reqed(self):
340
tcs = self.server.test_case_server
341
self.send_header(tcs.auth_header_sent,
342
'Basic realm="%s"' % tcs.auth_realm)
343
header = 'Digest realm="%s", ' % tcs.auth_realm
344
header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
346
self.send_header(tcs.auth_header_sent,header)
349
class AuthServer(http_server.HttpServer):
396
header += 'nonce="%s", algorithm=%s, qop=auth' % (tcs.auth_nonce, 'MD5')
397
self.send_header(tcs.auth_header_sent,header)
400
class AuthServer(HttpServer):
350
401
"""Extends HttpServer with a dictionary of passwords.
352
403
This is used as a base class for various schemes which should
460
506
class HTTPBasicAuthServer(HTTPAuthServer):
461
507
"""An HTTP server requiring basic authentication"""
463
def __init__(self, protocol_version=None):
464
HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
465
protocol_version=protocol_version)
510
HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
466
511
self.init_http_auth()
469
514
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
470
515
"""An HTTP server requiring digest authentication"""
472
def __init__(self, protocol_version=None):
473
DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
474
protocol_version=protocol_version)
475
self.init_http_auth()
478
class HTTPBasicAndDigestAuthServer(DigestAuthServer, HTTPAuthServer):
479
"""An HTTP server requiring basic or digest authentication"""
481
def __init__(self, protocol_version=None):
482
DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
484
protocol_version=protocol_version)
485
self.init_http_auth()
486
# We really accept Digest only
487
self.auth_scheme = 'digest'
518
DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
519
self.init_http_auth()
490
522
class ProxyBasicAuthServer(ProxyAuthServer):
491
523
"""A proxy server requiring basic authentication"""
493
def __init__(self, protocol_version=None):
494
ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
495
protocol_version=protocol_version)
526
ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
496
527
self.init_proxy_auth()
499
530
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
500
531
"""A proxy server requiring basic authentication"""
502
def __init__(self, protocol_version=None):
503
ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
504
protocol_version=protocol_version)
505
self.init_proxy_auth()
508
class ProxyBasicAndDigestAuthServer(DigestAuthServer, ProxyAuthServer):
509
"""An proxy server requiring basic or digest authentication"""
511
def __init__(self, protocol_version=None):
512
DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
514
protocol_version=protocol_version)
515
self.init_proxy_auth()
516
# We really accept Digest only
517
self.auth_scheme = 'digest'
534
ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
535
self.init_proxy_auth()