22
22
from cStringIO import StringIO
31
from warnings import warn
33
# TODO: load these only when running http tests
34
import BaseHTTPServer, SimpleHTTPServer, socket, time
37
from bzrlib import errors
38
from bzrlib.errors import (TransportNotPossible, NoSuchFile,
39
TransportError, ConnectionError, InvalidURL)
40
from bzrlib.branch import Branch
29
from bzrlib import errors, ui
41
30
from bzrlib.trace import mutter
42
from bzrlib.transport import Transport, register_transport, Server
43
from bzrlib.transport.http.response import (HttpMultipartRangeResponse,
45
from bzrlib.ui import ui_factory
31
from bzrlib.transport import (
37
# TODO: This is not used anymore by HttpTransport_urllib
38
# (extracting the auth info and prompting the user for a password
39
# have been split), only the tests still use it. It should be
40
# deleted and the tests rewritten ASAP to stay in sync.
48
41
def extract_auth(url, password_manager):
49
42
"""Extract auth parameters from am HTTP/HTTPS url and add them to the given
50
43
password manager. Return the url, minus those auth parameters (which
239
241
raise NotImplementedError(self._get)
243
def get_request(self):
244
return SmartClientHTTPMediumRequest(self)
246
def get_smart_medium(self):
247
"""See Transport.get_smart_medium.
249
HttpTransportBase directly implements the minimal interface of
250
SmartMediumClient, so this returns self.
254
def _retry_get(self, relpath, ranges, exc_info):
255
"""A GET request have failed, let's retry with a simpler request."""
258
# The server does not gives us enough data or
259
# bogus-looking result, let's try again with
260
# a simpler request if possible.
261
if self._range_hint == 'multi':
262
self._range_hint = 'single'
263
mutter('Retry %s with single range request' % relpath)
265
elif self._range_hint == 'single':
266
self._range_hint = None
267
mutter('Retry %s without ranges' % relpath)
270
# Note that since the offsets and the ranges may not
271
# be in the same order, we don't try to calculate a
272
# restricted single range encompassing unprocessed
274
code, f = self._get(relpath, ranges)
275
return try_again, code, f
277
# We tried all the tricks, but nothing worked. We
278
# re-raise original exception; the 'mutter' calls
279
# above will indicate that further tries were
281
raise exc_info[0], exc_info[1], exc_info[2]
241
283
def readv(self, relpath, offsets):
242
284
"""Get parts of the file at the given relative path.
247
289
ranges = self.offsets_to_ranges(offsets)
248
290
mutter('http readv of %s collapsed %s offsets => %s',
249
291
relpath, len(offsets), ranges)
250
code, f = self._get(relpath, ranges)
297
code, f = self._get(relpath, ranges)
298
except (errors.InvalidRange, errors.ShortReadvError), e:
299
try_again, code, f = self._retry_get(relpath, ranges,
251
302
for start, size in offsets:
252
f.seek(start, (start < 0) and 2 or 0)
255
if len(data) != size:
256
raise errors.ShortReadvError(relpath, start, size,
306
f.seek(start, (start < 0) and 2 or 0)
310
if len(data) != size:
311
raise errors.ShortReadvError(relpath, start, size,
313
except (errors.InvalidRange, errors.ShortReadvError), e:
314
# Note that we replace 'f' here and that it
315
# may need cleaning one day before being
317
try_again, code, f = self._retry_get(relpath, ranges,
319
# After one or more tries, we get the data.
258
320
yield start, data
349
def _post(self, body_bytes):
350
"""POST body_bytes to .bzr/smart on this transport.
352
:returns: (response code, response body file-like object).
354
# TODO: Requiring all the body_bytes to be available at the beginning of
355
# the POST may require large client buffers. It would be nice to have
356
# an interface that allows streaming via POST when possible (and
357
# degrades to a local buffer when not).
358
raise NotImplementedError(self._post)
287
360
def put_file(self, relpath, f, mode=None):
288
361
"""Copy the file-like object into the location.
290
363
:param relpath: Location to put the contents, relative to base.
291
364
:param f: File-like object.
293
raise TransportNotPossible('http PUT not supported')
366
raise errors.TransportNotPossible('http PUT not supported')
295
368
def mkdir(self, relpath, mode=None):
296
369
"""Create a directory at the given path."""
297
raise TransportNotPossible('http does not support mkdir()')
370
raise errors.TransportNotPossible('http does not support mkdir()')
299
372
def rmdir(self, relpath):
300
373
"""See Transport.rmdir."""
301
raise TransportNotPossible('http does not support rmdir()')
374
raise errors.TransportNotPossible('http does not support rmdir()')
303
376
def append_file(self, relpath, f, mode=None):
304
377
"""Append the text in the file-like object into the final
307
raise TransportNotPossible('http does not support append()')
380
raise errors.TransportNotPossible('http does not support append()')
309
382
def copy(self, rel_from, rel_to):
310
383
"""Copy the item at rel_from to the location at rel_to"""
311
raise TransportNotPossible('http does not support copy()')
384
raise errors.TransportNotPossible('http does not support copy()')
313
386
def copy_to(self, relpaths, other, mode=None, pb=None):
314
387
"""Copy a set of entries from self into another Transport.
381
455
return self.__class__(self.abspath(offset), self)
457
def attempted_range_header(self, ranges, tail_amount):
458
"""Prepare a HTTP Range header at a level the server should accept"""
460
if self._range_hint == 'multi':
462
return self.range_header(ranges, tail_amount)
463
elif self._range_hint == 'single':
464
# Combine all the requested ranges into a single
467
start, ignored = ranges[0]
468
ignored, end = ranges[-1]
469
if tail_amount not in (0, None):
470
# Nothing we can do here to combine ranges
471
# with tail_amount, just returns None. The
472
# whole file should be downloaded.
475
return self.range_header([(start, end)], 0)
477
# Only tail_amount, requested, leave range_header
479
return self.range_header(ranges, tail_amount)
384
484
def range_header(ranges, tail_amount):
385
485
"""Turn a list of bytes ranges into a HTTP Range header value.
387
:param offsets: A list of byte ranges, (start, end). An empty list
487
:param ranges: A list of byte ranges, (start, end).
488
:param tail_amount: The amount to get from the end of the file.
390
490
:return: HTTP range header string.
492
At least a non-empty ranges *or* a tail_amount must be
393
496
for start, end in ranges:
399
502
return ','.join(strings)
402
#---------------- test server facilities ----------------
403
# TODO: load these only when running tests
406
class WebserverNotAvailable(Exception):
410
class BadWebserverPath(ValueError):
412
return 'path %s is not in %s' % self.args
415
class TestingHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
417
def log_message(self, format, *args):
418
self.server.test_case.log('webserver - %s - - [%s] %s "%s" "%s"',
419
self.address_string(),
420
self.log_date_time_string(),
422
self.headers.get('referer', '-'),
423
self.headers.get('user-agent', '-'))
425
def handle_one_request(self):
426
"""Handle a single HTTP request.
428
You normally don't need to override this method; see the class
429
__doc__ string for information on how to handle specific HTTP
430
commands such as GET and POST.
433
for i in xrange(1,11): # Don't try more than 10 times
435
self.raw_requestline = self.rfile.readline()
436
except socket.error, e:
437
if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
438
# omitted for now because some tests look at the log of
439
# the server and expect to see no errors. see recent
440
# email thread. -- mbp 20051021.
441
## self.log_message('EAGAIN (%d) while reading from raw_requestline' % i)
447
if not self.raw_requestline:
448
self.close_connection = 1
450
if not self.parse_request(): # An error code has been sent, just exit
452
mname = 'do_' + self.command
453
if getattr(self, mname, None) is None:
454
self.send_error(501, "Unsupported method (%r)" % self.command)
456
method = getattr(self, mname)
459
if sys.platform == 'win32':
460
# On win32 you cannot access non-ascii filenames without
461
# decoding them into unicode first.
462
# However, under Linux, you can access bytestream paths
463
# without any problems. If this function was always active
464
# it would probably break tests when LANG=C was set
465
def translate_path(self, path):
466
"""Translate a /-separated PATH to the local filename syntax.
468
For bzr, all url paths are considered to be utf8 paths.
469
On Linux, you can access these paths directly over the bytestream
470
request, but on win32, you must decode them, and access them
473
# abandon query parameters
474
path = urlparse.urlparse(path)[2]
475
path = posixpath.normpath(urllib.unquote(path))
476
path = path.decode('utf-8')
477
words = path.split('/')
478
words = filter(None, words)
481
drive, word = os.path.splitdrive(word)
482
head, word = os.path.split(word)
483
if word in (os.curdir, os.pardir): continue
484
path = os.path.join(path, word)
488
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
489
def __init__(self, server_address, RequestHandlerClass, test_case):
490
BaseHTTPServer.HTTPServer.__init__(self, server_address,
492
self.test_case = test_case
495
class HttpServer(Server):
496
"""A test server for http transports."""
498
# used to form the url that connects to this server
499
_url_protocol = 'http'
501
# Subclasses can provide a specific request handler
502
def __init__(self, request_handler=TestingHTTPRequestHandler):
503
Server.__init__(self)
504
self.request_handler = request_handler
506
def _http_start(self):
508
httpd = TestingHTTPServer(('localhost', 0),
509
self.request_handler,
511
host, port = httpd.socket.getsockname()
512
self._http_base_url = '%s://localhost:%s/' % (self._url_protocol, port)
513
self._http_starting.release()
514
httpd.socket.settimeout(0.1)
516
while self._http_running:
518
httpd.handle_request()
519
except socket.timeout:
522
def _get_remote_url(self, path):
523
path_parts = path.split(os.path.sep)
524
if os.path.isabs(path):
525
if path_parts[:len(self._local_path_parts)] != \
526
self._local_path_parts:
527
raise BadWebserverPath(path, self.test_dir)
528
remote_path = '/'.join(path_parts[len(self._local_path_parts):])
530
remote_path = '/'.join(path_parts)
532
self._http_starting.acquire()
533
self._http_starting.release()
534
return self._http_base_url + remote_path
536
def log(self, format, *args):
537
"""Capture Server log output."""
538
self.logs.append(format % args)
541
"""See bzrlib.transport.Server.setUp."""
542
self._home_dir = os.getcwdu()
543
self._local_path_parts = self._home_dir.split(os.path.sep)
544
self._http_starting = threading.Lock()
545
self._http_starting.acquire()
546
self._http_running = True
547
self._http_base_url = None
548
self._http_thread = threading.Thread(target=self._http_start)
549
self._http_thread.setDaemon(True)
550
self._http_thread.start()
551
self._http_proxy = os.environ.get("http_proxy")
552
if self._http_proxy is not None:
553
del os.environ["http_proxy"]
557
"""See bzrlib.transport.Server.tearDown."""
558
self._http_running = False
559
self._http_thread.join()
560
if self._http_proxy is not None:
562
os.environ["http_proxy"] = self._http_proxy
565
"""See bzrlib.transport.Server.get_url."""
566
return self._get_remote_url(self._home_dir)
568
def get_bogus_url(self):
569
"""See bzrlib.transport.Server.get_bogus_url."""
570
# this is chosen to try to prevent trouble with proxies, weird dns,
572
return 'http://127.0.0.1:1/'
504
def send_http_smart_request(self, bytes):
505
code, body_filelike = self._post(bytes)
506
assert code == 200, 'unexpected HTTP response code %r' % (code,)
510
class SmartClientHTTPMediumRequest(smart.SmartClientMediumRequest):
511
"""A SmartClientMediumRequest that works with an HTTP medium."""
513
def __init__(self, medium):
514
smart.SmartClientMediumRequest.__init__(self, medium)
517
def _accept_bytes(self, bytes):
518
self._buffer += bytes
520
def _finished_writing(self):
521
data = self._medium.send_http_smart_request(self._buffer)
522
self._response_body = data
524
def _read_bytes(self, count):
525
return self._response_body.read(count)
527
def _finished_reading(self):
528
"""See SmartClientMediumRequest._finished_reading."""