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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 (
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):
40
class HTTPServerWithSmarts(http_server.HttpServer):
112
41
"""HTTPServerWithSmarts extends the HttpServer with POST methods that will
113
42
trigger a smart server to execute with a transport rooted at the rootdir of
118
HttpServer.__init__(self, SmartRequestHandler)
121
class SmartRequestHandler(TestingHTTPRequestHandler):
122
"""Extend TestingHTTPRequestHandler to support smart client POSTs."""
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.
124
57
def do_POST(self):
125
58
"""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)
63
t = get_transport(chroot_server.get_url())
66
chroot_server.tearDown()
68
def do_POST_inner(self, chrooted_transport):
126
69
self.send_response(200)
127
70
self.send_header("Content-type", "application/octet-stream")
128
transport = get_transport(self.server.test_case_server._home_dir)
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'])
129
78
# TODO: We might like to support streaming responses. 1.0 allows no
130
79
# Content-length in this case, so for integrity we should perform our
131
80
# own chunking within the stream.
133
82
# the HTTP chunking as this will allow HTTP persistence safely, even if
134
83
# we have to stop early due to error, but we would also have to use the
135
84
# 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(
136
88
out_buffer = StringIO()
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'])
89
smart_protocol_request = protocol_factory(t, out_buffer.write, '/')
142
90
# Perhaps there should be a SmartServerHTTPMedium that takes care of
143
91
# feeding the bytes in the http request to the smart_protocol_request,
144
92
# but for now it's simpler to just feed the bytes directly.
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.")
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.")
148
97
self.send_header("Content-Length", str(len(out_buffer.getvalue())))
149
98
self.end_headers()
150
99
self.wfile.write(out_buffer.getvalue())
153
class LimitedRangeRequestHandler(TestingHTTPRequestHandler):
154
"""Errors out when range specifiers exceed the limit"""
156
def get_multiple_ranges(self, file, file_size, ranges):
157
"""Refuses the multiple ranges request"""
158
tcs = self.server.test_case_server
159
if tcs.range_limit is not None and len(ranges) > tcs.range_limit:
161
# Emulate apache behavior
162
self.send_error(400, "Bad Request")
164
return TestingHTTPRequestHandler.get_multiple_ranges(self, file,
168
tcs = self.server.test_case_server
169
tcs.GET_request_nb += 1
170
return TestingHTTPRequestHandler.do_GET(self)
173
class LimitedRangeHTTPServer(HttpServer):
174
"""An HttpServer erroring out on requests with too much range specifiers"""
176
def __init__(self, request_handler=LimitedRangeRequestHandler,
178
HttpServer.__init__(self, request_handler)
179
self.range_limit = range_limit
180
self.GET_request_nb = 0
183
class SingleRangeRequestHandler(TestingHTTPRequestHandler):
184
"""Always reply to range request as if they were single.
186
Don't be explicit about it, just to annoy the clients.
189
def get_multiple_ranges(self, file, file_size, ranges):
190
"""Answer as if it was a single range request and ignores the rest"""
191
(start, end) = ranges[0]
192
return self.get_single_range(file, file_size, start, end)
195
class SingleOnlyRangeRequestHandler(TestingHTTPRequestHandler):
196
"""Only reply to simple range requests, errors out on multiple"""
198
def get_multiple_ranges(self, file, file_size, ranges):
199
"""Refuses the multiple ranges request"""
202
self.send_error(416, "Requested range not satisfiable")
204
(start, end) = ranges[0]
205
return self.get_single_range(file, file_size, start, end)
208
class NoRangeRequestHandler(TestingHTTPRequestHandler):
209
"""Ignore range requests without notice"""
211
# Just bypass the range handling done by TestingHTTPRequestHandler
212
do_GET = SimpleHTTPRequestHandler.do_GET
215
class TestCaseWithWebserver(TestCaseWithTransport):
102
class TestCaseWithWebserver(tests.TestCaseWithTransport):
216
103
"""A support class that provides readonly urls that are http://.
218
105
This is done by forcing the readonly server to be an http
426
317
self.send_header(tcs.auth_header_sent,header)
429
class AuthServer(HttpServer):
320
class DigestAndBasicAuthRequestHandler(DigestAuthRequestHandler):
321
"""Implements a digest and basic authentication of a request.
323
I.e. the server proposes both schemes and the client should choose the best
324
one it can handle, which, in that case, should be digest, the only scheme
328
def send_header_auth_reqed(self):
329
tcs = self.server.test_case_server
330
self.send_header(tcs.auth_header_sent,
331
'Basic realm="%s"' % tcs.auth_realm)
332
header = 'Digest realm="%s", ' % tcs.auth_realm
333
header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
335
self.send_header(tcs.auth_header_sent,header)
338
class AuthServer(http_server.HttpServer):
430
339
"""Extends HttpServer with a dictionary of passwords.
432
341
This is used as a base class for various schemes which should
535
449
class HTTPBasicAuthServer(HTTPAuthServer):
536
450
"""An HTTP server requiring basic authentication"""
539
HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
452
def __init__(self, protocol_version=None):
453
HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
454
protocol_version=protocol_version)
540
455
self.init_http_auth()
543
458
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
544
459
"""An HTTP server requiring digest authentication"""
547
DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
548
self.init_http_auth()
461
def __init__(self, protocol_version=None):
462
DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
463
protocol_version=protocol_version)
464
self.init_http_auth()
467
class HTTPBasicAndDigestAuthServer(DigestAuthServer, HTTPAuthServer):
468
"""An HTTP server requiring basic or digest authentication"""
470
def __init__(self, protocol_version=None):
471
DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
473
protocol_version=protocol_version)
474
self.init_http_auth()
475
# We really accept Digest only
476
self.auth_scheme = 'digest'
551
479
class ProxyBasicAuthServer(ProxyAuthServer):
552
480
"""A proxy server requiring basic authentication"""
555
ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
482
def __init__(self, protocol_version=None):
483
ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
484
protocol_version=protocol_version)
556
485
self.init_proxy_auth()
559
488
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
560
489
"""A proxy server requiring basic authentication"""
563
ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
564
self.init_proxy_auth()
491
def __init__(self, protocol_version=None):
492
ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
493
protocol_version=protocol_version)
494
self.init_proxy_auth()
497
class ProxyBasicAndDigestAuthServer(DigestAuthServer, ProxyAuthServer):
498
"""An proxy server requiring basic or digest authentication"""
500
def __init__(self, protocol_version=None):
501
DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
503
protocol_version=protocol_version)
504
self.init_proxy_auth()
505
# We really accept Digest only
506
self.auth_scheme = 'digest'