From c5ffc7bf5db448bb211c015a20ab763f72e098cb Mon Sep 17 00:00:00 2001 From: junglecow Date: Mon, 13 Nov 2006 14:24:41 +0000 Subject: [PATCH] - Added support for PyOpenSSL (with fallback to old library if not found) - Also added a wrapper for cleaner compatibility handling of old SSL library - Changed exception handling to be more precise. (Catching Exception. Ish.) - Added lots of debug printing You will need to install pyopenssl (Debian unstable: python-pyopenssl) to try this. --- src/common/xmpp/transports_nb.py | 321 ++++++++++++++++++++++++++++--- 1 file changed, 299 insertions(+), 22 deletions(-) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index ad3456e6a..edab59ea5 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -25,6 +25,33 @@ import sys import os import errno +import traceback +import thread + +import logging +h = logging.StreamHandler() +f = logging.Formatter('%(asctime)s %(name)s: %(levelname)s: %(message)s') +h.setFormatter(f) +log = logging.getLogger('Gajim.transports') +log.addHandler(h) +log.setLevel(logging.DEBUG) +log.propagate = False +del h, f + +USE_PYOPENSSL = False + +try: + #raise ImportError("Manually disabled PyOpenSSL") + import OpenSSL.SSL + import OpenSSL.crypto + USE_PYOPENSSL = True + print "PyOpenSSL loaded." +except ImportError: + # FIXME: Remove these prints before release, replace with a warning dialog. + print "=" * 79 + print "PyOpenSSL not found, falling back to Python builtin SSL objects (insecure)." + print "=" * 79 + # timeout to connect to the server socket, it doesn't include auth CONNECT_TIMEOUT_SECONDS = 30 @@ -33,7 +60,120 @@ DISCONNECT_TIMEOUT_SECONDS = 10 # size of the buffer which reads data from server # if lower, more stanzas will be fragmented and processed twice -RECV_BUFSIZE = 1048576 +RECV_BUFSIZE = 32768 # 2x maximum size of ssl packet, should be plenty +#RECV_BUFSIZE = 16 # FIXME: (#2634) gajim breaks with this setting: it's inefficient but should work. + +def torf(cond, tv, fv): + if cond: return tv + return fv + +class SSLWrapper: + def __init__(this, sslobj): + this.sslobj = sslobj + print "init called with", sslobj + + # We can return None out of this function to signal that no data is + # available right now. Better than an exception, which differs + # depending on which SSL lib we're using. Unfortunately returning '' + # can indicate that the socket has been closed, so to be sure, we avoid + # this. + + def recv(this, data, flags=None): + raise NotImplementedException() + + def send(this, data, flags=None): + raise NotImplementedException() + +class PyOpenSSLWrapper(SSLWrapper): + '''Wrapper class for PyOpenSSL's recv() and send() methods''' + parent = SSLWrapper + + def __init__(this, *args): + this.parent.__init__(this, *args) + + def is_numtoolarge(this, e): + t = ('asn1 encoding routines', 'a2d_ASN1_OBJECT', 'first num too large') + return isinstance(e.args, (list, tuple)) and len(e.args) == 1 and \ + isinstance(e.args[0], (list, tuple)) and len(e.args[0]) == 2 and \ + e.args[0][0] == e.args[0][1] == t + + def recv(this, bufsize, flags=None): + retval = None + try: + #print "recv: thread id:", thread.get_ident() + if flags is None: retval = this.sslobj.recv(bufsize) + else: retval = this.sslobj.recv(bufsize, flags) + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e: + log.debug("Recv: " + repr(e)) + except OpenSSL.SSL.Error, e: + "Recv: Caught OpenSSL.SSL.Error:" + traceback.print_exc() + print "Current Stack:" + traceback.print_stack() + if this.is_numtoolarge(e): + # print an error but ignore this exception + log.warning("Recv: OpenSSL: asn1enc: first num too large (eaten)") + else: + raise + + return retval + + def send(this, data, flags=None): + try: + #print "send: thread id:", thread.get_ident() + if flags is None: return this.sslobj.send(data) + else: return this.sslobj.send(data, flags) + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e: + log.debug("Send: " + repr(e)) + except OpenSSL.SSL.Error, e: + print "Send: Caught OpenSSL.SSL.Error:" + traceback.print_exc() + print "Current Stack:" + traceback.print_stack() + if this.is_numtoolarge(e): + # warn, but ignore this exception + log.warning("Send: OpenSSL: asn1enc: first num too large (ignoring)") + else: + raise + return 0 + +class StdlibSSLWrapper(SSLWrapper): + '''Wrapper class for Python's socket.ssl read() and write() methods''' + parent = SSLWrapper + + def __init__(this, *args): + this.parent.__init__(this, *args) + + def recv(this, bufsize, flags=None): + # we simply ignore flags since ssl object doesn't support it + retval = None + try: + retval = this.sslobj.read(bufsize) + except socket.sslerror, e: + print "Got socket.sslerror" + print e, e.args + traceback.print_exc() + print "Current Stack:" + traceback.print_stack() + if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): + raise + if retval is None: return '' + if retval == '': raise SSLWrapper.SocketShutdown('Connection Closed') + return retval + + def send(this, data, flags=None): + # we simply ignore flags since ssl object doesn't support it + try: + return this.sslobj.write(data) + except socket.sslerror, e: + print "Got socket.sslerror" + print e, e.args + traceback.print_exc() + print "Current Stack:" + traceback.print_stack() + if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): + raise + return 0 class NonBlockingTcp(PlugIn, IdleObject): ''' This class can be used instead of transports.Tcp in threadless implementations ''' @@ -109,6 +249,7 @@ class NonBlockingTcp(PlugIn, IdleObject): self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.setblocking(False) except: + traceback.print_exc() sys.exc_clear() if self.on_connect_failure: self.on_connect_failure() @@ -161,6 +302,7 @@ class NonBlockingTcp(PlugIn, IdleObject): self._sock.shutdown(socket.SHUT_RDWR) self._sock.close() except: + traceback.print_exc() # socket is already closed sys.exc_clear() # socket descriptor cannot be (un)plugged anymore @@ -204,34 +346,56 @@ class NonBlockingTcp(PlugIn, IdleObject): def _do_receive(self): ''' Reads all pending incoming data. Calls owner's disconnected() method if appropriate.''' - received = '' + ERR_DISCONN = -2 # Misc error signifying that we got disconnected + ERR_OTHER = -1 # Other error + received = None errnum = 0 + errtxt = 'No Error Set' try: # get as many bites, as possible, but not more than RECV_BUFSIZE received = self._recv(RECV_BUFSIZE) - except Exception, e: - if len(e.args) > 0 and isinstance(e.args[0], int): - errnum = e[0] - sys.exc_clear() - # "received" will be empty anyhow - if errnum == socket.SSL_ERROR_WANT_READ: - pass - elif errnum in [errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN]: + except (socket.error, socket.herror, socket.gaierror), e: + print "Got socket exception" + traceback.print_exc() + print "Current Stack:" + traceback.print_stack() + errnum = e[0] + errtxt = str(errnum) + ':' + e[1] + except socket.sslerror, e: + print "Got unknown socket.sslerror" + traceback.print_exc() + print "Current Stack:" + traceback.print_stack() + errnum = ERR_OTHER + errtxt = repr("socket.sslerror: " + e.args) + + # Should we really do this? In C, recv() will happily return 0 + # in nonblocking mode when there is no data waiting, and in + # some cases select() will mark the socket for reading when + # there is nothing to read, and the socket is still open. For + # example, this can happen when the remote sends a zero-length + # tcp packet. + if received is '': + errnum = ERR_DISCONN + errtxt = "Connection closed unexpectedly" + + if errnum in (ERR_DISCONN, errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN): + self.DEBUG(errtxt, 'error') self.pollend() # don't proccess result, cas it will raise error return - elif not received : - if errnum != socket.SSL_ERROR_EOF: - # 8 EOF occurred in violation of protocol - self.DEBUG('Socket error while receiving data', 'error') - self.pollend() - if self.state >= 0: - self.disconnect() - return - + + if received is None: + if errnum != 0: + self.DEBUG(errtxt, 'error') + if self.state >= 0: + self.disconnect() + return + received = '' + if self.state < 0: return - + # we have received some bites, stop the timeout! self.renew_send_timeout() if self.on_receive: @@ -268,6 +432,7 @@ class NonBlockingTcp(PlugIn, IdleObject): self._plug_idle() self._on_send() except socket.error, e: + traceback.print_exc() sys.exc_clear() if e[0] == socket.SSL_ERROR_WANT_WRITE: return True @@ -287,6 +452,7 @@ class NonBlockingTcp(PlugIn, IdleObject): try: self._sock.connect(self._server) except socket.error, e: + traceback.print_exc() errnum = e[0] sys.exc_clear() # in progress, or would block @@ -361,6 +527,14 @@ class NonBlockingTcp(PlugIn, IdleObject): class NonBlockingTLS(PlugIn): ''' TLS connection used to encrypts already estabilished tcp connection.''' + + # from ssl.h (partial extract) + ssl_h_bits = { "SSL_ST_CONNECT": 0x1000, "SSL_ST_ACCEPT": 0x2000, + "SSL_CB_LOOP": 0x01, "SSL_CB_EXIT": 0x02, + "SSL_CB_READ": 0x04, "SSL_CB_WRITE": 0x08, + "SSL_CB_ALERT": 0x4000, + "SSL_CB_HANDSHAKE_START": 0x10, "SSL_CB_HANDSHAKE_DONE": 0x20} + def PlugIn(self, owner, now=0, on_tls_start = None): ''' If the 'now' argument is true then starts using encryption immidiatedly. If 'now' in false then starts encryption as soon as TLS feature is @@ -375,6 +549,7 @@ class NonBlockingTLS(PlugIn): try: res = self._startSSL() except Exception, e: + traceback.print_exc() self._owner.socket.pollend() return self.tls_start() @@ -416,18 +591,118 @@ class NonBlockingTLS(PlugIn): self.tls_start() raise NodeProcessed + def _dumpX509(this, cert): + print "Digest (SHA-1):", cert.digest("sha1") + print "Digest (MD5):", cert.digest("md5") + print "Serial #:", cert.get_serial_number() + print "Version:", cert.get_version() + print "Expired:", torf(cert.has_expired(), "Yes", "No") + print "Subject:" + this._dumpX509Name(cert.get_subject()) + print "Issuer:" + this._dumpX509Name(cert.get_issuer()) + this._dumpPKey(cert.get_pubkey()) + + def _dumpX509Name(this, name): + print "X509Name:", str(name) + + def _dumpPKey(this, pkey): + typedict = {OpenSSL.crypto.TYPE_RSA: "RSA", OpenSSL.crypto.TYPE_DSA: "DSA"} + print "PKey bits:", pkey.bits() + print "PKey type: %s (%d)" % (typedict.get(pkey.type(), "Unknown"), pkey.type()) + def _startSSL(self): ''' Immidiatedly switch socket to TLS mode. Used internally.''' + print "_startSSL called" + if USE_PYOPENSSL: return self._startSSL_pyOpenSSL() + return self._startSSL_stdlib() + + def _startSSL_pyOpenSSL(self): + print "_startSSL_pyOpenSSL called, thread id:", thread.get_ident() + tcpsock = self._owner.Connection + # FIXME: should method be configurable? + tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD) + tcpsock._sslContext.set_info_callback(self._ssl_info_callback) + tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext, tcpsock._sock) + tcpsock._sslObj.set_connect_state() # set to client mode + + wrapper = PyOpenSSLWrapper(tcpsock._sslObj) + tcpsock._recv = wrapper.recv + tcpsock._send = wrapper.send + + print "Initiating handshake..." + #tcpsock._sslObj.setblocking(True) + try: + self.starttls='in progress' + tcpsock._sslObj.do_handshake() + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e: + log.debug("do_handshake: " + str(e)) + #tcpsock._sslObj.setblocking(False) + #print "Done handshake" + print "Async handshake started..." + + def _on_ssl_handshake_done(self): + print "Handshake done!" + self.starttls='success' + + tcpsock = self._owner.Connection + cert = tcpsock._sslObj.get_peer_certificate() + peer = cert.get_subject() + issuer = cert.get_issuer() + tcpsock._sslIssuer = unicode(issuer) + tcpsock._sslServer = unicode(peer) + + # FIXME: remove debug prints + peercert = tcpsock._sslObj.get_peer_certificate() + ciphers = tcpsock._sslObj.get_cipher_list() + + print "Ciphers:", ciphers + print "Peer cert:", peercert + self._dumpX509(peercert) + + print OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, peercert) + + def _startSSL_stdlib(self): + print "_startSSL_stdlib called" tcpsock=self._owner.Connection tcpsock._sock.setblocking(True) tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) tcpsock._sock.setblocking(False) tcpsock._sslIssuer = tcpsock._sslObj.issuer() tcpsock._sslServer = tcpsock._sslObj.server() - tcpsock._recv = tcpsock._sslObj.read - tcpsock._send = tcpsock._sslObj.write + wrapper = StdlibSSLWrapper(tcpsock._sslObj) + tcpsock._recv = wrapper.recv + tcpsock._send = wrapper.send self.starttls='success' + def _ssl_info_callback(this, sslconn, type, st): + # Exceptions can't propagate up through this callback, so print them here. + try: this._ssl_info_callback_guarded(sslconn, type, st) + except: traceback.print_exc() + + def _ssl_info_callback_guarded(this, sslconn, type, st): + b = this.ssl_h_bits + + #if type & b['SSL_CB_LOOP']: + # if type & SSL_ST_CONNECT: tls_state = "connect" + # elif type & SSL_ST_ACCEPT: tls_state = "accept" + # else: tls_state = "undefined" + # print "tls_state: %s: %s" % (tls_state, sslconn.state_string()) + + #if type & b['SSL_CB_ALERT']: + # if type & SSL_CB_READ: rdwr = "read" + # elif type & SSL_CB_WRITE: rdwr = "write" + # else: rdwr = "unknown" + # print "tls_alert: %s:%d: %s" % (rdwr, st, sslconn.state_string()) + + #mask = "" + #for k, v in b.iteritems(): + # if type & v: mask += " " + k + #print "mask:", mask, st + + if type & b['SSL_CB_HANDSHAKE_DONE']: + this._on_ssl_handshake_done() + def StartTLSHandler(self, conn, starttls): ''' Handle server reply if TLS is allowed to process. Behaves accordingly. Used internally.''' @@ -441,6 +716,7 @@ class NonBlockingTLS(PlugIn): try: self._startSSL() except Exception, e: + traceback.print_exc() self._owner.socket.pollend() return self._owner.Dispatcher.PlugOut() @@ -499,6 +775,7 @@ class NBHTTPPROXYsocket(NonBlockingTcp): try: proto, code, desc = reply.split('\n')[0].split(' ', 2) except: + traceback.print_exc() raise error('Invalid proxy reply') if code <> '200': self.DEBUG('Invalid proxy reply: %s %s %s' % (proto, code, desc),'error')