~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http/__init__.py

[merge] bzr.dev 2255, resolve conflicts, update copyrights

Show diffs side-by-side

added added

removed removed

Lines of Context:
20
20
"""
21
21
 
22
22
from cStringIO import StringIO
23
 
import errno
24
23
import mimetools
25
 
import os
26
 
import posixpath
27
24
import re
28
 
import sys
29
25
import urlparse
30
26
import urllib
31
 
from warnings import warn
32
 
 
33
 
# TODO: load these only when running http tests
34
 
import BaseHTTPServer, SimpleHTTPServer, socket, time
35
 
import threading
36
 
 
37
 
from bzrlib import errors
38
 
from bzrlib.errors import (TransportNotPossible, NoSuchFile,
39
 
                           TransportError, ConnectionError, InvalidURL)
40
 
from bzrlib.branch import Branch
 
27
import sys
 
28
 
 
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,
44
 
                                            HttpRangeResponse)
45
 
from bzrlib.ui import ui_factory
46
 
 
47
 
 
 
31
from bzrlib.transport import (
 
32
    smart,
 
33
    Transport,
 
34
    )
 
35
 
 
36
 
 
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
53
46
    assert re.match(r'^(https?)(\+\w+)?://', url), \
54
47
            'invalid absolute url %r' % url
55
48
    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
56
 
    
 
49
 
57
50
    if '@' in netloc:
58
51
        auth, netloc = netloc.split('@', 1)
59
52
        if ':' in auth:
68
61
        if password is not None:
69
62
            password = urllib.unquote(password)
70
63
        else:
71
 
            password = ui_factory.get_password(prompt='HTTP %(user)@%(host) password',
72
 
                                               user=username, host=host)
 
64
            password = ui.ui_factory.get_password(
 
65
                prompt='HTTP %(user)s@%(host)s password',
 
66
                user=username, host=host)
73
67
        password_manager.add_password(None, host, username, password)
74
68
    url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
75
69
    return url
102
96
        if not first_line.startswith('HTTP'):
103
97
            if first_header: # The first header *must* start with HTTP
104
98
                raise errors.InvalidHttpResponse(url,
105
 
                    'Opening header line did not start with HTTP: %s' 
 
99
                    'Opening header line did not start with HTTP: %s'
106
100
                    % (first_line,))
107
101
                assert False, 'Opening header line was not HTTP'
108
102
            else:
119
113
    return m
120
114
 
121
115
 
122
 
class HttpTransportBase(Transport):
 
116
class HttpTransportBase(Transport, smart.SmartClientMedium):
123
117
    """Base class for http implementations.
124
118
 
125
119
    Does URL parsing, etc, but not any network IO.
131
125
    # _proto: "http" or "https"
132
126
    # _qualified_proto: may have "+pycurl", etc
133
127
 
134
 
    def __init__(self, base):
 
128
    def __init__(self, base, from_transport=None):
135
129
        """Set the base path where files will be stored."""
136
130
        proto_match = re.match(r'^(https?)(\+\w+)?://', base)
137
131
        if not proto_match:
144
138
        if base[-1] != '/':
145
139
            base = base + '/'
146
140
        super(HttpTransportBase, self).__init__(base)
147
 
        # In the future we might actually connect to the remote host
148
 
        # rather than using get_url
149
 
        # self._connection = None
150
141
        (apparent_proto, self._host,
151
142
            self._path, self._parameters,
152
143
            self._query, self._fragment) = urlparse.urlparse(self.base)
153
144
        self._qualified_proto = apparent_proto
 
145
        # range hint is handled dynamically throughout the life
 
146
        # of the object. We start by trying mulri-range requests
 
147
        # and if the server returns bougs results, we retry with
 
148
        # single range requests and, finally, we forget about
 
149
        # range if the server really can't understand. Once
 
150
        # aquired, this piece of info is propogated to clones.
 
151
        if from_transport is not None:
 
152
            self._range_hint = from_transport._range_hint
 
153
        else:
 
154
            self._range_hint = 'multi'
154
155
 
155
156
    def abspath(self, relpath):
156
157
        """Return the full url to the given relative path.
163
164
        """
164
165
        assert isinstance(relpath, basestring)
165
166
        if isinstance(relpath, unicode):
166
 
            raise InvalidURL(relpath, 'paths must not be unicode.')
 
167
            raise errors.InvalidURL(relpath, 'paths must not be unicode.')
167
168
        if isinstance(relpath, basestring):
168
169
            relpath_parts = relpath.split('/')
169
170
        else:
174
175
        else:
175
176
            # Except for the root, no trailing slashes are allowed
176
177
            if len(relpath_parts) > 1 and relpath_parts[-1] == '':
177
 
                raise ValueError("path %r within branch %r seems to be a directory"
178
 
                                 % (relpath, self._path))
 
178
                raise ValueError(
 
179
                    "path %r within branch %r seems to be a directory"
 
180
                    % (relpath, self._path))
179
181
            basepath = self._path.split('/')
180
182
            if len(basepath) > 0 and basepath[-1] == '':
181
183
                basepath = basepath[:-1]
238
240
        """
239
241
        raise NotImplementedError(self._get)
240
242
 
 
243
    def get_request(self):
 
244
        return SmartClientHTTPMediumRequest(self)
 
245
 
 
246
    def get_smart_medium(self):
 
247
        """See Transport.get_smart_medium.
 
248
 
 
249
        HttpTransportBase directly implements the minimal interface of
 
250
        SmartMediumClient, so this returns self.
 
251
        """
 
252
        return self
 
253
 
 
254
    def _retry_get(self, relpath, ranges, exc_info):
 
255
        """A GET request have failed, let's retry with a simpler request."""
 
256
 
 
257
        try_again = False
 
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)
 
264
            try_again = True
 
265
        elif self._range_hint == 'single':
 
266
            self._range_hint = None
 
267
            mutter('Retry %s without ranges' % relpath)
 
268
            try_again = True
 
269
        if try_again:
 
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
 
273
            # offsets.
 
274
            code, f = self._get(relpath, ranges)
 
275
            return try_again, code, f
 
276
        else:
 
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
 
280
            # unsuccessful
 
281
            raise exc_info[0], exc_info[1], exc_info[2]
 
282
 
241
283
    def readv(self, relpath, offsets):
242
284
        """Get parts of the file at the given relative path.
243
285
 
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)
 
292
 
 
293
        try_again = True
 
294
        while try_again:
 
295
            try_again = False
 
296
            try:
 
297
                code, f = self._get(relpath, ranges)
 
298
            except (errors.InvalidRange, errors.ShortReadvError), e:
 
299
                try_again, code, f = self._retry_get(relpath, ranges,
 
300
                                                     sys.exc_info())
 
301
 
251
302
        for start, size in offsets:
252
 
            f.seek(start, (start < 0) and 2 or 0)
253
 
            start = f.tell()
254
 
            data = f.read(size)
255
 
            if len(data) != size:
256
 
                raise errors.ShortReadvError(relpath, start, size,
257
 
                                             actual=len(data))
 
303
            try_again = True
 
304
            while try_again:
 
305
                try_again = False
 
306
                f.seek(start, (start < 0) and 2 or 0)
 
307
                start = f.tell()
 
308
                try:
 
309
                    data = f.read(size)
 
310
                    if len(data) != size:
 
311
                        raise errors.ShortReadvError(relpath, start, size,
 
312
                                                     actual=len(data))
 
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
 
316
                    # thrown that way.
 
317
                    try_again, code, f = self._retry_get(relpath, ranges,
 
318
                                                         sys.exc_info())
 
319
            # After one or more tries, we get the data.
258
320
            yield start, data
259
321
 
260
322
    @staticmethod
284
346
 
285
347
        return combined
286
348
 
 
349
    def _post(self, body_bytes):
 
350
        """POST body_bytes to .bzr/smart on this transport.
 
351
        
 
352
        :returns: (response code, response body file-like object).
 
353
        """
 
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)
 
359
 
287
360
    def put_file(self, relpath, f, mode=None):
288
361
        """Copy the file-like object into the location.
289
362
 
290
363
        :param relpath: Location to put the contents, relative to base.
291
364
        :param f:       File-like object.
292
365
        """
293
 
        raise TransportNotPossible('http PUT not supported')
 
366
        raise errors.TransportNotPossible('http PUT not supported')
294
367
 
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()')
298
371
 
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()')
302
375
 
303
376
    def append_file(self, relpath, f, mode=None):
304
377
        """Append the text in the file-like object into the final
305
378
        location.
306
379
        """
307
 
        raise TransportNotPossible('http does not support append()')
 
380
        raise errors.TransportNotPossible('http does not support append()')
308
381
 
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()')
312
385
 
313
386
    def copy_to(self, relpaths, other, mode=None, pb=None):
314
387
        """Copy a set of entries from self into another Transport.
322
395
        # the remote location is the same, and rather than download, and
323
396
        # then upload, it could just issue a remote copy_this command.
324
397
        if isinstance(other, HttpTransportBase):
325
 
            raise TransportNotPossible('http cannot be the target of copy_to()')
 
398
            raise errors.TransportNotPossible(
 
399
                'http cannot be the target of copy_to()')
326
400
        else:
327
401
            return super(HttpTransportBase, self).\
328
402
                    copy_to(relpaths, other, mode=mode, pb=pb)
329
403
 
330
404
    def move(self, rel_from, rel_to):
331
405
        """Move the item at rel_from to the location at rel_to"""
332
 
        raise TransportNotPossible('http does not support move()')
 
406
        raise errors.TransportNotPossible('http does not support move()')
333
407
 
334
408
    def delete(self, relpath):
335
409
        """Delete the item at relpath"""
336
 
        raise TransportNotPossible('http does not support delete()')
 
410
        raise errors.TransportNotPossible('http does not support delete()')
337
411
 
338
412
    def is_readonly(self):
339
413
        """See Transport.is_readonly."""
346
420
    def stat(self, relpath):
347
421
        """Return the stat information for a file.
348
422
        """
349
 
        raise TransportNotPossible('http does not support stat()')
 
423
        raise errors.TransportNotPossible('http does not support stat()')
350
424
 
351
425
    def lock_read(self, relpath):
352
426
        """Lock the given file for shared (read) access.
367
441
 
368
442
        :return: A lock object, which should be passed to Transport.unlock()
369
443
        """
370
 
        raise TransportNotPossible('http does not support lock_write()')
 
444
        raise errors.TransportNotPossible('http does not support lock_write()')
371
445
 
372
446
    def clone(self, offset=None):
373
447
        """Return a new HttpTransportBase with root at self.base + offset
380
454
        else:
381
455
            return self.__class__(self.abspath(offset), self)
382
456
 
 
457
    def attempted_range_header(self, ranges, tail_amount):
 
458
        """Prepare a HTTP Range header at a level the server should accept"""
 
459
 
 
460
        if self._range_hint == 'multi':
 
461
            # Nothing to do here
 
462
            return self.range_header(ranges, tail_amount)
 
463
        elif self._range_hint == 'single':
 
464
            # Combine all the requested ranges into a single
 
465
            # encompassing one
 
466
            if len(ranges) > 0:
 
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.
 
473
                    return None
 
474
                else:
 
475
                    return self.range_header([(start, end)], 0)
 
476
            else:
 
477
                # Only tail_amount, requested, leave range_header
 
478
                # do its work
 
479
                return self.range_header(ranges, tail_amount)
 
480
        else:
 
481
            return None
 
482
 
383
483
    @staticmethod
384
484
    def range_header(ranges, tail_amount):
385
485
        """Turn a list of bytes ranges into a HTTP Range header value.
386
486
 
387
 
        :param offsets: A list of byte ranges, (start, end). An empty list
388
 
        is not accepted.
 
487
        :param ranges: A list of byte ranges, (start, end).
 
488
        :param tail_amount: The amount to get from the end of the file.
389
489
 
390
490
        :return: HTTP range header string.
 
491
 
 
492
        At least a non-empty ranges *or* a tail_amount must be
 
493
        provided.
391
494
        """
392
495
        strings = []
393
496
        for start, end in ranges:
398
501
 
399
502
        return ','.join(strings)
400
503
 
401
 
 
402
 
#---------------- test server facilities ----------------
403
 
# TODO: load these only when running tests
404
 
 
405
 
 
406
 
class WebserverNotAvailable(Exception):
407
 
    pass
408
 
 
409
 
 
410
 
class BadWebserverPath(ValueError):
411
 
    def __str__(self):
412
 
        return 'path %s is not in %s' % self.args
413
 
 
414
 
 
415
 
class TestingHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
416
 
 
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(),
421
 
                                  format % args,
422
 
                                  self.headers.get('referer', '-'),
423
 
                                  self.headers.get('user-agent', '-'))
424
 
 
425
 
    def handle_one_request(self):
426
 
        """Handle a single HTTP request.
427
 
 
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.
431
 
 
432
 
        """
433
 
        for i in xrange(1,11): # Don't try more than 10 times
434
 
            try:
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)
442
 
                    time.sleep(0.01)
443
 
                    continue
444
 
                raise
445
 
            else:
446
 
                break
447
 
        if not self.raw_requestline:
448
 
            self.close_connection = 1
449
 
            return
450
 
        if not self.parse_request(): # An error code has been sent, just exit
451
 
            return
452
 
        mname = 'do_' + self.command
453
 
        if getattr(self, mname, None) is None:
454
 
            self.send_error(501, "Unsupported method (%r)" % self.command)
455
 
            return
456
 
        method = getattr(self, mname)
457
 
        method()
458
 
 
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.
467
 
 
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
471
 
            as Unicode files.
472
 
            """
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)
479
 
            path = os.getcwdu()
480
 
            for word in 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)
485
 
            return path
486
 
 
487
 
 
488
 
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
489
 
    def __init__(self, server_address, RequestHandlerClass, test_case):
490
 
        BaseHTTPServer.HTTPServer.__init__(self, server_address,
491
 
                                                RequestHandlerClass)
492
 
        self.test_case = test_case
493
 
 
494
 
 
495
 
class HttpServer(Server):
496
 
    """A test server for http transports."""
497
 
 
498
 
    # used to form the url that connects to this server
499
 
    _url_protocol = 'http'
500
 
 
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
505
 
 
506
 
    def _http_start(self):
507
 
        httpd = None
508
 
        httpd = TestingHTTPServer(('localhost', 0),
509
 
                                  self.request_handler,
510
 
                                  self)
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)
515
 
 
516
 
        while self._http_running:
517
 
            try:
518
 
                httpd.handle_request()
519
 
            except socket.timeout:
520
 
                pass
521
 
 
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):])
529
 
        else:
530
 
            remote_path = '/'.join(path_parts)
531
 
 
532
 
        self._http_starting.acquire()
533
 
        self._http_starting.release()
534
 
        return self._http_base_url + remote_path
535
 
 
536
 
    def log(self, format, *args):
537
 
        """Capture Server log output."""
538
 
        self.logs.append(format % args)
539
 
 
540
 
    def setUp(self):
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"]
554
 
        self.logs = []
555
 
 
556
 
    def tearDown(self):
557
 
        """See bzrlib.transport.Server.tearDown."""
558
 
        self._http_running = False
559
 
        self._http_thread.join()
560
 
        if self._http_proxy is not None:
561
 
            import os
562
 
            os.environ["http_proxy"] = self._http_proxy
563
 
 
564
 
    def get_url(self):
565
 
        """See bzrlib.transport.Server.get_url."""
566
 
        return self._get_remote_url(self._home_dir)
567
 
        
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,
571
 
        # etc
572
 
        return 'http://127.0.0.1:1/'
573
 
 
 
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,)
 
507
        return body_filelike
 
508
 
 
509
 
 
510
class SmartClientHTTPMediumRequest(smart.SmartClientMediumRequest):
 
511
    """A SmartClientMediumRequest that works with an HTTP medium."""
 
512
 
 
513
    def __init__(self, medium):
 
514
        smart.SmartClientMediumRequest.__init__(self, medium)
 
515
        self._buffer = ''
 
516
 
 
517
    def _accept_bytes(self, bytes):
 
518
        self._buffer += bytes
 
519
 
 
520
    def _finished_writing(self):
 
521
        data = self._medium.send_http_smart_request(self._buffer)
 
522
        self._response_body = data
 
523
 
 
524
    def _read_bytes(self, count):
 
525
        return self._response_body.read(count)
 
526
 
 
527
    def _finished_reading(self):
 
528
        """See SmartClientMediumRequest._finished_reading."""
 
529
        pass