403
386
strings.append('-%d' % tail_amount)
405
388
return ','.join(strings)
408
#---------------- test server facilities ----------------
409
# TODO: load these only when running tests
410
# FIXME: By moving them to HTTPTestUtil.py ?
413
class WebserverNotAvailable(Exception):
417
class BadWebserverPath(ValueError):
419
return 'path %s is not in %s' % self.args
422
class TestingHTTPRequestHandler(SimpleHTTPRequestHandler):
424
def log_message(self, format, *args):
425
self.server.test_case.log('webserver - %s - - [%s] %s "%s" "%s"',
426
self.address_string(),
427
self.log_date_time_string(),
429
self.headers.get('referer', '-'),
430
self.headers.get('user-agent', '-'))
432
def handle_one_request(self):
433
"""Handle a single HTTP request.
435
You normally don't need to override this method; see the class
436
__doc__ string for information on how to handle specific HTTP
437
commands such as GET and POST.
440
for i in xrange(1,11): # Don't try more than 10 times
442
self.raw_requestline = self.rfile.readline()
443
except socket.error, e:
444
if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
445
# omitted for now because some tests look at the log of
446
# the server and expect to see no errors. see recent
447
# email thread. -- mbp 20051021.
448
## self.log_message('EAGAIN (%d) while reading from raw_requestline' % i)
454
if not self.raw_requestline:
455
self.close_connection = 1
457
if not self.parse_request(): # An error code has been sent, just exit
459
mname = 'do_' + self.command
460
if getattr(self, mname, None) is None:
461
self.send_error(501, "Unsupported method (%r)" % self.command)
463
method = getattr(self, mname)
466
_range_regexp = re.compile(r'^(?P<start>\d+)-(?P<end>\d+)$')
467
_tail_regexp = re.compile(r'^-(?P<tail>\d+)$')
469
def parse_ranges(self, ranges_header):
470
"""Parse the range header value and returns ranges and tail"""
473
assert ranges_header.startswith('bytes=')
474
ranges_header = ranges_header[len('bytes='):]
475
for range_str in ranges_header.split(','):
476
range_match = self._range_regexp.match(range_str)
477
if range_match is not None:
478
ranges.append((int(range_match.group('start')),
479
int(range_match.group('end'))))
481
tail_match = self._tail_regexp.match(range_str)
482
if tail_match is not None:
483
tail = int(tail_match.group('tail'))
486
def send_range_content(self, file, start, length):
488
self.wfile.write(file.read(length))
490
def get_single_range(self, file, file_size, start, end):
491
self.send_response(206)
492
length = end - start + 1
493
self.send_header('Accept-Ranges', 'bytes')
494
self.send_header("Content-Length", "%d" % length)
496
self.send_header("Content-Type", 'application/octet-stream')
497
self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
501
self.send_range_content(file, start, length)
503
def get_multiple_ranges(self, file, file_size, ranges):
504
self.send_response(206)
505
self.send_header('Accept-Ranges', 'bytes')
506
boundary = "%d" % random.randint(0,0x7FFFFFFF)
507
self.send_header("Content-Type",
508
"multipart/byteranges; boundary=%s" % boundary)
510
for (start, end) in ranges:
511
self.wfile.write("--%s\r\n" % boundary)
512
self.send_header("Content-type", 'application/octet-stream')
513
self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
517
self.send_range_content(file, start, end - start + 1)
518
self.wfile.write("--%s\r\n" % boundary)
522
"""Serve a GET request.
524
Handles the Range header.
527
path = self.translate_path(self.path)
528
ranges_header_value = self.headers.get('Range')
529
if ranges_header_value is None or os.path.isdir(path):
530
# Let the mother class handle most cases
531
return SimpleHTTPRequestHandler.do_GET(self)
534
# Always read in binary mode. Opening files in text
535
# mode may cause newline translations, making the
536
# actual size of the content transmitted *less* than
537
# the content-length!
538
file = open(path, 'rb')
540
self.send_error(404, "File not found")
543
file_size = os.fstat(file.fileno())[6]
544
tail, ranges = self.parse_ranges(ranges_header_value)
545
# Normalize tail into ranges
547
ranges.append((file_size - tail, file_size))
553
for (start, end) in ranges:
554
if start >= file_size or end >= file_size:
558
# RFC2616 14-16 says that invalid Range headers
559
# should be ignored and in that case, the whole file
560
# should be returned as if no Range header was
562
file.close() # Will be reopened by the following call
563
return SimpleHTTPRequestHandler.do_GET(self)
566
(start, end) = ranges[0]
567
self.get_single_range(file, file_size, start, end)
569
self.get_multiple_ranges(file, file_size, ranges)
572
if sys.platform == 'win32':
573
# On win32 you cannot access non-ascii filenames without
574
# decoding them into unicode first.
575
# However, under Linux, you can access bytestream paths
576
# without any problems. If this function was always active
577
# it would probably break tests when LANG=C was set
578
def translate_path(self, path):
579
"""Translate a /-separated PATH to the local filename syntax.
581
For bzr, all url paths are considered to be utf8 paths.
582
On Linux, you can access these paths directly over the bytestream
583
request, but on win32, you must decode them, and access them
586
# abandon query parameters
587
path = urlparse.urlparse(path)[2]
588
path = posixpath.normpath(urllib.unquote(path))
589
path = path.decode('utf-8')
590
words = path.split('/')
591
words = filter(None, words)
594
drive, word = os.path.splitdrive(word)
595
head, word = os.path.split(word)
596
if word in (os.curdir, os.pardir): continue
597
path = os.path.join(path, word)
601
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
602
def __init__(self, server_address, RequestHandlerClass, test_case):
603
BaseHTTPServer.HTTPServer.__init__(self, server_address,
605
self.test_case = test_case
608
class HttpServer(Server):
609
"""A test server for http transports.
611
Subclasses can provide a specific request handler.
614
# used to form the url that connects to this server
615
_url_protocol = 'http'
617
# Subclasses can provide a specific request handler
618
def __init__(self, request_handler=TestingHTTPRequestHandler):
619
Server.__init__(self)
620
self.request_handler = request_handler
622
def _http_start(self):
624
httpd = TestingHTTPServer(('localhost', 0),
625
self.request_handler,
627
host, port = httpd.socket.getsockname()
628
self._http_base_url = '%s://localhost:%s/' % (self._url_protocol, port)
629
self._http_starting.release()
630
httpd.socket.settimeout(0.1)
632
while self._http_running:
634
httpd.handle_request()
635
except socket.timeout:
638
def _get_remote_url(self, path):
639
path_parts = path.split(os.path.sep)
640
if os.path.isabs(path):
641
if path_parts[:len(self._local_path_parts)] != \
642
self._local_path_parts:
643
raise BadWebserverPath(path, self.test_dir)
644
remote_path = '/'.join(path_parts[len(self._local_path_parts):])
646
remote_path = '/'.join(path_parts)
648
self._http_starting.acquire()
649
self._http_starting.release()
650
return self._http_base_url + remote_path
652
def log(self, format, *args):
653
"""Capture Server log output."""
654
self.logs.append(format % args)
657
"""See bzrlib.transport.Server.setUp."""
658
self._home_dir = os.getcwdu()
659
self._local_path_parts = self._home_dir.split(os.path.sep)
660
self._http_starting = threading.Lock()
661
self._http_starting.acquire()
662
self._http_running = True
663
self._http_base_url = None
664
self._http_thread = threading.Thread(target=self._http_start)
665
self._http_thread.setDaemon(True)
666
self._http_thread.start()
667
self._http_proxy = os.environ.get("http_proxy")
668
if self._http_proxy is not None:
669
del os.environ["http_proxy"]
673
"""See bzrlib.transport.Server.tearDown."""
674
self._http_running = False
675
self._http_thread.join()
676
if self._http_proxy is not None:
678
os.environ["http_proxy"] = self._http_proxy
681
"""See bzrlib.transport.Server.get_url."""
682
return self._get_remote_url(self._home_dir)
684
def get_bogus_url(self):
685
"""See bzrlib.transport.Server.get_bogus_url."""
686
# this is chosen to try to prevent trouble with proxies, weird dns,
688
return 'http://127.0.0.1:1/'
691
class WallRequestHandler(TestingHTTPRequestHandler):
692
"""Whatever request comes in, close the connection"""
694
def handle_one_request(self):
695
"""Handle a single HTTP request, by abruptly closing the connection"""
696
self.close_connection = 1
699
class BadStatusRequestHandler(TestingHTTPRequestHandler):
700
"""Whatever request comes in, returns a bad status"""
702
def parse_request(self):
703
"""Fakes handling a single HTTP request, returns a bad status"""
704
ignored = TestingHTTPRequestHandler.parse_request(self)
707
self.send_response(0, "Bad status")
709
except socket.error, e:
710
if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
711
# We don't want to pollute the test reuslts with
712
# spurious server errors while test succeed. In
713
# our case, it may occur that the test have
714
# already read the 'Bad Status' and closed the
715
# socket while we are still trying to send some
716
# headers... So the test is ok but if we raise
717
# the exception the output is dirty. So we don't
718
# raise, but we close the connection, just to be
720
self.close_connection = 1
727
class InvalidStatusRequestHandler(TestingHTTPRequestHandler):
728
"""Whatever request comes in, returns am invalid status"""
730
def parse_request(self):
731
"""Fakes handling a single HTTP request, returns a bad status"""
732
ignored = TestingHTTPRequestHandler.parse_request(self)
733
self.wfile.write("Invalid status line\r\n")
737
class BadProtocolRequestHandler(TestingHTTPRequestHandler):
738
"""Whatever request comes in, returns a bad protocol version"""
740
def parse_request(self):
741
"""Fakes handling a single HTTP request, returns a bad status"""
742
ignored = TestingHTTPRequestHandler.parse_request(self)
743
# Returns an invalid protocol version, but curl just
744
# ignores it and those cannot be tested.
745
self.wfile.write("%s %d %s\r\n" % ('HTTP/0.0',
747
'Look at my protocol version'))