New submission from Chris Stawarz:

The current version of the ssl module doesn't support non-blocking
creation of SSLSocket objects.  The reason for this is that the SSL
handshaking (SSL_connect/SSL_accept) takes place during the
construction of the SSLContext object (in newPySSLObject).  This means
that if the socket being wrapped is non-blocking, and the handshake
fails with SSL_ERROR_WANT_READ/SSL_ERROR_WANT_WRITE, then the entire
SSLContext is scrapped, and newPySSLObject must be run again in its
entirety.  Unfortunately, restarting from scratch on the same socket
appears to confuse the remote host, and the new attempt fails.

The attached patch fixes this problem by removing the handshaking code
from newPySSLObject and adding a do_handshake method to SSLContext.
It also adds a new parameter (do_handshake_on_connect) to the
SSLSocket constructor and the wrap_socket function.  The default value
of the parameter is True, which preserves the current behavior of the
module by immediately calling do_handshake after sslwrap.  If
do_handshake_on_connect is set to False, then the caller is
responsible for calling do_handshake.  This allows code that uses
non-blocking sockets to first create the SSLSocket and then
iteratively call do_handshake and select.select until the process
completes (which is exactly how non-blocking reads and writes are
handled).

----------
components: Documentation, Library (Lib), Tests
files: ssl_nonblocking_handshake_patch.txt
messages: 56295
nosy: chris.stawarz
severity: normal
status: open
title: ssl module doesn't support non-blocking handshakes
type: rfe
versions: Python 2.6

__________________________________
Tracker <[EMAIL PROTECTED]>
<http://bugs.python.org/issue1251>
__________________________________
Index: Doc/library/ssl.rst
===================================================================
--- Doc/library/ssl.rst (revision 58397)
+++ Doc/library/ssl.rst (working copy)
@@ -54,7 +54,7 @@
    network connection.  This error is a subtype of :exc:`socket.error`, which
    in turn is a subtype of :exc:`IOError`.
 
-.. function:: wrap_socket (sock, keyfile=None, certfile=None, 
server_side=False, cert_reqs=CERT_NONE, ssl_version={see docs}, ca_certs=None)
+.. function:: wrap_socket (sock, keyfile=None, certfile=None, 
server_side=False, cert_reqs=CERT_NONE, ssl_version={see docs}, ca_certs=None, 
do_handshake_on_connect=True)
 
    Takes an instance ``sock`` of :class:`socket.socket`, and returns an 
instance of :class:`ssl.SSLSocket`, a subtype
    of :class:`socket.socket`, which wraps the underlying socket in an SSL 
context.
@@ -98,6 +98,10 @@
    See the discussion of :ref:`ssl-certificates` for more information about 
how to arrange
    the certificates in this file.
 
+   The parameter ``do_handshake_on_connect`` is a boolean that indicates 
whether a TLS/SSL
+   handshake should be initiated as soon as the socket is connected.  If 
False, the
+   socket's :meth:`do_handshake` method must be called to perform a handshake.
+
    The parameter ``ssl_version`` specifies which version of the SSL protocol 
to use.
    Typically, the server chooses a particular protocol version, and the client
    must adapt to the server's choice.  Most of the versions are not 
interoperable
@@ -289,7 +293,11 @@
    number of secret bits being used.  If no connection has been
    established, returns ``None``.
 
+.. method:: SSLSocket.do_handshake()
 
+   Perform a TLS/SSL handshake.
+
+
 .. index:: single: certificates
 
 .. index:: single: X509 certificate
Index: Lib/ssl.py
===================================================================
--- Lib/ssl.py  (revision 58397)
+++ Lib/ssl.py  (working copy)
@@ -86,7 +86,8 @@
 
     def __init__(self, sock, keyfile=None, certfile=None,
                  server_side=False, cert_reqs=CERT_NONE,
-                 ssl_version=PROTOCOL_SSLv23, ca_certs=None):
+                 ssl_version=PROTOCOL_SSLv23, ca_certs=None,
+                 do_handshake_on_connect=True):
         socket.__init__(self, _sock=sock._sock)
         if certfile and not keyfile:
             keyfile = certfile
@@ -101,11 +102,14 @@
             self._sslobj = _ssl.sslwrap(self._sock, server_side,
                                         keyfile, certfile,
                                         cert_reqs, ssl_version, ca_certs)
+            if do_handshake_on_connect:
+                self.do_handshake()
         self.keyfile = keyfile
         self.certfile = certfile
         self.cert_reqs = cert_reqs
         self.ssl_version = ssl_version
         self.ca_certs = ca_certs
+        self.do_handshake_on_connect = do_handshake_on_connect
 
     def read(self, len=1024):
 
@@ -189,6 +193,12 @@
         self._sslobj = None
         socket.close(self)
 
+    def do_handshake(self):
+
+        """Perform a TLS/SSL handshake."""
+
+        self._sslobj.do_handshake()
+
     def connect(self, addr):
 
         """Connects to remote ADDR, and then wraps the connection in
@@ -202,6 +212,8 @@
         self._sslobj = _ssl.sslwrap(self._sock, False, self.keyfile, 
self.certfile,
                                     self.cert_reqs, self.ssl_version,
                                     self.ca_certs)
+        if self.do_handshake_on_connect:
+            self.do_handshake()
 
     def accept(self):
 
@@ -212,7 +224,7 @@
         newsock, addr = socket.accept(self)
         return (SSLSocket(newsock, True, self.keyfile, self.certfile,
                           self.cert_reqs, self.ssl_version,
-                          self.ca_certs), addr)
+                          self.ca_certs, self.do_handshake_on_connect), addr)
 
 
     def makefile(self, mode='r', bufsize=-1):
@@ -459,11 +471,13 @@
 
 def wrap_socket(sock, keyfile=None, certfile=None,
                 server_side=False, cert_reqs=CERT_NONE,
-                ssl_version=PROTOCOL_SSLv23, ca_certs=None):
+                ssl_version=PROTOCOL_SSLv23, ca_certs=None,
+                do_handshake_on_connect=True):
 
     return SSLSocket(sock, keyfile=keyfile, certfile=certfile,
                      server_side=server_side, cert_reqs=cert_reqs,
-                     ssl_version=ssl_version, ca_certs=ca_certs)
+                     ssl_version=ssl_version, ca_certs=ca_certs,
+                     do_handshake_on_connect=do_handshake_on_connect)
 
 # some utility functions
 
@@ -549,5 +563,7 @@
     for compability with Python 2.5 and earlier.  Will disappear in
     Python 3.0."""
 
-    return _ssl.sslwrap(sock._sock, 0, keyfile, certfile, CERT_NONE,
-                        PROTOCOL_SSLv23, None)
+    ssl_sock = _ssl.sslwrap(sock._sock, 0, keyfile, certfile, CERT_NONE,
+                            PROTOCOL_SSLv23, None)
+    ssl_sock.do_handshake()
+    return ssl_sock
Index: Lib/test/test_ssl.py
===================================================================
--- Lib/test/test_ssl.py        (revision 58397)
+++ Lib/test/test_ssl.py        (working copy)
@@ -3,6 +3,7 @@
 import sys
 import unittest
 from test import test_support
+import select
 import socket
 import errno
 import subprocess
@@ -57,6 +58,25 @@
             s.close()
 
 
+    def testNonBlockingHandshake(self):
+        s = ssl.wrap_socket(socket.socket(socket.AF_INET),
+                            cert_reqs=ssl.CERT_NONE,
+                            do_handshake_on_connect=False)
+        s.connect(("svn.python.org", 443))
+        s.setblocking(False)
+        while True:
+            try:
+                s.do_handshake()
+                break
+            except ssl.SSLError, err:
+                if err.args[0] == ssl.SSL_ERROR_WANT_READ:
+                    select.select([s], [], [])
+                elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE:
+                    select.select([], [s], [])
+                else:
+                    raise
+        s.close()
+
     def testCrucialConstants(self):
         ssl.PROTOCOL_SSLv2
         ssl.PROTOCOL_SSLv23
Index: Modules/_ssl.c
===================================================================
--- Modules/_ssl.c      (revision 58397)
+++ Modules/_ssl.c      (working copy)
@@ -265,8 +265,6 @@
        PySSLObject *self;
        char *errstr = NULL;
        int ret;
-       int err;
-       int sockstate;
        int verification_mode;
 
        self = PyObject_New(PySSLObject, &PySSL_Type); /* Create new object */
@@ -388,57 +386,6 @@
                SSL_set_accept_state(self->ssl);
        PySSL_END_ALLOW_THREADS
 
-       /* Actually negotiate SSL connection */
-       /* XXX If SSL_connect() returns 0, it's also a failure. */
-       sockstate = 0;
-       do {
-               PySSL_BEGIN_ALLOW_THREADS
-               if (socket_type == PY_SSL_CLIENT)
-                       ret = SSL_connect(self->ssl);
-               else
-                       ret = SSL_accept(self->ssl);
-               err = SSL_get_error(self->ssl, ret);
-               PySSL_END_ALLOW_THREADS
-               if(PyErr_CheckSignals()) {
-                       goto fail;
-               }
-               if (err == SSL_ERROR_WANT_READ) {
-                       sockstate = check_socket_and_wait_for_timeout(Sock, 0);
-               } else if (err == SSL_ERROR_WANT_WRITE) {
-                       sockstate = check_socket_and_wait_for_timeout(Sock, 1);
-               } else {
-                       sockstate = SOCKET_OPERATION_OK;
-               }
-               if (sockstate == SOCKET_HAS_TIMED_OUT) {
-                       PyErr_SetString(PySSLErrorObject,
-                               ERRSTR("The connect operation timed out"));
-                       goto fail;
-               } else if (sockstate == SOCKET_HAS_BEEN_CLOSED) {
-                       PyErr_SetString(PySSLErrorObject,
-                               ERRSTR("Underlying socket has been closed."));
-                       goto fail;
-               } else if (sockstate == SOCKET_TOO_LARGE_FOR_SELECT) {
-                       PyErr_SetString(PySSLErrorObject,
-                         ERRSTR("Underlying socket too large for select()."));
-                       goto fail;
-               } else if (sockstate == SOCKET_IS_NONBLOCKING) {
-                       break;
-               }
-       } while (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE);
-       if (ret < 1) {
-               PySSL_SetError(self, ret, __FILE__, __LINE__);
-               goto fail;
-       }
-       self->ssl->debug = 1;
-
-       PySSL_BEGIN_ALLOW_THREADS
-       if ((self->peer_cert = SSL_get_peer_certificate(self->ssl))) {
-               X509_NAME_oneline(X509_get_subject_name(self->peer_cert),
-                                 self->server, X509_NAME_MAXLEN);
-               X509_NAME_oneline(X509_get_issuer_name(self->peer_cert),
-                                 self->issuer, X509_NAME_MAXLEN);
-       }
-       PySSL_END_ALLOW_THREADS
        self->Socket = Sock;
        Py_INCREF(self->Socket);
        return self;
@@ -488,6 +435,65 @@
 
 /* SSL object methods */
 
+static PyObject *PySSL_SSLdo_handshake(PySSLObject *self)
+{
+       int ret;
+       int err;
+       int sockstate;
+
+       /* Actually negotiate SSL connection */
+       /* XXX If SSL_do_handshake() returns 0, it's also a failure. */
+       sockstate = 0;
+       do {
+               PySSL_BEGIN_ALLOW_THREADS
+               ret = SSL_do_handshake(self->ssl);
+               err = SSL_get_error(self->ssl, ret);
+               PySSL_END_ALLOW_THREADS
+               if(PyErr_CheckSignals()) {
+                       return NULL;
+               }
+               if (err == SSL_ERROR_WANT_READ) {
+                       sockstate = 
check_socket_and_wait_for_timeout(self->Socket, 0);
+               } else if (err == SSL_ERROR_WANT_WRITE) {
+                       sockstate = 
check_socket_and_wait_for_timeout(self->Socket, 1);
+               } else {
+                       sockstate = SOCKET_OPERATION_OK;
+               }
+               if (sockstate == SOCKET_HAS_TIMED_OUT) {
+                       PyErr_SetString(PySSLErrorObject,
+                               ERRSTR("The handshake operation timed out"));
+                       return NULL;
+               } else if (sockstate == SOCKET_HAS_BEEN_CLOSED) {
+                       PyErr_SetString(PySSLErrorObject,
+                               ERRSTR("Underlying socket has been closed."));
+                       return NULL;
+               } else if (sockstate == SOCKET_TOO_LARGE_FOR_SELECT) {
+                       PyErr_SetString(PySSLErrorObject,
+                         ERRSTR("Underlying socket too large for select()."));
+                       return NULL;
+               } else if (sockstate == SOCKET_IS_NONBLOCKING) {
+                       break;
+               }
+       } while (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE);
+       if (ret < 1)
+               return PySSL_SetError(self, ret, __FILE__, __LINE__);
+       self->ssl->debug = 1;
+
+       if (self->peer_cert)
+               X509_free (self->peer_cert);
+       PySSL_BEGIN_ALLOW_THREADS
+       if ((self->peer_cert = SSL_get_peer_certificate(self->ssl))) {
+               X509_NAME_oneline(X509_get_subject_name(self->peer_cert),
+                                 self->server, X509_NAME_MAXLEN);
+               X509_NAME_oneline(X509_get_issuer_name(self->peer_cert),
+                                 self->issuer, X509_NAME_MAXLEN);
+       }
+       PySSL_END_ALLOW_THREADS
+
+       Py_INCREF(Py_None);
+       return Py_None;
+}
+
 static PyObject *
 PySSL_server(PySSLObject *self)
 {
@@ -1286,6 +1292,7 @@
 Read up to len bytes from the SSL socket.");
 
 static PyMethodDef PySSLMethods[] = {
+       {"do_handshake", (PyCFunction)PySSL_SSLdo_handshake, METH_NOARGS},
        {"write", (PyCFunction)PySSL_SSLwrite, METH_VARARGS,
         PySSL_SSLwrite_doc},
        {"read", (PyCFunction)PySSL_SSLread, METH_VARARGS,
_______________________________________________
Python-bugs-list mailing list 
Unsubscribe: 
http://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com

Reply via email to