diff --git a/mercurial/httpclient/__init__.py b/mercurial/httpclient/__init__.py --- a/mercurial/httpclient/__init__.py +++ b/mercurial/httpclient/__init__.py @@ -165,7 +165,13 @@ class HTTPResponse(object): 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 @@ class HTTPConnection(object): # 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 diff --git a/mercurial/httpclient/tests/simple_http_test.py b/mercurial/httpclient/tests/simple_http_test.py --- a/mercurial/httpclient/tests/simple_http_test.py +++ b/mercurial/httpclient/tests/simple_http_test.py @@ -39,7 +39,7 @@ class SimpleHttpTest(util.HttpTestBase, 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 @@ dotencode '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() diff --git a/mercurial/httpclient/tests/test_proxy_support.py b/mercurial/httpclient/tests/test_proxy_support.py --- a/mercurial/httpclient/tests/test_proxy_support.py +++ b/mercurial/httpclient/tests/test_proxy_support.py @@ -97,12 +97,12 @@ class ProxyHttpTest(util.HttpTestBase, u '\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' diff --git a/mercurial/httpclient/tests/test_ssl.py b/mercurial/httpclient/tests/test_ssl.py new file mode 100644 --- /dev/null +++ b/mercurial/httpclient/tests/test_ssl.py @@ -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')) diff --git a/mercurial/httpclient/tests/util.py b/mercurial/httpclient/tests/util.py --- a/mercurial/httpclient/tests/util.py +++ b/mercurial/httpclient/tests/util.py @@ -109,12 +109,29 @@ def mockselect(r, w, x, timeout=0): 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 @@ class HttpTestBase(object): 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