1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
1
# Copyright (C) 2005 Canonical Ltd
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.
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.
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
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Base implementation of Transport over http.
19
There are separate implementation modules for each http client implementation.
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16
"""Implementation of Transport over http.
22
20
from cStringIO import StringIO
21
import urllib, urllib2
23
from warnings import warn
36
from bzrlib.smart import medium
37
from bzrlib.symbol_versioning import (
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
40
30
from bzrlib.trace import mutter
41
from bzrlib.transport import (
48
# TODO: This is not used anymore by HttpTransport_urllib
49
# (extracting the auth info and prompting the user for a password
50
# have been split), only the tests still use it. It should be
51
# deleted and the tests rewritten ASAP to stay in sync.
52
33
def extract_auth(url, password_manager):
53
"""Extract auth parameters from am HTTP/HTTPS url and add them to the given
35
Extract auth parameters from am HTTP/HTTPS url and add them to the given
54
36
password manager. Return the url, minus those auth parameters (which
57
if not re.match(r'^(https?)(\+\w+)?://', url):
59
'invalid absolute url %r' % (url,))
60
scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
63
auth, netloc = netloc.split('@', 1)
39
assert url.startswith('http://') or url.startswith('https://')
40
scheme, host = url.split('//', 1)
42
host, path = host.split('/', 1)
48
auth, host = host.split('@', 1)
65
50
username, password = auth.split(':', 1)
67
52
username, password = auth, None
69
host = netloc.split(':', 1)[0]
72
username = urllib.unquote(username)
54
host, port = host.split(':', 1)
56
# FIXME: if password isn't given, should we ask for it?
73
57
if password is not None:
58
username = urllib.unquote(username)
74
59
password = urllib.unquote(password)
76
password = ui.ui_factory.get_password(
77
prompt='HTTP %(user)s@%(host)s password',
78
user=username, host=host)
79
password_manager.add_password(None, host, username, password)
80
url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
60
password_manager.add_password(None, host, username, password)
61
url = scheme + '//' + host + port + path
84
class HttpTransportBase(ConnectedTransport):
85
"""Base class for http implementations.
87
Does URL parsing, etc, but not any network IO.
89
The protocol can be given as e.g. http+urllib://host/ to use a particular
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)
74
class HttpTransport(Transport):
75
"""This is the transport agent for http:// access.
77
TODO: Implement pipelined versions of all of the *_multi() functions.
93
# _unqualified_scheme: "http" or "https"
94
# _scheme: may have "+pycurl", etc
96
def __init__(self, base, _impl_name, _from_transport=None):
80
def __init__(self, base):
97
81
"""Set the base path where files will be stored."""
98
proto_match = re.match(r'^(https?)(\+\w+)?://', base)
100
raise AssertionError("not a http url: %r" % base)
101
self._unqualified_scheme = proto_match.group(1)
102
self._impl_name = _impl_name
103
super(HttpTransportBase, self).__init__(base,
104
_from_transport=_from_transport)
106
# range hint is handled dynamically throughout the life
107
# of the transport object. We start by trying multi-range
108
# requests and if the server returns bogus results, we
109
# retry with single range requests and, finally, we
110
# forget about range if the server really can't
111
# understand. Once acquired, this piece of info is
112
# propagated to clones.
113
if _from_transport is not None:
114
self._range_hint = _from_transport._range_hint
116
self._range_hint = 'multi'
82
assert base.startswith('http://') or base.startswith('https://')
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)
93
def should_cache(self):
94
"""Return True if the data pulled across should be cached locally.
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.
104
return HttpTransport(self.base)
106
return HttpTransport(self.abspath(offset))
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
112
assert isinstance(relpath, basestring)
113
if isinstance(relpath, basestring):
114
relpath_parts = relpath.split('/')
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:
130
if len(basepath) == 0:
131
# In most filesystems, a request for the parent
132
# of root, just returns root.
135
elif p == '.' or 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, '', '', ''))
118
146
def has(self, relpath):
119
raise NotImplementedError("has() is abstract on %r" % self)
121
def get(self, relpath):
147
"""Does the target location exist?
149
TODO: HttpTransport.has() should use a HEAD request,
150
not a full GET request.
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
159
path = self.abspath(relpath)
161
# Without the read and then close()
162
# we tend to have busy sockets.
166
except urllib2.URLError, e:
167
mutter('url error code: %s for has url: %r', e.code, path)
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:
176
raise TransportError(orig_error=e)
178
def get(self, relpath, decode=False):
122
179
"""Get the file at the given relative path.
124
181
:param relpath: The relative path to the file
126
code, response_file = self._get(relpath, None)
127
# FIXME: some callers want an iterable... One step forward, three steps
128
# backwards :-/ And not only an iterable, but an iterable that can be
129
# seeked backwards, so we will never be able to do that. One such
130
# known client is bzrlib.bundle.serializer.v4.get_bundle_reader. At the
131
# time of this writing it's even the only known client -- vila20071203
132
return StringIO(response_file.read())
134
def _get(self, relpath, ranges, tail_amount=0):
135
"""Get a file, or part of a file.
137
:param relpath: Path relative to transport base URL
138
:param ranges: None to get the whole file;
139
or a list of _CoalescedOffset to fetch parts of a file.
140
:param tail_amount: The amount to get from the end of the file.
142
:returns: (http_code, result_file)
144
raise NotImplementedError(self._get)
146
def _remote_path(self, relpath):
147
"""See ConnectedTransport._remote_path.
149
user and passwords are not embedded in the path provided to the server.
151
relative = urlutils.unescape(relpath).encode('utf-8')
152
path = self._combine_paths(self._path, relative)
153
return self._unsplit_url(self._unqualified_scheme,
154
None, None, self._host, self._port, path)
156
def _create_auth(self):
157
"""Returns a dict returning the credentials provided at build time."""
158
auth = dict(host=self._host, port=self._port,
159
user=self._user, password=self._password,
160
protocol=self._unqualified_scheme,
164
def get_smart_medium(self):
165
"""See Transport.get_smart_medium."""
166
if self._medium is None:
167
# Since medium holds some state (smart server probing at least), we
168
# need to keep it around. Note that this is needed because medium
169
# has the same 'base' attribute as the transport so it can't be
170
# shared between transports having different bases.
171
self._medium = SmartClientHTTPMedium(self)
175
def _degrade_range_hint(self, relpath, ranges, exc_info):
176
if self._range_hint == 'multi':
177
self._range_hint = 'single'
178
mutter('Retry "%s" with single range request' % relpath)
179
elif self._range_hint == 'single':
180
self._range_hint = None
181
mutter('Retry "%s" without ranges' % relpath)
183
# We tried all the tricks, but nothing worked. We re-raise the
184
# original exception; the 'mutter' calls above will indicate that
185
# further tries were unsuccessful
186
raise exc_info[0], exc_info[1], exc_info[2]
188
# _coalesce_offsets is a helper for readv, it try to combine ranges without
189
# degrading readv performances. _bytes_to_read_before_seek is the value
190
# used for the limit parameter and has been tuned for other transports. For
191
# HTTP, the name is inappropriate but the parameter is still useful and
192
# helps reduce the number of chunks in the response. The overhead for a
193
# chunk (headers, length, footer around the data itself is variable but
194
# around 50 bytes. We use 128 to reduce the range specifiers that appear in
195
# the header, some servers (notably Apache) enforce a maximum length for a
196
# header and issue a '400: Bad request' error when too much ranges are
198
_bytes_to_read_before_seek = 128
199
# No limit on the offset number that get combined into one, we are trying
200
# to avoid downloading the whole file.
201
_max_readv_combine = 0
202
# By default Apache has a limit of ~400 ranges before replying with a 400
203
# Bad Request. So we go underneath that amount to be safe.
204
_max_get_ranges = 200
205
# We impose no limit on the range size. But see _pycurl.py for a different
209
def _readv(self, relpath, offsets):
210
"""Get parts of the file at the given relative path.
212
:param offsets: A list of (offset, size) tuples.
213
:param return: A list or generator of (offset, data) tuples
215
# offsets may be a generator, we will iterate it several times, so
217
offsets = list(offsets)
220
retried_offset = None
224
# Coalesce the offsets to minimize the GET requests issued
225
sorted_offsets = sorted(offsets)
226
coalesced = self._coalesce_offsets(
227
sorted_offsets, limit=self._max_readv_combine,
228
fudge_factor=self._bytes_to_read_before_seek,
229
max_size=self._get_max_size)
231
# Turn it into a list, we will iterate it several times
232
coalesced = list(coalesced)
233
if 'http' in debug.debug_flags:
234
mutter('http readv of %s offsets => %s collapsed %s',
235
relpath, len(offsets), len(coalesced))
237
# Cache the data read, but only until it's been used
239
# We will iterate on the data received from the GET requests and
240
# serve the corresponding offsets respecting the initial order. We
241
# need an offset iterator for that.
242
iter_offsets = iter(offsets)
243
cur_offset_and_size = iter_offsets.next()
246
for cur_coal, rfile in self._coalesce_readv(relpath, coalesced):
247
# Split the received chunk
248
for offset, size in cur_coal.ranges:
249
start = cur_coal.start + offset
251
data = rfile.read(size)
254
raise errors.ShortReadvError(relpath, start, size,
256
if (start, size) == cur_offset_and_size:
257
# The offset requested are sorted as the coalesced
258
# ones, no need to cache. Win !
259
yield cur_offset_and_size[0], data
260
cur_offset_and_size = iter_offsets.next()
262
# Different sorting. We need to cache.
263
data_map[(start, size)] = data
265
# Yield everything we can
266
while cur_offset_and_size in data_map:
267
# Clean the cached data since we use it
268
# XXX: will break if offsets contains duplicates --
270
this_data = data_map.pop(cur_offset_and_size)
271
yield cur_offset_and_size[0], this_data
272
cur_offset_and_size = iter_offsets.next()
274
except (errors.ShortReadvError, errors.InvalidRange,
275
errors.InvalidHttpRange), e:
276
mutter('Exception %r: %s during http._readv',e, e)
277
if (not isinstance(e, errors.ShortReadvError)
278
or retried_offset == cur_offset_and_size):
279
# We don't degrade the range hint for ShortReadvError since
280
# they do not indicate a problem with the server ability to
281
# handle ranges. Except when we fail to get back a required
282
# offset twice in a row. In that case, falling back to
283
# single range or whole file should help or end up in a
285
self._degrade_range_hint(relpath, coalesced, sys.exc_info())
286
# Some offsets may have been already processed, so we retry
287
# only the unsuccessful ones.
288
offsets = [cur_offset_and_size] + [o for o in iter_offsets]
289
retried_offset = cur_offset_and_size
292
def _coalesce_readv(self, relpath, coalesced):
293
"""Issue several GET requests to satisfy the coalesced offsets"""
295
def get_and_yield(relpath, coalesced):
297
# Note that the _get below may raise
298
# errors.InvalidHttpRange. It's the caller's responsibility to
299
# decide how to retry since it may provide different coalesced
301
code, rfile = self._get(relpath, coalesced)
302
for coal in coalesced:
305
if self._range_hint is None:
306
# Download whole file
307
for c, rfile in get_and_yield(relpath, coalesced):
310
total = len(coalesced)
311
if self._range_hint == 'multi':
312
max_ranges = self._max_get_ranges
313
elif self._range_hint == 'single':
316
raise AssertionError("Unknown _range_hint %r"
317
% (self._range_hint,))
318
# TODO: Some web servers may ignore the range requests and return
319
# the whole file, we may want to detect that and avoid further
321
# Hint: test_readv_multiple_get_requests will fail once we do that
324
for coal in coalesced:
325
if ((self._get_max_size > 0
326
and cumul + coal.length > self._get_max_size)
327
or len(ranges) >= max_ranges):
328
# Get that much and yield
329
for c, rfile in get_and_yield(relpath, ranges):
331
# Restart with the current offset
337
# Get the rest and yield
338
for c, rfile in get_and_yield(relpath, ranges):
341
def recommended_page_size(self):
342
"""See Transport.recommended_page_size().
344
For HTTP we suggest a large page size to reduce the overhead
345
introduced by latency.
349
def _post(self, body_bytes):
350
"""POST body_bytes to .bzr/smart on this transport.
352
:returns: (response code, response body file-like object).
354
# TODO: Requiring all the body_bytes to be available at the beginning of
355
# the POST may require large client buffers. It would be nice to have
356
# an interface that allows streaming via POST when possible (and
357
# degrades to a local buffer when not).
358
raise NotImplementedError(self._post)
360
def put_file(self, relpath, f, mode=None):
361
"""Copy the file-like object into the location.
185
path = self.abspath(relpath)
187
except urllib2.HTTPError, e:
188
mutter('url error code: %s for has url: %r', e.code, path)
190
raise NoSuchFile(path, extra=e)
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)),
202
def put(self, relpath, f, mode=None):
203
"""Copy the file-like or string object into the location.
363
205
:param relpath: Location to put the contents, relative to base.
364
:param f: File-like object.
206
:param f: File-like or string object.
366
raise errors.TransportNotPossible('http PUT not supported')
208
raise TransportNotPossible('http PUT not supported')
368
210
def mkdir(self, relpath, mode=None):
369
211
"""Create a directory at the given path."""
370
raise errors.TransportNotPossible('http does not support mkdir()')
212
raise TransportNotPossible('http does not support mkdir()')
372
214
def rmdir(self, relpath):
373
215
"""See Transport.rmdir."""
374
raise errors.TransportNotPossible('http does not support rmdir()')
216
raise TransportNotPossible('http does not support rmdir()')
376
def append_file(self, relpath, f, mode=None):
218
def append(self, relpath, f):
377
219
"""Append the text in the file-like object into the final
380
raise errors.TransportNotPossible('http does not support append()')
222
raise TransportNotPossible('http does not support append()')
382
224
def copy(self, rel_from, rel_to):
383
225
"""Copy the item at rel_from to the location at rel_to"""
384
raise errors.TransportNotPossible('http does not support copy()')
226
raise TransportNotPossible('http does not support copy()')
386
228
def copy_to(self, relpaths, other, mode=None, pb=None):
387
229
"""Copy a set of entries from self into another Transport.
451
282
:return: A lock object, which should be passed to Transport.unlock()
453
raise errors.TransportNotPossible('http does not support lock_write()')
455
def _attempted_range_header(self, offsets, tail_amount):
456
"""Prepare a HTTP Range header at a level the server should accept.
458
:return: the range header representing offsets/tail_amount or None if
459
no header can be built.
462
if self._range_hint == 'multi':
463
# Generate the header describing all offsets
464
return self._range_header(offsets, tail_amount)
465
elif self._range_hint == 'single':
466
# Combine all the requested ranges into a single
469
if tail_amount not in (0, None):
470
# Nothing we can do here to combine ranges with tail_amount
471
# in a single range, just returns None. The whole file
472
# should be downloaded.
475
start = offsets[0].start
477
end = last.start + last.length - 1
478
whole = self._coalesce_offsets([(start, end - start + 1)],
479
limit=0, fudge_factor=0)
480
return self._range_header(list(whole), 0)
482
# Only tail_amount, requested, leave range_header
484
return self._range_header(offsets, tail_amount)
489
def _range_header(ranges, tail_amount):
490
"""Turn a list of bytes ranges into a HTTP Range header value.
492
:param ranges: A list of _CoalescedOffset
493
:param tail_amount: The amount to get from the end of the file.
495
:return: HTTP range header string.
497
At least a non-empty ranges *or* a tail_amount must be
501
for offset in ranges:
502
strings.append('%d-%d' % (offset.start,
503
offset.start + offset.length - 1))
506
strings.append('-%d' % tail_amount)
508
return ','.join(strings)
510
def _redirected_to(self, source, target):
511
"""Returns a transport suitable to re-issue a redirected request.
513
:param source: The source url as returned by the server.
514
:param target: The target url as returned by the server.
516
The redirection can be handled only if the relpath involved is not
517
renamed by the redirection.
519
:returns: A transport or None.
521
def relpath(abspath):
522
"""Returns the path relative to our base.
524
The constraints are weaker than the real relpath method because the
525
abspath is coming from the server and may slightly differ from our
526
base. We don't check the scheme, host, port, user, password parts,
527
relying on the caller to give us a proper url (i.e. one returned by
528
the server mirroring the one we sent).
533
path) = self._split_url(abspath)
535
return path[pl:].strip('/')
537
relpath = relpath(source)
538
if not target.endswith(relpath):
539
# The final part of the url has been renamed, we can't handle the
546
path) = self._split_url(target)
547
# Recalculate base path. This is needed to ensure that when the
548
# redirected tranport will be used to re-try whatever request was
549
# redirected, we end up with the same url
550
base_path = path[:-len(relpath)]
551
if scheme in ('http', 'https'):
552
# Same protocol family (i.e. http[s]), we will preserve the same
553
# http client implementation when a redirection occurs from one to
554
# the other (otherwise users may be surprised that bzr switches
555
# from one implementation to the other, and devs may suffer
557
if (scheme == self._unqualified_scheme
558
and host == self._host
559
and port == self._port
560
and (user is None or user == self._user)):
561
# If a user is specified, it should match, we don't care about
562
# passwords, wrong passwords will be rejected anyway.
563
new_transport = self.clone(base_path)
565
# Rebuild the url preserving the scheme qualification and the
566
# credentials (if they don't apply, the redirected to server
567
# will tell us, but if they do apply, we avoid prompting the
569
redir_scheme = scheme + '+' + self._impl_name
570
new_url = self._unsplit_url(redir_scheme,
571
self._user, self._password,
574
new_transport = get_transport(new_url)
576
# Redirected to a different protocol
577
new_url = self._unsplit_url(scheme,
581
new_transport = get_transport(new_url)
585
# TODO: May be better located in smart/medium.py with the other
586
# SmartMedium classes
587
class SmartClientHTTPMedium(medium.SmartClientMedium):
589
def __init__(self, http_transport):
590
super(SmartClientHTTPMedium, self).__init__(http_transport.base)
591
# We don't want to create a circular reference between the http
592
# transport and its associated medium. Since the transport will live
593
# longer than the medium, the medium keep only a weak reference to its
595
self._http_transport_ref = weakref.ref(http_transport)
597
def get_request(self):
598
return SmartClientHTTPMediumRequest(self)
600
def should_probe(self):
603
def remote_path_from_transport(self, transport):
604
# Strip the optional 'bzr+' prefix from transport so it will have the
605
# same scheme as self.
606
transport_base = transport.base
607
if transport_base.startswith('bzr+'):
608
transport_base = transport_base[4:]
609
rel_url = urlutils.relative_url(self.base, transport_base)
610
return urllib.unquote(rel_url)
612
def send_http_smart_request(self, bytes):
614
# Get back the http_transport hold by the weak reference
615
t = self._http_transport_ref()
616
code, body_filelike = t._post(bytes)
618
raise InvalidHttpResponse(
619
t._remote_path('.bzr/smart'),
620
'Expected 200 response code, got %r' % (code,))
621
except errors.InvalidHttpResponse, e:
622
raise errors.SmartProtocolError(str(e))
625
def _report_activity(self, bytes, direction):
626
"""See SmartMedium._report_activity.
628
Does nothing; the underlying plain HTTP transport will report the
629
activity that this medium would report.
634
# TODO: May be better located in smart/medium.py with the other
635
# SmartMediumRequest classes
636
class SmartClientHTTPMediumRequest(medium.SmartClientMediumRequest):
637
"""A SmartClientMediumRequest that works with an HTTP medium."""
639
def __init__(self, client_medium):
640
medium.SmartClientMediumRequest.__init__(self, client_medium)
643
def _accept_bytes(self, bytes):
644
self._buffer += bytes
646
def _finished_writing(self):
647
data = self._medium.send_http_smart_request(self._buffer)
648
self._response_body = data
650
def _read_bytes(self, count):
651
"""See SmartClientMediumRequest._read_bytes."""
652
return self._response_body.read(count)
654
def _read_line(self):
655
line, excess = medium._get_line(self._response_body.read)
657
raise AssertionError(
658
'_get_line returned excess bytes, but this mediumrequest '
659
'cannot handle excess. (%r)' % (excess,))
662
def _finished_reading(self):
663
"""See SmartClientMediumRequest._finished_reading."""
284
raise TransportNotPossible('http does not support lock_write()')
287
#---------------- test server facilities ----------------
288
import BaseHTTPServer, SimpleHTTPServer, socket, time
292
class WebserverNotAvailable(Exception):
296
class BadWebserverPath(ValueError):
298
return 'path %s is not in %s' % self.args
301
class TestingHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
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(),
309
def handle_one_request(self):
310
"""Handle a single HTTP request.
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.
317
for i in xrange(1,11): # Don't try more than 10 times
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)
331
if not self.raw_requestline:
332
self.close_connection = 1
334
if not self.parse_request(): # An error code has been sent, just exit
336
mname = 'do_' + self.command
337
if not hasattr(self, mname):
338
self.send_error(501, "Unsupported method (%r)" % self.command)
340
method = getattr(self, mname)
343
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
344
def __init__(self, server_address, RequestHandlerClass, test_case):
345
BaseHTTPServer.HTTPServer.__init__(self, server_address,
347
self.test_case = test_case
350
class HttpServer(Server):
351
"""A test server for http transports."""
353
_HTTP_PORTS = range(13000, 0x8000)
355
def _http_start(self):
357
for port in self._HTTP_PORTS:
359
httpd = TestingHTTPServer(('localhost', port),
360
TestingHTTPRequestHandler,
362
except socket.error, e:
363
if e.args[0] == errno.EADDRINUSE:
365
print >>sys.stderr, "Cannot run webserver :-("
371
raise WebserverNotAvailable("Cannot run webserver :-( "
372
"no free ports in range %s..%s" %
373
(_HTTP_PORTS[0], _HTTP_PORTS[-1]))
375
self._http_base_url = 'http://localhost:%s/' % port
376
self._http_starting.release()
377
httpd.socket.settimeout(0.1)
379
while self._http_running:
381
httpd.handle_request()
382
except socket.timeout:
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):])
393
remote_path = '/'.join(path_parts)
395
self._http_starting.acquire()
396
self._http_starting.release()
397
return self._http_base_url + remote_path
399
def log(self, *args, **kwargs):
400
"""Capture Server log output."""
401
self.logs.append(args[3])
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"]
420
"""See bzrlib.transport.Server.tearDown."""
421
self._http_running = False
422
self._http_thread.join()
423
if self._http_proxy is not None:
425
os.environ["http_proxy"] = self._http_proxy
428
"""See bzrlib.transport.Server.get_url."""
429
return self._get_remote_url(self._home_dir)
431
def get_bogus_url(self):
432
"""See bzrlib.transport.Server.get_bogus_url."""
433
return 'http://jasldkjsalkdjalksjdkljasd'
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),