~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http.py

  • Committer: Robert Collins
  • Date: 2006-03-02 03:12:34 UTC
  • mto: (1594.2.4 integration)
  • mto: This revision was merged to the branch mainline in revision 1596.
  • Revision ID: robertc@robertcollins.net-20060302031234-cf6b75961f27c5df
InterVersionedFile implemented.

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
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
    """
 
97
 
 
98
    def __init__(self, base):
 
99
        """Set the base path where files will be stored."""
 
100
        assert base.startswith('http://') or base.startswith('https://')
 
101
        if base[-1] != '/':
 
102
            base = base + '/'
 
103
        super(HttpTransport, self).__init__(base)
 
104
        # In the future we might actually connect to the remote host
 
105
        # rather than using get_url
 
106
        # self._connection = None
 
107
        (self._proto, self._host,
 
108
            self._path, self._parameters,
 
109
            self._query, self._fragment) = urlparse.urlparse(self.base)
 
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))
 
125
 
 
126
    def abspath(self, relpath):
 
127
        """Return the full url to the given relative path.
 
128
        This can be supplied with a string or a list
 
129
        """
 
130
        assert isinstance(relpath, basestring)
 
131
        if isinstance(relpath, basestring):
 
132
            relpath_parts = relpath.split('/')
 
133
        else:
 
134
            # TODO: Don't call this with an array - no magic interfaces
 
135
            relpath_parts = relpath[:]
 
136
        if len(relpath_parts) > 1:
 
137
            if relpath_parts[0] == '':
 
138
                raise ValueError("path %r within branch %r seems to be absolute"
 
139
                                 % (relpath, self._path))
 
140
            if relpath_parts[-1] == '':
 
141
                raise ValueError("path %r within branch %r seems to be a directory"
 
142
                                 % (relpath, self._path))
 
143
        basepath = self._path.split('/')
 
144
        if len(basepath) > 0 and basepath[-1] == '':
 
145
            basepath = basepath[:-1]
 
146
        for p in relpath_parts:
 
147
            if p == '..':
 
148
                if len(basepath) == 0:
 
149
                    # In most filesystems, a request for the parent
 
150
                    # of root, just returns root.
 
151
                    continue
 
152
                basepath.pop()
 
153
            elif p == '.' or p == '':
 
154
                continue # No-op
 
155
            else:
 
156
                basepath.append(p)
 
157
        # Possibly, we could use urlparse.urljoin() here, but
 
158
        # I'm concerned about when it chooses to strip the last
 
159
        # portion of the path, and when it doesn't.
 
160
        path = '/'.join(basepath)
 
161
        return urlparse.urlunparse((self._proto,
 
162
                self._host, path, '', '', ''))
 
163
 
 
164
    def has(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.HTTPError, 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):
 
194
        """Get the file at the given relative path.
 
195
 
 
196
        :param relpath: The relative path to the file
 
197
        """
 
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)
 
216
 
 
217
    def put(self, relpath, f, mode=None):
 
218
        """Copy the file-like or string object into the location.
 
219
 
 
220
        :param relpath: Location to put the contents, relative to base.
 
221
        :param f:       File-like or string object.
 
222
        """
 
223
        raise TransportNotPossible('http PUT not supported')
 
224
 
 
225
    def mkdir(self, relpath, mode=None):
 
226
        """Create a directory at the given path."""
 
227
        raise TransportNotPossible('http does not support mkdir()')
 
228
 
 
229
    def rmdir(self, relpath):
 
230
        """See Transport.rmdir."""
 
231
        raise TransportNotPossible('http does not support rmdir()')
 
232
 
 
233
    def append(self, relpath, f):
 
234
        """Append the text in the file-like object into the final
 
235
        location.
 
236
        """
 
237
        raise TransportNotPossible('http does not support append()')
 
238
 
 
239
    def copy(self, rel_from, rel_to):
 
240
        """Copy the item at rel_from to the location at rel_to"""
 
241
        raise TransportNotPossible('http does not support copy()')
 
242
 
 
243
    def copy_to(self, relpaths, other, mode=None, pb=None):
 
244
        """Copy a set of entries from self into another Transport.
 
245
 
 
246
        :param relpaths: A list/generator of entries to be copied.
 
247
 
 
248
        TODO: if other is LocalTransport, is it possible to
 
249
              do better than put(get())?
 
250
        """
 
251
        # At this point HttpTransport might be able to check and see if
 
252
        # the remote location is the same, and rather than download, and
 
253
        # then upload, it could just issue a remote copy_this command.
 
254
        if isinstance(other, HttpTransport):
 
255
            raise TransportNotPossible('http cannot be the target of copy_to()')
 
256
        else:
 
257
            return super(HttpTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
 
258
 
 
259
    def move(self, rel_from, rel_to):
 
260
        """Move the item at rel_from to the location at rel_to"""
 
261
        raise TransportNotPossible('http does not support move()')
 
262
 
 
263
    def delete(self, relpath):
 
264
        """Delete the item at relpath"""
 
265
        raise TransportNotPossible('http does not support delete()')
 
266
 
 
267
    def is_readonly(self):
 
268
        """See Transport.is_readonly."""
 
269
        return True
 
270
 
 
271
    def listable(self):
 
272
        """See Transport.listable."""
 
273
        return False
 
274
 
 
275
    def stat(self, relpath):
 
276
        """Return the stat information for a file.
 
277
        """
 
278
        raise TransportNotPossible('http does not support stat()')
 
279
 
 
280
    def lock_read(self, relpath):
 
281
        """Lock the given file for shared (read) access.
 
282
        :return: A lock object, which should be passed to Transport.unlock()
 
283
        """
 
284
        # The old RemoteBranch ignore lock for reading, so we will
 
285
        # continue that tradition and return a bogus lock object.
 
286
        class BogusLock(object):
 
287
            def __init__(self, path):
 
288
                self.path = path
 
289
            def unlock(self):
 
290
                pass
 
291
        return BogusLock(relpath)
 
292
 
 
293
    def lock_write(self, relpath):
 
294
        """Lock the given file for exclusive (write) access.
 
295
        WARNING: many transports do not support this, so trying avoid using it
 
296
 
 
297
        :return: A lock object, which should be passed to Transport.unlock()
 
298
        """
 
299
        raise TransportNotPossible('http does not support lock_write()')
 
300
 
 
301
 
 
302
#---------------- test server facilities ----------------
 
303
import BaseHTTPServer, SimpleHTTPServer, socket, time
 
304
import threading
 
305
 
 
306
 
 
307
class WebserverNotAvailable(Exception):
 
308
    pass
 
309
 
 
310
 
 
311
class BadWebserverPath(ValueError):
 
312
    def __str__(self):
 
313
        return 'path %s is not in %s' % self.args
 
314
 
 
315
 
 
316
class TestingHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
 
317
 
 
318
    def log_message(self, format, *args):
 
319
        self.server.test_case.log('webserver - %s - - [%s] %s "%s" "%s"',
 
320
                                  self.address_string(),
 
321
                                  self.log_date_time_string(),
 
322
                                  format % args,
 
323
                                  self.headers.get('referer', '-'),
 
324
                                  self.headers.get('user-agent', '-'))
 
325
 
 
326
    def handle_one_request(self):
 
327
        """Handle a single HTTP request.
 
328
 
 
329
        You normally don't need to override this method; see the class
 
330
        __doc__ string for information on how to handle specific HTTP
 
331
        commands such as GET and POST.
 
332
 
 
333
        """
 
334
        for i in xrange(1,11): # Don't try more than 10 times
 
335
            try:
 
336
                self.raw_requestline = self.rfile.readline()
 
337
            except socket.error, e:
 
338
                if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
 
339
                    # omitted for now because some tests look at the log of
 
340
                    # the server and expect to see no errors.  see recent
 
341
                    # email thread. -- mbp 20051021. 
 
342
                    ## self.log_message('EAGAIN (%d) while reading from raw_requestline' % i)
 
343
                    time.sleep(0.01)
 
344
                    continue
 
345
                raise
 
346
            else:
 
347
                break
 
348
        if not self.raw_requestline:
 
349
            self.close_connection = 1
 
350
            return
 
351
        if not self.parse_request(): # An error code has been sent, just exit
 
352
            return
 
353
        mname = 'do_' + self.command
 
354
        if not hasattr(self, mname):
 
355
            self.send_error(501, "Unsupported method (%r)" % self.command)
 
356
            return
 
357
        method = getattr(self, mname)
 
358
        method()
 
359
 
 
360
 
 
361
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
 
362
    def __init__(self, server_address, RequestHandlerClass, test_case):
 
363
        BaseHTTPServer.HTTPServer.__init__(self, server_address,
 
364
                                                RequestHandlerClass)
 
365
        self.test_case = test_case
 
366
 
 
367
 
 
368
class HttpServer(Server):
 
369
    """A test server for http transports."""
 
370
 
 
371
    def _http_start(self):
 
372
        httpd = None
 
373
        httpd = TestingHTTPServer(('localhost', 0),
 
374
                                  TestingHTTPRequestHandler,
 
375
                                  self)
 
376
        host, port = httpd.socket.getsockname()
 
377
        self._http_base_url = 'http://localhost:%s/' % port
 
378
        self._http_starting.release()
 
379
        httpd.socket.settimeout(0.1)
 
380
 
 
381
        while self._http_running:
 
382
            try:
 
383
                httpd.handle_request()
 
384
            except socket.timeout:
 
385
                pass
 
386
 
 
387
    def _get_remote_url(self, path):
 
388
        path_parts = path.split(os.path.sep)
 
389
        if os.path.isabs(path):
 
390
            if path_parts[:len(self._local_path_parts)] != \
 
391
                   self._local_path_parts:
 
392
                raise BadWebserverPath(path, self.test_dir)
 
393
            remote_path = '/'.join(path_parts[len(self._local_path_parts):])
 
394
        else:
 
395
            remote_path = '/'.join(path_parts)
 
396
 
 
397
        self._http_starting.acquire()
 
398
        self._http_starting.release()
 
399
        return self._http_base_url + remote_path
 
400
 
 
401
    def log(self, format, *args):
 
402
        """Capture Server log output."""
 
403
        self.logs.append(format % args)
 
404
 
 
405
    def setUp(self):
 
406
        """See bzrlib.transport.Server.setUp."""
 
407
        self._home_dir = os.getcwdu()
 
408
        self._local_path_parts = self._home_dir.split(os.path.sep)
 
409
        self._http_starting = threading.Lock()
 
410
        self._http_starting.acquire()
 
411
        self._http_running = True
 
412
        self._http_base_url = None
 
413
        self._http_thread = threading.Thread(target=self._http_start)
 
414
        self._http_thread.setDaemon(True)
 
415
        self._http_thread.start()
 
416
        self._http_proxy = os.environ.get("http_proxy")
 
417
        if self._http_proxy is not None:
 
418
            del os.environ["http_proxy"]
 
419
        self.logs = []
 
420
 
 
421
    def tearDown(self):
 
422
        """See bzrlib.transport.Server.tearDown."""
 
423
        self._http_running = False
 
424
        self._http_thread.join()
 
425
        if self._http_proxy is not None:
 
426
            import os
 
427
            os.environ["http_proxy"] = self._http_proxy
 
428
 
 
429
    def get_url(self):
 
430
        """See bzrlib.transport.Server.get_url."""
 
431
        return self._get_remote_url(self._home_dir)
 
432
        
 
433
    def get_bogus_url(self):
 
434
        """See bzrlib.transport.Server.get_bogus_url."""
 
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
            ]