##// END OF EJS Templates
Import new http library as mercurial.httpclient....
Augie Fackler -
r14243:861f2821 default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (650 lines changed) Show them Hide them
@@ -0,0 +1,650
1 # Copyright 2010, Google Inc.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 """Improved HTTP/1.1 client library
30
31 This library contains an HTTPConnection which is similar to the one in
32 httplib, but has several additional features:
33
34 * supports keepalives natively
35 * uses select() to block for incoming data
36 * notices when the server responds early to a request
37 * implements ssl inline instead of in a different class
38 """
39
40 import cStringIO
41 import errno
42 import httplib
43 import logging
44 import rfc822
45 import select
46 import socket
47
48 import socketutil
49
50 logger = logging.getLogger(__name__)
51
52 __all__ = ['HTTPConnection', 'HTTPResponse']
53
54 HTTP_VER_1_0 = 'HTTP/1.0'
55 HTTP_VER_1_1 = 'HTTP/1.1'
56
57 _LEN_CLOSE_IS_END = -1
58
59 OUTGOING_BUFFER_SIZE = 1 << 15
60 INCOMING_BUFFER_SIZE = 1 << 20
61
62 HDR_ACCEPT_ENCODING = 'accept-encoding'
63 HDR_CONNECTION_CTRL = 'connection'
64 HDR_CONTENT_LENGTH = 'content-length'
65 HDR_XFER_ENCODING = 'transfer-encoding'
66
67 XFER_ENCODING_CHUNKED = 'chunked'
68
69 CONNECTION_CLOSE = 'close'
70
71 EOL = '\r\n'
72 _END_HEADERS = EOL * 2
73
74 # Based on some searching around, 1 second seems like a reasonable
75 # default here.
76 TIMEOUT_ASSUME_CONTINUE = 1
77 TIMEOUT_DEFAULT = None
78
79
80 class HTTPResponse(object):
81 """Response from an HTTP server.
82
83 The response will continue to load as available. If you need the
84 complete response before continuing, check the .complete() method.
85 """
86 def __init__(self, sock, timeout):
87 self.sock = sock
88 self.raw_response = ''
89 self._body = None
90 self._headers_len = 0
91 self._content_len = 0
92 self.headers = None
93 self.will_close = False
94 self.status_line = ''
95 self.status = None
96 self.http_version = None
97 self.reason = None
98 self._chunked = False
99 self._chunked_done = False
100 self._chunked_until_next = 0
101 self._chunked_skip_bytes = 0
102 self._chunked_preloaded_block = None
103
104 self._read_location = 0
105 self._eol = EOL
106
107 self._timeout = timeout
108
109 @property
110 def _end_headers(self):
111 return self._eol * 2
112
113 def complete(self):
114 """Returns true if this response is completely loaded.
115 """
116 if self._chunked:
117 return self._chunked_done
118 if self._content_len == _LEN_CLOSE_IS_END:
119 return False
120 return self._body is not None and len(self._body) >= self._content_len
121
122 def readline(self):
123 """Read a single line from the response body.
124
125 This may block until either a line ending is found or the
126 response is complete.
127 """
128 eol = self._body.find('\n', self._read_location)
129 while eol == -1 and not self.complete():
130 self._select()
131 eol = self._body.find('\n', self._read_location)
132 if eol != -1:
133 eol += 1
134 else:
135 eol = len(self._body)
136 data = self._body[self._read_location:eol]
137 self._read_location = eol
138 return data
139
140 def read(self, length=None):
141 # if length is None, unbounded read
142 while (not self.complete() # never select on a finished read
143 and (not length # unbounded, so we wait for complete()
144 or (self._read_location + length) > len(self._body))):
145 self._select()
146 if not length:
147 length = len(self._body) - self._read_location
148 elif len(self._body) < (self._read_location + length):
149 length = len(self._body) - self._read_location
150 r = self._body[self._read_location:self._read_location + length]
151 self._read_location += len(r)
152 if self.complete() and self.will_close:
153 self.sock.close()
154 return r
155
156 def _select(self):
157 r, _, _ = select.select([self.sock], [], [], self._timeout)
158 if not r:
159 # socket was not readable. If the response is not complete
160 # and we're not a _LEN_CLOSE_IS_END response, raise a timeout.
161 # If we are a _LEN_CLOSE_IS_END response and we have no data,
162 # raise a timeout.
163 if not (self.complete() or
164 (self._content_len == _LEN_CLOSE_IS_END and self._body)):
165 logger.info('timed out with timeout of %s', self._timeout)
166 raise HTTPTimeoutException('timeout reading data')
167 logger.info('cl: %r body: %r', self._content_len, self._body)
168 data = self.sock.recv(INCOMING_BUFFER_SIZE)
169 logger.debug('response read %d data during _select', len(data))
170 if not data:
171 if not self.headers:
172 self._load_response(self._end_headers)
173 self._content_len = 0
174 elif self._content_len == _LEN_CLOSE_IS_END:
175 self._content_len = len(self._body)
176 return False
177 else:
178 self._load_response(data)
179 return True
180
181 def _chunked_parsedata(self, data):
182 if self._chunked_preloaded_block:
183 data = self._chunked_preloaded_block + data
184 self._chunked_preloaded_block = None
185 while data:
186 logger.debug('looping with %d data remaining', len(data))
187 # Slice out anything we should skip
188 if self._chunked_skip_bytes:
189 if len(data) <= self._chunked_skip_bytes:
190 self._chunked_skip_bytes -= len(data)
191 data = ''
192 break
193 else:
194 data = data[self._chunked_skip_bytes:]
195 self._chunked_skip_bytes = 0
196
197 # determine how much is until the next chunk
198 if self._chunked_until_next:
199 amt = self._chunked_until_next
200 logger.debug('reading remaining %d of existing chunk', amt)
201 self._chunked_until_next = 0
202 body = data
203 else:
204 try:
205 amt, body = data.split(self._eol, 1)
206 except ValueError:
207 self._chunked_preloaded_block = data
208 logger.debug('saving %r as a preloaded block for chunked',
209 self._chunked_preloaded_block)
210 return
211 amt = int(amt, base=16)
212 logger.debug('reading chunk of length %d', amt)
213 if amt == 0:
214 self._chunked_done = True
215
216 # read through end of what we have or the chunk
217 self._body += body[:amt]
218 if len(body) >= amt:
219 data = body[amt:]
220 self._chunked_skip_bytes = len(self._eol)
221 else:
222 self._chunked_until_next = amt - len(body)
223 self._chunked_skip_bytes = 0
224 data = ''
225
226 def _load_response(self, data):
227 if self._chunked:
228 self._chunked_parsedata(data)
229 return
230 elif self._body is not None:
231 self._body += data
232 return
233
234 # We haven't seen end of headers yet
235 self.raw_response += data
236 # This is a bogus server with bad line endings
237 if self._eol not in self.raw_response:
238 for bad_eol in ('\n', '\r'):
239 if (bad_eol in self.raw_response
240 # verify that bad_eol is not the end of the incoming data
241 # as this could be a response line that just got
242 # split between \r and \n.
243 and (self.raw_response.index(bad_eol) <
244 (len(self.raw_response) - 1))):
245 logger.info('bogus line endings detected, '
246 'using %r for EOL', bad_eol)
247 self._eol = bad_eol
248 break
249 # exit early if not at end of headers
250 if self._end_headers not in self.raw_response or self.headers:
251 return
252
253 # handle 100-continue response
254 hdrs, body = self.raw_response.split(self._end_headers, 1)
255 http_ver, status = hdrs.split(' ', 1)
256 if status.startswith('100'):
257 self.raw_response = body
258 logger.debug('continue seen, setting body to %r', body)
259 return
260
261 # arriving here means we should parse response headers
262 # as all headers have arrived completely
263 hdrs, body = self.raw_response.split(self._end_headers, 1)
264 del self.raw_response
265 if self._eol in hdrs:
266 self.status_line, hdrs = hdrs.split(self._eol, 1)
267 else:
268 self.status_line = hdrs
269 hdrs = ''
270 # TODO HTTP < 1.0 support
271 (self.http_version, self.status,
272 self.reason) = self.status_line.split(' ', 2)
273 self.status = int(self.status)
274 if self._eol != EOL:
275 hdrs = hdrs.replace(self._eol, '\r\n')
276 headers = rfc822.Message(cStringIO.StringIO(hdrs))
277 if HDR_CONTENT_LENGTH in headers:
278 self._content_len = int(headers[HDR_CONTENT_LENGTH])
279 if self.http_version == HTTP_VER_1_0:
280 self.will_close = True
281 elif HDR_CONNECTION_CTRL in headers:
282 self.will_close = (
283 headers[HDR_CONNECTION_CTRL].lower() == CONNECTION_CLOSE)
284 if self._content_len == 0:
285 self._content_len = _LEN_CLOSE_IS_END
286 if (HDR_XFER_ENCODING in headers
287 and headers[HDR_XFER_ENCODING].lower() == XFER_ENCODING_CHUNKED):
288 self._body = ''
289 self._chunked_parsedata(body)
290 self._chunked = True
291 if self._body is None:
292 self._body = body
293 self.headers = headers
294
295
296 class HTTPConnection(object):
297 """Connection to a single http server.
298
299 Supports 100-continue and keepalives natively. Uses select() for
300 non-blocking socket operations.
301 """
302 http_version = HTTP_VER_1_1
303 response_class = HTTPResponse
304
305 def __init__(self, host, port=None, use_ssl=None, ssl_validator=None,
306 timeout=TIMEOUT_DEFAULT,
307 continue_timeout=TIMEOUT_ASSUME_CONTINUE,
308 proxy_hostport=None, **ssl_opts):
309 """Create a new HTTPConnection.
310
311 Args:
312 host: The host to which we'll connect.
313 port: Optional. The port over which we'll connect. Default 80 for
314 non-ssl, 443 for ssl.
315 use_ssl: Optional. Wether to use ssl. Defaults to False if port is
316 not 443, true if port is 443.
317 ssl_validator: a function(socket) to validate the ssl cert
318 timeout: Optional. Connection timeout, default is TIMEOUT_DEFAULT.
319 continue_timeout: Optional. Timeout for waiting on an expected
320 "100 Continue" response. Default is TIMEOUT_ASSUME_CONTINUE.
321 proxy_hostport: Optional. Tuple of (host, port) to use as an http
322 proxy for the connection. Default is to not use a proxy.
323 """
324 if port is None and host.count(':') == 1 or ']:' in host:
325 host, port = host.rsplit(':', 1)
326 port = int(port)
327 if '[' in host:
328 host = host[1:-1]
329 if use_ssl is None and port is None:
330 use_ssl = False
331 port = 80
332 elif use_ssl is None:
333 use_ssl = (port == 443)
334 elif port is None:
335 port = (use_ssl and 443 or 80)
336 self.port = port
337 if use_ssl and not socketutil.have_ssl:
338 raise Exception('ssl requested but unavailable on this Python')
339 self.ssl = use_ssl
340 self.ssl_opts = ssl_opts
341 self._ssl_validator = ssl_validator
342 self.host = host
343 self.sock = None
344 self._current_response = None
345 self._current_response_taken = False
346 if proxy_hostport is None:
347 self._proxy_host = self._proxy_port = None
348 else:
349 self._proxy_host, self._proxy_port = proxy_hostport
350
351 self.timeout = timeout
352 self.continue_timeout = continue_timeout
353
354 def _connect(self):
355 """Connect to the host and port specified in __init__."""
356 if self.sock:
357 return
358 if self._proxy_host is not None:
359 logger.info('Connecting to http proxy %s:%s',
360 self._proxy_host, self._proxy_port)
361 sock = socketutil.create_connection((self._proxy_host,
362 self._proxy_port))
363 if self.ssl:
364 # TODO proxy header support
365 data = self.buildheaders('CONNECT', '%s:%d' % (self.host,
366 self.port),
367 {}, HTTP_VER_1_0)
368 sock.send(data)
369 sock.setblocking(0)
370 r = self.response_class(sock, self.timeout)
371 timeout_exc = HTTPTimeoutException(
372 'Timed out waiting for CONNECT response from proxy')
373 while not r.complete():
374 try:
375 if not r._select():
376 raise timeout_exc
377 except HTTPTimeoutException:
378 # This raise/except pattern looks goofy, but
379 # _select can raise the timeout as well as the
380 # loop body. I wish it wasn't this convoluted,
381 # but I don't have a better solution
382 # immediately handy.
383 raise timeout_exc
384 if r.status != 200:
385 raise HTTPProxyConnectFailedException(
386 'Proxy connection failed: %d %s' % (r.status,
387 r.read()))
388 logger.info('CONNECT (for SSL) to %s:%s via proxy succeeded.',
389 self.host, self.port)
390 else:
391 sock = socketutil.create_connection((self.host, self.port))
392 if self.ssl:
393 logger.debug('wrapping socket for ssl with options %r',
394 self.ssl_opts)
395 sock = socketutil.wrap_socket(sock, **self.ssl_opts)
396 if self._ssl_validator:
397 self._ssl_validator(sock)
398 sock.setblocking(0)
399 self.sock = sock
400
401 def buildheaders(self, method, path, headers, http_ver):
402 if self.ssl and self.port == 443 or self.port == 80:
403 # default port for protocol, so leave it out
404 hdrhost = self.host
405 else:
406 # include nonstandard port in header
407 if ':' in self.host: # must be IPv6
408 hdrhost = '[%s]:%d' % (self.host, self.port)
409 else:
410 hdrhost = '%s:%d' % (self.host, self.port)
411 if self._proxy_host and not self.ssl:
412 # When talking to a regular http proxy we must send the
413 # full URI, but in all other cases we must not (although
414 # technically RFC 2616 says servers must accept our
415 # request if we screw up, experimentally few do that
416 # correctly.)
417 assert path[0] == '/', 'path must start with a /'
418 path = 'http://%s%s' % (hdrhost, path)
419 outgoing = ['%s %s %s%s' % (method, path, http_ver, EOL)]
420 headers['host'] = ('Host', hdrhost)
421 headers[HDR_ACCEPT_ENCODING] = (HDR_ACCEPT_ENCODING, 'identity')
422 for hdr, val in headers.itervalues():
423 outgoing.append('%s: %s%s' % (hdr, val, EOL))
424 outgoing.append(EOL)
425 return ''.join(outgoing)
426
427 def close(self):
428 """Close the connection to the server.
429
430 This is a no-op if the connection is already closed. The
431 connection may automatically close if requessted by the server
432 or required by the nature of a response.
433 """
434 if self.sock is None:
435 return
436 self.sock.close()
437 self.sock = None
438 logger.info('closed connection to %s on %s', self.host, self.port)
439
440 def busy(self):
441 """Returns True if this connection object is currently in use.
442
443 If a response is still pending, this will return True, even if
444 the request has finished sending. In the future,
445 HTTPConnection may transparently juggle multiple connections
446 to the server, in which case this will be useful to detect if
447 any of those connections is ready for use.
448 """
449 cr = self._current_response
450 if cr is not None:
451 if self._current_response_taken:
452 if cr.will_close:
453 self.sock = None
454 self._current_response = None
455 return False
456 elif cr.complete():
457 self._current_response = None
458 return False
459 return True
460 return False
461
462 def request(self, method, path, body=None, headers={},
463 expect_continue=False):
464 """Send a request to the server.
465
466 For increased flexibility, this does not return the response
467 object. Future versions of HTTPConnection that juggle multiple
468 sockets will be able to send (for example) 5 requests all at
469 once, and then let the requests arrive as data is
470 available. Use the `getresponse()` method to retrieve the
471 response.
472 """
473 if self.busy():
474 raise httplib.CannotSendRequest(
475 'Can not send another request before '
476 'current response is read!')
477 self._current_response_taken = False
478
479 logger.info('sending %s request for %s to %s on port %s',
480 method, path, self.host, self.port)
481 hdrs = dict((k.lower(), (k, v)) for k, v in headers.iteritems())
482 if hdrs.get('expect', ('', ''))[1].lower() == '100-continue':
483 expect_continue = True
484 elif expect_continue:
485 hdrs['expect'] = ('Expect', '100-Continue')
486
487 chunked = False
488 if body and HDR_CONTENT_LENGTH not in hdrs:
489 if getattr(body, '__len__', False):
490 hdrs[HDR_CONTENT_LENGTH] = (HDR_CONTENT_LENGTH, len(body))
491 elif getattr(body, 'read', False):
492 hdrs[HDR_XFER_ENCODING] = (HDR_XFER_ENCODING,
493 XFER_ENCODING_CHUNKED)
494 chunked = True
495 else:
496 raise BadRequestData('body has no __len__() nor read()')
497
498 self._connect()
499 outgoing_headers = self.buildheaders(
500 method, path, hdrs, self.http_version)
501 response = None
502 first = True
503
504 def reconnect(where):
505 logger.info('reconnecting during %s', where)
506 self.close()
507 self._connect()
508
509 while ((outgoing_headers or body)
510 and not (response and response.complete())):
511 select_timeout = self.timeout
512 out = outgoing_headers or body
513 blocking_on_continue = False
514 if expect_continue and not outgoing_headers and not (
515 response and response.headers):
516 logger.info(
517 'waiting up to %s seconds for'
518 ' continue response from server',
519 self.continue_timeout)
520 select_timeout = self.continue_timeout
521 blocking_on_continue = True
522 out = False
523 if out:
524 w = [self.sock]
525 else:
526 w = []
527 r, w, x = select.select([self.sock], w, [], select_timeout)
528 # if we were expecting a 100 continue and it's been long
529 # enough, just go ahead and assume it's ok. This is the
530 # recommended behavior from the RFC.
531 if r == w == x == []:
532 if blocking_on_continue:
533 expect_continue = False
534 logger.info('no response to continue expectation from '
535 'server, optimistically sending request body')
536 else:
537 raise HTTPTimeoutException('timeout sending data')
538 # TODO exceptional conditions with select? (what are those be?)
539 # TODO if the response is loading, must we finish sending at all?
540 #
541 # Certainly not if it's going to close the connection and/or
542 # the response is already done...I think.
543 was_first = first
544
545 # incoming data
546 if r:
547 try:
548 data = r[0].recv(INCOMING_BUFFER_SIZE)
549 if not data:
550 logger.info('socket appears closed in read')
551 outgoing_headers = body = None
552 break
553 if response is None:
554 response = self.response_class(r[0], self.timeout)
555 response._load_response(data)
556 if (response._content_len == _LEN_CLOSE_IS_END
557 and len(data) == 0):
558 response._content_len = len(response._body)
559 if response.complete():
560 w = []
561 response.will_close = True
562 except socket.error, e:
563 if e[0] != errno.EPIPE and not was_first:
564 raise
565 if (response._content_len
566 and response._content_len != _LEN_CLOSE_IS_END):
567 outgoing_headers = sent_data + outgoing_headers
568 reconnect('read')
569
570 # outgoing data
571 if w and out:
572 try:
573 if getattr(out, 'read', False):
574 data = out.read(OUTGOING_BUFFER_SIZE)
575 if not data:
576 continue
577 if len(data) < OUTGOING_BUFFER_SIZE:
578 if chunked:
579 body = '0' + EOL + EOL
580 else:
581 body = None
582 if chunked:
583 out = hex(len(data))[2:] + EOL + data + EOL
584 else:
585 out = data
586 amt = w[0].send(out)
587 except socket.error, e:
588 if e[0] == socket.SSL_ERROR_WANT_WRITE and self.ssl:
589 # This means that SSL hasn't flushed its buffer into
590 # the socket yet.
591 # TODO: find a way to block on ssl flushing its buffer
592 # similar to selecting on a raw socket.
593 continue
594 elif (e[0] not in (errno.ECONNRESET, errno.EPIPE)
595 and not first):
596 raise
597 reconnect('write')
598 amt = self.sock.send(out)
599 logger.debug('sent %d', amt)
600 first = False
601 # stash data we think we sent in case the socket breaks
602 # when we read from it
603 if was_first:
604 sent_data = out[:amt]
605 if out is body:
606 body = out[amt:]
607 else:
608 outgoing_headers = out[amt:]
609
610 # close if the server response said to or responded before eating
611 # the whole request
612 if response is None:
613 response = self.response_class(self.sock, self.timeout)
614 complete = response.complete()
615 data_left = bool(outgoing_headers or body)
616 if data_left:
617 logger.info('stopped sending request early, '
618 'will close the socket to be safe.')
619 response.will_close = True
620 if response.will_close:
621 # The socket will be closed by the response, so we disown
622 # the socket
623 self.sock = None
624 self._current_response = response
625
626 def getresponse(self):
627 if self._current_response is None:
628 raise httplib.ResponseNotReady()
629 r = self._current_response
630 while r.headers is None:
631 r._select()
632 if r.complete() or r.will_close:
633 self.sock = None
634 self._current_response = None
635 else:
636 self._current_response_taken = True
637 return r
638
639
640 class HTTPTimeoutException(httplib.HTTPException):
641 """A timeout occurred while waiting on the server."""
642
643
644 class BadRequestData(httplib.HTTPException):
645 """Request body object has neither __len__ nor read."""
646
647
648 class HTTPProxyConnectFailedException(httplib.HTTPException):
649 """Connecting to the HTTP proxy failed."""
650 # no-check-code
@@ -0,0 +1,134
1 # Copyright 2010, Google Inc.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 """Abstraction to simplify socket use for Python < 2.6
30
31 This will attempt to use the ssl module and the new
32 socket.create_connection method, but fall back to the old
33 methods if those are unavailable.
34 """
35 import logging
36 import socket
37
38 logger = logging.getLogger(__name__)
39
40 try:
41 import ssl
42 ssl.wrap_socket # make demandimporters load the module
43 have_ssl = True
44 except ImportError:
45 import httplib
46 import urllib2
47 have_ssl = getattr(urllib2, 'HTTPSHandler', False)
48 ssl = False
49
50
51 try:
52 create_connection = socket.create_connection
53 except AttributeError:
54 def create_connection(address):
55 host, port = address
56 msg = "getaddrinfo returns an empty list"
57 sock = None
58 for res in socket.getaddrinfo(host, port, 0,
59 socket.SOCK_STREAM):
60 af, socktype, proto, _canonname, sa = res
61 try:
62 sock = socket.socket(af, socktype, proto)
63 logger.info("connect: (%s, %s)", host, port)
64 sock.connect(sa)
65 except socket.error, msg:
66 logger.info('connect fail: %s %s', host, port)
67 if sock:
68 sock.close()
69 sock = None
70 continue
71 break
72 if not sock:
73 raise socket.error, msg
74 return sock
75
76 if ssl:
77 wrap_socket = ssl.wrap_socket
78 CERT_NONE = ssl.CERT_NONE
79 CERT_OPTIONAL = ssl.CERT_OPTIONAL
80 CERT_REQUIRED = ssl.CERT_REQUIRED
81 PROTOCOL_SSLv2 = ssl.PROTOCOL_SSLv2
82 PROTOCOL_SSLv3 = ssl.PROTOCOL_SSLv3
83 PROTOCOL_SSLv23 = ssl.PROTOCOL_SSLv23
84 PROTOCOL_TLSv1 = ssl.PROTOCOL_TLSv1
85 else:
86 class FakeSocket(httplib.FakeSocket):
87 """Socket wrapper that supports SSL.
88 """
89 # backport the behavior from Python 2.6, which is to busy wait
90 # on the socket instead of anything nice. Sigh.
91 # See http://bugs.python.org/issue3890 for more info.
92 def recv(self, buflen=1024, flags=0):
93 """ssl-aware wrapper around socket.recv
94 """
95 if flags != 0:
96 raise ValueError(
97 "non-zero flags not allowed in calls to recv() on %s" %
98 self.__class__)
99 while True:
100 try:
101 return self._ssl.read(buflen)
102 except socket.sslerror, x:
103 if x.args[0] == socket.SSL_ERROR_WANT_READ:
104 continue
105 else:
106 raise x
107
108 PROTOCOL_SSLv2 = 0
109 PROTOCOL_SSLv3 = 1
110 PROTOCOL_SSLv23 = 2
111 PROTOCOL_TLSv1 = 3
112
113 CERT_NONE = 0
114 CERT_OPTIONAL = 1
115 CERT_REQUIRED = 2
116
117 def wrap_socket(sock, keyfile=None, certfile=None,
118 server_side=False, cert_reqs=CERT_NONE,
119 ssl_version=PROTOCOL_SSLv23, ca_certs=None,
120 do_handshake_on_connect=True,
121 suppress_ragged_eofs=True):
122 if cert_reqs != CERT_NONE and ca_certs:
123 raise CertificateValidationUnsupported(
124 'SSL certificate validation requires the ssl module'
125 '(included in Python 2.6 and later.)')
126 sslob = socket.ssl(sock)
127 # borrow httplib's workaround for no ssl.wrap_socket
128 sock = FakeSocket(sock, sslob)
129 return sock
130
131
132 class CertificateValidationUnsupported(Exception):
133 """Exception raised when cert validation is requested but unavailable."""
134 # no-check-code
@@ -0,0 +1,1
1 # no-check-code
@@ -0,0 +1,366
1 # Copyright 2010, Google Inc.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 import unittest
30
31 import http
32
33 # relative import to ease embedding the library
34 import util
35
36
37 class SimpleHttpTest(util.HttpTestBase, unittest.TestCase):
38
39 def _run_simple_test(self, host, server_data, expected_req, expected_data):
40 con = http.HTTPConnection(host)
41 con._connect()
42 con.sock.data = server_data
43 con.request('GET', '/')
44
45 self.assertStringEqual(expected_req, con.sock.sent)
46 self.assertEqual(expected_data, con.getresponse().read())
47
48 def test_broken_data_obj(self):
49 con = http.HTTPConnection('1.2.3.4:80')
50 con._connect()
51 self.assertRaises(http.BadRequestData,
52 con.request, 'POST', '/', body=1)
53
54 def test_no_keepalive_http_1_0(self):
55 expected_request_one = """GET /remote/.hg/requires HTTP/1.1
56 Host: localhost:9999
57 range: bytes=0-
58 accept-encoding: identity
59 accept: application/mercurial-0.1
60 user-agent: mercurial/proto-1.0
61
62 """.replace('\n', '\r\n')
63 expected_response_headers = """HTTP/1.0 200 OK
64 Server: SimpleHTTP/0.6 Python/2.6.1
65 Date: Sun, 01 May 2011 13:56:57 GMT
66 Content-type: application/octet-stream
67 Content-Length: 33
68 Last-Modified: Sun, 01 May 2011 13:56:56 GMT
69
70 """.replace('\n', '\r\n')
71 expected_response_body = """revlogv1
72 store
73 fncache
74 dotencode
75 """
76 con = http.HTTPConnection('localhost:9999')
77 con._connect()
78 con.sock.data = [expected_response_headers, expected_response_body]
79 con.request('GET', '/remote/.hg/requires',
80 headers={'accept-encoding': 'identity',
81 'range': 'bytes=0-',
82 'accept': 'application/mercurial-0.1',
83 'user-agent': 'mercurial/proto-1.0',
84 })
85 self.assertStringEqual(expected_request_one, con.sock.sent)
86 self.assertEqual(con.sock.closed, False)
87 self.assertNotEqual(con.sock.data, [])
88 self.assert_(con.busy())
89 resp = con.getresponse()
90 self.assertStringEqual(resp.read(), expected_response_body)
91 self.failIf(con.busy())
92 self.assertEqual(con.sock, None)
93 self.assertEqual(resp.sock.data, [])
94 self.assert_(resp.sock.closed)
95
96 def test_multiline_header(self):
97 con = http.HTTPConnection('1.2.3.4:80')
98 con._connect()
99 con.sock.data = ['HTTP/1.1 200 OK\r\n',
100 'Server: BogusServer 1.0\r\n',
101 'Multiline: Value\r\n',
102 ' Rest of value\r\n',
103 'Content-Length: 10\r\n',
104 '\r\n'
105 '1234567890'
106 ]
107 con.request('GET', '/')
108
109 expected_req = ('GET / HTTP/1.1\r\n'
110 'Host: 1.2.3.4\r\n'
111 'accept-encoding: identity\r\n\r\n')
112
113 self.assertEqual(('1.2.3.4', 80), con.sock.sa)
114 self.assertEqual(expected_req, con.sock.sent)
115 resp = con.getresponse()
116 self.assertEqual('1234567890', resp.read())
117 self.assertEqual(['Value\n Rest of value'],
118 resp.headers.getheaders('multiline'))
119
120 def testSimpleRequest(self):
121 con = http.HTTPConnection('1.2.3.4:80')
122 con._connect()
123 con.sock.data = ['HTTP/1.1 200 OK\r\n',
124 'Server: BogusServer 1.0\r\n',
125 'MultiHeader: Value\r\n'
126 'MultiHeader: Other Value\r\n'
127 'MultiHeader: One More!\r\n'
128 'Content-Length: 10\r\n',
129 '\r\n'
130 '1234567890'
131 ]
132 con.request('GET', '/')
133
134 expected_req = ('GET / HTTP/1.1\r\n'
135 'Host: 1.2.3.4\r\n'
136 'accept-encoding: identity\r\n\r\n')
137
138 self.assertEqual(('1.2.3.4', 80), con.sock.sa)
139 self.assertEqual(expected_req, con.sock.sent)
140 resp = con.getresponse()
141 self.assertEqual('1234567890', resp.read())
142 self.assertEqual(['Value', 'Other Value', 'One More!'],
143 resp.headers.getheaders('multiheader'))
144 self.assertEqual(['BogusServer 1.0'],
145 resp.headers.getheaders('server'))
146
147 def testHeaderlessResponse(self):
148 con = http.HTTPConnection('1.2.3.4', use_ssl=False)
149 con._connect()
150 con.sock.data = ['HTTP/1.1 200 OK\r\n',
151 '\r\n'
152 '1234567890'
153 ]
154 con.request('GET', '/')
155
156 expected_req = ('GET / HTTP/1.1\r\n'
157 'Host: 1.2.3.4\r\n'
158 'accept-encoding: identity\r\n\r\n')
159
160 self.assertEqual(('1.2.3.4', 80), con.sock.sa)
161 self.assertEqual(expected_req, con.sock.sent)
162 resp = con.getresponse()
163 self.assertEqual('1234567890', resp.read())
164 self.assertEqual({}, dict(resp.headers))
165 self.assertEqual(resp.status, 200)
166
167 def testReadline(self):
168 con = http.HTTPConnection('1.2.3.4')
169 con._connect()
170 # make sure it trickles in one byte at a time
171 # so that we touch all the cases in readline
172 con.sock.data = list(''.join(
173 ['HTTP/1.1 200 OK\r\n',
174 'Server: BogusServer 1.0\r\n',
175 'Connection: Close\r\n',
176 '\r\n'
177 '1\n2\nabcdefg\n4\n5']))
178
179 expected_req = ('GET / HTTP/1.1\r\n'
180 'Host: 1.2.3.4\r\n'
181 'accept-encoding: identity\r\n\r\n')
182
183 con.request('GET', '/')
184 self.assertEqual(('1.2.3.4', 80), con.sock.sa)
185 self.assertEqual(expected_req, con.sock.sent)
186 r = con.getresponse()
187 for expected in ['1\n', '2\n', 'abcdefg\n', '4\n', '5']:
188 actual = r.readline()
189 self.assertEqual(expected, actual,
190 'Expected %r, got %r' % (expected, actual))
191
192 def testIPv6(self):
193 self._run_simple_test('[::1]:8221',
194 ['HTTP/1.1 200 OK\r\n',
195 'Server: BogusServer 1.0\r\n',
196 'Content-Length: 10',
197 '\r\n\r\n'
198 '1234567890'],
199 ('GET / HTTP/1.1\r\n'
200 'Host: [::1]:8221\r\n'
201 'accept-encoding: identity\r\n\r\n'),
202 '1234567890')
203 self._run_simple_test('::2',
204 ['HTTP/1.1 200 OK\r\n',
205 'Server: BogusServer 1.0\r\n',
206 'Content-Length: 10',
207 '\r\n\r\n'
208 '1234567890'],
209 ('GET / HTTP/1.1\r\n'
210 'Host: ::2\r\n'
211 'accept-encoding: identity\r\n\r\n'),
212 '1234567890')
213 self._run_simple_test('[::3]:443',
214 ['HTTP/1.1 200 OK\r\n',
215 'Server: BogusServer 1.0\r\n',
216 'Content-Length: 10',
217 '\r\n\r\n'
218 '1234567890'],
219 ('GET / HTTP/1.1\r\n'
220 'Host: ::3\r\n'
221 'accept-encoding: identity\r\n\r\n'),
222 '1234567890')
223
224 def doPost(self, con, expect_body, body_to_send='This is some POST data'):
225 con.request('POST', '/', body=body_to_send,
226 expect_continue=True)
227 expected_req = ('POST / HTTP/1.1\r\n'
228 'Host: 1.2.3.4\r\n'
229 'content-length: %d\r\n'
230 'Expect: 100-Continue\r\n'
231 'accept-encoding: identity\r\n\r\n' %
232 len(body_to_send))
233 if expect_body:
234 expected_req += body_to_send
235 return expected_req
236
237 def testEarlyContinueResponse(self):
238 con = http.HTTPConnection('1.2.3.4:80')
239 con._connect()
240 sock = con.sock
241 sock.data = ['HTTP/1.1 403 Forbidden\r\n',
242 'Server: BogusServer 1.0\r\n',
243 'Content-Length: 18',
244 '\r\n\r\n'
245 "You can't do that."]
246 expected_req = self.doPost(con, expect_body=False)
247 self.assertEqual(('1.2.3.4', 80), sock.sa)
248 self.assertStringEqual(expected_req, sock.sent)
249 self.assertEqual("You can't do that.", con.getresponse().read())
250 self.assertEqual(sock.closed, True)
251
252 def testDeniedAfterContinueTimeoutExpires(self):
253 con = http.HTTPConnection('1.2.3.4:80')
254 con._connect()
255 sock = con.sock
256 sock.data = ['HTTP/1.1 403 Forbidden\r\n',
257 'Server: BogusServer 1.0\r\n',
258 'Content-Length: 18\r\n',
259 'Connection: close',
260 '\r\n\r\n'
261 "You can't do that."]
262 sock.read_wait_sentinel = 'Dear server, send response!'
263 sock.close_on_empty = True
264 # send enough data out that we'll chunk it into multiple
265 # blocks and the socket will close before we can send the
266 # whole request.
267 post_body = ('This is some POST data\n' * 1024 * 32 +
268 'Dear server, send response!\n' +
269 'This is some POST data\n' * 1024 * 32)
270 expected_req = self.doPost(con, expect_body=False,
271 body_to_send=post_body)
272 self.assertEqual(('1.2.3.4', 80), sock.sa)
273 self.assert_('POST data\n' in sock.sent)
274 self.assert_('Dear server, send response!\n' in sock.sent)
275 # We expect not all of our data was sent.
276 self.assertNotEqual(sock.sent, expected_req)
277 self.assertEqual("You can't do that.", con.getresponse().read())
278 self.assertEqual(sock.closed, True)
279
280 def testPostData(self):
281 con = http.HTTPConnection('1.2.3.4:80')
282 con._connect()
283 sock = con.sock
284 sock.read_wait_sentinel = 'POST data'
285 sock.early_data = ['HTTP/1.1 100 Co', 'ntinue\r\n\r\n']
286 sock.data = ['HTTP/1.1 200 OK\r\n',
287 'Server: BogusServer 1.0\r\n',
288 'Content-Length: 16',
289 '\r\n\r\n',
290 "You can do that."]
291 expected_req = self.doPost(con, expect_body=True)
292 self.assertEqual(('1.2.3.4', 80), sock.sa)
293 self.assertEqual(expected_req, sock.sent)
294 self.assertEqual("You can do that.", con.getresponse().read())
295 self.assertEqual(sock.closed, False)
296
297 def testServerWithoutContinue(self):
298 con = http.HTTPConnection('1.2.3.4:80')
299 con._connect()
300 sock = con.sock
301 sock.read_wait_sentinel = 'POST data'
302 sock.data = ['HTTP/1.1 200 OK\r\n',
303 'Server: BogusServer 1.0\r\n',
304 'Content-Length: 16',
305 '\r\n\r\n',
306 "You can do that."]
307 expected_req = self.doPost(con, expect_body=True)
308 self.assertEqual(('1.2.3.4', 80), sock.sa)
309 self.assertEqual(expected_req, sock.sent)
310 self.assertEqual("You can do that.", con.getresponse().read())
311 self.assertEqual(sock.closed, False)
312
313 def testServerWithSlowContinue(self):
314 con = http.HTTPConnection('1.2.3.4:80')
315 con._connect()
316 sock = con.sock
317 sock.read_wait_sentinel = 'POST data'
318 sock.data = ['HTTP/1.1 100 ', 'Continue\r\n\r\n',
319 'HTTP/1.1 200 OK\r\n',
320 'Server: BogusServer 1.0\r\n',
321 'Content-Length: 16',
322 '\r\n\r\n',
323 "You can do that."]
324 expected_req = self.doPost(con, expect_body=True)
325 self.assertEqual(('1.2.3.4', 80), sock.sa)
326 self.assertEqual(expected_req, sock.sent)
327 resp = con.getresponse()
328 self.assertEqual("You can do that.", resp.read())
329 self.assertEqual(200, resp.status)
330 self.assertEqual(sock.closed, False)
331
332 def testSlowConnection(self):
333 con = http.HTTPConnection('1.2.3.4:80')
334 con._connect()
335 # simulate one byte arriving at a time, to check for various
336 # corner cases
337 con.sock.data = list('HTTP/1.1 200 OK\r\n'
338 'Server: BogusServer 1.0\r\n'
339 'Content-Length: 10'
340 '\r\n\r\n'
341 '1234567890')
342 con.request('GET', '/')
343
344 expected_req = ('GET / HTTP/1.1\r\n'
345 'Host: 1.2.3.4\r\n'
346 'accept-encoding: identity\r\n\r\n')
347
348 self.assertEqual(('1.2.3.4', 80), con.sock.sa)
349 self.assertEqual(expected_req, con.sock.sent)
350 self.assertEqual('1234567890', con.getresponse().read())
351
352 def testTimeout(self):
353 con = http.HTTPConnection('1.2.3.4:80')
354 con._connect()
355 con.sock.data = []
356 con.request('GET', '/')
357 self.assertRaises(http.HTTPTimeoutException,
358 con.getresponse)
359
360 expected_req = ('GET / HTTP/1.1\r\n'
361 'Host: 1.2.3.4\r\n'
362 'accept-encoding: identity\r\n\r\n')
363
364 self.assertEqual(('1.2.3.4', 80), con.sock.sa)
365 self.assertEqual(expected_req, con.sock.sent)
366 # no-check-code
@@ -0,0 +1,68
1 # Copyright 2010, Google Inc.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 """Tests against malformed responses.
30
31 Server implementations that respond with only LF instead of CRLF have
32 been observed. Checking against ones that use only CR is a hedge
33 against that potential insanit.y
34 """
35 import unittest
36
37 import http
38
39 # relative import to ease embedding the library
40 import util
41
42
43 class SimpleHttpTest(util.HttpTestBase, unittest.TestCase):
44
45 def bogusEOL(self, eol):
46 con = http.HTTPConnection('1.2.3.4:80')
47 con._connect()
48 con.sock.data = ['HTTP/1.1 200 OK%s' % eol,
49 'Server: BogusServer 1.0%s' % eol,
50 'Content-Length: 10',
51 eol * 2,
52 '1234567890']
53 con.request('GET', '/')
54
55 expected_req = ('GET / HTTP/1.1\r\n'
56 'Host: 1.2.3.4\r\n'
57 'accept-encoding: identity\r\n\r\n')
58
59 self.assertEqual(('1.2.3.4', 80), con.sock.sa)
60 self.assertEqual(expected_req, con.sock.sent)
61 self.assertEqual('1234567890', con.getresponse().read())
62
63 def testOnlyLinefeed(self):
64 self.bogusEOL('\n')
65
66 def testOnlyCarriageReturn(self):
67 self.bogusEOL('\r')
68 # no-check-code
@@ -0,0 +1,137
1 # Copyright 2010, Google Inc.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 import cStringIO
30 import unittest
31
32 import http
33
34 # relative import to ease embedding the library
35 import util
36
37
38 def chunkedblock(x, eol='\r\n'):
39 r"""Make a chunked transfer-encoding block.
40
41 >>> chunkedblock('hi')
42 '2\r\nhi\r\n'
43 >>> chunkedblock('hi' * 10)
44 '14\r\nhihihihihihihihihihi\r\n'
45 >>> chunkedblock('hi', eol='\n')
46 '2\nhi\n'
47 """
48 return ''.join((hex(len(x))[2:], eol, x, eol))
49
50
51 class ChunkedTransferTest(util.HttpTestBase, unittest.TestCase):
52 def testChunkedUpload(self):
53 con = http.HTTPConnection('1.2.3.4:80')
54 con._connect()
55 sock = con.sock
56 sock.read_wait_sentinel = 'end-of-body'
57 sock.data = ['HTTP/1.1 200 OK\r\n',
58 'Server: BogusServer 1.0\r\n',
59 'Content-Length: 6',
60 '\r\n\r\n',
61 "Thanks"]
62
63 zz = 'zz\n'
64 con.request('POST', '/', body=cStringIO.StringIO(
65 (zz * (0x8010 / 3)) + 'end-of-body'))
66 expected_req = ('POST / HTTP/1.1\r\n'
67 'transfer-encoding: chunked\r\n'
68 'Host: 1.2.3.4\r\n'
69 'accept-encoding: identity\r\n\r\n')
70 expected_req += chunkedblock('zz\n' * (0x8000 / 3) + 'zz')
71 expected_req += chunkedblock(
72 '\n' + 'zz\n' * ((0x1b - len('end-of-body')) / 3) + 'end-of-body')
73 expected_req += '0\r\n\r\n'
74 self.assertEqual(('1.2.3.4', 80), sock.sa)
75 self.assertStringEqual(expected_req, sock.sent)
76 self.assertEqual("Thanks", con.getresponse().read())
77 self.assertEqual(sock.closed, False)
78
79 def testChunkedDownload(self):
80 con = http.HTTPConnection('1.2.3.4:80')
81 con._connect()
82 sock = con.sock
83 sock.data = ['HTTP/1.1 200 OK\r\n',
84 'Server: BogusServer 1.0\r\n',
85 'transfer-encoding: chunked',
86 '\r\n\r\n',
87 chunkedblock('hi '),
88 chunkedblock('there'),
89 chunkedblock(''),
90 ]
91 con.request('GET', '/')
92 self.assertStringEqual('hi there', con.getresponse().read())
93
94 def testChunkedDownloadBadEOL(self):
95 con = http.HTTPConnection('1.2.3.4:80')
96 con._connect()
97 sock = con.sock
98 sock.data = ['HTTP/1.1 200 OK\n',
99 'Server: BogusServer 1.0\n',
100 'transfer-encoding: chunked',
101 '\n\n',
102 chunkedblock('hi ', eol='\n'),
103 chunkedblock('there', eol='\n'),
104 chunkedblock('', eol='\n'),
105 ]
106 con.request('GET', '/')
107 self.assertStringEqual('hi there', con.getresponse().read())
108
109 def testChunkedDownloadPartialChunkBadEOL(self):
110 con = http.HTTPConnection('1.2.3.4:80')
111 con._connect()
112 sock = con.sock
113 sock.data = ['HTTP/1.1 200 OK\n',
114 'Server: BogusServer 1.0\n',
115 'transfer-encoding: chunked',
116 '\n\n',
117 chunkedblock('hi ', eol='\n'),
118 ] + list(chunkedblock('there\n' * 5, eol='\n')) + [
119 chunkedblock('', eol='\n')]
120 con.request('GET', '/')
121 self.assertStringEqual('hi there\nthere\nthere\nthere\nthere\n',
122 con.getresponse().read())
123
124 def testChunkedDownloadPartialChunk(self):
125 con = http.HTTPConnection('1.2.3.4:80')
126 con._connect()
127 sock = con.sock
128 sock.data = ['HTTP/1.1 200 OK\r\n',
129 'Server: BogusServer 1.0\r\n',
130 'transfer-encoding: chunked',
131 '\r\n\r\n',
132 chunkedblock('hi '),
133 ] + list(chunkedblock('there\n' * 5)) + [chunkedblock('')]
134 con.request('GET', '/')
135 self.assertStringEqual('hi there\nthere\nthere\nthere\nthere\n',
136 con.getresponse().read())
137 # no-check-code
@@ -0,0 +1,132
1 # Copyright 2010, Google Inc.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 import unittest
30 import socket
31
32 import http
33
34 # relative import to ease embedding the library
35 import util
36
37
38 def make_preloaded_socket(data):
39 """Make a socket pre-loaded with data so it can be read during connect.
40
41 Useful for https proxy tests because we have to read from the
42 socket during _connect rather than later on.
43 """
44 def s(*args, **kwargs):
45 sock = util.MockSocket(*args, **kwargs)
46 sock.data = data[:]
47 return sock
48 return s
49
50
51 class ProxyHttpTest(util.HttpTestBase, unittest.TestCase):
52
53 def _run_simple_test(self, host, server_data, expected_req, expected_data):
54 con = http.HTTPConnection(host)
55 con._connect()
56 con.sock.data = server_data
57 con.request('GET', '/')
58
59 self.assertEqual(expected_req, con.sock.sent)
60 self.assertEqual(expected_data, con.getresponse().read())
61
62 def testSimpleRequest(self):
63 con = http.HTTPConnection('1.2.3.4:80',
64 proxy_hostport=('magicproxy', 4242))
65 con._connect()
66 con.sock.data = ['HTTP/1.1 200 OK\r\n',
67 'Server: BogusServer 1.0\r\n',
68 'MultiHeader: Value\r\n'
69 'MultiHeader: Other Value\r\n'
70 'MultiHeader: One More!\r\n'
71 'Content-Length: 10\r\n',
72 '\r\n'
73 '1234567890'
74 ]
75 con.request('GET', '/')
76
77 expected_req = ('GET http://1.2.3.4/ HTTP/1.1\r\n'
78 'Host: 1.2.3.4\r\n'
79 'accept-encoding: identity\r\n\r\n')
80
81 self.assertEqual(('127.0.0.42', 4242), con.sock.sa)
82 self.assertStringEqual(expected_req, con.sock.sent)
83 resp = con.getresponse()
84 self.assertEqual('1234567890', resp.read())
85 self.assertEqual(['Value', 'Other Value', 'One More!'],
86 resp.headers.getheaders('multiheader'))
87 self.assertEqual(['BogusServer 1.0'],
88 resp.headers.getheaders('server'))
89
90 def testSSLRequest(self):
91 con = http.HTTPConnection('1.2.3.4:443',
92 proxy_hostport=('magicproxy', 4242))
93 socket.socket = make_preloaded_socket(
94 ['HTTP/1.1 200 OK\r\n',
95 'Server: BogusServer 1.0\r\n',
96 'Content-Length: 10\r\n',
97 '\r\n'
98 '1234567890'])
99 con._connect()
100 con.sock.data = ['HTTP/1.1 200 OK\r\n',
101 'Server: BogusServer 1.0\r\n',
102 'Content-Length: 10\r\n',
103 '\r\n'
104 '1234567890'
105 ]
106 con.request('GET', '/')
107
108 expected_req = ('CONNECT 1.2.3.4:443 HTTP/1.0\r\n'
109 'Host: 1.2.3.4\r\n'
110 'accept-encoding: identity\r\n'
111 '\r\n'
112 'GET / HTTP/1.1\r\n'
113 'Host: 1.2.3.4\r\n'
114 'accept-encoding: identity\r\n\r\n')
115
116 self.assertEqual(('127.0.0.42', 4242), con.sock.sa)
117 self.assertStringEqual(expected_req, con.sock.sent)
118 resp = con.getresponse()
119 self.assertEqual(resp.status, 200)
120 self.assertEqual('1234567890', resp.read())
121 self.assertEqual(['BogusServer 1.0'],
122 resp.headers.getheaders('server'))
123
124 def testSSLProxyFailure(self):
125 con = http.HTTPConnection('1.2.3.4:443',
126 proxy_hostport=('magicproxy', 4242))
127 socket.socket = make_preloaded_socket(
128 ['HTTP/1.1 407 Proxy Authentication Required\r\n\r\n'])
129 self.assertRaises(http.HTTPProxyConnectFailedException, con._connect)
130 self.assertRaises(http.HTTPProxyConnectFailedException,
131 con.request, 'GET', '/')
132 # no-check-code
@@ -0,0 +1,160
1 # Copyright 2010, Google Inc.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 import difflib
30 import socket
31
32 import http
33
34
35 class MockSocket(object):
36 """Mock non-blocking socket object.
37
38 This is ONLY capable of mocking a nonblocking socket.
39
40 Attributes:
41 early_data: data to always send as soon as end of headers is seen
42 data: a list of strings to return on recv(), with the
43 assumption that the socket would block between each
44 string in the list.
45 read_wait_sentinel: data that must be written to the socket before
46 beginning the response.
47 close_on_empty: If true, close the socket when it runs out of data
48 for the client.
49 """
50 def __init__(self, af, socktype, proto):
51 self.af = af
52 self.socktype = socktype
53 self.proto = proto
54
55 self.early_data = []
56 self.data = []
57 self.remote_closed = self.closed = False
58 self.close_on_empty = False
59 self.sent = ''
60 self.read_wait_sentinel = http._END_HEADERS
61
62 def close(self):
63 self.closed = True
64
65 def connect(self, sa):
66 self.sa = sa
67
68 def setblocking(self, timeout):
69 assert timeout == 0
70
71 def recv(self, amt=-1):
72 if self.early_data:
73 datalist = self.early_data
74 elif not self.data:
75 return ''
76 else:
77 datalist = self.data
78 if amt == -1:
79 return datalist.pop(0)
80 data = datalist.pop(0)
81 if len(data) > amt:
82 datalist.insert(0, data[amt:])
83 if not self.data and not self.early_data and self.close_on_empty:
84 self.remote_closed = True
85 return data[:amt]
86
87 @property
88 def ready_for_read(self):
89 return ((self.early_data and http._END_HEADERS in self.sent)
90 or (self.read_wait_sentinel in self.sent and self.data)
91 or self.closed)
92
93 def send(self, data):
94 # this is a horrible mock, but nothing needs us to raise the
95 # correct exception yet
96 assert not self.closed, 'attempted to write to a closed socket'
97 assert not self.remote_closed, ('attempted to write to a'
98 ' socket closed by the server')
99 if len(data) > 8192:
100 data = data[:8192]
101 self.sent += data
102 return len(data)
103
104
105 def mockselect(r, w, x, timeout=0):
106 """Simple mock for select()
107 """
108 readable = filter(lambda s: s.ready_for_read, r)
109 return readable, w[:], []
110
111
112 def mocksslwrap(sock, keyfile=None, certfile=None,
113 server_side=False, cert_reqs=http.socketutil.CERT_NONE,
114 ssl_version=http.socketutil.PROTOCOL_SSLv23, ca_certs=None,
115 do_handshake_on_connect=True,
116 suppress_ragged_eofs=True):
117 return sock
118
119
120 def mockgetaddrinfo(host, port, unused, streamtype):
121 assert unused == 0
122 assert streamtype == socket.SOCK_STREAM
123 if host.count('.') != 3:
124 host = '127.0.0.42'
125 return [(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, '',
126 (host, port))]
127
128
129 class HttpTestBase(object):
130 def setUp(self):
131 self.orig_socket = socket.socket
132 socket.socket = MockSocket
133
134 self.orig_getaddrinfo = socket.getaddrinfo
135 socket.getaddrinfo = mockgetaddrinfo
136
137 self.orig_select = http.select.select
138 http.select.select = mockselect
139
140 self.orig_sslwrap = http.socketutil.wrap_socket
141 http.socketutil.wrap_socket = mocksslwrap
142
143 def tearDown(self):
144 socket.socket = self.orig_socket
145 http.select.select = self.orig_select
146 http.socketutil.wrap_socket = self.orig_sslwrap
147 socket.getaddrinfo = self.orig_getaddrinfo
148
149 def assertStringEqual(self, l, r):
150 try:
151 self.assertEqual(l, r, ('failed string equality check, '
152 'see stdout for details'))
153 except:
154 add_nl = lambda li: map(lambda x: x + '\n', li)
155 print 'failed expectation:'
156 print ''.join(difflib.unified_diff(
157 add_nl(l.splitlines()), add_nl(r.splitlines()),
158 fromfile='expected', tofile='got'))
159 raise
160 # no-check-code
@@ -1,399 +1,400
1 #
1 #
2 # This is the mercurial setup script.
2 # This is the mercurial setup script.
3 #
3 #
4 # 'python setup.py install', or
4 # 'python setup.py install', or
5 # 'python setup.py --help' for more options
5 # 'python setup.py --help' for more options
6
6
7 import sys
7 import sys
8 if not hasattr(sys, 'version_info') or sys.version_info < (2, 4, 0, 'final'):
8 if not hasattr(sys, 'version_info') or sys.version_info < (2, 4, 0, 'final'):
9 raise SystemExit("Mercurial requires Python 2.4 or later.")
9 raise SystemExit("Mercurial requires Python 2.4 or later.")
10
10
11 if sys.version_info[0] >= 3:
11 if sys.version_info[0] >= 3:
12 def b(s):
12 def b(s):
13 '''A helper function to emulate 2.6+ bytes literals using string
13 '''A helper function to emulate 2.6+ bytes literals using string
14 literals.'''
14 literals.'''
15 return s.encode('latin1')
15 return s.encode('latin1')
16 else:
16 else:
17 def b(s):
17 def b(s):
18 '''A helper function to emulate 2.6+ bytes literals using string
18 '''A helper function to emulate 2.6+ bytes literals using string
19 literals.'''
19 literals.'''
20 return s
20 return s
21
21
22 # Solaris Python packaging brain damage
22 # Solaris Python packaging brain damage
23 try:
23 try:
24 import hashlib
24 import hashlib
25 sha = hashlib.sha1()
25 sha = hashlib.sha1()
26 except:
26 except:
27 try:
27 try:
28 import sha
28 import sha
29 except:
29 except:
30 raise SystemExit(
30 raise SystemExit(
31 "Couldn't import standard hashlib (incomplete Python install).")
31 "Couldn't import standard hashlib (incomplete Python install).")
32
32
33 try:
33 try:
34 import zlib
34 import zlib
35 except:
35 except:
36 raise SystemExit(
36 raise SystemExit(
37 "Couldn't import standard zlib (incomplete Python install).")
37 "Couldn't import standard zlib (incomplete Python install).")
38
38
39 try:
39 try:
40 import bz2
40 import bz2
41 except:
41 except:
42 raise SystemExit(
42 raise SystemExit(
43 "Couldn't import standard bz2 (incomplete Python install).")
43 "Couldn't import standard bz2 (incomplete Python install).")
44
44
45 import os, subprocess, time
45 import os, subprocess, time
46 import shutil
46 import shutil
47 import tempfile
47 import tempfile
48 from distutils import log
48 from distutils import log
49 from distutils.core import setup, Extension
49 from distutils.core import setup, Extension
50 from distutils.dist import Distribution
50 from distutils.dist import Distribution
51 from distutils.command.build import build
51 from distutils.command.build import build
52 from distutils.command.build_ext import build_ext
52 from distutils.command.build_ext import build_ext
53 from distutils.command.build_py import build_py
53 from distutils.command.build_py import build_py
54 from distutils.command.install_scripts import install_scripts
54 from distutils.command.install_scripts import install_scripts
55 from distutils.spawn import spawn, find_executable
55 from distutils.spawn import spawn, find_executable
56 from distutils.ccompiler import new_compiler
56 from distutils.ccompiler import new_compiler
57 from distutils.errors import CCompilerError
57 from distutils.errors import CCompilerError
58 from distutils.sysconfig import get_python_inc
58 from distutils.sysconfig import get_python_inc
59 from distutils.version import StrictVersion
59 from distutils.version import StrictVersion
60
60
61 scripts = ['hg']
61 scripts = ['hg']
62 if os.name == 'nt':
62 if os.name == 'nt':
63 scripts.append('contrib/win32/hg.bat')
63 scripts.append('contrib/win32/hg.bat')
64
64
65 # simplified version of distutils.ccompiler.CCompiler.has_function
65 # simplified version of distutils.ccompiler.CCompiler.has_function
66 # that actually removes its temporary files.
66 # that actually removes its temporary files.
67 def hasfunction(cc, funcname):
67 def hasfunction(cc, funcname):
68 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
68 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
69 devnull = oldstderr = None
69 devnull = oldstderr = None
70 try:
70 try:
71 try:
71 try:
72 fname = os.path.join(tmpdir, 'funcname.c')
72 fname = os.path.join(tmpdir, 'funcname.c')
73 f = open(fname, 'w')
73 f = open(fname, 'w')
74 f.write('int main(void) {\n')
74 f.write('int main(void) {\n')
75 f.write(' %s();\n' % funcname)
75 f.write(' %s();\n' % funcname)
76 f.write('}\n')
76 f.write('}\n')
77 f.close()
77 f.close()
78 # Redirect stderr to /dev/null to hide any error messages
78 # Redirect stderr to /dev/null to hide any error messages
79 # from the compiler.
79 # from the compiler.
80 # This will have to be changed if we ever have to check
80 # This will have to be changed if we ever have to check
81 # for a function on Windows.
81 # for a function on Windows.
82 devnull = open('/dev/null', 'w')
82 devnull = open('/dev/null', 'w')
83 oldstderr = os.dup(sys.stderr.fileno())
83 oldstderr = os.dup(sys.stderr.fileno())
84 os.dup2(devnull.fileno(), sys.stderr.fileno())
84 os.dup2(devnull.fileno(), sys.stderr.fileno())
85 objects = cc.compile([fname], output_dir=tmpdir)
85 objects = cc.compile([fname], output_dir=tmpdir)
86 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
86 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
87 except:
87 except:
88 return False
88 return False
89 return True
89 return True
90 finally:
90 finally:
91 if oldstderr is not None:
91 if oldstderr is not None:
92 os.dup2(oldstderr, sys.stderr.fileno())
92 os.dup2(oldstderr, sys.stderr.fileno())
93 if devnull is not None:
93 if devnull is not None:
94 devnull.close()
94 devnull.close()
95 shutil.rmtree(tmpdir)
95 shutil.rmtree(tmpdir)
96
96
97 # py2exe needs to be installed to work
97 # py2exe needs to be installed to work
98 try:
98 try:
99 import py2exe
99 import py2exe
100 py2exeloaded = True
100 py2exeloaded = True
101 except ImportError:
101 except ImportError:
102 py2exeloaded = False
102 py2exeloaded = False
103
103
104 def runcmd(cmd, env):
104 def runcmd(cmd, env):
105 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
105 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
106 stderr=subprocess.PIPE, env=env)
106 stderr=subprocess.PIPE, env=env)
107 out, err = p.communicate()
107 out, err = p.communicate()
108 return out, err
108 return out, err
109
109
110 def runhg(cmd, env):
110 def runhg(cmd, env):
111 out, err = runcmd(cmd, env)
111 out, err = runcmd(cmd, env)
112 # If root is executing setup.py, but the repository is owned by
112 # If root is executing setup.py, but the repository is owned by
113 # another user (as in "sudo python setup.py install") we will get
113 # another user (as in "sudo python setup.py install") we will get
114 # trust warnings since the .hg/hgrc file is untrusted. That is
114 # trust warnings since the .hg/hgrc file is untrusted. That is
115 # fine, we don't want to load it anyway. Python may warn about
115 # fine, we don't want to load it anyway. Python may warn about
116 # a missing __init__.py in mercurial/locale, we also ignore that.
116 # a missing __init__.py in mercurial/locale, we also ignore that.
117 err = [e for e in err.splitlines()
117 err = [e for e in err.splitlines()
118 if not e.startswith(b('Not trusting file')) \
118 if not e.startswith(b('Not trusting file')) \
119 and not e.startswith(b('warning: Not importing'))]
119 and not e.startswith(b('warning: Not importing'))]
120 if err:
120 if err:
121 return ''
121 return ''
122 return out
122 return out
123
123
124 version = ''
124 version = ''
125
125
126 if os.path.isdir('.hg'):
126 if os.path.isdir('.hg'):
127 # Execute hg out of this directory with a custom environment which
127 # Execute hg out of this directory with a custom environment which
128 # includes the pure Python modules in mercurial/pure. We also take
128 # includes the pure Python modules in mercurial/pure. We also take
129 # care to not use any hgrc files and do no localization.
129 # care to not use any hgrc files and do no localization.
130 pypath = ['mercurial', os.path.join('mercurial', 'pure')]
130 pypath = ['mercurial', os.path.join('mercurial', 'pure')]
131 env = {'PYTHONPATH': os.pathsep.join(pypath),
131 env = {'PYTHONPATH': os.pathsep.join(pypath),
132 'HGRCPATH': '',
132 'HGRCPATH': '',
133 'LANGUAGE': 'C'}
133 'LANGUAGE': 'C'}
134 if 'LD_LIBRARY_PATH' in os.environ:
134 if 'LD_LIBRARY_PATH' in os.environ:
135 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
135 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
136 if 'SystemRoot' in os.environ:
136 if 'SystemRoot' in os.environ:
137 # Copy SystemRoot into the custom environment for Python 2.6
137 # Copy SystemRoot into the custom environment for Python 2.6
138 # under Windows. Otherwise, the subprocess will fail with
138 # under Windows. Otherwise, the subprocess will fail with
139 # error 0xc0150004. See: http://bugs.python.org/issue3440
139 # error 0xc0150004. See: http://bugs.python.org/issue3440
140 env['SystemRoot'] = os.environ['SystemRoot']
140 env['SystemRoot'] = os.environ['SystemRoot']
141 cmd = [sys.executable, 'hg', 'id', '-i', '-t']
141 cmd = [sys.executable, 'hg', 'id', '-i', '-t']
142 l = runhg(cmd, env).split()
142 l = runhg(cmd, env).split()
143 while len(l) > 1 and l[-1][0].isalpha(): # remove non-numbered tags
143 while len(l) > 1 and l[-1][0].isalpha(): # remove non-numbered tags
144 l.pop()
144 l.pop()
145 if len(l) > 1: # tag found
145 if len(l) > 1: # tag found
146 version = l[-1]
146 version = l[-1]
147 if l[0].endswith('+'): # propagate the dirty status to the tag
147 if l[0].endswith('+'): # propagate the dirty status to the tag
148 version += '+'
148 version += '+'
149 elif len(l) == 1: # no tag found
149 elif len(l) == 1: # no tag found
150 cmd = [sys.executable, 'hg', 'parents', '--template',
150 cmd = [sys.executable, 'hg', 'parents', '--template',
151 '{latesttag}+{latesttagdistance}-']
151 '{latesttag}+{latesttagdistance}-']
152 version = runhg(cmd, env) + l[0]
152 version = runhg(cmd, env) + l[0]
153 if version.endswith('+'):
153 if version.endswith('+'):
154 version += time.strftime('%Y%m%d')
154 version += time.strftime('%Y%m%d')
155 elif os.path.exists('.hg_archival.txt'):
155 elif os.path.exists('.hg_archival.txt'):
156 kw = dict([[t.strip() for t in l.split(':', 1)]
156 kw = dict([[t.strip() for t in l.split(':', 1)]
157 for l in open('.hg_archival.txt')])
157 for l in open('.hg_archival.txt')])
158 if 'tag' in kw:
158 if 'tag' in kw:
159 version = kw['tag']
159 version = kw['tag']
160 elif 'latesttag' in kw:
160 elif 'latesttag' in kw:
161 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
161 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
162 else:
162 else:
163 version = kw.get('node', '')[:12]
163 version = kw.get('node', '')[:12]
164
164
165 if version:
165 if version:
166 f = open("mercurial/__version__.py", "w")
166 f = open("mercurial/__version__.py", "w")
167 f.write('# this file is autogenerated by setup.py\n')
167 f.write('# this file is autogenerated by setup.py\n')
168 f.write('version = "%s"\n' % version)
168 f.write('version = "%s"\n' % version)
169 f.close()
169 f.close()
170
170
171
171
172 try:
172 try:
173 from mercurial import __version__
173 from mercurial import __version__
174 version = __version__.version
174 version = __version__.version
175 except ImportError:
175 except ImportError:
176 version = 'unknown'
176 version = 'unknown'
177
177
178 class hgbuildmo(build):
178 class hgbuildmo(build):
179
179
180 description = "build translations (.mo files)"
180 description = "build translations (.mo files)"
181
181
182 def run(self):
182 def run(self):
183 if not find_executable('msgfmt'):
183 if not find_executable('msgfmt'):
184 self.warn("could not find msgfmt executable, no translations "
184 self.warn("could not find msgfmt executable, no translations "
185 "will be built")
185 "will be built")
186 return
186 return
187
187
188 podir = 'i18n'
188 podir = 'i18n'
189 if not os.path.isdir(podir):
189 if not os.path.isdir(podir):
190 self.warn("could not find %s/ directory" % podir)
190 self.warn("could not find %s/ directory" % podir)
191 return
191 return
192
192
193 join = os.path.join
193 join = os.path.join
194 for po in os.listdir(podir):
194 for po in os.listdir(podir):
195 if not po.endswith('.po'):
195 if not po.endswith('.po'):
196 continue
196 continue
197 pofile = join(podir, po)
197 pofile = join(podir, po)
198 modir = join('locale', po[:-3], 'LC_MESSAGES')
198 modir = join('locale', po[:-3], 'LC_MESSAGES')
199 mofile = join(modir, 'hg.mo')
199 mofile = join(modir, 'hg.mo')
200 mobuildfile = join('mercurial', mofile)
200 mobuildfile = join('mercurial', mofile)
201 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
201 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
202 if sys.platform != 'sunos5':
202 if sys.platform != 'sunos5':
203 # msgfmt on Solaris does not know about -c
203 # msgfmt on Solaris does not know about -c
204 cmd.append('-c')
204 cmd.append('-c')
205 self.mkpath(join('mercurial', modir))
205 self.mkpath(join('mercurial', modir))
206 self.make_file([pofile], mobuildfile, spawn, (cmd,))
206 self.make_file([pofile], mobuildfile, spawn, (cmd,))
207
207
208
208
209 # Insert hgbuildmo first so that files in mercurial/locale/ are found
209 # Insert hgbuildmo first so that files in mercurial/locale/ are found
210 # when build_py is run next.
210 # when build_py is run next.
211 build.sub_commands.insert(0, ('build_mo', None))
211 build.sub_commands.insert(0, ('build_mo', None))
212
212
213 Distribution.pure = 0
213 Distribution.pure = 0
214 Distribution.global_options.append(('pure', None, "use pure (slow) Python "
214 Distribution.global_options.append(('pure', None, "use pure (slow) Python "
215 "code instead of C extensions"))
215 "code instead of C extensions"))
216
216
217 class hgbuildext(build_ext):
217 class hgbuildext(build_ext):
218
218
219 def build_extension(self, ext):
219 def build_extension(self, ext):
220 try:
220 try:
221 build_ext.build_extension(self, ext)
221 build_ext.build_extension(self, ext)
222 except CCompilerError:
222 except CCompilerError:
223 if not getattr(ext, 'optional', False):
223 if not getattr(ext, 'optional', False):
224 raise
224 raise
225 log.warn("Failed to build optional extension '%s' (skipping)",
225 log.warn("Failed to build optional extension '%s' (skipping)",
226 ext.name)
226 ext.name)
227
227
228 class hgbuildpy(build_py):
228 class hgbuildpy(build_py):
229
229
230 def finalize_options(self):
230 def finalize_options(self):
231 build_py.finalize_options(self)
231 build_py.finalize_options(self)
232
232
233 if self.distribution.pure:
233 if self.distribution.pure:
234 if self.py_modules is None:
234 if self.py_modules is None:
235 self.py_modules = []
235 self.py_modules = []
236 for ext in self.distribution.ext_modules:
236 for ext in self.distribution.ext_modules:
237 if ext.name.startswith("mercurial."):
237 if ext.name.startswith("mercurial."):
238 self.py_modules.append("mercurial.pure.%s" % ext.name[10:])
238 self.py_modules.append("mercurial.pure.%s" % ext.name[10:])
239 self.distribution.ext_modules = []
239 self.distribution.ext_modules = []
240 else:
240 else:
241 if not os.path.exists(os.path.join(get_python_inc(), 'Python.h')):
241 if not os.path.exists(os.path.join(get_python_inc(), 'Python.h')):
242 raise SystemExit("Python headers are required to build Mercurial")
242 raise SystemExit("Python headers are required to build Mercurial")
243
243
244 def find_modules(self):
244 def find_modules(self):
245 modules = build_py.find_modules(self)
245 modules = build_py.find_modules(self)
246 for module in modules:
246 for module in modules:
247 if module[0] == "mercurial.pure":
247 if module[0] == "mercurial.pure":
248 if module[1] != "__init__":
248 if module[1] != "__init__":
249 yield ("mercurial", module[1], module[2])
249 yield ("mercurial", module[1], module[2])
250 else:
250 else:
251 yield module
251 yield module
252
252
253 class hginstallscripts(install_scripts):
253 class hginstallscripts(install_scripts):
254 '''
254 '''
255 This is a specialization of install_scripts that replaces the @LIBDIR@ with
255 This is a specialization of install_scripts that replaces the @LIBDIR@ with
256 the configured directory for modules. If possible, the path is made relative
256 the configured directory for modules. If possible, the path is made relative
257 to the directory for scripts.
257 to the directory for scripts.
258 '''
258 '''
259
259
260 def initialize_options(self):
260 def initialize_options(self):
261 install_scripts.initialize_options(self)
261 install_scripts.initialize_options(self)
262
262
263 self.install_lib = None
263 self.install_lib = None
264
264
265 def finalize_options(self):
265 def finalize_options(self):
266 install_scripts.finalize_options(self)
266 install_scripts.finalize_options(self)
267 self.set_undefined_options('install',
267 self.set_undefined_options('install',
268 ('install_lib', 'install_lib'))
268 ('install_lib', 'install_lib'))
269
269
270 def run(self):
270 def run(self):
271 install_scripts.run(self)
271 install_scripts.run(self)
272
272
273 if (os.path.splitdrive(self.install_dir)[0] !=
273 if (os.path.splitdrive(self.install_dir)[0] !=
274 os.path.splitdrive(self.install_lib)[0]):
274 os.path.splitdrive(self.install_lib)[0]):
275 # can't make relative paths from one drive to another, so use an
275 # can't make relative paths from one drive to another, so use an
276 # absolute path instead
276 # absolute path instead
277 libdir = self.install_lib
277 libdir = self.install_lib
278 else:
278 else:
279 common = os.path.commonprefix((self.install_dir, self.install_lib))
279 common = os.path.commonprefix((self.install_dir, self.install_lib))
280 rest = self.install_dir[len(common):]
280 rest = self.install_dir[len(common):]
281 uplevel = len([n for n in os.path.split(rest) if n])
281 uplevel = len([n for n in os.path.split(rest) if n])
282
282
283 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
283 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
284
284
285 for outfile in self.outfiles:
285 for outfile in self.outfiles:
286 fp = open(outfile, 'rb')
286 fp = open(outfile, 'rb')
287 data = fp.read()
287 data = fp.read()
288 fp.close()
288 fp.close()
289
289
290 # skip binary files
290 # skip binary files
291 if '\0' in data:
291 if '\0' in data:
292 continue
292 continue
293
293
294 data = data.replace('@LIBDIR@', libdir.encode('string_escape'))
294 data = data.replace('@LIBDIR@', libdir.encode('string_escape'))
295 fp = open(outfile, 'wb')
295 fp = open(outfile, 'wb')
296 fp.write(data)
296 fp.write(data)
297 fp.close()
297 fp.close()
298
298
299 cmdclass = {'build_mo': hgbuildmo,
299 cmdclass = {'build_mo': hgbuildmo,
300 'build_ext': hgbuildext,
300 'build_ext': hgbuildext,
301 'build_py': hgbuildpy,
301 'build_py': hgbuildpy,
302 'install_scripts': hginstallscripts}
302 'install_scripts': hginstallscripts}
303
303
304 packages = ['mercurial', 'mercurial.hgweb', 'hgext', 'hgext.convert',
304 packages = ['mercurial', 'mercurial.hgweb',
305 'hgext.highlight', 'hgext.zeroconf']
305 'mercurial.httpclient', 'mercurial.httpclient.tests',
306 'hgext', 'hgext.convert', 'hgext.highlight', 'hgext.zeroconf']
306
307
307 pymodules = []
308 pymodules = []
308
309
309 extmodules = [
310 extmodules = [
310 Extension('mercurial.base85', ['mercurial/base85.c']),
311 Extension('mercurial.base85', ['mercurial/base85.c']),
311 Extension('mercurial.bdiff', ['mercurial/bdiff.c']),
312 Extension('mercurial.bdiff', ['mercurial/bdiff.c']),
312 Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c']),
313 Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c']),
313 Extension('mercurial.mpatch', ['mercurial/mpatch.c']),
314 Extension('mercurial.mpatch', ['mercurial/mpatch.c']),
314 Extension('mercurial.parsers', ['mercurial/parsers.c']),
315 Extension('mercurial.parsers', ['mercurial/parsers.c']),
315 ]
316 ]
316
317
317 osutil_ldflags = []
318 osutil_ldflags = []
318
319
319 if sys.platform == 'darwin':
320 if sys.platform == 'darwin':
320 osutil_ldflags += ['-framework', 'ApplicationServices']
321 osutil_ldflags += ['-framework', 'ApplicationServices']
321
322
322 # disable osutil.c under windows + python 2.4 (issue1364)
323 # disable osutil.c under windows + python 2.4 (issue1364)
323 if sys.platform == 'win32' and sys.version_info < (2, 5, 0, 'final'):
324 if sys.platform == 'win32' and sys.version_info < (2, 5, 0, 'final'):
324 pymodules.append('mercurial.pure.osutil')
325 pymodules.append('mercurial.pure.osutil')
325 else:
326 else:
326 extmodules.append(Extension('mercurial.osutil', ['mercurial/osutil.c'],
327 extmodules.append(Extension('mercurial.osutil', ['mercurial/osutil.c'],
327 extra_link_args=osutil_ldflags))
328 extra_link_args=osutil_ldflags))
328
329
329 if sys.platform == 'linux2' and os.uname()[2] > '2.6':
330 if sys.platform == 'linux2' and os.uname()[2] > '2.6':
330 # The inotify extension is only usable with Linux 2.6 kernels.
331 # The inotify extension is only usable with Linux 2.6 kernels.
331 # You also need a reasonably recent C library.
332 # You also need a reasonably recent C library.
332 # In any case, if it fails to build the error will be skipped ('optional').
333 # In any case, if it fails to build the error will be skipped ('optional').
333 cc = new_compiler()
334 cc = new_compiler()
334 if hasfunction(cc, 'inotify_add_watch'):
335 if hasfunction(cc, 'inotify_add_watch'):
335 inotify = Extension('hgext.inotify.linux._inotify',
336 inotify = Extension('hgext.inotify.linux._inotify',
336 ['hgext/inotify/linux/_inotify.c'],
337 ['hgext/inotify/linux/_inotify.c'],
337 ['mercurial'])
338 ['mercurial'])
338 inotify.optional = True
339 inotify.optional = True
339 extmodules.append(inotify)
340 extmodules.append(inotify)
340 packages.extend(['hgext.inotify', 'hgext.inotify.linux'])
341 packages.extend(['hgext.inotify', 'hgext.inotify.linux'])
341
342
342 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
343 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
343 'help/*.txt']}
344 'help/*.txt']}
344
345
345 def ordinarypath(p):
346 def ordinarypath(p):
346 return p and p[0] != '.' and p[-1] != '~'
347 return p and p[0] != '.' and p[-1] != '~'
347
348
348 for root in ('templates',):
349 for root in ('templates',):
349 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
350 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
350 curdir = curdir.split(os.sep, 1)[1]
351 curdir = curdir.split(os.sep, 1)[1]
351 dirs[:] = filter(ordinarypath, dirs)
352 dirs[:] = filter(ordinarypath, dirs)
352 for f in filter(ordinarypath, files):
353 for f in filter(ordinarypath, files):
353 f = os.path.join(curdir, f)
354 f = os.path.join(curdir, f)
354 packagedata['mercurial'].append(f)
355 packagedata['mercurial'].append(f)
355
356
356 datafiles = []
357 datafiles = []
357 setupversion = version
358 setupversion = version
358 extra = {}
359 extra = {}
359
360
360 if py2exeloaded:
361 if py2exeloaded:
361 extra['console'] = [
362 extra['console'] = [
362 {'script':'hg',
363 {'script':'hg',
363 'copyright':'Copyright (C) 2005-2010 Matt Mackall and others',
364 'copyright':'Copyright (C) 2005-2010 Matt Mackall and others',
364 'product_version':version}]
365 'product_version':version}]
365
366
366 if os.name == 'nt':
367 if os.name == 'nt':
367 # Windows binary file versions for exe/dll files must have the
368 # Windows binary file versions for exe/dll files must have the
368 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
369 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
369 setupversion = version.split('+', 1)[0]
370 setupversion = version.split('+', 1)[0]
370
371
371 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
372 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
372 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
373 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
373 # distutils.sysconfig
374 # distutils.sysconfig
374 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[0].splitlines()[0]
375 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[0].splitlines()[0]
375 # Also parse only first digit, because 3.2.1 can't be parsed nicely
376 # Also parse only first digit, because 3.2.1 can't be parsed nicely
376 if (version.startswith('Xcode') and
377 if (version.startswith('Xcode') and
377 StrictVersion(version.split()[1]) >= StrictVersion('4.0')):
378 StrictVersion(version.split()[1]) >= StrictVersion('4.0')):
378 os.environ['ARCHFLAGS'] = '-arch i386 -arch x86_64'
379 os.environ['ARCHFLAGS'] = '-arch i386 -arch x86_64'
379
380
380 setup(name='mercurial',
381 setup(name='mercurial',
381 version=setupversion,
382 version=setupversion,
382 author='Matt Mackall',
383 author='Matt Mackall',
383 author_email='mpm@selenic.com',
384 author_email='mpm@selenic.com',
384 url='http://mercurial.selenic.com/',
385 url='http://mercurial.selenic.com/',
385 description='Scalable distributed SCM',
386 description='Scalable distributed SCM',
386 license='GNU GPLv2+',
387 license='GNU GPLv2+',
387 scripts=scripts,
388 scripts=scripts,
388 packages=packages,
389 packages=packages,
389 py_modules=pymodules,
390 py_modules=pymodules,
390 ext_modules=extmodules,
391 ext_modules=extmodules,
391 data_files=datafiles,
392 data_files=datafiles,
392 package_data=packagedata,
393 package_data=packagedata,
393 cmdclass=cmdclass,
394 cmdclass=cmdclass,
394 options=dict(py2exe=dict(packages=['hgext', 'email']),
395 options=dict(py2exe=dict(packages=['hgext', 'email']),
395 bdist_mpkg=dict(zipdist=True,
396 bdist_mpkg=dict(zipdist=True,
396 license='COPYING',
397 license='COPYING',
397 readme='contrib/macosx/Readme.html',
398 readme='contrib/macosx/Readme.html',
398 welcome='contrib/macosx/Welcome.html')),
399 welcome='contrib/macosx/Welcome.html')),
399 **extra)
400 **extra)
General Comments 0
You need to be logged in to leave comments. Login now