- 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.
This commit is contained in:
junglecow 2006-11-13 14:24:41 +00:00
parent 0f53cbb3f6
commit c5ffc7bf5d
1 changed files with 299 additions and 22 deletions

View File

@ -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')