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
18
import http.client as http_client
19
import http.server as http_server
26
from urllib.parse import urlparse
32
from . import test_server
35
class BadWebserverPath(ValueError):
37
return 'path %s is not in %s' % self.args
40
class TestingHTTPRequestHandler(http_server.SimpleHTTPRequestHandler):
41
"""Handles one request.
43
A TestingHTTPRequestHandler is instantiated for every request received by
44
the associated server. Note that 'request' here is inherited from the base
45
TCPServer class, for the HTTP server it is really a connection which itself
46
will handle one or several HTTP requests.
48
# Default protocol version
49
protocol_version = 'HTTP/1.1'
51
# The Message-like class used to parse the request headers
52
MessageClass = http_client.HTTPMessage
55
http_server.SimpleHTTPRequestHandler.setup(self)
56
self._cwd = self.server._home_dir
57
tcs = self.server.test_case_server
58
if tcs.protocol_version is not None:
59
# If the test server forced a protocol version, use it
60
self.protocol_version = tcs.protocol_version
62
def log_message(self, format, *args):
63
tcs = self.server.test_case_server
64
tcs.log('webserver - %s - - [%s] %s "%s" "%s"',
65
self.address_string(),
66
self.log_date_time_string(),
68
self.headers.get('referer', '-'),
69
self.headers.get('user-agent', '-'))
71
def handle_one_request(self):
72
"""Handle a single HTTP request.
74
We catch all socket errors occurring when the client close the
75
connection early to avoid polluting the test results.
78
self._handle_one_request()
79
except socket.error as e:
80
# Any socket error should close the connection, but some errors are
81
# due to the client closing early and we don't want to pollute test
82
# results, so we raise only the others.
83
self.close_connection = 1
85
or e.args[0] not in (errno.EPIPE, errno.ECONNRESET,
86
errno.ECONNABORTED, errno.EBADF)):
89
error_content_type = 'text/plain'
90
error_message_format = '''\
95
def send_error(self, code, message=None):
96
"""Send and log an error reply.
98
We redefine the python-provided version to be able to set a
99
``Content-Length`` header as some http/1.1 clients complain otherwise
102
:param code: The HTTP error code.
104
:param message: The explanation of the error code, Defaults to a short
110
message = self.responses[code][0]
113
self.log_error("code %d, message %s", code, message)
114
content = (self.error_message_format %
115
{'code': code, 'message': message})
116
self.send_response(code, message)
117
self.send_header("Content-Type", self.error_content_type)
118
self.send_header("Content-Length", "%d" % len(content))
119
self.send_header('Connection', 'close')
121
if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
122
self.wfile.write(content.encode('utf-8'))
124
def _handle_one_request(self):
125
http_server.SimpleHTTPRequestHandler.handle_one_request(self)
127
_range_regexp = re.compile(r'^(?P<start>\d+)-(?P<end>\d+)?$')
128
_tail_regexp = re.compile(r'^-(?P<tail>\d+)$')
130
def _parse_ranges(self, ranges_header, file_size):
131
"""Parse the range header value and returns ranges.
133
RFC2616 14.35 says that syntactically invalid range specifiers MUST be
134
ignored. In that case, we return None instead of a range list.
136
:param ranges_header: The 'Range' header value.
138
:param file_size: The size of the requested file.
140
:return: A list of (start, end) tuples or None if some invalid range
141
specifier is encountered.
143
if not ranges_header.startswith('bytes='):
144
# Syntactically invalid header
149
ranges_header = ranges_header[len('bytes='):]
150
for range_str in ranges_header.split(','):
151
range_match = self._range_regexp.match(range_str)
152
if range_match is not None:
153
start = int(range_match.group('start'))
154
end_match = range_match.group('end')
155
if end_match is None:
156
# RFC2616 says end is optional and default to file_size
161
# Syntactically invalid range
163
ranges.append((start, end))
165
tail_match = self._tail_regexp.match(range_str)
166
if tail_match is not None:
167
tail = int(tail_match.group('tail'))
169
# Syntactically invalid range
172
# Normalize tail into ranges
173
ranges.append((max(0, file_size - tail), file_size))
176
for start, end in ranges:
177
if start >= file_size:
178
# RFC2616 14.35, ranges are invalid if start >= file_size
180
# RFC2616 14.35, end values should be truncated
181
# to file_size -1 if they exceed it
182
end = min(end, file_size - 1)
183
checked_ranges.append((start, end))
184
return checked_ranges
186
def _header_line_length(self, keyword, value):
187
header_line = '%s: %s\r\n' % (keyword, value)
188
return len(header_line)
190
def send_range_content(self, file, start, length):
192
self.wfile.write(file.read(length))
194
def get_single_range(self, file, file_size, start, end):
195
self.send_response(206)
196
length = end - start + 1
197
self.send_header('Accept-Ranges', 'bytes')
198
self.send_header("Content-Length", "%d" % length)
200
self.send_header("Content-Type", 'application/octet-stream')
201
self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
205
self.send_range_content(file, start, length)
207
def get_multiple_ranges(self, file, file_size, ranges):
208
self.send_response(206)
209
self.send_header('Accept-Ranges', 'bytes')
210
boundary = '%d' % random.randint(0, 0x7FFFFFFF)
211
self.send_header('Content-Type',
212
'multipart/byteranges; boundary=%s' % boundary)
213
boundary_line = b'--%s\r\n' % boundary.encode('ascii')
214
# Calculate the Content-Length
216
for (start, end) in ranges:
217
content_length += len(boundary_line)
218
content_length += self._header_line_length(
219
'Content-type', 'application/octet-stream')
220
content_length += self._header_line_length(
221
'Content-Range', 'bytes %d-%d/%d' % (start, end, file_size))
222
content_length += len('\r\n') # end headers
223
content_length += end - start + 1
224
content_length += len(boundary_line)
225
self.send_header('Content-length', content_length)
228
# Send the multipart body
229
for (start, end) in ranges:
230
self.wfile.write(boundary_line)
231
self.send_header('Content-type', 'application/octet-stream')
232
self.send_header('Content-Range', 'bytes %d-%d/%d'
233
% (start, end, file_size))
235
self.send_range_content(file, start, end - start + 1)
237
self.wfile.write(boundary_line)
240
"""Serve a GET request.
242
Handles the Range header.
245
self.server.test_case_server.GET_request_nb += 1
247
path = self.translate_path(self.path)
248
ranges_header_value = self.headers.get('Range')
249
if ranges_header_value is None or os.path.isdir(path):
250
# Let the mother class handle most cases
251
return http_server.SimpleHTTPRequestHandler.do_GET(self)
254
# Always read in binary mode. Opening files in text
255
# mode may cause newline translations, making the
256
# actual size of the content transmitted *less* than
257
# the content-length!
260
self.send_error(404, "File not found")
263
file_size = os.fstat(f.fileno())[6]
264
ranges = self._parse_ranges(ranges_header_value, file_size)
266
# RFC2616 14.16 and 14.35 says that when a server
267
# encounters unsatisfiable range specifiers, it
268
# SHOULD return a 416.
270
# FIXME: We SHOULD send a Content-Range header too,
271
# but the implementation of send_error does not
272
# allows that. So far.
273
self.send_error(416, "Requested range not satisfiable")
277
(start, end) = ranges[0]
278
self.get_single_range(f, file_size, start, end)
280
self.get_multiple_ranges(f, file_size, ranges)
283
def translate_path(self, path):
284
"""Translate a /-separated PATH to the local filename syntax.
286
If the server requires it, proxy the path before the usual translation
288
if self.server.test_case_server.proxy_requests:
289
# We need to act as a proxy and accept absolute urls,
290
# which SimpleHTTPRequestHandler (parent) is not
291
# ready for. So we just drop the protocol://host:port
292
# part in front of the request-url (because we know
293
# we would not forward the request to *another*
296
# So we do what SimpleHTTPRequestHandler.translate_path
297
# do beginning with python 2.4.3: abandon query
298
# parameters, scheme, host port, etc (which ensure we
299
# provide the right behaviour on all python versions).
300
path = urlparse(path)[2]
301
# And now, we can apply *our* trick to proxy files
304
return self._translate_path(path)
306
def _translate_path(self, path):
307
"""Translate a /-separated PATH to the local filename syntax.
309
Note that we're translating http URLs here, not file URLs.
310
The URL root location is the server's startup directory.
311
Components that mean special things to the local file system
312
(e.g. drive or directory names) are ignored. (XXX They should
313
probably be diagnosed.)
315
Override from python standard library to stop it calling os.getcwd()
317
# abandon query parameters
318
path = urlparse(path)[2]
319
path = posixpath.normpath(urlutils.unquote(path))
320
words = path.split('/')
322
for num, word in enumerate(w for w in words if w):
324
drive, word = os.path.splitdrive(word)
325
head, word = os.path.split(word)
326
if word in (os.curdir, os.pardir):
328
path = os.path.join(path, word)
332
class TestingHTTPServerMixin:
334
def __init__(self, test_case_server):
335
# test_case_server can be used to communicate between the
336
# tests and the server (or the request handler and the
337
# server), allowing dynamic behaviors to be defined from
339
self.test_case_server = test_case_server
340
self._home_dir = test_case_server._home_dir
343
class TestingHTTPServer(test_server.TestingTCPServer, TestingHTTPServerMixin):
345
def __init__(self, server_address, request_handler_class,
347
test_server.TestingTCPServer.__init__(self, server_address,
348
request_handler_class)
349
TestingHTTPServerMixin.__init__(self, test_case_server)
352
class TestingThreadingHTTPServer(test_server.TestingThreadingTCPServer,
353
TestingHTTPServerMixin):
354
"""A threading HTTP test server for HTTP 1.1.
356
Since tests can initiate several concurrent connections to the same http
357
server, we need an independent connection for each of them. We achieve that
358
by spawning a new thread for each connection.
361
def __init__(self, server_address, request_handler_class,
363
test_server.TestingThreadingTCPServer.__init__(self, server_address,
364
request_handler_class)
365
TestingHTTPServerMixin.__init__(self, test_case_server)
368
class HttpServer(test_server.TestingTCPServerInAThread):
369
"""A test server for http transports.
371
Subclasses can provide a specific request handler.
374
# The real servers depending on the protocol
375
http_server_class = {'HTTP/1.0': TestingHTTPServer,
376
'HTTP/1.1': TestingThreadingHTTPServer,
379
# Whether or not we proxy the requests (see
380
# TestingHTTPRequestHandler.translate_path).
381
proxy_requests = False
383
# used to form the url that connects to this server
384
_url_protocol = 'http'
386
def __init__(self, request_handler=TestingHTTPRequestHandler,
387
protocol_version=None):
390
:param request_handler: a class that will be instantiated to handle an
391
http connection (one or several requests).
393
:param protocol_version: if specified, will override the protocol
394
version of the request handler.
396
# Depending on the protocol version, we will create the approriate
398
if protocol_version is None:
399
# Use the request handler one
400
proto_vers = request_handler.protocol_version
402
# Use our own, it will be used to override the request handler
404
proto_vers = protocol_version
405
# Get the appropriate server class for the required protocol
406
serv_cls = self.http_server_class.get(proto_vers, None)
408
raise http_client.UnknownProtocol(proto_vers)
409
self.host = 'localhost'
411
super(HttpServer, self).__init__((self.host, self.port),
414
self.protocol_version = proto_vers
415
# Allows tests to verify number of GET requests issued
416
self.GET_request_nb = 0
417
self._http_base_url = None
420
def create_server(self):
421
return self.server_class(
422
(self.host, self.port), self.request_handler_class, self)
424
def _get_remote_url(self, path):
425
path_parts = path.split(os.path.sep)
426
if os.path.isabs(path):
427
if path_parts[:len(self._local_path_parts)] != \
428
self._local_path_parts:
429
raise BadWebserverPath(path, self.test_dir)
430
remote_path = '/'.join(path_parts[len(self._local_path_parts):])
432
remote_path = '/'.join(path_parts)
434
return self._http_base_url + remote_path
436
def log(self, format, *args):
437
"""Capture Server log output."""
438
self.logs.append(format % args)
440
def start_server(self, backing_transport_server=None):
441
"""See breezy.transport.Server.start_server.
443
:param backing_transport_server: The transport that requests over this
444
protocol should be forwarded to. Note that this is currently not
447
# XXX: TODO: make the server back onto vfs_server rather than local
449
if not (backing_transport_server is None
450
or isinstance(backing_transport_server,
451
test_server.LocalURLServer)):
452
raise AssertionError(
453
"HTTPServer currently assumes local transport, got %s" %
454
backing_transport_server)
455
self._home_dir = osutils.getcwd()
456
self._local_path_parts = self._home_dir.split(os.path.sep)
459
super(HttpServer, self).start_server()
460
self._http_base_url = '%s://%s:%s/' % (
461
self._url_protocol, self.host, self.port)
464
"""See breezy.transport.Server.get_url."""
465
return self._get_remote_url(self._home_dir)
467
def get_bogus_url(self):
468
"""See breezy.transport.Server.get_bogus_url."""
469
# this is chosen to try to prevent trouble with proxies, weird dns,
471
return self._url_protocol + '://127.0.0.1:1/'