~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http.py

[merge] Erik Bågfors: add --revision to bzr pull

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
"""Implementation of Transport over http.
 
17
"""
 
18
 
 
19
import os, errno
 
20
from cStringIO import StringIO
 
21
import urllib, urllib2
 
22
import urlparse
 
23
from warnings import warn
 
24
 
 
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
 
30
from bzrlib.branch import Branch
 
31
from bzrlib.trace import mutter
 
32
from bzrlib.ui import ui_factory
 
33
 
 
34
 
 
35
def extract_auth(url, password_manager):
 
36
    """
 
37
    Extract auth parameters from am HTTP/HTTPS url and add them to the given
 
38
    password manager.  Return the url, minus those auth parameters (which
 
39
    confuse urllib2).
 
40
    """
 
41
    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
 
42
    assert (scheme == 'http') or (scheme == 'https')
 
43
    
 
44
    if '@' in netloc:
 
45
        auth, netloc = netloc.split('@', 1)
 
46
        if ':' in auth:
 
47
            username, password = auth.split(':', 1)
 
48
        else:
 
49
            username, password = auth, None
 
50
        if ':' in netloc:
 
51
            host = netloc.split(':', 1)[0]
 
52
        else:
 
53
            host = netloc
 
54
        username = urllib.unquote(username)
 
55
        if password is not None:
 
56
            password = urllib.unquote(password)
 
57
        else:
 
58
            password = ui_factory.get_password(prompt='HTTP %(user)@%(host) password',
 
59
                                               user=username, host=host)
 
60
        password_manager.add_password(None, host, username, password)
 
61
    url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
 
62
    return url
 
63
 
 
64
    
 
65
def get_url(url):
 
66
    import urllib2
 
67
    mutter("get_url %s", url)
 
68
    manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
 
69
    url = extract_auth(url, manager)
 
70
    auth_handler = urllib2.HTTPBasicAuthHandler(manager)
 
71
    opener = urllib2.build_opener(auth_handler)
 
72
 
 
73
    request = urllib2.Request(url)
 
74
    request.add_header('User-Agent', 'bzr/%s' % bzrlib.__version__)
 
75
    response = opener.open(request)
 
76
    return response
 
77
 
 
78
class HttpTransport(Transport):
 
79
    """This is the transport agent for http:// access.
 
80
    
 
81
    TODO: Implement pipelined versions of all of the *_multi() functions.
 
82
    """
 
83
 
 
84
    def __init__(self, base):
 
85
        """Set the base path where files will be stored."""
 
86
        assert base.startswith('http://') or base.startswith('https://')
 
87
        if base[-1] != '/':
 
88
            base = base + '/'
 
89
        super(HttpTransport, self).__init__(base)
 
90
        # In the future we might actually connect to the remote host
 
91
        # rather than using get_url
 
92
        # self._connection = None
 
93
        (self._proto, self._host,
 
94
            self._path, self._parameters,
 
95
            self._query, self._fragment) = urlparse.urlparse(self.base)
 
96
 
 
97
    def should_cache(self):
 
98
        """Return True if the data pulled across should be cached locally.
 
99
        """
 
100
        return True
 
101
 
 
102
    def clone(self, offset=None):
 
103
        """Return a new HttpTransport with root at self.base + offset
 
104
        For now HttpTransport does not actually connect, so just return
 
105
        a new HttpTransport object.
 
106
        """
 
107
        if offset is None:
 
108
            return HttpTransport(self.base)
 
109
        else:
 
110
            return HttpTransport(self.abspath(offset))
 
111
 
 
112
    def abspath(self, relpath):
 
113
        """Return the full url to the given relative path.
 
114
        This can be supplied with a string or a list
 
115
        """
 
116
        assert isinstance(relpath, basestring)
 
117
        if isinstance(relpath, basestring):
 
118
            relpath_parts = relpath.split('/')
 
119
        else:
 
120
            # TODO: Don't call this with an array - no magic interfaces
 
121
            relpath_parts = relpath[:]
 
122
        if len(relpath_parts) > 1:
 
123
            if relpath_parts[0] == '':
 
124
                raise ValueError("path %r within branch %r seems to be absolute"
 
125
                                 % (relpath, self._path))
 
126
            if relpath_parts[-1] == '':
 
127
                raise ValueError("path %r within branch %r seems to be a directory"
 
128
                                 % (relpath, self._path))
 
129
        basepath = self._path.split('/')
 
130
        if len(basepath) > 0 and basepath[-1] == '':
 
131
            basepath = basepath[:-1]
 
132
        for p in relpath_parts:
 
133
            if p == '..':
 
134
                if len(basepath) == 0:
 
135
                    # In most filesystems, a request for the parent
 
136
                    # of root, just returns root.
 
137
                    continue
 
138
                basepath.pop()
 
139
            elif p == '.' or p == '':
 
140
                continue # No-op
 
141
            else:
 
142
                basepath.append(p)
 
143
        # Possibly, we could use urlparse.urljoin() here, but
 
144
        # I'm concerned about when it chooses to strip the last
 
145
        # portion of the path, and when it doesn't.
 
146
        path = '/'.join(basepath)
 
147
        return urlparse.urlunparse((self._proto,
 
148
                self._host, path, '', '', ''))
 
149
 
 
150
    def has(self, relpath):
 
151
        """Does the target location exist?
 
152
 
 
153
        TODO: HttpTransport.has() should use a HEAD request,
 
154
        not a full GET request.
 
155
 
 
156
        TODO: This should be changed so that we don't use
 
157
        urllib2 and get an exception, the code path would be
 
158
        cleaner if we just do an http HEAD request, and parse
 
159
        the return code.
 
160
        """
 
161
        path = relpath
 
162
        try:
 
163
            path = self.abspath(relpath)
 
164
            f = get_url(path)
 
165
            # Without the read and then close()
 
166
            # we tend to have busy sockets.
 
167
            f.read()
 
168
            f.close()
 
169
            return True
 
170
        except urllib2.URLError, e:
 
171
            mutter('url error code: %s for has url: %r', e.code, path)
 
172
            if e.code == 404:
 
173
                return False
 
174
            raise
 
175
        except IOError, e:
 
176
            mutter('io error: %s %s for has url: %r', 
 
177
                e.errno, errno.errorcode.get(e.errno), path)
 
178
            if e.errno == errno.ENOENT:
 
179
                return False
 
180
            raise TransportError(orig_error=e)
 
181
 
 
182
    def get(self, relpath, decode=False):
 
183
        """Get the file at the given relative path.
 
184
 
 
185
        :param relpath: The relative path to the file
 
186
        """
 
187
        path = relpath
 
188
        try:
 
189
            path = self.abspath(relpath)
 
190
            return get_url(path)
 
191
        except urllib2.HTTPError, e:
 
192
            mutter('url error code: %s for has url: %r', e.code, path)
 
193
            if e.code == 404:
 
194
                raise NoSuchFile(path, extra=e)
 
195
            raise
 
196
        except (BzrError, IOError), e:
 
197
            if hasattr(e, 'errno'):
 
198
                mutter('io error: %s %s for has url: %r', 
 
199
                    e.errno, errno.errorcode.get(e.errno), path)
 
200
                if e.errno == errno.ENOENT:
 
201
                    raise NoSuchFile(path, extra=e)
 
202
            raise ConnectionError(msg = "Error retrieving %s: %s" 
 
203
                             % (self.abspath(relpath), str(e)),
 
204
                             orig_error=e)
 
205
 
 
206
    def put(self, relpath, f, mode=None):
 
207
        """Copy the file-like or string object into the location.
 
208
 
 
209
        :param relpath: Location to put the contents, relative to base.
 
210
        :param f:       File-like or string object.
 
211
        """
 
212
        raise TransportNotPossible('http PUT not supported')
 
213
 
 
214
    def mkdir(self, relpath, mode=None):
 
215
        """Create a directory at the given path."""
 
216
        raise TransportNotPossible('http does not support mkdir()')
 
217
 
 
218
    def rmdir(self, relpath):
 
219
        """See Transport.rmdir."""
 
220
        raise TransportNotPossible('http does not support rmdir()')
 
221
 
 
222
    def append(self, relpath, f):
 
223
        """Append the text in the file-like object into the final
 
224
        location.
 
225
        """
 
226
        raise TransportNotPossible('http does not support append()')
 
227
 
 
228
    def copy(self, rel_from, rel_to):
 
229
        """Copy the item at rel_from to the location at rel_to"""
 
230
        raise TransportNotPossible('http does not support copy()')
 
231
 
 
232
    def copy_to(self, relpaths, other, mode=None, pb=None):
 
233
        """Copy a set of entries from self into another Transport.
 
234
 
 
235
        :param relpaths: A list/generator of entries to be copied.
 
236
 
 
237
        TODO: if other is LocalTransport, is it possible to
 
238
              do better than put(get())?
 
239
        """
 
240
        # At this point HttpTransport might be able to check and see if
 
241
        # the remote location is the same, and rather than download, and
 
242
        # then upload, it could just issue a remote copy_this command.
 
243
        if isinstance(other, HttpTransport):
 
244
            raise TransportNotPossible('http cannot be the target of copy_to()')
 
245
        else:
 
246
            return super(HttpTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
 
247
 
 
248
    def move(self, rel_from, rel_to):
 
249
        """Move the item at rel_from to the location at rel_to"""
 
250
        raise TransportNotPossible('http does not support move()')
 
251
 
 
252
    def delete(self, relpath):
 
253
        """Delete the item at relpath"""
 
254
        raise TransportNotPossible('http does not support delete()')
 
255
 
 
256
    def is_readonly(self):
 
257
        """See Transport.is_readonly."""
 
258
        return True
 
259
 
 
260
    def listable(self):
 
261
        """See Transport.listable."""
 
262
        return False
 
263
 
 
264
    def stat(self, relpath):
 
265
        """Return the stat information for a file.
 
266
        """
 
267
        raise TransportNotPossible('http does not support stat()')
 
268
 
 
269
    def lock_read(self, relpath):
 
270
        """Lock the given file for shared (read) access.
 
271
        :return: A lock object, which should be passed to Transport.unlock()
 
272
        """
 
273
        # The old RemoteBranch ignore lock for reading, so we will
 
274
        # continue that tradition and return a bogus lock object.
 
275
        class BogusLock(object):
 
276
            def __init__(self, path):
 
277
                self.path = path
 
278
            def unlock(self):
 
279
                pass
 
280
        return BogusLock(relpath)
 
281
 
 
282
    def lock_write(self, relpath):
 
283
        """Lock the given file for exclusive (write) access.
 
284
        WARNING: many transports do not support this, so trying avoid using it
 
285
 
 
286
        :return: A lock object, which should be passed to Transport.unlock()
 
287
        """
 
288
        raise TransportNotPossible('http does not support lock_write()')
 
289
 
 
290
 
 
291
#---------------- test server facilities ----------------
 
292
import BaseHTTPServer, SimpleHTTPServer, socket, time
 
293
import threading
 
294
 
 
295
 
 
296
class WebserverNotAvailable(Exception):
 
297
    pass
 
298
 
 
299
 
 
300
class BadWebserverPath(ValueError):
 
301
    def __str__(self):
 
302
        return 'path %s is not in %s' % self.args
 
303
 
 
304
 
 
305
class TestingHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
 
306
 
 
307
    def log_message(self, format, *args):
 
308
        self.server.test_case.log("webserver - %s - - [%s] %s",
 
309
                                  self.address_string(),
 
310
                                  self.log_date_time_string(),
 
311
                                  format%args)
 
312
 
 
313
    def handle_one_request(self):
 
314
        """Handle a single HTTP request.
 
315
 
 
316
        You normally don't need to override this method; see the class
 
317
        __doc__ string for information on how to handle specific HTTP
 
318
        commands such as GET and POST.
 
319
 
 
320
        """
 
321
        for i in xrange(1,11): # Don't try more than 10 times
 
322
            try:
 
323
                self.raw_requestline = self.rfile.readline()
 
324
            except socket.error, e:
 
325
                if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
 
326
                    # omitted for now because some tests look at the log of
 
327
                    # the server and expect to see no errors.  see recent
 
328
                    # email thread. -- mbp 20051021. 
 
329
                    ## self.log_message('EAGAIN (%d) while reading from raw_requestline' % i)
 
330
                    time.sleep(0.01)
 
331
                    continue
 
332
                raise
 
333
            else:
 
334
                break
 
335
        if not self.raw_requestline:
 
336
            self.close_connection = 1
 
337
            return
 
338
        if not self.parse_request(): # An error code has been sent, just exit
 
339
            return
 
340
        mname = 'do_' + self.command
 
341
        if not hasattr(self, mname):
 
342
            self.send_error(501, "Unsupported method (%r)" % self.command)
 
343
            return
 
344
        method = getattr(self, mname)
 
345
        method()
 
346
 
 
347
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
 
348
    def __init__(self, server_address, RequestHandlerClass, test_case):
 
349
        BaseHTTPServer.HTTPServer.__init__(self, server_address,
 
350
                                                RequestHandlerClass)
 
351
        self.test_case = test_case
 
352
 
 
353
 
 
354
class HttpServer(Server):
 
355
    """A test server for http transports."""
 
356
 
 
357
    _HTTP_PORTS = range(13000, 0x8000)
 
358
 
 
359
    def _http_start(self):
 
360
        httpd = None
 
361
        for port in self._HTTP_PORTS:
 
362
            try:
 
363
                httpd = TestingHTTPServer(('localhost', port),
 
364
                                          TestingHTTPRequestHandler,
 
365
                                          self)
 
366
            except socket.error, e:
 
367
                if e.args[0] == errno.EADDRINUSE:
 
368
                    continue
 
369
                print >>sys.stderr, "Cannot run webserver :-("
 
370
                raise
 
371
            else:
 
372
                break
 
373
 
 
374
        if httpd is None:
 
375
            raise WebserverNotAvailable("Cannot run webserver :-( "
 
376
                                        "no free ports in range %s..%s" %
 
377
                                        (_HTTP_PORTS[0], _HTTP_PORTS[-1]))
 
378
 
 
379
        self._http_base_url = 'http://localhost:%s/' % port
 
380
        self._http_starting.release()
 
381
        httpd.socket.settimeout(0.1)
 
382
 
 
383
        while self._http_running:
 
384
            try:
 
385
                httpd.handle_request()
 
386
            except socket.timeout:
 
387
                pass
 
388
 
 
389
    def _get_remote_url(self, path):
 
390
        path_parts = path.split(os.path.sep)
 
391
        if os.path.isabs(path):
 
392
            if path_parts[:len(self._local_path_parts)] != \
 
393
                   self._local_path_parts:
 
394
                raise BadWebserverPath(path, self.test_dir)
 
395
            remote_path = '/'.join(path_parts[len(self._local_path_parts):])
 
396
        else:
 
397
            remote_path = '/'.join(path_parts)
 
398
 
 
399
        self._http_starting.acquire()
 
400
        self._http_starting.release()
 
401
        return self._http_base_url + remote_path
 
402
 
 
403
    def log(self, *args, **kwargs):
 
404
        """Capture Server log output."""
 
405
        self.logs.append(args[3])
 
406
 
 
407
    def setUp(self):
 
408
        """See bzrlib.transport.Server.setUp."""
 
409
        self._home_dir = os.getcwdu()
 
410
        self._local_path_parts = self._home_dir.split(os.path.sep)
 
411
        self._http_starting = threading.Lock()
 
412
        self._http_starting.acquire()
 
413
        self._http_running = True
 
414
        self._http_base_url = None
 
415
        self._http_thread = threading.Thread(target=self._http_start)
 
416
        self._http_thread.setDaemon(True)
 
417
        self._http_thread.start()
 
418
        self._http_proxy = os.environ.get("http_proxy")
 
419
        if self._http_proxy is not None:
 
420
            del os.environ["http_proxy"]
 
421
        self.logs = []
 
422
 
 
423
    def tearDown(self):
 
424
        """See bzrlib.transport.Server.tearDown."""
 
425
        self._http_running = False
 
426
        self._http_thread.join()
 
427
        if self._http_proxy is not None:
 
428
            import os
 
429
            os.environ["http_proxy"] = self._http_proxy
 
430
 
 
431
    def get_url(self):
 
432
        """See bzrlib.transport.Server.get_url."""
 
433
        return self._get_remote_url(self._home_dir)
 
434
        
 
435
    def get_bogus_url(self):
 
436
        """See bzrlib.transport.Server.get_bogus_url."""
 
437
        return 'http://jasldkjsalkdjalksjdkljasd'
 
438
 
 
439
 
 
440
def get_test_permutations():
 
441
    """Return the permutations to be used in testing."""
 
442
    warn("There are no HTTPS transport provider tests yet.")
 
443
    return [(HttpTransport, HttpServer),
 
444
            ]