1
# Copyright (C) 2006-2011 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
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
23
import SimpleHTTPServer
31
from . import test_server
34
class BadWebserverPath(ValueError):
36
return 'path %s is not in %s' % self.args
39
class TestingHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
40
"""Handles one request.
42
A TestingHTTPRequestHandler is instantiated for every request received by
43
the associated server. Note that 'request' here is inherited from the base
44
TCPServer class, for the HTTP server it is really a connection which itself
45
will handle one or several HTTP requests.
47
# Default protocol version
48
protocol_version = 'HTTP/1.1'
50
# The Message-like class used to parse the request headers
51
MessageClass = httplib.HTTPMessage
54
SimpleHTTPServer.SimpleHTTPRequestHandler.setup(self)
55
self._cwd = self.server._home_dir
56
tcs = self.server.test_case_server
57
if tcs.protocol_version is not None:
58
# If the test server forced a protocol version, use it
59
self.protocol_version = tcs.protocol_version
61
def log_message(self, format, *args):
62
tcs = self.server.test_case_server
63
tcs.log('webserver - %s - - [%s] %s "%s" "%s"',
64
self.address_string(),
65
self.log_date_time_string(),
67
self.headers.get('referer', '-'),
68
self.headers.get('user-agent', '-'))
70
def handle_one_request(self):
71
"""Handle a single HTTP request.
73
We catch all socket errors occurring when the client close the
74
connection early to avoid polluting the test results.
77
self._handle_one_request()
78
except socket.error as e:
79
# Any socket error should close the connection, but some errors are
80
# due to the client closing early and we don't want to pollute test
81
# results, so we raise only the others.
82
self.close_connection = 1
84
or e.args[0] not in (errno.EPIPE, errno.ECONNRESET,
85
errno.ECONNABORTED, errno.EBADF)):
88
error_content_type = 'text/plain'
89
error_message_format = '''\
94
def send_error(self, code, message=None):
95
"""Send and log an error reply.
97
We redefine the python-provided version to be able to set a
98
``Content-Length`` header as some http/1.1 clients complain otherwise
101
:param code: The HTTP error code.
103
:param message: The explanation of the error code, Defaults to a short
109
message = self.responses[code][0]
112
self.log_error("code %d, message %s", code, message)
113
content = (self.error_message_format %
114
{'code': code, 'message': message})
115
self.send_response(code, message)
116
self.send_header("Content-Type", self.error_content_type)
117
self.send_header("Content-Length", "%d" % len(content))
118
self.send_header('Connection', 'close')
120
if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
121
self.wfile.write(content)
123
def _handle_one_request(self):
124
SimpleHTTPServer.SimpleHTTPRequestHandler.handle_one_request(self)
126
_range_regexp = re.compile(r'^(?P<start>\d+)-(?P<end>\d+)?$')
127
_tail_regexp = re.compile(r'^-(?P<tail>\d+)$')
129
def _parse_ranges(self, ranges_header, file_size):
130
"""Parse the range header value and returns ranges.
132
RFC2616 14.35 says that syntactically invalid range specifiers MUST be
133
ignored. In that case, we return None instead of a range list.
135
:param ranges_header: The 'Range' header value.
137
:param file_size: The size of the requested file.
139
:return: A list of (start, end) tuples or None if some invalid range
140
specifier is encountered.
142
if not ranges_header.startswith('bytes='):
143
# Syntactically invalid header
148
ranges_header = ranges_header[len('bytes='):]
149
for range_str in ranges_header.split(','):
150
range_match = self._range_regexp.match(range_str)
151
if range_match is not None:
152
start = int(range_match.group('start'))
153
end_match = range_match.group('end')
154
if end_match is None:
155
# RFC2616 says end is optional and default to file_size
160
# Syntactically invalid range
162
ranges.append((start, end))
164
tail_match = self._tail_regexp.match(range_str)
165
if tail_match is not None:
166
tail = int(tail_match.group('tail'))
168
# Syntactically invalid range
171
# Normalize tail into ranges
172
ranges.append((max(0, file_size - tail), file_size))
175
for start, end in ranges:
176
if start >= file_size:
177
# RFC2616 14.35, ranges are invalid if start >= file_size
179
# RFC2616 14.35, end values should be truncated
180
# to file_size -1 if they exceed it
181
end = min(end, file_size - 1)
182
checked_ranges.append((start, end))
183
return checked_ranges
185
def _header_line_length(self, keyword, value):
186
header_line = '%s: %s\r\n' % (keyword, value)
187
return len(header_line)
190
"""Overrides base implementation to work around a bug in python2.5."""
191
path = self.translate_path(self.path)
192
if os.path.isdir(path) and not self.path.endswith('/'):
193
# redirect browser - doing basically what apache does when
194
# DirectorySlash option is On which is quite common (braindead, but
196
self.send_response(301)
197
self.send_header("Location", self.path + "/")
198
# Indicates that the body is empty for HTTP/1.1 clients
199
self.send_header('Content-Length', '0')
203
return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
205
def send_range_content(self, file, start, length):
207
self.wfile.write(file.read(length))
209
def get_single_range(self, file, file_size, start, end):
210
self.send_response(206)
211
length = end - start + 1
212
self.send_header('Accept-Ranges', 'bytes')
213
self.send_header("Content-Length", "%d" % length)
215
self.send_header("Content-Type", 'application/octet-stream')
216
self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
220
self.send_range_content(file, start, length)
222
def get_multiple_ranges(self, file, file_size, ranges):
223
self.send_response(206)
224
self.send_header('Accept-Ranges', 'bytes')
225
boundary = '%d' % random.randint(0,0x7FFFFFFF)
226
self.send_header('Content-Type',
227
'multipart/byteranges; boundary=%s' % boundary)
228
boundary_line = '--%s\r\n' % boundary
229
# Calculate the Content-Length
231
for (start, end) in ranges:
232
content_length += len(boundary_line)
233
content_length += self._header_line_length(
234
'Content-type', 'application/octet-stream')
235
content_length += self._header_line_length(
236
'Content-Range', 'bytes %d-%d/%d' % (start, end, file_size))
237
content_length += len('\r\n') # end headers
238
content_length += end - start + 1
239
content_length += len(boundary_line)
240
self.send_header('Content-length', content_length)
243
# Send the multipart body
244
for (start, end) in ranges:
245
self.wfile.write(boundary_line)
246
self.send_header('Content-type', 'application/octet-stream')
247
self.send_header('Content-Range', 'bytes %d-%d/%d'
248
% (start, end, file_size))
250
self.send_range_content(file, start, end - start + 1)
252
self.wfile.write(boundary_line)
255
"""Serve a GET request.
257
Handles the Range header.
260
self.server.test_case_server.GET_request_nb += 1
262
path = self.translate_path(self.path)
263
ranges_header_value = self.headers.get('Range')
264
if ranges_header_value is None or os.path.isdir(path):
265
# Let the mother class handle most cases
266
return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
269
# Always read in binary mode. Opening files in text
270
# mode may cause newline translations, making the
271
# actual size of the content transmitted *less* than
272
# the content-length!
275
self.send_error(404, "File not found")
278
file_size = os.fstat(f.fileno())[6]
279
ranges = self._parse_ranges(ranges_header_value, file_size)
281
# RFC2616 14.16 and 14.35 says that when a server
282
# encounters unsatisfiable range specifiers, it
283
# SHOULD return a 416.
285
# FIXME: We SHOULD send a Content-Range header too,
286
# but the implementation of send_error does not
287
# allows that. So far.
288
self.send_error(416, "Requested range not satisfiable")
292
(start, end) = ranges[0]
293
self.get_single_range(f, file_size, start, end)
295
self.get_multiple_ranges(f, file_size, ranges)
298
def translate_path(self, path):
299
"""Translate a /-separated PATH to the local filename syntax.
301
If the server requires it, proxy the path before the usual translation
303
if self.server.test_case_server.proxy_requests:
304
# We need to act as a proxy and accept absolute urls,
305
# which SimpleHTTPRequestHandler (parent) is not
306
# ready for. So we just drop the protocol://host:port
307
# part in front of the request-url (because we know
308
# we would not forward the request to *another*
311
# So we do what SimpleHTTPRequestHandler.translate_path
312
# do beginning with python 2.4.3: abandon query
313
# parameters, scheme, host port, etc (which ensure we
314
# provide the right behaviour on all python versions).
315
path = urlparse.urlparse(path)[2]
316
# And now, we can apply *our* trick to proxy files
319
return self._translate_path(path)
321
def _translate_path(self, path):
322
"""Translate a /-separated PATH to the local filename syntax.
324
Note that we're translating http URLs here, not file URLs.
325
The URL root location is the server's startup directory.
326
Components that mean special things to the local file system
327
(e.g. drive or directory names) are ignored. (XXX They should
328
probably be diagnosed.)
330
Override from python standard library to stop it calling os.getcwd()
332
# abandon query parameters
333
path = urlparse.urlparse(path)[2]
334
path = posixpath.normpath(urlutils.unquote(path))
335
path = path.decode('utf-8')
336
words = path.split('/')
338
for num, word in enumerate(w for w in words if w):
340
drive, word = os.path.splitdrive(word)
341
head, word = os.path.split(word)
342
if word in (os.curdir, os.pardir): continue
343
path = os.path.join(path, word)
347
class TestingHTTPServerMixin:
349
def __init__(self, test_case_server):
350
# test_case_server can be used to communicate between the
351
# tests and the server (or the request handler and the
352
# server), allowing dynamic behaviors to be defined from
354
self.test_case_server = test_case_server
355
self._home_dir = test_case_server._home_dir
358
class TestingHTTPServer(test_server.TestingTCPServer, TestingHTTPServerMixin):
360
def __init__(self, server_address, request_handler_class,
362
test_server.TestingTCPServer.__init__(self, server_address,
363
request_handler_class)
364
TestingHTTPServerMixin.__init__(self, test_case_server)
367
class TestingThreadingHTTPServer(test_server.TestingThreadingTCPServer,
368
TestingHTTPServerMixin):
369
"""A threading HTTP test server for HTTP 1.1.
371
Since tests can initiate several concurrent connections to the same http
372
server, we need an independent connection for each of them. We achieve that
373
by spawning a new thread for each connection.
375
def __init__(self, server_address, request_handler_class,
377
test_server.TestingThreadingTCPServer.__init__(self, server_address,
378
request_handler_class)
379
TestingHTTPServerMixin.__init__(self, test_case_server)
382
class HttpServer(test_server.TestingTCPServerInAThread):
383
"""A test server for http transports.
385
Subclasses can provide a specific request handler.
388
# The real servers depending on the protocol
389
http_server_class = {'HTTP/1.0': TestingHTTPServer,
390
'HTTP/1.1': TestingThreadingHTTPServer,
393
# Whether or not we proxy the requests (see
394
# TestingHTTPRequestHandler.translate_path).
395
proxy_requests = False
397
# used to form the url that connects to this server
398
_url_protocol = 'http'
400
def __init__(self, request_handler=TestingHTTPRequestHandler,
401
protocol_version=None):
404
:param request_handler: a class that will be instantiated to handle an
405
http connection (one or several requests).
407
:param protocol_version: if specified, will override the protocol
408
version of the request handler.
410
# Depending on the protocol version, we will create the approriate
412
if protocol_version is None:
413
# Use the request handler one
414
proto_vers = request_handler.protocol_version
416
# Use our own, it will be used to override the request handler
418
proto_vers = protocol_version
419
# Get the appropriate server class for the required protocol
420
serv_cls = self.http_server_class.get(proto_vers, None)
422
raise httplib.UnknownProtocol(proto_vers)
423
self.host = 'localhost'
425
super(HttpServer, self).__init__((self.host, self.port),
428
self.protocol_version = proto_vers
429
# Allows tests to verify number of GET requests issued
430
self.GET_request_nb = 0
431
self._http_base_url = None
434
def create_server(self):
435
return self.server_class(
436
(self.host, self.port), self.request_handler_class, self)
438
def _get_remote_url(self, path):
439
path_parts = path.split(os.path.sep)
440
if os.path.isabs(path):
441
if path_parts[:len(self._local_path_parts)] != \
442
self._local_path_parts:
443
raise BadWebserverPath(path, self.test_dir)
444
remote_path = '/'.join(path_parts[len(self._local_path_parts):])
446
remote_path = '/'.join(path_parts)
448
return self._http_base_url + remote_path
450
def log(self, format, *args):
451
"""Capture Server log output."""
452
self.logs.append(format % args)
454
def start_server(self, backing_transport_server=None):
455
"""See breezy.transport.Server.start_server.
457
:param backing_transport_server: The transport that requests over this
458
protocol should be forwarded to. Note that this is currently not
461
# XXX: TODO: make the server back onto vfs_server rather than local
463
if not (backing_transport_server is None
464
or isinstance(backing_transport_server,
465
test_server.LocalURLServer)):
466
raise AssertionError(
467
"HTTPServer currently assumes local transport, got %s" %
468
backing_transport_server)
469
self._home_dir = osutils.getcwd()
470
self._local_path_parts = self._home_dir.split(os.path.sep)
473
super(HttpServer, self).start_server()
474
self._http_base_url = '%s://%s:%s/' % (
475
self._url_protocol, self.host, self.port)
478
"""See breezy.transport.Server.get_url."""
479
return self._get_remote_url(self._home_dir)
481
def get_bogus_url(self):
482
"""See breezy.transport.Server.get_bogus_url."""
483
# this is chosen to try to prevent trouble with proxies, weird dns,
485
return self._url_protocol + '://127.0.0.1:1/'
488
class HttpServer_urllib(HttpServer):
489
"""Subclass of HttpServer that gives http+urllib urls.
491
This is for use in testing: connections to this server will always go
492
through urllib where possible.
495
# urls returned by this server should require the urllib client impl
496
_url_protocol = 'http+urllib'