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
17
17
"""Base implementation of Transport over http.
19
19
There are separate implementation modules for each http client implementation.
22
from __future__ import absolute_import
22
from cStringIO import StringIO
30
29
from bzrlib import (
37
35
from bzrlib.smart import medium
36
from bzrlib.symbol_versioning import (
38
39
from bzrlib.trace import mutter
39
40
from bzrlib.transport import (
40
41
ConnectedTransport,
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
55
if not re.match(r'^(https?)(\+\w+)?://', url):
57
'invalid absolute url %r' % (url,))
58
scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
61
auth, netloc = netloc.split('@', 1)
63
username, password = auth.split(':', 1)
65
username, password = auth, None
67
host = netloc.split(':', 1)[0]
70
username = urllib.unquote(username)
71
if password is not None:
72
password = urllib.unquote(password)
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))
82
class HttpTransportBase(ConnectedTransport, medium.SmartClientMedium):
45
83
"""Base class for http implementations.
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
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)
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)
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
86
126
code, response_file = self._get(relpath, None)
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())
89
134
def _get(self, relpath, ranges, tail_amount=0):
90
135
"""Get a file, or part of a file.
104
149
user and passwords are not embedded in the path provided to the server.
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
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)
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)
164
def get_request(self):
165
return SmartClientHTTPMediumRequest(self)
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)
168
"""See Transport.get_smart_medium.
170
HttpTransportBase directly implements the minimal interface of
171
SmartMediumClient, so this returns self.
130
175
def _degrade_range_hint(self, relpath, ranges, exc_info):
131
176
if self._range_hint == 'multi':
407
450
raise errors.TransportNotPossible('http does not support lock_write()')
452
def clone(self, offset=None):
453
"""Return a new HttpTransportBase with root at self.base + offset
455
We leave the daughter classes take advantage of the hint
456
that it's a cloning not a raw creation.
459
return self.__class__(self.base, self)
461
return self.__class__(self.abspath(offset), self)
409
463
def _attempted_range_header(self, offsets, tail_amount):
410
464
"""Prepare a HTTP Range header at a level the server should accept.
462
516
return ','.join(strings)
464
def _redirected_to(self, source, target):
465
"""Returns a transport suitable to re-issue a redirected request.
467
:param source: The source url as returned by the server.
468
:param target: The target url as returned by the server.
470
The redirection can be handled only if the relpath involved is not
471
renamed by the redirection.
473
:returns: A transport or None.
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
486
target_path = parsed_target.path
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)]
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
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)
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
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,
517
return transport.get_transport_from_url(new_url)
519
# Redirected to a different protocol
520
new_url = self._unsplit_url(parsed_target.scheme,
522
parsed_target.password,
523
parsed_target.host, parsed_target.port,
525
return transport.get_transport_from_url(new_url)
528
# TODO: May be better located in smart/medium.py with the other
529
# SmartMedium classes
530
class SmartClientHTTPMedium(medium.SmartClientMedium):
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
538
self._http_transport_ref = weakref.ref(http_transport)
540
def get_request(self):
541
return SmartClientHTTPMediumRequest(self)
518
def send_http_smart_request(self, bytes):
520
code, body_filelike = self._post(bytes)
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))
543
529
def should_probe(self):
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)
555
def send_http_smart_request(self, bytes):
557
# Get back the http_transport hold by the weak reference
558
t = self._http_transport_ref()
559
code, body_filelike = t._post(bytes)
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))
568
def _report_activity(self, bytes, direction):
569
"""See SmartMedium._report_activity.
571
Does nothing; the underlying plain HTTP transport will report the
572
activity that this medium would report.
576
def disconnect(self):
577
"""See SmartClientMedium.disconnect()."""
578
t = self._http_transport_ref()
582
# TODO: May be better located in smart/medium.py with the other
583
# SmartMediumRequest classes
539
return urllib.unquote(rel_url)
584
542
class SmartClientHTTPMediumRequest(medium.SmartClientMediumRequest):
585
543
"""A SmartClientMediumRequest that works with an HTTP medium."""