httpclient: import revision b8c3511a8cae from py-nonblocking-http
Fixes issues with SSL_ERROR_WANT_READ incorrectly breaking the
response read.
--- a/mercurial/httpclient/__init__.py Mon May 16 21:56:26 2011 +0200
+++ b/mercurial/httpclient/__init__.py Thu May 12 10:48:31 2011 -0500
@@ -165,7 +165,13 @@
logger.info('timed out with timeout of %s', self._timeout)
raise HTTPTimeoutException('timeout reading data')
logger.info('cl: %r body: %r', self._content_len, self._body)
- data = self.sock.recv(INCOMING_BUFFER_SIZE)
+ try:
+ data = self.sock.recv(INCOMING_BUFFER_SIZE)
+ except socket.sslerror, e:
+ if e.args[0] != socket.SSL_ERROR_WANT_READ:
+ raise
+ logger.debug('SSL_WANT_READ in _select, should retry later')
+ return True
logger.debug('response read %d data during _select', len(data))
if not data:
if not self.headers:
@@ -545,7 +551,14 @@
# incoming data
if r:
try:
- data = r[0].recv(INCOMING_BUFFER_SIZE)
+ try:
+ data = r[0].recv(INCOMING_BUFFER_SIZE)
+ except socket.sslerror, e:
+ if e.args[0] != socket.SSL_ERROR_WANT_READ:
+ raise
+ logger.debug(
+ 'SSL_WANT_READ while sending data, retrying...')
+ continue
if not data:
logger.info('socket appears closed in read')
outgoing_headers = body = None
--- a/mercurial/httpclient/tests/simple_http_test.py Mon May 16 21:56:26 2011 +0200
+++ b/mercurial/httpclient/tests/simple_http_test.py Thu May 12 10:48:31 2011 -0500
@@ -39,7 +39,7 @@
def _run_simple_test(self, host, server_data, expected_req, expected_data):
con = http.HTTPConnection(host)
con._connect()
- con.sock.data = server_data
+ con.sock.data.extend(server_data)
con.request('GET', '/')
self.assertStringEqual(expected_req, con.sock.sent)
@@ -224,19 +224,6 @@
'accept-encoding: identity\r\n\r\n'),
'1234567890')
- def doPost(self, con, expect_body, body_to_send='This is some POST data'):
- con.request('POST', '/', body=body_to_send,
- expect_continue=True)
- expected_req = ('POST / HTTP/1.1\r\n'
- 'Host: 1.2.3.4\r\n'
- 'content-length: %d\r\n'
- 'Expect: 100-Continue\r\n'
- 'accept-encoding: identity\r\n\r\n' %
- len(body_to_send))
- if expect_body:
- expected_req += body_to_send
- return expected_req
-
def testEarlyContinueResponse(self):
con = http.HTTPConnection('1.2.3.4:80')
con._connect()
--- a/mercurial/httpclient/tests/test_proxy_support.py Mon May 16 21:56:26 2011 +0200
+++ b/mercurial/httpclient/tests/test_proxy_support.py Thu May 12 10:48:31 2011 -0500
@@ -97,12 +97,12 @@
'\r\n'
'1234567890'])
con._connect()
- con.sock.data = ['HTTP/1.1 200 OK\r\n',
- 'Server: BogusServer 1.0\r\n',
- 'Content-Length: 10\r\n',
- '\r\n'
- '1234567890'
- ]
+ con.sock.data.extend(['HTTP/1.1 200 OK\r\n',
+ 'Server: BogusServer 1.0\r\n',
+ 'Content-Length: 10\r\n',
+ '\r\n'
+ '1234567890'
+ ])
con.request('GET', '/')
expected_req = ('CONNECT 1.2.3.4:443 HTTP/1.0\r\n'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/httpclient/tests/test_ssl.py Thu May 12 10:48:31 2011 -0500
@@ -0,0 +1,94 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+import unittest
+
+import http
+
+# relative import to ease embedding the library
+import util
+
+
+
+class HttpSslTest(util.HttpTestBase, unittest.TestCase):
+ def testSslRereadRequired(self):
+ con = http.HTTPConnection('1.2.3.4:443')
+ con._connect()
+ # extend the list instead of assign because of how
+ # MockSSLSocket works.
+ con.sock.data.extend(['HTTP/1.1 200 OK\r\n',
+ 'Server: BogusServer 1.0\r\n',
+ 'MultiHeader: Value\r\n'
+ 'MultiHeader: Other Value\r\n'
+ 'MultiHeader: One More!\r\n'
+ 'Content-Length: 10\r\n',
+ '\r\n'
+ '1234567890'
+ ])
+ con.request('GET', '/')
+
+ expected_req = ('GET / HTTP/1.1\r\n'
+ 'Host: 1.2.3.4\r\n'
+ 'accept-encoding: identity\r\n\r\n')
+
+ self.assertEqual(('1.2.3.4', 443), con.sock.sa)
+ self.assertEqual(expected_req, con.sock.sent)
+ resp = con.getresponse()
+ self.assertEqual('1234567890', resp.read())
+ self.assertEqual(['Value', 'Other Value', 'One More!'],
+ resp.headers.getheaders('multiheader'))
+ self.assertEqual(['BogusServer 1.0'],
+ resp.headers.getheaders('server'))
+
+ def testSslRereadInEarlyResponse(self):
+ con = http.HTTPConnection('1.2.3.4:443')
+ con._connect()
+ # extend the list instead of assign because of how
+ # MockSSLSocket works.
+ con.sock.early_data.extend(['HTTP/1.1 200 OK\r\n',
+ 'Server: BogusServer 1.0\r\n',
+ 'MultiHeader: Value\r\n'
+ 'MultiHeader: Other Value\r\n'
+ 'MultiHeader: One More!\r\n'
+ 'Content-Length: 10\r\n',
+ '\r\n'
+ '1234567890'
+ ])
+
+ expected_req = self.doPost(con, False)
+ self.assertEqual(None, con.sock,
+ 'Connection should have disowned socket')
+
+ resp = con.getresponse()
+ self.assertEqual(('1.2.3.4', 443), resp.sock.sa)
+ self.assertEqual(expected_req, resp.sock.sent)
+ self.assertEqual('1234567890', resp.read())
+ self.assertEqual(['Value', 'Other Value', 'One More!'],
+ resp.headers.getheaders('multiheader'))
+ self.assertEqual(['BogusServer 1.0'],
+ resp.headers.getheaders('server'))
--- a/mercurial/httpclient/tests/util.py Mon May 16 21:56:26 2011 +0200
+++ b/mercurial/httpclient/tests/util.py Thu May 12 10:48:31 2011 -0500
@@ -109,12 +109,29 @@
return readable, w[:], []
+class MockSSLSocket(object):
+ def __init__(self, sock):
+ self._sock = sock
+ self._fail_recv = True
+
+ def __getattr__(self, key):
+ return getattr(self._sock, key)
+
+ def recv(self, amt=-1):
+ try:
+ if self._fail_recv:
+ raise socket.sslerror(socket.SSL_ERROR_WANT_READ)
+ return self._sock.recv(amt=amt)
+ finally:
+ self._fail_recv = not self._fail_recv
+
+
def mocksslwrap(sock, keyfile=None, certfile=None,
server_side=False, cert_reqs=http.socketutil.CERT_NONE,
ssl_version=http.socketutil.PROTOCOL_SSLv23, ca_certs=None,
do_handshake_on_connect=True,
suppress_ragged_eofs=True):
- return sock
+ return MockSSLSocket(sock)
def mockgetaddrinfo(host, port, unused, streamtype):
@@ -157,4 +174,17 @@
add_nl(l.splitlines()), add_nl(r.splitlines()),
fromfile='expected', tofile='got'))
raise
+
+ def doPost(self, con, expect_body, body_to_send='This is some POST data'):
+ con.request('POST', '/', body=body_to_send,
+ expect_continue=True)
+ expected_req = ('POST / HTTP/1.1\r\n'
+ 'Host: 1.2.3.4\r\n'
+ 'content-length: %d\r\n'
+ 'Expect: 100-Continue\r\n'
+ 'accept-encoding: identity\r\n\r\n' %
+ len(body_to_send))
+ if expect_body:
+ expected_req += body_to_send
+ return expected_req
# no-check-code