~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http.py

  • Committer: Robert Collins
  • Date: 2006-02-13 21:52:05 UTC
  • mto: (1534.1.22 integration)
  • mto: This revision was merged to the branch mainline in revision 1554.
  • Revision ID: robertc@robertcollins.net-20060213215205-cb3bacc87092a639
Update per review comments.

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