71
55
def handle_one_request(self):
72
56
"""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.
58
You normally don't need to override this method; see the class
59
__doc__ string for information on how to handle specific HTTP
60
commands such as GET and POST.
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.
63
for i in xrange(1,11): # Don't try more than 10 times
65
self.raw_requestline = self.rfile.readline()
66
except socket.error, e:
67
if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
68
# omitted for now because some tests look at the log of
69
# the server and expect to see no errors. see recent
70
# email thread. -- mbp 20051021.
71
## self.log_message('EAGAIN (%d) while reading from raw_requestline' % i)
77
if not self.raw_requestline:
83
78
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+)?$')
80
if not self.parse_request(): # An error code has been sent, just exit
82
mname = 'do_' + self.command
83
if getattr(self, mname, None) is None:
84
self.send_error(501, "Unsupported method (%r)" % self.command)
86
method = getattr(self, mname)
89
_range_regexp = re.compile(r'^(?P<start>\d+)-(?P<end>\d+)$')
128
90
_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.
92
def parse_ranges(self, ranges_header):
93
"""Parse the range header value and returns ranges and tail.
95
RFC2616 14.35 says that syntactically invalid range
96
specifiers MUST be ignored. In that case, we return 0 for
97
tail and [] for ranges.
143
101
if not ranges_header.startswith('bytes='):
144
102
# Syntactically invalid header
149
105
ranges_header = ranges_header[len('bytes='):]
150
106
for range_str in ranges_header.split(','):
107
# FIXME: RFC2616 says end is optional and default to file_size
151
108
range_match = self._range_regexp.match(range_str)
152
109
if range_match is not None:
153
110
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
111
end = int(range_match.group('end'))
161
113
# Syntactically invalid range
163
115
ranges.append((start, end))
165
117
tail_match = self._tail_regexp.match(range_str)
207
142
def get_multiple_ranges(self, file, file_size, ranges):
208
143
self.send_response(206)
209
144
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)
145
boundary = "%d" % random.randint(0,0x7FFFFFFF)
146
self.send_header("Content-Type",
147
"multipart/byteranges; boundary=%s" % boundary)
226
148
self.end_headers()
228
# Send the multipart body
229
149
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))
150
self.wfile.write("--%s\r\n" % boundary)
151
self.send_header("Content-type", 'application/octet-stream')
152
self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
234
155
self.end_headers()
235
156
self.send_range_content(file, start, end - start + 1)
237
self.wfile.write(boundary_line)
157
self.wfile.write("--%s\r\n" % boundary)
239
159
def do_GET(self):
240
160
"""Serve a GET request.
242
162
Handles the Range header.
245
self.server.test_case_server.GET_request_nb += 1
247
165
path = self.translate_path(self.path)
248
166
ranges_header_value = self.headers.get('Range')
249
167
if ranges_header_value is None or os.path.isdir(path):
250
168
# Let the mother class handle most cases
251
return http_server.SimpleHTTPRequestHandler.do_GET(self)
169
return SimpleHTTPRequestHandler.do_GET(self)
254
172
# Always read in binary mode. Opening files in text
255
173
# mode may cause newline translations, making the
256
174
# actual size of the content transmitted *less* than
257
175
# the content-length!
176
file = open(path, 'rb')
260
178
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)
181
file_size = os.fstat(file.fileno())[6]
182
tail, ranges = self.parse_ranges(ranges_header_value)
183
# Normalize tail into ranges
185
ranges.append((file_size - tail, file_size))
187
self._satisfiable_ranges = True
189
self._satisfiable_ranges = False
191
def check_range(range_specifier):
192
start, end = range_specifier
193
# RFC2616 14.35, ranges are invalid if start >= file_size
194
if start >= file_size:
195
self._satisfiable_ranges = False # Side-effect !
197
# RFC2616 14.35, end values should be truncated
198
# to file_size -1 if they exceed it
199
end = min(end, file_size - 1)
202
ranges = map(check_range, ranges)
204
if not self._satisfiable_ranges:
266
205
# RFC2616 14.16 and 14.35 says that when a server
267
206
# encounters unsatisfiable range specifiers, it
268
207
# SHOULD return a 416.
270
209
# FIXME: We SHOULD send a Content-Range header too,
271
210
# but the implementation of send_error does not
272
211
# allows that. So far.
276
215
if len(ranges) == 1:
277
216
(start, end) = ranges[0]
278
self.get_single_range(f, file_size, start, end)
217
self.get_single_range(file, 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):
219
self.get_multiple_ranges(file, file_size, ranges)
222
if sys.platform == 'win32':
223
# On win32 you cannot access non-ascii filenames without
224
# decoding them into unicode first.
225
# However, under Linux, you can access bytestream paths
226
# without any problems. If this function was always active
227
# it would probably break tests when LANG=C was set
228
def translate_path(self, path):
229
"""Translate a /-separated PATH to the local filename syntax.
231
For bzr, all url paths are considered to be utf8 paths.
232
On Linux, you can access these paths directly over the bytestream
233
request, but on win32, you must decode them, and access them
236
# abandon query parameters
237
path = urlparse.urlparse(path)[2]
238
path = posixpath.normpath(urllib.unquote(path))
239
path = path.decode('utf-8')
240
words = path.split('/')
241
words = filter(None, words)
324
244
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):
245
head, word = os.path.split(word)
246
if word in (os.curdir, os.pardir): continue
247
path = os.path.join(path, word)
251
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
253
def __init__(self, server_address, RequestHandlerClass,
255
BaseHTTPServer.HTTPServer.__init__(self, server_address,
335
257
# test_case_server can be used to communicate between the
336
258
# tests and the server (or the request handler and the
337
259
# server), allowing dynamic behaviors to be defined from
338
260
# the tests cases.
339
261
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):
264
class HttpServer(Server):
369
265
"""A test server for http transports.
371
267
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
270
# used to form the url that connects to this server
384
271
_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)
273
# Subclasses can provide a specific request handler
274
def __init__(self, request_handler=TestingHTTPRequestHandler):
275
Server.__init__(self)
276
self.request_handler = request_handler
409
277
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)
281
def _get_httpd(self):
282
if self._httpd is None:
283
self._httpd = TestingHTTPServer((self.host, self.port),
284
self.request_handler,
286
host, self.port = self._httpd.socket.getsockname()
289
def _http_start(self):
290
httpd = self._get_httpd()
291
self._http_base_url = '%s://%s:%s/' % (self._url_protocol,
294
self._http_starting.release()
295
httpd.socket.settimeout(0.1)
297
while self._http_running:
299
httpd.handle_request()
300
except socket.timeout:
424
303
def _get_remote_url(self, path):
425
304
path_parts = path.split(os.path.sep)
426
305
if os.path.isabs(path):
427
306
if path_parts[:len(self._local_path_parts)] != \
428
self._local_path_parts:
307
self._local_path_parts:
429
308
raise BadWebserverPath(path, self.test_dir)
430
309
remote_path = '/'.join(path_parts[len(self._local_path_parts):])
437
316
"""Capture Server log output."""
438
317
self.logs.append(format % args)
440
def start_server(self, backing_transport_server=None):
441
"""See breezy.transport.Server.start_server.
319
def setUp(self, backing_transport_server=None):
320
"""See bzrlib.transport.Server.setUp.
443
322
:param backing_transport_server: The transport that requests over this
444
323
protocol should be forwarded to. Note that this is currently not
445
324
supported for HTTP.
447
326
# 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()
328
assert backing_transport_server is None or \
329
isinstance(backing_transport_server, LocalURLServer), \
330
"HTTPServer currently assumes local transport, got %s" % \
331
backing_transport_server
332
self._home_dir = os.getcwdu()
456
333
self._local_path_parts = self._home_dir.split(os.path.sep)
334
self._http_starting = threading.Lock()
335
self._http_starting.acquire()
336
self._http_running = True
337
self._http_base_url = None
338
self._http_thread = threading.Thread(target=self._http_start)
339
self._http_thread.setDaemon(True)
340
self._http_thread.start()
341
# Wait for the server thread to start (i.e release the lock)
342
self._http_starting.acquire()
343
self._http_starting.release()
459
super(HttpServer, self).start_server()
460
self._http_base_url = '%s://%s:%s/' % (
461
self._url_protocol, self.host, self.port)
347
"""See bzrlib.transport.Server.tearDown."""
348
self._http_running = False
349
self._http_thread.join()
463
351
def get_url(self):
464
"""See breezy.transport.Server.get_url."""
352
"""See bzrlib.transport.Server.get_url."""
465
353
return self._get_remote_url(self._home_dir)
467
355
def get_bogus_url(self):
468
"""See breezy.transport.Server.get_bogus_url."""
356
"""See bzrlib.transport.Server.get_bogus_url."""
469
357
# this is chosen to try to prevent trouble with proxies, weird dns,
471
return self._url_protocol + '://127.0.0.1:1/'
359
return 'http://127.0.0.1:1/'
362
class HttpServer_urllib(HttpServer):
363
"""Subclass of HttpServer that gives http+urllib urls.
365
This is for use in testing: connections to this server will always go
366
through urllib where possible.
369
# urls returned by this server should require the urllib client impl
370
_url_protocol = 'http+urllib'
373
class HttpServer_PyCurl(HttpServer):
374
"""Subclass of HttpServer that gives http+pycurl urls.
376
This is for use in testing: connections to this server will always go
377
through pycurl where possible.
380
# We don't care about checking the pycurl availability as
381
# this server will be required only when pycurl is present
383
# urls returned by this server should require the pycurl client impl
384
_url_protocol = 'http+pycurl'