moved TLS and SSL classes from transports_nb to new tls_nb module, fixed HTTP CONNECT proxy transport

This commit is contained in:
tomk 2008-06-30 23:02:12 +00:00
parent f3820706fb
commit 937bb01a69
4 changed files with 529 additions and 486 deletions

View File

@ -70,7 +70,9 @@ class PlugIn:
self._old_owners_methods.append(owner.__dict__[method.__name__])
owner.__dict__[method.__name__]=method
owner.__dict__[self.__class__.__name__]=self
if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner)
# following will not work for classes inheriting plugin()
#if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner)
if hasattr(self,'plugin'): return self.plugin(owner)
def PlugOut(self):
""" Unregister all our staff from main instance and detach from it. """

View File

@ -25,7 +25,7 @@ import socket
import debug
import random
import transports_nb, dispatcher_nb, auth_nb, roster_nb, protocol
import transports_nb, tls_nb, dispatcher_nb, auth_nb, roster_nb, protocol
from client import *
import logging
@ -184,10 +184,11 @@ class NBCommonClient:
bosh_uri = proxy['host'],
bosh_port = tcp_port)
else:
self.socket = transports_nb.NBHTTPProxySocket(
on_disconnect=self.on_disconnect,
proxy_creds=(None, None),
xmpp_server=(self.Server, self.Port))
# HTTP CONNECT to proxy from environment variables
self.socket = transports_nb.NBHTTPProxySocket(
on_disconnect=self.on_disconnect,
proxy_creds=(None, None),
xmpp_server=(self.Server, self.Port))
else:
self._on_tcp_failure = self._on_connect_failure
tcp_server=self.Server
@ -432,7 +433,7 @@ class NonBlockingClient(NBCommonClient):
self._on_connect()
return
# otherwise start TLS
transports_nb.NonBlockingTLS().PlugIn(
tls_nb.NonBlockingTLS().PlugIn(
self,
on_tls_success=lambda: self._xmpp_connect(socket_type='tls'),
on_tls_failure=self._on_connect_failure)

437
src/common/xmpp/tls_nb.py Normal file
View File

@ -0,0 +1,437 @@
## tls_nb.py
## based on transports_nb.py
##
## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov
## modified by Dimitur Kirov <dkirov@gmail.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2, or (at your option)
## any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
import socket
from client import PlugIn
from protocol import *
import sys
import os
import errno
import time
import traceback
import logging
log = logging.getLogger('gajim.c.x.tls_nb')
consoleloghandler = logging.StreamHandler()
consoleloghandler.setLevel(logging.DEBUG)
consoleloghandler.setFormatter(
logging.Formatter('%(levelname)s: %(message)s')
)
log.setLevel(logging.DEBUG)
log.addHandler(consoleloghandler)
log.propagate = False
# I don't need to load gajim.py just because of few TLS variables, so I changed
# %s/common\.gajim\.DATA_DIR/\'\.\.\/data\'/c
# %s/common\.gajim\.MY_CACERTS/\'\%s\/\.gajim\/cacerts\.pem\' \% os\.environ\[\'HOME\'\]/c
# To change it back do:
# %s/\'\.\.\/data\'/common\.gajim\.DATA_DIR/c
# %s/\'%s\/\.gajim\/cacerts\.pem\'\ %\ os\.environ\[\'HOME\'\]/common\.gajim\.MY_CACERTS/c
# TODO: make the paths configurable - as constructor parameters or sth
# import common.gajim
USE_PYOPENSSL = False
try:
#raise ImportError("Manually disabled PyOpenSSL")
import OpenSSL.SSL
import OpenSSL.crypto
USE_PYOPENSSL = True
log.info("PyOpenSSL loaded")
except ImportError:
log.debug("Import of PyOpenSSL failed:", exc_info=True)
# FIXME: Remove these prints before release, replace with a warning dialog.
print >> sys.stderr, "=" * 79
print >> sys.stderr, "PyOpenSSL not found, falling back to Python builtin SSL objects (insecure)."
print >> sys.stderr, "=" * 79
def torf(cond, tv, fv):
if cond: return tv
return fv
def gattr(obj, attr, default=None):
try:
return getattr(obj, attr)
except:
return default
class SSLWrapper:
class Error(IOError):
def __init__(self, sock=None, exc=None, errno=None, strerror=None, peer=None):
self.parent = IOError
errno = errno or gattr(exc, 'errno')
strerror = strerror or gattr(exc, 'strerror') or gattr(exc, 'args')
if not isinstance(strerror, basestring): strerror = repr(strerror)
self.sock = sock
self.exc = exc
self.peer = peer
self.exc_name = None
self.exc_args = None
self.exc_str = None
self.exc_repr = None
if self.exc is not None:
self.exc_name = str(self.exc.__class__)
self.exc_args = gattr(self.exc, 'args')
self.exc_str = str(self.exc)
self.exc_repr = repr(self.exc)
if not errno:
try:
if isinstance(exc, OpenSSL.SSL.SysCallError):
if self.exc_args[0] > 0:
errno = self.exc_args[0]
strerror = self.exc_args[1]
except: pass
self.parent.__init__(self, errno, strerror)
if self.peer is None and sock is not None:
try:
ppeer = self.sock.getpeername()
if len(ppeer) == 2 and isinstance(ppeer[0], basestring) \
and isinstance(ppeer[1], int):
self.peer = ppeer
except: pass
def __str__(self):
s = str(self.__class__)
if self.peer: s += " for %s:%d" % self.peer
if self.errno is not None: s += ": [Errno: %d]" % self.errno
if self.strerror: s += " (%s)" % self.strerror
if self.exc_name:
s += ", Caused by %s" % self.exc_name
if self.exc_str:
if self.strerror: s += "(%s)" % self.exc_str
else: s += "(%s)" % str(self.exc_args)
return s
def __init__(self, sslobj, sock=None):
self.sslobj = sslobj
self.sock = sock
log.debug("%s.__init__ called with %s", self.__class__, sslobj)
def recv(self, data, flags=None):
""" Receive wrapper for SSL object
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 by returning None. """
raise NotImplementedException()
def send(self, data, flags=None, now = False):
raise NotImplementedException()
class PyOpenSSLWrapper(SSLWrapper):
'''Wrapper class for PyOpenSSL's recv() and send() methods'''
def __init__(self, *args):
self.parent = SSLWrapper
self.parent.__init__(self, *args)
def is_numtoolarge(self, 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(self, bufsize, flags=None):
retval = None
try:
if flags is None: retval = self.sslobj.recv(bufsize)
else: retval = self.sslobj.recv(bufsize, flags)
except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e:
pass
# log.debug("Recv: " + repr(e))
except OpenSSL.SSL.SysCallError, e:
log.debug("Recv: Got OpenSSL.SSL.SysCallError: " + repr(e), exc_info=True)
#traceback.print_exc()
raise SSLWrapper.Error(self.sock or self.sslobj, e)
except OpenSSL.SSL.Error, e:
if self.is_numtoolarge(e):
# warn, but ignore this exception
log.warning("Recv: OpenSSL: asn1enc: first num too large (ignored)")
else:
log.debug("Recv: Caught OpenSSL.SSL.Error:", exc_info=True)
#traceback.print_exc()
#print "Current Stack:"
#traceback.print_stack()
raise SSLWrapper.Error(self.sock or self.sslobj, e)
return retval
def send(self, data, flags=None, now = False):
try:
if flags is None: return self.sslobj.send(data)
else: return self.sslobj.send(data, flags)
except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e:
#log.debug("Send: " + repr(e))
time.sleep(0.1) # prevent 100% CPU usage
except OpenSSL.SSL.SysCallError, e:
log.error("Send: Got OpenSSL.SSL.SysCallError: " + repr(e), exc_info=True)
#traceback.print_exc()
raise SSLWrapper.Error(self.sock or self.sslobj, e)
except OpenSSL.SSL.Error, e:
if self.is_numtoolarge(e):
# warn, but ignore this exception
log.warning("Send: OpenSSL: asn1enc: first num too large (ignored)")
else:
log.error("Send: Caught OpenSSL.SSL.Error:", exc_info=True)
#traceback.print_exc()
#print "Current Stack:"
#traceback.print_stack()
raise SSLWrapper.Error(self.sock or self.sslobj, e)
return 0
class StdlibSSLWrapper(SSLWrapper):
'''Wrapper class for Python's socket.ssl read() and write() methods'''
def __init__(self, *args):
self.parent = SSLWrapper
self.parent.__init__(self, *args)
def recv(self, bufsize, flags=None):
# we simply ignore flags since ssl object doesn't support it
try:
return self.sslobj.read(bufsize)
except socket.sslerror, e:
#log.debug("Recv: Caught socket.sslerror:", exc_info=True)
#traceback.print_exc()
if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE):
raise SSLWrapper.Error(self.sock or self.sslobj, e)
return None
def send(self, data, flags=None, now = False):
# we simply ignore flags since ssl object doesn't support it
try:
return self.sslobj.write(data)
except socket.sslerror, e:
#log.debug("Send: Caught socket.sslerror:", exc_info=True)
#traceback.print_exc()
if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE):
raise SSLWrapper.Error(self.sock or self.sslobj, e)
return 0
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, on_tls_success, on_tls_failure, now=0):
''' If the 'now' argument is true then starts using encryption immidiatedly.
If 'now' in false then starts encryption as soon as TLS feature is
declared by the server (if it were already declared - it is ok).
'''
if owner.__dict__.has_key('NonBlockingTLS'):
return # Already enabled.
PlugIn.PlugIn(self, owner)
DBG_LINE='NonBlockingTLS'
self.on_tls_success = on_tls_success
self.on_tls_faliure = on_tls_failure
if now:
try:
res = self._startSSL()
except Exception, e:
log.error("PlugIn: while trying _startSSL():", exc_info=True)
#traceback.print_exc()
self._owner.socket.pollend()
return
on_tls_success()
return res
if self._owner.Dispatcher.Stream.features:
try:
self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
except NodeProcessed:
pass
else:
self._owner.RegisterHandlerOnce('features',self.FeaturesHandler, xmlns=NS_STREAMS)
self.starttls = None
def plugout(self,now=0):
''' Unregisters TLS handler's from owner's dispatcher. Take note that encription
can not be stopped once started. You can only break the connection and start over.'''
# if dispatcher is not plugged we cannot (un)register handlers
if self._owner.__dict__.has_key('Dispatcher'):
self._owner.UnregisterHandler('features', self.FeaturesHandler,xmlns=NS_STREAMS)
self._owner.Dispatcher.PlugOut()
self._owner = None
def FeaturesHandler(self, conn, feats):
''' Used to analyse server <features/> tag for TLS support.
If TLS is supported starts the encryption negotiation. Used internally '''
if not feats.getTag('starttls', namespace=NS_TLS):
self.DEBUG("TLS unsupported by remote server.", 'warn')
self.on_tls_failure("TLS unsupported by remote server.")
return
self.DEBUG("TLS supported by remote server. Requesting TLS start.", 'ok')
self._owner.RegisterHandlerOnce('proceed', self.StartTLSHandler, xmlns=NS_TLS)
self._owner.RegisterHandlerOnce('failure', self.StartTLSHandler, xmlns=NS_TLS)
self._owner.send('<starttls xmlns="%s"/>' % NS_TLS)
raise NodeProcessed
def _dumpX509(self, cert, stream=sys.stderr):
print >> stream, "Digest (SHA-1):", cert.digest("sha1")
print >> stream, "Digest (MD5):", cert.digest("md5")
print >> stream, "Serial #:", cert.get_serial_number()
print >> stream, "Version:", cert.get_version()
print >> stream, "Expired:", torf(cert.has_expired(), "Yes", "No")
print >> stream, "Subject:"
self._dumpX509Name(cert.get_subject(), stream)
print >> stream, "Issuer:"
self._dumpX509Name(cert.get_issuer(), stream)
self._dumpPKey(cert.get_pubkey(), stream)
def _dumpX509Name(self, name, stream=sys.stderr):
print >> stream, "X509Name:", str(name)
def _dumpPKey(self, pkey, stream=sys.stderr):
typedict = {OpenSSL.crypto.TYPE_RSA: "RSA", OpenSSL.crypto.TYPE_DSA: "DSA"}
print >> stream, "PKey bits:", pkey.bits()
print >> stream, "PKey type: %s (%d)" % (typedict.get(pkey.type(), "Unknown"), pkey.type())
def _startSSL(self):
''' Immidiatedly switch socket to TLS mode. Used internally.'''
log.debug("_startSSL called")
if USE_PYOPENSSL: return self._startSSL_pyOpenSSL()
return self._startSSL_stdlib()
def _startSSL_pyOpenSSL(self):
#log.debug("_startSSL_pyOpenSSL called, thread id: %s", str(thread.get_ident()))
log.debug("_startSSL_pyOpenSSL called")
tcpsock = self._owner.Connection
# FIXME: should method be configurable?
tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
#tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
tcpsock.ssl_errnum = 0
tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER, self._ssl_verify_callback)
cacerts = os.path.join('../data', 'other', 'cacerts.pem')
try:
tcpsock._sslContext.load_verify_locations(cacerts)
except:
log.warning('Unable to load SSL certificats from file %s' % \
os.path.abspath(cacerts))
# load users certs
if os.path.isfile('%s/.gajim/cacerts.pem' % os.environ['HOME']):
store = tcpsock._sslContext.get_cert_store()
f = open('%s/.gajim/cacerts.pem' % os.environ['HOME'])
lines = f.readlines()
i = 0
begin = -1
for line in lines:
if 'BEGIN CERTIFICATE' in line:
begin = i
elif 'END CERTIFICATE' in line and begin > -1:
cert = ''.join(lines[begin:i+2])
try:
X509cert = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert)
store.add_cert(X509cert)
except OpenSSL.crypto.Error, exception_obj:
log.warning('Unable to load a certificate from file %s: %s' %\
('%s/.gajim/cacerts.pem' % os.environ['HOME'], exception_obj.args[0][0][2]))
except:
log.warning(
'Unknown error while loading certificate from file %s' % \
'%s/.gajim/cacerts.pem' % os.environ['HOME'])
begin = -1
i += 1
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
log.debug("Initiating handshake...")
# FIXME: Figure out why _connect_success is called before the
# SSL handshake is completed in STARTTLS mode. See #2838.
tcpsock._sslObj.setblocking(True)
try:
self.starttls='in progress'
tcpsock._sslObj.do_handshake()
except:
log.error('Error while TLS handshake: ', exc_info=True)
self.on_tls_failure('Error while TLS Handshake')
return
tcpsock._sslObj.setblocking(False)
log.debug("Synchronous handshake completed")
#log.debug("Async handshake started...")
# fake it, for now
self.starttls='success'
def _startSSL_stdlib(self):
log.debug("_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()
wrapper = StdlibSSLWrapper(tcpsock._sslObj, tcpsock._sock)
tcpsock._recv = wrapper.recv
tcpsock._send = wrapper.send
self.starttls='success'
def _ssl_verify_callback(self, sslconn, cert, errnum, depth, ok):
# Exceptions can't propagate up through this callback, so print them here.
try:
self._owner.Connection.ssl_fingerprint_sha1 = cert.digest('sha1')
if errnum == 0:
return True
self._owner.Connection.ssl_errnum = errnum
self._owner.Connection.ssl_cert_pem = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert)
return True
except:
log.error("Exception caught in _ssl_info_callback:", exc_info=True)
traceback.print_exc() # Make sure something is printed, even if log is disabled.
def StartTLSHandler(self, conn, starttls):
''' Handle server reply if TLS is allowed to process. Behaves accordingly.
Used internally.'''
if starttls.getNamespace() <> NS_TLS:
self.on_tls_failure('Unknown namespace: %s' % starttls.getNamespace())
return
self.starttls = starttls.getName()
if self.starttls == 'failure':
self.on_tls_failure('TLS <failure> received: %s' % self.starttls)
return
self.DEBUG('Got starttls proceed response. Switching to TLS/SSL...','ok')
try:
self._startSSL()
except Exception, e:
log.error("StartTLSHandler:", exc_info=True)
self.on_tls_failure('in StartTLSHandler')
#traceback.print_exc()
return
self._owner.Dispatcher.PlugOut()
self.on_tls_success()

View File

@ -14,8 +14,8 @@
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
import socket,select,base64,dispatcher_nb
import struct
import socket,base64
from simplexml import ustr
from client import PlugIn
from idlequeue import IdleObject
@ -27,7 +27,6 @@ import errno
import time
import traceback
import threading
import logging
log = logging.getLogger('gajim.c.x.transports_nb')
@ -52,21 +51,6 @@ def urisplit(self, uri):
grouped = re.match(regex, uri).groups()
proto, host, path = grouped[1], grouped[3], grouped[4]
return proto, host, path
# I don't need to load gajim.py just because of few TLS variables, so I changed
# %s/common\.gajim\.DATA_DIR/\'\.\.\/data\'/c
# %s/common\.gajim\.MY_CACERTS/\'\%s\/\.gajim\/cacerts\.pem\' \% os\.environ\[\'HOME\'\]/c
# To change it back do:
# %s/\'\.\.\/data\'/common\.gajim\.DATA_DIR/c
# %s/\'%s\/\.gajim\/cacerts\.pem\'\ %\ os\.environ\[\'HOME\'\]/common\.gajim\.MY_CACERTS/c
# TODO: make the paths configurable - as constructor parameters or sth
# import common.gajim
# timeout to connect to the server socket, it doesn't include auth
CONNECT_TIMEOUT_SECONDS = 30
@ -130,8 +114,8 @@ class NonBlockingTcp(PlugIn, IdleObject):
self.set_timeout, self.remove_timeout]
def plugin(self, owner):
owner.Connection=self
print 'plugin called'
owner.Connection=self
self.idlequeue = owner.idlequeue
def plugout(self):
@ -372,6 +356,7 @@ class NonBlockingTcp(PlugIn, IdleObject):
else:
self.on_receive = None
return
log.debug('setting onreceive on %s' % recv_handler)
self.on_receive = recv_handler
@ -406,484 +391,33 @@ class NonBlockingTcp(PlugIn, IdleObject):
# in case of some other exception
# FIXME: is this needed??
if errnum != 0:
self.DEBUG(self.DBG, errstr, 'error')
log.error("CConnection to %s lost: %s %s" % (self.server, errnum, errstr))
if not errors_only and self.state in [CONNECTING, CONNECTED]:
self.pollend(retry=True)
self.disconnect()
return
received = ''
# we have received some bytes, stop the timeout!
self.renew_send_timeout()
# pass received data to owner
#self.
if self.on_receive:
self._raise_event(DATA_RECEIVED, received)
self._on_receive(received)
else:
# This should never happen, so we need the debug
# This should never happen, so we need the debug. (If there is no handler
# on receive spacified, data are passed to Dispatcher.ProcessNonBlocking)
log.error('SOCKET Unhandled data received: %s' % received)
self.disconnect()
def _on_receive(self, data):
# Overriding this method allows modifying received data before it is passed
# to callback.
# to owner's callback.
log.debug('About to call on_receive which is %s' % self.on_receive)
self.on_receive(data)
class NonBlockingHttpBOSH(NonBlockingTcp):
'''
Socket wrapper that makes HTTP message out of send data and peels-off
HTTP headers from incoming messages
'''
def __init__(self, bosh_uri, bosh_port, on_disconnect):
self.bosh_protocol, self.bosh_host, self.bosh_path = self.urisplit(bosh_uri)
if self.bosh_protocol is None:
self.bosh_protocol = 'http'
if self.bosh_path == '':
bosh_path = '/'
self.bosh_port = bosh_port
def send(self, raw_data, now=False):
NonBlockingTcp.send(
self,
self.build_http_message(raw_data),
now)
def _on_receive(self,data):
'''Preceeds pass of received data to Client class. Gets rid of HTTP headers
and checks them.'''
statusline, headers, httpbody = self.parse_http_message(data)
if statusline[1] != '200':
log.error('HTTP Error: %s %s' % (statusline[1], statusline[2]))
self.disconnect()
self.on_receive(httpbody)
def build_http_message(self, httpbody):
'''
Builds bosh http message with given body.
Values for headers and status line fields are taken from class variables.
)
'''
headers = ['POST %s HTTP/1.1' % self.bosh_path,
'Host: %s:%s' % (self.bosh_host, self.bosh_port),
'Content-Type: text/xml; charset=utf-8',
'Content-Length: %s' % len(str(httpbody)),
'\r\n']
headers = '\r\n'.join(headers)
return('%s%s\r\n' % (headers, httpbody))
def parse_http_message(self, message):
'''
splits http message to tuple (
statusline - list of e.g. ['HTTP/1.1', '200', 'OK'],
headers - dictionary of headers e.g. {'Content-Length': '604',
'Content-Type': 'text/xml; charset=utf-8'},
httpbody - string with http body
)
'''
message = message.replace('\r','')
(header, httpbody) = message.split('\n\n')
header = header.split('\n')
statusline = header[0].split(' ')
header = header[1:]
headers = {}
for dummy in header:
row = dummy.split(' ',1)
headers[row[0][:-1]] = row[1]
return (statusline, headers, httpbody)
USE_PYOPENSSL = False
try:
#raise ImportError("Manually disabled PyOpenSSL")
import OpenSSL.SSL
import OpenSSL.crypto
USE_PYOPENSSL = True
log.info("PyOpenSSL loaded")
except ImportError:
log.debug("Import of PyOpenSSL failed:", exc_info=True)
# FIXME: Remove these prints before release, replace with a warning dialog.
print >> sys.stderr, "=" * 79
print >> sys.stderr, "PyOpenSSL not found, falling back to Python builtin SSL objects (insecure)."
print >> sys.stderr, "=" * 79
def torf(cond, tv, fv):
if cond: return tv
return fv
def gattr(obj, attr, default=None):
try:
return getattr(obj, attr)
except:
return default
class SSLWrapper:
class Error(IOError):
def __init__(self, sock=None, exc=None, errno=None, strerror=None, peer=None):
self.parent = IOError
errno = errno or gattr(exc, 'errno')
strerror = strerror or gattr(exc, 'strerror') or gattr(exc, 'args')
if not isinstance(strerror, basestring): strerror = repr(strerror)
self.sock = sock
self.exc = exc
self.peer = peer
self.exc_name = None
self.exc_args = None
self.exc_str = None
self.exc_repr = None
if self.exc is not None:
self.exc_name = str(self.exc.__class__)
self.exc_args = gattr(self.exc, 'args')
self.exc_str = str(self.exc)
self.exc_repr = repr(self.exc)
if not errno:
try:
if isinstance(exc, OpenSSL.SSL.SysCallError):
if self.exc_args[0] > 0:
errno = self.exc_args[0]
strerror = self.exc_args[1]
except: pass
self.parent.__init__(self, errno, strerror)
if self.peer is None and sock is not None:
try:
ppeer = self.sock.getpeername()
if len(ppeer) == 2 and isinstance(ppeer[0], basestring) \
and isinstance(ppeer[1], int):
self.peer = ppeer
except: pass
def __str__(self):
s = str(self.__class__)
if self.peer: s += " for %s:%d" % self.peer
if self.errno is not None: s += ": [Errno: %d]" % self.errno
if self.strerror: s += " (%s)" % self.strerror
if self.exc_name:
s += ", Caused by %s" % self.exc_name
if self.exc_str:
if self.strerror: s += "(%s)" % self.exc_str
else: s += "(%s)" % str(self.exc_args)
return s
def __init__(self, sslobj, sock=None):
self.sslobj = sslobj
self.sock = sock
log.debug("%s.__init__ called with %s", self.__class__, sslobj)
def recv(self, data, flags=None):
""" Receive wrapper for SSL object
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 by returning None. """
raise NotImplementedException()
def send(self, data, flags=None, now = False):
raise NotImplementedException()
class PyOpenSSLWrapper(SSLWrapper):
'''Wrapper class for PyOpenSSL's recv() and send() methods'''
def __init__(self, *args):
self.parent = SSLWrapper
self.parent.__init__(self, *args)
def is_numtoolarge(self, 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(self, bufsize, flags=None):
retval = None
try:
if flags is None: retval = self.sslobj.recv(bufsize)
else: retval = self.sslobj.recv(bufsize, flags)
except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e:
pass
# log.debug("Recv: " + repr(e))
except OpenSSL.SSL.SysCallError, e:
log.debug("Recv: Got OpenSSL.SSL.SysCallError: " + repr(e), exc_info=True)
#traceback.print_exc()
raise SSLWrapper.Error(self.sock or self.sslobj, e)
except OpenSSL.SSL.Error, e:
if self.is_numtoolarge(e):
# warn, but ignore this exception
log.warning("Recv: OpenSSL: asn1enc: first num too large (ignored)")
else:
log.debug("Recv: Caught OpenSSL.SSL.Error:", exc_info=True)
#traceback.print_exc()
#print "Current Stack:"
#traceback.print_stack()
raise SSLWrapper.Error(self.sock or self.sslobj, e)
return retval
def send(self, data, flags=None, now = False):
try:
if flags is None: return self.sslobj.send(data)
else: return self.sslobj.send(data, flags)
except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e:
#log.debug("Send: " + repr(e))
time.sleep(0.1) # prevent 100% CPU usage
except OpenSSL.SSL.SysCallError, e:
log.error("Send: Got OpenSSL.SSL.SysCallError: " + repr(e), exc_info=True)
#traceback.print_exc()
raise SSLWrapper.Error(self.sock or self.sslobj, e)
except OpenSSL.SSL.Error, e:
if self.is_numtoolarge(e):
# warn, but ignore this exception
log.warning("Send: OpenSSL: asn1enc: first num too large (ignored)")
else:
log.error("Send: Caught OpenSSL.SSL.Error:", exc_info=True)
#traceback.print_exc()
#print "Current Stack:"
#traceback.print_stack()
raise SSLWrapper.Error(self.sock or self.sslobj, e)
return 0
class StdlibSSLWrapper(SSLWrapper):
'''Wrapper class for Python's socket.ssl read() and write() methods'''
def __init__(self, *args):
self.parent = SSLWrapper
self.parent.__init__(self, *args)
def recv(self, bufsize, flags=None):
# we simply ignore flags since ssl object doesn't support it
try:
return self.sslobj.read(bufsize)
except socket.sslerror, e:
#log.debug("Recv: Caught socket.sslerror:", exc_info=True)
#traceback.print_exc()
if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE):
raise SSLWrapper.Error(self.sock or self.sslobj, e)
return None
def send(self, data, flags=None, now = False):
# we simply ignore flags since ssl object doesn't support it
try:
return self.sslobj.write(data)
except socket.sslerror, e:
#log.debug("Send: Caught socket.sslerror:", exc_info=True)
#traceback.print_exc()
if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE):
raise SSLWrapper.Error(self.sock or self.sslobj, e)
return 0
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, on_tls_success, on_tls_failure, now=0):
''' If the 'now' argument is true then starts using encryption immidiatedly.
If 'now' in false then starts encryption as soon as TLS feature is
declared by the server (if it were already declared - it is ok).
'''
if owner.__dict__.has_key('NonBlockingTLS'):
return # Already enabled.
PlugIn.PlugIn(self, owner)
DBG_LINE='NonBlockingTLS'
self.on_tls_success = on_tls_success
self.on_tls_faliure = on_tls_failure
if now:
try:
res = self._startSSL()
except Exception, e:
log.error("PlugIn: while trying _startSSL():", exc_info=True)
#traceback.print_exc()
self._owner.socket.pollend()
return
on_tls_success()
return res
if self._owner.Dispatcher.Stream.features:
try:
self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
except NodeProcessed:
pass
else:
self._owner.RegisterHandlerOnce('features',self.FeaturesHandler, xmlns=NS_STREAMS)
self.starttls = None
def plugout(self,now=0):
''' Unregisters TLS handler's from owner's dispatcher. Take note that encription
can not be stopped once started. You can only break the connection and start over.'''
# if dispatcher is not plugged we cannot (un)register handlers
if self._owner.__dict__.has_key('Dispatcher'):
self._owner.UnregisterHandler('features', self.FeaturesHandler,xmlns=NS_STREAMS)
self._owner.Dispatcher.PlugOut()
self._owner = None
def FeaturesHandler(self, conn, feats):
''' Used to analyse server <features/> tag for TLS support.
If TLS is supported starts the encryption negotiation. Used internally '''
if not feats.getTag('starttls', namespace=NS_TLS):
self.DEBUG("TLS unsupported by remote server.", 'warn')
self.on_tls_failure("TLS unsupported by remote server.")
return
self.DEBUG("TLS supported by remote server. Requesting TLS start.", 'ok')
self._owner.RegisterHandlerOnce('proceed', self.StartTLSHandler, xmlns=NS_TLS)
self._owner.RegisterHandlerOnce('failure', self.StartTLSHandler, xmlns=NS_TLS)
self._owner.send('<starttls xmlns="%s"/>' % NS_TLS)
raise NodeProcessed
def _dumpX509(self, cert, stream=sys.stderr):
print >> stream, "Digest (SHA-1):", cert.digest("sha1")
print >> stream, "Digest (MD5):", cert.digest("md5")
print >> stream, "Serial #:", cert.get_serial_number()
print >> stream, "Version:", cert.get_version()
print >> stream, "Expired:", torf(cert.has_expired(), "Yes", "No")
print >> stream, "Subject:"
self._dumpX509Name(cert.get_subject(), stream)
print >> stream, "Issuer:"
self._dumpX509Name(cert.get_issuer(), stream)
self._dumpPKey(cert.get_pubkey(), stream)
def _dumpX509Name(self, name, stream=sys.stderr):
print >> stream, "X509Name:", str(name)
def _dumpPKey(self, pkey, stream=sys.stderr):
typedict = {OpenSSL.crypto.TYPE_RSA: "RSA", OpenSSL.crypto.TYPE_DSA: "DSA"}
print >> stream, "PKey bits:", pkey.bits()
print >> stream, "PKey type: %s (%d)" % (typedict.get(pkey.type(), "Unknown"), pkey.type())
def _startSSL(self):
''' Immidiatedly switch socket to TLS mode. Used internally.'''
log.debug("_startSSL called")
if USE_PYOPENSSL: return self._startSSL_pyOpenSSL()
return self._startSSL_stdlib()
def _startSSL_pyOpenSSL(self):
#log.debug("_startSSL_pyOpenSSL called, thread id: %s", str(thread.get_ident()))
log.debug("_startSSL_pyOpenSSL called")
tcpsock = self._owner.Connection
# FIXME: should method be configurable?
tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
#tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
tcpsock.ssl_errnum = 0
tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER, self._ssl_verify_callback)
cacerts = os.path.join('../data', 'other', 'cacerts.pem')
try:
tcpsock._sslContext.load_verify_locations(cacerts)
except:
log.warning('Unable to load SSL certificats from file %s' % \
os.path.abspath(cacerts))
# load users certs
if os.path.isfile('%s/.gajim/cacerts.pem' % os.environ['HOME']):
store = tcpsock._sslContext.get_cert_store()
f = open('%s/.gajim/cacerts.pem' % os.environ['HOME'])
lines = f.readlines()
i = 0
begin = -1
for line in lines:
if 'BEGIN CERTIFICATE' in line:
begin = i
elif 'END CERTIFICATE' in line and begin > -1:
cert = ''.join(lines[begin:i+2])
try:
X509cert = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert)
store.add_cert(X509cert)
except OpenSSL.crypto.Error, exception_obj:
log.warning('Unable to load a certificate from file %s: %s' %\
('%s/.gajim/cacerts.pem' % os.environ['HOME'], exception_obj.args[0][0][2]))
except:
log.warning(
'Unknown error while loading certificate from file %s' % \
'%s/.gajim/cacerts.pem' % os.environ['HOME'])
begin = -1
i += 1
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
log.debug("Initiating handshake...")
# FIXME: Figure out why _connect_success is called before the
# SSL handshake is completed in STARTTLS mode. See #2838.
tcpsock._sslObj.setblocking(True)
try:
self.starttls='in progress'
tcpsock._sslObj.do_handshake()
except:
log.error('Error while TLS handshake: ', exc_info=True)
self.on_tls_failure('Error while TLS Handshake')
return
tcpsock._sslObj.setblocking(False)
log.debug("Synchronous handshake completed")
#log.debug("Async handshake started...")
# fake it, for now
self.starttls='success'
def _startSSL_stdlib(self):
log.debug("_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()
wrapper = StdlibSSLWrapper(tcpsock._sslObj, tcpsock._sock)
tcpsock._recv = wrapper.recv
tcpsock._send = wrapper.send
self.starttls='success'
def _ssl_verify_callback(self, sslconn, cert, errnum, depth, ok):
# Exceptions can't propagate up through this callback, so print them here.
try:
self._owner.Connection.ssl_fingerprint_sha1 = cert.digest('sha1')
if errnum == 0:
return True
self._owner.Connection.ssl_errnum = errnum
self._owner.Connection.ssl_cert_pem = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert)
return True
except:
log.error("Exception caught in _ssl_info_callback:", exc_info=True)
traceback.print_exc() # Make sure something is printed, even if log is disabled.
def StartTLSHandler(self, conn, starttls):
''' Handle server reply if TLS is allowed to process. Behaves accordingly.
Used internally.'''
if starttls.getNamespace() <> NS_TLS:
self.on_tls_failure('Unknown namespace: %s' % starttls.getNamespace())
return
self.starttls = starttls.getName()
if self.starttls == 'failure':
self.on_tls_failure('TLS <failure> received: %s' % self.starttls)
return
self.DEBUG('Got starttls proceed response. Switching to TLS/SSL...','ok')
try:
self._startSSL()
except Exception, e:
log.error("StartTLSHandler:", exc_info=True)
self.on_tls_failure('in StartTLSHandler')
#traceback.print_exc()
return
self._owner.Dispatcher.PlugOut()
self.on_tls_success()
#dispatcher_nb.Dispatcher().PlugIn(self._owner)
class NBProxySocket(NonBlockingTcp):
'''
@ -957,8 +491,10 @@ class NBHTTPProxySocket(NBProxySocket):
return
if len(reply) != 2:
pass
self.onreceive(self._on_proxy_auth)
self.after_proxy_connect()
#self.onreceive(self._on_proxy_auth)
# FIXME: find out what it this method for
def _on_proxy_auth(self, reply):
if self.reply.find('\n\n') == -1:
if reply is None:
@ -1109,3 +645,70 @@ class NBSOCKS5ProxySocket(NBProxySocket):
def DEBUG(self, text, severity):
''' Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy".'''
return self._owner.DEBUG(DBG_CONNECT_PROXY, text, severity)
class NonBlockingHttpBOSH(NonBlockingTcp):
'''
Socket wrapper that makes HTTP message out of send data and peels-off
HTTP headers from incoming messages
'''
def __init__(self, bosh_uri, bosh_port, on_disconnect):
self.bosh_protocol, self.bosh_host, self.bosh_path = self.urisplit(bosh_uri)
if self.bosh_protocol is None:
self.bosh_protocol = 'http'
if self.bosh_path == '':
bosh_path = '/'
self.bosh_port = bosh_port
def send(self, raw_data, now=False):
NonBlockingTcp.send(
self,
self.build_http_message(raw_data),
now)
def _on_receive(self,data):
'''Preceeds passing received data to Client class. Gets rid of HTTP headers
and checks them.'''
statusline, headers, httpbody = self.parse_http_message(data)
if statusline[1] != '200':
log.error('HTTP Error: %s %s' % (statusline[1], statusline[2]))
self.disconnect()
self.on_receive(httpbody)
def build_http_message(self, httpbody):
'''
Builds bosh http message with given body.
Values for headers and status line fields are taken from class variables.
)
'''
headers = ['POST %s HTTP/1.1' % self.bosh_path,
'Host: %s:%s' % (self.bosh_host, self.bosh_port),
'Content-Type: text/xml; charset=utf-8',
'Content-Length: %s' % len(str(httpbody)),
'\r\n']
headers = '\r\n'.join(headers)
return('%s%s\r\n' % (headers, httpbody))
def parse_http_message(self, message):
'''
splits http message to tuple (
statusline - list of e.g. ['HTTP/1.1', '200', 'OK'],
headers - dictionary of headers e.g. {'Content-Length': '604',
'Content-Type': 'text/xml; charset=utf-8'},
httpbody - string with http body
)
'''
message = message.replace('\r','')
(header, httpbody) = message.split('\n\n')
header = header.split('\n')
statusline = header[0].split(' ')
header = header[1:]
headers = {}
for dummy in header:
row = dummy.split(' ',1)
headers[row[0][:-1]] = row[1]
return (statusline, headers, httpbody)