From 937bb01a6908570533dfc28deda7c8e483526f59 Mon Sep 17 00:00:00 2001 From: tomk Date: Mon, 30 Jun 2008 23:02:12 +0000 Subject: [PATCH] moved TLS and SSL classes from transports_nb to new tls_nb module, fixed HTTP CONNECT proxy transport --- src/common/xmpp/client.py | 4 +- src/common/xmpp/client_nb.py | 13 +- src/common/xmpp/tls_nb.py | 437 ++++++++++++++++++++++++ src/common/xmpp/transports_nb.py | 561 +++++-------------------------- 4 files changed, 529 insertions(+), 486 deletions(-) create mode 100644 src/common/xmpp/tls_nb.py diff --git a/src/common/xmpp/client.py b/src/common/xmpp/client.py index aba778780..201da2a78 100644 --- a/src/common/xmpp/client.py +++ b/src/common/xmpp/client.py @@ -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. """ diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index f362d02a9..308300151 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -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) diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py new file mode 100644 index 000000000..e0b975de9 --- /dev/null +++ b/src/common/xmpp/tls_nb.py @@ -0,0 +1,437 @@ +## tls_nb.py +## based on transports_nb.py +## +## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov +## modified by Dimitur Kirov +## +## 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 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('' % 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 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() diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 4ca3c51aa..c69f33be8 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -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 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('' % 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 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)