1
# Copyright (C) 2005 Canonical Ltd
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.
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.
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.
20
from cStringIO import StringIO
21
import urllib, urllib2
23
from warnings import warn
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
35
def extract_auth(url, password_manager):
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
41
scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
42
assert (scheme == 'http') or (scheme == 'https')
45
auth, netloc = netloc.split('@', 1)
47
username, password = auth.split(':', 1)
49
username, password = auth, None
51
host = netloc.split(':', 1)[0]
54
username = urllib.unquote(username)
55
if password is not None:
56
password = urllib.unquote(password)
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))
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)
73
request = urllib2.Request(url)
74
request.add_header('User-Agent', 'bzr/%s' % bzrlib.__version__)
75
response = opener.open(request)
78
class HttpTransport(Transport):
79
"""This is the transport agent for http:// access.
81
TODO: Implement pipelined versions of all of the *_multi() functions.
84
def __init__(self, base):
85
"""Set the base path where files will be stored."""
86
assert base.startswith('http://') or base.startswith('https://')
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)
97
def should_cache(self):
98
"""Return True if the data pulled across should be cached locally.
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.
108
return HttpTransport(self.base)
110
return HttpTransport(self.abspath(offset))
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
116
assert isinstance(relpath, basestring)
117
if isinstance(relpath, basestring):
118
relpath_parts = relpath.split('/')
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:
134
if len(basepath) == 0:
135
# In most filesystems, a request for the parent
136
# of root, just returns root.
139
elif p == '.' or 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, '', '', ''))
150
def has(self, relpath):
151
"""Does the target location exist?
153
TODO: HttpTransport.has() should use a HEAD request,
154
not a full GET request.
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
163
path = self.abspath(relpath)
165
# Without the read and then close()
166
# we tend to have busy sockets.
170
except urllib2.URLError, e:
171
mutter('url error code: %s for has url: %r', e.code, path)
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:
180
raise TransportError(orig_error=e)
182
def get(self, relpath, decode=False):
183
"""Get the file at the given relative path.
185
:param relpath: The relative path to the file
189
path = self.abspath(relpath)
191
except urllib2.HTTPError, e:
192
mutter('url error code: %s for has url: %r', e.code, path)
194
raise NoSuchFile(path, extra=e)
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)),
206
def put(self, relpath, f, mode=None):
207
"""Copy the file-like or string object into the location.
209
:param relpath: Location to put the contents, relative to base.
210
:param f: File-like or string object.
212
raise TransportNotPossible('http PUT not supported')
214
def mkdir(self, relpath, mode=None):
215
"""Create a directory at the given path."""
216
raise TransportNotPossible('http does not support mkdir()')
218
def rmdir(self, relpath):
219
"""See Transport.rmdir."""
220
raise TransportNotPossible('http does not support rmdir()')
222
def append(self, relpath, f):
223
"""Append the text in the file-like object into the final
226
raise TransportNotPossible('http does not support append()')
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()')
232
def copy_to(self, relpaths, other, mode=None, pb=None):
233
"""Copy a set of entries from self into another Transport.
235
:param relpaths: A list/generator of entries to be copied.
237
TODO: if other is LocalTransport, is it possible to
238
do better than put(get())?
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()')
246
return super(HttpTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
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()')
252
def delete(self, relpath):
253
"""Delete the item at relpath"""
254
raise TransportNotPossible('http does not support delete()')
256
def is_readonly(self):
257
"""See Transport.is_readonly."""
261
"""See Transport.listable."""
264
def stat(self, relpath):
265
"""Return the stat information for a file.
267
raise TransportNotPossible('http does not support stat()')
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()
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):
280
return BogusLock(relpath)
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
286
:return: A lock object, which should be passed to Transport.unlock()
288
raise TransportNotPossible('http does not support lock_write()')
291
#---------------- test server facilities ----------------
292
import BaseHTTPServer, SimpleHTTPServer, socket, time
296
class WebserverNotAvailable(Exception):
300
class BadWebserverPath(ValueError):
302
return 'path %s is not in %s' % self.args
305
class TestingHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
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(),
313
def handle_one_request(self):
314
"""Handle a single HTTP request.
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.
321
for i in xrange(1,11): # Don't try more than 10 times
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)
335
if not self.raw_requestline:
336
self.close_connection = 1
338
if not self.parse_request(): # An error code has been sent, just exit
340
mname = 'do_' + self.command
341
if not hasattr(self, mname):
342
self.send_error(501, "Unsupported method (%r)" % self.command)
344
method = getattr(self, mname)
347
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
348
def __init__(self, server_address, RequestHandlerClass, test_case):
349
BaseHTTPServer.HTTPServer.__init__(self, server_address,
351
self.test_case = test_case
354
class HttpServer(Server):
355
"""A test server for http transports."""
357
_HTTP_PORTS = range(13000, 0x8000)
359
def _http_start(self):
361
for port in self._HTTP_PORTS:
363
httpd = TestingHTTPServer(('localhost', port),
364
TestingHTTPRequestHandler,
366
except socket.error, e:
367
if e.args[0] == errno.EADDRINUSE:
369
print >>sys.stderr, "Cannot run webserver :-("
375
raise WebserverNotAvailable("Cannot run webserver :-( "
376
"no free ports in range %s..%s" %
377
(_HTTP_PORTS[0], _HTTP_PORTS[-1]))
379
self._http_base_url = 'http://localhost:%s/' % port
380
self._http_starting.release()
381
httpd.socket.settimeout(0.1)
383
while self._http_running:
385
httpd.handle_request()
386
except socket.timeout:
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):])
397
remote_path = '/'.join(path_parts)
399
self._http_starting.acquire()
400
self._http_starting.release()
401
return self._http_base_url + remote_path
403
def log(self, *args, **kwargs):
404
"""Capture Server log output."""
405
self.logs.append(args[3])
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"]
424
"""See bzrlib.transport.Server.tearDown."""
425
self._http_running = False
426
self._http_thread.join()
427
if self._http_proxy is not None:
429
os.environ["http_proxy"] = self._http_proxy
432
"""See bzrlib.transport.Server.get_url."""
433
return self._get_remote_url(self._home_dir)
435
def get_bogus_url(self):
436
"""See bzrlib.transport.Server.get_bogus_url."""
437
return 'http://jasldkjsalkdjalksjdkljasd'
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),