added prototype of BOSHClient class and script for usage example, removed import of common.gajim from transports_nb
This commit is contained in:
parent
794a5f33d5
commit
cb2d629535
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -33,7 +33,16 @@ import thread
|
||||||
import logging
|
import logging
|
||||||
log = logging.getLogger('gajim.c.x.transports_nb')
|
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
|
USE_PYOPENSSL = False
|
||||||
|
|
||||||
|
@ -762,16 +771,16 @@ class NonBlockingTLS(PlugIn):
|
||||||
#tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
|
#tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
|
||||||
tcpsock.ssl_errnum = 0
|
tcpsock.ssl_errnum = 0
|
||||||
tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER, self._ssl_verify_callback)
|
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:
|
try:
|
||||||
tcpsock._sslContext.load_verify_locations(cacerts)
|
tcpsock._sslContext.load_verify_locations(cacerts)
|
||||||
except:
|
except:
|
||||||
log.warning('Unable to load SSL certificats from file %s' % \
|
log.warning('Unable to load SSL certificats from file %s' % \
|
||||||
os.path.abspath(cacerts))
|
os.path.abspath(cacerts))
|
||||||
# load users certs
|
# 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()
|
store = tcpsock._sslContext.get_cert_store()
|
||||||
f = open(common.gajim.MY_CACERTS)
|
f = open('%s/.gajim/cacerts.pem' % os.environ['HOME'])
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
i = 0
|
i = 0
|
||||||
begin = -1
|
begin = -1
|
||||||
|
@ -786,11 +795,11 @@ class NonBlockingTLS(PlugIn):
|
||||||
store.add_cert(X509cert)
|
store.add_cert(X509cert)
|
||||||
except OpenSSL.crypto.Error, exception_obj:
|
except OpenSSL.crypto.Error, exception_obj:
|
||||||
log.warning('Unable to load a certificate from file %s: %s' %\
|
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:
|
except:
|
||||||
log.warning(
|
log.warning(
|
||||||
'Unknown error while loading certificate from file %s' % \
|
'Unknown error while loading certificate from file %s' % \
|
||||||
common.gajim.MY_CACERTS)
|
'%s/.gajim/cacerts.pem' % os.environ['HOME'])
|
||||||
begin = -1
|
begin = -1
|
||||||
i += 1
|
i += 1
|
||||||
tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext, tcpsock._sock)
|
tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext, tcpsock._sock)
|
||||||
|
|
Loading…
Reference in New Issue