httpclient: import revision b8c3511a8cae from py-nonblocking-http
authorAugie Fackler <durin42@gmail.com>
Thu, 12 May 2011 10:48:31 -0500
changeset 14341 5c3de67e7402
parent 14340 defa319d8bb7
child 14342 c0b6a734b4f3
httpclient: import revision b8c3511a8cae from py-nonblocking-http Fixes issues with SSL_ERROR_WANT_READ incorrectly breaking the response read.
mercurial/httpclient/__init__.py
mercurial/httpclient/tests/simple_http_test.py
mercurial/httpclient/tests/test_proxy_support.py
mercurial/httpclient/tests/test_ssl.py
mercurial/httpclient/tests/util.py
--- 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