added prototype of BOSHClient class and script for usage example, removed import of common.gajim from transports_nb

This commit is contained in:
tomk 2008-05-31 16:51:40 +00:00
parent 794a5f33d5
commit cb2d629535
3 changed files with 298 additions and 6 deletions

View File

@ -0,0 +1,194 @@
## client_bosh.py
##
## Copyright (C) 2008 Tomas Karasek <tom.to.the.k@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 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 <body> 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 <stream..> 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))

View File

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

View File

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