##// 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 2 # This is the mercurial setup script.
3 3 #
4 4 # 'python setup.py install', or
5 5 # 'python setup.py --help' for more options
6 6
7 7 import sys
8 8 if not hasattr(sys, 'version_info') or sys.version_info < (2, 4, 0, 'final'):
9 9 raise SystemExit("Mercurial requires Python 2.4 or later.")
10 10
11 11 if sys.version_info[0] >= 3:
12 12 def b(s):
13 13 '''A helper function to emulate 2.6+ bytes literals using string
14 14 literals.'''
15 15 return s.encode('latin1')
16 16 else:
17 17 def b(s):
18 18 '''A helper function to emulate 2.6+ bytes literals using string
19 19 literals.'''
20 20 return s
21 21
22 22 # Solaris Python packaging brain damage
23 23 try:
24 24 import hashlib
25 25 sha = hashlib.sha1()
26 26 except:
27 27 try:
28 28 import sha
29 29 except:
30 30 raise SystemExit(
31 31 "Couldn't import standard hashlib (incomplete Python install).")
32 32
33 33 try:
34 34 import zlib
35 35 except:
36 36 raise SystemExit(
37 37 "Couldn't import standard zlib (incomplete Python install).")
38 38
39 39 try:
40 40 import bz2
41 41 except:
42 42 raise SystemExit(
43 43 "Couldn't import standard bz2 (incomplete Python install).")
44 44
45 45 import os, subprocess, time
46 46 import shutil
47 47 import tempfile
48 48 from distutils import log
49 49 from distutils.core import setup, Extension
50 50 from distutils.dist import Distribution
51 51 from distutils.command.build import build
52 52 from distutils.command.build_ext import build_ext
53 53 from distutils.command.build_py import build_py
54 54 from distutils.command.install_scripts import install_scripts
55 55 from distutils.spawn import spawn, find_executable
56 56 from distutils.ccompiler import new_compiler
57 57 from distutils.errors import CCompilerError
58 58 from distutils.sysconfig import get_python_inc
59 59 from distutils.version import StrictVersion
60 60
61 61 scripts = ['hg']
62 62 if os.name == 'nt':
63 63 scripts.append('contrib/win32/hg.bat')
64 64
65 65 # simplified version of distutils.ccompiler.CCompiler.has_function
66 66 # that actually removes its temporary files.
67 67 def hasfunction(cc, funcname):
68 68 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
69 69 devnull = oldstderr = None
70 70 try:
71 71 try:
72 72 fname = os.path.join(tmpdir, 'funcname.c')
73 73 f = open(fname, 'w')
74 74 f.write('int main(void) {\n')
75 75 f.write(' %s();\n' % funcname)
76 76 f.write('}\n')
77 77 f.close()
78 78 # Redirect stderr to /dev/null to hide any error messages
79 79 # from the compiler.
80 80 # This will have to be changed if we ever have to check
81 81 # for a function on Windows.
82 82 devnull = open('/dev/null', 'w')
83 83 oldstderr = os.dup(sys.stderr.fileno())
84 84 os.dup2(devnull.fileno(), sys.stderr.fileno())
85 85 objects = cc.compile([fname], output_dir=tmpdir)
86 86 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
87 87 except:
88 88 return False
89 89 return True
90 90 finally:
91 91 if oldstderr is not None:
92 92 os.dup2(oldstderr, sys.stderr.fileno())
93 93 if devnull is not None:
94 94 devnull.close()
95 95 shutil.rmtree(tmpdir)
96 96
97 97 # py2exe needs to be installed to work
98 98 try:
99 99 import py2exe
100 100 py2exeloaded = True
101 101 except ImportError:
102 102 py2exeloaded = False
103 103
104 104 def runcmd(cmd, env):
105 105 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
106 106 stderr=subprocess.PIPE, env=env)
107 107 out, err = p.communicate()
108 108 return out, err
109 109
110 110 def runhg(cmd, env):
111 111 out, err = runcmd(cmd, env)
112 112 # If root is executing setup.py, but the repository is owned by
113 113 # another user (as in "sudo python setup.py install") we will get
114 114 # trust warnings since the .hg/hgrc file is untrusted. That is
115 115 # fine, we don't want to load it anyway. Python may warn about
116 116 # a missing __init__.py in mercurial/locale, we also ignore that.
117 117 err = [e for e in err.splitlines()
118 118 if not e.startswith(b('Not trusting file')) \
119 119 and not e.startswith(b('warning: Not importing'))]
120 120 if err:
121 121 return ''
122 122 return out
123 123
124 124 version = ''
125 125
126 126 if os.path.isdir('.hg'):
127 127 # Execute hg out of this directory with a custom environment which
128 128 # includes the pure Python modules in mercurial/pure. We also take
129 129 # care to not use any hgrc files and do no localization.
130 130 pypath = ['mercurial', os.path.join('mercurial', 'pure')]
131 131 env = {'PYTHONPATH': os.pathsep.join(pypath),
132 132 'HGRCPATH': '',
133 133 'LANGUAGE': 'C'}
134 134 if 'LD_LIBRARY_PATH' in os.environ:
135 135 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
136 136 if 'SystemRoot' in os.environ:
137 137 # Copy SystemRoot into the custom environment for Python 2.6
138 138 # under Windows. Otherwise, the subprocess will fail with
139 139 # error 0xc0150004. See: http://bugs.python.org/issue3440
140 140 env['SystemRoot'] = os.environ['SystemRoot']
141 141 cmd = [sys.executable, 'hg', 'id', '-i', '-t']
142 142 l = runhg(cmd, env).split()
143 143 while len(l) > 1 and l[-1][0].isalpha(): # remove non-numbered tags
144 144 l.pop()
145 145 if len(l) > 1: # tag found
146 146 version = l[-1]
147 147 if l[0].endswith('+'): # propagate the dirty status to the tag
148 148 version += '+'
149 149 elif len(l) == 1: # no tag found
150 150 cmd = [sys.executable, 'hg', 'parents', '--template',
151 151 '{latesttag}+{latesttagdistance}-']
152 152 version = runhg(cmd, env) + l[0]
153 153 if version.endswith('+'):
154 154 version += time.strftime('%Y%m%d')
155 155 elif os.path.exists('.hg_archival.txt'):
156 156 kw = dict([[t.strip() for t in l.split(':', 1)]
157 157 for l in open('.hg_archival.txt')])
158 158 if 'tag' in kw:
159 159 version = kw['tag']
160 160 elif 'latesttag' in kw:
161 161 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
162 162 else:
163 163 version = kw.get('node', '')[:12]
164 164
165 165 if version:
166 166 f = open("mercurial/__version__.py", "w")
167 167 f.write('# this file is autogenerated by setup.py\n')
168 168 f.write('version = "%s"\n' % version)
169 169 f.close()
170 170
171 171
172 172 try:
173 173 from mercurial import __version__
174 174 version = __version__.version
175 175 except ImportError:
176 176 version = 'unknown'
177 177
178 178 class hgbuildmo(build):
179 179
180 180 description = "build translations (.mo files)"
181 181
182 182 def run(self):
183 183 if not find_executable('msgfmt'):
184 184 self.warn("could not find msgfmt executable, no translations "
185 185 "will be built")
186 186 return
187 187
188 188 podir = 'i18n'
189 189 if not os.path.isdir(podir):
190 190 self.warn("could not find %s/ directory" % podir)
191 191 return
192 192
193 193 join = os.path.join
194 194 for po in os.listdir(podir):
195 195 if not po.endswith('.po'):
196 196 continue
197 197 pofile = join(podir, po)
198 198 modir = join('locale', po[:-3], 'LC_MESSAGES')
199 199 mofile = join(modir, 'hg.mo')
200 200 mobuildfile = join('mercurial', mofile)
201 201 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
202 202 if sys.platform != 'sunos5':
203 203 # msgfmt on Solaris does not know about -c
204 204 cmd.append('-c')
205 205 self.mkpath(join('mercurial', modir))
206 206 self.make_file([pofile], mobuildfile, spawn, (cmd,))
207 207
208 208
209 209 # Insert hgbuildmo first so that files in mercurial/locale/ are found
210 210 # when build_py is run next.
211 211 build.sub_commands.insert(0, ('build_mo', None))
212 212
213 213 Distribution.pure = 0
214 214 Distribution.global_options.append(('pure', None, "use pure (slow) Python "
215 215 "code instead of C extensions"))
216 216
217 217 class hgbuildext(build_ext):
218 218
219 219 def build_extension(self, ext):
220 220 try:
221 221 build_ext.build_extension(self, ext)
222 222 except CCompilerError:
223 223 if not getattr(ext, 'optional', False):
224 224 raise
225 225 log.warn("Failed to build optional extension '%s' (skipping)",
226 226 ext.name)
227 227
228 228 class hgbuildpy(build_py):
229 229
230 230 def finalize_options(self):
231 231 build_py.finalize_options(self)
232 232
233 233 if self.distribution.pure:
234 234 if self.py_modules is None:
235 235 self.py_modules = []
236 236 for ext in self.distribution.ext_modules:
237 237 if ext.name.startswith("mercurial."):
238 238 self.py_modules.append("mercurial.pure.%s" % ext.name[10:])
239 239 self.distribution.ext_modules = []
240 240 else:
241 241 if not os.path.exists(os.path.join(get_python_inc(), 'Python.h')):
242 242 raise SystemExit("Python headers are required to build Mercurial")
243 243
244 244 def find_modules(self):
245 245 modules = build_py.find_modules(self)
246 246 for module in modules:
247 247 if module[0] == "mercurial.pure":
248 248 if module[1] != "__init__":
249 249 yield ("mercurial", module[1], module[2])
250 250 else:
251 251 yield module
252 252
253 253 class hginstallscripts(install_scripts):
254 254 '''
255 255 This is a specialization of install_scripts that replaces the @LIBDIR@ with
256 256 the configured directory for modules. If possible, the path is made relative
257 257 to the directory for scripts.
258 258 '''
259 259
260 260 def initialize_options(self):
261 261 install_scripts.initialize_options(self)
262 262
263 263 self.install_lib = None
264 264
265 265 def finalize_options(self):
266 266 install_scripts.finalize_options(self)
267 267 self.set_undefined_options('install',
268 268 ('install_lib', 'install_lib'))
269 269
270 270 def run(self):
271 271 install_scripts.run(self)
272 272
273 273 if (os.path.splitdrive(self.install_dir)[0] !=
274 274 os.path.splitdrive(self.install_lib)[0]):
275 275 # can't make relative paths from one drive to another, so use an
276 276 # absolute path instead
277 277 libdir = self.install_lib
278 278 else:
279 279 common = os.path.commonprefix((self.install_dir, self.install_lib))
280 280 rest = self.install_dir[len(common):]
281 281 uplevel = len([n for n in os.path.split(rest) if n])
282 282
283 283 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
284 284
285 285 for outfile in self.outfiles:
286 286 fp = open(outfile, 'rb')
287 287 data = fp.read()
288 288 fp.close()
289 289
290 290 # skip binary files
291 291 if '\0' in data:
292 292 continue
293 293
294 294 data = data.replace('@LIBDIR@', libdir.encode('string_escape'))
295 295 fp = open(outfile, 'wb')
296 296 fp.write(data)
297 297 fp.close()
298 298
299 299 cmdclass = {'build_mo': hgbuildmo,
300 300 'build_ext': hgbuildext,
301 301 'build_py': hgbuildpy,
302 302 'install_scripts': hginstallscripts}
303 303
304 packages = ['mercurial', 'mercurial.hgweb', 'hgext', 'hgext.convert',
305 'hgext.highlight', 'hgext.zeroconf']
304 packages = ['mercurial', 'mercurial.hgweb',
305 'mercurial.httpclient', 'mercurial.httpclient.tests',
306 'hgext', 'hgext.convert', 'hgext.highlight', 'hgext.zeroconf']
306 307
307 308 pymodules = []
308 309
309 310 extmodules = [
310 311 Extension('mercurial.base85', ['mercurial/base85.c']),
311 312 Extension('mercurial.bdiff', ['mercurial/bdiff.c']),
312 313 Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c']),
313 314 Extension('mercurial.mpatch', ['mercurial/mpatch.c']),
314 315 Extension('mercurial.parsers', ['mercurial/parsers.c']),
315 316 ]
316 317
317 318 osutil_ldflags = []
318 319
319 320 if sys.platform == 'darwin':
320 321 osutil_ldflags += ['-framework', 'ApplicationServices']
321 322
322 323 # disable osutil.c under windows + python 2.4 (issue1364)
323 324 if sys.platform == 'win32' and sys.version_info < (2, 5, 0, 'final'):
324 325 pymodules.append('mercurial.pure.osutil')
325 326 else:
326 327 extmodules.append(Extension('mercurial.osutil', ['mercurial/osutil.c'],
327 328 extra_link_args=osutil_ldflags))
328 329
329 330 if sys.platform == 'linux2' and os.uname()[2] > '2.6':
330 331 # The inotify extension is only usable with Linux 2.6 kernels.
331 332 # You also need a reasonably recent C library.
332 333 # In any case, if it fails to build the error will be skipped ('optional').
333 334 cc = new_compiler()
334 335 if hasfunction(cc, 'inotify_add_watch'):
335 336 inotify = Extension('hgext.inotify.linux._inotify',
336 337 ['hgext/inotify/linux/_inotify.c'],
337 338 ['mercurial'])
338 339 inotify.optional = True
339 340 extmodules.append(inotify)
340 341 packages.extend(['hgext.inotify', 'hgext.inotify.linux'])
341 342
342 343 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
343 344 'help/*.txt']}
344 345
345 346 def ordinarypath(p):
346 347 return p and p[0] != '.' and p[-1] != '~'
347 348
348 349 for root in ('templates',):
349 350 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
350 351 curdir = curdir.split(os.sep, 1)[1]
351 352 dirs[:] = filter(ordinarypath, dirs)
352 353 for f in filter(ordinarypath, files):
353 354 f = os.path.join(curdir, f)
354 355 packagedata['mercurial'].append(f)
355 356
356 357 datafiles = []
357 358 setupversion = version
358 359 extra = {}
359 360
360 361 if py2exeloaded:
361 362 extra['console'] = [
362 363 {'script':'hg',
363 364 'copyright':'Copyright (C) 2005-2010 Matt Mackall and others',
364 365 'product_version':version}]
365 366
366 367 if os.name == 'nt':
367 368 # Windows binary file versions for exe/dll files must have the
368 369 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
369 370 setupversion = version.split('+', 1)[0]
370 371
371 372 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
372 373 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
373 374 # distutils.sysconfig
374 375 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[0].splitlines()[0]
375 376 # Also parse only first digit, because 3.2.1 can't be parsed nicely
376 377 if (version.startswith('Xcode') and
377 378 StrictVersion(version.split()[1]) >= StrictVersion('4.0')):
378 379 os.environ['ARCHFLAGS'] = '-arch i386 -arch x86_64'
379 380
380 381 setup(name='mercurial',
381 382 version=setupversion,
382 383 author='Matt Mackall',
383 384 author_email='mpm@selenic.com',
384 385 url='http://mercurial.selenic.com/',
385 386 description='Scalable distributed SCM',
386 387 license='GNU GPLv2+',
387 388 scripts=scripts,
388 389 packages=packages,
389 390 py_modules=pymodules,
390 391 ext_modules=extmodules,
391 392 data_files=datafiles,
392 393 package_data=packagedata,
393 394 cmdclass=cmdclass,
394 395 options=dict(py2exe=dict(packages=['hgext', 'email']),
395 396 bdist_mpkg=dict(zipdist=True,
396 397 license='COPYING',
397 398 readme='contrib/macosx/Readme.html',
398 399 welcome='contrib/macosx/Welcome.html')),
399 400 **extra)
General Comments 0
You need to be logged in to leave comments. Login now