From cb2d6295358b2dc1e47238599727ec8d41a86af3 Mon Sep 17 00:00:00 2001 From: tomk Date: Sat, 31 May 2008 16:51:40 +0000 Subject: [PATCH] added prototype of BOSHClient class and script for usage example, removed import of common.gajim from transports_nb --- src/common/xmpp/client_bosh.py | 194 ++++++++++++++++++++ src/common/xmpp/examples/run_client_bosh.py | 89 +++++++++ src/common/xmpp/transports_nb.py | 21 ++- 3 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 src/common/xmpp/client_bosh.py create mode 100644 src/common/xmpp/examples/run_client_bosh.py diff --git a/src/common/xmpp/client_bosh.py b/src/common/xmpp/client_bosh.py new file mode 100644 index 000000000..c2003b375 --- /dev/null +++ b/src/common/xmpp/client_bosh.py @@ -0,0 +1,194 @@ +## client_bosh.py +## +## Copyright (C) 2008 Tomas Karasek +## +## 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 locale, random +import protocol +import simplexml +import debug +import dispatcher_nb +from client_nb import NBCommonClient + +DBG_BOSHCLIENT='boshclient' + +class BOSHClient(NBCommonClient): + ''' + BOSH (XMPP over HTTP) client implementation. It should provide the same + methods and functionality as NonBlockingClient. + ''' + + + def __init__(self, server, bosh_conn_mgr, port=5222, bosh_port=5280, + on_connect=None, on_connect_failure=None, on_proxy_failure=None, caller=None): + ''' + Class constuctor has the same parameters as NBCommonClient plus bosh_conn_mgr + and bosh_port - Connection manager address and port. bosh_conn_mgr should be + in form: 'http://httpcm.jabber.org/http-bind/' + Tcp connection will be opened to bosh_conn_mgr:bosh_port instead of + server:port. + ''' + self.bosh_protocol, self.bosh_host, self.bosh_uri = self.urisplit(bosh_conn_mgr) + if self.bosh_protocol is None: + self.bosh_protocol = 'http' + + self.bosh_port = bosh_port + + if self.bosh_uri == '': + bosh_uri = '/' + + self.xmpp_server = server + self.xmpp_port = port + + self.bosh_hold = 1 + self.bosh_wait=60 + self.bosh_rid=-1 + self.bosh_httpversion = 'HTTP/1.1' + + NBCommonClient.__init__(self, self.bosh_host, self.bosh_port, caller=caller, + on_connect=on_connect, on_connect_failure=on_connect_failure, + on_proxy_failure=on_proxy_failure) + + # Namespace and DBG are detected in NBCommonClient constructor + # with isinstance(). Since BOSHClient is descendant of NBCommonClient + # and client_bosh.py is NOT imported in client_nb.py, NB_COMPONENT_ACCEPT + # is put to namespace. This is not very nice, thus: + # TODO: refactor Namespace and DBG recognition in NBCommonClient or + # derived classes + self.Namespace, self.DBG = protocol.NS_HTTP_BIND, DBG_BOSHCLIENT + # pop of DBG_COMPONENT + self.debug_flags.pop() + self.debug_flags.append(self.DBG) + self.debug_flags.append(simplexml.DBG_NODEBUILDER) + + + def urisplit(self, uri): + ''' + Function for splitting URI string to tuple (protocol, host, path). + e.g. urisplit('http://httpcm.jabber.org/webclient') returns + ('http', 'httpcm.jabber.org', '/webclient') + ''' + import re + regex = '(([^:/]+)(://))?([^/]*)(/?.*)' + grouped = re.match(regex, uri).groups() + proto, host, path = grouped[1], grouped[3], grouped[4] + return proto, host, path + + + def _on_connected(self): + ''' + method called after socket starts connecting from NonBlockingTcp._do_connect + ''' + self.onreceive(self.on_bosh_session_init_response) + dispatcher_nb.Dispatcher().PlugIn(self) + + + 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) + + + def on_bosh_session_init_response(self, data): + ''' + Called on init response - should check relevant attributes from body tag + ''' + if data: + statusline, headers, httpbody = self.parse_http_message(data) + + if statusline[1] != '200': + self.DEBUG(self.DBG, "HTTP Error in received session init response: %s" + % statusline, 'error') + # error handling TBD! + + # ATM, whole tag is pass to ProcessNonBocking. + # Question is how to peel off the body tag from incoming stanzas and make + # use of ordinar xmpp traffic handling. + self.Dispatcher.ProcessNonBlocking(httpbody) + + + def _check_stream_start(self, ns, tag, attrs): + ''' + callback stub called from XML Parser when is discovered + ''' + self.DEBUG(self.DBG, 'CHECK_STREAM_START: ns: %s, tag: %s, attrs: %s' + % (ns, tag, attrs), 'info') + + + def StreamInit(self): + ''' + Initiation of BOSH session. Called instead of Dispatcher.StreamInit() + Initial body tag is created and sent to Conn Manager. + ''' + self.Dispatcher.Stream = simplexml.NodeBuilder() + self.Dispatcher.Stream._dispatch_depth = 2 + self.Dispatcher.Stream.dispatch = self.Dispatcher.dispatch + self.Dispatcher.Stream.stream_header_received = self._check_stream_start + self.debug_flags.append(simplexml.DBG_NODEBUILDER) + self.Dispatcher.Stream.DEBUG = self.DEBUG + self.Dispatcher.Stream.features = None + + initial_body_tag = simplexml.Node('body') + initial_body_tag.setNamespace(self.Namespace) + initial_body_tag.setAttr('content', 'text/xml; charset=utf-8') + initial_body_tag.setAttr('hold', str(self.bosh_hold)) + initial_body_tag.setAttr('to', self.xmpp_server) + initial_body_tag.setAttr('wait', str(self.bosh_wait)) + + r = random.Random() + r.seed() + # with 50-bit random initial rid, session would have to go up + # to 7881299347898368 messages to raise rid over 2**53 + # (see http://www.xmpp.org/extensions/xep-0124.html#rids) + self.bosh_rid = r.getrandbits(50) + initial_body_tag.setAttr('rid', str(self.bosh_rid)) + + if locale.getdefaultlocale()[0]: + initial_body_tag.setAttr('xml:lang', + locale.getdefaultlocale()[0].split('_')[0]) + initial_body_tag.setAttr('xmpp:version', '1.0') + initial_body_tag.setAttr('xmlns:xmpp', 'urn:xmpp:xbosh') + + self.send(self.build_bosh_message(initial_body_tag)) + + + def build_bosh_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_uri, + 'Host: %s' % self.bosh_host, + '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)) + + + diff --git a/src/common/xmpp/examples/run_client_bosh.py b/src/common/xmpp/examples/run_client_bosh.py new file mode 100644 index 000000000..3ec215a25 --- /dev/null +++ b/src/common/xmpp/examples/run_client_bosh.py @@ -0,0 +1,89 @@ +# Example script for usage of BOSHClient class +# -------------------------------------------- +# run `python run_client_bosh.py` in gajim/src/common/xmpp/examples/ directory +# and quit with CTRL + c. +# Script will open TCP connection to Connection Manager, send BOSH initial +# request and receive initial response. Handling of init response is not +# done yet. + + +# imports gtk because of gobject.timeout_add() which is used for processing +# idlequeue +# TODO: rewrite to thread timer +import gtk +import gobject +import sys, os.path + +xmpppy_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') +sys.path.append(xmpppy_dir) + +import idlequeue +from client_bosh import BOSHClient + + +class DummyConnection(): + ''' + Dummy class for test run of bosh_client. I use it because in Connection class + the gajim.py module is imported and stuff from it is read in there, so it + would be difficult to debug IMHO. On the other hand, I will have to test with + Connection class sooner or later somehow because that's the main place where + BOSHClient should be used. + DummyConnection class holds and processes IdleQueue for BOSHClient. + ''' + + def __init__(self, iq_interval_ms=1000): + self.classname = self.__class__.__name__ + self.iq_interval_ms = iq_interval_ms + self.idlequeue = idlequeue.IdleQueue() + self.timer=gobject.timeout_add(iq_interval_ms, self.process_idlequeue) + + + def process_idlequeue(self): + ''' + called each iq_interval_ms miliseconds. Checks for idlequeue timeouts. + ''' + self.idlequeue.process() + return True + + # callback stubs follows + def _event_dispatcher(self, realm, event, data): + print "\n>>> %s._event_dispatcher called:" % self.classname + print ">>> realm: %s, event: %s, data: %s\n" % (realm, event, data) + + + def onconsucc(self): + print '%s: CONNECTION SUCCEEDED' % self.classname + + + def onconfail(self, retry=None): + print '%s: CONNECTION FAILED.. retry?: %s' % (self.classname, retry) + + +if __name__ == "__main__": + dc = DummyConnection() + + # you can use my instalation of ejabberd2: + server = 'star.securitynet.cz' + bosh_conn_mgr = 'http://star.securitynet.cz/http-bind/' + + #server='jabbim.cz' + #bosh_conn_mgr='http://bind.jabbim.cz/' + + bc = BOSHClient( + server = server, + bosh_conn_mgr = bosh_conn_mgr, + bosh_port = 80, + on_connect = dc.onconsucc, + on_connect_failure = dc.onconfail, + caller = dc + ) + + bc.set_idlequeue(dc.idlequeue) + + bc.connect() + + try: + gtk.main() + except KeyboardInterrupt: + dc.process_idlequeue() + diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index f7ab85a56..2c3364d45 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -33,7 +33,16 @@ import thread import logging log = logging.getLogger('gajim.c.x.transports_nb') -import common.gajim +# 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 + +# import common.gajim + USE_PYOPENSSL = False @@ -762,16 +771,16 @@ class NonBlockingTLS(PlugIn): #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(common.gajim.DATA_DIR, 'other', 'cacerts.pem') + 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(common.gajim.MY_CACERTS): + if os.path.isfile('%s/.gajim/cacerts.pem' % os.environ['HOME']): store = tcpsock._sslContext.get_cert_store() - f = open(common.gajim.MY_CACERTS) + f = open('%s/.gajim/cacerts.pem' % os.environ['HOME']) lines = f.readlines() i = 0 begin = -1 @@ -786,11 +795,11 @@ class NonBlockingTLS(PlugIn): store.add_cert(X509cert) except OpenSSL.crypto.Error, exception_obj: log.warning('Unable to load a certificate from file %s: %s' %\ - (common.gajim.MY_CACERTS, exception_obj.args[0][0][2])) + ('%s/.gajim/cacerts.pem' % os.environ['HOME'], exception_obj.args[0][0][2])) except: log.warning( 'Unknown error while loading certificate from file %s' % \ - common.gajim.MY_CACERTS) + '%s/.gajim/cacerts.pem' % os.environ['HOME']) begin = -1 i += 1 tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext, tcpsock._sock)