~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: John Arbash Meinel
  • Author(s): Mark Hammond
  • Date: 2008-09-09 17:02:21 UTC
  • mto: This revision was merged to the branch mainline in revision 3697.
  • Revision ID: john@arbash-meinel.com-20080909170221-svim3jw2mrz0amp3
An updated transparent icon for bzr.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
"""Base implementation of Transport over http.
18
18
 
19
19
There are separate implementation modules for each http client implementation.
20
20
"""
21
21
 
22
 
from __future__ import absolute_import
23
 
 
24
 
import os
 
22
from cStringIO import StringIO
 
23
import mimetools
25
24
import re
26
25
import urlparse
 
26
import urllib
27
27
import sys
28
 
import weakref
29
28
 
30
29
from bzrlib import (
31
30
    debug,
32
31
    errors,
33
 
    transport,
34
32
    ui,
35
33
    urlutils,
36
34
    )
37
35
from bzrlib.smart import medium
 
36
from bzrlib.symbol_versioning import (
 
37
        deprecated_method,
 
38
        )
38
39
from bzrlib.trace import mutter
39
40
from bzrlib.transport import (
40
41
    ConnectedTransport,
 
42
    _CoalescedOffset,
 
43
    Transport,
41
44
    )
42
45
 
43
 
 
44
 
class HttpTransportBase(ConnectedTransport):
 
46
# TODO: This is not used anymore by HttpTransport_urllib
 
47
# (extracting the auth info and prompting the user for a password
 
48
# have been split), only the tests still use it. It should be
 
49
# deleted and the tests rewritten ASAP to stay in sync.
 
50
def extract_auth(url, password_manager):
 
51
    """Extract auth parameters from am HTTP/HTTPS url and add them to the given
 
52
    password manager.  Return the url, minus those auth parameters (which
 
53
    confuse urllib2).
 
54
    """
 
55
    if not re.match(r'^(https?)(\+\w+)?://', url):
 
56
        raise ValueError(
 
57
            'invalid absolute url %r' % (url,))
 
58
    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
 
59
 
 
60
    if '@' in netloc:
 
61
        auth, netloc = netloc.split('@', 1)
 
62
        if ':' in auth:
 
63
            username, password = auth.split(':', 1)
 
64
        else:
 
65
            username, password = auth, None
 
66
        if ':' in netloc:
 
67
            host = netloc.split(':', 1)[0]
 
68
        else:
 
69
            host = netloc
 
70
        username = urllib.unquote(username)
 
71
        if password is not None:
 
72
            password = urllib.unquote(password)
 
73
        else:
 
74
            password = ui.ui_factory.get_password(
 
75
                prompt='HTTP %(user)s@%(host)s password',
 
76
                user=username, host=host)
 
77
        password_manager.add_password(None, host, username, password)
 
78
    url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
 
79
    return url
 
80
 
 
81
 
 
82
class HttpTransportBase(ConnectedTransport, medium.SmartClientMedium):
45
83
    """Base class for http implementations.
46
84
 
47
85
    Does URL parsing, etc, but not any network IO.
53
91
    # _unqualified_scheme: "http" or "https"
54
92
    # _scheme: may have "+pycurl", etc
55
93
 
56
 
    def __init__(self, base, _impl_name, _from_transport=None):
 
94
    def __init__(self, base, _from_transport=None):
57
95
        """Set the base path where files will be stored."""
58
96
        proto_match = re.match(r'^(https?)(\+\w+)?://', base)
59
97
        if not proto_match:
60
98
            raise AssertionError("not a http url: %r" % base)
61
99
        self._unqualified_scheme = proto_match.group(1)
62
 
        self._impl_name = _impl_name
 
100
        impl_name = proto_match.group(2)
 
101
        if impl_name:
 
102
            impl_name = impl_name[1:]
 
103
        self._impl_name = impl_name
63
104
        super(HttpTransportBase, self).__init__(base,
64
105
                                                _from_transport=_from_transport)
65
 
        self._medium = None
66
106
        # range hint is handled dynamically throughout the life
67
107
        # of the transport object. We start by trying multi-range
68
108
        # requests and if the server returns bogus results, we
84
124
        :param relpath: The relative path to the file
85
125
        """
86
126
        code, response_file = self._get(relpath, None)
87
 
        return response_file
 
127
        # FIXME: some callers want an iterable... One step forward, three steps
 
128
        # backwards :-/ And not only an iterable, but an iterable that can be
 
129
        # seeked backwards, so we will never be able to do that.  One such
 
130
        # known client is bzrlib.bundle.serializer.v4.get_bundle_reader. At the
 
131
        # time of this writing it's even the only known client -- vila20071203
 
132
        return StringIO(response_file.read())
88
133
 
89
134
    def _get(self, relpath, ranges, tail_amount=0):
90
135
        """Get a file, or part of a file.
103
148
 
104
149
        user and passwords are not embedded in the path provided to the server.
105
150
        """
106
 
        url = self._parsed_url.clone(relpath)
107
 
        url.user = url.quoted_user = None
108
 
        url.password = url.quoted_password = None
109
 
        url.scheme = self._unqualified_scheme
110
 
        return str(url)
 
151
        relative = urlutils.unescape(relpath).encode('utf-8')
 
152
        path = self._combine_paths(self._path, relative)
 
153
        return self._unsplit_url(self._unqualified_scheme,
 
154
                                 None, None, self._host, self._port, path)
111
155
 
112
156
    def _create_auth(self):
113
 
        """Returns a dict containing the credentials provided at build time."""
114
 
        auth = dict(host=self._parsed_url.host, port=self._parsed_url.port,
115
 
                    user=self._parsed_url.user, password=self._parsed_url.password,
 
157
        """Returns a dict returning the credentials provided at build time."""
 
158
        auth = dict(host=self._host, port=self._port,
 
159
                    user=self._user, password=self._password,
116
160
                    protocol=self._unqualified_scheme,
117
 
                    path=self._parsed_url.path)
 
161
                    path=self._path)
118
162
        return auth
119
163
 
 
164
    def get_request(self):
 
165
        return SmartClientHTTPMediumRequest(self)
 
166
 
120
167
    def get_smart_medium(self):
121
 
        """See Transport.get_smart_medium."""
122
 
        if self._medium is None:
123
 
            # Since medium holds some state (smart server probing at least), we
124
 
            # need to keep it around. Note that this is needed because medium
125
 
            # has the same 'base' attribute as the transport so it can't be
126
 
            # shared between transports having different bases.
127
 
            self._medium = SmartClientHTTPMedium(self)
128
 
        return self._medium
 
168
        """See Transport.get_smart_medium.
 
169
 
 
170
        HttpTransportBase directly implements the minimal interface of
 
171
        SmartMediumClient, so this returns self.
 
172
        """
 
173
        return self
129
174
 
130
175
    def _degrade_range_hint(self, relpath, ranges, exc_info):
131
176
        if self._range_hint == 'multi':
167
212
        :param offsets: A list of (offset, size) tuples.
168
213
        :param return: A list or generator of (offset, data) tuples
169
214
        """
 
215
 
170
216
        # offsets may be a generator, we will iterate it several times, so
171
217
        # build a list
172
218
        offsets = list(offsets)
202
248
                    # Split the received chunk
203
249
                    for offset, size in cur_coal.ranges:
204
250
                        start = cur_coal.start + offset
205
 
                        rfile.seek(start, os.SEEK_SET)
 
251
                        rfile.seek(start, 0)
206
252
                        data = rfile.read(size)
207
253
                        data_len = len(data)
208
254
                        if data_len != size:
227
273
                        cur_offset_and_size = iter_offsets.next()
228
274
 
229
275
            except (errors.ShortReadvError, errors.InvalidRange,
230
 
                    errors.InvalidHttpRange, errors.HttpBoundaryMissing), e:
 
276
                    errors.InvalidHttpRange), e:
231
277
                mutter('Exception %r: %s during http._readv',e, e)
232
278
                if (not isinstance(e, errors.ShortReadvError)
233
279
                    or retried_offset == cur_offset_and_size):
303
349
 
304
350
    def _post(self, body_bytes):
305
351
        """POST body_bytes to .bzr/smart on this transport.
306
 
 
 
352
        
307
353
        :returns: (response code, response body file-like object).
308
354
        """
309
355
        # TODO: Requiring all the body_bytes to be available at the beginning of
366
412
 
367
413
    def external_url(self):
368
414
        """See bzrlib.transport.Transport.external_url."""
369
 
        # HTTP URL's are externally usable as long as they don't mention their
370
 
        # implementation qualifier
371
 
        url = self._parsed_url.clone()
372
 
        url.scheme = self._unqualified_scheme
373
 
        return str(url)
 
415
        # HTTP URL's are externally usable.
 
416
        return self.base
374
417
 
375
418
    def is_readonly(self):
376
419
        """See Transport.is_readonly."""
406
449
        """
407
450
        raise errors.TransportNotPossible('http does not support lock_write()')
408
451
 
 
452
    def clone(self, offset=None):
 
453
        """Return a new HttpTransportBase with root at self.base + offset
 
454
 
 
455
        We leave the daughter classes take advantage of the hint
 
456
        that it's a cloning not a raw creation.
 
457
        """
 
458
        if offset is None:
 
459
            return self.__class__(self.base, self)
 
460
        else:
 
461
            return self.__class__(self.abspath(offset), self)
 
462
 
409
463
    def _attempted_range_header(self, offsets, tail_amount):
410
464
        """Prepare a HTTP Range header at a level the server should accept.
411
465
 
461
515
 
462
516
        return ','.join(strings)
463
517
 
464
 
    def _redirected_to(self, source, target):
465
 
        """Returns a transport suitable to re-issue a redirected request.
466
 
 
467
 
        :param source: The source url as returned by the server.
468
 
        :param target: The target url as returned by the server.
469
 
 
470
 
        The redirection can be handled only if the relpath involved is not
471
 
        renamed by the redirection.
472
 
 
473
 
        :returns: A transport or None.
474
 
        """
475
 
        parsed_source = self._split_url(source)
476
 
        parsed_target = self._split_url(target)
477
 
        pl = len(self._parsed_url.path)
478
 
        # determine the excess tail - the relative path that was in
479
 
        # the original request but not part of this transports' URL.
480
 
        excess_tail = parsed_source.path[pl:].strip("/")
481
 
        if not target.endswith(excess_tail):
482
 
            # The final part of the url has been renamed, we can't handle the
483
 
            # redirection.
484
 
            return None
485
 
 
486
 
        target_path = parsed_target.path
487
 
        if excess_tail:
488
 
            # Drop the tail that was in the redirect but not part of
489
 
            # the path of this transport.
490
 
            target_path = target_path[:-len(excess_tail)]
491
 
 
492
 
        if parsed_target.scheme in ('http', 'https'):
493
 
            # Same protocol family (i.e. http[s]), we will preserve the same
494
 
            # http client implementation when a redirection occurs from one to
495
 
            # the other (otherwise users may be surprised that bzr switches
496
 
            # from one implementation to the other, and devs may suffer
497
 
            # debugging it).
498
 
            if (parsed_target.scheme == self._unqualified_scheme
499
 
                and parsed_target.host == self._parsed_url.host
500
 
                and parsed_target.port == self._parsed_url.port
501
 
                and (parsed_target.user is None or
502
 
                     parsed_target.user == self._parsed_url.user)):
503
 
                # If a user is specified, it should match, we don't care about
504
 
                # passwords, wrong passwords will be rejected anyway.
505
 
                return self.clone(target_path)
506
 
            else:
507
 
                # Rebuild the url preserving the scheme qualification and the
508
 
                # credentials (if they don't apply, the redirected to server
509
 
                # will tell us, but if they do apply, we avoid prompting the
510
 
                # user)
511
 
                redir_scheme = parsed_target.scheme + '+' + self._impl_name
512
 
                new_url = self._unsplit_url(redir_scheme,
513
 
                    self._parsed_url.user,
514
 
                    self._parsed_url.password,
515
 
                    parsed_target.host, parsed_target.port,
516
 
                    target_path)
517
 
                return transport.get_transport_from_url(new_url)
518
 
        else:
519
 
            # Redirected to a different protocol
520
 
            new_url = self._unsplit_url(parsed_target.scheme,
521
 
                    parsed_target.user,
522
 
                    parsed_target.password,
523
 
                    parsed_target.host, parsed_target.port,
524
 
                    target_path)
525
 
            return transport.get_transport_from_url(new_url)
526
 
 
527
 
 
528
 
# TODO: May be better located in smart/medium.py with the other
529
 
# SmartMedium classes
530
 
class SmartClientHTTPMedium(medium.SmartClientMedium):
531
 
 
532
 
    def __init__(self, http_transport):
533
 
        super(SmartClientHTTPMedium, self).__init__(http_transport.base)
534
 
        # We don't want to create a circular reference between the http
535
 
        # transport and its associated medium. Since the transport will live
536
 
        # longer than the medium, the medium keep only a weak reference to its
537
 
        # transport.
538
 
        self._http_transport_ref = weakref.ref(http_transport)
539
 
 
540
 
    def get_request(self):
541
 
        return SmartClientHTTPMediumRequest(self)
 
518
    def send_http_smart_request(self, bytes):
 
519
        try:
 
520
            code, body_filelike = self._post(bytes)
 
521
            if code != 200:
 
522
                raise InvalidHttpResponse(
 
523
                    self._remote_path('.bzr/smart'),
 
524
                    'Expected 200 response code, got %r' % (code,))
 
525
        except errors.InvalidHttpResponse, e:
 
526
            raise errors.SmartProtocolError(str(e))
 
527
        return body_filelike
542
528
 
543
529
    def should_probe(self):
544
530
        return True
550
536
        if transport_base.startswith('bzr+'):
551
537
            transport_base = transport_base[4:]
552
538
        rel_url = urlutils.relative_url(self.base, transport_base)
553
 
        return urlutils.unquote(rel_url)
554
 
 
555
 
    def send_http_smart_request(self, bytes):
556
 
        try:
557
 
            # Get back the http_transport hold by the weak reference
558
 
            t = self._http_transport_ref()
559
 
            code, body_filelike = t._post(bytes)
560
 
            if code != 200:
561
 
                raise errors.InvalidHttpResponse(
562
 
                    t._remote_path('.bzr/smart'),
563
 
                    'Expected 200 response code, got %r' % (code,))
564
 
        except (errors.InvalidHttpResponse, errors.ConnectionReset), e:
565
 
            raise errors.SmartProtocolError(str(e))
566
 
        return body_filelike
567
 
 
568
 
    def _report_activity(self, bytes, direction):
569
 
        """See SmartMedium._report_activity.
570
 
 
571
 
        Does nothing; the underlying plain HTTP transport will report the
572
 
        activity that this medium would report.
573
 
        """
574
 
        pass
575
 
 
576
 
    def disconnect(self):
577
 
        """See SmartClientMedium.disconnect()."""
578
 
        t = self._http_transport_ref()
579
 
        t.disconnect()
580
 
 
581
 
 
582
 
# TODO: May be better located in smart/medium.py with the other
583
 
# SmartMediumRequest classes
 
539
        return urllib.unquote(rel_url)
 
540
 
 
541
 
584
542
class SmartClientHTTPMediumRequest(medium.SmartClientMediumRequest):
585
543
    """A SmartClientMediumRequest that works with an HTTP medium."""
586
544
 
610
568
    def _finished_reading(self):
611
569
        """See SmartClientMediumRequest._finished_reading."""
612
570
        pass
613
 
 
614
 
 
615
 
def unhtml_roughly(maybe_html, length_limit=1000):
616
 
    """Very approximate html->text translation, for presenting error bodies.
617
 
 
618
 
    :param length_limit: Truncate the result to this many characters.
619
 
 
620
 
    >>> unhtml_roughly("<b>bad</b> things happened\\n")
621
 
    ' bad  things happened '
622
 
    """
623
 
    return re.subn(r"(<[^>]*>|\n|&nbsp;)", " ", maybe_html)[0][:length_limit]