~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http.py

  • Committer: Martin Pool
  • Date: 2006-02-22 04:29:54 UTC
  • mfrom: (1566 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1569.
  • Revision ID: mbp@sourcefrog.net-20060222042954-60333f08dd56a646
[merge] from bzr.dev before integration
Fix undefined ordering in sign_my_revisions breaking tests

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
2
 
#
 
1
# Copyright (C) 2005 Canonical Ltd
 
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
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
#
 
7
 
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
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
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
"""Base implementation of Transport over http.
18
 
 
19
 
There are separate implementation modules for each http client implementation.
 
16
"""Implementation of Transport over http.
20
17
"""
21
18
 
22
 
from collections import deque
 
19
import os, errno
23
20
from cStringIO import StringIO
24
 
import errno
25
 
import mimetools
26
 
import os
27
 
import posixpath
28
 
import re
29
 
import sys
 
21
import urllib, urllib2
30
22
import urlparse
31
 
import urllib
32
23
from warnings import warn
33
24
 
34
 
# TODO: load these only when running http tests
35
 
import BaseHTTPServer, SimpleHTTPServer, socket, time
36
 
import threading
37
 
 
38
 
from bzrlib.errors import (TransportNotPossible, NoSuchFile,
39
 
                           TransportError, ConnectionError, InvalidURL)
 
25
import bzrlib
 
26
from bzrlib.transport import Transport, Server
 
27
from bzrlib.errors import (TransportNotPossible, NoSuchFile, 
 
28
                           TransportError, ConnectionError)
 
29
from bzrlib.errors import BzrError, BzrCheckError
40
30
from bzrlib.branch import Branch
41
31
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
32
from bzrlib.ui import ui_factory
46
33
 
47
34
 
48
35
def extract_auth(url, password_manager):
49
 
    """Extract auth parameters from am HTTP/HTTPS url and add them to the given
 
36
    """
 
37
    Extract auth parameters from am HTTP/HTTPS url and add them to the given
50
38
    password manager.  Return the url, minus those auth parameters (which
51
39
    confuse urllib2).
52
40
    """
53
 
    assert re.match(r'^(https?)(\+\w+)?://', url), \
54
 
            'invalid absolute url %r' % url
55
41
    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
 
42
    assert (scheme == 'http') or (scheme == 'https')
56
43
    
57
44
    if '@' in netloc:
58
45
        auth, netloc = netloc.split('@', 1)
75
62
    return url
76
63
 
77
64
 
78
 
def _extract_headers(header_file, skip_first=True):
79
 
    """Extract the mapping for an rfc822 header
80
 
 
81
 
    This is a helper function for the test suite, and for _pycurl.
82
 
    (urllib already parses the headers for us)
83
 
 
84
 
    :param header_file: A file-like object to read
85
 
    :param skip_first: HTTP headers start with the HTTP response as
86
 
                       the first line. Skip this line while parsing
87
 
    :return: mimetools.Message object
88
 
    """
89
 
    header_file.seek(0, 0)
90
 
    if skip_first:
91
 
        header_file.readline()
92
 
    m = mimetools.Message(header_file)
93
 
    return m
94
 
 
95
 
 
96
 
class HttpTransportBase(Transport):
97
 
    """Base class for http implementations.
98
 
 
99
 
    Does URL parsing, etc, but not any network IO.
100
 
 
101
 
    The protocol can be given as e.g. http+urllib://host/ to use a particular
102
 
    implementation.
103
 
    """
104
 
 
105
 
    # _proto: "http" or "https"
106
 
    # _qualified_proto: may have "+pycurl", etc
 
65
class Request(urllib2.Request):
 
66
    """Request object for urllib2 that allows the method to be overridden."""
 
67
 
 
68
    method = None
 
69
 
 
70
    def get_method(self):
 
71
        if self.method is not None:
 
72
            return self.method
 
73
        else:
 
74
            return urllib2.Request.get_method(self)
 
75
 
 
76
 
 
77
def get_url(url, method=None):
 
78
    import urllib2
 
79
    mutter("get_url %s", url)
 
80
    manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
 
81
    url = extract_auth(url, manager)
 
82
    auth_handler = urllib2.HTTPBasicAuthHandler(manager)
 
83
    opener = urllib2.build_opener(auth_handler)
 
84
 
 
85
    request = Request(url)
 
86
    request.method = method
 
87
    request.add_header('User-Agent', 'bzr/%s' % bzrlib.__version__)
 
88
    response = opener.open(request)
 
89
    return response
 
90
 
 
91
 
 
92
class HttpTransport(Transport):
 
93
    """This is the transport agent for http:// access.
 
94
    
 
95
    TODO: Implement pipelined versions of all of the *_multi() functions.
 
96
    """
107
97
 
108
98
    def __init__(self, base):
109
99
        """Set the base path where files will be stored."""
110
 
        proto_match = re.match(r'^(https?)(\+\w+)?://', base)
111
 
        if not proto_match:
112
 
            raise AssertionError("not a http url: %r" % base)
113
 
        self._proto = proto_match.group(1)
114
 
        impl_name = proto_match.group(2)
115
 
        if impl_name:
116
 
            impl_name = impl_name[1:]
117
 
        self._impl_name = impl_name
 
100
        assert base.startswith('http://') or base.startswith('https://')
118
101
        if base[-1] != '/':
119
102
            base = base + '/'
120
 
        super(HttpTransportBase, self).__init__(base)
 
103
        super(HttpTransport, self).__init__(base)
121
104
        # In the future we might actually connect to the remote host
122
105
        # rather than using get_url
123
106
        # self._connection = None
124
 
        (apparent_proto, self._host,
 
107
        (self._proto, self._host,
125
108
            self._path, self._parameters,
126
109
            self._query, self._fragment) = urlparse.urlparse(self.base)
127
 
        self._qualified_proto = apparent_proto
 
110
 
 
111
    def should_cache(self):
 
112
        """Return True if the data pulled across should be cached locally.
 
113
        """
 
114
        return True
 
115
 
 
116
    def clone(self, offset=None):
 
117
        """Return a new HttpTransport with root at self.base + offset
 
118
        For now HttpTransport does not actually connect, so just return
 
119
        a new HttpTransport object.
 
120
        """
 
121
        if offset is None:
 
122
            return HttpTransport(self.base)
 
123
        else:
 
124
            return HttpTransport(self.abspath(offset))
128
125
 
129
126
    def abspath(self, relpath):
130
127
        """Return the full url to the given relative path.
131
 
 
132
 
        This can be supplied with a string or a list.
133
 
 
134
 
        The URL returned always has the protocol scheme originally used to 
135
 
        construct the transport, even if that includes an explicit
136
 
        implementation qualifier.
 
128
        This can be supplied with a string or a list
137
129
        """
138
130
        assert isinstance(relpath, basestring)
139
 
        if isinstance(relpath, unicode):
140
 
            raise InvalidURL(relpath, 'paths must not be unicode.')
141
131
        if isinstance(relpath, basestring):
142
132
            relpath_parts = relpath.split('/')
143
133
        else:
168
158
        # I'm concerned about when it chooses to strip the last
169
159
        # portion of the path, and when it doesn't.
170
160
        path = '/'.join(basepath)
171
 
        if path == '':
172
 
            path = '/'
173
 
        result = urlparse.urlunparse((self._qualified_proto,
174
 
                                    self._host, path, '', '', ''))
175
 
        return result
176
 
 
177
 
    def _real_abspath(self, relpath):
178
 
        """Produce absolute path, adjusting protocol if needed"""
179
 
        abspath = self.abspath(relpath)
180
 
        qp = self._qualified_proto
181
 
        rp = self._proto
182
 
        if self._qualified_proto != self._proto:
183
 
            abspath = rp + abspath[len(qp):]
184
 
        if not isinstance(abspath, str):
185
 
            # escaping must be done at a higher level
186
 
            abspath = abspath.encode('ascii')
187
 
        return abspath
 
161
        return urlparse.urlunparse((self._proto,
 
162
                self._host, path, '', '', ''))
188
163
 
189
164
    def has(self, relpath):
190
 
        raise NotImplementedError("has() is abstract on %r" % self)
191
 
 
192
 
    def get(self, relpath):
 
165
        """Does the target location exist?
 
166
 
 
167
        TODO: This should be changed so that we don't use
 
168
        urllib2 and get an exception, the code path would be
 
169
        cleaner if we just do an http HEAD request, and parse
 
170
        the return code.
 
171
        """
 
172
        path = relpath
 
173
        try:
 
174
            path = self.abspath(relpath)
 
175
            f = get_url(path, method='HEAD')
 
176
            # Without the read and then close()
 
177
            # we tend to have busy sockets.
 
178
            f.read()
 
179
            f.close()
 
180
            return True
 
181
        except urllib2.URLError, e:
 
182
            mutter('url error code: %s for has url: %r', e.code, path)
 
183
            if e.code == 404:
 
184
                return False
 
185
            raise
 
186
        except IOError, e:
 
187
            mutter('io error: %s %s for has url: %r', 
 
188
                e.errno, errno.errorcode.get(e.errno), path)
 
189
            if e.errno == errno.ENOENT:
 
190
                return False
 
191
            raise TransportError(orig_error=e)
 
192
 
 
193
    def get(self, relpath, decode=False):
193
194
        """Get the file at the given relative path.
194
195
 
195
196
        :param relpath: The relative path to the file
196
197
        """
197
 
        code, response_file = self._get(relpath, None)
198
 
        return response_file
199
 
 
200
 
    def _get(self, relpath, ranges, tail_amount=0):
201
 
        """Get a file, or part of a file.
202
 
 
203
 
        :param relpath: Path relative to transport base URL
204
 
        :param byte_range: None to get the whole file;
205
 
            or [(start,end)] to fetch parts of a file.
206
 
        :param tail_amount: How much data to fetch from the tail of
207
 
        the file.
208
 
 
209
 
        :returns: (http_code, result_file)
210
 
 
211
 
        Note that the current http implementations can only fetch one range at
212
 
        a time through this call.
213
 
        """
214
 
        raise NotImplementedError(self._get)
215
 
 
216
 
    def readv(self, relpath, offsets):
217
 
        """Get parts of the file at the given relative path.
218
 
 
219
 
        :param offsets: A list of (offset, size) tuples.
220
 
        :param return: A list or generator of (offset, data) tuples
221
 
        """
222
 
        ranges, tail_amount = self.offsets_to_ranges(offsets)
223
 
        mutter('readv of %s %s => %s tail:%s',
224
 
                relpath, offsets, ranges, tail_amount)
225
 
        code, f = self._get(relpath, ranges, tail_amount)
226
 
        for start, size in offsets:
227
 
            f.seek(start, (start < 0) and 2 or 0)
228
 
            start = f.tell()
229
 
            data = f.read(size)
230
 
            assert len(data) == size
231
 
            yield start, data
232
 
 
233
 
    @staticmethod
234
 
    def offsets_to_ranges(offsets, fudge_factor=0):
235
 
        """Turn a list of offsets and sizes into a list of byte ranges.
236
 
 
237
 
        :param offsets: A list of tuples of (start, size).  An empty list
238
 
            is not accepted.
239
 
        :param fudge_factor: Fudge together ranges that are fudge_factor
240
 
            bytes apart
241
 
 
242
 
        :return: a list of inclusive byte ranges (start, end) and the 
243
 
            amount of data to fetch from the tail of the file. 
244
 
            Adjacent ranges will be combined.
245
 
        """
246
 
        # We need a copy of the offsets, as the caller might expect it to
247
 
        # remain unsorted. This doesn't seem expensive for memory at least.
248
 
        offsets = sorted(offsets)
249
 
 
250
 
        max_negative = 0
251
 
        prev_end = None
252
 
        combined = []
253
 
 
254
 
        for start, size in offsets:
255
 
            if start < 0:
256
 
                max_negative = min(start, max_negative)
257
 
            else:
258
 
                end = start + size - 1
259
 
                if prev_end is None:
260
 
                    combined.append([start, end])
261
 
                elif start <= prev_end + 1 + fudge_factor:
262
 
                    combined[-1][1] = end
263
 
                else:
264
 
                    combined.append([start, end])
265
 
                prev_end = end
266
 
 
267
 
        return combined, -max_negative
 
198
        path = relpath
 
199
        try:
 
200
            path = self.abspath(relpath)
 
201
            return get_url(path)
 
202
        except urllib2.HTTPError, e:
 
203
            mutter('url error code: %s for has url: %r', e.code, path)
 
204
            if e.code == 404:
 
205
                raise NoSuchFile(path, extra=e)
 
206
            raise
 
207
        except (BzrError, IOError), e:
 
208
            if hasattr(e, 'errno'):
 
209
                mutter('io error: %s %s for has url: %r', 
 
210
                    e.errno, errno.errorcode.get(e.errno), path)
 
211
                if e.errno == errno.ENOENT:
 
212
                    raise NoSuchFile(path, extra=e)
 
213
            raise ConnectionError(msg = "Error retrieving %s: %s" 
 
214
                             % (self.abspath(relpath), str(e)),
 
215
                             orig_error=e)
268
216
 
269
217
    def put(self, relpath, f, mode=None):
270
218
        """Copy the file-like or string object into the location.
303
251
        # At this point HttpTransport might be able to check and see if
304
252
        # the remote location is the same, and rather than download, and
305
253
        # then upload, it could just issue a remote copy_this command.
306
 
        if isinstance(other, HttpTransportBase):
 
254
        if isinstance(other, HttpTransport):
307
255
            raise TransportNotPossible('http cannot be the target of copy_to()')
308
256
        else:
309
 
            return super(HttpTransportBase, self).\
310
 
                    copy_to(relpaths, other, mode=mode, pb=pb)
 
257
            return super(HttpTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
311
258
 
312
259
    def move(self, rel_from, rel_to):
313
260
        """Move the item at rel_from to the location at rel_to"""
351
298
        """
352
299
        raise TransportNotPossible('http does not support lock_write()')
353
300
 
354
 
    def clone(self, offset=None):
355
 
        """Return a new HttpTransportBase with root at self.base + offset
356
 
        For now HttpTransportBase does not actually connect, so just return
357
 
        a new HttpTransportBase object.
358
 
        """
359
 
        if offset is None:
360
 
            return self.__class__(self.base)
361
 
        else:
362
 
            return self.__class__(self.abspath(offset))
363
 
 
364
 
    @staticmethod
365
 
    def range_header(ranges, tail_amount):
366
 
        """Turn a list of bytes ranges into a HTTP Range header value.
367
 
 
368
 
        :param offsets: A list of byte ranges, (start, end). An empty list
369
 
        is not accepted.
370
 
 
371
 
        :return: HTTP range header string.
372
 
        """
373
 
        strings = []
374
 
        for start, end in ranges:
375
 
            strings.append('%d-%d' % (start, end))
376
 
 
377
 
        if tail_amount:
378
 
            strings.append('-%d' % tail_amount)
379
 
 
380
 
        return 'bytes=' + ','.join(strings)
381
 
 
382
301
 
383
302
#---------------- test server facilities ----------------
384
 
# TODO: load these only when running tests
 
303
import BaseHTTPServer, SimpleHTTPServer, socket, time
 
304
import threading
385
305
 
386
306
 
387
307
class WebserverNotAvailable(Exception):
437
357
        method = getattr(self, mname)
438
358
        method()
439
359
 
440
 
    if sys.platform == 'win32':
441
 
        # On win32 you cannot access non-ascii filenames without
442
 
        # decoding them into unicode first.
443
 
        # However, under Linux, you can access bytestream paths
444
 
        # without any problems. If this function was always active
445
 
        # it would probably break tests when LANG=C was set
446
 
        def translate_path(self, path):
447
 
            """Translate a /-separated PATH to the local filename syntax.
448
 
 
449
 
            For bzr, all url paths are considered to be utf8 paths.
450
 
            On Linux, you can access these paths directly over the bytestream
451
 
            request, but on win32, you must decode them, and access them
452
 
            as Unicode files.
453
 
            """
454
 
            # abandon query parameters
455
 
            path = urlparse.urlparse(path)[2]
456
 
            path = posixpath.normpath(urllib.unquote(path))
457
 
            path = path.decode('utf-8')
458
 
            words = path.split('/')
459
 
            words = filter(None, words)
460
 
            path = os.getcwdu()
461
 
            for word in words:
462
 
                drive, word = os.path.splitdrive(word)
463
 
                head, word = os.path.split(word)
464
 
                if word in (os.curdir, os.pardir): continue
465
 
                path = os.path.join(path, word)
466
 
            return path
467
 
 
468
360
 
469
361
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
470
362
    def __init__(self, server_address, RequestHandlerClass, test_case):
476
368
class HttpServer(Server):
477
369
    """A test server for http transports."""
478
370
 
479
 
    # used to form the url that connects to this server
480
 
    _url_protocol = 'http'
481
 
 
482
371
    def _http_start(self):
483
372
        httpd = None
484
373
        httpd = TestingHTTPServer(('localhost', 0),
485
374
                                  TestingHTTPRequestHandler,
486
375
                                  self)
487
376
        host, port = httpd.socket.getsockname()
488
 
        self._http_base_url = '%s://localhost:%s/' % (self._url_protocol, port)
 
377
        self._http_base_url = 'http://localhost:%s/' % port
489
378
        self._http_starting.release()
490
379
        httpd.socket.settimeout(0.1)
491
380
 
543
432
        
544
433
    def get_bogus_url(self):
545
434
        """See bzrlib.transport.Server.get_bogus_url."""
546
 
        # this is chosen to try to prevent trouble with proxies, wierd dns,
547
 
        # etc
548
 
        return 'http://127.0.0.1:1/'
549
 
 
 
435
        return 'http://jasldkjsalkdjalksjdkljasd'
 
436
 
 
437
 
 
438
def get_test_permutations():
 
439
    """Return the permutations to be used in testing."""
 
440
    warn("There are no HTTPS transport provider tests yet.")
 
441
    return [(HttpTransport, HttpServer),
 
442
            ]