diff options
301 files changed, 14655 insertions, 4484 deletions
@@ -1,4 +1,4 @@ -*.pyc +*.py[co] build/ dist/ MANIFEST @@ -8,3 +8,7 @@ docs/_build/ .coverage sleekxmpp.egg-info/ .ropeproject/ +4913 +*~ +.baboon/ +.DS_STORE diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..22e3abf1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python +python: + - "2.6" + - "2.7" + - "3.2" + - "3.3" + - "3.4" +install: + - "pip install ." +script: testall.py @@ -119,7 +119,7 @@ SUELTA – A PURE-PYTHON SASL CLIENT LIBRARY This software is subject to "The MIT License" -Copyright 2007-2010 David Alan Cridland +Copyright 2004-2013 David Alan Cridland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -3,7 +3,7 @@ SleekXMPP SleekXMPP is an MIT licensed XMPP library for Python 2.6/3.1+, and is featured in examples in -`XMPP: The Definitive Guide <http://oreilly.com/catalog/9780596521271>`_ +`XMPP: The Definitive Guide <http://oreilly.com/catalog/9780596521271>`_ by Kevin Smith, Remko Tronçon, and Peter Saint-Andre. If you've arrived here from reading the Definitive Guide, please see the notes on updating the examples to the latest version of SleekXMPP. @@ -45,16 +45,14 @@ The latest source code for SleekXMPP may be found on `Github ``develop`` branch. **Latest Release** - - `1.1.8 <http://github.com/fritzy/SleekXMPP/zipball/1.1.8>`_ + - `1.3.1 <http://github.com/fritzy/SleekXMPP/zipball/1.3.1>`_ **Develop Releases** - `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_ -**Older Stable Releases** - - `1.0 <http://github.com/fritzy/SleekXMPP/zipball/1.0>`_ Installing DNSPython ---------------------- +-------------------- If you are using Python3 and wish to use dnspython, you will have to checkout and install the ``python3`` branch:: @@ -74,6 +72,7 @@ help with SleekXMPP. **Chat** `sleek@conference.jabber.org <xmpp:sleek@conference.jabber.org?join>`_ + Documentation and Testing ------------------------- Documentation can be found both inline in the code, and as a Sphinx project in ``/docs``. @@ -145,7 +144,7 @@ SleekXMPP projects:: if __name__ == '__main__': - # Ideally use optparse or argparse to get JID, + # Ideally use optparse or argparse to get JID, # password, and log level. logging.basicConfig(level=logging.DEBUG, @@ -159,15 +158,15 @@ SleekXMPP projects:: Credits ------- **Main Author:** Nathan Fritz - `fritzy@netflint.net <xmpp:fritzy@netflint.net?message>`_, + `fritzy@netflint.net <xmpp:fritzy@netflint.net?message>`_, `@fritzy <http://twitter.com/fritzy>`_ Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP - <http://code.google.com/p/seesmic-as3-xmpp/>`_, and a former member of + <http://code.google.com/p/seesmic-as3-xmpp/>`_, and a former member of the XMPP Council. **Co-Author:** Lance Stout - `lancestout@gmail.com <xmpp:lancestout@gmail.com?message>`_, + `lancestout@gmail.com <xmpp:lancestout@gmail.com?message>`_, `@lancestout <http://twitter.com/lancestout>`_ **Contributors:** diff --git a/docs/create_plugin.rst b/docs/create_plugin.rst index 12efa84c..2b0514b8 100644 --- a/docs/create_plugin.rst +++ b/docs/create_plugin.rst @@ -222,7 +222,7 @@ handler function to process registration requests. self.description = "In-Band Registration" self.xep = "0077" - self.xmpp.registerHandler( + self.xmpp.register_handler( Callback('In-Band Registration', MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns), self.__handleRegistration)) @@ -601,7 +601,7 @@ with some additional registration fields implemented. self.form_instructions = "" self.backend = UserStore() - self.xmpp.registerHandler( + self.xmpp.register_handler( Callback('In-Band Registration', MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns), self.__handleRegistration)) diff --git a/docs/event_index.rst b/docs/event_index.rst index 2c5dfd39..ee8f5a95 100644 --- a/docs/event_index.rst +++ b/docs/event_index.rst @@ -6,14 +6,20 @@ Event Index connected - **Data:** ``{}`` - - **Source:** :py:class:`~sleekxmpp.clientxmpp.ClientXMPP` + - **Source:** :py:class:`~sleekxmpp.xmlstream.XMLstream` Signal that a connection has been made with the XMPP server, but a session has not yet been established. + connection_failed + - **Data:** ``{}`` or ``Failure Stanza`` if available + - **Source:** :py:class:`~sleekxmpp.xmlstream.XMLstream` + + Signal that a connection can not be established after number of attempts. + changed_status - **Data:** :py:class:`~sleekxmpp.Presence` - - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + - **Source:** :py:class:`~sleekxmpp.roster.item.RosterItem` Triggered when a presence stanza is received from a JID with a show type different than the last presence stanza from the same JID. @@ -65,8 +71,8 @@ Event Index disconnected - **Data:** ``{}`` - - **Source:** :py:class:`~sleekxmpp.ClientXMPP` - + - **Source:** :py:class:`~sleekxmpp.xmlstream.XMLstream` + Signal that the connection with the XMPP server has been lost. entity_time @@ -93,16 +99,16 @@ Event Index got_online - **Data:** :py:class:`~sleekxmpp.Presence` - - **Source:** :py:class:`~sleekxmpp.BaseXMPP` - + - **Source:** :py:class:`~sleekxmpp.roster.item.RosterItem` + If a presence stanza is received from a JID which was previously marked as offline, and the presence has a show type of '``chat``', '``dnd``', '``away``', or '``xa``', then this event is triggered as well. got_offline - **Data:** :py:class:`~sleekxmpp.Presence` - - **Source:** :py:class:`~sleekxmpp.BaseXMPP` - + - **Source:** :py:class:`~sleekxmpp.roster.item.RosterItem` + Signal that an unavailable presence stanza has been received from a JID. groupchat_invite @@ -110,7 +116,7 @@ Event Index - **Source:** groupchat_direct_invite - - **Data:** :py:class:`~sleekxmpp.Message` + - **Data:** :py:class:`~sleekxmpp.Message` - **Source:** :py:class:`~sleekxmpp.plugins.xep_0249.direct` groupchat_message @@ -147,18 +153,18 @@ Event Index sure to check the message type in order to handle error messages. message_form - - **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form` - - **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004` - + - **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004` + Currently the same as :term:`message_xform`. message_xform - - **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form` - - **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004` - + - **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004` + Triggered whenever a data form is received inside a message. - mucc::[room]::got_offline + muc::[room]::got_offline - **Data:** - **Source:** @@ -187,8 +193,8 @@ Event Index A presence stanza with a type of '``error``' is received. presence_form - - **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form` - - **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004` + - **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004` This event is present in the XEP-0004 plugin code, but is currently not used. @@ -229,22 +235,20 @@ Event Index A presence stanza with a type of '``unsubscribed``' is received. roster_update - - **Data:** :py:class:`~sleekxmpp.stanza.Roster` - - **Source:** :py:class:`~sleekxmpp.ClientXMPP` + - **Data:** :py:class:`~sleekxmpp.stanza.Roster` + - **Source:** :py:class:`~sleekxmpp.ClientXMPP` An IQ result containing roster entries is received. sent_presence - **Data:** ``{}`` - - **Source:** :py:class:`BaseXMPP <sleekxmpp.BaseXMPP>` + - **Source:** :py:class:`~sleekxmpp.roster.multi.Roster` Signal that an initial presence stanza has been written to the XML stream. session_end - **Data:** ``{}`` - - **Source:** :py:class:`ClientXMPP <sleekxmpp.ClientXMPP>`, - :py:class:`ComponentXMPP <sleekxmpp.ComponentXMPP>` - :py:class:`XEP-0078 <sleekxmpp.plugins.xep_0078>` + - **Source:** :py:class:`~sleekxmpp.xmlstream.XMLstream` Signal that a connection to the XMPP server has been lost and the current stream session has ended. Currently equivalent to :term:`disconnected`, but @@ -256,14 +260,14 @@ Event Index session_start - **Data:** ``{}`` - - **Source:** :py:class:`ClientXMPP <sleekxmpp.ClientXMPP>`, + - **Source:** :py:class:`ClientXMPP <sleekxmpp.ClientXMPP>`, :py:class:`ComponentXMPP <sleekxmpp.ComponentXMPP>` :py:class:`XEP-0078 <sleekxmpp.plugins.xep_0078>` Signal that a connection to the XMPP server has been made and a session has been established. socket_error - - **Data:** ``Socket`` exception object + - **Data:** ``Socket`` exception object - **Source:** :py:class:`~sleekxmpp.xmlstream.XMLstream` stream_error diff --git a/docs/getting_started/echobot.rst b/docs/getting_started/echobot.rst index 053a76f2..7d29ec58 100644 --- a/docs/getting_started/echobot.rst +++ b/docs/getting_started/echobot.rst @@ -69,8 +69,8 @@ use ASCII. We can get Python to use UTF-8 as the default encoding by including: .. code-block:: python if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') .. warning:: diff --git a/examples/IoT_TestDevice.py b/examples/IoT_TestDevice.py new file mode 100755 index 00000000..cd80cee2 --- /dev/null +++ b/examples/IoT_TestDevice.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import os +import sys +# This can be used when you are in a test environment and need to make paths right +sys.path=['/Users/jocke/Dropbox/06_dev/SleekXMPP']+sys.path + +import logging +import unittest +import distutils.core +import datetime + +from glob import glob +from os.path import splitext, basename, join as pjoin +from optparse import OptionParser +from urllib import urlopen + +import sleekxmpp +# Python versions before 3.0 do not use UTF-8 encoding +# by default. To ensure that Unicode is handled properly +# throughout SleekXMPP, we will set the default encoding +# ourselves to UTF-8. +if sys.version_info < (3, 0): + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') +else: + raw_input = input + +from sleekxmpp.plugins.xep_0323.device import Device + +#from sleekxmpp.exceptions import IqError, IqTimeout + +class IoT_TestDevice(sleekxmpp.ClientXMPP): + + """ + A simple IoT device that can act as server or client + """ + def __init__(self, jid, password): + sleekxmpp.ClientXMPP.__init__(self, jid, password) + self.add_event_handler("session_start", self.session_start) + self.add_event_handler("message", self.message) + self.device=None + self.releaseMe=False + self.beServer=True + self.clientJID=None + + def datacallback(self,from_jid,result,nodeId=None,timestamp=None,fields=None,error_msg=None): + """ + This method will be called when you ask another IoT device for data with the xep_0323 + se script below for the registration of the callback + """ + logging.debug("we got data %s from %s",str(result),from_jid) + + def beClientOrServer(self,server=True,clientJID=None ): + if server: + self.beServer=True + self.clientJID=None + else: + self.beServer=False + self.clientJID=clientJID + + def testForRelease(self): + # todo thread safe + return self.releaseMe + + def doReleaseMe(self): + # todo thread safe + self.releaseMe=True + + def addDevice(self, device): + self.device=device + + def session_start(self, event): + self.send_presence() + self.get_roster() + # tell your preffered friend that you are alive + self.send_message(mto='jocke@jabber.sust.se', mbody=self.boundjid.bare +' is now online use xep_323 stanza to talk to me') + + if not(self.beServer): + session=self['xep_0323'].request_data(self.boundjid.full,self.clientJID,self.datacallback) + + def message(self, msg): + if msg['type'] in ('chat', 'normal'): + logging.debug("got normal chat message" + str(msg)) + ip=urlopen('http://icanhazip.com').read() + msg.reply("Hi I am " + self.boundjid.full + " and I am on IP " + ip).send() + else: + logging.debug("got unknown message type %s", str(msg['type'])) + +class TheDevice(Device): + """ + This is the actual device object that you will use to get information from your real hardware + You will be called in the refresh method when someone is requesting information from you + """ + def __init__(self,nodeId): + Device.__init__(self,nodeId) + self.counter=0 + + def refresh(self,fields): + """ + the implementation of the refresh method + """ + self._set_momentary_timestamp(self._get_timestamp()) + self.counter+=self.counter + self._add_field_momentary_data(self, "Temperature", self.counter) + +if __name__ == '__main__': + + # Setup the command line arguments. + # + # This script can act both as + # "server" an IoT device that can provide sensorinformation + # python IoT_TestDevice.py -j "serverjid@yourdomain.com" -p "password" -n "TestIoT" --debug + # + # "client" an IoT device or other party that would like to get data from another device + + optp = OptionParser() + + # Output verbosity options. + optp.add_option('-q', '--quiet', help='set logging to ERROR', + action='store_const', dest='loglevel', + const=logging.ERROR, default=logging.INFO) + optp.add_option('-d', '--debug', help='set logging to DEBUG', + action='store_const', dest='loglevel', + const=logging.DEBUG, default=logging.INFO) + optp.add_option('-v', '--verbose', help='set logging to COMM', + action='store_const', dest='loglevel', + const=5, default=logging.INFO) + optp.add_option('-t', '--pingto', help='set jid to ping', + action='store', type='string', dest='pingjid', + default=None) + + # JID and password options. + optp.add_option("-j", "--jid", dest="jid", + help="JID to use") + optp.add_option("-p", "--password", dest="password", + help="password to use") + + # IoT test + optp.add_option("-c", "--sensorjid", dest="sensorjid", + help="Another device to call for data on", default=None) + optp.add_option("-n", "--nodeid", dest="nodeid", + help="I am a device get ready to be called", default=None) + + opts, args = optp.parse_args() + + # Setup logging. + logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + + if opts.jid is None: + opts.jid = raw_input("Username: ") + if opts.password is None: + opts.password = getpass.getpass("Password: ") + + + xmpp = IoT_TestDevice(opts.jid,opts.password) + xmpp.register_plugin('xep_0030') + #xmpp['xep_0030'].add_feature(feature='urn:xmpp:iot:sensordata', + # node=None, + # jid=None) + xmpp.register_plugin('xep_0323') + xmpp.register_plugin('xep_0325') + + if opts.nodeid: + + # xmpp['xep_0030'].add_feature(feature='urn:xmpp:sn', + # node=opts.nodeid, + # jid=xmpp.boundjid.full) + + myDevice = TheDevice(opts.nodeid) + # myDevice._add_field(name="Relay", typename="numeric", unit="Bool"); + myDevice._add_field(name="Temperature", typename="numeric", unit="C") + myDevice._set_momentary_timestamp("2013-03-07T16:24:30") + myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"}) + + xmpp['xep_0323'].register_node(nodeId=opts.nodeid, device=myDevice, commTimeout=10) + xmpp.beClientOrServer(server=True) + while not(xmpp.testForRelease()): + xmpp.connect() + xmpp.process(block=True) + logging.debug("lost connection") + if opts.sensorjid: + logging.debug("will try to call another device for data") + xmpp.beClientOrServer(server=False,clientJID=opts.sensorjid) + xmpp.connect() + xmpp.process(block=True) + logging.debug("ready ending") + + else: + print "noopp didn't happen" + diff --git a/examples/adhoc_provider.py b/examples/adhoc_provider.py index a72158c3..86a575c9 100755 --- a/examples/adhoc_provider.py +++ b/examples/adhoc_provider.py @@ -21,8 +21,8 @@ import sleekxmpp # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input diff --git a/examples/adhoc_user.py b/examples/adhoc_user.py index bbd42d81..7df9f793 100755 --- a/examples/adhoc_user.py +++ b/examples/adhoc_user.py @@ -21,8 +21,8 @@ import sleekxmpp # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input diff --git a/examples/admin_commands.py b/examples/admin_commands.py new file mode 100755 index 00000000..5d9bf841 --- /dev/null +++ b/examples/admin_commands.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import sys +import logging +import getpass +from optparse import OptionParser + +import sleekxmpp + +# Python versions before 3.0 do not use UTF-8 encoding +# by default. To ensure that Unicode is handled properly +# throughout SleekXMPP, we will set the default encoding +# ourselves to UTF-8. +if sys.version_info < (3, 0): + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') +else: + raw_input = input + + +class AdminCommands(sleekxmpp.ClientXMPP): + + """ + A simple SleekXMPP bot that uses admin commands to + add a new user to a server. + """ + + def __init__(self, jid, password, command): + sleekxmpp.ClientXMPP.__init__(self, jid, password) + + self.command = command + + self.add_event_handler("session_start", self.start) + + def start(self, event): + """ + Process the session_start event. + + Typical actions for the session_start event are + requesting the roster and broadcasting an initial + presence stanza. + + Arguments: + event -- An empty dictionary. The session_start + event does not provide any additional + data. + """ + self.send_presence() + self.get_roster() + + def command_success(iq, session): + print('Command completed') + if iq['command']['form']: + for var, field in iq['command']['form']['fields'].items(): + print('%s: %s' % (var, field['value'])) + if iq['command']['notes']: + print('Command Notes:') + for note in iq['command']['notes']: + print('%s: %s' % note) + self.disconnect() + + def command_error(iq, session): + print('Error completing command') + print('%s: %s' % (iq['error']['condition'], + iq['error']['text'])) + self['xep_0050'].terminate_command(session) + self.disconnect() + + def process_form(iq, session): + form = iq['command']['form'] + answers = {} + for var, field in form['fields'].items(): + if var != 'FORM_TYPE': + if field['type'] == 'boolean': + answers[var] = raw_input('%s (y/n): ' % field['label']) + if answers[var].lower() in ('1', 'true', 'y', 'yes'): + answers[var] = '1' + else: + answers[var] = '0' + else: + answers[var] = raw_input('%s: ' % field['label']) + else: + answers['FORM_TYPE'] = field['value'] + form['type'] = 'submit' + form['values'] = answers + + session['next'] = command_success + session['payload'] = form + + self['xep_0050'].complete_command(session) + + session = {'next': process_form, + 'error': command_error} + + command = self.command.replace('-', '_') + handler = getattr(self['xep_0133'], command, None) + + if handler: + handler(session={ + 'next': process_form, + 'error': command_error + }) + else: + print('Invalid command name: %s' % self.command) + self.disconnect() + + +if __name__ == '__main__': + # Setup the command line arguments. + optp = OptionParser() + + # Output verbosity options. + optp.add_option('-q', '--quiet', help='set logging to ERROR', + action='store_const', dest='loglevel', + const=logging.ERROR, default=logging.INFO) + optp.add_option('-d', '--debug', help='set logging to DEBUG', + action='store_const', dest='loglevel', + const=logging.DEBUG, default=logging.INFO) + optp.add_option('-v', '--verbose', help='set logging to COMM', + action='store_const', dest='loglevel', + const=5, default=logging.INFO) + + # JID and password options. + optp.add_option("-j", "--jid", dest="jid", + help="JID to use") + optp.add_option("-p", "--password", dest="password", + help="password to use") + optp.add_option("-c", "--command", dest="command", + help="admin command to use") + + opts, args = optp.parse_args() + + # Setup logging. + logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + + if opts.jid is None: + opts.jid = raw_input("Username: ") + if opts.password is None: + opts.password = getpass.getpass("Password: ") + if opts.command is None: + opts.command = raw_input("Admin command: ") + + # Setup the CommandBot and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + xmpp = AdminCommands(opts.jid, opts.password, opts.command) + xmpp.register_plugin('xep_0133') # Service Administration + + # If you are working with an OpenFire server, you may need + # to adjust the SSL version used: + # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 + + # If you want to verify the SSL certificates offered by a server: + # xmpp.ca_certs = "path/to/ca/cert" + + # Connect to the XMPP server and start processing XMPP stanzas. + if xmpp.connect(): + # If you do not have the dnspython library installed, you will need + # to manually specify the name of the server if it does not match + # the one in the JID. For example, to use Google Talk you would + # need to use: + # + # if xmpp.connect(('talk.google.com', 5222)): + # ... + xmpp.process(block=True) + print("Done") + else: + print("Unable to connect.") diff --git a/examples/custom_stanzas/custom_stanza_provider.py b/examples/custom_stanzas/custom_stanza_provider.py index b532c17c..0ebdb77e 100755 --- a/examples/custom_stanzas/custom_stanza_provider.py +++ b/examples/custom_stanzas/custom_stanza_provider.py @@ -28,8 +28,8 @@ from stanza import Action # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input @@ -51,13 +51,13 @@ class ActionBot(sleekxmpp.ClientXMPP): # our roster. self.add_event_handler("session_start", self.start) - self.registerHandler( + self.register_handler( Callback('Some custom iq', StanzaPath('iq@type=set/action'), self._handle_action)) - self.add_event_handler('custom_action', - self._handle_action_event, + self.add_event_handler('custom_action', + self._handle_action_event, threaded=True) register_stanza_plugin(Iq, Action) diff --git a/examples/custom_stanzas/custom_stanza_user.py b/examples/custom_stanzas/custom_stanza_user.py index 5b5042c7..418e3218 100755 --- a/examples/custom_stanzas/custom_stanza_user.py +++ b/examples/custom_stanzas/custom_stanza_user.py @@ -26,8 +26,8 @@ from stanza import Action # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input diff --git a/examples/disco_browser.py b/examples/disco_browser.py index ff0af97d..aeb4fb5e 100755 --- a/examples/disco_browser.py +++ b/examples/disco_browser.py @@ -15,6 +15,7 @@ import getpass from optparse import OptionParser import sleekxmpp +from sleekxmpp.exceptions import IqError, IqTimeout # Python versions before 3.0 do not use UTF-8 encoding @@ -22,8 +23,8 @@ import sleekxmpp # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input @@ -83,50 +84,54 @@ class Disco(sleekxmpp.ClientXMPP): self.get_roster() self.send_presence() - if self.get in self.info_types: - # By using block=True, the result stanza will be - # returned. Execution will block until the reply is - # received. Non-blocking options would be to listen - # for the disco_info event, or passing a handler - # function using the callback parameter. - info = self['xep_0030'].get_info(jid=self.target_jid, - node=self.target_node, - block=True) - if self.get in self.items_types: - # The same applies from above. Listen for the - # disco_items event or pass a callback function - # if you need to process a non-blocking request. - items = self['xep_0030'].get_items(jid=self.target_jid, - node=self.target_node, - block=True) + try: + if self.get in self.info_types: + # By using block=True, the result stanza will be + # returned. Execution will block until the reply is + # received. Non-blocking options would be to listen + # for the disco_info event, or passing a handler + # function using the callback parameter. + info = self['xep_0030'].get_info(jid=self.target_jid, + node=self.target_node, + block=True) + elif self.get in self.items_types: + # The same applies from above. Listen for the + # disco_items event or pass a callback function + # if you need to process a non-blocking request. + items = self['xep_0030'].get_items(jid=self.target_jid, + node=self.target_node, + block=True) + else: + logging.error("Invalid disco request type.") + return + except IqError as e: + logging.error("Entity returned an error: %s" % e.iq['error']['condition']) + except IqTimeout: + logging.error("No response received.") else: - logging.error("Invalid disco request type.") - self.disconnect() - return - - header = 'XMPP Service Discovery: %s' % self.target_jid - print(header) - print('-' * len(header)) - if self.target_node != '': - print('Node: %s' % self.target_node) + header = 'XMPP Service Discovery: %s' % self.target_jid + print(header) print('-' * len(header)) - - if self.get in self.identity_types: - print('Identities:') - for identity in info['disco_info']['identities']: - print(' - %s' % str(identity)) - - if self.get in self.feature_types: - print('Features:') - for feature in info['disco_info']['features']: - print(' - %s' % feature) - - if self.get in self.items_types: - print('Items:') - for item in items['disco_items']['items']: - print(' - %s' % str(item)) - - self.disconnect() + if self.target_node != '': + print('Node: %s' % self.target_node) + print('-' * len(header)) + + if self.get in self.identity_types: + print('Identities:') + for identity in info['disco_info']['identities']: + print(' - %s' % str(identity)) + + if self.get in self.feature_types: + print('Features:') + for feature in info['disco_info']['features']: + print(' - %s' % feature) + + if self.get in self.items_types: + print('Items:') + for item in items['disco_items']['items']: + print(' - %s' % str(item)) + finally: + self.disconnect() if __name__ == '__main__': diff --git a/examples/download_avatars.py b/examples/download_avatars.py new file mode 100755 index 00000000..64300cff --- /dev/null +++ b/examples/download_avatars.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import sys +import logging +import getpass +import threading +from optparse import OptionParser + +import sleekxmpp +from sleekxmpp.exceptions import XMPPError + + +# Python versions before 3.0 do not use UTF-8 encoding +# by default. To ensure that Unicode is handled properly +# throughout SleekXMPP, we will set the default encoding +# ourselves to UTF-8. +if sys.version_info < (3, 0): + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') +else: + raw_input = input + + +FILE_TYPES = { + 'image/png': 'png', + 'image/gif': 'gif', + 'image/jpeg': 'jpg' +} + + +class AvatarDownloader(sleekxmpp.ClientXMPP): + + """ + A basic script for downloading the avatars for a user's contacts. + """ + + def __init__(self, jid, password): + sleekxmpp.ClientXMPP.__init__(self, jid, password) + self.add_event_handler("session_start", self.start, threaded=True) + self.add_event_handler("changed_status", self.wait_for_presences) + + self.add_event_handler('vcard_avatar_update', self.on_vcard_avatar) + self.add_event_handler('avatar_metadata_publish', self.on_avatar) + + self.received = set() + self.presences_received = threading.Event() + + def start(self, event): + """ + Process the session_start event. + + Typical actions for the session_start event are + requesting the roster and broadcasting an initial + presence stanza. + + Arguments: + event -- An empty dictionary. The session_start + event does not provide any additional + data. + """ + self.send_presence() + self.get_roster() + + print('Waiting for presence updates...\n') + self.presences_received.wait(15) + self.disconnect(wait=True) + + def on_vcard_avatar(self, pres): + print("Received vCard avatar update from %s" % pres['from'].bare) + try: + result = self['xep_0054'].get_vcard(pres['from'], cached=True) + except XMPPError: + print("Error retrieving avatar for %s" % pres['from']) + return + avatar = result['vcard_temp']['PHOTO'] + + filetype = FILE_TYPES.get(avatar['TYPE'], 'png') + filename = 'vcard_avatar_%s_%s.%s' % ( + pres['from'].bare, + pres['vcard_temp_update']['photo'], + filetype) + with open(filename, 'w+') as img: + img.write(avatar['BINVAL']) + + def on_avatar(self, msg): + print("Received avatar update from %s" % msg['from']) + metadata = msg['pubsub_event']['items']['item']['avatar_metadata'] + for info in metadata['items']: + if not info['url']: + try: + result = self['xep_0084'].retrieve_avatar(msg['from'], info['id']) + except XMPPError: + print("Error retrieving avatar for %s" % msg['from']) + return + + avatar = result['pubsub']['items']['item']['avatar_data'] + + filetype = FILE_TYPES.get(metadata['type'], 'png') + filename = 'avatar_%s_%s.%s' % (msg['from'].bare, info['id'], filetype) + with open(filename, 'w+') as img: + img.write(avatar['value']) + else: + # We could retrieve the avatar via HTTP, etc here instead. + pass + + def wait_for_presences(self, pres): + """ + Wait to receive updates from all roster contacts. + """ + self.received.add(pres['from'].bare) + if len(self.received) >= len(self.client_roster.keys()): + self.presences_received.set() + else: + self.presences_received.clear() + + +if __name__ == '__main__': + # Setup the command line arguments. + optp = OptionParser() + optp.add_option('-q','--quiet', help='set logging to ERROR', + action='store_const', + dest='loglevel', + const=logging.ERROR, + default=logging.ERROR) + optp.add_option('-d','--debug', help='set logging to DEBUG', + action='store_const', + dest='loglevel', + const=logging.DEBUG, + default=logging.ERROR) + optp.add_option('-v','--verbose', help='set logging to COMM', + action='store_const', + dest='loglevel', + const=5, + default=logging.ERROR) + + # JID and password options. + optp.add_option("-j", "--jid", dest="jid", + help="JID to use") + optp.add_option("-p", "--password", dest="password", + help="password to use") + opts,args = optp.parse_args() + + # Setup logging. + logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + + if opts.jid is None: + opts.jid = raw_input("Username: ") + if opts.password is None: + opts.password = getpass.getpass("Password: ") + + xmpp = AvatarDownloader(opts.jid, opts.password) + xmpp.register_plugin('xep_0054') + xmpp.register_plugin('xep_0153') + xmpp.register_plugin('xep_0084') + + # If you are working with an OpenFire server, you may need + # to adjust the SSL version used: + # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 + + # If you want to verify the SSL certificates offered by a server: + # xmpp.ca_certs = "path/to/ca/cert" + + # Connect to the XMPP server and start processing XMPP stanzas. + if xmpp.connect(): + # If you do not have the dnspython library installed, you will need + # to manually specify the name of the server if it does not match + # the one in the JID. For example, to use Google Talk you would + # need to use: + # + # if xmpp.connect(('talk.google.com', 5222)): + # ... + xmpp.process(block=True) + else: + print("Unable to connect.") diff --git a/examples/echo_client.py b/examples/echo_client.py index 73990089..f2d38847 100755 --- a/examples/echo_client.py +++ b/examples/echo_client.py @@ -21,8 +21,8 @@ import sleekxmpp # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input diff --git a/examples/echo_component.py b/examples/echo_component.py index 82f6eb9f..9a24f2fa 100755 --- a/examples/echo_component.py +++ b/examples/echo_component.py @@ -22,8 +22,8 @@ from sleekxmpp.componentxmpp import ComponentXMPP # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input diff --git a/examples/gtalk_custom_domain.py b/examples/gtalk_custom_domain.py index 0226c146..c974fc55 100755 --- a/examples/gtalk_custom_domain.py +++ b/examples/gtalk_custom_domain.py @@ -25,8 +25,8 @@ from sleekxmpp.xmlstream import cert # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input diff --git a/examples/ibb_transfer/ibb_receiver.py b/examples/ibb_transfer/ibb_receiver.py index b11acabf..6aba98e3 100755 --- a/examples/ibb_transfer/ibb_receiver.py +++ b/examples/ibb_transfer/ibb_receiver.py @@ -21,8 +21,8 @@ import sleekxmpp # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input @@ -38,7 +38,7 @@ class IBBReceiver(sleekxmpp.ClientXMPP): self.register_plugin('xep_0030') # Service Discovery self.register_plugin('xep_0047', { - 'accept_stream': self.accept_stream + 'auto_accept': True }) # In-band Bytestreams # The session_start event will be triggered when @@ -48,7 +48,7 @@ class IBBReceiver(sleekxmpp.ClientXMPP): # our roster. self.add_event_handler("session_start", self.start) - self.add_event_handler("ibb_stream_start", self.stream_opened) + self.add_event_handler("ibb_stream_start", self.stream_opened, threaded=True) self.add_event_handler("ibb_stream_data", self.stream_data) def start(self, event): @@ -69,7 +69,7 @@ class IBBReceiver(sleekxmpp.ClientXMPP): def accept_stream(self, iq): """ - Check that it is ok to accept a stream request. + Check that it is ok to accept a stream request. Controlling stream acceptance can be done via either: - setting 'auto_accept' to False in the plugin @@ -83,9 +83,7 @@ class IBBReceiver(sleekxmpp.ClientXMPP): return True def stream_opened(self, stream): - # NOTE: IBB streams are bi-directional, so the original sender is - # now the opened stream's receiver. - print('Stream opened: %s from ' % (stream.sid, stream.receiver)) + print('Stream opened: %s from %s' % (stream.sid, stream.peer_jid)) # You could run a loop reading from the stream using stream.recv(), # or use the ibb_stream_data event. diff --git a/examples/ibb_transfer/ibb_sender.py b/examples/ibb_transfer/ibb_sender.py index cd856378..7c380b68 100755 --- a/examples/ibb_transfer/ibb_sender.py +++ b/examples/ibb_transfer/ibb_sender.py @@ -21,8 +21,8 @@ import sleekxmpp # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input diff --git a/examples/migrate_roster.py b/examples/migrate_roster.py new file mode 100755 index 00000000..797e4f44 --- /dev/null +++ b/examples/migrate_roster.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import logging +import getpass +from optparse import OptionParser + +import sleekxmpp + +# Python versions before 3.0 do not use UTF-8 encoding +# by default. To ensure that Unicode is handled properly +# throughout SleekXMPP, we will set the default encoding +# ourselves to UTF-8. +if sys.version_info < (3, 0): + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') +else: + raw_input = input + + +# Setup the command line arguments. +optp = OptionParser() + +# Output verbosity options. +optp.add_option('-q', '--quiet', help='set logging to ERROR', + action='store_const', dest='loglevel', + const=logging.ERROR, default=logging.INFO) +optp.add_option('-d', '--debug', help='set logging to DEBUG', + action='store_const', dest='loglevel', + const=logging.DEBUG, default=logging.INFO) +optp.add_option('-v', '--verbose', help='set logging to COMM', + action='store_const', dest='loglevel', + const=5, default=logging.INFO) + +# JID and password options. +optp.add_option("--oldjid", dest="old_jid", + help="JID of the old account") +optp.add_option("--oldpassword", dest="old_password", + help="password of the old account") + +optp.add_option("--newjid", dest="new_jid", + help="JID of the old account") +optp.add_option("--newpassword", dest="new_password", + help="password of the old account") + + +opts, args = optp.parse_args() + +# Setup logging. +logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + +if opts.old_jid is None: + opts.old_jid = raw_input("Old JID: ") +if opts.old_password is None: + opts.old_password = getpass.getpass("Old Password: ") + +if opts.new_jid is None: + opts.new_jid = raw_input("New JID: ") +if opts.new_password is None: + opts.new_password = getpass.getpass("New Password: ") + + +old_xmpp = sleekxmpp.ClientXMPP(opts.old_jid, opts.old_password) + +# If you are connecting to Facebook and wish to use the +# X-FACEBOOK-PLATFORM authentication mechanism, you will need +# your API key and an access token. Then you'll set: +# xmpp.credentials['api_key'] = 'THE_API_KEY' +# xmpp.credentials['access_token'] = 'THE_ACCESS_TOKEN' + +# If you are connecting to MSN, then you will need an +# access token, and it does not matter what JID you +# specify other than that the domain is 'messenger.live.com', +# so '_@messenger.live.com' will work. You can specify +# the access token as so: +# xmpp.credentials['access_token'] = 'THE_ACCESS_TOKEN' + +# If you are working with an OpenFire server, you may need +# to adjust the SSL version used: +# xmpp.ssl_version = ssl.PROTOCOL_SSLv3 + +# If you want to verify the SSL certificates offered by a server: +# xmpp.ca_certs = "path/to/ca/cert" + +roster = [] + +def on_session(event): + roster.append(old_xmpp.get_roster()) + old_xmpp.disconnect() +old_xmpp.add_event_handler('session_start', on_session) + +if old_xmpp.connect(): + old_xmpp.process(block=True) + +if not roster: + print('No roster to migrate') + sys.exit() + +new_xmpp = sleekxmpp.ClientXMPP(opts.new_jid, opts.new_password) +def on_session2(event): + new_xmpp.get_roster() + new_xmpp.send_presence() + + logging.info(roster[0]) + data = roster[0]['roster']['items'] + logging.info(data) + + for jid, item in data.items(): + if item['subscription'] != 'none': + new_xmpp.send_presence(ptype='subscribe', pto=jid) + new_xmpp.update_roster(jid, + name = item['name'], + groups = item['groups']) + new_xmpp.disconnect() +new_xmpp.add_event_handler('session_start', on_session2) + +if new_xmpp.connect(): + new_xmpp.process(block=True) diff --git a/examples/muc.py b/examples/muc.py index 7b93da16..5b5c764c 100755 --- a/examples/muc.py +++ b/examples/muc.py @@ -21,8 +21,8 @@ import sleekxmpp # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input diff --git a/examples/ping.py b/examples/ping.py index fe4d23a4..1a1c2e94 100755 --- a/examples/ping.py +++ b/examples/ping.py @@ -21,8 +21,8 @@ import sleekxmpp # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input @@ -37,7 +37,7 @@ class PingTest(sleekxmpp.ClientXMPP): def __init__(self, jid, password, pingjid): sleekxmpp.ClientXMPP.__init__(self, jid, password) if pingjid is None: - pingjid = self.jid + pingjid = self.boundjid.bare self.pingjid = pingjid # The session_start event will be triggered when @@ -62,16 +62,18 @@ class PingTest(sleekxmpp.ClientXMPP): """ self.send_presence() self.get_roster() - result = self['xep_0199'].send_ping(self.pingjid, - timeout=10, - errorfalse=True) - logging.info("Pinging...") - if result is False: - logging.info("Couldn't ping.") - self.disconnect() - sys.exit(1) - else: - logging.info("Success! RTT: %s", str(result)) + + try: + rtt = self['xep_0199'].ping(self.pingjid, + timeout=10) + logging.info("Success! RTT: %s", rtt) + except IqError as e: + logging.info("Error pinging %s: %s", + self.pingjid, + e.iq['error']['condition']) + except IqTimeout: + logging.info("No response from %s", self.pingjid) + finally: self.disconnect() diff --git a/examples/proxy_echo_client.py b/examples/proxy_echo_client.py index 25bfc891..98935b9c 100755 --- a/examples/proxy_echo_client.py +++ b/examples/proxy_echo_client.py @@ -21,8 +21,8 @@ import sleekxmpp # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input diff --git a/examples/pubsub_client.py b/examples/pubsub_client.py index 0a244f3b..9a65553b 100644..100755 --- a/examples/pubsub_client.py +++ b/examples/pubsub_client.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import sys import logging import getpass @@ -12,15 +15,15 @@ from sleekxmpp.xmlstream import ET, tostring # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input class PubsubClient(sleekxmpp.ClientXMPP): - def __init__(self, jid, password, server, + def __init__(self, jid, password, server, node=None, action='list', data=''): super(PubsubClient, self).__init__(jid, password) @@ -28,7 +31,7 @@ class PubsubClient(sleekxmpp.ClientXMPP): self.register_plugin('xep_0059') self.register_plugin('xep_0060') - self.actions = ['nodes', 'create', 'delete', + self.actions = ['nodes', 'create', 'delete', 'publish', 'get', 'retract', 'purge', 'subscribe', 'unsubscribe'] diff --git a/examples/pubsub_events.py b/examples/pubsub_events.py index 6fe7159b..12c33a76 100644..100755 --- a/examples/pubsub_events.py +++ b/examples/pubsub_events.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import sys import logging import getpass @@ -14,8 +17,8 @@ from sleekxmpp.xmlstream.handler import Callback # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input @@ -77,7 +80,7 @@ class PubsubEvents(sleekxmpp.ClientXMPP): """Handle receiving a node deletion event.""" print('Deleted node %s' % ( msg['pubsub_event']['delete']['node'])) - + def _config(self, msg): """Handle receiving a node configuration event.""" print('Configured node %s:' % ( diff --git a/examples/register_account.py b/examples/register_account.py index 20377b26..422e5602 100644..100755 --- a/examples/register_account.py +++ b/examples/register_account.py @@ -22,8 +22,8 @@ from sleekxmpp.exceptions import IqError, IqTimeout # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input @@ -51,7 +51,7 @@ class RegisterBot(sleekxmpp.ClientXMPP): # The register event provides an Iq result stanza with # a registration form from the server. This may include - # the basic registration fields, a data form, an + # the basic registration fields, a data form, an # out-of-band URL, or any combination. For more advanced # cases, you will need to examine the fields provided # and respond accordingly. SleekXMPP provides plugins @@ -104,7 +104,7 @@ class RegisterBot(sleekxmpp.ClientXMPP): resp.send(now=True) logging.info("Account created for %s!" % self.boundjid) except IqError as e: - logging.error("Could not register account: %s" % + logging.error("Could not register account: %s" % e.iq['error']['text']) self.disconnect() except IqTimeout: @@ -153,6 +153,10 @@ if __name__ == '__main__': xmpp.register_plugin('xep_0066') # Out-of-band Data xmpp.register_plugin('xep_0077') # In-band Registration + # Some servers don't advertise support for inband registration, even + # though they allow it. If this applies to your server, use: + xmpp['xep_0077'].force_registration = True + # If you are working with an OpenFire server, you may need # to adjust the SSL version used: # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 diff --git a/examples/roster_browser.py b/examples/roster_browser.py index b366d00f..a16de24c 100644..100755 --- a/examples/roster_browser.py +++ b/examples/roster_browser.py @@ -24,8 +24,8 @@ from sleekxmpp.exceptions import IqError, IqTimeout # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input @@ -68,7 +68,7 @@ class RosterBrowser(sleekxmpp.ClientXMPP): try: self.get_roster() except IqError as err: - print('Error: %' % err.iq['error']['condition']) + print('Error: %s' % err.iq['error']['condition']) except IqTimeout: print('Error: Request timed out') self.send_presence() diff --git a/examples/rpc_async.py b/examples/rpc_async.py index 0b6d1936..e3e23b69 100644..100755 --- a/examples/rpc_async.py +++ b/examples/rpc_async.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + """ SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Dann Martens @@ -11,34 +14,34 @@ from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \ import time class Boomerang(Endpoint): - + def FQN(self): return 'boomerang' - + @remote def throw(self): print "Duck!" - + def main(): session = Remote.new_session('kangaroo@xmpp.org/rpc', '*****') - session.new_handler(ANY_ALL, Boomerang) - + session.new_handler(ANY_ALL, Boomerang) + boomerang = session.new_proxy('kangaroo@xmpp.org/rpc', Boomerang) - + callback = Future() - + boomerang.async(callback).throw() - + time.sleep(10) - + session.close() - - - + + + if __name__ == '__main__': main() -
\ No newline at end of file + diff --git a/examples/rpc_client_side.py b/examples/rpc_client_side.py index 135d6237..e792fc94 100644..100755 --- a/examples/rpc_client_side.py +++ b/examples/rpc_client_side.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + """ SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Dann Martens @@ -12,18 +15,18 @@ import threading import time class Thermostat(Endpoint): - + def FQN(self): return 'thermostat' - + def __init__(self, initial_temperature): self._temperature = initial_temperature - self._event = threading.Event() - + self._event = threading.Event() + @remote def set_temperature(self, temperature): return NotImplemented - + @remote def get_temperature(self): return NotImplemented @@ -31,23 +34,23 @@ class Thermostat(Endpoint): @remote(False) def release(self): return NotImplemented - + def main(): session = Remote.new_session('operator@xmpp.org/rpc', '*****') - + thermostat = session.new_proxy('thermostat@xmpp.org/rpc', Thermostat) - + print("Current temperature is %s" % thermostat.get_temperature()) - + thermostat.set_temperature(20) - + time.sleep(10) - + session.close() - + if __name__ == '__main__': main() - + diff --git a/examples/rpc_server_side.py b/examples/rpc_server_side.py index d1a11c17..9e8b48d6 100644..100755 --- a/examples/rpc_server_side.py +++ b/examples/rpc_server_side.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + """ SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Dann Martens @@ -11,42 +14,42 @@ from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \ import threading class Thermostat(Endpoint): - + def FQN(self): return 'thermostat' - + def __init__(self, initial_temperature): self._temperature = initial_temperature - self._event = threading.Event() - + self._event = threading.Event() + @remote def set_temperature(self, temperature): print("Setting temperature to %s" % temperature) self._temperature = temperature - + @remote def get_temperature(self): return self._temperature @remote(False) def release(self): - self._event.set() - + self._event.set() + def wait_for_release(self): - self._event.wait() - + self._event.wait() + def main(): session = Remote.new_session('sleek@xmpp.org/rpc', '*****') - + thermostat = session.new_handler(ANY_ALL, Thermostat, 18) - + thermostat.wait_for_release() - + session.close() - + if __name__ == '__main__': main() - + diff --git a/examples/send_client.py b/examples/send_client.py index 5b34bbc9..192469ae 100755 --- a/examples/send_client.py +++ b/examples/send_client.py @@ -21,8 +21,8 @@ import sleekxmpp # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input diff --git a/examples/set_avatar.py b/examples/set_avatar.py new file mode 100755 index 00000000..08e0b664 --- /dev/null +++ b/examples/set_avatar.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import os +import sys +import imghdr +import logging +import getpass +import threading +from optparse import OptionParser + +import sleekxmpp +from sleekxmpp.exceptions import XMPPError + + +# Python versions before 3.0 do not use UTF-8 encoding +# by default. To ensure that Unicode is handled properly +# throughout SleekXMPP, we will set the default encoding +# ourselves to UTF-8. +if sys.version_info < (3, 0): + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') +else: + raw_input = input + + +class AvatarSetter(sleekxmpp.ClientXMPP): + + """ + A basic script for downloading the avatars for a user's contacts. + """ + + def __init__(self, jid, password, filepath): + sleekxmpp.ClientXMPP.__init__(self, jid, password) + + self.add_event_handler("session_start", self.start, threaded=True) + + self.filepath = filepath + + def start(self, event): + """ + Process the session_start event. + + Typical actions for the session_start event are + requesting the roster and broadcasting an initial + presence stanza. + + Arguments: + event -- An empty dictionary. The session_start + event does not provide any additional + data. + """ + self.send_presence() + self.get_roster() + + avatar_file = None + try: + avatar_file = open(os.path.expanduser(self.filepath), 'rb') + except IOError: + print('Could not find file: %s' % self.filepath) + return self.disconnect() + + avatar = avatar_file.read() + + avatar_type = 'image/%s' % imghdr.what('', avatar) + avatar_id = self['xep_0084'].generate_id(avatar) + avatar_bytes = len(avatar) + + avatar_file.close() + + used_xep84 = False + try: + print('Publish XEP-0084 avatar data') + self['xep_0084'].publish_avatar(avatar) + used_xep84 = True + except XMPPError: + print('Could not publish XEP-0084 avatar') + + try: + print('Update vCard with avatar') + self['xep_0153'].set_avatar(avatar=avatar, mtype=avatar_type) + except XMPPError: + print('Could not set vCard avatar') + + if used_xep84: + try: + print('Advertise XEP-0084 avatar metadata') + self['xep_0084'].publish_avatar_metadata([ + {'id': avatar_id, + 'type': avatar_type, + 'bytes': avatar_bytes} + # We could advertise multiple avatars to provide + # options in image type, source (HTTP vs pubsub), + # size, etc. + # {'id': ....} + ]) + except XMPPError: + print('Could not publish XEP-0084 metadata') + + print('Wait for presence updates to propagate...') + self.schedule('end', 5, self.disconnect, kwargs={'wait': True}) + + +if __name__ == '__main__': + # Setup the command line arguments. + optp = OptionParser() + optp.add_option('-q','--quiet', help='set logging to ERROR', + action='store_const', + dest='loglevel', + const=logging.ERROR, + default=logging.ERROR) + optp.add_option('-d','--debug', help='set logging to DEBUG', + action='store_const', + dest='loglevel', + const=logging.DEBUG, + default=logging.ERROR) + optp.add_option('-v','--verbose', help='set logging to COMM', + action='store_const', + dest='loglevel', + const=5, + default=logging.ERROR) + + # JID and password options. + optp.add_option("-j", "--jid", dest="jid", + help="JID to use") + optp.add_option("-p", "--password", dest="password", + help="password to use") + optp.add_option("-f", "--file", dest="filepath", + help="path to the avatar file") + opts,args = optp.parse_args() + + # Setup logging. + logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + + if opts.jid is None: + opts.jid = raw_input("Username: ") + if opts.password is None: + opts.password = getpass.getpass("Password: ") + if opts.filepath is None: + opts.filepath = raw_input("Avatar file location: ") + + xmpp = AvatarSetter(opts.jid, opts.password, opts.filepath) + xmpp.register_plugin('xep_0054') + xmpp.register_plugin('xep_0153') + xmpp.register_plugin('xep_0084') + + # If you are working with an OpenFire server, you may need + # to adjust the SSL version used: + # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 + + # If you want to verify the SSL certificates offered by a server: + # xmpp.ca_certs = "path/to/ca/cert" + + # Connect to the XMPP server and start processing XMPP stanzas. + if xmpp.connect(): + # If you do not have the dnspython library installed, you will need + # to manually specify the name of the server if it does not match + # the one in the JID. For example, to use Google Talk you would + # need to use: + # + # if xmpp.connect(('talk.google.com', 5222)): + # ... + xmpp.process(block=True) + else: + print("Unable to connect.") diff --git a/examples/thirdparty_auth.py b/examples/thirdparty_auth.py index 727311ae..f4d5c400 100644..100755 --- a/examples/thirdparty_auth.py +++ b/examples/thirdparty_auth.py @@ -29,8 +29,8 @@ from sleekxmpp.xmlstream import JID # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') else: raw_input = input diff --git a/examples/user_location.py b/examples/user_location.py index 2a64cada..2a64cada 100644..100755 --- a/examples/user_location.py +++ b/examples/user_location.py diff --git a/examples/user_tune.py b/examples/user_tune.py index 09e050f0..09e050f0 100644..100755 --- a/examples/user_tune.py +++ b/examples/user_tune.py @@ -42,6 +42,7 @@ CLASSIFIERS = [ 'Intended Audience :: Developers', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.1', 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', 'Topic :: Software Development :: Libraries :: Python Modules', ] @@ -49,6 +50,8 @@ packages = [ 'sleekxmpp', 'sleekxmpp/stanza', 'sleekxmpp/test', 'sleekxmpp/roster', + 'sleekxmpp/util', + 'sleekxmpp/util/sasl', 'sleekxmpp/xmlstream', 'sleekxmpp/xmlstream/matcher', 'sleekxmpp/xmlstream/handler', @@ -58,11 +61,16 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0009', 'sleekxmpp/plugins/xep_0009/stanza', 'sleekxmpp/plugins/xep_0012', + 'sleekxmpp/plugins/xep_0013', + 'sleekxmpp/plugins/xep_0016', + 'sleekxmpp/plugins/xep_0020', 'sleekxmpp/plugins/xep_0027', 'sleekxmpp/plugins/xep_0030', 'sleekxmpp/plugins/xep_0030/stanza', 'sleekxmpp/plugins/xep_0033', 'sleekxmpp/plugins/xep_0047', + 'sleekxmpp/plugins/xep_0048', + 'sleekxmpp/plugins/xep_0049', 'sleekxmpp/plugins/xep_0050', 'sleekxmpp/plugins/xep_0054', 'sleekxmpp/plugins/xep_0059', @@ -70,23 +78,30 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0060/stanza', 'sleekxmpp/plugins/xep_0065', 'sleekxmpp/plugins/xep_0066', + 'sleekxmpp/plugins/xep_0071', 'sleekxmpp/plugins/xep_0077', 'sleekxmpp/plugins/xep_0078', 'sleekxmpp/plugins/xep_0080', 'sleekxmpp/plugins/xep_0084', 'sleekxmpp/plugins/xep_0085', 'sleekxmpp/plugins/xep_0086', + 'sleekxmpp/plugins/xep_0091', 'sleekxmpp/plugins/xep_0092', + 'sleekxmpp/plugins/xep_0095', + 'sleekxmpp/plugins/xep_0096', 'sleekxmpp/plugins/xep_0107', 'sleekxmpp/plugins/xep_0108', 'sleekxmpp/plugins/xep_0115', 'sleekxmpp/plugins/xep_0118', 'sleekxmpp/plugins/xep_0128', + 'sleekxmpp/plugins/xep_0131', + 'sleekxmpp/plugins/xep_0152', 'sleekxmpp/plugins/xep_0153', 'sleekxmpp/plugins/xep_0172', 'sleekxmpp/plugins/xep_0184', 'sleekxmpp/plugins/xep_0186', 'sleekxmpp/plugins/xep_0191', + 'sleekxmpp/plugins/xep_0196', 'sleekxmpp/plugins/xep_0198', 'sleekxmpp/plugins/xep_0199', 'sleekxmpp/plugins/xep_0202', @@ -94,8 +109,25 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0221', 'sleekxmpp/plugins/xep_0224', 'sleekxmpp/plugins/xep_0231', + 'sleekxmpp/plugins/xep_0235', 'sleekxmpp/plugins/xep_0249', + 'sleekxmpp/plugins/xep_0257', 'sleekxmpp/plugins/xep_0258', + 'sleekxmpp/plugins/xep_0279', + 'sleekxmpp/plugins/xep_0280', + 'sleekxmpp/plugins/xep_0297', + 'sleekxmpp/plugins/xep_0308', + 'sleekxmpp/plugins/xep_0313', + 'sleekxmpp/plugins/xep_0319', + 'sleekxmpp/plugins/xep_0323', + 'sleekxmpp/plugins/xep_0323/stanza', + 'sleekxmpp/plugins/xep_0325', + 'sleekxmpp/plugins/xep_0325/stanza', + 'sleekxmpp/plugins/google', + 'sleekxmpp/plugins/google/gmail', + 'sleekxmpp/plugins/google/auth', + 'sleekxmpp/plugins/google/settings', + 'sleekxmpp/plugins/google/nosave', 'sleekxmpp/features', 'sleekxmpp/features/feature_mechanisms', 'sleekxmpp/features/feature_mechanisms/stanza', @@ -103,9 +135,8 @@ packages = [ 'sleekxmpp', 'sleekxmpp/features/feature_bind', 'sleekxmpp/features/feature_session', 'sleekxmpp/features/feature_rosterver', + 'sleekxmpp/features/feature_preapproval', 'sleekxmpp/thirdparty', - 'sleekxmpp/thirdparty/suelta', - 'sleekxmpp/thirdparty/suelta/mechanisms', ] setup( diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index a1f1c0f1..85ee32b6 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -6,13 +6,25 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.basexmpp import BaseXMPP -from sleekxmpp.clientxmpp import ClientXMPP -from sleekxmpp.componentxmpp import ComponentXMPP +import logging +if hasattr(logging, 'NullHandler'): + NullHandler = logging.NullHandler +else: + class NullHandler(logging.Handler): + def handle(self, record): + pass +logging.getLogger(__name__).addHandler(NullHandler()) +del NullHandler + + from sleekxmpp.stanza import Message, Presence, Iq +from sleekxmpp.jid import JID, InvalidJID +from sleekxmpp.xmlstream.stanzabase import ET, ElementBase, register_stanza_plugin from sleekxmpp.xmlstream.handler import * from sleekxmpp.xmlstream import XMLStream, RestartStream from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET +from sleekxmpp.basexmpp import BaseXMPP +from sleekxmpp.clientxmpp import ClientXMPP +from sleekxmpp.componentxmpp import ComponentXMPP from sleekxmpp.version import __version__, __version_info__ diff --git a/sleekxmpp/api.py b/sleekxmpp/api.py index 4004f5b7..8de61b34 100644 --- a/sleekxmpp/api.py +++ b/sleekxmpp/api.py @@ -101,8 +101,10 @@ class APIRegistry(object): if not jid: jid = self.xmpp.boundjid - if jid and not isinstance(jid, JID): + elif jid and not isinstance(jid, JID): jid = JID(jid) + elif jid == JID(''): + jid = self.xmpp.boundjid if node is None: node = '' diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index da5b3e41..8cd61b63 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -18,15 +18,13 @@ import sys import logging import threading -import sleekxmpp -from sleekxmpp import plugins, features, roster +from sleekxmpp import plugins, roster, stanza from sleekxmpp.api import APIRegistry from sleekxmpp.exceptions import IqError, IqTimeout from sleekxmpp.stanza import Message, Presence, Iq, StreamError from sleekxmpp.stanza.roster import Roster from sleekxmpp.stanza.nick import Nick -from sleekxmpp.stanza.htmlim import HTMLIM from sleekxmpp.xmlstream import XMLStream, JID from sleekxmpp.xmlstream import ET, register_stanza_plugin @@ -34,8 +32,7 @@ from sleekxmpp.xmlstream.matcher import MatchXPath from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.stanzabase import XML_NS -from sleekxmpp.features import * -from sleekxmpp.plugins import PluginManager, register_plugin, load_plugin +from sleekxmpp.plugins import PluginManager, load_plugin log = logging.getLogger(__name__) @@ -43,8 +40,8 @@ log = logging.getLogger(__name__) # In order to make sure that Unicode is handled properly # in Python 2.x, reset the default encoding. if sys.version_info < (3, 0): - reload(sys) - sys.setdefaultencoding('utf8') + from sleekxmpp.util.misc_ops import setdefaultencoding + setdefaultencoding('utf8') class BaseXMPP(XMLStream): @@ -68,10 +65,20 @@ class BaseXMPP(XMLStream): #: An identifier for the stream as given by the server. self.stream_id = None - #: The JabberID (JID) used by this connection. - self.boundjid = JID(jid) + #: The JabberID (JID) requested for this connection. + self.requested_jid = JID(jid, cache_lock=True) + + #: The JabberID (JID) used by this connection, + #: as set after session binding. This may even be a + #: different bare JID than what was requested. + self.boundjid = JID(jid, cache_lock=True) self._expected_server_name = self.boundjid.host + self._redirect_attempts = 0 + + #: The maximum number of consecutive see-other-host + #: redirections that will be followed before quitting. + self.max_redirects = 5 self.session_bind_event = threading.Event() @@ -91,19 +98,30 @@ class BaseXMPP(XMLStream): #: owner JIDs, as in the case for components. For clients #: which only have a single JID, see :attr:`client_roster`. self.roster = roster.Roster(self) - self.roster.add(self.boundjid.bare) + self.roster.add(self.boundjid) #: The single roster for the bound JID. This is the #: equivalent of:: #: #: self.roster[self.boundjid.bare] - self.client_roster = self.roster[self.boundjid.bare] + self.client_roster = self.roster[self.boundjid] #: The distinction between clients and components can be #: important, primarily for choosing how to handle the #: ``'to'`` and ``'from'`` JIDs of stanzas. self.is_component = False + #: Messages may optionally be tagged with ID values. Setting + #: :attr:`use_message_ids` to `True` will assign all outgoing + #: messages an ID. Some plugin features require enabling + #: this option. + self.use_message_ids = False + + #: Presence updates may optionally be tagged with ID values. + #: Setting :attr:`use_message_ids` to `True` will assign all + #: outgoing messages an ID. + self.use_presence_ids = False + #: The API registry is a way to process callbacks based on #: JID+node combinations. Each callback in the registry is #: marked with: @@ -127,7 +145,7 @@ class BaseXMPP(XMLStream): #: A reference to :mod:`sleekxmpp.stanza` to make accessing #: stanza classes easier. - self.stanza = sleekxmpp.stanza + self.stanza = stanza self.register_handler( Callback('IM', @@ -144,6 +162,8 @@ class BaseXMPP(XMLStream): MatchXPath("{%s}error" % self.stream_ns), self._handle_stream_error)) + self.add_event_handler('session_start', + self._handle_session_start) self.add_event_handler('disconnected', self._handle_disconnected) self.add_event_handler('presence_available', @@ -178,7 +198,6 @@ class BaseXMPP(XMLStream): # Initialize a few default stanza plugins. register_stanza_plugin(Iq, Roster) register_stanza_plugin(Message, Nick) - register_stanza_plugin(Message, HTMLIM) def start_stream_handler(self, xml): """Save the stream ID once the streams have been established. @@ -189,6 +208,10 @@ class BaseXMPP(XMLStream): self.stream_version = xml.get('version', '') self.peer_default_lang = xml.get('{%s}lang' % XML_NS, None) + if not self.is_component and not self.stream_version: + log.warning('Legacy XMPP 0.9 protocol detected.') + self.event('legacy_protocol') + def process(self, *args, **kwargs): """Initialize plugins and begin processing the XML stream. @@ -214,13 +237,6 @@ class BaseXMPP(XMLStream): - The send queue processor - The scheduler """ - if 'xep_0115' in self.plugin: - name = 'xep_0115' - if not hasattr(self.plugin[name], 'post_inited'): - if hasattr(self.plugin[name], 'post_init'): - self.plugin[name].post_init() - self.plugin[name].post_inited = True - for name in self.plugin: if not hasattr(self.plugin[name], 'post_inited'): if hasattr(self.plugin[name], 'post_init'): @@ -228,7 +244,7 @@ class BaseXMPP(XMLStream): self.plugin[name].post_inited = True return XMLStream.process(self, *args, **kwargs) - def register_plugin(self, plugin, pconfig={}, module=None): + def register_plugin(self, plugin, pconfig=None, module=None): """Register and configure a plugin for use in this stream. :param plugin: The name of the plugin class. Plugin names must @@ -591,7 +607,7 @@ class BaseXMPP(XMLStream): @resource.setter def resource(self, value): - log.warning("fulljid property deprecated. Use boundjid.full") + log.warning("fulljid property deprecated. Use boundjid.resource") self.boundjid.resource = value @property @@ -645,7 +661,7 @@ class BaseXMPP(XMLStream): def set_jid(self, jid): """Rip a JID apart and claim it as our own.""" log.debug("setting jid to %s", jid) - self.boundjid.full = jid + self.boundjid = JID(jid, cache_lock=True) def getjidresource(self, fulljid): if '/' in fulljid: @@ -656,6 +672,10 @@ class BaseXMPP(XMLStream): def getjidbare(self, fulljid): return fulljid.split('/', 1)[0] + def _handle_session_start(self, event): + """Reset redirection attempt count.""" + self._redirect_attempts = 0 + def _handle_disconnected(self, event): """When disconnected, reset the roster""" self.roster.reset() @@ -666,6 +686,15 @@ class BaseXMPP(XMLStream): if error['condition'] == 'see-other-host': other_host = error['see_other_host'] + if not other_host: + log.warning("No other host specified.") + return + + if self._redirect_attempts > self.max_redirects: + log.error("Exceeded maximum number of redirection attempts.") + return + + self._redirect_attempts += 1 host = other_host port = 5222 @@ -691,17 +720,13 @@ class BaseXMPP(XMLStream): msg['to'] = self.boundjid self.event('message', msg) - def _handle_available(self, presence): - pto = presence['to'].bare - pfrom = presence['from'].bare - self.roster[pto][pfrom].handle_available(presence) + def _handle_available(self, pres): + self.roster[pres['to']][pres['from']].handle_available(pres) - def _handle_unavailable(self, presence): - pto = presence['to'].bare - pfrom = presence['from'].bare - self.roster[pto][pfrom].handle_unavailable(presence) + def _handle_unavailable(self, pres): + self.roster[pres['to']][pres['from']].handle_unavailable(pres) - def _handle_new_subscription(self, stanza): + def _handle_new_subscription(self, pres): """Attempt to automatically handle subscription requests. Subscriptions will be approved if the request is from @@ -713,10 +738,12 @@ class BaseXMPP(XMLStream): If a subscription is accepted, a request for a mutual subscription will be sent if :attr:`auto_subscribe` is ``True``. """ - roster = self.roster[stanza['to'].bare] - item = self.roster[stanza['to'].bare][stanza['from'].bare] + roster = self.roster[pres['to']] + item = self.roster[pres['to']][pres['from']] if item['whitelisted']: item.authorize() + if roster.auto_subscribe: + item.subscribe() elif roster.auto_authorize: item.authorize() if roster.auto_subscribe: @@ -724,30 +751,20 @@ class BaseXMPP(XMLStream): elif roster.auto_authorize == False: item.unauthorize() - def _handle_removed_subscription(self, presence): - pto = presence['to'].bare - pfrom = presence['from'].bare - self.roster[pto][pfrom].unauthorize() - - def _handle_subscribe(self, presence): - pto = presence['to'].bare - pfrom = presence['from'].bare - self.roster[pto][pfrom].handle_subscribe(presence) - - def _handle_subscribed(self, presence): - pto = presence['to'].bare - pfrom = presence['from'].bare - self.roster[pto][pfrom].handle_subscribed(presence) - - def _handle_unsubscribe(self, presence): - pto = presence['to'].bare - pfrom = presence['from'].bare - self.roster[pto][pfrom].handle_unsubscribe(presence) - - def _handle_unsubscribed(self, presence): - pto = presence['to'].bare - pfrom = presence['from'].bare - self.roster[pto][pfrom].handle_unsubscribed(presence) + def _handle_removed_subscription(self, pres): + self.roster[pres['to']][pres['from']].handle_unauthorize(pres) + + def _handle_subscribe(self, pres): + self.roster[pres['to']][pres['from']].handle_subscribe(pres) + + def _handle_subscribed(self, pres): + self.roster[pres['to']][pres['from']].handle_subscribed(pres) + + def _handle_unsubscribe(self, pres): + self.roster[pres['to']][pres['from']].handle_unsubscribe(pres) + + def _handle_unsubscribed(self, pres): + self.roster[pres['to']][pres['from']].handle_unsubscribed(pres) def _handle_presence(self, presence): """Process incoming presence stanzas. diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 48637dad..8db6ef17 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -52,7 +52,6 @@ class ClientXMPP(BaseXMPP): :param jid: The JID of the XMPP user account. :param password: The password for the XMPP user account. - :param ssl: **Deprecated.** :param plugin_config: A dictionary of plugin configurations. :param plugin_whitelist: A list of approved plugins that will be loaded when calling @@ -60,11 +59,15 @@ class ClientXMPP(BaseXMPP): :param escape_quotes: **Deprecated.** """ - def __init__(self, jid, password, plugin_config={}, plugin_whitelist=[], - escape_quotes=True, sasl_mech=None, lang='en'): + def __init__(self, jid, password, plugin_config=None, plugin_whitelist=None, escape_quotes=True, sasl_mech=None, + lang='en'): + if not plugin_whitelist: + plugin_whitelist = [] + if not plugin_config: + plugin_config = {} + BaseXMPP.__init__(self, jid, 'jabber:client') - self.set_jid(jid) self.escape_quotes = escape_quotes self.plugin_config = plugin_config self.plugin_whitelist = plugin_whitelist @@ -95,8 +98,9 @@ class ClientXMPP(BaseXMPP): self.bound = False self.bindfail = False - self.add_event_handler('connected', self._handle_connected) + self.add_event_handler('connected', self._reset_connection_state) self.add_event_handler('session_bind', self._handle_session_bind) + self.add_event_handler('roster_update', self._handle_roster) self.register_stanza(StreamFeatures) @@ -107,15 +111,18 @@ class ClientXMPP(BaseXMPP): self.register_handler( Callback('Roster Update', StanzaPath('iq@type=set/roster'), - self._handle_roster)) + lambda iq: self.event('roster_update', iq))) # Setup default stream features self.register_plugin('feature_starttls') self.register_plugin('feature_bind') self.register_plugin('feature_session') - self.register_plugin('feature_mechanisms', - pconfig={'use_mech': sasl_mech} if sasl_mech else None) self.register_plugin('feature_rosterver') + self.register_plugin('feature_preapproval') + self.register_plugin('feature_mechanisms') + + if sasl_mech: + self['feature_mechanisms'].use_mech = sasl_mech @property def password(self): @@ -133,7 +140,7 @@ class ClientXMPP(BaseXMPP): be attempted. If that fails, the server user in the JID will be used. - :param address -- A tuple containing the server's host and port. + :param address: A tuple containing the server's host and port. :param reattempt: If ``True``, repeat attempting to connect if an error occurs. Defaults to ``True``. :param use_tls: Indicates if TLS should be used for the @@ -152,8 +159,6 @@ class ClientXMPP(BaseXMPP): address = (self.boundjid.host, 5222) self.dns_service = 'xmpp-client' - self._expected_server_name = self.boundjid.host - return XMLStream.connect(self, address[0], address[1], use_tls=use_tls, use_ssl=use_ssl, reattempt=reattempt) @@ -179,8 +184,7 @@ class ClientXMPP(BaseXMPP): self._stream_feature_order.remove((order, name)) self._stream_feature_order.sort() - def update_roster(self, jid, name=None, subscription=None, groups=[], - block=True, timeout=None, callback=None): + def update_roster(self, jid, **kwargs): """Add or change a roster item. :param jid: The JID of the entry to modify. @@ -201,6 +205,16 @@ class ClientXMPP(BaseXMPP): Will be executed when the roster is received. Implies ``block=False``. """ + current = self.client_roster[jid] + + name = kwargs.get('name', current['name']) + subscription = kwargs.get('subscription', current['subscription']) + groups = kwargs.get('groups', current['groups']) + + block = kwargs.get('block', True) + timeout = kwargs.get('timeout', None) + callback = kwargs.get('callback', None) + return self.client_roster.update(jid, name, subscription, groups, block, timeout, callback) @@ -233,17 +247,25 @@ class ClientXMPP(BaseXMPP): if 'rosterver' in self.features: iq['roster']['ver'] = self.client_roster.version - if not block and callback is None: - callback = lambda resp: self._handle_roster(resp) + + if not block or callback is not None: + block = False + if callback is None: + callback = lambda resp: self.event('roster_update', resp) + else: + orig_cb = callback + def wrapped(resp): + self.event('roster_update', resp) + orig_cb(resp) + callback = wrapped response = iq.send(block, timeout, callback) - self.event('roster_received', response) if block: - self._handle_roster(response) + self.event('roster_update', response) return response - def _handle_connected(self, event=None): + def _reset_connection_state(self, event=None): #TODO: Use stream state here self.authenticated = False self.sessionstarted = False @@ -263,6 +285,8 @@ class ClientXMPP(BaseXMPP): # Don't continue if the feature requires # restarting the XML stream. return True + log.debug('Finished processing stream features.') + self.event('stream_negotiated') def _handle_roster(self, iq): """Update the roster after receiving a roster stanza. @@ -277,17 +301,18 @@ class ClientXMPP(BaseXMPP): if iq['roster']['ver']: roster.version = iq['roster']['ver'] items = iq['roster']['items'] - for jid in items: - item = items[jid] - roster[jid]['name'] = item['name'] - roster[jid]['groups'] = item['groups'] - roster[jid]['from'] = item['subscription'] in ['from', 'both'] - roster[jid]['to'] = item['subscription'] in ['to', 'both'] - roster[jid]['pending_out'] = (item['ask'] == 'subscribe') - roster[jid].save(remove=(item['subscription'] == 'remove')) + valid_subscriptions = ('to', 'from', 'both', 'none', 'remove') + for jid, item in items.items(): + if item['subscription'] in valid_subscriptions: + roster[jid]['name'] = item['name'] + roster[jid]['groups'] = item['groups'] + roster[jid]['from'] = item['subscription'] in ('from', 'both') + roster[jid]['to'] = item['subscription'] in ('to', 'both') + roster[jid]['pending_out'] = (item['ask'] == 'subscribe') + + roster[jid].save(remove=(item['subscription'] == 'remove')) - self.event("roster_update", iq) if iq['type'] == 'set': resp = self.Iq(stype='result', sto=iq['from'], diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index 20748b69..4b229a6f 100644 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -49,8 +49,13 @@ class ComponentXMPP(BaseXMPP): Defaults to ``False``. """ - def __init__(self, jid, secret, host=None, port=None, - plugin_config={}, plugin_whitelist=[], use_jc_ns=False): + def __init__(self, jid, secret, host=None, port=None, plugin_config=None, plugin_whitelist=None, use_jc_ns=False): + + if not plugin_whitelist: + plugin_whitelist = [] + if not plugin_config: + plugin_config = {} + if use_jc_ns: default_ns = 'jabber:client' else: @@ -123,12 +128,6 @@ class ComponentXMPP(BaseXMPP): """ if xml.tag.startswith('{jabber:client}'): xml.tag = xml.tag.replace('jabber:client', self.default_ns) - - # The incoming_filter call is only made on top level stanza - # elements. So we manually continue filtering on sub-elements. - for sub in xml: - self.incoming_filter(sub) - return xml def start_stream_handler(self, xml): @@ -158,10 +157,8 @@ class ComponentXMPP(BaseXMPP): """ self.session_bind_event.set() self.session_started_event.set() - self.event("session_bind", self.xmpp.boundjid.full, direct=True) - self.event("session_start") + self.event('session_bind', self.boundjid, direct=True) + self.event('session_start') - def _handle_probe(self, presence): - pto = presence['to'].bare - pfrom = presence['from'].bare - self.roster[pto][pfrom].handle_probe(presence) + def _handle_probe(self, pres): + self.roster[pres['to']][pres['from']].handle_probe(pres) diff --git a/sleekxmpp/exceptions.py b/sleekxmpp/exceptions.py index 8036532d..8a2aa75c 100644 --- a/sleekxmpp/exceptions.py +++ b/sleekxmpp/exceptions.py @@ -42,7 +42,7 @@ class XMPPError(Exception): Defaults to ``True``. """ - def __init__(self, condition='undefined-condition', text=None, + def __init__(self, condition='undefined-condition', text='', etype='cancel', extension=None, extension_ns=None, extension_args=None, clear=True): if extension_args is None: diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py index 1ef1e0cf..869de7e9 100644 --- a/sleekxmpp/features/__init__.py +++ b/sleekxmpp/features/__init__.py @@ -11,5 +11,6 @@ __all__ = [ 'feature_mechanisms', 'feature_bind', 'feature_session', - 'feature_rosterver' + 'feature_rosterver', + 'feature_preapproval' ] diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py index 2253d5ae..ee4c1e9b 100644 --- a/sleekxmpp/features/feature_bind/bind.py +++ b/sleekxmpp/features/feature_bind/bind.py @@ -8,10 +8,11 @@ import logging +from sleekxmpp.jid import JID from sleekxmpp.stanza import Iq, StreamFeatures from sleekxmpp.features.feature_bind import stanza from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.plugins import BasePlugin, register_plugin +from sleekxmpp.plugins import BasePlugin log = logging.getLogger(__name__) @@ -40,25 +41,25 @@ class FeatureBind(BasePlugin): Arguments: features -- The stream features stanza. """ - log.debug("Requesting resource: %s", self.xmpp.boundjid.resource) + log.debug("Requesting resource: %s", self.xmpp.requested_jid.resource) iq = self.xmpp.Iq() iq['type'] = 'set' iq.enable('bind') - if self.xmpp.boundjid.resource: - iq['bind']['resource'] = self.xmpp.boundjid.resource + if self.xmpp.requested_jid.resource: + iq['bind']['resource'] = self.xmpp.requested_jid.resource response = iq.send(now=True) - self.xmpp.set_jid(response['bind']['jid']) + self.xmpp.boundjid = JID(response['bind']['jid'], cache_lock=True) self.xmpp.bound = True - self.xmpp.event('session_bind', self.xmpp.boundjid.full, direct=True) + self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True) self.xmpp.session_bind_event.set() self.xmpp.features.add('bind') - log.info("Node set to: %s", self.xmpp.boundjid.full) + log.info("JID set to: %s", self.xmpp.boundjid.full) if 'session' not in features['features']: log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.session_started_event.set() - self.xmpp.event("session_start") + self.xmpp.event('session_start') diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index 930aa8fe..1d8f8798 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -6,12 +6,11 @@ See the file LICENSE for copying permission. """ +import ssl import logging -from sleekxmpp.thirdparty import suelta -from sleekxmpp.thirdparty.suelta.exceptions import SASLCancelled, SASLError -from sleekxmpp.thirdparty.suelta.exceptions import SASLPrepFailure - +from sleekxmpp.util import sasl +from sleekxmpp.util.stringprep_profiles import StringPrepError from sleekxmpp.stanza import StreamFeatures from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin from sleekxmpp.plugins import BasePlugin @@ -29,42 +28,32 @@ class FeatureMechanisms(BasePlugin): description = 'RFC 6120: Stream Feature: SASL' dependencies = set() stanza = stanza + default_config = { + 'use_mech': None, + 'use_mechs': None, + 'min_mech': None, + 'sasl_callback': None, + 'security_callback': None, + 'encrypted_plain': True, + 'unencrypted_plain': False, + 'unencrypted_digest': False, + 'unencrypted_cram': False, + 'unencrypted_scram': True, + 'order': 100 + } def plugin_init(self): - self.use_mech = self.config.get('use_mech', None) + if self.sasl_callback is None: + self.sasl_callback = self._default_credentials - if not self.use_mech and not self.xmpp.boundjid.user: - self.use_mech = 'ANONYMOUS' + if self.security_callback is None: + self.security_callback = self._default_security - def tls_active(): - return 'starttls' in self.xmpp.features - - def basic_callback(mech, values): - creds = self.xmpp.credentials - for value in values: - if value == 'username': - values['username'] = self.xmpp.boundjid.user - elif value == 'password': - values['password'] = creds['password'] - elif value == 'email': - jid = self.xmpp.boundjid.bare - values['email'] = creds.get('email', jid) - elif value in creds: - values[value] = creds[value] - mech.fulfill(values) - - sasl_callback = self.config.get('sasl_callback', None) - if sasl_callback is None: - sasl_callback = basic_callback + creds = self.sasl_callback(set(['username']), set()) + if not self.use_mech and not creds['username']: + self.use_mech = 'ANONYMOUS' self.mech = None - self.sasl = suelta.SASL(self.xmpp.boundjid.domain, 'xmpp', - username=self.xmpp.boundjid.user, - sec_query=suelta.sec_query_allow, - request_values=sasl_callback, - tls_active=tls_active, - mech=self.use_mech) - self.mech_list = set() self.attempted_mechs = set() @@ -95,7 +84,51 @@ class FeatureMechanisms(BasePlugin): self.xmpp.register_feature('mechanisms', self._handle_sasl_auth, restart=True, - order=self.config.get('order', 100)) + order=self.order) + + def _default_credentials(self, required_values, optional_values): + creds = self.xmpp.credentials + result = {} + values = required_values.union(optional_values) + for value in values: + if value == 'username': + result[value] = creds.get('username', self.xmpp.requested_jid.user) + elif value == 'email': + jid = self.xmpp.requested_jid.bare + result[value] = creds.get('email', jid) + elif value == 'channel_binding': + if hasattr(self.xmpp.socket, 'get_channel_binding'): + result[value] = self.xmpp.socket.get_channel_binding() + else: + log.debug("Channel binding not supported.") + log.debug("Use Python 3.3+ for channel binding and " + \ + "SCRAM-SHA-1-PLUS support") + result[value] = None + elif value == 'host': + result[value] = creds.get('host', self.xmpp.requested_jid.domain) + elif value == 'realm': + result[value] = creds.get('realm', self.xmpp.requested_jid.domain) + elif value == 'service-name': + result[value] = creds.get('service-name', self.xmpp._service_name) + elif value == 'service': + result[value] = creds.get('service', 'xmpp') + elif value in creds: + result[value] = creds[value] + return result + + def _default_security(self, values): + result = {} + for value in values: + if value == 'encrypted': + if 'starttls' in self.xmpp.features: + result[value] = True + elif isinstance(self.xmpp.socket, ssl.SSLSocket): + result[value] = True + else: + result[value] = False + else: + result[value] = self.config.get(value, False) + return result def _handle_sasl_auth(self, features): """ @@ -109,37 +142,62 @@ class FeatureMechanisms(BasePlugin): # server has incorrectly offered it again. return False - if not self.use_mech: - self.mech_list = set(features['mechanisms']) - else: - self.mech_list = set([self.use_mech]) + enforce_limit = False + limited_mechs = self.use_mechs + + if limited_mechs is None: + limited_mechs = set() + elif limited_mechs and not isinstance(limited_mechs, set): + limited_mechs = set(limited_mechs) + enforce_limit = True + + if self.use_mech: + limited_mechs.add(self.use_mech) + enforce_limit = True + + if enforce_limit: + self.use_mechs = limited_mechs + + self.mech_list = set(features['mechanisms']) + return self._send_auth() def _send_auth(self): mech_list = self.mech_list - self.attempted_mechs - self.mech = self.sasl.choose_mechanism(mech_list) - - if mech_list and self.mech is not None: - resp = stanza.Auth(self.xmpp) - resp['mechanism'] = self.mech.name - try: - resp['value'] = self.mech.process() - except SASLCancelled: - self.attempted_mechs.add(self.mech.name) - self._send_auth() - except SASLError: - self.attempted_mechs.add(self.mech.name) - self._send_auth() - except SASLPrepFailure: - log.exception("A credential value did not pass SASLprep.") - self.xmpp.disconnect() - else: - resp.send(now=True) - else: + try: + self.mech = sasl.choose(mech_list, + self.sasl_callback, + self.security_callback, + limit=self.use_mechs, + min_mech=self.min_mech) + except sasl.SASLNoAppropriateMechanism: log.error("No appropriate login method.") self.xmpp.event("no_auth", direct=True) + self.xmpp.event("failed_auth", direct=True) self.attempted_mechs = set() + return self.xmpp.disconnect() + except StringPrepError: + log.exception("A credential value did not pass SASLprep.") + self.xmpp.disconnect() + + resp = stanza.Auth(self.xmpp) + resp['mechanism'] = self.mech.name + try: + resp['value'] = self.mech.process() + except sasl.SASLCancelled: + self.attempted_mechs.add(self.mech.name) + self._send_auth() + except sasl.SASLMutualAuthFailed: + log.error("Mutual authentication failed! " + \ + "A security breach is possible.") + self.attempted_mechs.add(self.mech.name) self.xmpp.disconnect() + except sasl.SASLFailed: + self.attempted_mechs.add(self.mech.name) + self._send_auth() + else: + resp.send(now=True) + return True def _handle_challenge(self, stanza): @@ -147,20 +205,35 @@ class FeatureMechanisms(BasePlugin): resp = self.stanza.Response(self.xmpp) try: resp['value'] = self.mech.process(stanza['value']) - except SASLCancelled: + except sasl.SASLCancelled: self.stanza.Abort(self.xmpp).send() - except SASLError: + except sasl.SASLMutualAuthFailed: + log.error("Mutual authentication failed! " + \ + "A security breach is possible.") + self.attempted_mechs.add(self.mech.name) + self.xmpp.disconnect() + except sasl.SASLFailed: self.stanza.Abort(self.xmpp).send() else: + if resp.get_value() == '': + resp.del_value() resp.send(now=True) def _handle_success(self, stanza): """SASL authentication succeeded. Restart the stream.""" - self.attempted_mechs = set() - self.xmpp.authenticated = True - self.xmpp.features.add('mechanisms') - self.xmpp.event('auth_success', stanza, direct=True) - raise RestartStream() + try: + final = self.mech.process(stanza['value']) + except sasl.SASLMutualAuthFailed: + log.error("Mutual authentication failed! " + \ + "A security breach is possible.") + self.attempted_mechs.add(self.mech.name) + self.xmpp.disconnect() + else: + self.attempted_mechs = set() + self.xmpp.authenticated = True + self.xmpp.features.add('mechanisms') + self.xmpp.event('auth_success', stanza, direct=True) + raise RestartStream() def _handle_fail(self, stanza): """SASL authentication failed. Disconnect and shutdown.""" diff --git a/sleekxmpp/features/feature_mechanisms/stanza/auth.py b/sleekxmpp/features/feature_mechanisms/stanza/auth.py index 8b9d18b6..6b6f85a3 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/auth.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py @@ -8,8 +8,7 @@ import base64 -from sleekxmpp.thirdparty.suelta.util import bytes - +from sleekxmpp.util import bytes from sleekxmpp.xmlstream import StanzaBase @@ -41,7 +40,7 @@ class Auth(StanzaBase): if not self['mechanism'] in self.plain_mechs: if values: self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') - else: + elif values == b'': self.xml.text = '=' else: self.xml.text = bytes(values).decode('utf-8') diff --git a/sleekxmpp/features/feature_mechanisms/stanza/challenge.py b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py index 85d65403..24290281 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/challenge.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py @@ -8,8 +8,7 @@ import base64 -from sleekxmpp.thirdparty.suelta.util import bytes - +from sleekxmpp.util import bytes from sleekxmpp.xmlstream import StanzaBase diff --git a/sleekxmpp/features/feature_mechanisms/stanza/response.py b/sleekxmpp/features/feature_mechanisms/stanza/response.py index 78636c9e..ca7624f1 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/response.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/response.py @@ -8,8 +8,7 @@ import base64 -from sleekxmpp.thirdparty.suelta.util import bytes - +from sleekxmpp.util import bytes from sleekxmpp.xmlstream import StanzaBase diff --git a/sleekxmpp/features/feature_mechanisms/stanza/success.py b/sleekxmpp/features/feature_mechanisms/stanza/success.py index 7a5a73f2..7a4eab8e 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/success.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/success.py @@ -6,8 +6,10 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.xmlstream import StanzaBase +import base64 +from sleekxmpp.util import bytes +from sleekxmpp.xmlstream import StanzaBase class Success(StanzaBase): @@ -16,9 +18,21 @@ class Success(StanzaBase): name = 'success' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' - interfaces = set() + interfaces = set(['value']) plugin_attrib = name def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() + + def get_value(self): + return base64.b64decode(bytes(self.xml.text)) + + def set_value(self, values): + if values: + self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + else: + self.xml.text = '=' + + def del_value(self): + self.xml.text = '' diff --git a/sleekxmpp/features/feature_preapproval/__init__.py b/sleekxmpp/features/feature_preapproval/__init__.py new file mode 100644 index 00000000..ae8b6b70 --- /dev/null +++ b/sleekxmpp/features/feature_preapproval/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.features.feature_preapproval.preapproval import FeaturePreApproval +from sleekxmpp.features.feature_preapproval.stanza import PreApproval + + +register_plugin(FeaturePreApproval) diff --git a/sleekxmpp/features/feature_preapproval/preapproval.py b/sleekxmpp/features/feature_preapproval/preapproval.py new file mode 100644 index 00000000..c7106ed3 --- /dev/null +++ b/sleekxmpp/features/feature_preapproval/preapproval.py @@ -0,0 +1,42 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import StreamFeatures +from sleekxmpp.features.feature_preapproval import stanza +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.base import BasePlugin + + +log = logging.getLogger(__name__) + + +class FeaturePreApproval(BasePlugin): + + name = 'feature_preapproval' + description = 'RFC 6121: Stream Feature: Subscription Pre-Approval' + dependences = set() + stanza = stanza + + def plugin_init(self): + self.xmpp.register_feature('preapproval', + self._handle_preapproval, + restart=False, + order=9001) + + register_stanza_plugin(StreamFeatures, stanza.PreApproval) + + def _handle_preapproval(self, features): + """Save notice that the server support subscription pre-approvals. + + Arguments: + features -- The stream features stanza. + """ + log.debug("Server supports subscription pre-approvals.") + self.xmpp.features.add('preapproval') diff --git a/sleekxmpp/features/feature_preapproval/stanza.py b/sleekxmpp/features/feature_preapproval/stanza.py new file mode 100644 index 00000000..4a59bd16 --- /dev/null +++ b/sleekxmpp/features/feature_preapproval/stanza.py @@ -0,0 +1,17 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase + + +class PreApproval(ElementBase): + + name = 'sub' + namespace = 'urn:xmpp:features:pre-approval' + interfaces = set() + plugin_attrib = 'preapproval' diff --git a/sleekxmpp/features/feature_rosterver/rosterver.py b/sleekxmpp/features/feature_rosterver/rosterver.py index 9e0bb8e8..2991f587 100644 --- a/sleekxmpp/features/feature_rosterver/rosterver.py +++ b/sleekxmpp/features/feature_rosterver/rosterver.py @@ -8,7 +8,7 @@ import logging -from sleekxmpp.stanza import Iq, StreamFeatures +from sleekxmpp.stanza import StreamFeatures from sleekxmpp.features.feature_rosterver import stanza from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.plugins.base import BasePlugin diff --git a/sleekxmpp/features/feature_session/session.py b/sleekxmpp/features/feature_session/session.py index c799a763..ceadd5f3 100644 --- a/sleekxmpp/features/feature_session/session.py +++ b/sleekxmpp/features/feature_session/session.py @@ -51,4 +51,4 @@ class FeatureSession(BasePlugin): log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.session_started_event.set() - self.xmpp.event("session_start") + self.xmpp.event('session_start') diff --git a/sleekxmpp/features/feature_starttls/starttls.py b/sleekxmpp/features/feature_starttls/starttls.py index 212b9da5..eb5eee1d 100644 --- a/sleekxmpp/features/feature_starttls/starttls.py +++ b/sleekxmpp/features/feature_starttls/starttls.py @@ -54,13 +54,9 @@ class FeatureSTARTTLS(BasePlugin): return False elif not self.xmpp.use_tls: return False - elif self.xmpp.ssl_support: + else: self.xmpp.send(features['starttls'], now=True) return True - else: - log.warning("The module tlslite is required to log in" + \ - " to some servers, and has not been found.") - return False def _handle_starttls_proceed(self, proceed): """Restart the XML stream when TLS is accepted.""" diff --git a/sleekxmpp/jid.py b/sleekxmpp/jid.py new file mode 100644 index 00000000..ac5ba30d --- /dev/null +++ b/sleekxmpp/jid.py @@ -0,0 +1,638 @@ +# -*- coding: utf-8 -*- +""" + sleekxmpp.jid + ~~~~~~~~~~~~~~~~~~~~~~~ + + This module allows for working with Jabber IDs (JIDs). + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details +""" + +from __future__ import unicode_literals + +import re +import socket +import stringprep +import threading +import encodings.idna + +from copy import deepcopy + +from sleekxmpp.util import stringprep_profiles +from sleekxmpp.thirdparty import OrderedDict + +#: These characters are not allowed to appear in a JID. +ILLEGAL_CHARS = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r' + \ + '\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19' + \ + '\x1a\x1b\x1c\x1d\x1e\x1f' + \ + ' !"#$%&\'()*+,./:;<=>?@[\\]^_`{|}~\x7f' + +#: The basic regex pattern that a JID must match in order to determine +#: the local, domain, and resource parts. This regex does NOT do any +#: validation, which requires application of nodeprep, resourceprep, etc. +JID_PATTERN = re.compile( + "^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$" +) + +#: The set of escape sequences for the characters not allowed by nodeprep. +JID_ESCAPE_SEQUENCES = set(['\\20', '\\22', '\\26', '\\27', '\\2f', + '\\3a', '\\3c', '\\3e', '\\40', '\\5c']) + +#: A mapping of unallowed characters to their escape sequences. An escape +#: sequence for '\' is also included since it must also be escaped in +#: certain situations. +JID_ESCAPE_TRANSFORMATIONS = {' ': '\\20', + '"': '\\22', + '&': '\\26', + "'": '\\27', + '/': '\\2f', + ':': '\\3a', + '<': '\\3c', + '>': '\\3e', + '@': '\\40', + '\\': '\\5c'} + +#: The reverse mapping of escape sequences to their original forms. +JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ', + '\\22': '"', + '\\26': '&', + '\\27': "'", + '\\2f': '/', + '\\3a': ':', + '\\3c': '<', + '\\3e': '>', + '\\40': '@', + '\\5c': '\\'} + +JID_CACHE = OrderedDict() +JID_CACHE_LOCK = threading.Lock() +JID_CACHE_MAX_SIZE = 1024 + +def _cache(key, parts, locked): + JID_CACHE[key] = (parts, locked) + if len(JID_CACHE) > JID_CACHE_MAX_SIZE: + with JID_CACHE_LOCK: + while len(JID_CACHE) > JID_CACHE_MAX_SIZE: + found = None + for key, item in JID_CACHE.items(): + if not item[1]: # if not locked + found = key + break + if not found: # more than MAX_SIZE locked + # warn? + break + del JID_CACHE[found] + +# pylint: disable=c0103 +#: The nodeprep profile of stringprep used to validate the local, +#: or username, portion of a JID. +nodeprep = stringprep_profiles.create( + nfkc=True, + bidi=True, + mappings=[ + stringprep_profiles.b1_mapping, + stringprep.map_table_b2], + prohibited=[ + stringprep.in_table_c11, + stringprep.in_table_c12, + stringprep.in_table_c21, + stringprep.in_table_c22, + stringprep.in_table_c3, + stringprep.in_table_c4, + stringprep.in_table_c5, + stringprep.in_table_c6, + stringprep.in_table_c7, + stringprep.in_table_c8, + stringprep.in_table_c9, + lambda c: c in ' \'"&/:<>@'], + unassigned=[stringprep.in_table_a1]) + +# pylint: disable=c0103 +#: The resourceprep profile of stringprep, which is used to validate +#: the resource portion of a JID. +resourceprep = stringprep_profiles.create( + nfkc=True, + bidi=True, + mappings=[stringprep_profiles.b1_mapping], + prohibited=[ + stringprep.in_table_c12, + stringprep.in_table_c21, + stringprep.in_table_c22, + stringprep.in_table_c3, + stringprep.in_table_c4, + stringprep.in_table_c5, + stringprep.in_table_c6, + stringprep.in_table_c7, + stringprep.in_table_c8, + stringprep.in_table_c9], + unassigned=[stringprep.in_table_a1]) + + +def _parse_jid(data): + """ + Parse string data into the node, domain, and resource + components of a JID, if possible. + + :param string data: A string that is potentially a JID. + + :raises InvalidJID: + + :returns: tuple of the validated local, domain, and resource strings + """ + match = JID_PATTERN.match(data) + if not match: + raise InvalidJID('JID could not be parsed') + + (node, domain, resource) = match.groups() + + node = _validate_node(node) + domain = _validate_domain(domain) + resource = _validate_resource(resource) + + return node, domain, resource + + +def _validate_node(node): + """Validate the local, or username, portion of a JID. + + :raises InvalidJID: + + :returns: The local portion of a JID, as validated by nodeprep. + """ + try: + if node is not None: + node = nodeprep(node) + + if not node: + raise InvalidJID('Localpart must not be 0 bytes') + if len(node) > 1023: + raise InvalidJID('Localpart must be less than 1024 bytes') + return node + except stringprep_profiles.StringPrepError: + raise InvalidJID('Invalid local part') + + +def _validate_domain(domain): + """Validate the domain portion of a JID. + + IP literal addresses are left as-is, if valid. Domain names + are stripped of any trailing label separators (`.`), and are + checked with the nameprep profile of stringprep. If the given + domain is actually a punyencoded version of a domain name, it + is converted back into its original Unicode form. Domains must + also not start or end with a dash (`-`). + + :raises InvalidJID: + + :returns: The validated domain name + """ + ip_addr = False + + # First, check if this is an IPv4 address + try: + socket.inet_aton(domain) + ip_addr = True + except socket.error: + pass + + # Check if this is an IPv6 address + if not ip_addr and hasattr(socket, 'inet_pton'): + try: + socket.inet_pton(socket.AF_INET6, domain.strip('[]')) + domain = '[%s]' % domain.strip('[]') + ip_addr = True + except (socket.error, ValueError): + pass + + if not ip_addr: + # This is a domain name, which must be checked further + + if domain and domain[-1] == '.': + domain = domain[:-1] + + domain_parts = [] + for label in domain.split('.'): + try: + label = encodings.idna.nameprep(label) + encodings.idna.ToASCII(label) + pass_nameprep = True + except UnicodeError: + pass_nameprep = False + + if not pass_nameprep: + raise InvalidJID('Could not encode domain as ASCII') + + if label.startswith('xn--'): + label = encodings.idna.ToUnicode(label) + + for char in label: + if char in ILLEGAL_CHARS: + raise InvalidJID('Domain contains illegal characters') + + if '-' in (label[0], label[-1]): + raise InvalidJID('Domain started or ended with -') + + domain_parts.append(label) + domain = '.'.join(domain_parts) + + if not domain: + raise InvalidJID('Domain must not be 0 bytes') + if len(domain) > 1023: + raise InvalidJID('Domain must be less than 1024 bytes') + + return domain + + +def _validate_resource(resource): + """Validate the resource portion of a JID. + + :raises InvalidJID: + + :returns: The local portion of a JID, as validated by resourceprep. + """ + try: + if resource is not None: + resource = resourceprep(resource) + + if not resource: + raise InvalidJID('Resource must not be 0 bytes') + if len(resource) > 1023: + raise InvalidJID('Resource must be less than 1024 bytes') + return resource + except stringprep_profiles.StringPrepError: + raise InvalidJID('Invalid resource') + + +def _escape_node(node): + """Escape the local portion of a JID.""" + result = [] + + for i, char in enumerate(node): + if char == '\\': + if ''.join((node[i:i+3])) in JID_ESCAPE_SEQUENCES: + result.append('\\5c') + continue + result.append(char) + + for i, char in enumerate(result): + if char != '\\': + result[i] = JID_ESCAPE_TRANSFORMATIONS.get(char, char) + + escaped = ''.join(result) + + if escaped.startswith('\\20') or escaped.endswith('\\20'): + raise InvalidJID('Escaped local part starts or ends with "\\20"') + + _validate_node(escaped) + + return escaped + + +def _unescape_node(node): + """Unescape a local portion of a JID. + + .. note:: + The unescaped local portion is meant ONLY for presentation, + and should not be used for other purposes. + """ + unescaped = [] + seq = '' + for i, char in enumerate(node): + if char == '\\': + seq = node[i:i+3] + if seq not in JID_ESCAPE_SEQUENCES: + seq = '' + if seq: + if len(seq) == 3: + unescaped.append(JID_UNESCAPE_TRANSFORMATIONS.get(seq, char)) + + # Pop character off the escape sequence, and ignore it + seq = seq[1:] + else: + unescaped.append(char) + unescaped = ''.join(unescaped) + + return unescaped + + +def _format_jid(local=None, domain=None, resource=None): + """Format the given JID components into a full or bare JID. + + :param string local: Optional. The local portion of the JID. + :param string domain: Required. The domain name portion of the JID. + :param strin resource: Optional. The resource portion of the JID. + + :return: A full or bare JID string. + """ + result = [] + if local: + result.append(local) + result.append('@') + if domain: + result.append(domain) + if resource: + result.append('/') + result.append(resource) + return ''.join(result) + + +class InvalidJID(ValueError): + """ + Raised when attempting to create a JID that does not pass validation. + + It can also be raised if modifying an existing JID in such a way as + to make it invalid, such trying to remove the domain from an existing + full JID while the local and resource portions still exist. + """ + +# pylint: disable=R0903 +class UnescapedJID(object): + + """ + .. versionadded:: 1.1.10 + """ + + def __init__(self, local, domain, resource): + self._jid = (local, domain, resource) + + # pylint: disable=R0911 + def __getattr__(self, name): + """Retrieve the given JID component. + + :param name: one of: user, server, domain, resource, + full, or bare. + """ + if name == 'resource': + return self._jid[2] or '' + elif name in ('user', 'username', 'local', 'node'): + return self._jid[0] or '' + elif name in ('server', 'domain', 'host'): + return self._jid[1] or '' + elif name in ('full', 'jid'): + return _format_jid(*self._jid) + elif name == 'bare': + return _format_jid(self._jid[0], self._jid[1]) + elif name == '_jid': + return getattr(super(JID, self), '_jid') + else: + return None + + def __str__(self): + """Use the full JID as the string value.""" + return _format_jid(*self._jid) + + def __repr__(self): + """Use the full JID as the representation.""" + return self.__str__() + + +class JID(object): + + """ + A representation of a Jabber ID, or JID. + + Each JID may have three components: a user, a domain, and an optional + resource. For example: user@domain/resource + + When a resource is not used, the JID is called a bare JID. + The JID is a full JID otherwise. + + **JID Properties:** + :jid: Alias for ``full``. + :full: The string value of the full JID. + :bare: The string value of the bare JID. + :user: The username portion of the JID. + :username: Alias for ``user``. + :local: Alias for ``user``. + :node: Alias for ``user``. + :domain: The domain name portion of the JID. + :server: Alias for ``domain``. + :host: Alias for ``domain``. + :resource: The resource portion of the JID. + + :param string jid: + A string of the form ``'[user@]domain[/resource]'``. + :param string local: + Optional. Specify the local, or username, portion + of the JID. If provided, it will override the local + value provided by the `jid` parameter. The given + local value will also be escaped if necessary. + :param string domain: + Optional. Specify the domain of the JID. If + provided, it will override the domain given by + the `jid` parameter. + :param string resource: + Optional. Specify the resource value of the JID. + If provided, it will override the domain given + by the `jid` parameter. + + :raises InvalidJID: + """ + + # pylint: disable=W0212 + def __init__(self, jid=None, **kwargs): + locked = kwargs.get('cache_lock', False) + in_local = kwargs.get('local', None) + in_domain = kwargs.get('domain', None) + in_resource = kwargs.get('resource', None) + parts = None + if in_local or in_domain or in_resource: + parts = (in_local, in_domain, in_resource) + + # only check cache if there is a jid string, or parts, not if there + # are both + self._jid = None + key = None + if (jid is not None) and (parts is None): + if isinstance(jid, JID): + # it's already good to go, and there are no additions + self._jid = jid._jid + return + key = jid + self._jid, locked = JID_CACHE.get(jid, (None, locked)) + elif jid is None and parts is not None: + key = parts + self._jid, locked = JID_CACHE.get(parts, (None, locked)) + if not self._jid: + if not jid: + parsed_jid = (None, None, None) + elif not isinstance(jid, JID): + parsed_jid = _parse_jid(jid) + else: + parsed_jid = jid._jid + + local, domain, resource = parsed_jid + + if 'local' in kwargs: + local = _escape_node(in_local) + if 'domain' in kwargs: + domain = _validate_domain(in_domain) + if 'resource' in kwargs: + resource = _validate_resource(in_resource) + + self._jid = (local, domain, resource) + if key: + _cache(key, self._jid, locked) + + def unescape(self): + """Return an unescaped JID object. + + Using an unescaped JID is preferred for displaying JIDs + to humans, and they should NOT be used for any other + purposes than for presentation. + + :return: :class:`UnescapedJID` + + .. versionadded:: 1.1.10 + """ + return UnescapedJID(_unescape_node(self._jid[0]), + self._jid[1], + self._jid[2]) + + def regenerate(self): + """No-op + + .. deprecated:: 1.1.10 + """ + pass + + def reset(self, data): + """Start fresh from a new JID string. + + :param string data: A string of the form ``'[user@]domain[/resource]'``. + + .. deprecated:: 1.1.10 + """ + self._jid = JID(data)._jid + + @property + def resource(self): + return self._jid[2] or '' + + @property + def user(self): + return self._jid[0] or '' + + @property + def local(self): + return self._jid[0] or '' + + @property + def node(self): + return self._jid[0] or '' + + @property + def username(self): + return self._jid[0] or '' + + @property + def bare(self): + return _format_jid(self._jid[0], self._jid[1]) + + @property + def server(self): + return self._jid[1] or '' + + @property + def domain(self): + return self._jid[1] or '' + + @property + def host(self): + return self._jid[1] or '' + + @property + def full(self): + return _format_jid(*self._jid) + + @property + def jid(self): + return _format_jid(*self._jid) + + @property + def bare(self): + return _format_jid(self._jid[0], self._jid[1]) + + + @resource.setter + def resource(self, value): + self._jid = JID(self, resource=value)._jid + + @user.setter + def user(self, value): + self._jid = JID(self, local=value)._jid + + @username.setter + def username(self, value): + self._jid = JID(self, local=value)._jid + + @local.setter + def local(self, value): + self._jid = JID(self, local=value)._jid + + @node.setter + def node(self, value): + self._jid = JID(self, local=value)._jid + + @server.setter + def server(self, value): + self._jid = JID(self, domain=value)._jid + + @domain.setter + def domain(self, value): + self._jid = JID(self, domain=value)._jid + + @host.setter + def host(self, value): + self._jid = JID(self, domain=value)._jid + + @full.setter + def full(self, value): + self._jid = JID(value)._jid + + @jid.setter + def jid(self, value): + self._jid = JID(value)._jid + + @bare.setter + def bare(self, value): + parsed = JID(value)._jid + self._jid = (parsed[0], parsed[1], self._jid[2]) + + + def __str__(self): + """Use the full JID as the string value.""" + return _format_jid(*self._jid) + + def __repr__(self): + """Use the full JID as the representation.""" + return self.__str__() + + # pylint: disable=W0212 + def __eq__(self, other): + """Two JIDs are equal if they have the same full JID value.""" + if isinstance(other, UnescapedJID): + return False + + other = JID(other) + return self._jid == other._jid + + # pylint: disable=W0212 + def __ne__(self, other): + """Two JIDs are considered unequal if they are not equal.""" + return not self == other + + def __hash__(self): + """Hash a JID based on the string version of its full JID.""" + return hash(self.__str__()) + + def __copy__(self): + """Generate a duplicate JID.""" + return JID(self) + + def __deepcopy__(self, memo): + """Generate a duplicate JID.""" + return JID(deepcopy(str(self), memo)) diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index 1613ac4d..951f31eb 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -11,43 +11,53 @@ from sleekxmpp.plugins.base import register_plugin, load_plugin __all__ = [ - # Non-standard - 'gmail_notify', # Gmail searching and notifications - # XEPS 'xep_0004', # Data Forms 'xep_0009', # Jabber-RPC 'xep_0012', # Last Activity + 'xep_0013', # Flexible Offline Message Retrieval + 'xep_0016', # Privacy Lists + 'xep_0020', # Feature Negotiation 'xep_0027', # Current Jabber OpenPGP Usage 'xep_0030', # Service Discovery 'xep_0033', # Extended Stanza Addresses 'xep_0045', # Multi-User Chat (Client) 'xep_0047', # In-Band Bytestreams + 'xep_0048', # Bookmarks + 'xep_0049', # Private XML Storage 'xep_0050', # Ad-hoc Commands 'xep_0054', # vcard-temp 'xep_0059', # Result Set Management 'xep_0060', # Pubsub (Client) 'xep_0065', # SOCKS5 Bytestreams 'xep_0066', # Out of Band Data + 'xep_0071', # XHTML-IM 'xep_0077', # In-Band Registration # 'xep_0078', # Non-SASL auth. Don't automatically load + 'xep_0079', # Advanced Message Processing 'xep_0080', # User Location 'xep_0082', # XMPP Date and Time Profiles 'xep_0084', # User Avatar 'xep_0085', # Chat State Notifications 'xep_0086', # Legacy Error Codes + 'xep_0091', # Legacy Delayed Delivery 'xep_0092', # Software Version + 'xep_0106', # JID Escaping 'xep_0107', # User Mood 'xep_0108', # User Activity 'xep_0115', # Entity Capabilities 'xep_0118', # User Tune 'xep_0128', # Extended Service Discovery + 'xep_0131', # Standard Headers and Internet Metadata + 'xep_0133', # Service Administration + 'xep_0152', # Reachability Addresses 'xep_0153', # vCard-Based Avatars 'xep_0163', # Personal Eventing Protocol 'xep_0172', # User Nickname 'xep_0184', # Message Receipts 'xep_0186', # Invisible Command - 'xep_0191', # Simple Communications Blocking + 'xep_0191', # Blocking Command + 'xep_0196', # User Gaming 'xep_0198', # Stream Management 'xep_0199', # Ping 'xep_0202', # Entity Time @@ -57,9 +67,20 @@ __all__ = [ 'xep_0223', # Persistent Storage of Private Data via Pubsub 'xep_0224', # Attention 'xep_0231', # Bits of Binary + 'xep_0235', # OAuth Over XMPP + 'xep_0242', # XMPP Client Compliance 2009 'xep_0249', # Direct MUC Invitations 'xep_0256', # Last Activity in Presence + 'xep_0257', # Client Certificate Management for SASL EXTERNAL 'xep_0258', # Security Labels in XMPP 'xep_0270', # XMPP Compliance Suites 2010 + 'xep_0279', # Server IP Check + 'xep_0280', # Message Carbons + 'xep_0297', # Stanza Forwarding 'xep_0302', # XMPP Compliance Suites 2012 + 'xep_0308', # Last Message Correction + 'xep_0313', # Message Archive Management + 'xep_0319', # Last User Interaction in Presence + 'xep_0323', # IoT Systems Sensor Data + 'xep_0325', # IoT Systems Control ] diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py index 26f0c827..67675908 100644 --- a/sleekxmpp/plugins/base.py +++ b/sleekxmpp/plugins/base.py @@ -14,6 +14,7 @@ """ import sys +import copy import logging import threading @@ -272,6 +273,14 @@ class BasePlugin(object): #: be initialized as needed if this plugin is enabled. dependencies = set() + #: The basic, standard configuration for the plugin, which may + #: be overridden when initializing the plugin. The configuration + #: fields included here may be accessed directly as attributes of + #: the plugin. For example, including the configuration field 'foo' + #: would mean accessing `plugin.foo` returns the current value of + #: `plugin.config['foo']`. + default_config = {} + def __init__(self, xmpp, config=None): self.xmpp = xmpp if self.xmpp: @@ -279,7 +288,32 @@ class BasePlugin(object): #: A plugin's behaviour may be configurable, in which case those #: configuration settings will be provided as a dictionary. - self.config = config if config is not None else {} + self.config = copy.copy(self.default_config) + if config: + self.config.update(config) + + def __getattr__(self, key): + """Provide direct access to configuration fields. + + If the standard configuration includes the option `'foo'`, then + accessing `self.foo` should be the same as `self.config['foo']`. + """ + if key in self.default_config: + return self.config.get(key, None) + else: + return object.__getattribute__(self, key) + + def __setattr__(self, key, value): + """Provide direct assignment to configuration fields. + + If the standard configuration includes the option `'foo'`, then + assigning to `self.foo` should be the same as assigning to + `self.config['foo']`. + """ + if key in self.default_config: + self.config[key] = value + else: + super(BasePlugin, self).__setattr__(key, value) def _init(self): """Initialize plugin state, such as registering event handlers. diff --git a/sleekxmpp/plugins/google/__init__.py b/sleekxmpp/plugins/google/__init__.py new file mode 100644 index 00000000..bd7ca123 --- /dev/null +++ b/sleekxmpp/plugins/google/__init__.py @@ -0,0 +1,47 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin, BasePlugin + +from sleekxmpp.plugins.google.gmail import Gmail +from sleekxmpp.plugins.google.auth import GoogleAuth +from sleekxmpp.plugins.google.settings import GoogleSettings +from sleekxmpp.plugins.google.nosave import GoogleNoSave + + +class Google(BasePlugin): + + """ + Google: Custom GTalk Features + + Also see: <https://developers.google.com/talk/jep_extensions/extensions> + """ + + name = 'google' + description = 'Google: Custom GTalk Features' + dependencies = set([ + 'gmail', + 'google_settings', + 'google_nosave', + 'google_auth' + ]) + + def __getitem__(self, attr): + if attr in ('settings', 'nosave', 'auth'): + return self.xmpp['google_%s' % attr] + elif attr == 'gmail': + return self.xmpp['gmail'] + else: + raise KeyError(attr) + + +register_plugin(Gmail) +register_plugin(GoogleAuth) +register_plugin(GoogleSettings) +register_plugin(GoogleNoSave) +register_plugin(Google) diff --git a/sleekxmpp/plugins/google/auth/__init__.py b/sleekxmpp/plugins/google/auth/__init__.py new file mode 100644 index 00000000..5a8feb0d --- /dev/null +++ b/sleekxmpp/plugins/google/auth/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.google.auth import stanza +from sleekxmpp.plugins.google.auth.auth import GoogleAuth diff --git a/sleekxmpp/plugins/google/auth/auth.py b/sleekxmpp/plugins/google/auth/auth.py new file mode 100644 index 00000000..042bd404 --- /dev/null +++ b/sleekxmpp/plugins/google/auth/auth.py @@ -0,0 +1,52 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.google.auth import stanza + + +log = logging.getLogger(__name__) + + +class GoogleAuth(BasePlugin): + + """ + Google: Auth Extensions (JID Domain Discovery, OAuth2) + + Also see: + <https://developers.google.com/talk/jep_extensions/jid_domain_change> + <https://developers.google.com/talk/jep_extensions/oauth> + """ + + name = 'google_auth' + description = 'Google: Auth Extensions (JID Domain Discovery, OAuth2)' + dependencies = set(['feature_mechanisms']) + stanza = stanza + + def plugin_init(self): + self.xmpp.namespace_map['http://www.google.com/talk/protocol/auth'] = 'ga' + + register_stanza_plugin(self.xmpp['feature_mechanisms'].stanza.Auth, + stanza.GoogleAuth) + + self.xmpp.add_filter('out', self._auth) + + def plugin_end(self): + self.xmpp.del_filter('out', self._auth) + + def _auth(self, stanza): + if isinstance(stanza, self.xmpp['feature_mechanisms'].stanza.Auth): + stanza.stream = self.xmpp + stanza['google']['client_uses_full_bind_result'] = True + if stanza['mechanism'] == 'X-OAUTH2': + stanza['google']['service'] = 'oauth2' + print(stanza) + return stanza diff --git a/sleekxmpp/plugins/google/auth/stanza.py b/sleekxmpp/plugins/google/auth/stanza.py new file mode 100644 index 00000000..2d13f85a --- /dev/null +++ b/sleekxmpp/plugins/google/auth/stanza.py @@ -0,0 +1,49 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class GoogleAuth(ElementBase): + name = 'auth' + namespace = 'http://www.google.com/talk/protocol/auth' + plugin_attrib = 'google' + interfaces = set(['client_uses_full_bind_result', 'service']) + + discovery_attr= '{%s}client-uses-full-bind-result' % namespace + service_attr= '{%s}service' % namespace + + def setup(self, xml): + """Don't create XML for the plugin.""" + self.xml = ET.Element('') + print('setting up google extension') + + def get_client_uses_full_bind_result(self): + return self.parent()._get_attr(self.discovery_attr) == 'true' + + def set_client_uses_full_bind_result(self, value): + print('>>>', value) + if value in (True, 'true'): + self.parent()._set_attr(self.discovery_attr, 'true') + else: + self.parent()._del_attr(self.discovery_attr) + + def del_client_uses_full_bind_result(self): + self.parent()._del_attr(self.discovery_attr) + + def get_service(self): + return self.parent()._get_attr(self.service_attr, '') + + def set_service(self, value): + if value: + self.parent()._set_attr(self.service_attr, value) + else: + self.parent()._del_attr(self.service_attr) + + def del_service(self): + self.parent()._del_attr(self.service_attr) diff --git a/sleekxmpp/plugins/google/gmail/__init__.py b/sleekxmpp/plugins/google/gmail/__init__.py new file mode 100644 index 00000000..a92e363b --- /dev/null +++ b/sleekxmpp/plugins/google/gmail/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.google.gmail import stanza +from sleekxmpp.plugins.google.gmail.notifications import Gmail diff --git a/sleekxmpp/plugins/google/gmail/notifications.py b/sleekxmpp/plugins/google/gmail/notifications.py new file mode 100644 index 00000000..e65b2ca7 --- /dev/null +++ b/sleekxmpp/plugins/google/gmail/notifications.py @@ -0,0 +1,96 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import Iq +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.google.gmail import stanza + + +log = logging.getLogger(__name__) + + +class Gmail(BasePlugin): + + """ + Google: Gmail Notifications + + Also see <https://developers.google.com/talk/jep_extensions/gmail>. + """ + + name = 'gmail' + description = 'Google: Gmail Notifications' + dependencies = set() + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, stanza.GmailQuery) + register_stanza_plugin(Iq, stanza.MailBox) + register_stanza_plugin(Iq, stanza.NewMail) + + self.xmpp.register_handler( + Callback('Gmail New Mail', + MatchXPath('{%s}iq/{%s}%s' % ( + self.xmpp.default_ns, + stanza.NewMail.namespace, + stanza.NewMail.name)), + self._handle_new_mail)) + + self._last_result_time = None + self._last_result_tid = None + + def plugin_end(self): + self.xmpp.remove_handler('Gmail New Mail') + + def _handle_new_mail(self, iq): + log.info('Gmail: New email!') + iq.reply().send() + self.xmpp.event('gmail_notification') + + def check(self, block=True, timeout=None, callback=None): + last_time = self._last_result_time + last_tid = self._last_result_tid + + if not block: + callback = lambda iq: self._update_last_results(iq, callback) + + resp = self.search(newer_time=last_time, + newer_tid=last_tid, + block=block, + timeout=timeout, + callback=callback) + + if block: + self._update_last_results(resp) + return resp + + def _update_last_results(self, iq, callback=None): + self._last_result_time = iq['gmail_messages']['result_time'] + threads = iq['gmail_messages']['threads'] + if threads: + self._last_result_tid = threads[0]['tid'] + if callback: + callback(iq) + + def search(self, query=None, newer_time=None, newer_tid=None, block=True, + timeout=None, callback=None): + if not query: + log.info('Gmail: Checking for new email') + else: + log.info('Gmail: Searching for emails matching: "%s"', query) + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['to'] = self.xmpp.boundjid.bare + iq['gmail']['search'] = query + iq['gmail']['newer_than_time'] = newer_time + iq['gmail']['newer_than_tid'] = newer_tid + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/google/gmail/stanza.py b/sleekxmpp/plugins/google/gmail/stanza.py new file mode 100644 index 00000000..e7e308e1 --- /dev/null +++ b/sleekxmpp/plugins/google/gmail/stanza.py @@ -0,0 +1,101 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin + + +class GmailQuery(ElementBase): + namespace = 'google:mail:notify' + name = 'query' + plugin_attrib = 'gmail' + interfaces = set(['newer_than_time', 'newer_than_tid', 'search']) + + def get_search(self): + return self._get_attr('q', '') + + def set_search(self, search): + self._set_attr('q', search) + + def del_search(self): + self._del_attr('q') + + def get_newer_than_time(self): + return self._get_attr('newer-than-time', '') + + def set_newer_than_time(self, value): + self._set_attr('newer-than-time', value) + + def del_newer_than_time(self): + self._del_attr('newer-than-time') + + def get_newer_than_tid(self): + return self._get_attr('newer-than-tid', '') + + def set_newer_than_tid(self, value): + self._set_attr('newer-than-tid', value) + + def del_newer_than_tid(self): + self._del_attr('newer-than-tid') + + +class MailBox(ElementBase): + namespace = 'google:mail:notify' + name = 'mailbox' + plugin_attrib = 'gmail_messages' + interfaces = set(['result_time', 'url', 'matched', 'estimate']) + + def get_matched(self): + return self._get_attr('total-matched', '') + + def get_estimate(self): + return self._get_attr('total-estimate', '') == '1' + + def get_result_time(self): + return self._get_attr('result-time', '') + + +class MailThread(ElementBase): + namespace = 'google:mail:notify' + name = 'mail-thread-info' + plugin_attrib = 'thread' + plugin_multi_attrib = 'threads' + interfaces = set(['tid', 'participation', 'messages', 'date', + 'senders', 'url', 'labels', 'subject', 'snippet']) + sub_interfaces = set(['labels', 'subject', 'snippet']) + + def get_senders(self): + result = [] + senders = self.xml.findall('{%s}senders/{%s}sender' % ( + self.namespace, self.namespace)) + + for sender in senders: + result.append(MailSender(xml=sender)) + + return result + + +class MailSender(ElementBase): + namespace = 'google:mail:notify' + name = 'sender' + plugin_attrib = name + interfaces = set(['address', 'name', 'originator', 'unread']) + + def get_originator(self): + return self.xml.attrib.get('originator', '0') == '1' + + def get_unread(self): + return self.xml.attrib.get('unread', '0') == '1' + + +class NewMail(ElementBase): + namespace = 'google:mail:notify' + name = 'new-mail' + plugin_attrib = 'gmail_notification' + + +register_stanza_plugin(MailBox, MailThread, iterable=True) diff --git a/sleekxmpp/plugins/google/nosave/__init__.py b/sleekxmpp/plugins/google/nosave/__init__.py new file mode 100644 index 00000000..57847af5 --- /dev/null +++ b/sleekxmpp/plugins/google/nosave/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.google.nosave import stanza +from sleekxmpp.plugins.google.nosave.nosave import GoogleNoSave diff --git a/sleekxmpp/plugins/google/nosave/nosave.py b/sleekxmpp/plugins/google/nosave/nosave.py new file mode 100644 index 00000000..d6bef615 --- /dev/null +++ b/sleekxmpp/plugins/google/nosave/nosave.py @@ -0,0 +1,83 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import Iq, Message +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.google.nosave import stanza + + +log = logging.getLogger(__name__) + + +class GoogleNoSave(BasePlugin): + + """ + Google: Off the Record Chats + + NOTE: This is NOT an encryption method. + + Also see <https://developers.google.com/talk/jep_extensions/otr>. + """ + + name = 'google_nosave' + description = 'Google: Off the Record Chats' + dependencies = set(['google_settings']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, stanza.NoSave) + register_stanza_plugin(Iq, stanza.NoSaveQuery) + + self.xmpp.register_handler( + Callback('Google Nosave', + StanzaPath('iq@type=set/google_nosave'), + self._handle_nosave_change)) + + def plugin_end(self): + self.xmpp.remove_handler('Google Nosave') + + def enable(self, jid=None, block=True, timeout=None, callback=None): + if jid is None: + self.xmpp['google_settings'].update({'archiving_enabled': False}, + block=block, timeout=timeout, callback=callback) + else: + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['google_nosave']['item']['jid'] = jid + iq['google_nosave']['item']['value'] = True + return iq.send(block=block, timeout=timeout, callback=callback) + + def disable(self, jid=None, block=True, timeout=None, callback=None): + if jid is None: + self.xmpp['google_settings'].update({'archiving_enabled': True}, + block=block, timeout=timeout, callback=callback) + else: + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['google_nosave']['item']['jid'] = jid + iq['google_nosave']['item']['value'] = False + return iq.send(block=block, timeout=timeout, callback=callback) + + def get(self, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq.enable('google_nosave') + return iq.send(block=block, timeout=timeout, callback=callback) + + def _handle_nosave_change(self, iq): + reply = self.xmpp.Iq() + reply['type'] = 'result' + reply['id'] = iq['id'] + reply['to'] = iq['from'] + reply.send() + self.xmpp.event('google_nosave_change', iq) diff --git a/sleekxmpp/plugins/google/nosave/stanza.py b/sleekxmpp/plugins/google/nosave/stanza.py new file mode 100644 index 00000000..791d4b0c --- /dev/null +++ b/sleekxmpp/plugins/google/nosave/stanza.py @@ -0,0 +1,59 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.jid import JID +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin + + +class NoSave(ElementBase): + name = 'x' + namespace = 'google:nosave' + plugin_attrib = 'google_nosave' + interfaces = set(['value']) + + def get_value(self): + return self._get_attr('value', '') == 'enabled' + + def set_value(self, value): + self._set_attr('value', 'enabled' if value else 'disabled') + + +class NoSaveQuery(ElementBase): + name = 'query' + namespace = 'google:nosave' + plugin_attrib = 'google_nosave' + interfaces = set() + + +class Item(ElementBase): + name = 'item' + namespace = 'google:nosave' + plugin_attrib = 'item' + plugin_multi_attrib = 'items' + interfaces = set(['jid', 'source', 'value']) + + def get_value(self): + return self._get_attr('value', '') == 'enabled' + + def set_value(self, value): + self._set_attr('value', 'enabled' if value else 'disabled') + + def get_jid(self): + return JID(self._get_attr('jid', '')) + + def set_jid(self, value): + self._set_attr('jid', str(value)) + + def get_source(self): + return JID(self._get_attr('source', '')) + + def set_source(self, value): + self._set_attr('source', str(value)) + + +register_stanza_plugin(NoSaveQuery, Item) diff --git a/sleekxmpp/plugins/google/settings/__init__.py b/sleekxmpp/plugins/google/settings/__init__.py new file mode 100644 index 00000000..c3a0471d --- /dev/null +++ b/sleekxmpp/plugins/google/settings/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.google.settings import stanza +from sleekxmpp.plugins.google.settings.settings import GoogleSettings diff --git a/sleekxmpp/plugins/google/settings/settings.py b/sleekxmpp/plugins/google/settings/settings.py new file mode 100644 index 00000000..591956fc --- /dev/null +++ b/sleekxmpp/plugins/google/settings/settings.py @@ -0,0 +1,63 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.stanza import Iq +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.google.settings import stanza + + +class GoogleSettings(BasePlugin): + + """ + Google: Gmail Notifications + + Also see <https://developers.google.com/talk/jep_extensions/usersettings>. + """ + + name = 'google_settings' + description = 'Google: User Settings' + dependencies = set() + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, stanza.UserSettings) + + self.xmpp.register_handler( + Callback('Google Settings', + StanzaPath('iq@type=set/google_settings'), + self._handle_settings_change)) + + def plugin_end(self): + self.xmpp.remove_handler('Google Settings') + + def get(self, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq.enable('google_settings') + return iq.send(block=block, timeout=timeout, callback=callback) + + def update(self, settings, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq.enable('google_settings') + + for setting, value in settings.items(): + iq['google_settings'][setting] = value + + return iq.send(block=block, timeout=timeout, callback=callback) + + def _handle_settings_change(self, iq): + reply = self.xmpp.Iq() + reply['type'] = 'result' + reply['id'] = iq['id'] + reply['to'] = iq['from'] + reply.send() + self.xmpp.event('google_settings_change', iq) diff --git a/sleekxmpp/plugins/google/settings/stanza.py b/sleekxmpp/plugins/google/settings/stanza.py new file mode 100644 index 00000000..d8161770 --- /dev/null +++ b/sleekxmpp/plugins/google/settings/stanza.py @@ -0,0 +1,110 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin + + +class UserSettings(ElementBase): + name = 'usersetting' + namespace = 'google:setting' + plugin_attrib = 'google_settings' + interfaces = set(['auto_accept_suggestions', + 'mail_notifications', + 'archiving_enabled', + 'gmail', + 'email_verified', + 'domain_privacy_notice', + 'display_name']) + + def _get_setting(self, setting): + xml = self.xml.find('{%s}%s' % (self.namespace, setting)) + if xml is not None: + return xml.attrib.get('value', '') == 'true' + return False + + def _set_setting(self, setting, value): + self._del_setting(setting) + if value in (True, False): + xml = ET.Element('{%s}%s' % (self.namespace, setting)) + xml.attrib['value'] = 'true' if value else 'false' + self.xml.append(xml) + + def _del_setting(self, setting): + xml = self.xml.find('{%s}%s' % (self.namespace, setting)) + if xml is not None: + self.xml.remove(xml) + + def get_display_name(self): + xml = self.xml.find('{%s}%s' % (self.namespace, 'displayname')) + if xml is not None: + return xml.attrib.get('value', '') + return '' + + def set_display_name(self, value): + self._del_setting(setting) + if value: + xml = ET.Element('{%s}%s' % (self.namespace, 'displayname')) + xml.attrib['value'] = value + self.xml.append(xml) + + def del_display_name(self): + self._del_setting('displayname') + + def get_auto_accept_suggestions(self): + return self._get_setting('autoacceptsuggestions') + + def get_mail_notifications(self): + return self._get_setting('mailnotifications') + + def get_archiving_enabled(self): + return self._get_setting('archivingenabled') + + def get_gmail(self): + return self._get_setting('gmail') + + def get_email_verified(self): + return self._get_setting('emailverified') + + def get_domain_privacy_notice(self): + return self._get_setting('domainprivacynotice') + + def set_auto_accept_suggestions(self, value): + self._set_setting('autoacceptsuggestions', value) + + def set_mail_notifications(self, value): + self._set_setting('mailnotifications', value) + + def set_archiving_enabled(self, value): + self._set_setting('archivingenabled', value) + + def set_gmail(self, value): + self._set_setting('gmail', value) + + def set_email_verified(self, value): + self._set_setting('emailverified', value) + + def set_domain_privacy_notice(self, value): + self._set_setting('domainprivacynotice', value) + + def del_auto_accept_suggestions(self): + self._del_setting('autoacceptsuggestions') + + def del_mail_notifications(self): + self._del_setting('mailnotifications') + + def del_archiving_enabled(self): + self._del_setting('archivingenabled') + + def del_gmail(self): + self._del_setting('gmail') + + def del_email_verified(self): + self._del_setting('emailverified') + + def del_domain_privacy_notice(self): + self._del_setting('domainprivacynotice') diff --git a/sleekxmpp/plugins/jobs.py b/sleekxmpp/plugins/jobs.py deleted file mode 100644 index cb9deba8..00000000 --- a/sleekxmpp/plugins/jobs.py +++ /dev/null @@ -1,49 +0,0 @@ -from . import base -import logging -from xml.etree import cElementTree as ET - - -log = logging.getLogger(__name__) - - -class jobs(base.base_plugin): - def plugin_init(self): - self.xep = 'pubsubjob' - self.description = "Job distribution over Pubsub" - - def post_init(self): - pass - #TODO add event - - def createJobNode(self, host, jid, node, config=None): - pass - - def createJob(self, host, node, jobid=None, payload=None): - return self.xmpp.plugin['xep_0060'].setItem(host, node, ((jobid, payload),)) - - def claimJob(self, host, node, jobid, ifrom=None): - return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}claimed')) - - def unclaimJob(self, host, node, jobid): - return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}unclaimed')) - - def finishJob(self, host, node, jobid, payload=None): - finished = ET.Element('{http://andyet.net/protocol/pubsubjob}finished') - if payload is not None: - finished.append(payload) - return self._setState(host, node, jobid, finished) - - def _setState(self, host, node, jobid, state, ifrom=None): - iq = self.xmpp.Iq() - iq['to'] = host - if ifrom: iq['from'] = ifrom - iq['type'] = 'set' - iq['psstate']['node'] = node - iq['psstate']['item'] = jobid - iq['psstate']['payload'] = state - result = iq.send() - if result is None or type(result) == bool or result['type'] != 'result': - log.error("Unable to change %s:%s to %s", node, jobid, state) - return False - return True - diff --git a/sleekxmpp/plugins/old_0004.py b/sleekxmpp/plugins/old_0004.py deleted file mode 100644 index 7f086866..00000000 --- a/sleekxmpp/plugins/old_0004.py +++ /dev/null @@ -1,421 +0,0 @@ -""" - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. - - See the file LICENSE for copying permission. -""" -from . import base -import logging -from xml.etree import cElementTree as ET -import copy -import logging -#TODO support item groups and results - - -log = logging.getLogger(__name__) - - -class old_0004(base.base_plugin): - - def plugin_init(self): - self.xep = '0004' - self.description = '*Deprecated Data Forms' - self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform, name='Old Message Form') - - def post_init(self): - base.base_plugin.post_init(self) - self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') - log.warning("This implementation of XEP-0004 is deprecated.") - - def handler_message_xform(self, xml): - object = self.handle_form(xml) - self.xmpp.event("message_form", object) - - def handler_presence_xform(self, xml): - object = self.handle_form(xml) - self.xmpp.event("presence_form", object) - - def handle_form(self, xml): - xmlform = xml.find('{jabber:x:data}x') - object = self.buildForm(xmlform) - self.xmpp.event("message_xform", object) - return object - - def buildForm(self, xml): - form = Form(ftype=xml.attrib['type']) - form.fromXML(xml) - return form - - def makeForm(self, ftype='form', title='', instructions=''): - return Form(self.xmpp, ftype, title, instructions) - -class FieldContainer(object): - def __init__(self, stanza = 'form'): - self.fields = [] - self.field = {} - self.stanza = stanza - - def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None): - self.field[var] = FormField(var, ftype, label, desc, required, value) - self.fields.append(self.field[var]) - return self.field[var] - - def buildField(self, xml): - self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single')) - self.fields.append(self.field[xml.get('var', '__unnamed__')]) - self.field[xml.get('var', '__unnamed__')].buildField(xml) - - def buildContainer(self, xml): - self.stanza = xml.tag - for field in xml.findall('{jabber:x:data}field'): - self.buildField(field) - - def getXML(self, ftype): - container = ET.Element(self.stanza) - for field in self.fields: - container.append(field.getXML(ftype)) - return container - -class Form(FieldContainer): - types = ('form', 'submit', 'cancel', 'result') - def __init__(self, xmpp=None, ftype='form', title='', instructions=''): - if not ftype in self.types: - raise ValueError("Invalid Form Type") - FieldContainer.__init__(self) - self.xmpp = xmpp - self.type = ftype - self.title = title - self.instructions = instructions - self.reported = [] - self.items = [] - - def merge(self, form2): - form1 = Form(ftype=self.type) - form1.fromXML(self.getXML(self.type)) - for field in form2.fields: - if not field.var in form1.field: - form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value) - else: - form1.field[field.var].value = field.value - for option, label in field.options: - if (option, label) not in form1.field[field.var].options: - form1.fields[field.var].addOption(option, label) - return form1 - - def copy(self): - newform = Form(ftype=self.type) - newform.fromXML(self.getXML(self.type)) - return newform - - def update(self, form): - values = form.getValues() - for var in values: - if var in self.fields: - self.fields[var].setValue(self.fields[var]) - - def getValues(self): - result = {} - for field in self.fields: - value = field.value - if len(value) == 1: - value = value[0] - result[field.var] = value - return result - - def setValues(self, values={}): - for field in values: - if field in self.field: - if isinstance(values[field], list) or isinstance(values[field], tuple): - for value in values[field]: - self.field[field].setValue(value) - else: - self.field[field].setValue(values[field]) - - def fromXML(self, xml): - self.buildForm(xml) - - def addItem(self): - newitem = FieldContainer('item') - self.items.append(newitem) - return newitem - - def buildItem(self, xml): - newitem = self.addItem() - newitem.buildContainer(xml) - - def addReported(self): - reported = FieldContainer('reported') - self.reported.append(reported) - return reported - - def buildReported(self, xml): - reported = self.addReported() - reported.buildContainer(xml) - - def setTitle(self, title): - self.title = title - - def setInstructions(self, instructions): - self.instructions = instructions - - def setType(self, ftype): - self.type = ftype - - def getXMLMessage(self, to): - msg = self.xmpp.makeMessage(to) - msg.append(self.getXML()) - return msg - - def buildForm(self, xml): - self.type = xml.get('type', 'form') - if xml.find('{jabber:x:data}title') is not None: - self.setTitle(xml.find('{jabber:x:data}title').text) - if xml.find('{jabber:x:data}instructions') is not None: - self.setInstructions(xml.find('{jabber:x:data}instructions').text) - for field in xml.findall('{jabber:x:data}field'): - self.buildField(field) - for reported in xml.findall('{jabber:x:data}reported'): - self.buildReported(reported) - for item in xml.findall('{jabber:x:data}item'): - self.buildItem(item) - - #def getXML(self, tostring = False): - def getXML(self, ftype=None): - if ftype: - self.type = ftype - form = ET.Element('{jabber:x:data}x') - form.attrib['type'] = self.type - if self.title and self.type in ('form', 'result'): - title = ET.Element('{jabber:x:data}title') - title.text = self.title - form.append(title) - if self.instructions and self.type == 'form': - instructions = ET.Element('{jabber:x:data}instructions') - instructions.text = self.instructions - form.append(instructions) - for field in self.fields: - form.append(field.getXML(self.type)) - for reported in self.reported: - form.append(reported.getXML('{jabber:x:data}reported')) - for item in self.items: - form.append(item.getXML(self.type)) - #if tostring: - # form = self.xmpp.tostring(form) - return form - - def getXHTML(self): - form = ET.Element('{http://www.w3.org/1999/xhtml}form') - if self.title: - title = ET.Element('h2') - title.text = self.title - form.append(title) - if self.instructions: - instructions = ET.Element('p') - instructions.text = self.instructions - form.append(instructions) - for field in self.fields: - form.append(field.getXHTML()) - for field in self.reported: - form.append(field.getXHTML()) - for field in self.items: - form.append(field.getXHTML()) - return form - - - def makeSubmit(self): - self.setType('submit') - -class FormField(object): - types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single') - listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single') - lbtypes = ('fixed', 'text-multi') - def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None): - if not ftype in self.types: - raise ValueError("Invalid Field Type") - self.type = ftype - self.var = var - self.label = label - self.desc = desc - self.options = [] - self.required = False - self.value = [] - if self.type in self.listtypes: - self.islist = True - else: - self.islist = False - if self.type in self.lbtypes: - self.islinebreak = True - else: - self.islinebreak = False - if value: - self.setValue(value) - - def addOption(self, value, label): - if self.islist: - self.options.append((value, label)) - else: - raise ValueError("Cannot add options to non-list type field.") - - def setTrue(self): - if self.type == 'boolean': - self.value = [True] - - def setFalse(self): - if self.type == 'boolean': - self.value = [False] - - def require(self): - self.required = True - - def setDescription(self, desc): - self.desc = desc - - def setValue(self, value): - if self.type == 'boolean': - if value in ('1', 1, True, 'true', 'True', 'yes'): - value = True - else: - value = False - if self.islinebreak and value is not None: - self.value += value.split('\n') - else: - if len(self.value) and (not self.islist or self.type == 'list-single'): - self.value = [value] - else: - self.value.append(value) - - def delValue(self, value): - if type(self.value) == type([]): - try: - idx = self.value.index(value) - if idx != -1: - self.value.pop(idx) - except ValueError: - pass - else: - self.value = '' - - def setAnswer(self, value): - self.setValue(value) - - def buildField(self, xml): - self.type = xml.get('type', 'text-single') - self.label = xml.get('label', '') - for option in xml.findall('{jabber:x:data}option'): - self.addOption(option.find('{jabber:x:data}value').text, option.get('label', '')) - for value in xml.findall('{jabber:x:data}value'): - self.setValue(value.text) - if xml.find('{jabber:x:data}required') is not None: - self.require() - if xml.find('{jabber:x:data}desc') is not None: - self.setDescription(xml.find('{jabber:x:data}desc').text) - - def getXML(self, ftype): - field = ET.Element('{jabber:x:data}field') - if ftype != 'result': - field.attrib['type'] = self.type - if self.type != 'fixed': - if self.var: - field.attrib['var'] = self.var - if self.label: - field.attrib['label'] = self.label - if ftype == 'form': - for option in self.options: - optionxml = ET.Element('{jabber:x:data}option') - optionxml.attrib['label'] = option[1] - optionval = ET.Element('{jabber:x:data}value') - optionval.text = option[0] - optionxml.append(optionval) - field.append(optionxml) - if self.required: - required = ET.Element('{jabber:x:data}required') - field.append(required) - if self.desc: - desc = ET.Element('{jabber:x:data}desc') - desc.text = self.desc - field.append(desc) - for value in self.value: - valuexml = ET.Element('{jabber:x:data}value') - if value is True or value is False: - if value: - valuexml.text = '1' - else: - valuexml.text = '0' - else: - valuexml.text = value - field.append(valuexml) - return field - - def getXHTML(self): - field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type}) - if self.label: - label = ET.Element('p') - label.text = "%s: " % self.label - else: - label = ET.Element('p') - label.text = "%s: " % self.var - field.append(label) - if self.type == 'boolean': - formf = ET.Element('input', {'type': 'checkbox', 'name': self.var}) - if len(self.value) and self.value[0] in (True, 'true', '1'): - formf.attrib['checked'] = 'checked' - elif self.type == 'fixed': - formf = ET.Element('p') - try: - formf.text = ', '.join(self.value) - except: - pass - field.append(formf) - formf = ET.Element('input', {'type': 'hidden', 'name': self.var}) - try: - formf.text = ', '.join(self.value) - except: - pass - elif self.type == 'hidden': - formf = ET.Element('input', {'type': 'hidden', 'name': self.var}) - try: - formf.text = ', '.join(self.value) - except: - pass - elif self.type in ('jid-multi', 'list-multi'): - formf = ET.Element('select', {'name': self.var}) - for option in self.options: - optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'}) - optf.text = option[1] - if option[1] in self.value: - optf.attrib['selected'] = 'selected' - formf.append(option) - elif self.type in ('jid-single', 'text-single'): - formf = ET.Element('input', {'type': 'text', 'name': self.var}) - try: - formf.attrib['value'] = ', '.join(self.value) - except: - pass - elif self.type == 'list-single': - formf = ET.Element('select', {'name': self.var}) - for option in self.options: - optf = ET.Element('option', {'value': option[0]}) - optf.text = option[1] - if not optf.text: - optf.text = option[0] - if option[1] in self.value: - optf.attrib['selected'] = 'selected' - formf.append(optf) - elif self.type == 'text-multi': - formf = ET.Element('textarea', {'name': self.var}) - try: - formf.text = ', '.join(self.value) - except: - pass - if not formf.text: - formf.text = ' ' - elif self.type == 'text-private': - formf = ET.Element('input', {'type': 'password', 'name': self.var}) - try: - formf.attrib['value'] = ', '.join(self.value) - except: - pass - label.append(formf) - return field - diff --git a/sleekxmpp/plugins/old_0009.py b/sleekxmpp/plugins/old_0009.py deleted file mode 100644 index 625b03fb..00000000 --- a/sleekxmpp/plugins/old_0009.py +++ /dev/null @@ -1,277 +0,0 @@ -"""
-XEP-0009 XMPP Remote Procedure Calls
-"""
-from __future__ import with_statement
-from . import base
-import logging
-from xml.etree import cElementTree as ET
-import copy
-import time
-import base64
-
-def py2xml(*args):
- params = ET.Element("params")
- for x in args:
- param = ET.Element("param")
- param.append(_py2xml(x))
- params.append(param) #<params><param>...
- return params
-
-def _py2xml(*args):
- for x in args:
- val = ET.Element("value")
- if type(x) is int:
- i4 = ET.Element("i4")
- i4.text = str(x)
- val.append(i4)
- if type(x) is bool:
- boolean = ET.Element("boolean")
- boolean.text = str(int(x))
- val.append(boolean)
- elif type(x) is str:
- string = ET.Element("string")
- string.text = x
- val.append(string)
- elif type(x) is float:
- double = ET.Element("double")
- double.text = str(x)
- val.append(double)
- elif type(x) is rpcbase64:
- b64 = ET.Element("Base64")
- b64.text = x.encoded()
- val.append(b64)
- elif type(x) is rpctime:
- iso = ET.Element("dateTime.iso8601")
- iso.text = str(x)
- val.append(iso)
- elif type(x) is list:
- array = ET.Element("array")
- data = ET.Element("data")
- for y in x:
- data.append(_py2xml(y))
- array.append(data)
- val.append(array)
- elif type(x) is dict:
- struct = ET.Element("struct")
- for y in x.keys():
- member = ET.Element("member")
- name = ET.Element("name")
- name.text = y
- member.append(name)
- member.append(_py2xml(x[y]))
- struct.append(member)
- val.append(struct)
- return val
-
-def xml2py(params):
- vals = []
- for param in params.findall('param'):
- vals.append(_xml2py(param.find('value')))
- return vals
-
-def _xml2py(value):
- if value.find('i4') is not None:
- return int(value.find('i4').text)
- if value.find('int') is not None:
- return int(value.find('int').text)
- if value.find('boolean') is not None:
- return bool(value.find('boolean').text)
- if value.find('string') is not None:
- return value.find('string').text
- if value.find('double') is not None:
- return float(value.find('double').text)
- if value.find('Base64') is not None:
- return rpcbase64(value.find('Base64').text)
- if value.find('dateTime.iso8601') is not None:
- return rpctime(value.find('dateTime.iso8601'))
- if value.find('struct') is not None:
- struct = {}
- for member in value.find('struct').findall('member'):
- struct[member.find('name').text] = _xml2py(member.find('value'))
- return struct
- if value.find('array') is not None:
- array = []
- for val in value.find('array').find('data').findall('value'):
- array.append(_xml2py(val))
- return array
- raise ValueError()
-
-class rpcbase64(object):
- def __init__(self, data):
- #base 64 encoded string
- self.data = data
-
- def decode(self):
- return base64.decodestring(data)
-
- def __str__(self):
- return self.decode()
-
- def encoded(self):
- return self.data
-
-class rpctime(object):
- def __init__(self,data=None):
- #assume string data is in iso format YYYYMMDDTHH:MM:SS
- if type(data) is str:
- self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
- elif type(data) is time.struct_time:
- self.timestamp = data
- elif data is None:
- self.timestamp = time.gmtime()
- else:
- raise ValueError()
-
- def iso8601(self):
- #return a iso8601 string
- return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp)
-
- def __str__(self):
- return self.iso8601()
-
-class JabberRPCEntry(object):
- def __init__(self,call):
- self.call = call
- self.result = None
- self.error = None
- self.allow = {} #{'<jid>':['<resource1>',...],...}
- self.deny = {}
-
- def check_acl(self, jid, resource):
- #Check for deny
- if jid in self.deny.keys():
- if self.deny[jid] == None or resource in self.deny[jid]:
- return False
- #Check for allow
- if allow == None:
- return True
- if jid in self.allow.keys():
- if self.allow[jid] == None or resource in self.allow[jid]:
- return True
- return False
-
- def acl_allow(self, jid, resource):
- if jid == None:
- self.allow = None
- elif resource == None:
- self.allow[jid] = None
- elif jid in self.allow.keys():
- self.allow[jid].append(resource)
- else:
- self.allow[jid] = [resource]
-
- def acl_deny(self, jid, resource):
- if jid == None:
- self.deny = None
- elif resource == None:
- self.deny[jid] = None
- elif jid in self.deny.keys():
- self.deny[jid].append(resource)
- else:
- self.deny[jid] = [resource]
-
- def call_method(self, args):
- ret = self.call(*args)
-
-class xep_0009(base.base_plugin):
-
- def plugin_init(self):
- self.xep = '0009'
- self.description = 'Jabber-RPC'
- self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>",
- self._callMethod, name='Jabber RPC Call')
- self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>",
- self._callResult, name='Jabber RPC Result')
- self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>",
- self._callError, name='Jabber RPC Error')
- self.entries = {}
- self.activeCalls = []
-
- def post_init(self):
- base.base_plugin.post_init(self)
- self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
- self.xmpp.plugin['xep_0030'].add_identity('automatition','rpc')
-
- def register_call(self, method, name=None):
- #@returns an string that can be used in acl commands.
- with self.lock:
- if name is None:
- self.entries[method.__name__] = JabberRPCEntry(method)
- return method.__name__
- else:
- self.entries[name] = JabberRPCEntry(method)
- return name
-
- def acl_allow(self, entry, jid=None, resource=None):
- #allow the method entry to be called by the given jid and resource.
- #if jid is None it will allow any jid/resource.
- #if resource is None it will allow any resource belonging to the jid.
- with self.lock:
- if self.entries[entry]:
- self.entries[entry].acl_allow(jid,resource)
- else:
- raise ValueError()
-
- def acl_deny(self, entry, jid=None, resource=None):
- #Note: by default all requests are denied unless allowed with acl_allow.
- #If you deny an entry it will not be allowed regardless of acl_allow
- with self.lock:
- if self.entries[entry]:
- self.entries[entry].acl_deny(jid,resource)
- else:
- raise ValueError()
-
- def unregister_call(self, entry):
- #removes the registered call
- with self.lock:
- if self.entries[entry]:
- del self.entries[entry]
- else:
- raise ValueError()
-
- def makeMethodCallQuery(self,pmethod,params):
- query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc")
- methodCall = ET.Element('methodCall')
- methodName = ET.Element('methodName')
- methodName.text = pmethod
- methodCall.append(methodName)
- methodCall.append(params)
- query.append(methodCall)
- return query
-
- def makeIqMethodCall(self,pto,pmethod,params):
- iq = self.xmpp.makeIqSet()
- iq.set('to',pto)
- iq.append(self.makeMethodCallQuery(pmethod,params))
- return iq
-
- def makeIqMethodResponse(self,pto,pid,params):
- iq = self.xmpp.makeIqResult(pid)
- iq.set('to',pto)
- query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc")
- methodResponse = ET.Element('methodResponse')
- methodResponse.append(params)
- query.append(methodResponse)
- return iq
-
- def makeIqMethodError(self,pto,id,pmethod,params,condition):
- iq = self.xmpp.makeIqError(id)
- iq.set('to',pto)
- iq.append(self.makeMethodCallQuery(pmethod,params))
- iq.append(self.xmpp['xep_0086'].makeError(condition))
- return iq
-
-
-
- def call_remote(self, pto, pmethod, *args):
- #calls a remote method. Returns the id of the Iq.
- pass
-
- def _callMethod(self,xml):
- pass
-
- def _callResult(self,xml):
- pass
-
- def _callError(self,xml):
- pass
diff --git a/sleekxmpp/plugins/old_0050.py b/sleekxmpp/plugins/old_0050.py deleted file mode 100644 index 6e969a51..00000000 --- a/sleekxmpp/plugins/old_0050.py +++ /dev/null @@ -1,133 +0,0 @@ -""" - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. - - See the file LICENSE for copying permission. -""" -from __future__ import with_statement -from . import base -import logging -from xml.etree import cElementTree as ET -import time - -class old_0050(base.base_plugin): - """ - XEP-0050 Ad-Hoc Commands - """ - - def plugin_init(self): - self.xep = '0050' - self.description = 'Ad-Hoc Commands' - self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc None') - self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc Execute') - self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, name='Ad-Hoc Next', threaded=True) - self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel, name='Ad-Hoc Cancel') - self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete, name='Ad-Hoc Complete') - self.commands = {} - self.sessions = {} - self.sd = self.xmpp.plugin['xep_0030'] - - def post_init(self): - base.base_plugin.post_init(self) - self.sd.add_feature('http://jabber.org/protocol/commands') - - def addCommand(self, node, name, form, pointer=None, multi=False): - self.sd.add_item(None, name, 'http://jabber.org/protocol/commands', node) - self.sd.add_identity('automation', 'command-node', name, node) - self.sd.add_feature('http://jabber.org/protocol/commands', node) - self.sd.add_feature('jabber:x:data', node) - self.commands[node] = (name, form, pointer, multi) - - def getNewSession(self): - return str(time.time()) + '-' + self.xmpp.getNewId() - - def handler_command(self, xml): - in_command = xml.find('{http://jabber.org/protocol/commands}command') - sessionid = in_command.get('sessionid', None) - node = in_command.get('node') - sessionid = self.getNewSession() - name, form, pointer, multi = self.commands[node] - self.sessions[sessionid] = {} - self.sessions[sessionid]['jid'] = xml.get('from') - self.sessions[sessionid]['to'] = xml.get('to') - self.sessions[sessionid]['past'] = [(form, None)] - self.sessions[sessionid]['next'] = pointer - npointer = pointer - if multi: - actions = ['next'] - status = 'executing' - else: - if pointer is None: - status = 'completed' - actions = [] - else: - status = 'executing' - actions = ['complete'] - self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions)) - - def handler_command_complete(self, xml): - in_command = xml.find('{http://jabber.org/protocol/commands}command') - sessionid = in_command.get('sessionid', None) - pointer = self.sessions[sessionid]['next'] - results = self.xmpp.plugin['old_0004'].makeForm('result') - results.fromXML(in_command.find('{jabber:x:data}x')) - pointer(results,sessionid) - self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=None, id=xml.attrib['id'], sessionid=sessionid, status='completed', actions=[])) - del self.sessions[in_command.get('sessionid')] - - - def handler_command_next(self, xml): - in_command = xml.find('{http://jabber.org/protocol/commands}command') - sessionid = in_command.get('sessionid', None) - pointer = self.sessions[sessionid]['next'] - results = self.xmpp.plugin['old_0004'].makeForm('result') - results.fromXML(in_command.find('{jabber:x:data}x')) - form, npointer, next = pointer(results,sessionid) - self.sessions[sessionid]['next'] = npointer - self.sessions[sessionid]['past'].append((form, pointer)) - actions = [] - actions.append('prev') - if npointer is None: - status = 'completed' - else: - status = 'executing' - if next: - actions.append('next') - else: - actions.append('complete') - self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions)) - - def handler_command_cancel(self, xml): - command = xml.find('{http://jabber.org/protocol/commands}command') - try: - del self.sessions[command.get('sessionid')] - except: - pass - self.xmpp.send(self.makeCommand(xml.attrib['from'], command.attrib['node'], id=xml.attrib['id'], sessionid=command.attrib['sessionid'], status='canceled')) - - def makeCommand(self, to, node, id=None, form=None, sessionid=None, status='executing', actions=[]): - if not id: - id = self.xmpp.getNewId() - iq = self.xmpp.makeIqResult(id) - iq.attrib['from'] = self.xmpp.boundjid.full - iq.attrib['to'] = to - command = ET.Element('{http://jabber.org/protocol/commands}command') - command.attrib['node'] = node - command.attrib['status'] = status - xmlactions = ET.Element('actions') - for action in actions: - xmlactions.append(ET.Element(action)) - if xmlactions: - command.append(xmlactions) - if not sessionid: - sessionid = self.getNewSession() - else: - iq.attrib['from'] = self.sessions[sessionid]['to'] - command.attrib['sessionid'] = sessionid - if form is not None: - if hasattr(form,'getXML'): - form = form.getXML() - command.append(form) - iq.append(command) - return iq diff --git a/sleekxmpp/plugins/old_0060.py b/sleekxmpp/plugins/old_0060.py deleted file mode 100644 index 93124fca..00000000 --- a/sleekxmpp/plugins/old_0060.py +++ /dev/null @@ -1,313 +0,0 @@ -from __future__ import with_statement -from . import base -import logging -#from xml.etree import cElementTree as ET -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET -from . import stanza_pubsub -from . xep_0004 import Form - - -log = logging.getLogger(__name__) - - -class xep_0060(base.base_plugin): - """ - XEP-0060 Publish Subscribe - """ - - def plugin_init(self): - self.xep = '0060' - self.description = 'Publish-Subscribe' - - def create_node(self, jid, node, config=None, collection=False, ntype=None): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') - create = ET.Element('create') - create.set('node', node) - pubsub.append(create) - configure = ET.Element('configure') - if collection: - ntype = 'collection' - #if config is None: - # submitform = self.xmpp.plugin['xep_0004'].makeForm('submit') - #else: - if config is not None: - submitform = config - if 'FORM_TYPE' in submitform.field: - submitform.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config') - else: - submitform.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config') - if ntype: - if 'pubsub#node_type' in submitform.field: - submitform.field['pubsub#node_type'].setValue(ntype) - else: - submitform.addField('pubsub#node_type', value=ntype) - else: - if 'pubsub#node_type' in submitform.field: - submitform.field['pubsub#node_type'].setValue('leaf') - else: - submitform.addField('pubsub#node_type', value='leaf') - submitform['type'] = 'submit' - configure.append(submitform.xml) - pubsub.append(configure) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is False or result is None or result['type'] == 'error': return False - return True - - def subscribe(self, jid, node, bare=True, subscribee=None): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') - subscribe = ET.Element('subscribe') - subscribe.attrib['node'] = node - if subscribee is None: - if bare: - subscribe.attrib['jid'] = self.xmpp.boundjid.bare - else: - subscribe.attrib['jid'] = self.xmpp.boundjid.full - else: - subscribe.attrib['jid'] = subscribee - pubsub.append(subscribe) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is False or result is None or result['type'] == 'error': return False - return True - - def unsubscribe(self, jid, node, bare=True, subscribee=None): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') - unsubscribe = ET.Element('unsubscribe') - unsubscribe.attrib['node'] = node - if subscribee is None: - if bare: - unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare - else: - unsubscribe.attrib['jid'] = self.xmpp.boundjid.full - else: - unsubscribe.attrib['jid'] = subscribee - pubsub.append(unsubscribe) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is False or result is None or result['type'] == 'error': return False - return True - - def getNodeConfig(self, jid, node=None): # if no node, then grab default - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - if node is not None: - configure = ET.Element('configure') - configure.attrib['node'] = node - else: - configure = ET.Element('default') - pubsub.append(configure) - #TODO: Add configure support. - iq = self.xmpp.makeIqGet() - iq.append(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - #self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse) - result = iq.send() - if result is None or result == False or result['type'] == 'error': - log.warning("got error instead of config") - return False - if node is not None: - form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x') - else: - form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x') - if not form or form is None: - log.error("No form found.") - return False - return Form(xml=form) - - def getNodeSubscriptions(self, jid, node): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - subscriptions = ET.Element('subscriptions') - subscriptions.attrib['node'] = node - pubsub.append(subscriptions) - iq = self.xmpp.makeIqGet() - iq.append(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result == False or result['type'] == 'error': - log.warning("got error instead of config") - return False - else: - results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription') - if results is None: - return False - subs = {} - for sub in results: - subs[sub.get('jid')] = sub.get('subscription') - return subs - - def getNodeAffiliations(self, jid, node): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - affiliations = ET.Element('affiliations') - affiliations.attrib['node'] = node - pubsub.append(affiliations) - iq = self.xmpp.makeIqGet() - iq.append(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result == False or result['type'] == 'error': - log.warning("got error instead of config") - return False - else: - results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation') - if results is None: - return False - subs = {} - for sub in results: - subs[sub.get('jid')] = sub.get('affiliation') - return subs - - def deleteNode(self, jid, node): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - iq = self.xmpp.makeIqSet() - delete = ET.Element('delete') - delete.attrib['node'] = node - pubsub.append(delete) - iq.append(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - result = iq.send() - if result is not None and result is not False and result['type'] != 'error': - return True - else: - return False - - - def setNodeConfig(self, jid, node, config): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - configure = ET.Element('configure') - configure.attrib['node'] = node - config = config.getXML('submit') - configure.append(config) - pubsub.append(configure) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result['type'] == 'error': - return False - return True - - def setItem(self, jid, node, items=[]): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') - publish = ET.Element('publish') - publish.attrib['node'] = node - for pub_item in items: - id, payload = pub_item - item = ET.Element('item') - if id is not None: - item.attrib['id'] = id - item.append(payload) - publish.append(item) - pubsub.append(publish) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result is False or result['type'] == 'error': return False - return True - - def addItem(self, jid, node, items=[]): - return self.setItem(jid, node, items) - - def deleteItem(self, jid, node, item): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') - retract = ET.Element('retract') - retract.attrib['node'] = node - itemn = ET.Element('item') - itemn.attrib['id'] = item - retract.append(itemn) - pubsub.append(retract) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result is False or result['type'] == 'error': return False - return True - - def getNodes(self, jid): - response = self.xmpp.plugin['xep_0030'].getItems(jid) - items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') - nodes = {} - if items is not None and items is not False: - for item in items: - nodes[item.get('node')] = item.get('name') - return nodes - - def getItems(self, jid, node): - response = self.xmpp.plugin['xep_0030'].getItems(jid, node) - items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') - nodeitems = [] - if items is not None and items is not False: - for item in items: - nodeitems.append(item.get('node')) - return nodeitems - - def addNodeToCollection(self, jid, child, parent=''): - config = self.getNodeConfig(jid, child) - if not config or config is None: - self.lasterror = "Config Error" - return False - try: - config.field['pubsub#collection'].setValue(parent) - except KeyError: - log.warning("pubsub#collection doesn't exist in config, trying to add it") - config.addField('pubsub#collection', value=parent) - if not self.setNodeConfig(jid, child, config): - return False - return True - - def modifyAffiliation(self, ps_jid, node, user_jid, affiliation): - if affiliation not in ('owner', 'publisher', 'member', 'none', 'outcast'): - raise TypeError - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - affs = ET.Element('affiliations') - affs.attrib['node'] = node - aff = ET.Element('affiliation') - aff.attrib['jid'] = user_jid - aff.attrib['affiliation'] = affiliation - affs.append(aff) - pubsub.append(affs) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = ps_jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result is False or result['type'] == 'error': - return False - return True - - def addNodeToCollection(self, jid, child, parent=''): - config = self.getNodeConfig(jid, child) - if not config or config is None: - self.lasterror = "Config Error" - return False - try: - config.field['pubsub#collection'].setValue(parent) - except KeyError: - log.warning("pubsub#collection doesn't exist in config, trying to add it") - config.addField('pubsub#collection', value=parent) - if not self.setNodeConfig(jid, child, config): - return False - return True - - def removeNodeFromCollection(self, jid, child): - self.addNodeToCollection(jid, child, '') - diff --git a/sleekxmpp/plugins/xep_0004/stanza/field.py b/sleekxmpp/plugins/xep_0004/stanza/field.py index 1e175966..51f85995 100644 --- a/sleekxmpp/plugins/xep_0004/stanza/field.py +++ b/sleekxmpp/plugins/xep_0004/stanza/field.py @@ -41,10 +41,11 @@ class FormField(ElementBase): self._type = value def add_option(self, label='', value=''): - if self._type in self.option_types: - opt = FieldOption(parent=self) + if self._type is None or self._type in self.option_types: + opt = FieldOption() opt['label'] = label opt['value'] = value + self.append(opt) else: raise ValueError("Cannot add options to " + \ "a %s field." % self['type']) diff --git a/sleekxmpp/plugins/xep_0004/stanza/form.py b/sleekxmpp/plugins/xep_0004/stanza/form.py index bbf0ee7d..1d733760 100644 --- a/sleekxmpp/plugins/xep_0004/stanza/form.py +++ b/sleekxmpp/plugins/xep_0004/stanza/form.py @@ -65,7 +65,7 @@ class Form(ElementBase): if kwtype is None: kwtype = ftype - field = FormField(parent=self) + field = FormField() field['var'] = var field['type'] = kwtype field['value'] = value @@ -77,6 +77,7 @@ class Form(ElementBase): field['options'] = options else: del field['type'] + self.append(field) return field def getXML(self, type='submit'): @@ -144,14 +145,12 @@ class Form(ElementBase): def get_fields(self, use_dict=False): fields = OrderedDict() - fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) - for fieldXML in fieldsXML: - field = FormField(xml=fieldXML) - fields[field['var']] = field + for stanza in self['substanzas']: + if isinstance(stanza, FormField): + fields[stanza['var']] = stanza return fields def get_instructions(self): - instructions = '' instsXML = self.xml.findall('{%s}instructions' % self.namespace) return "\n".join([instXML.text for instXML in instsXML]) @@ -195,13 +194,21 @@ class Form(ElementBase): fields = fields.items() for var, field in fields: field['var'] = var - self.add_field(**field) + self.add_field( + var = field.get('var'), + label = field.get('label'), + desc = field.get('desc'), + required = field.get('required'), + value = field.get('value'), + options = field.get('options'), + type = field.get('type')) def set_instructions(self, instructions): del self['instructions'] if instructions in [None, '']: return - instructions = instructions.split('\n') + if not isinstance(instructions, list): + instructions = instructions.split('\n') for instruction in instructions: inst = ET.Element('{%s}instructions' % self.namespace) inst.text = instruction @@ -220,6 +227,8 @@ class Form(ElementBase): def set_values(self, values): fields = self['fields'] for field in values: + if field not in fields: + fields[field] = self.add_field(var=field) fields[field]['value'] = values[field] def merge(self, other): diff --git a/sleekxmpp/plugins/xep_0009/remote.py b/sleekxmpp/plugins/xep_0009/remote.py index 8c08e8f3..b02f587e 100644 --- a/sleekxmpp/plugins/xep_0009/remote.py +++ b/sleekxmpp/plugins/xep_0009/remote.py @@ -6,7 +6,7 @@ See the file LICENSE for copying permission. """ -from binding import py2xml, xml2py, xml2fault, fault2xml +from sleekxmpp.plugins.xep_0009.binding import py2xml, xml2py, xml2fault, fault2xml from threading import RLock import abc import inspect @@ -18,6 +18,45 @@ import traceback log = logging.getLogger(__name__) +# Define a function _isstr() to check if an object is a string in a way +# compatible with Python 2 and Python 3 (basestring does not exists in Python 3). +try: + basestring # This evaluation will throw an exception if basestring does not exists (Python 3). + def _isstr(obj): + return isinstance(obj, basestring) +except NameError: + def _isstr(obj): + return isinstance(obj, str) + + +# Class decorator to declare a metaclass to a class in a way compatible with Python 2 and 3. +# This decorator is copied from 'six' (https://bitbucket.org/gutworth/six): +# +# Copyright (c) 2010-2015 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +def _add_metaclass(metaclass): + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + def _intercept(method, name, public): def _resolver(instance, *args, **kwargs): log.debug("Locally calling %s.%s with arguments %s.", instance.FQN(), method.__name__, args) @@ -68,7 +107,7 @@ def remote(function_argument, public = True): if hasattr(function_argument, '__call__'): return _intercept(function_argument, None, public) else: - if not isinstance(function_argument, basestring): + if not _isstr(function_argument): if not isinstance(function_argument, bool): raise Exception('Expected an RPC method name or visibility modifier!') else: @@ -222,12 +261,11 @@ class TimeoutException(Exception): pass +@_add_metaclass(abc.ABCMeta) class Callback(object): ''' A base class for callback handlers. ''' - __metaclass__ = abc.ABCMeta - @abc.abstractproperty def set_value(self, value): @@ -291,7 +329,7 @@ class Future(Callback): self._event.set() - +@_add_metaclass(abc.ABCMeta) class Endpoint(object): ''' The Endpoint class is an abstract base class for all objects @@ -303,8 +341,6 @@ class Endpoint(object): which specifies which object an RPC call refers to. It is the first part in a RPC method name '<fqn>.<method>'. ''' - __metaclass__ = abc.ABCMeta - def __init__(self, session, target_jid): ''' @@ -491,7 +527,7 @@ class RemoteSession(object): def _find_key(self, dict, value): """return the key of dictionary dic given the value""" - search = [k for k, v in dict.iteritems() if v == value] + search = [k for k, v in dict.items() if v == value] if len(search) == 0: return None else: @@ -547,7 +583,7 @@ class RemoteSession(object): result = handler_cls(*args, **kwargs) Endpoint.__init__(result, self, self._client.boundjid.full) method_dict = result.get_methods() - for method_name, method in method_dict.iteritems(): + for method_name, method in method_dict.items(): #!!! self._client.plugin['xep_0009'].register_call(result.FQN(), method, method_name) self._register_call(result.FQN(), method, method_name) self._register_acl(result.FQN(), acl) @@ -569,11 +605,11 @@ class RemoteSession(object): self._register_callback(pid, callback) iq.send() - def close(self): + def close(self, wait=False): ''' Closes this session. ''' - self._client.disconnect(False) + self._client.disconnect(wait=wait) self._session_close_callback() def _on_jabber_rpc_method_call(self, iq): @@ -697,7 +733,8 @@ class Remote(object): if(client.boundjid.bare in cls._sessions): raise RemoteException("There already is a session associated with these credentials!") else: - cls._sessions[client.boundjid.bare] = client; + cls._sessions[client.boundjid.bare] = client + def _session_close_callback(): with Remote._lock: del cls._sessions[client.boundjid.bare] diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py index 4e1c538b..6179355e 100644 --- a/sleekxmpp/plugins/xep_0009/rpc.py +++ b/sleekxmpp/plugins/xep_0009/rpc.py @@ -32,15 +32,15 @@ class XEP_0009(BasePlugin): register_stanza_plugin(RPCQuery, MethodCall)
register_stanza_plugin(RPCQuery, MethodResponse)
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodCall' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
self._handle_method_call)
)
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodResponse' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
self._handle_method_response)
)
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('RPC Call', MatchXPath('{%s}iq/{%s}error' % (self.xmpp.default_ns, self.xmpp.default_ns)),
self._handle_error)
)
@@ -61,7 +61,7 @@ class XEP_0009(BasePlugin): iq.enable('rpc_query')
iq['rpc_query']['method_call']['method_name'] = pmethod
iq['rpc_query']['method_call']['params'] = params
- return iq;
+ return iq
def make_iq_method_response(self, pid, pto, params):
iq = self.xmpp.makeIqResult(pid)
@@ -93,7 +93,7 @@ class XEP_0009(BasePlugin): def _item_not_found(self, iq):
payload = iq.get_payload()
- iq.reply().error().set_payload(payload);
+ iq.reply().error().set_payload(payload)
iq['error']['code'] = '404'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'item-not-found'
diff --git a/sleekxmpp/plugins/xep_0013/__init__.py b/sleekxmpp/plugins/xep_0013/__init__.py new file mode 100644 index 00000000..ad400949 --- /dev/null +++ b/sleekxmpp/plugins/xep_0013/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0013.stanza import Offline +from sleekxmpp.plugins.xep_0013.offline import XEP_0013 + + +register_plugin(XEP_0013) diff --git a/sleekxmpp/plugins/xep_0013/offline.py b/sleekxmpp/plugins/xep_0013/offline.py new file mode 100644 index 00000000..a0d992a7 --- /dev/null +++ b/sleekxmpp/plugins/xep_0013/offline.py @@ -0,0 +1,134 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +import logging + +import sleekxmpp +from sleekxmpp.stanza import Message, Iq +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.xmlstream.handler import Collector +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0013 import stanza + + +log = logging.getLogger(__name__) + + +class XEP_0013(BasePlugin): + + """ + XEP-0013 Flexible Offline Message Retrieval + """ + + name = 'xep_0013' + description = 'XEP-0013: Flexible Offline Message Retrieval' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, stanza.Offline) + register_stanza_plugin(Message, stanza.Offline) + + def get_count(self, **kwargs): + return self.xmpp['xep_0030'].get_info( + node='http://jabber.org/protocol/offline', + local=False, + **kwargs) + + def get_headers(self, **kwargs): + return self.xmpp['xep_0030'].get_items( + node='http://jabber.org/protocol/offline', + local=False, + **kwargs) + + def view(self, nodes, ifrom=None, block=True, timeout=None, callback=None): + if not isinstance(nodes, (list, set)): + nodes = [nodes] + + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['from'] = ifrom + offline = iq['offline'] + for node in nodes: + item = stanza.Item() + item['node'] = node + item['action'] = 'view' + offline.append(item) + + collector = Collector( + 'Offline_Results_%s' % iq['id'], + StanzaPath('message/offline')) + self.xmpp.register_handler(collector) + + if not block and callback is not None: + def wrapped_cb(iq): + results = collector.stop() + if iq['type'] == 'result': + iq['offline']['results'] = results + callback(iq) + return iq.send(block=block, timeout=timeout, callback=wrapped_cb) + else: + try: + resp = iq.send(block=block, timeout=timeout, callback=callback) + resp['offline']['results'] = collector.stop() + return resp + except XMPPError as e: + collector.stop() + raise e + + def remove(self, nodes, ifrom=None, block=True, timeout=None, callback=None): + if not isinstance(nodes, (list, set)): + nodes = [nodes] + + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + offline = iq['offline'] + for node in nodes: + item = stanza.Item() + item['node'] = node + item['action'] = 'remove' + offline.append(item) + + return iq.send(block=block, timeout=timeout, callback=callback) + + def fetch(self, ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + iq['offline']['fetch'] = True + + collector = Collector( + 'Offline_Results_%s' % iq['id'], + StanzaPath('message/offline')) + self.xmpp.register_handler(collector) + + if not block and callback is not None: + def wrapped_cb(iq): + results = collector.stop() + if iq['type'] == 'result': + iq['offline']['results'] = results + callback(iq) + return iq.send(block=block, timeout=timeout, callback=wrapped_cb) + else: + try: + resp = iq.send(block=block, timeout=timeout, callback=callback) + resp['offline']['results'] = collector.stop() + return resp + except XMPPError as e: + collector.stop() + raise e + + def purge(self, ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + iq['offline']['purge'] = True + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0013/stanza.py b/sleekxmpp/plugins/xep_0013/stanza.py new file mode 100644 index 00000000..c9c69786 --- /dev/null +++ b/sleekxmpp/plugins/xep_0013/stanza.py @@ -0,0 +1,53 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.jid import JID +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin + + +class Offline(ElementBase): + name = 'offline' + namespace = 'http://jabber.org/protocol/offline' + plugin_attrib = 'offline' + interfaces = set(['fetch', 'purge', 'results']) + bool_interfaces = interfaces + + def setup(self, xml=None): + ElementBase.setup(self, xml) + self._results = [] + + # The results interface is meant only as an easy + # way to access the set of collected message responses + # from the query. + + def get_results(self): + return self._results + + def set_results(self, values): + self._results = values + + def del_results(self): + self._results = [] + + +class Item(ElementBase): + name = 'item' + namespace = 'http://jabber.org/protocol/offline' + plugin_attrib = 'item' + interfaces = set(['action', 'node', 'jid']) + + actions = set(['view', 'remove']) + + def get_jid(self): + return JID(self._get_attr('jid')) + + def set_jid(self, value): + self._set_attr('jid', str(value)) + + +register_stanza_plugin(Offline, Item, iterable=True) diff --git a/sleekxmpp/plugins/xep_0016/__init__.py b/sleekxmpp/plugins/xep_0016/__init__.py new file mode 100644 index 00000000..06704d26 --- /dev/null +++ b/sleekxmpp/plugins/xep_0016/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0016 import stanza +from sleekxmpp.plugins.xep_0016.stanza import Privacy +from sleekxmpp.plugins.xep_0016.privacy import XEP_0016 + + +register_plugin(XEP_0016) diff --git a/sleekxmpp/plugins/xep_0016/privacy.py b/sleekxmpp/plugins/xep_0016/privacy.py new file mode 100644 index 00000000..79fd68f0 --- /dev/null +++ b/sleekxmpp/plugins/xep_0016/privacy.py @@ -0,0 +1,110 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp import Iq +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0016 import stanza +from sleekxmpp.plugins.xep_0016.stanza import Privacy, Item + + +class XEP_0016(BasePlugin): + + name = 'xep_0016' + description = 'XEP-0016: Privacy Lists' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, Privacy) + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=Privacy.namespace) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(Privacy.namespace) + + def get_privacy_lists(self, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq.enable('privacy') + return iq.send(block=block, timeout=timeout, callback=callback) + + def get_list(self, name, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['privacy']['list']['name'] = name + return iq.send(block=block, timeout=timeout, callback=callback) + + def get_active(self, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['privacy'].enable('active') + return iq.send(block=block, timeout=timeout, callback=callback) + + def get_default(self, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['privacy'].enable('default') + return iq.send(block=block, timeout=timeout, callback=callback) + + def activate(self, name, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['privacy']['active']['name'] = name + return iq.send(block=block, timeout=timeout, callback=callback) + + def deactivate(self, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['privacy'].enable('active') + return iq.send(block=block, timeout=timeout, callback=callback) + + def make_default(self, name, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['privacy']['default']['name'] = name + return iq.send(block=block, timeout=timeout, callback=callback) + + def remove_default(self, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['privacy'].enable('default') + return iq.send(block=block, timeout=timeout, callback=callback) + + def edit_list(self, name, rules, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['privacy']['list']['name'] = name + priv_list = iq['privacy']['list'] + + if not rules: + rules = [] + + for rule in rules: + if isinstance(rule, Item): + priv_list.append(rule) + continue + + priv_list.add_item( + rule['value'], + rule['action'], + rule['order'], + itype=rule.get('type', None), + iq=rule.get('iq', None), + message=rule.get('message', None), + presence_in=rule.get('presence_in', + rule.get('presence-in', None)), + presence_out=rule.get('presence_out', + rule.get('presence-out', None))) + + def remove_list(self, name, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['privacy']['list']['name'] = name + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0016/stanza.py b/sleekxmpp/plugins/xep_0016/stanza.py new file mode 100644 index 00000000..3f9977fc --- /dev/null +++ b/sleekxmpp/plugins/xep_0016/stanza.py @@ -0,0 +1,103 @@ +from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin + + +class Privacy(ElementBase): + name = 'query' + namespace = 'jabber:iq:privacy' + plugin_attrib = 'privacy' + interfaces = set() + + def add_list(self, name): + priv_list = List() + priv_list['name'] = name + self.append(priv_list) + return priv_list + + +class Active(ElementBase): + name = 'active' + namespace = 'jabber:iq:privacy' + plugin_attrib = name + interfaces = set(['name']) + + +class Default(ElementBase): + name = 'default' + namespace = 'jabber:iq:privacy' + plugin_attrib = name + interfaces = set(['name']) + + +class List(ElementBase): + name = 'list' + namespace = 'jabber:iq:privacy' + plugin_attrib = name + plugin_multi_attrib = 'lists' + interfaces = set(['name']) + + def add_item(self, value, action, order, itype=None, iq=False, + message=False, presence_in=False, presence_out=False): + item = Item() + item.values = {'type': itype, + 'value': value, + 'action': action, + 'order': order, + 'message': message, + 'iq': iq, + 'presence_in': presence_in, + 'presence_out': presence_out} + self.append(item) + return item + + +class Item(ElementBase): + name = 'item' + namespace = 'jabber:iq:privacy' + plugin_attrib = name + plugin_multi_attrib = 'items' + interfaces = set(['type', 'value', 'action', 'order', 'iq', + 'message', 'presence_in', 'presence_out']) + bool_interfaces = set(['message', 'iq', 'presence_in', 'presence_out']) + + type_values = ('', 'jid', 'group', 'subscription') + action_values = ('allow', 'deny') + + def set_type(self, value): + if value and value not in self.type_values: + raise ValueError('Unknown type value: %s' % value) + else: + self._set_attr('type', value) + + def set_action(self, value): + if value not in self.action_values: + raise ValueError('Unknown action value: %s' % value) + else: + self._set_attr('action', value) + + def set_presence_in(self, value): + keep = True if value else False + self._set_sub_text('presence-in', '', keep=keep) + + def get_presence_in(self): + pres = self.xml.find('{%s}presence-in' % self.namespace) + return pres is not None + + def del_presence_in(self): + self._del_sub('{%s}presence-in' % self.namespace) + + def set_presence_out(self, value): + keep = True if value else False + self._set_sub_text('presence-in', '', keep=keep) + + def get_presence_out(self): + pres = self.xml.find('{%s}presence-in' % self.namespace) + return pres is not None + + def del_presence_out(self): + self._del_sub('{%s}presence-in' % self.namespace) + + +register_stanza_plugin(Privacy, Active) +register_stanza_plugin(Privacy, Default) +register_stanza_plugin(Privacy, List, iterable=True) +register_stanza_plugin(List, Item, iterable=True) diff --git a/sleekxmpp/plugins/xep_0020/__init__.py b/sleekxmpp/plugins/xep_0020/__init__.py new file mode 100644 index 00000000..c6aafe97 --- /dev/null +++ b/sleekxmpp/plugins/xep_0020/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0020 import stanza +from sleekxmpp.plugins.xep_0020.stanza import FeatureNegotiation +from sleekxmpp.plugins.xep_0020.feature_negotiation import XEP_0020 + + +register_plugin(XEP_0020) diff --git a/sleekxmpp/plugins/xep_0020/feature_negotiation.py b/sleekxmpp/plugins/xep_0020/feature_negotiation.py new file mode 100644 index 00000000..7cb82cd5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0020/feature_negotiation.py @@ -0,0 +1,36 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp import Iq, Message +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins.xep_0020 import stanza, FeatureNegotiation +from sleekxmpp.plugins.xep_0004 import Form + + +log = logging.getLogger(__name__) + + +class XEP_0020(BasePlugin): + + name = 'xep_0020' + description = 'XEP-0020: Feature Negotiation' + dependencies = set(['xep_0004', 'xep_0030']) + stanza = stanza + + def plugin_init(self): + self.xmpp['xep_0030'].add_feature(FeatureNegotiation.namespace) + + register_stanza_plugin(FeatureNegotiation, Form) + + register_stanza_plugin(Iq, FeatureNegotiation) + register_stanza_plugin(Message, FeatureNegotiation) diff --git a/sleekxmpp/plugins/xep_0020/stanza.py b/sleekxmpp/plugins/xep_0020/stanza.py new file mode 100644 index 00000000..13e4056e --- /dev/null +++ b/sleekxmpp/plugins/xep_0020/stanza.py @@ -0,0 +1,17 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase + + +class FeatureNegotiation(ElementBase): + + name = 'feature' + namespace = 'http://jabber.org/protocol/feature-neg' + plugin_attrib = 'feature_neg' + interfaces = set() diff --git a/sleekxmpp/plugins/xep_0027/gpg.py b/sleekxmpp/plugins/xep_0027/gpg.py index 9c6ca078..52c1c461 100644 --- a/sleekxmpp/plugins/xep_0027/gpg.py +++ b/sleekxmpp/plugins/xep_0027/gpg.py @@ -24,7 +24,7 @@ def _extract_data(data, kind): if not begin_headers and 'BEGIN PGP %s' % kind in line: begin_headers = True continue - if begin_headers and line == '': + if begin_headers and line.strip() == '': begin_data = True continue if 'END PGP %s' % kind in line: @@ -40,14 +40,15 @@ class XEP_0027(BasePlugin): description = 'XEP-0027: Current Jabber OpenPGP Usage' dependencies = set() stanza = stanza + default_config = { + 'gpg_binary': 'gpg', + 'gpg_home': '', + 'use_agent': True, + 'keyring': None, + 'key_server': 'pgp.mit.edu' + } def plugin_init(self): - self.gpg_binary = self.config.get('gpg_binary', 'gpg') - self.gpg_home = self.config.get('gpg_home', '') - self.use_agent = self.config.get('use_agent', True) - self.keyring = self.config.get('keyring', None) - self.key_server = self.config.get('key_server', 'pgp.mit.edu') - self.gpg = GPG(gnupghome=self.gpg_home, gpgbinary=self.gpg_binary, use_agent=self.use_agent, diff --git a/sleekxmpp/plugins/xep_0027/stanza.py b/sleekxmpp/plugins/xep_0027/stanza.py index 3170ca6e..08f2032b 100644 --- a/sleekxmpp/plugins/xep_0027/stanza.py +++ b/sleekxmpp/plugins/xep_0027/stanza.py @@ -39,7 +39,7 @@ class Encrypted(ElementBase): def set_encrypted(self, value): parent = self.parent() xmpp = parent.stream - data = xmpp['xep_0027'].encrypt(value, parent['to'].bare) + data = xmpp['xep_0027'].encrypt(value, parent['to']) if data: self.xml.text = data else: diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index eeb977b1..721f73f6 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -88,6 +88,10 @@ class XEP_0030(BasePlugin): description = 'XEP-0030: Service Discovery' dependencies = set() stanza = stanza + default_config = { + 'use_cache': True, + 'wrap_results': False + } def plugin_init(self): """ @@ -108,9 +112,6 @@ class XEP_0030(BasePlugin): self.static = StaticDisco(self.xmpp, self) - self.use_cache = self.config.get('use_cache', True) - self.wrap_results = self.config.get('wrap_results', False) - self._disco_ops = [ 'get_info', 'set_info', 'set_identities', 'set_features', 'get_items', 'set_items', 'del_items', 'add_identity', @@ -287,7 +288,7 @@ class XEP_0030(BasePlugin): 'cached': cached} return self.api['has_identity'](jid, node, ifrom, data) - def get_info(self, jid=None, node=None, local=False, + def get_info(self, jid=None, node=None, local=None, cached=None, **kwargs): """ Retrieve the disco#info results from a given JID/node combination. @@ -323,18 +324,21 @@ class XEP_0030(BasePlugin): callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. + timeout_callback -- Optional callback to execute when no result + has been received in timeout seconds. """ - if jid is not None and not isinstance(jid, JID): - jid = JID(jid) - if self.xmpp.is_component: - if jid.domain == self.xmpp.boundjid.domain: - local = True - else: - if str(jid) == str(self.xmpp.boundjid): - local = True - jid = jid.full - elif jid in (None, ''): - local = True + if local is None: + if jid is not None and not isinstance(jid, JID): + jid = JID(jid) + if self.xmpp.is_component: + if jid.domain == self.xmpp.boundjid.domain: + local = True + else: + if str(jid) == str(self.xmpp.boundjid): + local = True + jid = jid.full + elif jid in (None, ''): + local = True if local: log.debug("Looking up local disco#info data " + \ @@ -362,7 +366,8 @@ class XEP_0030(BasePlugin): iq['disco_info']['node'] = node if node else '' return iq.send(timeout=kwargs.get('timeout', None), block=kwargs.get('block', True), - callback=kwargs.get('callback', None)) + callback=kwargs.get('callback', None), + timeout_callback=kwargs.get('timeout_callback', None)) def set_info(self, jid=None, node=None, info=None): """ @@ -403,8 +408,10 @@ class XEP_0030(BasePlugin): iterator -- If True, return a result set iterator using the XEP-0059 plugin, if the plugin is loaded. Otherwise the parameter is ignored. + timeout_callback -- Optional callback to execute when no result + has been received in timeout seconds. """ - if local or jid is None: + if local or local is None and jid is None: items = self.api['get_items'](jid, node, kwargs.get('ifrom', None), kwargs) @@ -421,7 +428,8 @@ class XEP_0030(BasePlugin): else: return iq.send(timeout=kwargs.get('timeout', None), block=kwargs.get('block', True), - callback=kwargs.get('callback', None)) + callback=kwargs.get('callback', None), + timeout_callback=kwargs.get('timeout_callback', None)) def set_items(self, jid=None, node=None, **kwargs): """ @@ -596,7 +604,7 @@ class XEP_0030(BasePlugin): """ self.api['del_features'](jid, node, None, kwargs) - def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}): + def _run_node_handler(self, htype, jid, node=None, ifrom=None, data=None): """ Execute the most specific node handler for the given JID/node combination. @@ -607,6 +615,9 @@ class XEP_0030(BasePlugin): node -- The node requested. data -- Optional, custom data to pass to the handler. """ + if not data: + data = {} + return self.api[htype](jid, node, ifrom, data) def _handle_disco_info(self, iq): diff --git a/sleekxmpp/plugins/xep_0030/stanza/items.py b/sleekxmpp/plugins/xep_0030/stanza/items.py index 512f2336..10458614 100644 --- a/sleekxmpp/plugins/xep_0030/stanza/items.py +++ b/sleekxmpp/plugins/xep_0030/stanza/items.py @@ -128,9 +128,10 @@ class DiscoItems(ElementBase): def del_items(self): """Remove all items.""" self._items = set() - for item in self['substanzas']: - if isinstance(item, DiscoItem): - self.xml.remove(item.xml) + items = [i for i in self.iterables if isinstance(i, DiscoItem)] + for item in items: + self.xml.remove(item.xml) + self.iterables.remove(item) class DiscoItem(ElementBase): diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py index 7fbb3d43..ca5ed1ef 100644 --- a/sleekxmpp/plugins/xep_0045.py +++ b/sleekxmpp/plugins/xep_0045.py @@ -125,11 +125,12 @@ class XEP_0045(BasePlugin): self.xep = '0045' # load MUC support in presence stanzas register_stanza_plugin(Presence, MUCPresence) - self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence)) - self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message)) - self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject)) - self.xmpp.registerHandler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change)) - self.xmpp.registerHandler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % ( + self.xmpp.register_handler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence)) + self.xmpp.register_handler(Callback('MUCError', MatchXMLMask("<message xmlns='%s' type='error'><error/></message>" % self.xmpp.default_ns), self.handle_groupchat_error_message)) + self.xmpp.register_handler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message)) + self.xmpp.register_handler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject)) + self.xmpp.register_handler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change)) + self.xmpp.register_handler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % ( self.xmpp.default_ns, 'http://jabber.org/protocol/muc#user', 'http://jabber.org/protocol/muc#user')), self.handle_groupchat_invite)) @@ -137,7 +138,7 @@ class XEP_0045(BasePlugin): def handle_groupchat_invite(self, inv): """ Handle an invite into a muc. """ - logging.debug("MUC invite to %s from %s: %s", inv['from'], inv["from"], inv) + logging.debug("MUC invite to %s from %s: %s", inv['to'], inv["from"], inv) if inv['from'] not in self.rooms.keys(): self.xmpp.event("groupchat_invite", inv) @@ -156,6 +157,7 @@ class XEP_0045(BasePlugin): entry = pr['muc'].getStanzaValues() entry['show'] = pr['show'] entry['status'] = pr['status'] + entry['alt_nick'] = pr['nick'] if pr['type'] == 'unavailable': if entry['nick'] in self.rooms[entry['room']]: del self.rooms[entry['room']][entry['nick']] @@ -178,6 +180,14 @@ class XEP_0045(BasePlugin): self.xmpp.event('groupchat_message', msg) self.xmpp.event("muc::%s::message" % msg['from'].bare, msg) + def handle_groupchat_error_message(self, msg): + """ Handle a message error event in a muc. + """ + self.xmpp.event('groupchat_message_error', msg) + self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg) + + + def handle_groupchat_subject(self, msg): """ Handle a message coming from a muc indicating a change of subject (or announcing it when joining the room) @@ -197,30 +207,9 @@ class XEP_0045(BasePlugin): if entry is not None and entry['jid'].full == jid: return nick - def getRoomForm(self, room, ifrom=None): - iq = self.xmpp.makeIqGet() - iq['to'] = room - if ifrom is not None: - iq['from'] = ifrom - query = ET.Element('{http://jabber.org/protocol/muc#owner}query') - iq.append(query) - # For now, swallow errors to preserve existing API - try: - result = iq.send() - except IqError: - return False - except IqTimeout: - return False - xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') - if xform is None: return False - form = self.xmpp.plugin['old_0004'].buildForm(xform) - return form - def configureRoom(self, room, form=None, ifrom=None): if form is None: - form = self.getRoomForm(room, ifrom=ifrom) - #form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit') - #form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig') + form = self.getRoomConfig(room, ifrom=ifrom) iq = self.xmpp.makeIqSet() iq['to'] = room if ifrom is not None: @@ -244,11 +233,11 @@ class XEP_0045(BasePlugin): stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom) x = ET.Element('{http://jabber.org/protocol/muc}x') if password: - passelement = ET.Element('password') + passelement = ET.Element('{http://jabber.org/protocol/muc}password') passelement.text = password x.append(passelement) if maxhistory: - history = ET.Element('history') + history = ET.Element('{http://jabber.org/protocol/muc}history') if maxhistory == "0": history.attrib['maxchars'] = maxhistory else: @@ -270,10 +259,10 @@ class XEP_0045(BasePlugin): iq['from'] = ifrom iq['to'] = room query = ET.Element('{http://jabber.org/protocol/muc#owner}query') - destroy = ET.Element('destroy') + destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy') if altroom: destroy.attrib['jid'] = altroom - xreason = ET.Element('reason') + xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason') xreason.text = reason destroy.append(xreason) query.append(destroy) @@ -293,9 +282,9 @@ class XEP_0045(BasePlugin): raise TypeError query = ET.Element('{http://jabber.org/protocol/muc#admin}query') if nick is not None: - item = ET.Element('item', {'affiliation':affiliation, 'nick':nick}) + item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick}) else: - item = ET.Element('item', {'affiliation':affiliation, 'jid':jid}) + item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid}) query.append(item) iq = self.xmpp.makeIqSet(query) iq['to'] = room @@ -309,6 +298,24 @@ class XEP_0045(BasePlugin): return False return True + def setRole(self, room, nick, role): + """ Change role property of a nick in a room. + Typically, roles are temporary (they last only as long as you are in the + room), whereas affiliations are permanent (they last across groupchat + sessions). + """ + if role not in ('moderator', 'participant', 'visitor', 'none'): + raise TypeError + query = ET.Element('{http://jabber.org/protocol/muc#admin}query') + item = ET.Element('item', {'role':role, 'nick':nick}) + query.append(item) + iq = self.xmpp.makeIqSet(query) + iq['to'] = room + result = iq.send() + if result is False or result['type'] != 'result': + raise ValueError + return True + def invite(self, room, jid, reason='', mfrom=''): """ Invite a jid to a room.""" msg = self.xmpp.makeMessage(room) @@ -316,7 +323,7 @@ class XEP_0045(BasePlugin): x = ET.Element('{http://jabber.org/protocol/muc#user}x') invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid}) if reason: - rxml = ET.Element('reason') + rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason') rxml.text = reason invite.append(rxml) x.append(invite) diff --git a/sleekxmpp/plugins/xep_0047/ibb.py b/sleekxmpp/plugins/xep_0047/ibb.py index 2b8c57d4..62dddac2 100644 --- a/sleekxmpp/plugins/xep_0047/ibb.py +++ b/sleekxmpp/plugins/xep_0047/ibb.py @@ -20,21 +20,26 @@ class XEP_0047(BasePlugin): description = 'XEP-0047: In-band Bytestreams' dependencies = set(['xep_0030']) stanza = stanza + default_config = { + 'block_size': 4096, + 'max_block_size': 8192, + 'window_size': 1, + 'auto_accept': False, + } def plugin_init(self): - self.streams = {} - self.pending_streams = {3: 5} - self.pending_close_streams = {} + self._streams = {} + self._pending_streams = {} + self._pending_lock = threading.Lock() self._stream_lock = threading.Lock() - self.max_block_size = self.config.get('max_block_size', 8192) - self.window_size = self.config.get('window_size', 1) - self.auto_accept = self.config.get('auto_accept', True) - self.accept_stream = self.config.get('accept_stream', None) + self._preauthed_sids_lock = threading.Lock() + self._preauthed_sids = {} register_stanza_plugin(Iq, Open) register_stanza_plugin(Iq, Close) register_stanza_plugin(Iq, Data) + register_stanza_plugin(Message, Data) self.xmpp.register_handler(Callback( 'IBB Open', @@ -51,27 +56,71 @@ class XEP_0047(BasePlugin): StanzaPath('iq@type=set/ibb_data'), self._handle_data)) + self.xmpp.register_handler(Callback( + 'IBB Message Data', + StanzaPath('message/ibb_data'), + self._handle_data)) + + self.api.register(self._authorized, 'authorized', default=True) + self.api.register(self._authorized_sid, 'authorized_sid', default=True) + self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True) + self.api.register(self._get_stream, 'get_stream', default=True) + self.api.register(self._set_stream, 'set_stream', default=True) + self.api.register(self._del_stream, 'del_stream', default=True) + def plugin_end(self): self.xmpp.remove_handler('IBB Open') self.xmpp.remove_handler('IBB Close') self.xmpp.remove_handler('IBB Data') + self.xmpp.remove_handler('IBB Message Data') self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/ibb') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb') + def _get_stream(self, jid, sid, peer_jid, data): + return self._streams.get((jid, sid, peer_jid), None) + + def _set_stream(self, jid, sid, peer_jid, stream): + self._streams[(jid, sid, peer_jid)] = stream + + def _del_stream(self, jid, sid, peer_jid, data): + with self._stream_lock: + if (jid, sid, peer_jid) in self._streams: + del self._streams[(jid, sid, peer_jid)] + def _accept_stream(self, iq): - if self.accept_stream is not None: - return self.accept_stream(iq) + receiver = iq['to'] + sender = iq['from'] + sid = iq['ibb_open']['sid'] + + if self.api['authorized_sid'](receiver, sid, sender, iq): + return True + return self.api['authorized'](receiver, sid, sender, iq) + + def _authorized(self, jid, sid, ifrom, iq): if self.auto_accept: if iq['ibb_open']['block_size'] <= self.max_block_size: return True return False - def open_stream(self, jid, block_size=4096, sid=None, window=1, + def _authorized_sid(self, jid, sid, ifrom, iq): + with self._preauthed_sids_lock: + if (jid, sid, ifrom) in self._preauthed_sids: + del self._preauthed_sids[(jid, sid, ifrom)] + return True + return False + + def _preauthorize_sid(self, jid, sid, ifrom, data): + with self._preauthed_sids_lock: + self._preauthed_sids[(jid, sid, ifrom)] = True + + def open_stream(self, jid, block_size=None, sid=None, window=1, use_messages=False, ifrom=None, block=True, timeout=None, callback=None): if sid is None: sid = str(uuid.uuid4()) + if block_size is None: + block_size = self.block_size iq = self.xmpp.Iq() iq['type'] = 'set' @@ -82,12 +131,13 @@ class XEP_0047(BasePlugin): iq['ibb_open']['stanza'] = 'iq' stream = IBBytestream(self.xmpp, sid, block_size, - iq['to'], iq['from'], window) + iq['from'], iq['to'], window, + use_messages) with self._stream_lock: - self.pending_streams[iq['id']] = stream + self._pending_streams[iq['id']] = stream - self.pending_streams[iq['id']] = stream + self._pending_streams[iq['id']] = stream if block: resp = iq.send(timeout=timeout) @@ -107,49 +157,59 @@ class XEP_0047(BasePlugin): def _handle_opened_stream(self, iq): if iq['type'] == 'result': with self._stream_lock: - stream = self.pending_streams.get(iq['id'], None) - if stream is not None: - stream.sender = iq['to'] - stream.receiver = iq['from'] - stream.stream_started.set() - self.streams[stream.sid] = stream - self.xmpp.event('ibb_stream_start', stream) + stream = self._pending_streams.get(iq['id'], None) + if stream is not None: + log.debug('IBB stream (%s) accepted by %s', stream.sid, iq['from']) + stream.self_jid = iq['to'] + stream.peer_jid = iq['from'] + stream.stream_started.set() + self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream) + self.xmpp.event('ibb_stream_start', stream) + self.xmpp.event('stream:%s:%s' % (stream.sid, stream.peer_jid), stream) with self._stream_lock: - if iq['id'] in self.pending_streams: - del self.pending_streams[iq['id']] + if iq['id'] in self._pending_streams: + del self._pending_streams[iq['id']] def _handle_open_request(self, iq): sid = iq['ibb_open']['sid'] - size = iq['ibb_open']['block_size'] + size = iq['ibb_open']['block_size'] or self.block_size + + log.debug('Received IBB stream request from %s', iq['from']) + + if not sid: + raise XMPPError(etype='modify', condition='bad-request') + if not self._accept_stream(iq): - raise XMPPError('not-acceptable') + raise XMPPError(etype='modify', condition='not-acceptable') if size > self.max_block_size: raise XMPPError('resource-constraint') stream = IBBytestream(self.xmpp, sid, size, - iq['from'], iq['to'], + iq['to'], iq['from'], self.window_size) stream.stream_started.set() - self.streams[sid] = stream + self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream) iq.reply() iq.send() self.xmpp.event('ibb_stream_start', stream) + self.xmpp.event('stream:%s:%s' % (sid, stream.peer_jid), stream) - def _handle_data(self, iq): - sid = iq['ibb_data']['sid'] - stream = self.streams.get(sid, None) - if stream is not None and iq['from'] != stream.sender: - stream._recv_data(iq) + def _handle_data(self, stanza): + sid = stanza['ibb_data']['sid'] + stream = self.api['get_stream'](stanza['to'], sid, stanza['from']) + if stream is not None and stanza['from'] == stream.peer_jid: + stream._recv_data(stanza) else: raise XMPPError('item-not-found') def _handle_close(self, iq): sid = iq['ibb_close']['sid'] - stream = self.streams.get(sid, None) - if stream is not None and iq['from'] != stream.sender: + stream = self.api['get_stream'](iq['to'], sid, iq['from']) + if stream is not None and iq['from'] == stream.peer_jid: stream._closed(iq) + self.api['del_stream'](stream.self_jid, stream.sid, stream.peer_jid) else: raise XMPPError('item-not-found') diff --git a/sleekxmpp/plugins/xep_0047/stanza.py b/sleekxmpp/plugins/xep_0047/stanza.py index afba07a8..7e5d2fed 100644 --- a/sleekxmpp/plugins/xep_0047/stanza.py +++ b/sleekxmpp/plugins/xep_0047/stanza.py @@ -1,9 +1,9 @@ import re import base64 +from sleekxmpp.util import bytes from sleekxmpp.exceptions import XMPPError from sleekxmpp.xmlstream import ElementBase -from sleekxmpp.thirdparty.suelta.util import bytes VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*') @@ -14,7 +14,7 @@ def to_b64(data): def from_b64(data): - return bytes(base64.b64decode(bytes(data))).decode('utf-8') + return bytes(base64.b64decode(bytes(data))) class Open(ElementBase): diff --git a/sleekxmpp/plugins/xep_0047/stream.py b/sleekxmpp/plugins/xep_0047/stream.py index 49f56f36..9651edf8 100644 --- a/sleekxmpp/plugins/xep_0047/stream.py +++ b/sleekxmpp/plugins/xep_0047/stream.py @@ -1,11 +1,9 @@ import socket import threading import logging -try: - import queue -except ImportError: - import Queue as queue +from sleekxmpp.stanza import Iq +from sleekxmpp.util import Queue from sleekxmpp.exceptions import XMPPError @@ -14,14 +12,17 @@ log = logging.getLogger(__name__) class IBBytestream(object): - def __init__(self, xmpp, sid, block_size, to, ifrom, window_size=1): + def __init__(self, xmpp, sid, block_size, jid, peer, window_size=1, use_messages=False): self.xmpp = xmpp self.sid = sid self.block_size = block_size self.window_size = window_size + self.use_messages = use_messages - self.receiver = to - self.sender = ifrom + if jid is None: + jid = xmpp.boundjid + self.self_jid = jid + self.peer_jid = peer self.send_seq = -1 self.recv_seq = -1 @@ -33,7 +34,7 @@ class IBBytestream(object): self.stream_in_closed = threading.Event() self.stream_out_closed = threading.Event() - self.recv_queue = queue.Queue() + self.recv_queue = Queue() self.send_window = threading.BoundedSemaphore(value=self.window_size) self.window_ids = set() @@ -49,16 +50,27 @@ class IBBytestream(object): with self._send_seq_lock: self.send_seq = (self.send_seq + 1) % 65535 seq = self.send_seq - iq = self.xmpp.Iq() - iq['type'] = 'set' - iq['to'] = self.receiver - iq['from'] = self.sender - iq['ibb_data']['sid'] = self.sid - iq['ibb_data']['seq'] = seq - iq['ibb_data']['data'] = data - self.window_empty.clear() - self.window_ids.add(iq['id']) - iq.send(block=False, callback=self._recv_ack) + if self.use_messages: + msg = self.xmpp.Message() + msg['to'] = self.peer_jid + msg['from'] = self.self_jid + msg['id'] = self.xmpp.new_id() + msg['ibb_data']['sid'] = self.sid + msg['ibb_data']['seq'] = seq + msg['ibb_data']['data'] = data + msg.send() + self.send_window.release() + else: + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = self.peer_jid + iq['from'] = self.self_jid + iq['ibb_data']['sid'] = self.sid + iq['ibb_data']['seq'] = seq + iq['ibb_data']['data'] = data + self.window_empty.clear() + self.window_ids.add(iq['id']) + iq.send(block=False, callback=self._recv_ack) return len(data) def sendall(self, data): @@ -74,23 +86,25 @@ class IBBytestream(object): if iq['type'] == 'error': self.close() - def _recv_data(self, iq): + def _recv_data(self, stanza): with self._recv_seq_lock: - new_seq = iq['ibb_data']['seq'] + new_seq = stanza['ibb_data']['seq'] if new_seq != (self.recv_seq + 1) % 65535: self.close() raise XMPPError('unexpected-request') self.recv_seq = new_seq - data = iq['ibb_data']['data'] + data = stanza['ibb_data']['data'] if len(data) > self.block_size: self.close() raise XMPPError('not-acceptable') self.recv_queue.put(data) self.xmpp.event('ibb_stream_data', {'stream': self, 'data': data}) - iq.reply() - iq.send() + + if isinstance(stanza, Iq): + stanza.reply() + stanza.send() def recv(self, *args, **kwargs): return self.read(block=True) @@ -109,8 +123,8 @@ class IBBytestream(object): def close(self): iq = self.xmpp.Iq() iq['type'] = 'set' - iq['to'] = self.receiver - iq['from'] = self.sender + iq['to'] = self.peer_jid + iq['from'] = self.self_jid iq['ibb_close']['sid'] = self.sid self.stream_out_closed.set() iq.send(block=False, @@ -120,9 +134,6 @@ class IBBytestream(object): def _closed(self, iq): self.stream_in_closed.set() self.stream_out_closed.set() - while not self.window_empty.is_set(): - log.info('waiting for send window to empty') - self.window_empty.wait(timeout=1) iq.reply() iq.send() self.xmpp.event('ibb_stream_end', self) diff --git a/sleekxmpp/plugins/xep_0048/__init__.py b/sleekxmpp/plugins/xep_0048/__init__.py new file mode 100644 index 00000000..2c98d061 --- /dev/null +++ b/sleekxmpp/plugins/xep_0048/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0048.stanza import Bookmarks, Conference, URL +from sleekxmpp.plugins.xep_0048.bookmarks import XEP_0048 + + +register_plugin(XEP_0048) diff --git a/sleekxmpp/plugins/xep_0048/bookmarks.py b/sleekxmpp/plugins/xep_0048/bookmarks.py new file mode 100644 index 00000000..0bb5ae38 --- /dev/null +++ b/sleekxmpp/plugins/xep_0048/bookmarks.py @@ -0,0 +1,76 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp import Iq +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0048 import stanza, Bookmarks, Conference, URL + + +log = logging.getLogger(__name__) + + +class XEP_0048(BasePlugin): + + name = 'xep_0048' + description = 'XEP-0048: Bookmarks' + dependencies = set(['xep_0045', 'xep_0049', 'xep_0060', 'xep_0163', 'xep_0223']) + stanza = stanza + default_config = { + 'auto_join': False, + 'storage_method': 'xep_0049' + } + + def plugin_init(self): + register_stanza_plugin(self.xmpp['xep_0060'].stanza.Item, Bookmarks) + + self.xmpp['xep_0049'].register(Bookmarks) + self.xmpp['xep_0163'].register_pep('bookmarks', Bookmarks) + + self.xmpp.add_event_handler('session_start', self._autojoin) + + def plugin_end(self): + self.xmpp.del_event_handler('session_start', self._autojoin) + + def _autojoin(self, __): + if not self.auto_join: + return + + try: + result = self.get_bookmarks(method=self.storage_method) + except XMPPError: + return + + if self.storage_method == 'xep_0223': + bookmarks = result['pubsub']['items']['item']['bookmarks'] + else: + bookmarks = result['private']['bookmarks'] + + for conf in bookmarks['conferences']: + if conf['autojoin']: + log.debug('Auto joining %s as %s', conf['jid'], conf['nick']) + self.xmpp['xep_0045'].joinMUC(conf['jid'], conf['nick'], + password=conf['password']) + + def set_bookmarks(self, bookmarks, method=None, **iqargs): + if not method: + method = self.storage_method + return self.xmpp[method].store(bookmarks, **iqargs) + + def get_bookmarks(self, method=None, **iqargs): + if not method: + method = self.storage_method + + loc = 'storage:bookmarks' if method == 'xep_0223' else 'bookmarks' + + return self.xmpp[method].retrieve(loc, **iqargs) diff --git a/sleekxmpp/plugins/xep_0048/stanza.py b/sleekxmpp/plugins/xep_0048/stanza.py new file mode 100644 index 00000000..21829392 --- /dev/null +++ b/sleekxmpp/plugins/xep_0048/stanza.py @@ -0,0 +1,65 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin + + +class Bookmarks(ElementBase): + name = 'storage' + namespace = 'storage:bookmarks' + plugin_attrib = 'bookmarks' + interfaces = set() + + def add_conference(self, jid, nick, name=None, autojoin=None, password=None): + conf = Conference() + conf['jid'] = jid + conf['nick'] = nick + if name is None: + name = jid + conf['name'] = name + conf['autojoin'] = autojoin + conf['password'] = password + self.append(conf) + + def add_url(self, url, name=None): + saved_url = URL() + saved_url['url'] = url + if name is None: + name = url + saved_url['name'] = name + self.append(saved_url) + + +class Conference(ElementBase): + name = 'conference' + namespace = 'storage:bookmarks' + plugin_attrib = 'conference' + plugin_multi_attrib = 'conferences' + interfaces = set(['nick', 'password', 'autojoin', 'jid', 'name']) + sub_interfaces = set(['nick', 'password']) + + def get_autojoin(self): + value = self._get_attr('autojoin') + return value in ('1', 'true') + + def set_autojoin(self, value): + del self['autojoin'] + if value in ('1', 'true', True): + self._set_attr('autojoin', 'true') + + +class URL(ElementBase): + name = 'url' + namespace = 'storage:bookmarks' + plugin_attrib = 'url' + plugin_multi_attrib = 'urls' + interfaces = set(['url', 'name']) + + +register_stanza_plugin(Bookmarks, Conference, iterable=True) +register_stanza_plugin(Bookmarks, URL, iterable=True) diff --git a/sleekxmpp/plugins/xep_0049/__init__.py b/sleekxmpp/plugins/xep_0049/__init__.py new file mode 100644 index 00000000..b0c4f904 --- /dev/null +++ b/sleekxmpp/plugins/xep_0049/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0049.stanza import PrivateXML +from sleekxmpp.plugins.xep_0049.private_storage import XEP_0049 + + +register_plugin(XEP_0049) diff --git a/sleekxmpp/plugins/xep_0049/private_storage.py b/sleekxmpp/plugins/xep_0049/private_storage.py new file mode 100644 index 00000000..ef6cbdde --- /dev/null +++ b/sleekxmpp/plugins/xep_0049/private_storage.py @@ -0,0 +1,53 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp import Iq +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0049 import stanza, PrivateXML + + +log = logging.getLogger(__name__) + + +class XEP_0049(BasePlugin): + + name = 'xep_0049' + description = 'XEP-0049: Private XML Storage' + dependencies = set([]) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, PrivateXML) + + def register(self, stanza): + register_stanza_plugin(PrivateXML, stanza, iterable=True) + + def store(self, data, ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + + if not isinstance(data, list): + data = [data] + + for elem in data: + iq['private'].append(elem) + + return iq.send(block=block, timeout=timeout, callback=callback) + + def retrieve(self, name, ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['from'] = ifrom + iq['private'].enable(name) + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0049/stanza.py b/sleekxmpp/plugins/xep_0049/stanza.py new file mode 100644 index 00000000..d424e2f0 --- /dev/null +++ b/sleekxmpp/plugins/xep_0049/stanza.py @@ -0,0 +1,17 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ET, ElementBase + + +class PrivateXML(ElementBase): + + name = 'query' + namespace = 'jabber:iq:private' + plugin_attrib = 'private' + interfaces = set() diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py index a833221a..e5594c3f 100644 --- a/sleekxmpp/plugins/xep_0050/adhoc.py +++ b/sleekxmpp/plugins/xep_0050/adhoc.py @@ -82,12 +82,18 @@ class XEP_0050(BasePlugin): description = 'XEP-0050: Ad-Hoc Commands' dependencies = set(['xep_0030', 'xep_0004']) stanza = stanza + default_config = { + 'threaded': True, + 'session_db': None + } def plugin_init(self): """Start the XEP-0050 plugin.""" - self.threaded = self.config.get('threaded', True) + self.sessions = self.session_db + if self.sessions is None: + self.sessions = {} + self.commands = {} - self.sessions = self.config.get('session_db', {}) self.xmpp.register_handler( Callback("Ad-Hoc Execute", @@ -181,12 +187,6 @@ class XEP_0050(BasePlugin): jid = JID(jid) item_jid = jid.full - # Client disco uses only the bare JID - if self.xmpp.is_component: - jid = jid.full - else: - jid = jid.bare - self.xmpp['xep_0030'].add_identity(category='automation', itype='command-list', name='Ad-Hoc commands', @@ -267,20 +267,50 @@ class XEP_0050(BasePlugin): iq -- The command continuation request. """ sessionid = iq['command']['sessionid'] - session = self.sessions[sessionid] + session = self.sessions.get(sessionid) - handler = session['next'] - interfaces = session['interfaces'] - results = [] - for stanza in iq['command']['substanzas']: - if stanza.plugin_attrib in interfaces: - results.append(stanza) - if len(results) == 1: - results = results[0] + if session: + handler = session['next'] + interfaces = session['interfaces'] + results = [] + for stanza in iq['command']['substanzas']: + if stanza.plugin_attrib in interfaces: + results.append(stanza) + if len(results) == 1: + results = results[0] - session = handler(results, session) + session = handler(results, session) - self._process_command_response(iq, session) + self._process_command_response(iq, session) + else: + raise XMPPError('item-not-found') + + def _handle_command_prev(self, iq): + """ + Process a request for the prev step in the workflow + for a command with multiple steps. + + Arguments: + iq -- The command continuation request. + """ + sessionid = iq['command']['sessionid'] + session = self.sessions.get(sessionid) + + if session: + handler = session['prev'] + interfaces = session['interfaces'] + results = [] + for stanza in iq['command']['substanzas']: + if stanza.plugin_attrib in interfaces: + results.append(stanza) + if len(results) == 1: + results = results[0] + + session = handler(results, session) + + self._process_command_response(iq, session) + else: + raise XMPPError('item-not-found') def _process_command_response(self, iq, session): """ @@ -348,23 +378,23 @@ class XEP_0050(BasePlugin): """ node = iq['command']['node'] sessionid = iq['command']['sessionid'] - session = self.sessions[sessionid] - handler = session['cancel'] - if handler: - handler(iq, session) + session = self.sessions.get(sessionid) - try: + if session: + handler = session['cancel'] + if handler: + handler(iq, session) del self.sessions[sessionid] - except: - pass + iq.reply() + iq['command']['node'] = node + iq['command']['sessionid'] = sessionid + iq['command']['status'] = 'canceled' + iq['command']['notes'] = session['notes'] + iq.send() + else: + raise XMPPError('item-not-found') - iq.reply() - iq['command']['node'] = node - iq['command']['sessionid'] = sessionid - iq['command']['status'] = 'canceled' - iq['command']['notes'] = session['notes'] - iq.send() def _handle_command_complete(self, iq): """ @@ -378,28 +408,32 @@ class XEP_0050(BasePlugin): """ node = iq['command']['node'] sessionid = iq['command']['sessionid'] - session = self.sessions[sessionid] - handler = session['next'] - interfaces = session['interfaces'] - results = [] - for stanza in iq['command']['substanzas']: - if stanza.plugin_attrib in interfaces: - results.append(stanza) - if len(results) == 1: - results = results[0] + session = self.sessions.get(sessionid) - if handler: - handler(results, session) + if session: + handler = session['next'] + interfaces = session['interfaces'] + results = [] + for stanza in iq['command']['substanzas']: + if stanza.plugin_attrib in interfaces: + results.append(stanza) + if len(results) == 1: + results = results[0] - iq.reply() - iq['command']['node'] = node - iq['command']['sessionid'] = sessionid - iq['command']['actions'] = [] - iq['command']['status'] = 'completed' - iq['command']['notes'] = session['notes'] - iq.send() + if handler: + handler(results, session) + + del self.sessions[sessionid] - del self.sessions[sessionid] + iq.reply() + iq['command']['node'] = node + iq['command']['sessionid'] = sessionid + iq['command']['actions'] = [] + iq['command']['status'] = 'completed' + iq['command']['notes'] = session['notes'] + iq.send() + else: + raise XMPPError('item-not-found') # ================================================================= # Client side (command user) API @@ -537,7 +571,7 @@ class XEP_0050(BasePlugin): else: iq.send(block=False, callback=self._handle_command_result) - def continue_command(self, session): + def continue_command(self, session, direction='next'): """ Execute the next action of the command. @@ -551,7 +585,7 @@ class XEP_0050(BasePlugin): self.send_command(session['jid'], session['node'], ifrom=session.get('from', None), - action='next', + action=direction, payload=session.get('payload', None), sessionid=session['id'], flow=True, diff --git a/sleekxmpp/plugins/xep_0054/stanza.py b/sleekxmpp/plugins/xep_0054/stanza.py index 75b69d3e..72da0b51 100644 --- a/sleekxmpp/plugins/xep_0054/stanza.py +++ b/sleekxmpp/plugins/xep_0054/stanza.py @@ -1,8 +1,7 @@ import base64 import datetime as dt -from sleekxmpp.thirdparty.suelta.util import bytes - +from sleekxmpp.util import bytes from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin, JID from sleekxmpp.plugins import xep_0082 @@ -542,6 +541,7 @@ register_stanza_plugin(VCardTemp, Logo, iterable=True) register_stanza_plugin(VCardTemp, Mailer, iterable=True) register_stanza_plugin(VCardTemp, Note, iterable=True) register_stanza_plugin(VCardTemp, Nickname, iterable=True) +register_stanza_plugin(VCardTemp, Org, iterable=True) register_stanza_plugin(VCardTemp, Photo, iterable=True) register_stanza_plugin(VCardTemp, ProdID, iterable=True) register_stanza_plugin(VCardTemp, Rev, iterable=True) diff --git a/sleekxmpp/plugins/xep_0054/vcard_temp.py b/sleekxmpp/plugins/xep_0054/vcard_temp.py index 83cbccf8..97da8c7c 100644 --- a/sleekxmpp/plugins/xep_0054/vcard_temp.py +++ b/sleekxmpp/plugins/xep_0054/vcard_temp.py @@ -8,7 +8,7 @@ import logging -from sleekxmpp import Iq +from sleekxmpp import JID, Iq from sleekxmpp.exceptions import XMPPError from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback @@ -59,10 +59,20 @@ class XEP_0054(BasePlugin): def make_vcard(self): return VCardTemp() - def get_vcard(self, jid=None, ifrom=None, local=False, cached=False, + def get_vcard(self, jid=None, ifrom=None, local=None, cached=False, block=True, callback=None, timeout=None): - if self.xmpp.is_component and jid.domain == self.xmpp.boundjid.domain: - local = True + if local is None: + if jid is not None and not isinstance(jid, JID): + jid = JID(jid) + if self.xmpp.is_component: + if jid.domain == self.xmpp.boundjid.domain: + local = True + else: + if str(jid) == str(self.xmpp.boundjid): + local = True + jid = jid.full + elif jid in (None, ''): + local = True if local: vcard = self.api['get_vcard'](jid, None, ifrom) @@ -97,8 +107,8 @@ class XEP_0054(BasePlugin): def publish_vcard(self, vcard=None, jid=None, block=True, ifrom=None, callback=None, timeout=None): + self.api['set_vcard'](jid, None, ifrom, vcard) if self.xmpp.is_component: - self.api['set_vcard'](jid, None, ifrom, vcard) return iq = self.xmpp.Iq() diff --git a/sleekxmpp/plugins/xep_0059/rsm.py b/sleekxmpp/plugins/xep_0059/rsm.py index 59cfc10b..d73b45bc 100644 --- a/sleekxmpp/plugins/xep_0059/rsm.py +++ b/sleekxmpp/plugins/xep_0059/rsm.py @@ -25,11 +25,14 @@ class ResultIterator(): An iterator for Result Set Managment """ - def __init__(self, query, interface, amount=10, start=None, reverse=False): + def __init__(self, query, interface, results='substanzas', amount=10, + start=None, reverse=False): """ Arguments: query -- The template query interface -- The substanza of the query, for example disco_items + results -- The query stanza's interface which provides a + countable list of query results. amount -- The max amounts of items to request per iteration start -- From which item id to start reverse -- If True, page backwards through the results @@ -46,6 +49,7 @@ class ResultIterator(): self.amount = amount self.start = start self.interface = interface + self.results = results self.reverse = reverse self._stop = False @@ -85,7 +89,7 @@ class ResultIterator(): r[self.interface]['rsm']['first_index']: count = int(r[self.interface]['rsm']['count']) first = int(r[self.interface]['rsm']['first_index']) - num_items = len(r[self.interface]['substanzas']) + num_items = len(r[self.interface][self.results]) if first + num_items == count: self._stop = True @@ -123,7 +127,7 @@ class XEP_0059(BasePlugin): def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Set.namespace) - def iterate(self, stanza, interface): + def iterate(self, stanza, interface, results='substanzas'): """ Create a new result set iterator for a given stanza query. @@ -135,5 +139,7 @@ class XEP_0059(BasePlugin): result set management stanza should be appended. For example, for disco#items queries the interface 'disco_items' should be used. + results -- The name of the interface containing the + query results (typically just 'substanzas'). """ - return ResultIterator(stanza, interface) + return ResultIterator(stanza, interface, results) diff --git a/sleekxmpp/plugins/xep_0060/pubsub.py b/sleekxmpp/plugins/xep_0060/pubsub.py index 387c5a0f..bec5f565 100644 --- a/sleekxmpp/plugins/xep_0060/pubsub.py +++ b/sleekxmpp/plugins/xep_0060/pubsub.py @@ -26,7 +26,7 @@ class XEP_0060(BasePlugin): name = 'xep_0060' description = 'XEP-0060: Publish-Subscribe' - dependencies = set(['xep_0030', 'xep_0004']) + dependencies = set(['xep_0030', 'xep_0004', 'xep_0082', 'xep_0131']) stanza = stanza def plugin_init(self): @@ -53,6 +53,8 @@ class XEP_0060(BasePlugin): StanzaPath('message/pubsub_event/subscription'), self._handle_event_subscription)) + self.xmpp['xep_0131'].supported_headers.add('SubID') + def plugin_end(self): self.xmpp.remove_handler('Pubsub Event: Items') self.xmpp.remove_handler('Pubsub Event: Purge') @@ -421,7 +423,7 @@ class XEP_0060(BasePlugin): callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub_owner']['configure']['node'] = node - iq['pubsub_owner']['configure']['form'].values = config.values + iq['pubsub_owner']['configure'].append(config) return iq.send(block=block, callback=callback, timeout=timeout) def publish(self, jid, node, id=None, payload=None, options=None, diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py index b2fe3010..c1907a13 100644 --- a/sleekxmpp/plugins/xep_0060/stanza/pubsub.py +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py @@ -74,7 +74,12 @@ class Item(ElementBase): def set_payload(self, value): del self['payload'] - self.append(value) + if isinstance(value, ElementBase): + if value.tag_name() in self.plugin_tag_map: + self.init_plugin(value.plugin_attrib, existing_xml=value.xml) + self.xml.append(value.xml) + else: + self.xml.append(value) def get_payload(self): childs = list(self.xml) @@ -243,39 +248,6 @@ class PublishOptions(ElementBase): self.parent().xml.remove(self.xml) -class PubsubState(ElementBase): - """This is an experimental pubsub extension.""" - namespace = 'http://jabber.org/protocol/psstate' - name = 'state' - plugin_attrib = 'psstate' - interfaces = set(('node', 'item', 'payload')) - - def set_payload(self, value): - self.xml.append(value) - - def get_payload(self): - childs = list(self.xml) - if len(childs) > 0: - return childs[0] - - def del_payload(self): - for child in self.xml: - self.xml.remove(child) - - -class PubsubStateEvent(ElementBase): - """This is an experimental pubsub extension.""" - namespace = 'http://jabber.org/protocol/psstate#event' - name = 'event' - plugin_attrib = 'psstate_event' - intefaces = set(tuple()) - - -register_stanza_plugin(Iq, PubsubState) -register_stanza_plugin(Message, PubsubStateEvent) -register_stanza_plugin(PubsubStateEvent, PubsubState) - - register_stanza_plugin(Iq, Pubsub) register_stanza_plugin(Pubsub, Affiliations) register_stanza_plugin(Pubsub, Configure) diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py index 4a35db9d..d975a46d 100644 --- a/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py @@ -34,7 +34,8 @@ class DefaultConfig(ElementBase): return self['form'] def set_config(self, value): - self['form'].values = value.values + del self['from'] + self.append(value) return self @@ -93,7 +94,9 @@ class OwnerRedirect(ElementBase): class OwnerSubscriptions(Subscriptions): + name = 'subscriptions' namespace = 'http://jabber.org/protocol/pubsub#owner' + plugin_attrib = name interfaces = set(('node',)) def append(self, subscription): diff --git a/sleekxmpp/plugins/xep_0065/__init__.py b/sleekxmpp/plugins/xep_0065/__init__.py index c577d859..feca2ef1 100644 --- a/sleekxmpp/plugins/xep_0065/__init__.py +++ b/sleekxmpp/plugins/xep_0065/__init__.py @@ -1,4 +1,6 @@ from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0065.stanza import Socks5 from sleekxmpp.plugins.xep_0065.proxy import XEP_0065 diff --git a/sleekxmpp/plugins/xep_0065/proxy.py b/sleekxmpp/plugins/xep_0065/proxy.py index b027e4e0..d890b57a 100644 --- a/sleekxmpp/plugins/xep_0065/proxy.py +++ b/sleekxmpp/plugins/xep_0065/proxy.py @@ -1,359 +1,292 @@ -import sys import logging -import struct +import threading +import socket -from threading import Thread, Event from hashlib import sha1 -from select import select from uuid import uuid4 -from sleekxmpp.plugins.xep_0065 import stanza +from sleekxmpp.thirdparty.socks import socksocket, PROXY_TYPE_SOCKS5 -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.stanza import Iq +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.thirdparty.socks import socksocket, PROXY_TYPE_SOCKS5 +from sleekxmpp.plugins.base import base_plugin + +from sleekxmpp.plugins.xep_0065 import stanza, Socks5 + -# Registers the sleekxmpp logger log = logging.getLogger(__name__) class XEP_0065(base_plugin): - """ - XEP-0065 Socks5 Bytestreams - """ - description = "Socks5 Bytestreams" - dependencies = set(['xep_0030', ]) - xep = '0065' name = 'xep_0065' - - # A dict contains for each SID, the proxy thread currently - # running. - proxy_threads = {} + description = "Socks5 Bytestreams" + dependencies = set(['xep_0030']) + default_config = { + 'auto_accept': False + } def plugin_init(self): - """ Initializes the xep_0065 plugin and all event callbacks. - """ + register_stanza_plugin(Iq, Socks5) - # Shortcuts to access to the xep_0030 plugin. - self.disco = self.xmpp['xep_0030'] + self._proxies = {} + self._sessions = {} + self._sessions_lock = threading.Lock() - # Handler for the streamhost stanza. - self.xmpp.registerHandler( + self._preauthed_sids_lock = threading.Lock() + self._preauthed_sids = {} + + self.xmpp.register_handler( Callback('Socks5 Bytestreams', StanzaPath('iq@type=set/socks/streamhost'), self._handle_streamhost)) - # Handler for the streamhost-used stanza. - self.xmpp.registerHandler( - Callback('Socks5 Bytestreams', - StanzaPath('iq@type=result/socks/streamhost-used'), - self._handle_streamhost_used)) + self.api.register(self._authorized, 'authorized', default=True) + self.api.register(self._authorized_sid, 'authorized_sid', default=True) + self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True) - def get_socket(self, sid): - """ Returns the socket associated to the SID. - """ + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(Socks5.namespace) + + def plugin_end(self): + self.xmpp.remove_handler('Socks5 Bytestreams') + self.xmpp.remove_handler('Socks5 Streamhost Used') + self.xmpp['xep_0030'].del_feature(feature=Socks5.namespace) - proxy = self.proxy_threads.get(sid) - if proxy: - return proxy.s + def get_socket(self, sid): + """Returns the socket associated to the SID.""" + return self._sessions.get(sid, None) - def handshake(self, to, streamer=None): + def handshake(self, to, ifrom=None, sid=None, timeout=None): """ Starts the handshake to establish the socks5 bytestreams connection. """ - - # Discovers the proxy. - self.streamer = streamer or self.discover_proxy() - - # Requester requests network address from the proxy. - streamhost = self.get_network_address(self.streamer) - self.proxy_host = streamhost['socks']['streamhost']['host'] - self.proxy_port = streamhost['socks']['streamhost']['port'] - - # Generates the SID for this new handshake. - sid = uuid4().hex - - # Requester initiates S5B negotation with Target by sending + if not self._proxies: + self._proxies = self.discover_proxies() + + if sid is None: + sid = uuid4().hex + + used = self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout) + proxy = used['socks']['streamhost_used']['jid'] + + if proxy not in self._proxies: + log.warning('Received unknown SOCKS5 proxy: %s', proxy) + return + + with self._sessions_lock: + self._sessions[sid] = self._connect_proxy( + sid, + self.xmpp.boundjid, + to, + self._proxies[proxy][0], + self._proxies[proxy][1], + peer=to) + + # Request that the proxy activate the session with the target. + self.activate(proxy, sid, to, timeout=timeout) + socket = self.get_socket(sid) + self.xmpp.event('stream:%s:%s' % (sid, to), socket) + return socket + + def request_stream(self, to, sid=None, ifrom=None, block=True, timeout=None, callback=None): + if sid is None: + sid = uuid4().hex + + # Requester initiates S5B negotiation with Target by sending # IQ-set that includes the JabberID and network address of # StreamHost as well as the StreamID (SID) of the proposed # bytestream. - iq = self.xmpp.Iq(sto=to, stype='set') + iq = self.xmpp.Iq() + iq['to'] = to + iq['from'] = ifrom + iq['type'] = 'set' iq['socks']['sid'] = sid - iq['socks']['streamhost']['jid'] = self.streamer - iq['socks']['streamhost']['host'] = self.proxy_host - iq['socks']['streamhost']['port'] = self.proxy_port - - # Sends the new IQ. - return iq.send() + for proxy, (host, port) in self._proxies.items(): + iq['socks'].add_streamhost(proxy, host, port) + return iq.send(block=block, timeout=timeout, callback=callback) + + def discover_proxies(self, jid=None, ifrom=None, timeout=None): + """Auto-discover the JIDs of SOCKS5 proxies on an XMPP server.""" + if jid is None: + if self.xmpp.is_component: + jid = self.xmpp.server + else: + jid = self.xmpp.boundjid.server - def discover_proxy(self): - """ Auto-discovers (using XEP 0030) the available bytestream - proxy on the XMPP server. + discovered = set() - Returns the JID of the proxy. - """ - - # Gets all disco items. - disco_items = self.disco.get_items(self.xmpp.server) + disco_items = self.xmpp['xep_0030'].get_items(jid, timeout=timeout) for item in disco_items['disco_items']['items']: - # For each items, gets the disco info. - disco_info = self.disco.get_info(item[0]) - - # Gets and verifies if the identity is a bytestream proxy. - identities = disco_info['disco_info']['identities'] - for identity in identities: - if identity[0] == 'proxy' and identity[1] == 'bytestreams': - # Returns when the first occurence is found. - return '%s' % disco_info['from'] - - def get_network_address(self, streamer): - """ Gets the streamhost information of the proxy. + try: + disco_info = self.xmpp['xep_0030'].get_info(item[0], timeout=timeout) + except XMPPError: + continue + else: + # Verify that the identity is a bytestream proxy. + identities = disco_info['disco_info']['identities'] + for identity in identities: + if identity[0] == 'proxy' and identity[1] == 'bytestreams': + discovered.add(disco_info['from']) - streamer : The jid of the proxy. - """ + for jid in discovered: + try: + addr = self.get_network_address(jid, ifrom=ifrom, timeout=timeout) + self._proxies[jid] = (addr['socks']['streamhost']['host'], + addr['socks']['streamhost']['port']) + except XMPPError: + continue - iq = self.xmpp.Iq(sto=streamer, stype='get') - iq['socks'] # Adds the query eleme to the iq. + return self._proxies - return iq.send() + def get_network_address(self, proxy, ifrom=None, block=True, timeout=None, callback=None): + """Get the network information of a proxy.""" + iq = self.xmpp.Iq(sto=proxy, stype='get', sfrom=ifrom) + iq.enable('socks') + return iq.send(block=block, timeout=timeout, callback=callback) def _handle_streamhost(self, iq): - """ Handles all streamhost stanzas. - """ - - # Registers the streamhost info. - self.streamer = iq['socks']['streamhost']['jid'] - self.proxy_host = iq['socks']['streamhost']['host'] - self.proxy_port = iq['socks']['streamhost']['port'] - - # Sets the SID, the requester and the target. - sid = iq['socks']['sid'] - requester = '%s' % iq['from'] - target = '%s' % self.xmpp.boundjid - - # Next the Target attempts to open a standard TCP socket on - # the network address of the Proxy. - self.proxy_thread = Proxy(sid, requester, target, self.proxy_host, - self.proxy_port, self.on_recv) - self.proxy_thread.start() - - # Registers the new thread in the proxy_thread dict. - self.proxy_threads[sid] = self.proxy_thread - - # Wait until the proxy is connected - self.proxy_thread.connected.wait() - - # Replies to the incoming iq with a streamhost-used stanza. - res_iq = iq.reply() - res_iq['socks']['sid'] = sid - res_iq['socks']['streamhost-used']['jid'] = self.streamer - - # Sends the IQ - return res_iq.send() - - def _handle_streamhost_used(self, iq): - """ Handles all streamhost-used stanzas. - """ - - # Sets the SID, the requester and the target. + """Handle incoming SOCKS5 session request.""" sid = iq['socks']['sid'] - requester = '%s' % self.xmpp.boundjid - target = '%s' % iq['from'] - - # The Requester will establish a connection to the SOCKS5 - # proxy in the same way the Target did. - self.proxy_thread = Proxy(sid, requester, target, self.proxy_host, - self.proxy_port, self.on_recv) - self.proxy_thread.start() - - # Registers the new thread in the proxy_thread dict. - self.proxy_threads[sid] = self.proxy_thread + if not sid: + raise XMPPError(etype='modify', condition='bad-request') - # Wait until the proxy is connected - self.proxy_thread.connected.wait() + if not self._accept_stream(iq): + raise XMPPError(etype='modify', condition='not-acceptable') - # Requester sends IQ-set to StreamHost requesting that - # StreamHost activate the bytestream associated with the - # StreamID. - self.activate(iq['socks']['sid'], target) + streamhosts = iq['socks']['streamhosts'] + conn = None + used_streamhost = None - def activate(self, sid, to): - """ IQ-set to StreamHost requesting that StreamHost activate - the bytestream associated with the StreamID. - """ - - # Creates the activate IQ. - act_iq = self.xmpp.Iq(sto=self.streamer, stype='set') - act_iq['socks']['sid'] = sid - act_iq['socks']['activate'] = to - - # Send the IQ. - act_iq.send() + sender = iq['from'] + for streamhost in streamhosts: + try: + conn = self._connect_proxy(sid, + sender, + self.xmpp.boundjid, + streamhost['host'], + streamhost['port'], + peer=sender) + used_streamhost = streamhost['jid'] + break + except socket.error: + continue + else: + raise XMPPError(etype='cancel', condition='item-not-found') + + iq.reply() + with self._sessions_lock: + self._sessions[sid] = conn + iq['socks']['sid'] = sid + iq['socks']['streamhost_used']['jid'] = used_streamhost + iq.send() + self.xmpp.event('socks5_stream', conn) + self.xmpp.event('stream:%s:%s' % (sid, conn.peer_jid), conn) + + def activate(self, proxy, sid, target, ifrom=None, block=True, timeout=None, callback=None): + """Activate the socks5 session that has been negotiated.""" + iq = self.xmpp.Iq(sto=proxy, stype='set', sfrom=ifrom) + iq['socks']['sid'] = sid + iq['socks']['activate'] = target + iq.send(block=block, timeout=timeout, callback=callback) def deactivate(self, sid): - """ Closes the Proxy thread associated to this SID. - """ - - proxy = self.proxy_threads.get(sid) - if proxy: - proxy.s.close() - del self.proxy_threads[sid] + """Closes the proxy socket associated with this SID.""" + sock = self._sessions.get(sid) + if sock: + try: + # sock.close() will also delete sid from self._sessions (see _connect_proxy) + sock.close() + except socket.error: + pass + # Though this should not be neccessary remove the closed session anyway + with self._sessions_lock: + if sid in self._sessions: + log.warn(('SOCKS5 session with sid = "%s" was not ' + + 'removed from _sessions by sock.close()') % sid) + del self._sessions[sid] def close(self): - """ Closes all Proxy threads. - """ - - for sid, proxy in self.proxy_threads.items(): - proxy.s.close() - del self.proxy_threads[sid] - - def send(self, sid, data): - """ Sends the data over the Proxy socket associated to the - SID. - """ - - proxy = self.proxy_threads.get(sid) - if proxy: - proxy.s.sendall(data) + """Closes all proxy sockets.""" + for sid, sock in self._sessions.items(): + sock.close() + with self._sessions_lock: + self._sessions = {} - def on_recv(self, sid, data): - """ Calls when data is recv from the Proxy socket associated - to the SID. - - Triggers a socks_closed event if the socket is closed. The sid - is passed to this event. - - Triggers a socks_recv event if there's available data. A dict - that contains the sid and the data is passed to this event. - """ - - proxy = self.proxy_threads.get(sid) - if proxy: - if not data: - self.xmpp.event('socks_closed', sid) - else: - self.xmpp.event('socks_recv', {'sid': sid, 'data': data}) - - -class Proxy(Thread): - """ Establishes in a thread a connection between the client and - the server-side Socks5 proxy. - """ - - def __init__(self, sid, requester, target, proxy, proxy_port, - on_recv): - """ Initializes the proxy thread. + def _connect_proxy(self, sid, requester, target, proxy, proxy_port, peer=None): + """ Establishes a connection between the client and the server-side + Socks5 proxy. sid : The StreamID. <str> requester : The JID of the requester. <str> target : The JID of the target. <str> proxy_host : The hostname or the IP of the proxy. <str> proxy_port : The port of the proxy. <str> or <int> - on_recv : A callback called when data are received from the - socket. <Callable> + peer : The JID for the other side of the stream, regardless + of target or requester status. """ - - # Initializes the thread. - Thread.__init__(self) - # Because the xep_0065 plugin uses the proxy_port as string, # the Proxy class accepts the proxy_port argument as a string # or an integer. Here, we force to use the port as an integer. proxy_port = int(proxy_port) - # Creates a connected event to warn when to proxy is - # connected. - self.connected = Event() - - # Registers the arguments. - self.sid = sid - self.requester = requester - self.target = target - self.proxy = proxy - self.proxy_port = proxy_port - self.on_recv = on_recv - - def run(self): - """ Starts the thread. - """ - - # Creates the socks5 proxy socket - self.s = socksocket() - self.s.setproxy(PROXY_TYPE_SOCKS5, self.proxy, port=self.proxy_port) + sock = socksocket() + sock.setproxy(PROXY_TYPE_SOCKS5, proxy, port=proxy_port) # The hostname MUST be SHA1(SID + Requester JID + Target JID) # where the output is hexadecimal-encoded (not binary). digest = sha1() - digest.update(self.sid) # SID - digest.update(self.requester) # Requester JID - digest.update(self.target) # Target JID + digest.update(sid.encode('utf-8')) + digest.update(str(requester).encode('utf-8')) + digest.update(str(target).encode('utf-8')) - # Computes the digest in hex. - dest = '%s' % digest.hexdigest() + dest = digest.hexdigest() # The port MUST be 0. - self.s.connect((dest, 0)) + sock.connect((dest, 0)) log.info('Socket connected.') - self.connected.set() - # Blocks until the socket need to be closed. - self.listen() + _close = sock.close + def close(*args, **kwargs): + with self._sessions_lock: + if sid in self._sessions: + del self._sessions[sid] + _close() + log.info('Socket closed.') + sock.close = close - # Closes the socket. - self.s.close() - log.info('Socket closed.') + sock.peer_jid = peer + sock.self_jid = target if requester == peer else requester - def listen(self): - """ Listen for data on the socket. When receiving data, call - the callback on_recv callable. - """ + self.xmpp.event('socks_connected', sid) + return sock - socket_open = True - while socket_open: - ins = [] - try: - # Wait any read available data on socket. Timeout - # after 5 secs. - ins, out, err = select([self.s, ], [], [], 5) - except Exception as e: - # There's an error with the socket (maybe the socket - # has been closed and the file descriptor is bad). - log.debug('Socket error: %s' % e) - break + def _accept_stream(self, iq): + receiver = iq['to'] + sender = iq['from'] + sid = iq['socks']['sid'] - for s in ins: - data = self.recv_size(self.s) - if not data: - socket_open = False - - self.on_recv(self.sid, data) - - def recv_size(self, the_socket): - total_len = 0 - total_data = [] - size = sys.maxint - size_data = sock_data = '' - recv_size = 8192 - - while total_len < size: - sock_data = the_socket.recv(recv_size) - if not sock_data: - return ''.join(total_data) - - if not total_data: - if len(sock_data) > 4: - size_data += sock_data - size = struct.unpack('>i', size_data[:4])[0] - recv_size = size - if recv_size > 524288: - recv_size = 524288 - total_data.append(size_data[4:]) - else: - size_data += sock_data - else: - total_data.append(sock_data) - total_len = sum([len(i) for i in total_data]) - return ''.join(total_data) + if self.api['authorized_sid'](receiver, sid, sender, iq): + return True + return self.api['authorized'](receiver, sid, sender, iq) + + def _authorized(self, jid, sid, ifrom, iq): + return self.auto_accept + + def _authorized_sid(self, jid, sid, ifrom, iq): + with self._preauthed_sids_lock: + log.debug('>>> authed sids: %s', self._preauthed_sids) + log.debug('>>> lookup: %s %s %s', jid, sid, ifrom) + if (jid, sid, ifrom) in self._preauthed_sids: + del self._preauthed_sids[(jid, sid, ifrom)] + return True + return False + + def _preauthorize_sid(self, jid, sid, ifrom, data): + log.debug('>>>> %s %s %s %s', jid, sid, ifrom, data) + with self._preauthed_sids_lock: + self._preauthed_sids[(jid, sid, ifrom)] = True diff --git a/sleekxmpp/plugins/xep_0065/stanza.py b/sleekxmpp/plugins/xep_0065/stanza.py index ae57aba8..e48bf1b5 100644 --- a/sleekxmpp/plugins/xep_0065/stanza.py +++ b/sleekxmpp/plugins/xep_0065/stanza.py @@ -1,41 +1,47 @@ -from sleekxmpp import Iq +from sleekxmpp.jid import JID from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin -# The protocol namespace defined in the Socks5Bytestream (0065) spec. -namespace = 'http://jabber.org/protocol/bytestreams' +class Socks5(ElementBase): + name = 'query' + namespace = 'http://jabber.org/protocol/bytestreams' + plugin_attrib = 'socks' + interfaces = set(['sid', 'activate']) + sub_interfaces = set(['activate']) + def add_streamhost(self, jid, host, port): + sh = StreamHost(parent=self) + sh['jid'] = jid + sh['host'] = host + sh['port'] = port -class StreamHost(ElementBase): - """ The streamhost xml element. - """ - namespace = namespace +class StreamHost(ElementBase): name = 'streamhost' + namespace = 'http://jabber.org/protocol/bytestreams' plugin_attrib = 'streamhost' - interfaces = set(('host', 'jid', 'port')) + plugin_multi_attrib = 'streamhosts' + interfaces = set(['host', 'jid', 'port']) + def set_jid(self, value): + return self._set_attr('jid', str(value)) -class StreamHostUsed(ElementBase): - """ The streamhost-used xml element. - """ + def get_jid(self): + return JID(self._get_attr('jid')) - namespace = namespace + +class StreamHostUsed(ElementBase): name = 'streamhost-used' - plugin_attrib = 'streamhost-used' - interfaces = set(('jid',)) + namespace = 'http://jabber.org/protocol/bytestreams' + plugin_attrib = 'streamhost_used' + interfaces = set(['jid']) + def set_jid(self, value): + return self._set_attr('jid', str(value)) -class Socks5(ElementBase): - """ The query xml element. - """ + def get_jid(self): + return JID(self._get_attr('jid')) - namespace = namespace - name = 'query' - plugin_attrib = 'socks' - interfaces = set(('sid', 'activate')) - sub_interfaces = set(('activate',)) -register_stanza_plugin(Iq, Socks5) -register_stanza_plugin(Socks5, StreamHost) +register_stanza_plugin(Socks5, StreamHost, iterable=True) register_stanza_plugin(Socks5, StreamHostUsed) diff --git a/sleekxmpp/plugins/xep_0071/__init__.py b/sleekxmpp/plugins/xep_0071/__init__.py new file mode 100644 index 00000000..c21e9265 --- /dev/null +++ b/sleekxmpp/plugins/xep_0071/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0071.stanza import XHTML_IM +from sleekxmpp.plugins.xep_0071.xhtml_im import XEP_0071 + + +register_plugin(XEP_0071) diff --git a/sleekxmpp/plugins/xep_0071/stanza.py b/sleekxmpp/plugins/xep_0071/stanza.py new file mode 100644 index 00000000..d5ff1a1b --- /dev/null +++ b/sleekxmpp/plugins/xep_0071/stanza.py @@ -0,0 +1,81 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.stanza import Message +from sleekxmpp.util import unicode +from sleekxmpp.thirdparty import OrderedDict +from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin, tostring + + +XHTML_NS = 'http://www.w3.org/1999/xhtml' + + +class XHTML_IM(ElementBase): + + namespace = 'http://jabber.org/protocol/xhtml-im' + name = 'html' + interfaces = set(['body']) + lang_interfaces = set(['body']) + plugin_attrib = name + + def set_body(self, content, lang=None): + if lang is None: + lang = self.get_lang() + self.del_body(lang) + if lang == '*': + for sublang, subcontent in content.items(): + self.set_body(subcontent, sublang) + else: + if isinstance(content, type(ET.Element('test'))): + content = unicode(ET.tostring(content)) + else: + content = unicode(content) + header = '<body xmlns="%s"' % XHTML_NS + if lang: + header = '%s xml:lang="%s"' % (header, lang) + content = '%s>%s</body>' % (header, content) + xhtml = ET.fromstring(content) + self.xml.append(xhtml) + + def get_body(self, lang=None): + """Return the contents of the HTML body.""" + if lang is None: + lang = self.get_lang() + + bodies = self.xml.findall('{%s}body' % XHTML_NS) + + if lang == '*': + result = OrderedDict() + for body in bodies: + body_lang = body.attrib.get('{%s}lang' % self.xml_ns, '') + body_result = [] + body_result.append(body.text if body.text else '') + for child in body: + body_result.append(tostring(child, xmlns=XHTML_NS)) + body_result.append(body.tail if body.tail else '') + result[body_lang] = ''.join(body_result) + return result + else: + for body in bodies: + if body.attrib.get('{%s}lang' % self.xml_ns, self.get_lang()) == lang: + result = [] + result.append(body.text if body.text else '') + for child in body: + result.append(tostring(child, xmlns=XHTML_NS)) + result.append(body.tail if body.tail else '') + return ''.join(result) + return '' + + def del_body(self, lang=None): + if lang is None: + lang = self.get_lang() + bodies = self.xml.findall('{%s}body' % XHTML_NS) + for body in bodies: + if body.attrib.get('{%s}lang' % self.xml_ns, self.get_lang()) == lang: + self.xml.remove(body) + return diff --git a/sleekxmpp/plugins/xep_0071/xhtml_im.py b/sleekxmpp/plugins/xep_0071/xhtml_im.py new file mode 100644 index 00000000..096a00aa --- /dev/null +++ b/sleekxmpp/plugins/xep_0071/xhtml_im.py @@ -0,0 +1,30 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +from sleekxmpp.stanza import Message +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0071 import stanza, XHTML_IM + + +class XEP_0071(BasePlugin): + + name = 'xep_0071' + description = 'XEP-0071: XHTML-IM' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, XHTML_IM) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(feature=XHTML_IM.namespace) + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=XHTML_IM.namespace) diff --git a/sleekxmpp/plugins/xep_0077/register.py b/sleekxmpp/plugins/xep_0077/register.py index 7f00354b..ee07548b 100644 --- a/sleekxmpp/plugins/xep_0077/register.py +++ b/sleekxmpp/plugins/xep_0077/register.py @@ -7,6 +7,7 @@ """ import logging +import ssl from sleekxmpp.stanza import StreamFeatures, Iq from sleekxmpp.xmlstream import register_stanza_plugin, JID @@ -27,10 +28,13 @@ class XEP_0077(BasePlugin): description = 'XEP-0077: In-Band Registration' dependencies = set(['xep_0004', 'xep_0066']) stanza = stanza + default_config = { + 'create_account': True, + 'force_registration': False, + 'order': 50 + } def plugin_init(self): - self.create_account = self.config.get('create_account', True) - register_stanza_plugin(StreamFeatures, RegisterFeature) register_stanza_plugin(Iq, Register) @@ -38,14 +42,33 @@ class XEP_0077(BasePlugin): self.xmpp.register_feature('register', self._handle_register_feature, restart=False, - order=self.config.get('order', 50)) + order=self.order) register_stanza_plugin(Register, self.xmpp['xep_0004'].stanza.Form) register_stanza_plugin(Register, self.xmpp['xep_0066'].stanza.OOB) + self.xmpp.add_event_handler('connected', self._force_registration) + def plugin_end(self): if not self.xmpp.is_component: - self.xmpp.unregister_feature('register', self.config.get('order', 50)) + self.xmpp.unregister_feature('register', self.order) + + def _force_registration(self, event): + if self.force_registration: + self.xmpp.add_filter('in', self._force_stream_feature) + + def _force_stream_feature(self, stanza): + if isinstance(stanza, StreamFeatures): + if self.xmpp.use_tls or self.xmpp.use_ssl: + if 'starttls' not in self.xmpp.features: + return stanza + elif not isinstance(self.xmpp.socket, ssl.SSLSocket): + return stanza + if 'mechanisms' not in self.xmpp.features: + log.debug('Forced adding in-band registration stream feature') + stanza.enable('register') + self.xmpp.del_filter('in', self._force_stream_feature) + return stanza def _handle_register_feature(self, features): if 'mechanisms' in self.xmpp.features: diff --git a/sleekxmpp/plugins/xep_0078/legacyauth.py b/sleekxmpp/plugins/xep_0078/legacyauth.py index 8ea78fba..da6bfa2c 100644 --- a/sleekxmpp/plugins/xep_0078/legacyauth.py +++ b/sleekxmpp/plugins/xep_0078/legacyauth.py @@ -6,11 +6,13 @@ See the file LICENSE for copying permission. """ +import uuid import logging import hashlib import random import sys +from sleekxmpp.jid import JID from sleekxmpp.exceptions import IqError, IqTimeout from sleekxmpp.stanza import Iq, StreamFeatures from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin @@ -34,23 +36,37 @@ class XEP_0078(BasePlugin): description = 'XEP-0078: Non-SASL Authentication' dependencies = set() stanza = stanza + default_config = { + 'order': 15 + } def plugin_init(self): self.xmpp.register_feature('auth', self._handle_auth, restart=False, - order=self.config.get('order', 15)) + order=self.order) + + self.xmpp.add_event_handler('legacy_protocol', + self._handle_legacy_protocol) register_stanza_plugin(Iq, stanza.IqAuth) register_stanza_plugin(StreamFeatures, stanza.AuthFeature) def plugin_end(self): - self.xmpp.unregister_feature('auth', self.config.get('order', 15)) + self.xmpp.del_event_handler('legacy_protocol', + self._handle_legacy_protocol) + self.xmpp.unregister_feature('auth', self.order) def _handle_auth(self, features): # If we can or have already authenticated with SASL, do nothing. if 'mechanisms' in features['features']: return False + return self.authenticate() + + def _handle_legacy_protocol(self, event): + self.authenticate() + + def authenticate(self): if self.xmpp.authenticated: return False @@ -59,13 +75,13 @@ class XEP_0078(BasePlugin): # Step 1: Request the auth form iq = self.xmpp.Iq() iq['type'] = 'get' - iq['to'] = self.xmpp.boundjid.host - iq['auth']['username'] = self.xmpp.boundjid.user + iq['to'] = self.xmpp.requested_jid.host + iq['auth']['username'] = self.xmpp.requested_jid.user try: resp = iq.send(now=True) - except IqError: - log.info("Authentication failed: %s", resp['error']['condition']) + except IqError as err: + log.info("Authentication failed: %s", err.iq['error']['condition']) self.xmpp.event('failed_auth', direct=True) self.xmpp.disconnect() return True @@ -78,13 +94,14 @@ class XEP_0078(BasePlugin): # Step 2: Fill out auth form for either password or digest auth iq = self.xmpp.Iq() iq['type'] = 'set' - iq['auth']['username'] = self.xmpp.boundjid.user + iq['auth']['username'] = self.xmpp.requested_jid.user # A resource is required, so create a random one if necessary - if self.xmpp.boundjid.resource: - iq['auth']['resource'] = self.xmpp.boundjid.resource - else: - iq['auth']['resource'] = '%s' % random.random() + resource = self.xmpp.requested_jid.resource + if not resource: + resource = str(uuid.uuid4()) + + iq['auth']['resource'] = resource if 'digest' in resp['auth']['fields']: log.debug('Authenticating via jabber:iq:auth Digest') @@ -106,16 +123,22 @@ class XEP_0078(BasePlugin): result = iq.send(now=True) except IqError as err: log.info("Authentication failed") - self.xmpp.disconnect() self.xmpp.event("failed_auth", direct=True) + self.xmpp.disconnect() except IqTimeout: log.info("Authentication failed") - self.xmpp.disconnect() self.xmpp.event("failed_auth", direct=True) + self.xmpp.disconnect() self.xmpp.features.add('auth') self.xmpp.authenticated = True + + self.xmpp.boundjid = JID(self.xmpp.requested_jid, + resource=resource, + cache_lock=True) + self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True) + log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.session_started_event.set() diff --git a/sleekxmpp/plugins/xep_0079/__init__.py b/sleekxmpp/plugins/xep_0079/__init__.py new file mode 100644 index 00000000..09e66715 --- /dev/null +++ b/sleekxmpp/plugins/xep_0079/__init__.py @@ -0,0 +1,18 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0079.stanza import ( + AMP, Rule, InvalidRules, UnsupportedConditions, + UnsupportedActions, FailedRules, FailedRule, + AMPFeature) +from sleekxmpp.plugins.xep_0079.amp import XEP_0079 + + +register_plugin(XEP_0079) diff --git a/sleekxmpp/plugins/xep_0079/amp.py b/sleekxmpp/plugins/xep_0079/amp.py new file mode 100644 index 00000000..918fb841 --- /dev/null +++ b/sleekxmpp/plugins/xep_0079/amp.py @@ -0,0 +1,79 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +import logging + +from sleekxmpp.stanza import Message, Error, StreamFeatures +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.matcher import StanzaPath, MatchMany +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0079 import stanza + + +log = logging.getLogger(__name__) + + +class XEP_0079(BasePlugin): + + """ + XEP-0079 Advanced Message Processing + """ + + name = 'xep_0079' + description = 'XEP-0079: Advanced Message Processing' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, stanza.AMP) + register_stanza_plugin(Error, stanza.InvalidRules) + register_stanza_plugin(Error, stanza.UnsupportedConditions) + register_stanza_plugin(Error, stanza.UnsupportedActions) + register_stanza_plugin(Error, stanza.FailedRules) + + self.xmpp.register_handler( + Callback('AMP Response', + MatchMany([ + StanzaPath('message/error/failed_rules'), + StanzaPath('message/amp') + ]), + self._handle_amp_response)) + + if not self.xmpp.is_component: + self.xmpp.register_feature('amp', + self._handle_amp_feature, + restart=False, + order=9000) + register_stanza_plugin(StreamFeatures, stanza.AMPFeature) + + def plugin_end(self): + self.xmpp.remove_handler('AMP Response') + + def _handle_amp_response(self, msg): + log.debug('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + if msg['type'] == 'error': + self.xmpp.event('amp_error', msg) + elif msg['amp']['status'] in ('alert', 'notify'): + self.xmpp.event('amp_%s' % msg['amp']['status'], msg) + + def _handle_amp_feature(self, features): + log.debug('Advanced Message Processing is available.') + self.xmpp.features.add('amp') + + def discover_support(self, jid=None, **iqargs): + if jid is None: + if self.xmpp.is_component: + jid = self.xmpp.server_host + else: + jid = self.xmpp.boundjid.host + + return self.xmpp['xep_0030'].get_info( + jid=jid, + node='http://jabber.org/protocol/amp', + **iqargs) diff --git a/sleekxmpp/plugins/xep_0079/stanza.py b/sleekxmpp/plugins/xep_0079/stanza.py new file mode 100644 index 00000000..cb6932d6 --- /dev/null +++ b/sleekxmpp/plugins/xep_0079/stanza.py @@ -0,0 +1,96 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from __future__ import unicode_literals + +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin + + +class AMP(ElementBase): + namespace = 'http://jabber.org/protocol/amp' + name = 'amp' + plugin_attrib = 'amp' + interfaces = set(['from', 'to', 'status', 'per_hop']) + + def get_from(self): + return JID(self._get_attr('from')) + + def set_from(self, value): + return self._set_attr('from', str(value)) + + def get_to(self): + return JID(self._get_attr('from')) + + def set_to(self, value): + return self._set_attr('to', str(value)) + + def get_per_hop(self): + return self._get_attr('per-hop') == 'true' + + def set_per_hop(self, value): + if value: + return self._set_attr('per-hop', 'true') + else: + return self._del_attr('per-hop') + + def del_per_hop(self): + return self._del_attr('per-hop') + + def add_rule(self, action, condition, value): + rule = Rule(parent=self) + rule['action'] = action + rule['condition'] = condition + rule['value'] = value + + +class Rule(ElementBase): + namespace = 'http://jabber.org/protocol/amp' + name = 'rule' + plugin_attrib = name + plugin_multi_attrib = 'rules' + interfaces = set(['action', 'condition', 'value']) + + +class InvalidRules(ElementBase): + namespace = 'http://jabber.org/protocol/amp' + name = 'invalid-rules' + plugin_attrib = 'invalid_rules' + + +class UnsupportedConditions(ElementBase): + namespace = 'http://jabber.org/protocol/amp' + name = 'unsupported-conditions' + plugin_attrib = 'unsupported_conditions' + + +class UnsupportedActions(ElementBase): + namespace = 'http://jabber.org/protocol/amp' + name = 'unsupported-actions' + plugin_attrib = 'unsupported_actions' + + +class FailedRule(Rule): + namespace = 'http://jabber.org/protocol/amp#errors' + + +class FailedRules(ElementBase): + namespace = 'http://jabber.org/protocol/amp#errors' + name = 'failed-rules' + plugin_attrib = 'failed_rules' + + +class AMPFeature(ElementBase): + namespace = 'http://jabber.org/features/amp' + name = 'amp' + + +register_stanza_plugin(AMP, Rule, iterable=True) +register_stanza_plugin(InvalidRules, Rule, iterable=True) +register_stanza_plugin(UnsupportedConditions, Rule, iterable=True) +register_stanza_plugin(UnsupportedActions, Rule, iterable=True) +register_stanza_plugin(FailedRules, FailedRule, iterable=True) diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py index 02571fa7..26eb68fa 100644 --- a/sleekxmpp/plugins/xep_0082.py +++ b/sleekxmpp/plugins/xep_0082.py @@ -6,7 +6,6 @@ See the file LICENSE for copying permission. """ -import logging import datetime as dt from sleekxmpp.plugins import BasePlugin, register_plugin diff --git a/sleekxmpp/plugins/xep_0084/avatar.py b/sleekxmpp/plugins/xep_0084/avatar.py index bbac330a..677a888d 100644 --- a/sleekxmpp/plugins/xep_0084/avatar.py +++ b/sleekxmpp/plugins/xep_0084/avatar.py @@ -41,6 +41,9 @@ class XEP_0084(BasePlugin): def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('avatar_metadata', MetaData) + def generate_id(self, data): + return hashlib.sha1(data).hexdigest() + def retrieve_avatar(self, jid, id, url=None, ifrom=None, block=True, callback=None, timeout=None): return self.xmpp['xep_0060'].get_item(jid, Data.namespace, id, @@ -54,8 +57,7 @@ class XEP_0084(BasePlugin): payload = Data() payload['value'] = data return self.xmpp['xep_0163'].publish(payload, - node=Data.namespace, - id=hashlib.sha1(data).hexdigest(), + id=self.generate_id(data), ifrom=ifrom, block=block, callback=callback, @@ -67,17 +69,20 @@ class XEP_0084(BasePlugin): metadata = MetaData() if items is None: items = [] + if not isinstance(items, (list, set)): + items = [items] for info in items: metadata.add_info(info['id'], info['type'], info['bytes'], height=info.get('height', ''), width=info.get('width', ''), url=info.get('url', '')) - for pointer in pointers: - metadata.add_pointer(pointer) - return self.xmpp['xep_0163'].publish(payload, - node=Data.namespace, - id=hashlib.sha1(data).hexdigest(), + if pointers is not None: + for pointer in pointers: + metadata.add_pointer(pointer) + + return self.xmpp['xep_0163'].publish(metadata, + id=info['id'], ifrom=ifrom, block=block, callback=callback, diff --git a/sleekxmpp/plugins/xep_0084/stanza.py b/sleekxmpp/plugins/xep_0084/stanza.py index 1b204471..fd21e6f1 100644 --- a/sleekxmpp/plugins/xep_0084/stanza.py +++ b/sleekxmpp/plugins/xep_0084/stanza.py @@ -7,8 +7,8 @@ """ from base64 import b64encode, b64decode -from sleekxmpp.thirdparty.suelta.util import bytes +from sleekxmpp.util import bytes as sbytes from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin @@ -20,12 +20,15 @@ class Data(ElementBase): def get_value(self): if self.xml.text: - return b64decode(bytes(self.xml.text)) + return b64decode(sbytes(self.xml.text)) return '' def set_value(self, value): if value: - self.xml.text = b64encode(bytes(value)) + self.xml.text = b64encode(sbytes(value)) + # Python3 base64 encoded is bytes and needs to be decoded to string + if isinstance(self.xml.text, bytes): + self.xml.text = self.xml.text.decode() else: self.xml.text = '' @@ -43,7 +46,7 @@ class MetaData(ElementBase): info = Info() info.values = {'id': id, 'type': itype, - 'bytes': ibytes, + 'bytes': '%s' % ibytes, 'height': height, 'width': width, 'url': url} diff --git a/sleekxmpp/plugins/xep_0085/chat_states.py b/sleekxmpp/plugins/xep_0085/chat_states.py index 17e19d35..17f82afd 100644 --- a/sleekxmpp/plugins/xep_0085/chat_states.py +++ b/sleekxmpp/plugins/xep_0085/chat_states.py @@ -52,4 +52,5 @@ class XEP_0085(BasePlugin): def _handle_chat_state(self, msg): state = msg['chat_state'] log.debug("Chat State: %s, %s", state, msg['from'].jid) + self.xmpp.event('chatstate', msg) self.xmpp.event('chatstate_%s' % state, msg) diff --git a/sleekxmpp/plugins/xep_0086/legacy_error.py b/sleekxmpp/plugins/xep_0086/legacy_error.py index bed22ee2..f7d0ac9c 100644 --- a/sleekxmpp/plugins/xep_0086/legacy_error.py +++ b/sleekxmpp/plugins/xep_0086/legacy_error.py @@ -37,7 +37,10 @@ class XEP_0086(BasePlugin): description = 'XEP-0086: Error Condition Mappings'
dependencies = set()
stanza = stanza
+ default_config = {
+ 'override': True
+ }
def plugin_init(self):
register_stanza_plugin(Error, LegacyError,
- overrides=self.config.get('override', True))
+ overrides=self.override)
diff --git a/sleekxmpp/plugins/xep_0091/__init__.py b/sleekxmpp/plugins/xep_0091/__init__.py new file mode 100644 index 00000000..04f21ef5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0091/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0091 import stanza +from sleekxmpp.plugins.xep_0091.stanza import LegacyDelay +from sleekxmpp.plugins.xep_0091.legacy_delay import XEP_0091 + + +register_plugin(XEP_0091) diff --git a/sleekxmpp/plugins/xep_0091/legacy_delay.py b/sleekxmpp/plugins/xep_0091/legacy_delay.py new file mode 100644 index 00000000..7323d468 --- /dev/null +++ b/sleekxmpp/plugins/xep_0091/legacy_delay.py @@ -0,0 +1,29 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +from sleekxmpp.stanza import Message, Presence +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0091 import stanza + + +class XEP_0091(BasePlugin): + + """ + XEP-0091: Legacy Delayed Delivery + """ + + name = 'xep_0091' + description = 'XEP-0091: Legacy Delayed Delivery' + dependencies = set() + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, stanza.LegacyDelay) + register_stanza_plugin(Presence, stanza.LegacyDelay) diff --git a/sleekxmpp/plugins/xep_0091/stanza.py b/sleekxmpp/plugins/xep_0091/stanza.py new file mode 100644 index 00000000..17e55764 --- /dev/null +++ b/sleekxmpp/plugins/xep_0091/stanza.py @@ -0,0 +1,47 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt + +from sleekxmpp.jid import JID +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class LegacyDelay(ElementBase): + + name = 'x' + namespace = 'jabber:x:delay' + plugin_attrib = 'legacy_delay' + interfaces = set(('from', 'stamp', 'text')) + + def get_from(self): + from_ = self._get_attr('from') + return JID(from_) if from_ else None + + def set_from(self, value): + self._set_attr('from', str(value)) + + def get_stamp(self): + timestamp = self._get_attr('stamp') + return xep_0082.parse('%sZ' % timestamp) if timestamp else None + + def set_stamp(self, value): + if isinstance(value, dt.datetime): + value = value.astimezone(xep_0082.tzutc) + value = xep_0082.format_datetime(value) + self._set_attr('stamp', value[0:19].replace('-', '')) + + def get_text(self): + return self.xml.text + + def set_text(self, value): + self.xml.text = value + + def del_text(self): + self.xml.text = '' diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py index 463da158..b16ad516 100644 --- a/sleekxmpp/plugins/xep_0092/version.py +++ b/sleekxmpp/plugins/xep_0092/version.py @@ -30,16 +30,18 @@ class XEP_0092(BasePlugin): description = 'XEP-0092: Software Version' dependencies = set(['xep_0030']) stanza = stanza + default_config = { + 'software_name': 'SleekXMPP', + 'version': sleekxmpp.__version__, + 'os': '' + } def plugin_init(self): """ Start the XEP-0092 plugin. """ - self.name = self.config.get('name', 'SleekXMPP') - self.version = self.config.get('version', sleekxmpp.__version__) - self.os = self.config.get('os', '') - - self.getVersion = self.get_version + if 'name' in self.config: + self.software_name = self.config['name'] self.xmpp.register_handler( Callback('Software Version', @@ -63,12 +65,12 @@ class XEP_0092(BasePlugin): iq -- The Iq stanza containing the software version query. """ iq.reply() - iq['software_version']['name'] = self.name + iq['software_version']['name'] = self.software_name iq['software_version']['version'] = self.version iq['software_version']['os'] = self.os iq.send() - def get_version(self, jid, ifrom=None): + def get_version(self, jid, ifrom=None, block=True, timeout=None, callback=None): """ Retrieve the software version of a remote agent. @@ -80,11 +82,4 @@ class XEP_0092(BasePlugin): iq['from'] = ifrom iq['type'] = 'get' iq['query'] = Version.namespace - - result = iq.send() - - if result and result['type'] != 'error': - values = result['software_version'].values - del values['lang'] - return values - return False + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0095/__init__.py b/sleekxmpp/plugins/xep_0095/__init__.py new file mode 100644 index 00000000..4465ef5c --- /dev/null +++ b/sleekxmpp/plugins/xep_0095/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0095 import stanza +from sleekxmpp.plugins.xep_0095.stanza import SI +from sleekxmpp.plugins.xep_0095.stream_initiation import XEP_0095 + + +register_plugin(XEP_0095) diff --git a/sleekxmpp/plugins/xep_0095/stanza.py b/sleekxmpp/plugins/xep_0095/stanza.py new file mode 100644 index 00000000..34999a11 --- /dev/null +++ b/sleekxmpp/plugins/xep_0095/stanza.py @@ -0,0 +1,25 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase + + +class SI(ElementBase): + name = 'si' + namespace = 'http://jabber.org/protocol/si' + plugin_attrib = 'si' + interfaces = set(['id', 'mime_type', 'profile']) + + def get_mime_type(self): + return self._get_attr('mime-type', 'application/octet-stream') + + def set_mime_type(self, value): + self._set_attr('mime-type', value) + + def del_mime_type(self): + self._del_attr('mime-type') diff --git a/sleekxmpp/plugins/xep_0095/stream_initiation.py b/sleekxmpp/plugins/xep_0095/stream_initiation.py new file mode 100644 index 00000000..927248a5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0095/stream_initiation.py @@ -0,0 +1,214 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import threading + +from uuid import uuid4 + +from sleekxmpp import Iq, Message +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins.xep_0095 import stanza, SI + + +log = logging.getLogger(__name__) + + +SOCKS5 = 'http://jabber.org/protocol/bytestreams' +IBB = 'http://jabber.org/protocol/ibb' + + +class XEP_0095(BasePlugin): + + name = 'xep_0095' + description = 'XEP-0095: Stream Initiation' + dependencies = set(['xep_0020', 'xep_0030', 'xep_0047', 'xep_0065']) + stanza = stanza + + def plugin_init(self): + self._profiles = {} + self._methods = {} + self._methods_order = [] + self._pending_lock = threading.Lock() + self._pending= {} + + self.register_method(SOCKS5, 'xep_0065', 100) + self.register_method(IBB, 'xep_0047', 50) + + register_stanza_plugin(Iq, SI) + register_stanza_plugin(SI, self.xmpp['xep_0020'].stanza.FeatureNegotiation) + + self.xmpp.register_handler( + Callback('SI Request', + StanzaPath('iq@type=set/si'), + self._handle_request)) + + self.api.register(self._add_pending, 'add_pending', default=True) + self.api.register(self._get_pending, 'get_pending', default=True) + self.api.register(self._del_pending, 'del_pending', default=True) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(SI.namespace) + + def plugin_end(self): + self.xmpp.remove_handler('SI Request') + self.xmpp['xep_0030'].del_feature(feature=SI.namespace) + + def register_profile(self, profile_name, plugin): + self._profiles[profile_name] = plugin + + def unregister_profile(self, profile_name): + try: + del self._profiles[profile_name] + except KeyError: + pass + + def register_method(self, method, plugin_name, order=50): + self._methods[method] = (plugin_name, order) + self._methods_order.append((order, method, plugin_name)) + self._methods_order.sort() + + def unregister_method(self, method): + if method in self._methods: + plugin_name, order = self._methods[method] + del self._methods[method] + self._methods_order.remove((order, method, plugin_name)) + self._methods_order.sort() + + def _handle_request(self, iq): + profile = iq['si']['profile'] + sid = iq['si']['id'] + + if not sid: + raise XMPPError(etype='modify', condition='bad-request') + if profile not in self._profiles: + raise XMPPError( + etype='modify', + condition='bad-request', + extension='bad-profile', + extension_ns=SI.namespace) + + neg = iq['si']['feature_neg']['form']['fields'] + options = neg['stream-method']['options'] or [] + methods = [] + for opt in options: + methods.append(opt['value']) + for method in methods: + if method in self._methods: + supported = True + break + else: + raise XMPPError('bad-request', + extension='no-valid-streams', + extension_ns=SI.namespace) + + selected_method = None + log.debug('Available: %s', methods) + for order, method, plugin in self._methods_order: + log.debug('Testing: %s', method) + if method in methods: + selected_method = method + break + + receiver = iq['to'] + sender = iq['from'] + + self.api['add_pending'](receiver, sid, sender, { + 'response_id': iq['id'], + 'method': selected_method, + 'profile': profile + }) + self.xmpp.event('si_request', iq) + + def offer(self, jid, sid=None, mime_type=None, profile=None, + methods=None, payload=None, ifrom=None, + **iqargs): + if sid is None: + sid = uuid4().hex + if methods is None: + methods = list(self._methods.keys()) + if not isinstance(methods, (list, tuple, set)): + methods = [methods] + + si = self.xmpp.Iq() + si['to'] = jid + si['from'] = ifrom + si['type'] = 'set' + si['si']['id'] = sid + si['si']['mime_type'] = mime_type + si['si']['profile'] = profile + if not isinstance(payload, (list, tuple, set)): + payload = [payload] + for item in payload: + si['si'].append(item) + si['si']['feature_neg']['form'].add_field( + var='stream-method', + ftype='list-single', + options=methods) + return si.send(**iqargs) + + def accept(self, jid, sid, payload=None, ifrom=None, stream_handler=None): + stream = self.api['get_pending'](ifrom, sid, jid) + iq = self.xmpp.Iq() + iq['id'] = stream['response_id'] + iq['to'] = jid + iq['from'] = ifrom + iq['type'] = 'result' + if payload: + iq['si'].append(payload) + iq['si']['feature_neg']['form']['type'] = 'submit' + iq['si']['feature_neg']['form'].add_field( + var='stream-method', + ftype='list-single', + value=stream['method']) + + if ifrom is None: + ifrom = self.xmpp.boundjid + + method_plugin = self._methods[stream['method']][0] + self.xmpp[method_plugin].api['preauthorize_sid'](ifrom, sid, jid) + + self.api['del_pending'](ifrom, sid, jid) + + if stream_handler: + self.xmpp.add_event_handler('stream:%s:%s' % (sid, jid), + stream_handler, + threaded=True, + disposable=True) + return iq.send() + + def decline(self, jid, sid, ifrom=None): + stream = self.api['get_pending'](ifrom, sid, jid) + if not stream: + return + iq = self.xmpp.Iq() + iq['id'] = stream['response_id'] + iq['to'] = jid + iq['from'] = ifrom + iq['type'] = 'error' + iq['error']['condition'] = 'forbidden' + iq['error']['text'] = 'Offer declined' + self.api['del_pending'](ifrom, sid, jid) + return iq.send() + + def _add_pending(self, jid, node, ifrom, data): + with self._pending_lock: + self._pending[(jid, node, ifrom)] = data + + def _get_pending(self, jid, node, ifrom, data): + with self._pending_lock: + return self._pending.get((jid, node, ifrom), None) + + def _del_pending(self, jid, node, ifrom, data): + with self._pending_lock: + if (jid, node, ifrom) in self._pending: + del self._pending[(jid, node, ifrom)] diff --git a/sleekxmpp/plugins/xep_0096/__init__.py b/sleekxmpp/plugins/xep_0096/__init__.py new file mode 100644 index 00000000..5f836169 --- /dev/null +++ b/sleekxmpp/plugins/xep_0096/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0096 import stanza +from sleekxmpp.plugins.xep_0096.stanza import File +from sleekxmpp.plugins.xep_0096.file_transfer import XEP_0096 + + +register_plugin(XEP_0096) diff --git a/sleekxmpp/plugins/xep_0096/file_transfer.py b/sleekxmpp/plugins/xep_0096/file_transfer.py new file mode 100644 index 00000000..6873c7f5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0096/file_transfer.py @@ -0,0 +1,58 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp import Iq, Message +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins.xep_0096 import stanza, File + + +log = logging.getLogger(__name__) + + +class XEP_0096(BasePlugin): + + name = 'xep_0096' + description = 'XEP-0096: SI File Transfer' + dependencies = set(['xep_0095']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(self.xmpp['xep_0095'].stanza.SI, File) + + self.xmpp['xep_0095'].register_profile(File.namespace, self) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(File.namespace) + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=File.namespace) + self.xmpp['xep_0095'].unregister_profile(File.namespace, self) + + def request_file_transfer(self, jid, sid=None, name=None, size=None, + desc=None, hash=None, date=None, + allow_ranged=False, mime_type=None, + **iqargs): + data = File() + data['name'] = name + data['size'] = size + data['date'] = date + data['desc'] = desc + if allow_ranged: + data.enable('range') + + return self.xmpp['xep_0095'].offer(jid, + sid=sid, + mime_type=mime_type, + profile=File.namespace, + payload=data, + **iqargs) diff --git a/sleekxmpp/plugins/xep_0096/stanza.py b/sleekxmpp/plugins/xep_0096/stanza.py new file mode 100644 index 00000000..65eb5bc5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0096/stanza.py @@ -0,0 +1,48 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt + +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin +from sleekxmpp.plugins import xep_0082 + + +class File(ElementBase): + name = 'file' + namespace = 'http://jabber.org/protocol/si/profile/file-transfer' + plugin_attrib = 'file' + interfaces = set(['name', 'size', 'date', 'hash', 'desc']) + sub_interfaces = set(['desc']) + + def set_size(self, value): + self._set_attr('size', str(value)) + + def get_date(self): + timestamp = self._get_attr('date') + return xep_0082.parse(timestamp) + + def set_date(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('date', value) + + +class Range(ElementBase): + name = 'range' + namespace = 'http://jabber.org/protocol/si/profile/file-transfer' + plugin_attrib = 'range' + interfaces = set(['length', 'offset']) + + def set_length(self, value): + self._set_attr('length', str(value)) + + def set_offset(self, value): + self._set_attr('offset', str(value)) + + +register_stanza_plugin(File, Range) diff --git a/sleekxmpp/plugins/xep_0106.py b/sleekxmpp/plugins/xep_0106.py new file mode 100644 index 00000000..1859a77b --- /dev/null +++ b/sleekxmpp/plugins/xep_0106.py @@ -0,0 +1,26 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +from sleekxmpp.plugins import BasePlugin, register_plugin + + +class XEP_0106(BasePlugin): + + name = 'xep_0106' + description = 'XEP-0106: JID Escaping' + dependencies = set(['xep_0030']) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(feature='jid\\20escaping') + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature='jid\\20escaping') + + +register_plugin(XEP_0106) diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py index 8ce10edb..41b5c52e 100644 --- a/sleekxmpp/plugins/xep_0115/caps.py +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -9,8 +9,9 @@ import logging import hashlib import base64 +import threading -import sleekxmpp +from sleekxmpp import __version__ from sleekxmpp.stanza import StreamFeatures, Presence, Iq from sleekxmpp.xmlstream import register_stanza_plugin, JID from sleekxmpp.xmlstream.handler import Callback @@ -33,19 +34,19 @@ class XEP_0115(BasePlugin): description = 'XEP-0115: Entity Capabilities' dependencies = set(['xep_0030', 'xep_0128', 'xep_0004']) stanza = stanza + default_config = { + 'hash': 'sha-1', + 'caps_node': None, + 'broadcast': True + } def plugin_init(self): self.hashes = {'sha-1': hashlib.sha1, 'sha1': hashlib.sha1, 'md5': hashlib.md5} - self.hash = self.config.get('hash', 'sha-1') - self.caps_node = self.config.get('caps_node', None) - self.broadcast = self.config.get('broadcast', True) - if self.caps_node is None: - ver = sleekxmpp.__version__ - self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver + self.caps_node = 'http://sleekxmpp.com/ver/%s' % __version__ register_stanza_plugin(Presence, stanza.Capabilities) register_stanza_plugin(StreamFeatures, stanza.Capabilities) @@ -89,6 +90,9 @@ class XEP_0115(BasePlugin): disco.assign_verstring = self.assign_verstring disco.get_verstring = self.get_verstring + self._processing_lock = threading.Lock() + self._processing = set() + def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace) self.xmpp.del_filter('out', self._filter_add_caps) @@ -103,12 +107,17 @@ class XEP_0115(BasePlugin): self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace) def _filter_add_caps(self, stanza): - if isinstance(stanza, Presence) and self.broadcast: - ver = self.get_verstring(stanza['from']) - if ver: - stanza['caps']['node'] = self.caps_node - stanza['caps']['hash'] = self.hash - stanza['caps']['ver'] = ver + if not isinstance(stanza, Presence) or not self.broadcast: + return stanza + + if stanza['type'] not in ('available', 'chat', 'away', 'dnd', 'xa'): + return stanza + + ver = self.get_verstring(stanza['from']) + if ver: + stanza['caps']['node'] = self.caps_node + stanza['caps']['hash'] = self.hash + stanza['caps']['ver'] = ver return stanza def _handle_caps(self, presence): @@ -129,12 +138,22 @@ class XEP_0115(BasePlugin): def _process_caps(self, pres): if not pres['caps']['hash']: - log.debug("Received unsupported legacy caps.") + log.debug("Received unsupported legacy caps: %s, %s, %s", + pres['caps']['node'], + pres['caps']['ver'], + pres['caps']['ext']) self.xmpp.event('entity_caps_legacy', pres) return + ver = pres['caps']['ver'] + existing_verstring = self.get_verstring(pres['from'].full) - if str(existing_verstring) == str(pres['caps']['ver']): + if str(existing_verstring) == str(ver): + return + + existing_caps = self.get_caps(verstring=ver) + if existing_caps is not None: + self.assign_verstring(pres['from'], ver) return if pres['caps']['hash'] not in self.hashes: @@ -145,9 +164,16 @@ class XEP_0115(BasePlugin): except XMPPError: return - log.debug("New caps verification string: %s", pres['caps']['ver']) + # Only lookup the same caps once at a time. + with self._processing_lock: + if ver in self._processing: + log.debug('Already processing verstring %s' % ver) + return + self._processing.add(ver) + + log.debug("New caps verification string: %s", ver) try: - node = '%s#%s' % (pres['caps']['node'], pres['caps']['ver']) + node = '%s#%s' % (pres['caps']['node'], ver) caps = self.xmpp['xep_0030'].get_info(pres['from'], node) if isinstance(caps, Iq): @@ -157,7 +183,10 @@ class XEP_0115(BasePlugin): pres['caps']['ver']): self.assign_verstring(pres['from'], pres['caps']['ver']) except XMPPError: - log.debug("Could not retrieve disco#info results for caps") + log.debug("Could not retrieve disco#info results for caps for %s", node) + + with self._processing_lock: + self._processing.remove(ver) def _validate_caps(self, caps, hash, check_verstring): # Check Identities @@ -168,7 +197,6 @@ class XEP_0115(BasePlugin): return False # Check Features - full_features = caps.get_features(dedupe=False) deduped_features = caps.get_features() if len(full_features) != len(deduped_features): @@ -179,29 +207,32 @@ class XEP_0115(BasePlugin): form_types = [] deduped_form_types = set() for stanza in caps['substanzas']: - if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): - if 'FORM_TYPE' in stanza['fields']: - f_type = tuple(stanza['fields']['FORM_TYPE']['value']) - form_types.append(f_type) - deduped_form_types.add(f_type) - if len(form_types) != len(deduped_form_types): - log.debug("Duplicated FORM_TYPE values, " + \ - "invalid for caps") + if not isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): + log.debug("Non form extension found, ignoring for caps") + caps.xml.remove(stanza.xml) + continue + if 'FORM_TYPE' in stanza['fields']: + f_type = tuple(stanza['fields']['FORM_TYPE']['value']) + form_types.append(f_type) + deduped_form_types.add(f_type) + if len(form_types) != len(deduped_form_types): + log.debug("Duplicated FORM_TYPE values, " + \ + "invalid for caps") + return False + + if len(f_type) > 1: + deduped_type = set(f_type) + if len(f_type) != len(deduped_type): + log.debug("Extra FORM_TYPE data, invalid for caps") return False - if len(f_type) > 1: - deduped_type = set(f_type) - if len(f_type) != len(deduped_type): - log.debug("Extra FORM_TYPE data, invalid for caps") - return False - - if stanza['fields']['FORM_TYPE']['type'] != 'hidden': - log.debug("Field FORM_TYPE type not 'hidden', " + \ - "ignoring form for caps") - caps.xml.remove(stanza.xml) - else: - log.debug("No FORM_TYPE found, ignoring form for caps") + if stanza['fields']['FORM_TYPE']['type'] != 'hidden': + log.debug("Field FORM_TYPE type not 'hidden', " + \ + "ignoring form for caps") caps.xml.remove(stanza.xml) + else: + log.debug("No FORM_TYPE found, ignoring form for caps") + caps.xml.remove(stanza.xml) verstring = self.generate_verstring(caps, hash) if verstring != check_verstring: @@ -261,7 +292,7 @@ class XEP_0115(BasePlugin): binary = hash(S.encode('utf8')).digest() return base64.b64encode(binary).decode('utf-8') - def update_caps(self, jid=None, node=None): + def update_caps(self, jid=None, node=None, preserve=False): try: info = self.xmpp['xep_0030'].get_info(jid, node, local=True) if isinstance(info, Iq): @@ -275,19 +306,11 @@ class XEP_0115(BasePlugin): self.assign_verstring(jid, ver) if self.xmpp.session_started_event.is_set() and self.broadcast: - # Check if we've sent directed presence. If we haven't, we - # can just send a normal presence stanza. If we have, then - # we will send presence to each contact individually so - # that we don't clobber existing statuses. - directed = False - for contact in self.xmpp.roster[jid]: - if self.xmpp.roster[jid][contact].last_status is not None: - directed = True - if not directed: - self.xmpp.roster[jid].send_last_presence() - else: + if self.xmpp.is_component or preserve: for contact in self.xmpp.roster[jid]: self.xmpp.roster[jid][contact].send_last_presence() + else: + self.xmpp.roster[jid].send_last_presence() except XMPPError: return diff --git a/sleekxmpp/plugins/xep_0131/__init__.py b/sleekxmpp/plugins/xep_0131/__init__.py new file mode 100644 index 00000000..ec71c98d --- /dev/null +++ b/sleekxmpp/plugins/xep_0131/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0131 import stanza +from sleekxmpp.plugins.xep_0131.stanza import Headers +from sleekxmpp.plugins.xep_0131.headers import XEP_0131 + + +register_plugin(XEP_0131) diff --git a/sleekxmpp/plugins/xep_0131/headers.py b/sleekxmpp/plugins/xep_0131/headers.py new file mode 100644 index 00000000..3e47541a --- /dev/null +++ b/sleekxmpp/plugins/xep_0131/headers.py @@ -0,0 +1,41 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp import Message, Presence +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0131 import stanza +from sleekxmpp.plugins.xep_0131.stanza import Headers + + +class XEP_0131(BasePlugin): + + name = 'xep_0131' + description = 'XEP-0131: Stanza Headers and Internet Metadata' + dependencies = set(['xep_0030']) + stanza = stanza + default_config = { + 'supported_headers': set() + } + + def plugin_init(self): + register_stanza_plugin(Message, Headers) + register_stanza_plugin(Presence, Headers) + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=Headers.namespace) + for header in self.supported_headers: + self.xmpp['xep_0030'].del_feature( + feature='%s#%s' % (Headers.namespace, header)) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(Headers.namespace) + for header in self.supported_headers: + self.xmpp['xep_0030'].add_feature('%s#%s' % ( + Headers.namespace, + header)) diff --git a/sleekxmpp/plugins/xep_0131/stanza.py b/sleekxmpp/plugins/xep_0131/stanza.py new file mode 100644 index 00000000..347adf96 --- /dev/null +++ b/sleekxmpp/plugins/xep_0131/stanza.py @@ -0,0 +1,51 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.thirdparty import OrderedDict +from sleekxmpp.xmlstream import ET, ElementBase + + +class Headers(ElementBase): + name = 'headers' + namespace = 'http://jabber.org/protocol/shim' + plugin_attrib = 'headers' + interfaces = set(['headers']) + is_extension = True + + def get_headers(self): + result = OrderedDict() + headers = self.xml.findall('{%s}header' % self.namespace) + for header in headers: + name = header.attrib.get('name', '') + value = header.text + if name in result: + if not isinstance(result[name], set): + result[name] = [result[name]] + else: + result[name] = [] + result[name].add(value) + else: + result[name] = value + return result + + def set_headers(self, values): + self.del_headers() + for name in values: + vals = values[name] + if not isinstance(vals, (list, set)): + vals = [values[name]] + for value in vals: + header = ET.Element('{%s}header' % self.namespace) + header.attrib['name'] = name + header.text = value + self.xml.append(header) + + def del_headers(self): + headers = self.xml.findall('{%s}header' % self.namespace) + for header in headers: + self.xml.remove(header) diff --git a/sleekxmpp/plugins/xep_0133.py b/sleekxmpp/plugins/xep_0133.py new file mode 100644 index 00000000..7bbe4c3c --- /dev/null +++ b/sleekxmpp/plugins/xep_0133.py @@ -0,0 +1,54 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +from sleekxmpp.plugins import BasePlugin, register_plugin + + +class XEP_0133(BasePlugin): + + name = 'xep_0133' + description = 'XEP-0133: Service Administration' + dependencies = set(['xep_0030', 'xep_0004', 'xep_0050']) + commands = set(['add-user', 'delete-user', 'disable-user', + 'reenable-user', 'end-user-session', 'get-user-password', + 'change-user-password', 'get-user-roster', + 'get-user-lastlogin', 'user-stats', 'edit-blacklist', + 'edit-whitelist', 'get-registered-users-num', + 'get-disabled-users-num', 'get-online-users-num', + 'get-active-users-num', 'get-idle-users-num', + 'get-registered-users-list', 'get-disabled-users-list', + 'get-online-users-list', 'get-online-users', + 'get-active-users', 'get-idle-userslist', 'announce', + 'set-motd', 'edit-motd', 'delete-motd', 'set-welcome', + 'delete-welcome', 'edit-admin', 'restart', 'shutdown']) + + def get_commands(self, jid=None, **kwargs): + if jid is None: + jid = self.xmpp.boundjid.server + return self.xmpp['xep_0050'].get_commands(jid, **kwargs) + + +def create_command(name): + def admin_command(self, jid=None, session=None, ifrom=None, block=False): + if jid is None: + jid = self.xmpp.boundjid.server + self.xmpp['xep_0050'].start_command( + jid=jid, + node='http://jabber.org/protocol/admin#%s' % name, + session=session, + ifrom=ifrom, + block=block) + return admin_command + + +for cmd in XEP_0133.commands: + setattr(XEP_0133, cmd.replace('-', '_'), create_command(cmd)) + + +register_plugin(XEP_0133) diff --git a/sleekxmpp/plugins/xep_0152/__init__.py b/sleekxmpp/plugins/xep_0152/__init__.py new file mode 100644 index 00000000..7de031b7 --- /dev/null +++ b/sleekxmpp/plugins/xep_0152/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0152 import stanza +from sleekxmpp.plugins.xep_0152.stanza import Reachability +from sleekxmpp.plugins.xep_0152.reachability import XEP_0152 + + +register_plugin(XEP_0152) diff --git a/sleekxmpp/plugins/xep_0152/reachability.py b/sleekxmpp/plugins/xep_0152/reachability.py new file mode 100644 index 00000000..4cf81739 --- /dev/null +++ b/sleekxmpp/plugins/xep_0152/reachability.py @@ -0,0 +1,93 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0152 import stanza, Reachability + + +log = logging.getLogger(__name__) + + +class XEP_0152(BasePlugin): + + """ + XEP-0152: Reachability Addresses + """ + + name = 'xep_0152' + description = 'XEP-0152: Reachability Addresses' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=Reachability.namespace) + self.xmpp['xep_0163'].remove_interest(Reachability.namespace) + + def session_bind(self, jid): + self.xmpp['xep_0163'].register_pep('reachability', Reachability) + + def publish_reachability(self, addresses, options=None, + ifrom=None, block=True, callback=None, timeout=None): + """ + Publish alternative addresses where the user can be reached. + + Arguments: + addresses -- A list of dictionaries containing the URI and + optional description for each address. + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + if not isinstance(addresses, (list, tuple)): + addresses = [addresses] + reach = Reachability() + for address in addresses: + if not hasattr(address, 'items'): + address = {'uri': address} + + addr = stanza.Address() + for key, val in address.items(): + addr[key] = val + reach.append(addr) + return self.xmpp['xep_0163'].publish(reach, + node=Reachability.namespace, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user activity information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + reach = Reachability() + return self.xmpp['xep_0163'].publish(reach, + node=Reachability.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0152/stanza.py b/sleekxmpp/plugins/xep_0152/stanza.py new file mode 100644 index 00000000..bd173ce1 --- /dev/null +++ b/sleekxmpp/plugins/xep_0152/stanza.py @@ -0,0 +1,29 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin + + +class Reachability(ElementBase): + name = 'reach' + namespace = 'urn:xmpp:reach:0' + plugin_attrib = 'reach' + interfaces = set() + + +class Address(ElementBase): + name = 'addr' + namespace = 'urn:xmpp:reach:0' + plugin_attrib = 'address' + plugin_multi_attrib = 'addresses' + interfaces = set(['uri', 'desc']) + lang_interfaces = set(['desc']) + sub_interfaces = set(['desc']) + + +register_stanza_plugin(Reachability, Address, iterable=True) diff --git a/sleekxmpp/plugins/xep_0153/vcard_avatar.py b/sleekxmpp/plugins/xep_0153/vcard_avatar.py index 1e32595a..ec1ae782 100644 --- a/sleekxmpp/plugins/xep_0153/vcard_avatar.py +++ b/sleekxmpp/plugins/xep_0153/vcard_avatar.py @@ -8,11 +8,11 @@ import hashlib import logging +import threading from sleekxmpp.stanza import Presence +from sleekxmpp.exceptions import XMPPError from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.plugins.base import BasePlugin from sleekxmpp.plugins.xep_0153 import stanza, VCardTempUpdate @@ -30,11 +30,14 @@ class XEP_0153(BasePlugin): def plugin_init(self): self._hashes = {} + self._allow_advertising = threading.Event() + register_stanza_plugin(Presence, VCardTempUpdate) self.xmpp.add_filter('out', self._update_presence) self.xmpp.add_event_handler('session_start', self._start) + self.xmpp.add_event_handler('session_end', self._end) self.xmpp.add_event_handler('presence_available', self._recv_presence) self.xmpp.add_event_handler('presence_dnd', self._recv_presence) @@ -44,10 +47,12 @@ class XEP_0153(BasePlugin): self.api.register(self._set_hash, 'set_hash', default=True) self.api.register(self._get_hash, 'get_hash', default=True) + self.api.register(self._reset_hash, 'reset_hash', default=True) def plugin_end(self): self.xmpp.del_filter('out', self._update_presence) self.xmpp.del_event_handler('session_start', self._start) + self.xmpp.del_event_handler('session_end', self._end) self.xmpp.del_event_handler('presence_available', self._recv_presence) self.xmpp.del_event_handler('presence_dnd', self._recv_presence) self.xmpp.del_event_handler('presence_xa', self._recv_presence) @@ -56,56 +61,87 @@ class XEP_0153(BasePlugin): def set_avatar(self, jid=None, avatar=None, mtype=None, block=True, timeout=None, callback=None): + if jid is None: + jid = self.xmpp.boundjid.bare + vcard = self.xmpp['xep_0054'].get_vcard(jid, cached=True) vcard = vcard['vcard_temp'] vcard['PHOTO']['TYPE'] = mtype vcard['PHOTO']['BINVAL'] = avatar + self.xmpp['xep_0054'].publish_vcard(jid=jid, vcard=vcard) - self._reset_hash(jid) + + self.api['reset_hash'](jid) + self.xmpp.roster[jid].send_last_presence() def _start(self, event): - self.xmpp['xep_0054'].get_vcard() + try: + vcard = self.xmpp['xep_0054'].get_vcard(self.xmpp.boundjid.bare) + data = vcard['vcard_temp']['PHOTO']['BINVAL'] + if not data: + new_hash = '' + else: + new_hash = hashlib.sha1(data).hexdigest() + self.api['set_hash'](self.xmpp.boundjid, args=new_hash) + self._allow_advertising.set() + except XMPPError: + log.debug('Could not retrieve vCard for %s' % self.xmpp.boundjid.bare) + + def _end(self, event): + self._allow_advertising.clear() def _update_presence(self, stanza): if not isinstance(stanza, Presence): return stanza + if stanza['type'] not in ('available', 'dnd', 'chat', 'away', 'xa'): + return stanza + current_hash = self.api['get_hash'](stanza['from']) stanza['vcard_temp_update']['photo'] = current_hash return stanza - def _reset_hash(self, jid=None): + def _reset_hash(self, jid, node, ifrom, args): own_jid = (jid.bare == self.xmpp.boundjid.bare) if self.xmpp.is_component: own_jid = (jid.domain == self.xmpp.boundjid.domain) - if jid is not None: - jid = jid.bare self.api['set_hash'](jid, args=None) if own_jid: self.xmpp.roster[jid].send_last_presence() - iq = self.xmpp['xep_0054'].get_vcard( - jid=jid, - ifrom=self.xmpp.boundjid) - data = iq['vcard_temp']['PHOTO']['BINVAL'] - if not data: - new_hash = '' - else: - new_hash = hashlib.sha1(data).hexdigest() - self.api['set_hash'](jid, args=new_hash) - if own_jid: - self.xmpp.roster[jid].send_last_presence() + try: + iq = self.xmpp['xep_0054'].get_vcard(jid=jid.bare, ifrom=ifrom) + + data = iq['vcard_temp']['PHOTO']['BINVAL'] + if not data: + new_hash = '' + else: + new_hash = hashlib.sha1(data).hexdigest() + + self.api['set_hash'](jid, args=new_hash) + except XMPPError: + log.debug('Could not retrieve vCard for %s' % jid) def _recv_presence(self, pres): + try: + if pres['muc']['affiliation']: + # Don't process vCard avatars for MUC occupants + # since they all share the same bare JID. + return + except: pass + if not pres.match('presence/vcard_temp_update'): self.api['set_hash'](pres['from'], args=None) return + data = pres['vcard_temp_update']['photo'] if data is None: return - elif data == '' or data != self.api['get_hash'](pres['to']): - self._reset_hash(pres['from']) + elif data == '' or data != self.api['get_hash'](pres['from']): + ifrom = pres['to'] if self.xmpp.is_component else None + self.api['reset_hash'](pres['from'], ifrom=ifrom) + self.xmpp.event('vcard_avatar_update', pres) # ================================================================= diff --git a/sleekxmpp/plugins/xep_0163.py b/sleekxmpp/plugins/xep_0163.py index 5aa3aef9..2d1a63b7 100644 --- a/sleekxmpp/plugins/xep_0163.py +++ b/sleekxmpp/plugins/xep_0163.py @@ -107,6 +107,8 @@ class XEP_0163(BasePlugin): """ if node is None: node = stanza.namespace + if id is None: + id = 'current' return self.xmpp['xep_0060'].publish(ifrom, node, id=id, diff --git a/sleekxmpp/plugins/xep_0184/receipt.py b/sleekxmpp/plugins/xep_0184/receipt.py index 044fa83f..3e97d8db 100644 --- a/sleekxmpp/plugins/xep_0184/receipt.py +++ b/sleekxmpp/plugins/xep_0184/receipt.py @@ -26,13 +26,14 @@ class XEP_0184(BasePlugin): description = 'XEP-0184: Message Delivery Receipts' dependencies = set(['xep_0030']) stanza = stanza + default_config = { + 'auto_ack': True, + 'auto_request': False + } ack_types = ('normal', 'chat', 'headline') def plugin_init(self): - self.auto_ack = self.config.get('auto_ack', True) - self.auto_request = self.config.get('auto_request', False) - register_stanza_plugin(Message, Request) register_stanza_plugin(Message, Received) @@ -68,7 +69,7 @@ class XEP_0184(BasePlugin): ack['to'] = msg['from'] ack['from'] = msg['to'] ack['receipt'] = msg['id'] - ack['id'] = self.xmpp.new_id() + ack['id'] = msg['id'] ack.send() def _handle_receipt_received(self, msg): @@ -117,6 +118,9 @@ class XEP_0184(BasePlugin): if stanza['receipt']: return stanza + if not stanza['body']: + return stanza + if stanza['to'].resource: if not self.xmpp['xep_0030'].supports(stanza['to'], feature='urn:xmpp:receipts', diff --git a/sleekxmpp/plugins/xep_0191/blocking.py b/sleekxmpp/plugins/xep_0191/blocking.py index 0d903acc..57632319 100644 --- a/sleekxmpp/plugins/xep_0191/blocking.py +++ b/sleekxmpp/plugins/xep_0191/blocking.py @@ -22,7 +22,7 @@ log = logging.getLogger(__name__) class XEP_0191(BasePlugin): name = 'xep_0191' - description = 'XEP-0191: Simple Communications Blocking' + description = 'XEP-0191: Blocking Command' dependencies = set(['xep_0030']) stanza = stanza @@ -48,7 +48,7 @@ class XEP_0191(BasePlugin): def get_blocked(self, ifrom=None, block=True, timeout=None, callback=None): iq = self.xmpp.Iq() iq['type'] = 'get' - iq['from'] = 'ifrom' + iq['from'] = ifrom iq.enable('blocklist') return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0196/__init__.py b/sleekxmpp/plugins/xep_0196/__init__.py new file mode 100644 index 00000000..7aeaf6c9 --- /dev/null +++ b/sleekxmpp/plugins/xep_0196/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0196 import stanza +from sleekxmpp.plugins.xep_0196.stanza import UserGaming +from sleekxmpp.plugins.xep_0196.user_gaming import XEP_0196 + + +register_plugin(XEP_0196) diff --git a/sleekxmpp/plugins/xep_0196/stanza.py b/sleekxmpp/plugins/xep_0196/stanza.py new file mode 100644 index 00000000..571c89d7 --- /dev/null +++ b/sleekxmpp/plugins/xep_0196/stanza.py @@ -0,0 +1,20 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class UserGaming(ElementBase): + + name = 'gaming' + namespace = 'urn:xmpp:gaming:0' + plugin_attrib = 'gaming' + interfaces = set(['character_name', 'character_profile', 'name', + 'level', 'server_address', 'server_name', 'uri']) + sub_interfaces = interfaces + diff --git a/sleekxmpp/plugins/xep_0196/user_gaming.py b/sleekxmpp/plugins/xep_0196/user_gaming.py new file mode 100644 index 00000000..e78f1acc --- /dev/null +++ b/sleekxmpp/plugins/xep_0196/user_gaming.py @@ -0,0 +1,97 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0196 import stanza, UserGaming + + +log = logging.getLogger(__name__) + + +class XEP_0196(BasePlugin): + + """ + XEP-0196: User Gaming + """ + + name = 'xep_0196' + description = 'XEP-0196: User Gaming' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=UserGaming.namespace) + self.xmpp['xep_0163'].remove_interest(UserGaming.namespace) + + def session_bind(self, jid): + self.xmpp['xep_0163'].register_pep('user_gaming', UserGaming) + + def publish_gaming(self, name=None, level=None, server_name=None, uri=None, + character_name=None, character_profile=None, server_address=None, + options=None, ifrom=None, block=True, callback=None, timeout=None): + """ + Publish the user's current gaming status. + + Arguments: + name -- The name of the game. + level -- The user's level in the game. + uri -- A URI for the game or relevant gaming service + server_name -- The name of the server where the user is playing. + server_address -- The hostname or IP address of the server where the + user is playing. + character_name -- The name of the user's character in the game. + character_profile -- A URI for a profile of the user's character. + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + gaming = UserGaming() + gaming['name'] = name + gaming['level'] = level + gaming['uri'] = uri + gaming['character_name'] = character_name + gaming['character_profile'] = character_profile + gaming['server_name'] = server_name + gaming['server_address'] = server_address + return self.xmpp['xep_0163'].publish(gaming, + node=UserGaming.namespace, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user gaming information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + gaming = UserGaming() + return self.xmpp['xep_0163'].publish(gaming, + node=UserGaming.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0198/stream_management.py b/sleekxmpp/plugins/xep_0198/stream_management.py index a150ad39..48029913 100644 --- a/sleekxmpp/plugins/xep_0198/stream_management.py +++ b/sleekxmpp/plugins/xep_0198/stream_management.py @@ -34,39 +34,44 @@ class XEP_0198(BasePlugin): description = 'XEP-0198: Stream Management' dependencies = set() stanza = stanza + default_config = { + #: The last ack number received from the server. + 'last_ack': 0, - def plugin_init(self): - """Start the XEP-0198 plugin.""" - - # Only enable stream management for non-components, - # since components do not yet perform feature negotiation. - if self.xmpp.is_component: - return + #: The number of stanzas to wait between sending ack requests to + #: the server. Setting this to ``1`` will send an ack request after + #: every sent stanza. Defaults to ``5``. + 'window': 5, #: The stream management ID for the stream. Knowing this value is #: required in order to do stream resumption. - self.sm_id = self.config.get('sm_id', None) + 'sm_id': None, #: A counter of handled incoming stanzas, mod 2^32. - self.handled = self.config.get('handled', 0) + 'handled': 0, #: A counter of unacked outgoing stanzas, mod 2^32. - self.seq = self.config.get('seq', 0) + 'seq': 0, - #: The last ack number received from the server. - self.last_ack = self.config.get('last_ack', 0) + #: Control whether or not the ability to resume the stream will be + #: requested when enabling stream management. Defaults to ``True``. + 'allow_resume': True, + + 'order': 10100, + 'resume_order': 9000 + } + + def plugin_init(self): + """Start the XEP-0198 plugin.""" + + # Only enable stream management for non-components, + # since components do not yet perform feature negotiation. + if self.xmpp.is_component: + return - #: The number of stanzas to wait between sending ack requests to - #: the server. Setting this to ``1`` will send an ack request after - #: every sent stanza. Defaults to ``5``. - self.window = self.config.get('window', 5) self.window_counter = self.window self.window_counter_lock = threading.Lock() - #: Control whether or not the ability to resume the stream will be - #: requested when enabling stream management. Defaults to ``True``. - self.allow_resume = self.config.get('allow_resume', True) - self.enabled = threading.Event() self.unacked_queue = collections.deque() @@ -92,11 +97,11 @@ class XEP_0198(BasePlugin): self.xmpp.register_feature('sm', self._handle_sm_feature, restart=True, - order=self.config.get('order', 10100)) + order=self.order) self.xmpp.register_feature('sm', self._handle_sm_feature, restart=True, - order=self.config.get('resume_order', 9000)) + order=self.resume_order) self.xmpp.register_handler( Callback('Stream Management Enabled', @@ -137,8 +142,8 @@ class XEP_0198(BasePlugin): if self.xmpp.is_component: return - self.xmpp.unregister_feature('sm', self.config.get('order', 10100)) - self.xmpp.unregister_feature('sm', self.config.get('resume_order', 9000)) + self.xmpp.unregister_feature('sm', self.order) + self.xmpp.unregister_feature('sm', self.resume_order) self.xmpp.del_event_handler('session_end', self.session_end) self.xmpp.del_filter('in', self._handle_incoming) self.xmpp.del_filter('out_sync', self._handle_outgoing) diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py index b9d145aa..836ff4ae 100644 --- a/sleekxmpp/plugins/xep_0199/ping.py +++ b/sleekxmpp/plugins/xep_0199/ping.py @@ -9,8 +9,8 @@ import time import logging -import sleekxmpp -from sleekxmpp import Iq +from sleekxmpp.jid import JID +from sleekxmpp.stanza import Iq from sleekxmpp.exceptions import IqError, IqTimeout from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.matcher import StanzaPath @@ -38,7 +38,7 @@ class XEP_0199(BasePlugin): keepalive -- If True, periodically send ping requests to the server. If a ping is not answered, the connection will be reset. - frequency -- Time in seconds between keepalive pings. + interval -- Time in seconds between keepalive pings. Defaults to 300 seconds. timeout -- Time in seconds to wait for a ping response. Defaults to 30 seconds. @@ -51,14 +51,16 @@ class XEP_0199(BasePlugin): description = 'XEP-0199: XMPP Ping' dependencies = set(['xep_0030']) stanza = stanza + default_config = { + 'keepalive': False, + 'interval': 300, + 'timeout': 30 + } def plugin_init(self): """ Start the XEP-0199 plugin. """ - self.keepalive = self.config.get('keepalive', False) - self.frequency = float(self.config.get('frequency', 300)) - self.timeout = self.config.get('timeout', 30) register_stanza_plugin(Iq, Ping) @@ -69,88 +71,70 @@ class XEP_0199(BasePlugin): if self.keepalive: self.xmpp.add_event_handler('session_start', - self._handle_keepalive, + self.enable_keepalive, threaded=True) self.xmpp.add_event_handler('session_end', - self._handle_session_end) + self.disable_keepalive) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Ping.namespace) self.xmpp.remove_handler('Ping') if self.keepalive: self.xmpp.del_event_handler('session_start', - self._handle_keepalive) + self.enable_keepalive) self.xmpp.del_event_handler('session_end', - self._handle_session_end) + self.disable_keepalive) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Ping.namespace) - def _handle_keepalive(self, event): - """ - Begin periodic pinging of the server. If a ping is not - answered, the connection will be restarted. - - The pinging interval can be adjused using self.frequency - before beginning processing. + def enable_keepalive(self, interval=None, timeout=None): + if interval: + self.interval = interval + if timeout: + self.timeout = timeout - Arguments: - event -- The session_start event. - """ - def scheduled_ping(): - """Send ping request to the server.""" - log.debug("Pinging...") - try: - self.send_ping(self.xmpp.boundjid.host, self.timeout) - except IqError: - log.debug("Ping response was an error." + \ - "Requesting Reconnect.") - self.xmpp.reconnect() - except IqTimeout: - log.debug("Did not recieve ping back in time." + \ - "Requesting Reconnect.") - self.xmpp.reconnect() - - self.xmpp.schedule('Ping Keep Alive', - self.frequency, - scheduled_ping, + self.keepalive = True + self.xmpp.schedule('Ping keepalive', + self.interval, + self._keepalive, repeat=True) - def _handle_session_end(self, event): - self.xmpp.scheduler.remove('Ping Keep Alive') + def disable_keepalive(self, event=None): + self.xmpp.scheduler.remove('Ping keepalive') - def _handle_ping(self, iq): - """ - Automatically reply to ping requests. + def _keepalive(self, event=None): + log.debug("Keepalive ping...") + try: + rtt = self.ping(self.xmpp.boundjid.host, timeout=self.timeout) + except IqTimeout: + log.debug("Did not recieve ping back in time." + \ + "Requesting Reconnect.") + self.xmpp.reconnect() + else: + log.debug('Keepalive RTT: %s' % rtt) - Arguments: - iq -- The ping request. - """ + def _handle_ping(self, iq): + """Automatically reply to ping requests.""" log.debug("Pinged by %s", iq['from']) iq.reply().send() - def send_ping(self, jid, timeout=None, errorfalse=False, - ifrom=None, block=True, callback=None): - """ - Send a ping request and calculate the response time. + def send_ping(self, jid, ifrom=None, block=True, timeout=None, callback=None): + """Send a ping request. Arguments: jid -- The JID that will receive the ping. - timeout -- Time in seconds to wait for a response. - Defaults to self.timeout. - errorfalse -- Indicates if False should be returned - if an error stanza is received. Defaults - to False. ifrom -- Specifiy the sender JID. block -- Indicate if execution should block until a pong response is received. Defaults to True. + timeout -- Time in seconds to wait for a response. + Defaults to self.timeout. callback -- Optional handler to execute when a pong is received. Useful in conjunction with the option block=False. """ - log.debug("Pinging %s", jid) - if timeout is None: + if not timeout: timeout = self.timeout iq = self.xmpp.Iq() @@ -159,21 +143,44 @@ class XEP_0199(BasePlugin): iq['from'] = ifrom iq.enable('ping') - start_time = time.clock() - - try: - resp = iq.send(block=block, - timeout=timeout, - callback=callback) - except IqError as err: - resp = err.iq + return iq.send(block=block, timeout=timeout, callback=callback) - end_time = time.clock() + def ping(self, jid=None, ifrom=None, timeout=None): + """Send a ping request and calculate RTT. - delay = end_time - start_time + Arguments: + jid -- The JID that will receive the ping. + ifrom -- Specifiy the sender JID. + timeout -- Time in seconds to wait for a response. + Defaults to self.timeout. + """ + own_host = False + if not jid: + if self.xmpp.is_component: + jid = self.xmpp.server + else: + jid = self.xmpp.boundjid.host + jid = JID(jid) + if jid == self.xmpp.boundjid.host or \ + self.xmpp.is_component and jid == self.xmpp.server: + own_host = True + + if not timeout: + timeout = self.timeout - if not block: - return None + start = time.time() - log.debug("Pong: %s %f", jid, delay) - return delay + log.debug('Pinging %s' % jid) + try: + self.send_ping(jid, ifrom=ifrom, timeout=timeout) + except IqError as e: + if own_host: + rtt = time.time() - start + log.debug('Pinged %s, RTT: %s', jid, rtt) + return rtt + else: + raise e + else: + rtt = time.time() - start + log.debug('Pinged %s, RTT: %s', jid, rtt) + return rtt diff --git a/sleekxmpp/plugins/xep_0202/time.py b/sleekxmpp/plugins/xep_0202/time.py index 50af4730..d5b3af37 100644 --- a/sleekxmpp/plugins/xep_0202/time.py +++ b/sleekxmpp/plugins/xep_0202/time.py @@ -30,24 +30,25 @@ class XEP_0202(BasePlugin): description = 'XEP-0202: Entity Time'
dependencies = set(['xep_0030', 'xep_0082'])
stanza = stanza
+ default_config = {
+ #: As a default, respond to time requests with the
+ #: local time returned by XEP-0082. However, a
+ #: custom function can be supplied which accepts
+ #: the JID of the entity to query for the time.
+ 'local_time': None,
+ 'tz_offset': 0
+ }
def plugin_init(self):
"""Start the XEP-0203 plugin."""
- self.tz_offset = self.config.get('tz_offset', 0)
-
- # As a default, respond to time requests with the
- # local time returned by XEP-0082. However, a
- # custom function can be supplied which accepts
- # the JID of the entity to query for the time.
- self.local_time = self.config.get('local_time', None)
-
- def default_local_time(jid):
- return xep_0082.datetime(offset=self.tz_offset)
if not self.local_time:
+ def default_local_time(jid):
+ return xep_0082.datetime(offset=self.tz_offset)
+
self.local_time = default_local_time
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('Entity Time',
StanzaPath('iq/entity_time'),
self._handle_time_request))
diff --git a/sleekxmpp/plugins/xep_0203/stanza.py b/sleekxmpp/plugins/xep_0203/stanza.py index baae4cd3..e147e975 100644 --- a/sleekxmpp/plugins/xep_0203/stanza.py +++ b/sleekxmpp/plugins/xep_0203/stanza.py @@ -8,23 +8,28 @@ import datetime as dt +from sleekxmpp.jid import JID from sleekxmpp.xmlstream import ElementBase from sleekxmpp.plugins import xep_0082 class Delay(ElementBase): - """ - """ - name = 'delay' namespace = 'urn:xmpp:delay' plugin_attrib = 'delay' interfaces = set(('from', 'stamp', 'text')) + def get_from(self): + from_ = self._get_attr('from') + return JID(from_) if from_ else None + + def set_from(self, value): + self._set_attr('from', str(value)) + def get_stamp(self): timestamp = self._get_attr('stamp') - return xep_0082.parse(timestamp) + return xep_0082.parse(timestamp) if timestamp else None def set_stamp(self, value): if isinstance(value, dt.datetime): diff --git a/sleekxmpp/plugins/xep_0222.py b/sleekxmpp/plugins/xep_0222.py index 724ef968..2cc7f703 100644 --- a/sleekxmpp/plugins/xep_0222.py +++ b/sleekxmpp/plugins/xep_0222.py @@ -22,7 +22,7 @@ class XEP_0222(BasePlugin): """ name = 'xep_0222' - description = 'XEP-0222: Persistent Storage of Private Data via PubSub' + description = 'XEP-0222: Persistent Storage of Public Data via PubSub' dependencies = set(['xep_0163', 'xep_0060', 'xep_0004']) profile = {'pubsub#persist_items': True, @@ -76,10 +76,11 @@ class XEP_0222(BasePlugin): ftype='hidden', value='http://jabber.org/protocol/pubsub#publish-options') + fields = options['fields'] for field, value in self.profile.items(): - if field not in options.fields: + if field not in fields: options.add_field(var=field) - options.fields[field]['value'] = value + options['fields'][field]['value'] = value return self.xmpp['xep_0163'].publish(stanza, node, options=options, diff --git a/sleekxmpp/plugins/xep_0223.py b/sleekxmpp/plugins/xep_0223.py index ab99f277..abbecfc7 100644 --- a/sleekxmpp/plugins/xep_0223.py +++ b/sleekxmpp/plugins/xep_0223.py @@ -76,10 +76,11 @@ class XEP_0223(BasePlugin): ftype='hidden', value='http://jabber.org/protocol/pubsub#publish-options') + fields = options['fields'] for field, value in self.profile.items(): - if field not in options.fields: + if field not in fields: options.add_field(var=field) - options.fields[field]['value'] = value + options['fields'][field]['value'] = value return self.xmpp['xep_0163'].publish(stanza, node, options=options, diff --git a/sleekxmpp/plugins/xep_0231/bob.py b/sleekxmpp/plugins/xep_0231/bob.py index d86a5ddf..5e1f590b 100644 --- a/sleekxmpp/plugins/xep_0231/bob.py +++ b/sleekxmpp/plugins/xep_0231/bob.py @@ -10,7 +10,7 @@ import logging import hashlib -from sleekxmpp.stanza import Iq +from sleekxmpp.stanza import Iq, Message, Presence from sleekxmpp.exceptions import XMPPError from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath @@ -36,6 +36,8 @@ class XEP_0231(BasePlugin): self._cids = {} register_stanza_plugin(Iq, BitsOfBinary) + register_stanza_plugin(Message, BitsOfBinary) + register_stanza_plugin(Presence, BitsOfBinary) self.xmpp.register_handler( Callback('Bits of Binary - Iq', diff --git a/sleekxmpp/plugins/xep_0231/stanza.py b/sleekxmpp/plugins/xep_0231/stanza.py index a51f5a03..8bf0d6ee 100644 --- a/sleekxmpp/plugins/xep_0231/stanza.py +++ b/sleekxmpp/plugins/xep_0231/stanza.py @@ -7,9 +7,10 @@ See the file LICENSE for copying permission. """ +import base64 -from base64 import b64encode, b64decode +from sleekxmpp.util import bytes from sleekxmpp.xmlstream import ElementBase @@ -26,10 +27,10 @@ class BitsOfBinary(ElementBase): self._set_attr('max-age', value) def get_data(self): - return b64decode(self.xml.text) + return base64.b64decode(bytes(self.xml.text)) def set_data(self, value): - self.xml.text = b64encode(value) + self.xml.text = bytes(base64.b64encode(value)).decode('utf-8') def del_data(self): self.xml.text = '' diff --git a/sleekxmpp/plugins/xep_0235/__init__.py b/sleekxmpp/plugins/xep_0235/__init__.py new file mode 100644 index 00000000..29d4408a --- /dev/null +++ b/sleekxmpp/plugins/xep_0235/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0235 import stanza +from sleekxmpp.plugins.xep_0235.stanza import OAuth +from sleekxmpp.plugins.xep_0235.oauth import XEP_0235 + + +register_plugin(XEP_0235) diff --git a/sleekxmpp/plugins/xep_0235/oauth.py b/sleekxmpp/plugins/xep_0235/oauth.py new file mode 100644 index 00000000..df0e2ebf --- /dev/null +++ b/sleekxmpp/plugins/xep_0235/oauth.py @@ -0,0 +1,32 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +import logging + +from sleekxmpp import Message +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0235 import stanza, OAuth + + +class XEP_0235(BasePlugin): + + name = 'xep_0235' + description = 'XEP-0235: OAuth Over XMPP' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, OAuth) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature('urn:xmpp:oauth:0') + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:oauth:0') diff --git a/sleekxmpp/plugins/xep_0235/stanza.py b/sleekxmpp/plugins/xep_0235/stanza.py new file mode 100644 index 00000000..0050d583 --- /dev/null +++ b/sleekxmpp/plugins/xep_0235/stanza.py @@ -0,0 +1,80 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import hmac +import hashlib +import urllib +import base64 + +from sleekxmpp.xmlstream import ET, ElementBase, JID + + +class OAuth(ElementBase): + + name = 'oauth' + namespace = 'urn:xmpp:oauth:0' + plugin_attrib = 'oauth' + interfaces = set(['oauth_consumer_key', 'oauth_nonce', 'oauth_signature', + 'oauth_signature_method', 'oauth_timestamp', + 'oauth_token', 'oauth_version']) + sub_interfaces = interfaces + + def generate_signature(self, stanza, sfrom, sto, consumer_secret, + token_secret, method='HMAC-SHA1'): + self['oauth_signature_method'] = method + + request = urllib.quote('%s&%s' % (sfrom, sto), '') + parameters = urllib.quote('&'.join([ + 'oauth_consumer_key=%s' % self['oauth_consumer_key'], + 'oauth_nonce=%s' % self['oauth_nonce'], + 'oauth_signature_method=%s' % self['oauth_signature_method'], + 'oauth_timestamp=%s' % self['oauth_timestamp'], + 'oauth_token=%s' % self['oauth_token'], + 'oauth_version=%s' % self['oauth_version'] + ]), '') + + sigbase = '%s&%s&%s' % (stanza, request, parameters) + + consumer_secret = urllib.quote(consumer_secret, '') + token_secret = urllib.quote(token_secret, '') + key = '%s&%s' % (consumer_secret, token_secret) + + if method == 'HMAC-SHA1': + sig = base64.b64encode(hmac.new(key, sigbase, hashlib.sha1).digest()) + elif method == 'PLAINTEXT': + sig = key + + self['oauth_signature'] = sig + return sig + + def verify_signature(self, stanza, sfrom, sto, consumer_secret, + token_secret): + method = self['oauth_signature_method'] + + request = urllib.quote('%s&%s' % (sfrom, sto), '') + parameters = urllib.quote('&'.join([ + 'oauth_consumer_key=%s' % self['oauth_consumer_key'], + 'oauth_nonce=%s' % self['oauth_nonce'], + 'oauth_signature_method=%s' % self['oauth_signature_method'], + 'oauth_timestamp=%s' % self['oauth_timestamp'], + 'oauth_token=%s' % self['oauth_token'], + 'oauth_version=%s' % self['oauth_version'] + ]), '') + + sigbase = '%s&%s&%s' % (stanza, request, parameters) + + consumer_secret = urllib.quote(consumer_secret, '') + token_secret = urllib.quote(token_secret, '') + key = '%s&%s' % (consumer_secret, token_secret) + + if method == 'HMAC-SHA1': + sig = base64.b64encode(hmac.new(key, sigbase, hashlib.sha1).digest()) + elif method == 'PLAINTEXT': + sig = key + + return self['oauth_signature'] == sig diff --git a/sleekxmpp/plugins/xep_0242.py b/sleekxmpp/plugins/xep_0242.py new file mode 100644 index 00000000..c1bada27 --- /dev/null +++ b/sleekxmpp/plugins/xep_0242.py @@ -0,0 +1,21 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins import BasePlugin, register_plugin + + +class XEP_0242(BasePlugin): + + name = 'xep_0242' + description = 'XEP-0242: XMPP Client Compliance 2009' + dependencies = set(['xep_0030', 'xep_0115', 'xep_0054', + 'xep_0045', 'xep_0085', 'xep_0016', + 'xep_0191']) + + +register_plugin(XEP_0242) diff --git a/sleekxmpp/plugins/xep_0256.py b/sleekxmpp/plugins/xep_0256.py index dd407fff..0db8ea3b 100644 --- a/sleekxmpp/plugins/xep_0256.py +++ b/sleekxmpp/plugins/xep_0256.py @@ -25,10 +25,11 @@ class XEP_0256(BasePlugin): description = 'XEP-0256: Last Activity in Presence' dependencies = set(['xep_0012']) stanza = stanza + default_config = { + 'auto_last_activity': False + } def plugin_init(self): - self.auto_last_activity = self.config.get('auto_last_activity', False) - register_stanza_plugin(Presence, LastActivity) self.xmpp.add_filter('out', self._initial_presence_activity) diff --git a/sleekxmpp/plugins/xep_0257/__init__.py b/sleekxmpp/plugins/xep_0257/__init__.py new file mode 100644 index 00000000..8c5311fd --- /dev/null +++ b/sleekxmpp/plugins/xep_0257/__init__.py @@ -0,0 +1,17 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0257 import stanza +from sleekxmpp.plugins.xep_0257.stanza import Certs, AppendCert +from sleekxmpp.plugins.xep_0257.stanza import DisableCert, RevokeCert +from sleekxmpp.plugins.xep_0257.client_cert_management import XEP_0257 + + +register_plugin(XEP_0257) diff --git a/sleekxmpp/plugins/xep_0257/client_cert_management.py b/sleekxmpp/plugins/xep_0257/client_cert_management.py new file mode 100644 index 00000000..49317843 --- /dev/null +++ b/sleekxmpp/plugins/xep_0257/client_cert_management.py @@ -0,0 +1,65 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp import Iq +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0257 import stanza, Certs +from sleekxmpp.plugins.xep_0257 import AppendCert, DisableCert, RevokeCert + + +log = logging.getLogger(__name__) + + +class XEP_0257(BasePlugin): + + name = 'xep_0257' + description = 'XEP-0258: Client Certificate Management for SASL EXTERNAL' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, Certs) + register_stanza_plugin(Iq, AppendCert) + register_stanza_plugin(Iq, DisableCert) + register_stanza_plugin(Iq, RevokeCert) + + def get_certs(self, ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['from'] = ifrom + iq.enable('sasl_certs') + return iq.send(block=block, timeout=timeout, callback=callback) + + def add_cert(self, name, cert, allow_management=True, ifrom=None, + block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + iq['sasl_cert_append']['name'] = name + iq['sasl_cert_append']['x509cert'] = cert + iq['sasl_cert_append']['cert_management'] = allow_management + return iq.send(block=block, timeout=timeout, callback=callback) + + def disable_cert(self, name, ifrom=None, block=True, + timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + iq['sasl_cert_disable']['name'] = name + return iq.send(block=block, timeout=timeout, callback=callback) + + def revoke_cert(self, name, ifrom=None, block=True, + timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + iq['sasl_cert_revoke']['name'] = name + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0257/stanza.py b/sleekxmpp/plugins/xep_0257/stanza.py new file mode 100644 index 00000000..c3c41db2 --- /dev/null +++ b/sleekxmpp/plugins/xep_0257/stanza.py @@ -0,0 +1,87 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin + + +class Certs(ElementBase): + name = 'items' + namespace = 'urn:xmpp:saslcert:1' + plugin_attrib = 'sasl_certs' + interfaces = set() + + +class CertItem(ElementBase): + name = 'item' + namespace = 'urn:xmpp:saslcert:1' + plugin_attrib = 'item' + plugin_multi_attrib = 'items' + interfaces = set(['name', 'x509cert', 'users']) + sub_interfaces = set(['name', 'x509cert']) + + def get_users(self): + resources = self.xml.findall('{%s}users/{%s}resource' % ( + self.namespace, self.namespace)) + return set([res.text for res in resources]) + + def set_users(self, values): + users = self.xml.find('{%s}users' % self.namespace) + if users is None: + users = ET.Element('{%s}users' % self.namespace) + self.xml.append(users) + for resource in values: + res = ET.Element('{%s}resource' % self.namespace) + res.text = resource + users.append(res) + + def del_users(self): + users = self.xml.find('{%s}users' % self.namespace) + if users is not None: + self.xml.remove(users) + + +class AppendCert(ElementBase): + name = 'append' + namespace = 'urn:xmpp:saslcert:1' + plugin_attrib = 'sasl_cert_append' + interfaces = set(['name', 'x509cert', 'cert_management']) + sub_interfaces = set(['name', 'x509cert']) + + def get_cert_management(self): + manage = self.xml.find('{%s}no-cert-management' % self.namespace) + return manage is None + + def set_cert_management(self, value): + self.del_cert_management() + if not value: + manage = ET.Element('{%s}no-cert-management' % self.namespace) + self.xml.append(manage) + + def del_cert_management(self): + manage = self.xml.find('{%s}no-cert-management' % self.namespace) + if manage is not None: + self.xml.remove(manage) + + +class DisableCert(ElementBase): + name = 'disable' + namespace = 'urn:xmpp:saslcert:1' + plugin_attrib = 'sasl_cert_disable' + interfaces = set(['name']) + sub_interfaces = interfaces + + +class RevokeCert(ElementBase): + name = 'revoke' + namespace = 'urn:xmpp:saslcert:1' + plugin_attrib = 'sasl_cert_revoke' + interfaces = set(['name']) + sub_interfaces = interfaces + + +register_stanza_plugin(Certs, CertItem, iterable=True) diff --git a/sleekxmpp/plugins/xep_0258/stanza.py b/sleekxmpp/plugins/xep_0258/stanza.py index 4d828a46..a506064b 100644 --- a/sleekxmpp/plugins/xep_0258/stanza.py +++ b/sleekxmpp/plugins/xep_0258/stanza.py @@ -8,8 +8,7 @@ from base64 import b64encode, b64decode -from sleekxmpp.thirdparty.suelta.util import bytes - +from sleekxmpp.util import bytes from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin diff --git a/sleekxmpp/plugins/xep_0279/__init__.py b/sleekxmpp/plugins/xep_0279/__init__.py new file mode 100644 index 00000000..93db9e7c --- /dev/null +++ b/sleekxmpp/plugins/xep_0279/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0279 import stanza +from sleekxmpp.plugins.xep_0279.stanza import IPCheck +from sleekxmpp.plugins.xep_0279.ipcheck import XEP_0279 + + +register_plugin(XEP_0279) diff --git a/sleekxmpp/plugins/xep_0279/ipcheck.py b/sleekxmpp/plugins/xep_0279/ipcheck.py new file mode 100644 index 00000000..f8c167c7 --- /dev/null +++ b/sleekxmpp/plugins/xep_0279/ipcheck.py @@ -0,0 +1,39 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +import logging + +from sleekxmpp import Iq +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0279 import stanza, IPCheck + + +class XEP_0279(BasePlugin): + + name = 'xep_0279' + description = 'XEP-0279: Server IP Check' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, IPCheck) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature('urn:xmpp:sic:0') + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:sic:0') + + def check_ip(self, ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['from'] = ifrom + iq.enable('ip_check') + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0279/stanza.py b/sleekxmpp/plugins/xep_0279/stanza.py new file mode 100644 index 00000000..181b5957 --- /dev/null +++ b/sleekxmpp/plugins/xep_0279/stanza.py @@ -0,0 +1,30 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase + + +class IPCheck(ElementBase): + + name = 'ip' + namespace = 'urn:xmpp:sic:0' + plugin_attrib = 'ip_check' + interfaces = set(['ip_check']) + is_extension = True + + def get_ip_check(self): + return self.xml.text + + def set_ip_check(self, value): + if value: + self.xml.text = value + else: + self.xml.text = '' + + def del_ip_check(self): + self.xml.text = '' diff --git a/sleekxmpp/plugins/xep_0280/__init__.py b/sleekxmpp/plugins/xep_0280/__init__.py new file mode 100644 index 00000000..929321af --- /dev/null +++ b/sleekxmpp/plugins/xep_0280/__init__.py @@ -0,0 +1,17 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0280.stanza import ReceivedCarbon, SentCarbon +from sleekxmpp.plugins.xep_0280.stanza import PrivateCarbon +from sleekxmpp.plugins.xep_0280.stanza import CarbonEnable, CarbonDisable +from sleekxmpp.plugins.xep_0280.carbons import XEP_0280 + + +register_plugin(XEP_0280) diff --git a/sleekxmpp/plugins/xep_0280/carbons.py b/sleekxmpp/plugins/xep_0280/carbons.py new file mode 100644 index 00000000..482d046a --- /dev/null +++ b/sleekxmpp/plugins/xep_0280/carbons.py @@ -0,0 +1,81 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +import logging + +import sleekxmpp +from sleekxmpp.stanza import Message, Iq +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0280 import stanza + + +log = logging.getLogger(__name__) + + +class XEP_0280(BasePlugin): + + """ + XEP-0280 Message Carbons + """ + + name = 'xep_0280' + description = 'XEP-0280: Message Carbons' + dependencies = set(['xep_0030', 'xep_0297']) + stanza = stanza + + def plugin_init(self): + self.xmpp.register_handler( + Callback('Carbon Received', + StanzaPath('message/carbon_received'), + self._handle_carbon_received)) + self.xmpp.register_handler( + Callback('Carbon Sent', + StanzaPath('message/carbon_sent'), + self._handle_carbon_sent)) + + register_stanza_plugin(Message, stanza.ReceivedCarbon) + register_stanza_plugin(Message, stanza.SentCarbon) + register_stanza_plugin(Message, stanza.PrivateCarbon) + register_stanza_plugin(Iq, stanza.CarbonEnable) + register_stanza_plugin(Iq, stanza.CarbonDisable) + + register_stanza_plugin(stanza.ReceivedCarbon, + self.xmpp['xep_0297'].stanza.Forwarded) + register_stanza_plugin(stanza.SentCarbon, + self.xmpp['xep_0297'].stanza.Forwarded) + + def plugin_end(self): + self.xmpp.remove_handler('Carbon Received') + self.xmpp.remove_handler('Carbon Sent') + self.xmpp.plugin['xep_0030'].del_feature(feature='urn:xmpp:carbons:2') + + def session_bind(self, jid): + self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:carbons:2') + + def _handle_carbon_received(self, msg): + self.xmpp.event('carbon_received', msg) + + def _handle_carbon_sent(self, msg): + self.xmpp.event('carbon_sent', msg) + + def enable(self, ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + iq.enable('carbon_enable') + return iq.send(block=block, timeout=timeout, callback=callback) + + def disable(self, ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + iq.enable('carbon_disable') + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0280/stanza.py b/sleekxmpp/plugins/xep_0280/stanza.py new file mode 100644 index 00000000..2f3aad86 --- /dev/null +++ b/sleekxmpp/plugins/xep_0280/stanza.py @@ -0,0 +1,64 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.xmlstream import ElementBase + + +class ReceivedCarbon(ElementBase): + name = 'received' + namespace = 'urn:xmpp:carbons:2' + plugin_attrib = 'carbon_received' + interfaces = set(['carbon_received']) + is_extension = True + + def get_carbon_received(self): + return self['forwarded']['stanza'] + + def del_carbon_received(self): + del self['forwarded']['stanza'] + + def set_carbon_received(self, stanza): + self['forwarded']['stanza'] = stanza + + +class SentCarbon(ElementBase): + name = 'sent' + namespace = 'urn:xmpp:carbons:2' + plugin_attrib = 'carbon_sent' + interfaces = set(['carbon_sent']) + is_extension = True + + def get_carbon_sent(self): + return self['forwarded']['stanza'] + + def del_carbon_sent(self): + del self['forwarded']['stanza'] + + def set_carbon_sent(self, stanza): + self['forwarded']['stanza'] = stanza + + +class PrivateCarbon(ElementBase): + name = 'private' + namespace = 'urn:xmpp:carbons:2' + plugin_attrib = 'carbon_private' + interfaces = set() + + +class CarbonEnable(ElementBase): + name = 'enable' + namespace = 'urn:xmpp:carbons:2' + plugin_attrib = 'carbon_enable' + interfaces = set() + + +class CarbonDisable(ElementBase): + name = 'disable' + namespace = 'urn:xmpp:carbons:2' + plugin_attrib = 'carbon_disable' + interfaces = set() diff --git a/sleekxmpp/plugins/xep_0297/__init__.py b/sleekxmpp/plugins/xep_0297/__init__.py new file mode 100644 index 00000000..551d9420 --- /dev/null +++ b/sleekxmpp/plugins/xep_0297/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0297 import stanza +from sleekxmpp.plugins.xep_0297.stanza import Forwarded +from sleekxmpp.plugins.xep_0297.forwarded import XEP_0297 + + +register_plugin(XEP_0297) diff --git a/sleekxmpp/plugins/xep_0297/forwarded.py b/sleekxmpp/plugins/xep_0297/forwarded.py new file mode 100644 index 00000000..95703a2d --- /dev/null +++ b/sleekxmpp/plugins/xep_0297/forwarded.py @@ -0,0 +1,64 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +import logging + +from sleekxmpp import Iq, Message, Presence +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.xep_0297 import stanza, Forwarded + + +class XEP_0297(BasePlugin): + + name = 'xep_0297' + description = 'XEP-0297: Stanza Forwarding' + dependencies = set(['xep_0030', 'xep_0203']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, Forwarded) + + # While these are marked as iterable, that is just for + # making it easier to extract the forwarded stanza. There + # still can be only a single forwarded stanza. + register_stanza_plugin(Forwarded, Message, iterable=True) + register_stanza_plugin(Forwarded, Presence, iterable=True) + register_stanza_plugin(Forwarded, Iq, iterable=True) + + register_stanza_plugin(Forwarded, self.xmpp['xep_0203'].stanza.Delay) + + self.xmpp.register_handler( + Callback('Forwarded Stanza', + StanzaPath('message/forwarded'), + self._handle_forwarded)) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature('urn:xmpp:forward:0') + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:forward:0') + self.xmpp.remove_handler('Forwarded Stanza') + + def forward(self, stanza=None, mto=None, mbody=None, mfrom=None, delay=None): + stanza.stream = None + + msg = self.xmpp.Message() + msg['to'] = mto + msg['from'] = mfrom + msg['body'] = mbody + msg['forwarded']['stanza'] = stanza + if delay is not None: + msg['forwarded']['delay']['stamp'] = delay + msg.send() + + def _handle_forwarded(self, msg): + self.xmpp.event('forwarded_stanza', msg) diff --git a/sleekxmpp/plugins/xep_0297/stanza.py b/sleekxmpp/plugins/xep_0297/stanza.py new file mode 100644 index 00000000..8b97accc --- /dev/null +++ b/sleekxmpp/plugins/xep_0297/stanza.py @@ -0,0 +1,36 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.stanza import Message, Presence, Iq +from sleekxmpp.xmlstream import ElementBase + + +class Forwarded(ElementBase): + name = 'forwarded' + namespace = 'urn:xmpp:forward:0' + plugin_attrib = 'forwarded' + interfaces = set(['stanza']) + + def get_stanza(self): + for stanza in self: + if isinstance(stanza, (Message, Presence, Iq)): + return stanza + return '' + + def set_stanza(self, value): + self.del_stanza() + self.append(value) + + def del_stanza(self): + found_stanzas = [] + for stanza in self: + if isinstance(stanza, (Message, Presence, Iq)): + found_stanzas.append(stanza) + for stanza in found_stanzas: + self.iterables.remove(stanza) + self.xml.remove(stanza.xml) diff --git a/sleekxmpp/plugins/xep_0308/__init__.py b/sleekxmpp/plugins/xep_0308/__init__.py new file mode 100644 index 00000000..a6a100ee --- /dev/null +++ b/sleekxmpp/plugins/xep_0308/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0308.stanza import Replace +from sleekxmpp.plugins.xep_0308.correction import XEP_0308 + + +register_plugin(XEP_0308) diff --git a/sleekxmpp/plugins/xep_0308/correction.py b/sleekxmpp/plugins/xep_0308/correction.py new file mode 100644 index 00000000..d32b4bc4 --- /dev/null +++ b/sleekxmpp/plugins/xep_0308/correction.py @@ -0,0 +1,52 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +import logging + +import sleekxmpp +from sleekxmpp.stanza import Message +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0308 import stanza, Replace + + +log = logging.getLogger(__name__) + + +class XEP_0308(BasePlugin): + + """ + XEP-0308 Last Message Correction + """ + + name = 'xep_0308' + description = 'XEP-0308: Last Message Correction' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + self.xmpp.register_handler( + Callback('Message Correction', + StanzaPath('message/replace'), + self._handle_correction)) + + register_stanza_plugin(Message, Replace) + + self.xmpp.use_message_ids = True + + def plugin_end(self): + self.xmpp.remove_handler('Message Correction') + self.xmpp.plugin['xep_0030'].del_feature(feature=Replace.namespace) + + def session_bind(self, jid): + self.xmpp.plugin['xep_0030'].add_feature(Replace.namespace) + + def _handle_correction(self, msg): + self.xmpp.event('message_correction', msg) diff --git a/sleekxmpp/plugins/xep_0308/stanza.py b/sleekxmpp/plugins/xep_0308/stanza.py new file mode 100644 index 00000000..8f88cbc0 --- /dev/null +++ b/sleekxmpp/plugins/xep_0308/stanza.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.xmlstream import ElementBase + + +class Replace(ElementBase): + name = 'replace' + namespace = 'urn:xmpp:message-correct:0' + plugin_attrib = 'replace' + interfaces = set(['id']) diff --git a/sleekxmpp/plugins/xep_0313/__init__.py b/sleekxmpp/plugins/xep_0313/__init__.py new file mode 100644 index 00000000..8b6ed97d --- /dev/null +++ b/sleekxmpp/plugins/xep_0313/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0313.stanza import Result, MAM, Preferences +from sleekxmpp.plugins.xep_0313.mam import XEP_0313 + + +register_plugin(XEP_0313) diff --git a/sleekxmpp/plugins/xep_0313/mam.py b/sleekxmpp/plugins/xep_0313/mam.py new file mode 100644 index 00000000..4b82ca03 --- /dev/null +++ b/sleekxmpp/plugins/xep_0313/mam.py @@ -0,0 +1,94 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +import logging + +import sleekxmpp +from sleekxmpp.stanza import Message, Iq +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.xmlstream.handler import Collector +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0313 import stanza + + +log = logging.getLogger(__name__) + + +class XEP_0313(BasePlugin): + + """ + XEP-0313 Message Archive Management + """ + + name = 'xep_0313' + description = 'XEP-0313: Message Archive Management' + dependencies = set(['xep_0030', 'xep_0050', 'xep_0059', 'xep_0297']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, stanza.MAM) + register_stanza_plugin(Iq, stanza.Preferences) + register_stanza_plugin(Message, stanza.Result) + register_stanza_plugin(Message, stanza.Archived, iterable=True) + register_stanza_plugin(stanza.Result, self.xmpp['xep_0297'].stanza.Forwarded) + register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set) + + def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None, + block=True, timeout=None, callback=None, iterator=False): + iq = self.xmpp.Iq() + query_id = iq['id'] + + iq['to'] = jid + iq['from'] = ifrom + iq['type'] = 'get' + iq['mam']['queryid'] = query_id + iq['mam']['start'] = start + iq['mam']['end'] = end + iq['mam']['with'] = with_jid + + collector = Collector( + 'MAM_Results_%s' % query_id, + StanzaPath('message/mam_result@queryid=%s' % query_id)) + self.xmpp.register_handler(collector) + + if iterator: + return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results') + elif not block and callback is not None: + def wrapped_cb(iq): + results = collector.stop() + if iq['type'] == 'result': + iq['mam']['results'] = results + callback(iq) + return iq.send(block=block, timeout=timeout, callback=wrapped_cb) + else: + try: + resp = iq.send(block=block, timeout=timeout, callback=callback) + resp['mam']['results'] = collector.stop() + return resp + except XMPPError as e: + collector.stop() + raise e + + def set_preferences(self, jid=None, default=None, always=None, never=None, + ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = jid + iq['from'] = ifrom + iq['mam_prefs']['default'] = default + iq['mam_prefs']['always'] = always + iq['mam_prefs']['never'] = never + return iq.send(block=block, timeout=timeout, callback=callback) + + def get_configuration_commands(self, jid, **kwargs): + return self.xmpp['xep_0030'].get_items( + jid=jid, + node='urn:xmpp:mam#configure', + **kwargs) diff --git a/sleekxmpp/plugins/xep_0313/stanza.py b/sleekxmpp/plugins/xep_0313/stanza.py new file mode 100644 index 00000000..81576cd4 --- /dev/null +++ b/sleekxmpp/plugins/xep_0313/stanza.py @@ -0,0 +1,139 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +import datetime as dt + +from sleekxmpp.jid import JID +from sleekxmpp.xmlstream import ElementBase, ET +from sleekxmpp.plugins import xep_0082 + + +class MAM(ElementBase): + name = 'query' + namespace = 'urn:xmpp:mam:tmp' + plugin_attrib = 'mam' + interfaces = set(['queryid', 'start', 'end', 'with', 'results']) + sub_interfaces = set(['start', 'end', 'with']) + + def setup(self, xml=None): + ElementBase.setup(self, xml) + self._results = [] + + def get_start(self): + timestamp = self._get_sub_text('start') + return xep_0082.parse(timestamp) + + def set_start(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_sub_text('start', value) + + def get_end(self): + timestamp = self._get_sub_text('end') + return xep_0082.parse(timestamp) + + def set_end(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_sub_text('end', value) + + def get_with(self): + return JID(self._get_sub_text('with')) + + def set_with(self, value): + self._set_sub_text('with', str(value)) + + # The results interface is meant only as an easy + # way to access the set of collected message responses + # from the query. + + def get_results(self): + return self._results + + def set_results(self, values): + self._results = values + + def del_results(self): + self._results = [] + + +class Preferences(ElementBase): + name = 'prefs' + namespace = 'urn:xmpp:mam:tmp' + plugin_attrib = 'mam_prefs' + interfaces = set(['default', 'always', 'never']) + sub_interfaces = set(['always', 'never']) + + def get_always(self): + results = set() + + jids = self.xml.findall('{%s}always/{%s}jid' % ( + self.namespace, self.namespace)) + + for jid in jids: + results.add(JID(jid.text)) + + return results + + def set_always(self, value): + self._set_sub_text('always', '', keep=True) + always = self.xml.find('{%s}always' % self.namespace) + always.clear() + + if not isinstance(value, (list, set)): + value = [value] + + for jid in value: + jid_xml = ET.Element('{%s}jid' % self.namespace) + jid_xml.text = str(jid) + always.append(jid_xml) + + def get_never(self): + results = set() + + jids = self.xml.findall('{%s}never/{%s}jid' % ( + self.namespace, self.namespace)) + + for jid in jids: + results.add(JID(jid.text)) + + return results + + def set_never(self, value): + self._set_sub_text('never', '', keep=True) + never = self.xml.find('{%s}never' % self.namespace) + never.clear() + + if not isinstance(value, (list, set)): + value = [value] + + for jid in value: + jid_xml = ET.Element('{%s}jid' % self.namespace) + jid_xml.text = str(jid) + never.append(jid_xml) + + +class Result(ElementBase): + name = 'result' + namespace = 'urn:xmpp:mam:tmp' + plugin_attrib = 'mam_result' + interfaces = set(['queryid', 'id']) + + +class Archived(ElementBase): + name = 'archived' + namespace = 'urn:xmpp:mam:tmp' + plugin_attrib = 'mam_archived' + plugin_multi_attrib = 'mam_archives' + interfaces = set(['by', 'id']) + + def get_by(self): + return JID(self._get_attr('by')) + + def set_by(self): + return self._set_attr('by', str(value)) diff --git a/sleekxmpp/plugins/xep_0319/__init__.py b/sleekxmpp/plugins/xep_0319/__init__.py new file mode 100644 index 00000000..4756e63e --- /dev/null +++ b/sleekxmpp/plugins/xep_0319/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0319 import stanza +from sleekxmpp.plugins.xep_0319.stanza import Idle +from sleekxmpp.plugins.xep_0319.idle import XEP_0319 + + +register_plugin(XEP_0319) diff --git a/sleekxmpp/plugins/xep_0319/idle.py b/sleekxmpp/plugins/xep_0319/idle.py new file mode 100644 index 00000000..90456f9f --- /dev/null +++ b/sleekxmpp/plugins/xep_0319/idle.py @@ -0,0 +1,75 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from datetime import datetime, timedelta + +from sleekxmpp.stanza import Presence +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.xep_0319 import stanza + + +class XEP_0319(BasePlugin): + name = 'xep_0319' + description = 'XEP-0319: Last User Interaction in Presence' + dependencies = set(['xep_0012']) + stanza = stanza + + def plugin_init(self): + self._idle_stamps = {} + register_stanza_plugin(Presence, stanza.Idle) + self.api.register(self._set_idle, + 'set_idle', + default=True) + self.api.register(self._get_idle, + 'get_idle', + default=True) + self.xmpp.register_handler( + Callback('Idle Presence', + StanzaPath('presence/idle'), + self._idle_presence)) + self.xmpp.add_filter('out', self._stamp_idle_presence) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature('urn:xmpp:idle:1') + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:idle:1') + self.xmpp.del_filter('out', self._stamp_idle_presence) + self.xmpp.remove_handler('Idle Presence') + + def idle(self, jid=None, since=None): + seconds = None + if since is None: + since = datetime.now() + else: + seconds = datetime.now() - since + self.api['set_idle'](jid, None, None, since) + self.xmpp['xep_0012'].set_last_activity(jid=jid, seconds=seconds) + + def active(self, jid=None): + self.api['set_idle'](jid, None, None, None) + self.xmpp['xep_0012'].del_last_activity(jid) + + def _set_idle(self, jid, node, ifrom, data): + self._idle_stamps[jid] = data + + def _get_idle(self, jid, node, ifrom, data): + return self._idle_stamps.get(jid, None) + + def _idle_presence(self, pres): + self.xmpp.event('presence_idle', pres) + + def _stamp_idle_presence(self, stanza): + if isinstance(stanza, Presence): + since = self.api['get_idle'](stanza['from'] or self.xmpp.boundjid) + if since: + stanza['idle']['since'] = since + return stanza diff --git a/sleekxmpp/plugins/xep_0319/stanza.py b/sleekxmpp/plugins/xep_0319/stanza.py new file mode 100644 index 00000000..abfb4f41 --- /dev/null +++ b/sleekxmpp/plugins/xep_0319/stanza.py @@ -0,0 +1,28 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt + +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class Idle(ElementBase): + name = 'idle' + namespace = 'urn:xmpp:idle:1' + plugin_attrib = 'idle' + interfaces = set(['since']) + + def get_since(self): + timestamp = self._get_attr('since') + return xep_0082.parse(timestamp) + + def set_since(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('since', value) diff --git a/sleekxmpp/plugins/xep_0323/__init__.py b/sleekxmpp/plugins/xep_0323/__init__.py new file mode 100644 index 00000000..10779ada --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/__init__.py @@ -0,0 +1,18 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0323.sensordata import XEP_0323 +from sleekxmpp.plugins.xep_0323 import stanza + +register_plugin(XEP_0323) + +xep_0323=XEP_0323 diff --git a/sleekxmpp/plugins/xep_0323/device.py b/sleekxmpp/plugins/xep_0323/device.py new file mode 100644 index 00000000..80e6fd95 --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/device.py @@ -0,0 +1,258 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime +import logging + +class Device(object): + """ + Example implementation of a device readout object. + Is registered in the XEP_0323.register_node call + The device object may be any custom implementation to support + specific devices, but it must implement the functions: + has_field + request_fields + """ + + def __init__(self, nodeId, fields=None): + if not fields: + fields = {} + + self.nodeId = nodeId + self.fields = fields # see fields described below + # {'type':'numeric', + # 'name':'myname', + # 'value': 42, + # 'unit':'Z'}]; + self.timestamp_data = {} + self.momentary_data = {} + self.momentary_timestamp = "" + logging.debug("Device object started nodeId %s",nodeId) + + def has_field(self, field): + """ + Returns true if the supplied field name exists in this device. + + Arguments: + field -- The field name + """ + if field in self.fields.keys(): + return True + return False + + def refresh(self, fields): + """ + override method to do the refresh work + refresh values from hardware or other + """ + pass + + + def request_fields(self, fields, flags, session, callback): + """ + Starts a data readout. Verifies the requested fields, + refreshes the data (if needed) and calls the callback + with requested data. + + + Arguments: + fields -- List of field names to readout + flags -- [optional] data classifier flags for the field, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + session -- Session id, only used in the callback as identifier + callback -- Callback function to call when data is available. + + The callback function must support the following arguments: + + session -- Session id, as supplied in the request_fields call + nodeId -- Identifier for this device + result -- The current result status of the readout. Valid values are: + "error" - Readout failed. + "fields" - Contains readout data. + "done" - Indicates that the readout is complete. May contain + readout data. + timestamp_block -- [optional] Only applies when result != "error" + The readout data. Structured as a dictionary: + { + timestamp: timestamp for this datablock, + fields: list of field dictionary (one per readout field). + readout field dictionary format: + { + type: The field type (numeric, boolean, dateTime, timeSpan, string, enum) + name: The field name + value: The field value + unit: The unit of the field. Only applies to type numeric. + dataType: The datatype of the field. Only applies to type enum. + flags: [optional] data classifier flags for the field, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + } + } + error_msg -- [optional] Only applies when result == "error". + Error details when a request failed. + + """ + logging.debug("request_fields called looking for fields %s",fields) + if len(fields) > 0: + # Check availiability + for f in fields: + if f not in self.fields.keys(): + self._send_reject(session, callback) + return False + else: + # Request all fields + fields = self.fields.keys() + + + # Refresh data from device + # ... + logging.debug("about to refresh device fields %s",fields) + self.refresh(fields) + + if "momentary" in flags and flags['momentary'] == "true" or \ + "all" in flags and flags['all'] == "true": + ts_block = {} + timestamp = "" + + if len(self.momentary_timestamp) > 0: + timestamp = self.momentary_timestamp + else: + timestamp = self._get_timestamp() + + field_block = [] + for f in self.momentary_data: + if f in fields: + field_block.append({"name": f, + "type": self.fields[f]["type"], + "unit": self.fields[f]["unit"], + "dataType": self.fields[f]["dataType"], + "value": self.momentary_data[f]["value"], + "flags": self.momentary_data[f]["flags"]}) + ts_block["timestamp"] = timestamp + ts_block["fields"] = field_block + + callback(session, result="done", nodeId=self.nodeId, timestamp_block=ts_block) + return + + from_flag = self._datetime_flag_parser(flags, 'from') + to_flag = self._datetime_flag_parser(flags, 'to') + + for ts in sorted(self.timestamp_data.keys()): + tsdt = datetime.datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S") + if not from_flag is None: + if tsdt < from_flag: + #print (str(tsdt) + " < " + str(from_flag)) + continue + if not to_flag is None: + if tsdt > to_flag: + #print (str(tsdt) + " > " + str(to_flag)) + continue + + ts_block = {} + field_block = [] + + for f in self.timestamp_data[ts]: + if f in fields: + field_block.append({"name": f, + "type": self.fields[f]["type"], + "unit": self.fields[f]["unit"], + "dataType": self.fields[f]["dataType"], + "value": self.timestamp_data[ts][f]["value"], + "flags": self.timestamp_data[ts][f]["flags"]}) + + ts_block["timestamp"] = ts + ts_block["fields"] = field_block + callback(session, result="fields", nodeId=self.nodeId, timestamp_block=ts_block) + callback(session, result="done", nodeId=self.nodeId, timestamp_block=None) + + def _datetime_flag_parser(self, flags, flagname): + if not flagname in flags: + return None + + dt = None + try: + dt = datetime.datetime.strptime(flags[flagname], "%Y-%m-%dT%H:%M:%S") + except ValueError: + # Badly formatted datetime, ignore it + pass + return dt + + + def _get_timestamp(self): + """ + Generates a properly formatted timestamp of current time + """ + return datetime.datetime.now().replace(microsecond=0).isoformat() + + def _send_reject(self, session, callback): + """ + Sends a reject to the caller + + Arguments: + session -- Session id, see definition in request_fields function + callback -- Callback function, see definition in request_fields function + """ + callback(session, result="error", nodeId=self.nodeId, timestamp_block=None, error_msg="Reject") + + def _add_field(self, name, typename, unit=None, dataType=None): + """ + Adds a field to the device + + Arguments: + name -- Name of the field + typename -- Type of the field (numeric, boolean, dateTime, timeSpan, string, enum) + unit -- [optional] only applies to "numeric". Unit for the field. + dataType -- [optional] only applies to "enum". Datatype for the field. + """ + self.fields[name] = {"type": typename, "unit": unit, "dataType": dataType} + + def _add_field_timestamp_data(self, name, timestamp, value, flags=None): + """ + Adds timestamped data to a field + + Arguments: + name -- Name of the field + timestamp -- Timestamp for the data (string) + value -- Field value at the timestamp + flags -- [optional] data classifier flags for the field, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + """ + if not name in self.fields.keys(): + return False + if not timestamp in self.timestamp_data: + self.timestamp_data[timestamp] = {} + + self.timestamp_data[timestamp][name] = {"value": value, "flags": flags} + return True + + def _add_field_momentary_data(self, name, value, flags=None): + """ + Sets momentary data to a field + + Arguments: + name -- Name of the field + value -- Field value at the timestamp + flags -- [optional] data classifier flags for the field, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + """ + if name not in self.fields: + return False + if flags is None: + flags = {} + + flags["momentary"] = "true" + self.momentary_data[name] = {"value": value, "flags": flags} + return True + + def _set_momentary_timestamp(self, timestamp): + """ + This function is only for unit testing to produce predictable results. + """ + self.momentary_timestamp = timestamp + diff --git a/sleekxmpp/plugins/xep_0323/sensordata.py b/sleekxmpp/plugins/xep_0323/sensordata.py new file mode 100644 index 00000000..30c28504 --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/sensordata.py @@ -0,0 +1,723 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import time +import datetime +from threading import Thread, Lock, Timer + +from sleekxmpp.plugins.xep_0323.timerreset import TimerReset + +from sleekxmpp.xmlstream import JID +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0323 import stanza +from sleekxmpp.plugins.xep_0323.stanza import Sensordata + + +log = logging.getLogger(__name__) + + +class XEP_0323(BasePlugin): + + """ + XEP-0323: IoT Sensor Data + + + This XEP provides the underlying architecture, basic operations and data + structures for sensor data communication over XMPP networks. It includes + a hardware abstraction model, removing any technical detail implemented + in underlying technologies. + + Also see <http://xmpp.org/extensions/xep-0323.html> + + Configuration Values: + threaded -- Indicates if communication with sensors should be threaded. + Defaults to True. + + Events: + Sensor side + ----------- + Sensordata Event:Req -- Received a request for data + Sensordata Event:Cancel -- Received a cancellation for a request + + Client side + ----------- + Sensordata Event:Accepted -- Received a accept from sensor for a request + Sensordata Event:Rejected -- Received a reject from sensor for a request + Sensordata Event:Cancelled -- Received a cancel confirm from sensor + Sensordata Event:Fields -- Received fields from sensor for a request + This may be triggered multiple times since + the sensor can split up its response in + multiple messages. + Sensordata Event:Failure -- Received a failure indication from sensor + for a request. Typically a comm timeout. + + Attributes: + threaded -- Indicates if command events should be threaded. + Defaults to True. + sessions -- A dictionary or equivalent backend mapping + session IDs to dictionaries containing data + relevant to a request's session. This dictionary is used + both by the client and sensor side. On client side, seqnr + is used as key, while on sensor side, a session_id is used + as key. This ensures that the two will not collide, so + one instance can be both client and sensor. + Sensor side + ----------- + nodes -- A dictionary mapping sensor nodes that are serviced through + this XMPP instance to their device handlers ("drivers"). + Client side + ----------- + last_seqnr -- The last used sequence number (integer). One sequence of + communication (e.g. -->request, <--accept, <--fields) + between client and sensor is identified by a unique + sequence number (unique between the client/sensor pair) + + Methods: + plugin_init -- Overrides base_plugin.plugin_init + post_init -- Overrides base_plugin.post_init + plugin_end -- Overrides base_plugin.plugin_end + + Sensor side + ----------- + register_node -- Register a sensor as available from this XMPP + instance. + + Client side + ----------- + request_data -- Initiates a request for data from one or more + sensors. Non-blocking, a callback function will + be called when data is available. + + """ + + name = 'xep_0323' + description = 'XEP-0323 Internet of Things - Sensor Data' + dependencies = set(['xep_0030']) + stanza = stanza + + + default_config = { + 'threaded': True +# 'session_db': None + } + + def plugin_init(self): + """ Start the XEP-0323 plugin """ + + self.xmpp.register_handler( + Callback('Sensordata Event:Req', + StanzaPath('iq@type=get/req'), + self._handle_event_req)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Accepted', + StanzaPath('iq@type=result/accepted'), + self._handle_event_accepted)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Rejected', + StanzaPath('iq@type=error/rejected'), + self._handle_event_rejected)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Cancel', + StanzaPath('iq@type=get/cancel'), + self._handle_event_cancel)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Cancelled', + StanzaPath('iq@type=result/cancelled'), + self._handle_event_cancelled)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Fields', + StanzaPath('message/fields'), + self._handle_event_fields)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Failure', + StanzaPath('message/failure'), + self._handle_event_failure)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Started', + StanzaPath('message/started'), + self._handle_event_started)) + + # Server side dicts + self.nodes = {} + self.sessions = {} + + self.last_seqnr = 0 + self.seqnr_lock = Lock() + + ## For testning only + self.test_authenticated_from = "" + + def post_init(self): + """ Init complete. Register our features in Serivce discovery. """ + BasePlugin.post_init(self) + self.xmpp['xep_0030'].add_feature(Sensordata.namespace) + self.xmpp['xep_0030'].set_items(node=Sensordata.namespace, items=tuple()) + + def _new_session(self): + """ Return a new session ID. """ + return str(time.time()) + '-' + self.xmpp.new_id() + + def session_bind(self, jid): + logging.debug("setting the Disco discovery for %s" % Sensordata.namespace) + self.xmpp['xep_0030'].add_feature(Sensordata.namespace) + self.xmpp['xep_0030'].set_items(node=Sensordata.namespace, items=tuple()) + + + def plugin_end(self): + """ Stop the XEP-0323 plugin """ + self.sessions.clear() + self.xmpp.remove_handler('Sensordata Event:Req') + self.xmpp.remove_handler('Sensordata Event:Accepted') + self.xmpp.remove_handler('Sensordata Event:Rejected') + self.xmpp.remove_handler('Sensordata Event:Cancel') + self.xmpp.remove_handler('Sensordata Event:Cancelled') + self.xmpp.remove_handler('Sensordata Event:Fields') + self.xmpp['xep_0030'].del_feature(feature=Sensordata.namespace) + + + # ================================================================= + # Sensor side (data provider) API + + def register_node(self, nodeId, device, commTimeout, sourceId=None, cacheType=None): + """ + Register a sensor/device as available for serving of data through this XMPP + instance. + + The device object may by any custom implementation to support + specific devices, but it must implement the functions: + has_field + request_fields + according to the interfaces shown in the example device.py file. + + Arguments: + nodeId -- The identifier for the device + device -- The device object + commTimeout -- Time in seconds to wait between each callback from device during + a data readout. Float. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + self.nodes[nodeId] = {"device": device, + "commTimeout": commTimeout, + "sourceId": sourceId, + "cacheType": cacheType} + + def _set_authenticated(self, auth=''): + """ Internal testing function """ + self.test_authenticated_from = auth + + + def _handle_event_req(self, iq): + """ + Event handler for reception of an Iq with req - this is a request. + + Verifies that + - all the requested nodes are available + - at least one of the requested fields is available from at least + one of the nodes + + If the request passes verification, an accept response is sent, and + the readout process is started in a separate thread. + If the verification fails, a reject message is sent. + """ + + seqnr = iq['req']['seqnr'] + error_msg = '' + req_ok = True + + # Authentication + if len(self.test_authenticated_from) > 0 and not iq['from'] == self.test_authenticated_from: + # Invalid authentication + req_ok = False + error_msg = "Access denied" + + # Nodes + process_nodes = [] + if len(iq['req']['nodes']) > 0: + for n in iq['req']['nodes']: + if not n['nodeId'] in self.nodes: + req_ok = False + error_msg = "Invalid nodeId " + n['nodeId'] + process_nodes = [n['nodeId'] for n in iq['req']['nodes']] + else: + process_nodes = self.nodes.keys() + + # Fields - if we just find one we are happy, otherwise we reject + process_fields = [] + if len(iq['req']['fields']) > 0: + found = False + for f in iq['req']['fields']: + for node in self.nodes: + if self.nodes[node]["device"].has_field(f['name']): + found = True + break + if not found: + req_ok = False + error_msg = "Invalid field " + f['name'] + process_fields = [f['name'] for n in iq['req']['fields']] + + req_flags = iq['req']._get_flags() + + request_delay_sec = None + if 'when' in req_flags: + # Timed request - requires datetime string in iso format + # ex. 2013-04-05T15:00:03 + dt = None + try: + dt = datetime.datetime.strptime(req_flags['when'], "%Y-%m-%dT%H:%M:%S") + except ValueError: + req_ok = False + error_msg = "Invalid datetime in 'when' flag, please use ISO format (i.e. 2013-04-05T15:00:03)." + + if not dt is None: + # Datetime properly formatted + dtnow = datetime.datetime.now() + dtdiff = dt - dtnow + request_delay_sec = dtdiff.seconds + dtdiff.days * 24 * 3600 + if request_delay_sec <= 0: + req_ok = False + error_msg = "Invalid datetime in 'when' flag, cannot set a time in the past. Current time: " + dtnow.isoformat() + + if req_ok: + session = self._new_session() + self.sessions[session] = {"from": iq['from'], "to": iq['to'], "seqnr": seqnr} + self.sessions[session]["commTimers"] = {} + self.sessions[session]["nodeDone"] = {} + + #print("added session: " + str(self.sessions)) + + iq.reply() + iq['accepted']['seqnr'] = seqnr + if not request_delay_sec is None: + iq['accepted']['queued'] = "true" + iq.send(block=False) + + self.sessions[session]["node_list"] = process_nodes + + if not request_delay_sec is None: + # Delay request to requested time + timer = Timer(request_delay_sec, self._event_delayed_req, args=(session, process_fields, req_flags)) + self.sessions[session]["commTimers"]["delaytimer"] = timer + timer.start() + return + + if self.threaded: + #print("starting thread") + tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields, req_flags)) + tr_req.start() + #print("started thread") + else: + self._threaded_node_request(session, process_fields, req_flags) + + else: + iq.reply() + iq['type'] = 'error' + iq['rejected']['seqnr'] = seqnr + iq['rejected']['error'] = error_msg + iq.send(block=False) + + def _threaded_node_request(self, session, process_fields, flags): + """ + Helper function to handle the device readouts in a separate thread. + + Arguments: + session -- The request session id + process_fields -- The fields to request from the devices + flags -- [optional] flags to pass to the devices, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + """ + for node in self.sessions[session]["node_list"]: + self.sessions[session]["nodeDone"][node] = False + + for node in self.sessions[session]["node_list"]: + timer = TimerReset(self.nodes[node]['commTimeout'], self._event_comm_timeout, args=(session, node)) + self.sessions[session]["commTimers"][node] = timer + #print("Starting timer " + str(timer) + ", timeout: " + str(self.nodes[node]['commTimeout'])) + timer.start() + self.nodes[node]['device'].request_fields(process_fields, flags=flags, session=session, callback=self._device_field_request_callback) + + def _event_comm_timeout(self, session, nodeId): + """ + Triggered if any of the readout operations timeout. + Sends a failure message back to the client, stops communicating + with the failing device. + + Arguments: + session -- The request session id + nodeId -- The id of the device which timed out + """ + msg = self.xmpp.Message() + msg['from'] = self.sessions[session]['to'] + msg['to'] = self.sessions[session]['from'] + msg['failure']['seqnr'] = self.sessions[session]['seqnr'] + msg['failure']['error']['text'] = "Timeout" + msg['failure']['error']['nodeId'] = nodeId + msg['failure']['error']['timestamp'] = datetime.datetime.now().replace(microsecond=0).isoformat() + + # Drop communication with this device and check if we are done + self.sessions[session]["nodeDone"][nodeId] = True + if (self._all_nodes_done(session)): + msg['failure']['done'] = 'true' + msg.send() + # The session is complete, delete it + #print("del session " + session + " due to timeout") + del self.sessions[session] + + def _event_delayed_req(self, session, process_fields, req_flags): + """ + Triggered when the timer from a delayed request fires. + + Arguments: + session -- The request session id + process_fields -- The fields to request from the devices + flags -- [optional] flags to pass to the devices, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + """ + msg = self.xmpp.Message() + msg['from'] = self.sessions[session]['to'] + msg['to'] = self.sessions[session]['from'] + msg['started']['seqnr'] = self.sessions[session]['seqnr'] + msg.send() + + if self.threaded: + tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields, req_flags)) + tr_req.start() + else: + self._threaded_node_request(session, process_fields, req_flags) + + def _all_nodes_done(self, session): + """ + Checks wheter all devices are done replying to the readout. + + Arguments: + session -- The request session id + """ + for n in self.sessions[session]["nodeDone"]: + if not self.sessions[session]["nodeDone"][n]: + return False + return True + + def _device_field_request_callback(self, session, nodeId, result, timestamp_block, error_msg=None): + """ + Callback function called by the devices when they have any additional data. + Composes a message with the data and sends it back to the client, and resets + the timeout timer for the device. + + Arguments: + session -- The request session id + nodeId -- The device id which initiated the callback + result -- The current result status of the readout. Valid values are: + "error" - Readout failed. + "fields" - Contains readout data. + "done" - Indicates that the readout is complete. May contain + readout data. + timestamp_block -- [optional] Only applies when result != "error" + The readout data. Structured as a dictionary: + { + timestamp: timestamp for this datablock, + fields: list of field dictionary (one per readout field). + readout field dictionary format: + { + type: The field type (numeric, boolean, dateTime, timeSpan, string, enum) + name: The field name + value: The field value + unit: The unit of the field. Only applies to type numeric. + dataType: The datatype of the field. Only applies to type enum. + flags: [optional] data classifier flags for the field, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + } + } + error_msg -- [optional] Only applies when result == "error". + Error details when a request failed. + """ + if not session in self.sessions: + # This can happend if a session was deleted, like in a cancellation. Just drop the data. + return + + if result == "error": + self.sessions[session]["commTimers"][nodeId].cancel() + + msg = self.xmpp.Message() + msg['from'] = self.sessions[session]['to'] + msg['to'] = self.sessions[session]['from'] + msg['failure']['seqnr'] = self.sessions[session]['seqnr'] + msg['failure']['error']['text'] = error_msg + msg['failure']['error']['nodeId'] = nodeId + msg['failure']['error']['timestamp'] = datetime.datetime.now().replace(microsecond=0).isoformat() + + # Drop communication with this device and check if we are done + self.sessions[session]["nodeDone"][nodeId] = True + if (self._all_nodes_done(session)): + msg['failure']['done'] = 'true' + # The session is complete, delete it + # print("del session " + session + " due to error") + del self.sessions[session] + msg.send() + else: + msg = self.xmpp.Message() + msg['from'] = self.sessions[session]['to'] + msg['to'] = self.sessions[session]['from'] + msg['fields']['seqnr'] = self.sessions[session]['seqnr'] + + if timestamp_block is not None and len(timestamp_block) > 0: + node = msg['fields'].add_node(nodeId) + ts = node.add_timestamp(timestamp_block["timestamp"]) + + for f in timestamp_block["fields"]: + data = ts.add_data( typename=f['type'], + name=f['name'], + value=f['value'], + unit=f['unit'], + dataType=f['dataType'], + flags=f['flags']) + + if result == "done": + self.sessions[session]["commTimers"][nodeId].cancel() + self.sessions[session]["nodeDone"][nodeId] = True + msg['fields']['done'] = 'true' + if (self._all_nodes_done(session)): + # The session is complete, delete it + # print("del session " + session + " due to complete") + del self.sessions[session] + else: + # Restart comm timer + self.sessions[session]["commTimers"][nodeId].reset() + + msg.send() + + def _handle_event_cancel(self, iq): + """ Received Iq with cancel - this is a cancel request. + Delete the session and confirm. """ + + seqnr = iq['cancel']['seqnr'] + # Find the session + for s in self.sessions: + if self.sessions[s]['from'] == iq['from'] and self.sessions[s]['to'] == iq['to'] and self.sessions[s]['seqnr'] == seqnr: + # found it. Cancel all timers + for n in self.sessions[s]["commTimers"]: + self.sessions[s]["commTimers"][n].cancel() + + # Confirm + iq.reply() + iq['type'] = 'result' + iq['cancelled']['seqnr'] = seqnr + iq.send(block=False) + + # Delete session + del self.sessions[s] + return + + # Could not find session, send reject + iq.reply() + iq['type'] = 'error' + iq['rejected']['seqnr'] = seqnr + iq['rejected']['error'] = "Cancel request received, no matching request is active." + iq.send(block=False) + + # ================================================================= + # Client side (data retriever) API + + def request_data(self, from_jid, to_jid, callback, nodeIds=None, fields=None, flags=None): + """ + Called on the client side to initiade a data readout. + Composes a message with the request and sends it to the device(s). + Does not block, the callback will be called when data is available. + + Arguments: + from_jid -- The jid of the requester + to_jid -- The jid of the device(s) + callback -- The callback function to call when data is availble. + + The callback function must support the following arguments: + + from_jid -- The jid of the responding device(s) + result -- The current result status of the readout. Valid values are: + "accepted" - Readout request accepted + "queued" - Readout request accepted and queued + "rejected" - Readout request rejected + "failure" - Readout failed. + "cancelled" - Confirmation of request cancellation. + "started" - Previously queued request is now started + "fields" - Contains readout data. + "done" - Indicates that the readout is complete. + + nodeId -- [optional] Mandatory when result == "fields" or "failure". + The node Id of the responding device. One callback will only + contain data from one device. + timestamp -- [optional] Mandatory when result == "fields". + The timestamp of data in this callback. One callback will only + contain data from one timestamp. + fields -- [optional] Mandatory when result == "fields". + List of field dictionaries representing the readout data. + Dictionary format: + { + typename: The field type (numeric, boolean, dateTime, timeSpan, string, enum) + name: The field name + value: The field value + unit: The unit of the field. Only applies to type numeric. + dataType: The datatype of the field. Only applies to type enum. + flags: [optional] data classifier flags for the field, e.g. momentary. + Formatted as a dictionary like { "flag name": "flag value" ... } + } + + error_msg -- [optional] Mandatory when result == "rejected" or "failure". + Details about why the request is rejected or failed. + "rejected" means that the request is stopped, but note that the + request will continue even after a "failure". "failure" only means + that communication was stopped to that specific device, other + device(s) (if any) will continue their readout. + + nodeIds -- [optional] Limits the request to the node Ids in this list. + fields -- [optional] Limits the request to the field names in this list. + flags -- [optional] Limits the request according to the flags, or sets + readout conditions such as timing. + + Return value: + session -- Session identifier. Client can use this as a reference to cancel + the request. + """ + iq = self.xmpp.Iq() + iq['from'] = from_jid + iq['to'] = to_jid + iq['type'] = "get" + seqnr = self._get_new_seqnr() + iq['id'] = seqnr + iq['req']['seqnr'] = seqnr + if nodeIds is not None: + for nodeId in nodeIds: + iq['req'].add_node(nodeId) + if fields is not None: + for field in fields: + iq['req'].add_field(field) + + iq['req']._set_flags(flags) + + self.sessions[seqnr] = {"from": iq['from'], "to": iq['to'], "seqnr": seqnr, "callback": callback} + iq.send(block=False) + + return seqnr + + def cancel_request(self, session): + """ + Called on the client side to cancel a request for data readout. + Composes a message with the cancellation and sends it to the device(s). + Does not block, the callback will be called when cancellation is + confirmed. + + Arguments: + session -- The session id of the request to cancel + """ + seqnr = session + iq = self.xmpp.Iq() + iq['from'] = self.sessions[seqnr]['from'] + iq['to'] = self.sessions[seqnr]['to'] + iq['type'] = "get" + iq['id'] = seqnr + iq['cancel']['seqnr'] = seqnr + iq.send(block=False) + + def _get_new_seqnr(self): + """ Returns a unique sequence number (unique across threads) """ + self.seqnr_lock.acquire() + self.last_seqnr += 1 + self.seqnr_lock.release() + return str(self.last_seqnr) + + def _handle_event_accepted(self, iq): + """ Received Iq with accepted - request was accepted """ + seqnr = iq['accepted']['seqnr'] + result = "accepted" + if iq['accepted']['queued'] == 'true': + result = "queued" + + callback = self.sessions[seqnr]["callback"] + callback(from_jid=iq['from'], result=result) + + def _handle_event_rejected(self, iq): + """ Received Iq with rejected - this is a reject. + Delete the session. """ + seqnr = iq['rejected']['seqnr'] + callback = self.sessions[seqnr]["callback"] + callback(from_jid=iq['from'], result="rejected", error_msg=iq['rejected']['error']) + # Session terminated + del self.sessions[seqnr] + + def _handle_event_cancelled(self, iq): + """ + Received Iq with cancelled - this is a cancel confirm. + Delete the session. + """ + #print("Got cancelled") + seqnr = iq['cancelled']['seqnr'] + callback = self.sessions[seqnr]["callback"] + callback(from_jid=iq['from'], result="cancelled") + # Session cancelled + del self.sessions[seqnr] + + def _handle_event_fields(self, msg): + """ + Received Msg with fields - this is a data reponse to a request. + If this is the last data block, issue a "done" callback. + """ + seqnr = msg['fields']['seqnr'] + callback = self.sessions[seqnr]["callback"] + for node in msg['fields']['nodes']: + for ts in node['timestamps']: + fields = [] + for d in ts['datas']: + field_block = {} + field_block["name"] = d['name'] + field_block["typename"] = d._get_typename() + field_block["value"] = d['value'] + if not d['unit'] == "": field_block["unit"] = d['unit']; + if not d['dataType'] == "": field_block["dataType"] = d['dataType']; + flags = d._get_flags() + if not len(flags) == 0: + field_block["flags"] = flags + fields.append(field_block) + + callback(from_jid=msg['from'], result="fields", nodeId=node['nodeId'], timestamp=ts['value'], fields=fields) + + if msg['fields']['done'] == "true": + callback(from_jid=msg['from'], result="done") + # Session done + del self.sessions[seqnr] + + def _handle_event_failure(self, msg): + """ + Received Msg with failure - our request failed + Delete the session. + """ + seqnr = msg['failure']['seqnr'] + callback = self.sessions[seqnr]["callback"] + callback(from_jid=msg['from'], result="failure", nodeId=msg['failure']['error']['nodeId'], timestamp=msg['failure']['error']['timestamp'], error_msg=msg['failure']['error']['text']) + + # Session failed + del self.sessions[seqnr] + + def _handle_event_started(self, msg): + """ + Received Msg with started - our request was queued and is now started. + """ + seqnr = msg['started']['seqnr'] + callback = self.sessions[seqnr]["callback"] + callback(from_jid=msg['from'], result="started") + + diff --git a/sleekxmpp/plugins/xep_0323/stanza/__init__.py b/sleekxmpp/plugins/xep_0323/stanza/__init__.py new file mode 100644 index 00000000..c039cefa --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/stanza/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0323.stanza.sensordata import * + diff --git a/sleekxmpp/plugins/xep_0323/stanza/base.py b/sleekxmpp/plugins/xep_0323/stanza/base.py new file mode 100644 index 00000000..1dadcf46 --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/stanza/base.py @@ -0,0 +1,13 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ET + +pass diff --git a/sleekxmpp/plugins/xep_0323/stanza/sensordata.py b/sleekxmpp/plugins/xep_0323/stanza/sensordata.py new file mode 100644 index 00000000..e8718161 --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/stanza/sensordata.py @@ -0,0 +1,792 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp import Iq, Message +from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID +from re import match + +class Sensordata(ElementBase): + """ Placeholder for the namespace, not used as a stanza """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'sensordata' + plugin_attrib = name + interfaces = set(tuple()) + +class FieldTypes(): + """ + All field types are optional booleans that default to False + """ + field_types = set([ 'momentary','peak','status','computed','identity','historicalSecond','historicalMinute','historicalHour', \ + 'historicalDay','historicalWeek','historicalMonth','historicalQuarter','historicalYear','historicalOther']) + +class FieldStatus(): + """ + All field statuses are optional booleans that default to False + """ + field_status = set([ 'missing','automaticEstimate','manualEstimate','manualReadout','automaticReadout','timeOffset','warning','error', \ + 'signed','invoiced','endOfSeries','powerFailure','invoiceConfirmed']) + +class Request(ElementBase): + namespace = 'urn:xmpp:iot:sensordata' + name = 'req' + plugin_attrib = name + interfaces = set(['seqnr','nodes','fields','serviceToken','deviceToken','userToken','from','to','when','historical','all']) + interfaces.update(FieldTypes.field_types) + _flags = set(['serviceToken','deviceToken','userToken','from','to','when','historical','all']) + _flags.update(FieldTypes.field_types) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._nodes = set() + self._fields = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._nodes = set([node['nodeId'] for node in self['nodes']]) + self._fields = set([field['name'] for field in self['fields']]) + + def _get_flags(self): + """ + Helper function for getting of flags. Returns all flags in + dictionary format: { "flag name": "flag value" ... } + """ + flags = {} + for f in self._flags: + if not self[f] == "": + flags[f] = self[f] + return flags + + def _set_flags(self, flags): + """ + Helper function for setting of flags. + + Arguments: + flags -- Flags in dictionary format: { "flag name": "flag value" ... } + """ + for f in self._flags: + if flags is not None and f in flags: + self[f] = flags[f] + else: + self[f] = None + + def add_node(self, nodeId, sourceId=None, cacheType=None): + """ + Add a new node element. Each item is required to have a + nodeId, but may also specify a sourceId value and cacheType. + + Arguments: + nodeId -- The ID for the node. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + if nodeId not in self._nodes: + self._nodes.add((nodeId)) + node = RequestNode(parent=self) + node['nodeId'] = nodeId + node['sourceId'] = sourceId + node['cacheType'] = cacheType + self.iterables.append(node) + return node + return None + + def del_node(self, nodeId): + """ + Remove a single node. + + Arguments: + nodeId -- Node ID of the item to remove. + """ + if nodeId in self._nodes: + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + if node['nodeId'] == nodeId: + self.xml.remove(node.xml) + self.iterables.remove(node) + return True + return False + + def get_nodes(self): + """Return all nodes.""" + nodes = [] + for node in self['substanzas']: + if isinstance(node, RequestNode): + nodes.append(node) + return nodes + + def set_nodes(self, nodes): + """ + Set or replace all nodes. The given nodes must be in a + list or set where each item is a tuple of the form: + (nodeId, sourceId, cacheType) + + Arguments: + nodes -- A series of nodes in tuple format. + """ + self.del_nodes() + for node in nodes: + if isinstance(node, RequestNode): + self.add_node(node['nodeId'], node['sourceId'], node['cacheType']) + else: + nodeId, sourceId, cacheType = node + self.add_node(nodeId, sourceId, cacheType) + + def del_nodes(self): + """Remove all nodes.""" + self._nodes = set() + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + self.xml.remove(node.xml) + self.iterables.remove(node) + + + def add_field(self, name): + """ + Add a new field element. Each item is required to have a + name. + + Arguments: + name -- The name of the field. + """ + if name not in self._fields: + self._fields.add((name)) + field = RequestField(parent=self) + field['name'] = name + self.iterables.append(field) + return field + return None + + def del_field(self, name): + """ + Remove a single field. + + Arguments: + name -- name of field to remove. + """ + if name in self._fields: + fields = [i for i in self.iterables if isinstance(i, RequestField)] + for field in fields: + if field['name'] == name: + self.xml.remove(field.xml) + self.iterables.remove(field) + return True + return False + + def get_fields(self): + """Return all fields.""" + fields = [] + for field in self['substanzas']: + if isinstance(field, RequestField): + fields.append(field) + return fields + + def set_fields(self, fields): + """ + Set or replace all fields. The given fields must be in a + list or set where each item is RequestField or string + + Arguments: + fields -- A series of fields in RequestField or string format. + """ + self.del_fields() + for field in fields: + if isinstance(field, RequestField): + self.add_field(field['name']) + else: + self.add_field(field) + + def del_fields(self): + """Remove all fields.""" + self._fields = set() + fields = [i for i in self.iterables if isinstance(i, RequestField)] + for field in fields: + self.xml.remove(field.xml) + self.iterables.remove(field) + + +class RequestNode(ElementBase): + """ Node element in a request """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'node' + plugin_attrib = name + interfaces = set(['nodeId','sourceId','cacheType']) + +class RequestField(ElementBase): + """ Field element in a request """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'field' + plugin_attrib = name + interfaces = set(['name']) + +class Accepted(ElementBase): + namespace = 'urn:xmpp:iot:sensordata' + name = 'accepted' + plugin_attrib = name + interfaces = set(['seqnr','queued']) + +class Started(ElementBase): + namespace = 'urn:xmpp:iot:sensordata' + name = 'started' + plugin_attrib = name + interfaces = set(['seqnr']) + +class Failure(ElementBase): + namespace = 'urn:xmpp:iot:sensordata' + name = 'failure' + plugin_attrib = name + interfaces = set(['seqnr','done']) + +class Error(ElementBase): + """ Error element in a request failure """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'error' + plugin_attrib = name + interfaces = set(['nodeId','timestamp','sourceId','cacheType','text']) + + def get_text(self): + """Return then contents inside the XML tag.""" + return self.xml.text + + def set_text(self, value): + """Set then contents inside the XML tag. + + :param value: string + """ + + self.xml.text = value + return self + + def del_text(self): + """Remove the contents inside the XML tag.""" + self.xml.text = "" + return self + +class Rejected(ElementBase): + namespace = 'urn:xmpp:iot:sensordata' + name = 'rejected' + plugin_attrib = name + interfaces = set(['seqnr','error']) + sub_interfaces = set(['error']) + +class Fields(ElementBase): + """ Fields element, top level in a response message with data """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'fields' + plugin_attrib = name + interfaces = set(['seqnr','done','nodes']) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._nodes = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._nodes = set([node['nodeId'] for node in self['nodes']]) + + + def add_node(self, nodeId, sourceId=None, cacheType=None, substanzas=None): + """ + Add a new node element. Each item is required to have a + nodeId, but may also specify a sourceId value and cacheType. + + Arguments: + nodeId -- The ID for the node. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + if nodeId not in self._nodes: + self._nodes.add((nodeId)) + node = FieldsNode(parent=self) + node['nodeId'] = nodeId + node['sourceId'] = sourceId + node['cacheType'] = cacheType + if substanzas is not None: + node.set_timestamps(substanzas) + + self.iterables.append(node) + return node + return None + + def del_node(self, nodeId): + """ + Remove a single node. + + Arguments: + nodeId -- Node ID of the item to remove. + """ + if nodeId in self._nodes: + nodes = [i for i in self.iterables if isinstance(i, FieldsNode)] + for node in nodes: + if node['nodeId'] == nodeId: + self.xml.remove(node.xml) + self.iterables.remove(node) + return True + return False + + def get_nodes(self): + """Return all nodes.""" + nodes = [] + for node in self['substanzas']: + if isinstance(node, FieldsNode): + nodes.append(node) + return nodes + + def set_nodes(self, nodes): + """ + Set or replace all nodes. The given nodes must be in a + list or set where each item is a tuple of the form: + (nodeId, sourceId, cacheType) + + Arguments: + nodes -- A series of nodes in tuple format. + """ + #print(str(id(self)) + " set_nodes: got " + str(nodes)) + self.del_nodes() + for node in nodes: + if isinstance(node, FieldsNode): + self.add_node(node['nodeId'], node['sourceId'], node['cacheType'], substanzas=node['substanzas']) + else: + nodeId, sourceId, cacheType = node + self.add_node(nodeId, sourceId, cacheType) + + def del_nodes(self): + """Remove all nodes.""" + self._nodes = set() + nodes = [i for i in self.iterables if isinstance(i, FieldsNode)] + for node in nodes: + self.xml.remove(node.xml) + self.iterables.remove(node) + + +class FieldsNode(ElementBase): + """ Node element in response fields """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'node' + plugin_attrib = name + interfaces = set(['nodeId','sourceId','cacheType','timestamps']) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._timestamps = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._timestamps = set([ts['value'] for ts in self['timestamps']]) + + def add_timestamp(self, timestamp, substanzas=None): + """ + Add a new timestamp element. + + Arguments: + timestamp -- The timestamp in ISO format. + """ + #print(str(id(self)) + " add_timestamp: " + str(timestamp)) + + if timestamp not in self._timestamps: + self._timestamps.add((timestamp)) + ts = Timestamp(parent=self) + ts['value'] = timestamp + if not substanzas is None: + ts.set_datas(substanzas) + #print("add_timestamp with substanzas: " + str(substanzas)) + self.iterables.append(ts) + #print(str(id(self)) + " added_timestamp: " + str(id(ts))) + return ts + return None + + def del_timestamp(self, timestamp): + """ + Remove a single timestamp. + + Arguments: + timestamp -- timestamp (in ISO format) of the item to remove. + """ + #print("del_timestamp: ") + if timestamp in self._timestamps: + timestamps = [i for i in self.iterables if isinstance(i, Timestamp)] + for ts in timestamps: + if ts['value'] == timestamp: + self.xml.remove(ts.xml) + self.iterables.remove(ts) + return True + return False + + def get_timestamps(self): + """Return all timestamps.""" + #print(str(id(self)) + " get_timestamps: ") + timestamps = [] + for timestamp in self['substanzas']: + if isinstance(timestamp, Timestamp): + timestamps.append(timestamp) + return timestamps + + def set_timestamps(self, timestamps): + """ + Set or replace all timestamps. The given timestamps must be in a + list or set where each item is a timestamp + + Arguments: + timestamps -- A series of timestamps. + """ + #print(str(id(self)) + " set_timestamps: got " + str(timestamps)) + self.del_timestamps() + for timestamp in timestamps: + #print("set_timestamps: subset " + str(timestamp)) + #print("set_timestamps: subset.substanzas " + str(timestamp['substanzas'])) + if isinstance(timestamp, Timestamp): + self.add_timestamp(timestamp['value'], substanzas=timestamp['substanzas']) + else: + #print("set_timestamps: got " + str(timestamp)) + self.add_timestamp(timestamp) + + def del_timestamps(self): + """Remove all timestamps.""" + #print(str(id(self)) + " del_timestamps: ") + self._timestamps = set() + timestamps = [i for i in self.iterables if isinstance(i, Timestamp)] + for timestamp in timestamps: + self.xml.remove(timestamp.xml) + self.iterables.remove(timestamp) + +class Field(ElementBase): + """ + Field element in response Timestamp. This is a base class, + all instances of fields added to Timestamp must be of types: + DataNumeric + DataString + DataBoolean + DataDateTime + DataTimeSpan + DataEnum + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'field' + plugin_attrib = name + interfaces = set(['name','module','stringIds']) + interfaces.update(FieldTypes.field_types) + interfaces.update(FieldStatus.field_status) + + _flags = set() + _flags.update(FieldTypes.field_types) + _flags.update(FieldStatus.field_status) + + def set_stringIds(self, value): + """Verifies stringIds according to regexp from specification XMPP-0323. + + :param value: string + """ + + pattern = re.compile("^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$") + if pattern.match(value) is not None: + self.xml.stringIds = value + else: + # Bad content, add nothing + pass + + return self + + def _get_flags(self): + """ + Helper function for getting of flags. Returns all flags in + dictionary format: { "flag name": "flag value" ... } + """ + flags = {} + for f in self._flags: + if not self[f] == "": + flags[f] = self[f] + return flags + + def _set_flags(self, flags): + """ + Helper function for setting of flags. + + Arguments: + flags -- Flags in dictionary format: { "flag name": "flag value" ... } + """ + for f in self._flags: + if flags is not None and f in flags: + self[f] = flags[f] + else: + self[f] = None + + def _get_typename(self): + return "invalid type, use subclasses!" + + +class Timestamp(ElementBase): + """ Timestamp element in response Node """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'timestamp' + plugin_attrib = name + interfaces = set(['value','datas']) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._datas = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._datas = set([data['name'] for data in self['datas']]) + + def add_data(self, typename, name, value, module=None, stringIds=None, unit=None, dataType=None, flags=None): + """ + Add a new data element. + + Arguments: + typename -- The type of data element (numeric, string, boolean, dateTime, timeSpan or enum) + value -- The value of the data element + module -- [optional] language module to use for the data element + stringIds -- [optional] The stringIds used to find associated text in the language module + unit -- [optional] The unit. Only applicable for type numeric + dataType -- [optional] The dataType. Only applicable for type enum + """ + if name not in self._datas: + dataObj = None + if typename == "numeric": + dataObj = DataNumeric(parent=self) + dataObj['unit'] = unit + elif typename == "string": + dataObj = DataString(parent=self) + elif typename == "boolean": + dataObj = DataBoolean(parent=self) + elif typename == "dateTime": + dataObj = DataDateTime(parent=self) + elif typename == "timeSpan": + dataObj = DataTimeSpan(parent=self) + elif typename == "enum": + dataObj = DataEnum(parent=self) + dataObj['dataType'] = dataType + + dataObj['name'] = name + dataObj['value'] = value + dataObj['module'] = module + dataObj['stringIds'] = stringIds + + if flags is not None: + dataObj._set_flags(flags) + + self._datas.add(name) + self.iterables.append(dataObj) + return dataObj + return None + + def del_data(self, name): + """ + Remove a single data element. + + Arguments: + data_name -- The data element name to remove. + """ + if name in self._datas: + datas = [i for i in self.iterables if isinstance(i, Field)] + for data in datas: + if data['name'] == name: + self.xml.remove(data.xml) + self.iterables.remove(data) + return True + return False + + def get_datas(self): + """ Return all data elements. """ + datas = [] + for data in self['substanzas']: + if isinstance(data, Field): + datas.append(data) + return datas + + def set_datas(self, datas): + """ + Set or replace all data elements. The given elements must be in a + list or set where each item is a data element (numeric, string, boolean, dateTime, timeSpan or enum) + + Arguments: + datas -- A series of data elements. + """ + self.del_datas() + for data in datas: + self.add_data(typename=data._get_typename(), name=data['name'], value=data['value'], module=data['module'], stringIds=data['stringIds'], unit=data['unit'], dataType=data['dataType'], flags=data._get_flags()) + + def del_datas(self): + """Remove all data elements.""" + self._datas = set() + datas = [i for i in self.iterables if isinstance(i, Field)] + for data in datas: + self.xml.remove(data.xml) + self.iterables.remove(data) + +class DataNumeric(Field): + """ + Field data of type numeric. + Note that the value is expressed as a string. + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'numeric' + plugin_attrib = name + interfaces = set(['value', 'unit']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "numeric" + +class DataString(Field): + """ + Field data of type string + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'string' + plugin_attrib = name + interfaces = set(['value']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "string" + +class DataBoolean(Field): + """ + Field data of type boolean. + Note that the value is expressed as a string. + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'boolean' + plugin_attrib = name + interfaces = set(['value']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "boolean" + +class DataDateTime(Field): + """ + Field data of type dateTime. + Note that the value is expressed as a string. + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'dateTime' + plugin_attrib = name + interfaces = set(['value']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "dateTime" + +class DataTimeSpan(Field): + """ + Field data of type timeSpan. + Note that the value is expressed as a string. + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'timeSpan' + plugin_attrib = name + interfaces = set(['value']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "timeSpan" + +class DataEnum(Field): + """ + Field data of type enum. + Note that the value is expressed as a string. + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'enum' + plugin_attrib = name + interfaces = set(['value', 'dataType']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "enum" + +class Done(ElementBase): + """ Done element used to signal that all data has been transferred """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'done' + plugin_attrib = name + interfaces = set(['seqnr']) + +class Cancel(ElementBase): + """ Cancel element used to signal that a request shall be cancelled """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'cancel' + plugin_attrib = name + interfaces = set(['seqnr']) + +class Cancelled(ElementBase): + """ Cancelled element used to signal that cancellation is confirmed """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'cancelled' + plugin_attrib = name + interfaces = set(['seqnr']) + + +register_stanza_plugin(Iq, Request) +register_stanza_plugin(Request, RequestNode, iterable=True) +register_stanza_plugin(Request, RequestField, iterable=True) + +register_stanza_plugin(Iq, Accepted) +register_stanza_plugin(Message, Failure) +register_stanza_plugin(Failure, Error) + +register_stanza_plugin(Iq, Rejected) + +register_stanza_plugin(Message, Fields) +register_stanza_plugin(Fields, FieldsNode, iterable=True) +register_stanza_plugin(FieldsNode, Timestamp, iterable=True) +register_stanza_plugin(Timestamp, Field, iterable=True) +register_stanza_plugin(Timestamp, DataNumeric, iterable=True) +register_stanza_plugin(Timestamp, DataString, iterable=True) +register_stanza_plugin(Timestamp, DataBoolean, iterable=True) +register_stanza_plugin(Timestamp, DataDateTime, iterable=True) +register_stanza_plugin(Timestamp, DataTimeSpan, iterable=True) +register_stanza_plugin(Timestamp, DataEnum, iterable=True) + +register_stanza_plugin(Message, Started) + +register_stanza_plugin(Iq, Cancel) +register_stanza_plugin(Iq, Cancelled) diff --git a/sleekxmpp/plugins/xep_0323/timerreset.py b/sleekxmpp/plugins/xep_0323/timerreset.py new file mode 100644 index 00000000..398b47c1 --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/timerreset.py @@ -0,0 +1,69 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" +from threading import Thread, Event, Timer +import time + +def TimerReset(*args, **kwargs): + """ Global function for Timer """ + return _TimerReset(*args, **kwargs) + + +class _TimerReset(Thread): + """Call a function after a specified number of seconds: + + t = TimerReset(30.0, f, args=[], kwargs={}) + t.start() + t.cancel() # stop the timer's action if it's still waiting + """ + + def __init__(self, interval, function, args=None, kwargs=None): + if not kwargs: + kwargs = {} + if not args: + args = [] + + Thread.__init__(self) + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + self.finished = Event() + self.resetted = True + + def cancel(self): + """Stop the timer if it hasn't finished yet""" + self.finished.set() + + def run(self): + #print "Time: %s - timer running..." % time.asctime() + + while self.resetted: + #print "Time: %s - timer waiting for timeout in %.2f..." % (time.asctime(), self.interval) + self.resetted = False + self.finished.wait(self.interval) + + if not self.finished.isSet(): + self.function(*self.args, **self.kwargs) + self.finished.set() + #print "Time: %s - timer finished!" % time.asctime() + + def reset(self, interval=None): + """ Reset the timer """ + + if interval: + #print "Time: %s - timer resetting to %.2f..." % (time.asctime(), interval) + self.interval = interval + else: + #print "Time: %s - timer resetting..." % time.asctime() + pass + + self.resetted = True + self.finished.set() + self.finished.clear() diff --git a/sleekxmpp/plugins/xep_0325/__init__.py b/sleekxmpp/plugins/xep_0325/__init__.py new file mode 100644 index 00000000..01c38dce --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/__init__.py @@ -0,0 +1,18 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0325.control import XEP_0325 +from sleekxmpp.plugins.xep_0325 import stanza + +register_plugin(XEP_0325) + +xep_0325=XEP_0325 diff --git a/sleekxmpp/plugins/xep_0325/control.py b/sleekxmpp/plugins/xep_0325/control.py new file mode 100644 index 00000000..11e7a045 --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/control.py @@ -0,0 +1,569 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import time +from threading import Thread, Timer, Lock + +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0325 import stanza +from sleekxmpp.plugins.xep_0325.stanza import Control + + +log = logging.getLogger(__name__) + + +class XEP_0325(BasePlugin): + + """ + XEP-0325: IoT Control + + + Actuators are devices in sensor networks that can be controlled through + the network and act with the outside world. In sensor networks and + Internet of Things applications, actuators make it possible to automate + real-world processes. + This plugin implements a mechanism whereby actuators can be controlled + in XMPP-based sensor networks, making it possible to integrate sensors + and actuators of different brands, makes and models into larger + Internet of Things applications. + + Also see <http://xmpp.org/extensions/xep-0325.html> + + Configuration Values: + threaded -- Indicates if communication with sensors should be threaded. + Defaults to True. + + Events: + Sensor side + ----------- + Control Event:DirectSet -- Received a control message + Control Event:SetReq -- Received a control request + + Client side + ----------- + Control Event:SetResponse -- Received a response to a + control request, type result + Control Event:SetResponseError -- Received a response to a + control request, type error + + Attributes: + threaded -- Indicates if command events should be threaded. + Defaults to True. + sessions -- A dictionary or equivalent backend mapping + session IDs to dictionaries containing data + relevant to a request's session. This dictionary is used + both by the client and sensor side. On client side, seqnr + is used as key, while on sensor side, a session_id is used + as key. This ensures that the two will not collide, so + one instance can be both client and sensor. + Sensor side + ----------- + nodes -- A dictionary mapping sensor nodes that are serviced through + this XMPP instance to their device handlers ("drivers"). + Client side + ----------- + last_seqnr -- The last used sequence number (integer). One sequence of + communication (e.g. -->request, <--accept, <--fields) + between client and sensor is identified by a unique + sequence number (unique between the client/sensor pair) + + Methods: + plugin_init -- Overrides base_plugin.plugin_init + post_init -- Overrides base_plugin.post_init + plugin_end -- Overrides base_plugin.plugin_end + + Sensor side + ----------- + register_node -- Register a sensor as available from this XMPP + instance. + + Client side + ----------- + set_request -- Initiates a control request to modify data in + sensor(s). Non-blocking, a callback function will + be called when the sensor has responded. + set_command -- Initiates a control command to modify data in + sensor(s). Non-blocking. The sensor(s) will not + respond regardless of the result of the command, + so no callback is made. + + """ + + name = 'xep_0325' + description = 'XEP-0325 Internet of Things - Control' + dependencies = set(['xep_0030']) + stanza = stanza + + + default_config = { + 'threaded': True +# 'session_db': None + } + + def plugin_init(self): + """ Start the XEP-0325 plugin """ + + self.xmpp.register_handler( + Callback('Control Event:DirectSet', + StanzaPath('message/set'), + self._handle_direct_set)) + + self.xmpp.register_handler( + Callback('Control Event:SetReq', + StanzaPath('iq@type=set/set'), + self._handle_set_req)) + + self.xmpp.register_handler( + Callback('Control Event:SetResponse', + StanzaPath('iq@type=result/setResponse'), + self._handle_set_response)) + + self.xmpp.register_handler( + Callback('Control Event:SetResponseError', + StanzaPath('iq@type=error/setResponse'), + self._handle_set_response)) + + # Server side dicts + self.nodes = {} + self.sessions = {} + + self.last_seqnr = 0 + self.seqnr_lock = Lock() + + ## For testning only + self.test_authenticated_from = "" + + def post_init(self): + """ Init complete. Register our features in Serivce discovery. """ + BasePlugin.post_init(self) + self.xmpp['xep_0030'].add_feature(Control.namespace) + self.xmpp['xep_0030'].set_items(node=Control.namespace, items=tuple()) + + def _new_session(self): + """ Return a new session ID. """ + return str(time.time()) + '-' + self.xmpp.new_id() + + def plugin_end(self): + """ Stop the XEP-0325 plugin """ + self.sessions.clear() + self.xmpp.remove_handler('Control Event:DirectSet') + self.xmpp.remove_handler('Control Event:SetReq') + self.xmpp.remove_handler('Control Event:SetResponse') + self.xmpp.remove_handler('Control Event:SetResponseError') + self.xmpp['xep_0030'].del_feature(feature=Control.namespace) + self.xmpp['xep_0030'].set_items(node=Control.namespace, items=tuple()) + + + # ================================================================= + # Sensor side (data provider) API + + def register_node(self, nodeId, device, commTimeout, sourceId=None, cacheType=None): + """ + Register a sensor/device as available for control requests/commands + through this XMPP instance. + + The device object may by any custom implementation to support + specific devices, but it must implement the functions: + has_control_field + set_control_fields + according to the interfaces shown in the example device.py file. + + Arguments: + nodeId -- The identifier for the device + device -- The device object + commTimeout -- Time in seconds to wait between each callback from device during + a data readout. Float. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + self.nodes[nodeId] = {"device": device, + "commTimeout": commTimeout, + "sourceId": sourceId, + "cacheType": cacheType} + + def _set_authenticated(self, auth=''): + """ Internal testing function """ + self.test_authenticated_from = auth + + def _get_new_seqnr(self): + """ Returns a unique sequence number (unique across threads) """ + self.seqnr_lock.acquire() + self.last_seqnr += 1 + self.seqnr_lock.release() + return str(self.last_seqnr) + + def _handle_set_req(self, iq): + """ + Event handler for reception of an Iq with set req - this is a + control request. + + Verifies that + - all the requested nodes are available + (if no nodes are specified in the request, assume all nodes) + - all the control fields are available from all requested nodes + (if no nodes are specified in the request, assume all nodes) + + If the request passes verification, the control request is passed + to the devices (in a separate thread). + If the verification fails, a setResponse with error indication + is sent. + """ + + error_msg = '' + req_ok = True + missing_node = None + missing_field = None + + # Authentication + if len(self.test_authenticated_from) > 0 and not iq['from'] == self.test_authenticated_from: + # Invalid authentication + req_ok = False + error_msg = "Access denied" + + # Nodes + if len(iq['set']['nodes']) > 0: + for n in iq['set']['nodes']: + if not n['nodeId'] in self.nodes: + req_ok = False + missing_node = n['nodeId'] + error_msg = "Invalid nodeId " + n['nodeId'] + process_nodes = [n['nodeId'] for n in iq['set']['nodes']] + else: + process_nodes = self.nodes.keys() + + # Fields - for control we need to find all in all devices, otherwise we reject + process_fields = [] + if len(iq['set']['datas']) > 0: + for f in iq['set']['datas']: + for node in self.nodes: + if not self.nodes[node]["device"].has_control_field(f['name'], f._get_typename()): + req_ok = False + missing_field = f['name'] + error_msg = "Invalid field " + f['name'] + break + process_fields = [(f['name'], f._get_typename(), f['value']) for f in iq['set']['datas']] + + if req_ok: + session = self._new_session() + self.sessions[session] = {"from": iq['from'], "to": iq['to'], "seqnr": iq['id']} + self.sessions[session]["commTimers"] = {} + self.sessions[session]["nodeDone"] = {} + # Flag that a reply is exected when we are done + self.sessions[session]["reply"] = True + + self.sessions[session]["node_list"] = process_nodes + if self.threaded: + #print("starting thread") + tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields)) + tr_req.start() + #print("started thread") + else: + self._threaded_node_request(session, process_fields) + + else: + iq.reply() + iq['type'] = 'error' + iq['setResponse']['responseCode'] = "NotFound" + if missing_node is not None: + iq['setResponse'].add_node(missing_node) + if missing_field is not None: + iq['setResponse'].add_data(missing_field) + iq['setResponse']['error']['var'] = "Output" + iq['setResponse']['error']['text'] = error_msg + iq.send(block=False) + + def _handle_direct_set(self, msg): + """ + Event handler for reception of a Message with set command - this is a + direct control command. + + Verifies that + - all the requested nodes are available + (if no nodes are specified in the request, assume all nodes) + - all the control fields are available from all requested nodes + (if no nodes are specified in the request, assume all nodes) + + If the request passes verification, the control request is passed + to the devices (in a separate thread). + If the verification fails, do nothing. + """ + req_ok = True + + # Nodes + if len(msg['set']['nodes']) > 0: + for n in msg['set']['nodes']: + if not n['nodeId'] in self.nodes: + req_ok = False + error_msg = "Invalid nodeId " + n['nodeId'] + process_nodes = [n['nodeId'] for n in msg['set']['nodes']] + else: + process_nodes = self.nodes.keys() + + # Fields - for control we need to find all in all devices, otherwise we reject + process_fields = [] + if len(msg['set']['datas']) > 0: + for f in msg['set']['datas']: + for node in self.nodes: + if not self.nodes[node]["device"].has_control_field(f['name'], f._get_typename()): + req_ok = False + missing_field = f['name'] + error_msg = "Invalid field " + f['name'] + break + process_fields = [(f['name'], f._get_typename(), f['value']) for f in msg['set']['datas']] + + if req_ok: + session = self._new_session() + self.sessions[session] = {"from": msg['from'], "to": msg['to']} + self.sessions[session]["commTimers"] = {} + self.sessions[session]["nodeDone"] = {} + self.sessions[session]["reply"] = False + + self.sessions[session]["node_list"] = process_nodes + if self.threaded: + #print("starting thread") + tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields)) + tr_req.start() + #print("started thread") + else: + self._threaded_node_request(session, process_fields) + + + def _threaded_node_request(self, session, process_fields): + """ + Helper function to handle the device control in a separate thread. + + Arguments: + session -- The request session id + process_fields -- The fields to set in the devices. List of tuple format: + (name, datatype, value) + """ + for node in self.sessions[session]["node_list"]: + self.sessions[session]["nodeDone"][node] = False + + for node in self.sessions[session]["node_list"]: + timer = Timer(self.nodes[node]['commTimeout'], self._event_comm_timeout, args=(session, node)) + self.sessions[session]["commTimers"][node] = timer + timer.start() + self.nodes[node]['device'].set_control_fields(process_fields, session=session, callback=self._device_set_command_callback) + + def _event_comm_timeout(self, session, nodeId): + """ + Triggered if any of the control operations timeout. + Stop communicating with the failing device. + If the control command was an Iq request, sends a failure + message back to the client. + + Arguments: + session -- The request session id + nodeId -- The id of the device which timed out + """ + + if self.sessions[session]["reply"]: + # Reply is exected when we are done + iq = self.xmpp.Iq() + iq['from'] = self.sessions[session]['to'] + iq['to'] = self.sessions[session]['from'] + iq['type'] = "error" + iq['id'] = self.sessions[session]['seqnr'] + iq['setResponse']['responseCode'] = "OtherError" + iq['setResponse'].add_node(nodeId) + iq['setResponse']['error']['var'] = "Output" + iq['setResponse']['error']['text'] = "Timeout." + iq.send(block=False) + + ## TODO - should we send one timeout per node?? + + # Drop communication with this device and check if we are done + self.sessions[session]["nodeDone"][nodeId] = True + if (self._all_nodes_done(session)): + # The session is complete, delete it + del self.sessions[session] + + def _all_nodes_done(self, session): + """ + Checks wheter all devices are done replying to the control command. + + Arguments: + session -- The request session id + """ + for n in self.sessions[session]["nodeDone"]: + if not self.sessions[session]["nodeDone"][n]: + return False + return True + + def _device_set_command_callback(self, session, nodeId, result, error_field=None, error_msg=None): + """ + Callback function called by the devices when the control command is + complete or failed. + If needed, composes a message with the result and sends it back to the + client. + + Arguments: + session -- The request session id + nodeId -- The device id which initiated the callback + result -- The current result status of the control command. Valid values are: + "error" - Set fields failed. + "ok" - All fields were set. + error_field -- [optional] Only applies when result == "error" + The field name that failed (usually means it is missing) + error_msg -- [optional] Only applies when result == "error". + Error details when a request failed. + """ + + if not session in self.sessions: + # This can happend if a session was deleted, like in a timeout. Just drop the data. + return + + if result == "error": + self.sessions[session]["commTimers"][nodeId].cancel() + + if self.sessions[session]["reply"]: + # Reply is exected when we are done + iq = self.xmpp.Iq() + iq['from'] = self.sessions[session]['to'] + iq['to'] = self.sessions[session]['from'] + iq['type'] = "error" + iq['id'] = self.sessions[session]['seqnr'] + iq['setResponse']['responseCode'] = "OtherError" + iq['setResponse'].add_node(nodeId) + if error_field is not None: + iq['setResponse'].add_data(error_field) + iq['setResponse']['error']['var'] = error_field + iq['setResponse']['error']['text'] = error_msg + iq.send(block=False) + + # Drop communication with this device and check if we are done + self.sessions[session]["nodeDone"][nodeId] = True + if (self._all_nodes_done(session)): + # The session is complete, delete it + del self.sessions[session] + else: + self.sessions[session]["commTimers"][nodeId].cancel() + + self.sessions[session]["nodeDone"][nodeId] = True + if (self._all_nodes_done(session)): + if self.sessions[session]["reply"]: + # Reply is exected when we are done + iq = self.xmpp.Iq() + iq['from'] = self.sessions[session]['to'] + iq['to'] = self.sessions[session]['from'] + iq['type'] = "result" + iq['id'] = self.sessions[session]['seqnr'] + iq['setResponse']['responseCode'] = "OK" + iq.send(block=False) + + # The session is complete, delete it + del self.sessions[session] + + + # ================================================================= + # Client side (data controller) API + + def set_request(self, from_jid, to_jid, callback, fields, nodeIds=None): + """ + Called on the client side to initiade a control request. + Composes a message with the request and sends it to the device(s). + Does not block, the callback will be called when the device(s) + has responded. + + Arguments: + from_jid -- The jid of the requester + to_jid -- The jid of the device(s) + callback -- The callback function to call when data is availble. + + The callback function must support the following arguments: + + from_jid -- The jid of the responding device(s) + result -- The result of the control request. Valid values are: + "OK" - Control request completed successfully + "NotFound" - One or more nodes or fields are missing + "InsufficientPrivileges" - Not authorized. + "Locked" - Field(s) is locked and cannot + be changed at the moment. + "NotImplemented" - Request feature not implemented. + "FormError" - Error while setting with + a form (not implemented). + "OtherError" - Indicates other types of + errors, such as timeout. + Details in the error_msg. + + + nodeId -- [optional] Only applicable when result == "error" + List of node Ids of failing device(s). + + fields -- [optional] Only applicable when result == "error" + List of fields that failed.[optional] Mandatory when result == "rejected" or "failure". + + error_msg -- Details about why the request failed. + + fields -- Fields to set. List of tuple format: (name, typename, value). + nodeIds -- [optional] Limits the request to the node Ids in this list. + """ + iq = self.xmpp.Iq() + iq['from'] = from_jid + iq['to'] = to_jid + seqnr = self._get_new_seqnr() + iq['id'] = seqnr + iq['type'] = "set" + if nodeIds is not None: + for nodeId in nodeIds: + iq['set'].add_node(nodeId) + if fields is not None: + for name, typename, value in fields: + iq['set'].add_data(name=name, typename=typename, value=value) + + self.sessions[seqnr] = {"from": iq['from'], "to": iq['to'], "callback": callback} + iq.send(block=False) + + def set_command(self, from_jid, to_jid, fields, nodeIds=None): + """ + Called on the client side to initiade a control command. + Composes a message with the set commandand sends it to the device(s). + Does not block. Device(s) will not respond, regardless of result. + + Arguments: + from_jid -- The jid of the requester + to_jid -- The jid of the device(s) + + fields -- Fields to set. List of tuple format: (name, typename, value). + nodeIds -- [optional] Limits the request to the node Ids in this list. + """ + msg = self.xmpp.Message() + msg['from'] = from_jid + msg['to'] = to_jid + msg['type'] = "set" + if nodeIds is not None: + for nodeId in nodeIds: + msg['set'].add_node(nodeId) + if fields is not None: + for name, typename, value in fields: + msg['set'].add_data(name, typename, value) + + # We won't get any reply, so don't create a session + msg.send() + + def _handle_set_response(self, iq): + """ Received response from device(s) """ + #print("ooh") + seqnr = iq['id'] + from_jid = str(iq['from']) + result = iq['setResponse']['responseCode'] + nodeIds = [n['name'] for n in iq['setResponse']['nodes']] + fields = [f['name'] for f in iq['setResponse']['datas']] + error_msg = None + + if not iq['setResponse'].find('error') is None and not iq['setResponse']['error']['text'] == "": + error_msg = iq['setResponse']['error']['text'] + + callback = self.sessions[seqnr]["callback"] + callback(from_jid=from_jid, result=result, nodeIds=nodeIds, fields=fields, error_msg=error_msg) diff --git a/sleekxmpp/plugins/xep_0325/device.py b/sleekxmpp/plugins/xep_0325/device.py new file mode 100644 index 00000000..f1ed0733 --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/device.py @@ -0,0 +1,125 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime + +class Device(object): + """ + Example implementation of a device control object. + + The device object may by any custom implementation to support + specific devices, but it must implement the functions: + has_control_field + set_control_fields + """ + + def __init__(self, nodeId): + self.nodeId = nodeId + self.control_fields = {} + + def has_control_field(self, field, typename): + """ + Returns true if the supplied field name exists + and the type matches for control in this device. + + Arguments: + field -- The field name + typename -- The expected type + """ + if field in self.control_fields and self.control_fields[field]["type"] == typename: + return True + return False + + def set_control_fields(self, fields, session, callback): + """ + Starts a control setting procedure. Verifies the fields, + sets the data and (if needed) and calls the callback. + + Arguments: + fields -- List of control fields in tuple format: + (name, typename, value) + session -- Session id, only used in the callback as identifier + callback -- Callback function to call when control set is complete. + + The callback function must support the following arguments: + + session -- Session id, as supplied in the + request_fields call + nodeId -- Identifier for this device + result -- The current result status of the readout. + Valid values are: + "error" - Set fields failed. + "ok" - All fields were set. + error_field -- [optional] Only applies when result == "error" + The field name that failed + (usually means it is missing) + error_msg -- [optional] Only applies when result == "error". + Error details when a request failed. + """ + + if len(fields) > 0: + # Check availiability + for name, typename, value in fields: + if not self.has_control_field(name, typename): + self._send_control_reject(session, name, "NotFound", callback) + return False + + for name, typename, value in fields: + self._set_field_value(name, value) + + callback(session, result="ok", nodeId=self.nodeId) + return True + + def _send_control_reject(self, session, field, message, callback): + """ + Sends a reject to the caller + + Arguments: + session -- Session id, see definition in + set_control_fields function + callback -- Callback function, see definition in + set_control_fields function + """ + callback(session, result="error", nodeId=self.nodeId, error_field=field, error_msg=message) + + def _add_control_field(self, name, typename, value): + """ + Adds a control field to the device + + Arguments: + name -- Name of the field + typename -- Type of the field, one of: + (boolean, color, string, date, dateTime, + double, duration, int, long, time) + value -- Field value + """ + self.control_fields[name] = {"type": typename, "value": value} + + def _set_field_value(self, name, value): + """ + Set the value of a control field + + Arguments: + name -- Name of the field + value -- New value for the field + """ + if name in self.control_fields: + self.control_fields[name]["value"] = value + + def _get_field_value(self, name): + """ + Get the value of a control field. Only used for unit testing. + + Arguments: + name -- Name of the field + """ + if name in self.control_fields: + return self.control_fields[name]["value"] + return None diff --git a/sleekxmpp/plugins/xep_0325/stanza/__init__.py b/sleekxmpp/plugins/xep_0325/stanza/__init__.py new file mode 100644 index 00000000..746c2033 --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/stanza/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0325.stanza.control import * + diff --git a/sleekxmpp/plugins/xep_0325/stanza/base.py b/sleekxmpp/plugins/xep_0325/stanza/base.py new file mode 100644 index 00000000..1dadcf46 --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/stanza/base.py @@ -0,0 +1,13 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ET + +pass diff --git a/sleekxmpp/plugins/xep_0325/stanza/control.py b/sleekxmpp/plugins/xep_0325/stanza/control.py new file mode 100644 index 00000000..1fd5c35d --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/stanza/control.py @@ -0,0 +1,527 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp import Iq, Message +from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID +from re import match + +class Control(ElementBase): + """ Placeholder for the namespace, not used as a stanza """ + namespace = 'urn:xmpp:iot:control' + name = 'control' + plugin_attrib = name + interfaces = set(tuple()) + +class ControlSet(ElementBase): + namespace = 'urn:xmpp:iot:control' + name = 'set' + plugin_attrib = name + interfaces = set(['nodes','datas']) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._nodes = set() + self._datas = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._nodes = set([node['nodeId'] for node in self['nodes']]) + self._datas = set([data['name'] for data in self['datas']]) + + def add_node(self, nodeId, sourceId=None, cacheType=None): + """ + Add a new node element. Each item is required to have a + nodeId, but may also specify a sourceId value and cacheType. + + Arguments: + nodeId -- The ID for the node. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + if nodeId not in self._nodes: + self._nodes.add((nodeId)) + node = RequestNode(parent=self) + node['nodeId'] = nodeId + node['sourceId'] = sourceId + node['cacheType'] = cacheType + self.iterables.append(node) + return node + return None + + def del_node(self, nodeId): + """ + Remove a single node. + + Arguments: + nodeId -- Node ID of the item to remove. + """ + if nodeId in self._nodes: + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + if node['nodeId'] == nodeId: + self.xml.remove(node.xml) + self.iterables.remove(node) + return True + return False + + def get_nodes(self): + """Return all nodes.""" + nodes = [] + for node in self['substanzas']: + if isinstance(node, RequestNode): + nodes.append(node) + return nodes + + def set_nodes(self, nodes): + """ + Set or replace all nodes. The given nodes must be in a + list or set where each item is a tuple of the form: + (nodeId, sourceId, cacheType) + + Arguments: + nodes -- A series of nodes in tuple format. + """ + self.del_nodes() + for node in nodes: + if isinstance(node, RequestNode): + self.add_node(node['nodeId'], node['sourceId'], node['cacheType']) + else: + nodeId, sourceId, cacheType = node + self.add_node(nodeId, sourceId, cacheType) + + def del_nodes(self): + """Remove all nodes.""" + self._nodes = set() + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + self.xml.remove(node.xml) + self.iterables.remove(node) + + + def add_data(self, name, typename, value): + """ + Add a new data element. + + Arguments: + name -- The name of the data element + typename -- The type of data element + (boolean, color, string, date, dateTime, + double, duration, int, long, time) + value -- The value of the data element + """ + if name not in self._datas: + dataObj = None + if typename == "boolean": + dataObj = BooleanParameter(parent=self) + elif typename == "color": + dataObj = ColorParameter(parent=self) + elif typename == "string": + dataObj = StringParameter(parent=self) + elif typename == "date": + dataObj = DateParameter(parent=self) + elif typename == "dateTime": + dataObj = DateTimeParameter(parent=self) + elif typename == "double": + dataObj = DoubleParameter(parent=self) + elif typename == "duration": + dataObj = DurationParameter(parent=self) + elif typename == "int": + dataObj = IntParameter(parent=self) + elif typename == "long": + dataObj = LongParameter(parent=self) + elif typename == "time": + dataObj = TimeParameter(parent=self) + + dataObj['name'] = name + dataObj['value'] = value + + self._datas.add(name) + self.iterables.append(dataObj) + return dataObj + return None + + def del_data(self, name): + """ + Remove a single data element. + + Arguments: + data_name -- The data element name to remove. + """ + if name in self._datas: + datas = [i for i in self.iterables if isinstance(i, BaseParameter)] + for data in datas: + if data['name'] == name: + self.xml.remove(data.xml) + self.iterables.remove(data) + return True + return False + + def get_datas(self): + """ Return all data elements. """ + datas = [] + for data in self['substanzas']: + if isinstance(data, BaseParameter): + datas.append(data) + return datas + + def set_datas(self, datas): + """ + Set or replace all data elements. The given elements must be in a + list or set where each item is a data element (numeric, string, boolean, dateTime, timeSpan or enum) + + Arguments: + datas -- A series of data elements. + """ + self.del_datas() + for data in datas: + self.add_data(name=data['name'], typename=data._get_typename(), value=data['value']) + + def del_datas(self): + """Remove all data elements.""" + self._datas = set() + datas = [i for i in self.iterables if isinstance(i, BaseParameter)] + for data in datas: + self.xml.remove(data.xml) + self.iterables.remove(data) + + +class RequestNode(ElementBase): + """ Node element in a request """ + namespace = 'urn:xmpp:iot:control' + name = 'node' + plugin_attrib = name + interfaces = set(['nodeId','sourceId','cacheType']) + + +class ControlSetResponse(ElementBase): + namespace = 'urn:xmpp:iot:control' + name = 'setResponse' + plugin_attrib = name + interfaces = set(['responseCode']) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._nodes = set() + self._datas = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._nodes = set([node['nodeId'] for node in self['nodes']]) + self._datas = set([data['name'] for data in self['datas']]) + + def add_node(self, nodeId, sourceId=None, cacheType=None): + """ + Add a new node element. Each item is required to have a + nodeId, but may also specify a sourceId value and cacheType. + + Arguments: + nodeId -- The ID for the node. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + if nodeId not in self._nodes: + self._nodes.add(nodeId) + node = RequestNode(parent=self) + node['nodeId'] = nodeId + node['sourceId'] = sourceId + node['cacheType'] = cacheType + self.iterables.append(node) + return node + return None + + def del_node(self, nodeId): + """ + Remove a single node. + + Arguments: + nodeId -- Node ID of the item to remove. + """ + if nodeId in self._nodes: + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + if node['nodeId'] == nodeId: + self.xml.remove(node.xml) + self.iterables.remove(node) + return True + return False + + def get_nodes(self): + """Return all nodes.""" + nodes = [] + for node in self['substanzas']: + if isinstance(node, RequestNode): + nodes.append(node) + return nodes + + def set_nodes(self, nodes): + """ + Set or replace all nodes. The given nodes must be in a + list or set where each item is a tuple of the form: + (nodeId, sourceId, cacheType) + + Arguments: + nodes -- A series of nodes in tuple format. + """ + self.del_nodes() + for node in nodes: + if isinstance(node, RequestNode): + self.add_node(node['nodeId'], node['sourceId'], node['cacheType']) + else: + nodeId, sourceId, cacheType = node + self.add_node(nodeId, sourceId, cacheType) + + def del_nodes(self): + """Remove all nodes.""" + self._nodes = set() + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + self.xml.remove(node.xml) + self.iterables.remove(node) + + + def add_data(self, name): + """ + Add a new ResponseParameter element. + + Arguments: + name -- Name of the parameter + """ + if name not in self._datas: + self._datas.add(name) + data = ResponseParameter(parent=self) + data['name'] = name + self.iterables.append(data) + return data + return None + + def del_data(self, name): + """ + Remove a single ResponseParameter element. + + Arguments: + name -- The data element name to remove. + """ + if name in self._datas: + datas = [i for i in self.iterables if isinstance(i, ResponseParameter)] + for data in datas: + if data['name'] == name: + self.xml.remove(data.xml) + self.iterables.remove(data) + return True + return False + + def get_datas(self): + """ Return all ResponseParameter elements. """ + datas = set() + for data in self['substanzas']: + if isinstance(data, ResponseParameter): + datas.add(data) + return datas + + def set_datas(self, datas): + """ + Set or replace all data elements. The given elements must be in a + list or set of ResponseParameter elements + + Arguments: + datas -- A series of data element names. + """ + self.del_datas() + for data in datas: + self.add_data(name=data['name']) + + def del_datas(self): + """Remove all ResponseParameter elements.""" + self._datas = set() + datas = [i for i in self.iterables if isinstance(i, ResponseParameter)] + for data in datas: + self.xml.remove(data.xml) + self.iterables.remove(data) + + +class Error(ElementBase): + namespace = 'urn:xmpp:iot:control' + name = 'error' + plugin_attrib = name + interfaces = set(['var','text']) + + def get_text(self): + """Return then contents inside the XML tag.""" + return self.xml.text + + def set_text(self, value): + """Set then contents inside the XML tag. + + Arguments: + value -- string + """ + + self.xml.text = value + return self + + def del_text(self): + """Remove the contents inside the XML tag.""" + self.xml.text = "" + return self + +class ResponseParameter(ElementBase): + """ + Parameter element in ControlSetResponse. + """ + namespace = 'urn:xmpp:iot:control' + name = 'parameter' + plugin_attrib = name + interfaces = set(['name']) + + +class BaseParameter(ElementBase): + """ + Parameter element in SetCommand. This is a base class, + all instances of parameters added to SetCommand must be of types: + BooleanParameter + ColorParameter + StringParameter + DateParameter + DateTimeParameter + DoubleParameter + DurationParameter + IntParameter + LongParameter + TimeParameter + """ + namespace = 'urn:xmpp:iot:control' + name = 'baseParameter' + plugin_attrib = name + interfaces = set(['name','value']) + + def _get_typename(self): + return self.name + + +class BooleanParameter(BaseParameter): + """ + Field data of type boolean. + Note that the value is expressed as a string. + """ + name = 'boolean' + plugin_attrib = name + +class ColorParameter(BaseParameter): + """ + Field data of type color. + Note that the value is expressed as a string. + """ + name = 'color' + plugin_attrib = name + +class StringParameter(BaseParameter): + """ + Field data of type string. + """ + name = 'string' + plugin_attrib = name + +class DateParameter(BaseParameter): + """ + Field data of type date. + Note that the value is expressed as a string. + """ + name = 'date' + plugin_attrib = name + +class DateTimeParameter(BaseParameter): + """ + Field data of type dateTime. + Note that the value is expressed as a string. + """ + name = 'dateTime' + plugin_attrib = name + +class DoubleParameter(BaseParameter): + """ + Field data of type double. + Note that the value is expressed as a string. + """ + name = 'double' + plugin_attrib = name + +class DurationParameter(BaseParameter): + """ + Field data of type duration. + Note that the value is expressed as a string. + """ + name = 'duration' + plugin_attrib = name + +class IntParameter(BaseParameter): + """ + Field data of type int. + Note that the value is expressed as a string. + """ + name = 'int' + plugin_attrib = name + +class LongParameter(BaseParameter): + """ + Field data of type long (64-bit int). + Note that the value is expressed as a string. + """ + name = 'long' + plugin_attrib = name + +class TimeParameter(BaseParameter): + """ + Field data of type time. + Note that the value is expressed as a string. + """ + name = 'time' + plugin_attrib = name + +register_stanza_plugin(Iq, ControlSet) +register_stanza_plugin(Message, ControlSet) + +register_stanza_plugin(ControlSet, RequestNode, iterable=True) + +register_stanza_plugin(ControlSet, BooleanParameter, iterable=True) +register_stanza_plugin(ControlSet, ColorParameter, iterable=True) +register_stanza_plugin(ControlSet, StringParameter, iterable=True) +register_stanza_plugin(ControlSet, DateParameter, iterable=True) +register_stanza_plugin(ControlSet, DateTimeParameter, iterable=True) +register_stanza_plugin(ControlSet, DoubleParameter, iterable=True) +register_stanza_plugin(ControlSet, DurationParameter, iterable=True) +register_stanza_plugin(ControlSet, IntParameter, iterable=True) +register_stanza_plugin(ControlSet, LongParameter, iterable=True) +register_stanza_plugin(ControlSet, TimeParameter, iterable=True) + +register_stanza_plugin(Iq, ControlSetResponse) +register_stanza_plugin(ControlSetResponse, Error) +register_stanza_plugin(ControlSetResponse, RequestNode, iterable=True) +register_stanza_plugin(ControlSetResponse, ResponseParameter, iterable=True) + diff --git a/sleekxmpp/roster/__init__.py b/sleekxmpp/roster/__init__.py index 4335d367..18b380c9 100644 --- a/sleekxmpp/roster/__init__.py +++ b/sleekxmpp/roster/__init__.py @@ -6,7 +6,6 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.xmlstream import JID from sleekxmpp.roster.item import RosterItem from sleekxmpp.roster.single import RosterNode from sleekxmpp.roster.multi import Roster diff --git a/sleekxmpp/roster/item.py b/sleekxmpp/roster/item.py index 6e9c0d01..ae194e0a 100644 --- a/sleekxmpp/roster/item.py +++ b/sleekxmpp/roster/item.py @@ -479,11 +479,11 @@ class RosterItem(object): self.xmpp.event('roster_subscription_removed', presence) def handle_probe(self, presence): - if self['to']: + if self['from']: self.send_last_presence() if self['pending_out']: self.subscribe() - if not self['to']: + if not self['from']: self._unsubscribed() def reset(self): diff --git a/sleekxmpp/roster/multi.py b/sleekxmpp/roster/multi.py index 9a04aebb..5d070ec8 100644 --- a/sleekxmpp/roster/multi.py +++ b/sleekxmpp/roster/multi.py @@ -94,10 +94,12 @@ class Roster(object): Arguments: key -- Return the roster for this JID. """ - if isinstance(key, JID): - key = key.bare if key is None: - key = self.xmpp.boundjid.bare + key = self.xmpp.boundjid + if not isinstance(key, JID): + key = JID(key) + key = key.bare + if key not in self._rosters: self.add(key) self._rosters[key].auto_authorize = self.auto_authorize @@ -119,8 +121,10 @@ class Roster(object): Arguments: node -- The JID for the new roster node. """ - if isinstance(node, JID): - node = node.bare + if not isinstance(node, JID): + node = JID(node) + + node = node.bare if node not in self._rosters: self._rosters[node] = RosterNode(self.xmpp, node, self.db) diff --git a/sleekxmpp/roster/single.py b/sleekxmpp/roster/single.py index f8c9c781..e9ce4f21 100644 --- a/sleekxmpp/roster/single.py +++ b/sleekxmpp/roster/single.py @@ -89,8 +89,11 @@ class RosterNode(object): A new item entry will be created if one does not already exist. """ - if isinstance(key, JID): - key = key.bare + if key is None: + key = JID('') + if not isinstance(key, JID): + key = JID(key) + key = key.bare if key not in self._jids: self.add(key, save=True) return self._jids[key] @@ -101,8 +104,11 @@ class RosterNode(object): To remove an item from the server, use the remove() method. """ - if isinstance(key, JID): - key = key.bare + if key is None: + key = JID('') + if not isinstance(key, JID): + key = JID(key) + key = key.bare if key in self._jids: del self._jids[key] @@ -231,8 +237,7 @@ class RosterNode(object): if not self.xmpp.is_component: return self.update(jid, subscription='remove') - def update(self, jid, name=None, subscription=None, groups=[], - block=True, timeout=None, callback=None): + def update(self, jid, name=None, subscription=None, groups=None, block=True, timeout=None, callback=None): """ Update a JID's subscription information. @@ -252,6 +257,9 @@ class RosterNode(object): Will be executed when the roster is received. Implies block=False. """ + if not groups: + groups = [] + self[jid]['name'] = name self[jid]['groups'] = groups self[jid].save() diff --git a/sleekxmpp/stanza/atom.py b/sleekxmpp/stanza/atom.py index 244ef315..4e9591a5 100644 --- a/sleekxmpp/stanza/atom.py +++ b/sleekxmpp/stanza/atom.py @@ -6,8 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.xmlstream import ElementBase - +from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase class AtomEntry(ElementBase): @@ -22,5 +21,23 @@ class AtomEntry(ElementBase): namespace = 'http://www.w3.org/2005/Atom' name = 'entry' plugin_attrib = 'entry' - interfaces = set(('title', 'summary')) - sub_interfaces = set(('title', 'summary')) + interfaces = set(('title', 'summary', 'id', 'published', 'updated')) + sub_interfaces = set(('title', 'summary', 'id', 'published', + 'updated')) + +class AtomAuthor(ElementBase): + + """ + An Atom author. + + Stanza Interface: + name -- The printable author name + uri -- The bare jid of the author + """ + + name = 'author' + plugin_attrib = 'author' + interfaces = set(('name', 'uri')) + sub_interfaces = set(('name', 'uri')) + +register_stanza_plugin(AtomEntry, AtomAuthor) diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py index 60bc65bc..56558ba8 100644 --- a/sleekxmpp/stanza/error.py +++ b/sleekxmpp/stanza/error.py @@ -52,7 +52,7 @@ class Error(ElementBase): name = 'error' plugin_attrib = 'error' interfaces = set(('code', 'condition', 'text', 'type', - 'gone', 'redirect')) + 'gone', 'redirect', 'by')) sub_interfaces = set(('text',)) plugin_attrib_map = {} plugin_tag_map = {} diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py index d21a74e1..c43178f2 100644 --- a/sleekxmpp/stanza/htmlim.py +++ b/sleekxmpp/stanza/htmlim.py @@ -7,78 +7,13 @@ """ from sleekxmpp.stanza import Message -from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin - - -class HTMLIM(ElementBase): - - """ - XEP-0071: XHTML-IM defines a method for embedding XHTML content - within a <message> stanza so that lightweight markup can be used - to format the message contents and to create links. - - Only a subset of XHTML is recommended for use with XHTML-IM. - See the full spec at 'http://xmpp.org/extensions/xep-0071.html' - for more information. - - Example stanza: - <message to="user@example.com"> - <body>Non-html message content.</body> - <html xmlns="http://jabber.org/protocol/xhtml-im"> - <body xmlns="http://www.w3.org/1999/xhtml"> - <p><b>HTML!</b></p> - </body> - </html> - </message> - - Stanza Interface: - body -- The contents of the HTML body tag. - - Methods: - setup -- Overrides ElementBase.setup. - get_body -- Return the HTML body contents. - set_body -- Set the HTML body contents. - del_body -- Remove the HTML body contents. - """ - - namespace = 'http://jabber.org/protocol/xhtml-im' - name = 'html' - interfaces = set(('body',)) - plugin_attrib = name - - def set_body(self, html): - """ - Set the contents of the HTML body. - - Arguments: - html -- Either a string or XML object. If the top level - element is not <body> with a namespace of - 'http://www.w3.org/1999/xhtml', it will be wrapped. - """ - if isinstance(html, str): - html = ET.XML(html) - if html.tag != '{http://www.w3.org/1999/xhtml}body': - body = ET.Element('{http://www.w3.org/1999/xhtml}body') - body.append(html) - self.xml.append(body) - else: - self.xml.append(html) - - def get_body(self): - """Return the contents of the HTML body.""" - html = self.xml.find('{http://www.w3.org/1999/xhtml}body') - if html is None: - return '' - return html - - def del_body(self): - """Remove the HTML body contents.""" - if self.parent is not None: - self.parent().xml.remove(self.xml) +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0071 import XHTML_IM as HTMLIM register_stanza_plugin(Message, HTMLIM) + # To comply with PEP8, method names now use underscores. # Deprecated method names are re-mapped for backwards compatibility. HTMLIM.setBody = HTMLIM.set_body diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index f45b3c67..088de4c0 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -9,7 +9,7 @@ from sleekxmpp.stanza.rootstanza import RootStanza from sleekxmpp.xmlstream import StanzaBase, ET from sleekxmpp.xmlstream.handler import Waiter, Callback -from sleekxmpp.xmlstream.matcher import MatcherId +from sleekxmpp.xmlstream.matcher import MatchIDSender, MatcherId from sleekxmpp.exceptions import IqTimeout, IqError @@ -115,9 +115,13 @@ class Iq(RootStanza): """ query = self.xml.find("{%s}query" % value) if query is None and value: - self.clear() - query = ET.Element("{%s}query" % value) - self.xml.append(query) + plugin = self.plugin_tag_map.get('{%s}query' % value, None) + if plugin: + self.enable(plugin.plugin_attrib) + else: + self.clear() + query = ET.Element("{%s}query" % value) + self.xml.append(query) return self def get_query(self): @@ -154,7 +158,7 @@ class Iq(RootStanza): StanzaBase.reply(self, clear) return self - def send(self, block=True, timeout=None, callback=None, now=False): + def send(self, block=True, timeout=None, callback=None, now=False, timeout_callback=None): """ Send an <iq> stanza over the XML stream. @@ -181,20 +185,47 @@ class Iq(RootStanza): now -- Indicates if the send queue should be skipped and send the stanza immediately. Used during stream initialization. Defaults to False. + timeout_callback -- Optional reference to a stream handler function. + Will be executed when the timeout expires before a + response has been received with the originally-sent IQ + stanza. Only called if there is a callback parameter + (and therefore are in async mode). """ if timeout is None: timeout = self.stream.response_timeout + + if self.stream.session_bind_event.is_set(): + matcher = MatchIDSender({ + 'id': self['id'], + 'self': self.stream.boundjid, + 'peer': self['to'] + }) + else: + matcher = MatcherId(self['id']) + if callback is not None and self['type'] in ('get', 'set'): handler_name = 'IqCallback_%s' % self['id'] - handler = Callback(handler_name, - MatcherId(self['id']), - callback, - once=True) + if timeout_callback: + self.callback = callback + self.timeout_callback = timeout_callback + self.stream.schedule('IqTimeout_%s' % self['id'], + timeout, + self._fire_timeout, + repeat=False) + handler = Callback(handler_name, + matcher, + self._handle_result, + once=True) + else: + handler = Callback(handler_name, + matcher, + callback, + once=True) self.stream.register_handler(handler) StanzaBase.send(self, now=now) return handler_name elif block and self['type'] in ('get', 'set'): - waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id'])) + waitfor = Waiter('IqWait_%s' % self['id'], matcher) self.stream.register_handler(waitfor) StanzaBase.send(self, now=now) result = waitfor.wait(timeout) @@ -206,6 +237,16 @@ class Iq(RootStanza): else: return StanzaBase.send(self, now=now) + def _handle_result(self, iq): + # we got the IQ, so don't fire the timeout + self.stream.scheduler.remove('IqTimeout_%s' % self['id']) + self.callback(iq) + + def _fire_timeout(self): + # don't fire the handler for the IQ, if it finally does come in + self.stream.remove_handler('IqCallback_%s' % self['id']) + self.timeout_callback(self) + def _set_stanza_values(self, values): """ Set multiple stanza interface values using a dictionary. diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py index 02133682..0bb6e587 100644 --- a/sleekxmpp/stanza/message.py +++ b/sleekxmpp/stanza/message.py @@ -63,6 +63,17 @@ class Message(RootStanza): lang_interfaces = sub_interfaces types = set(['normal', 'chat', 'headline', 'error', 'groupchat']) + def __init__(self, *args, **kwargs): + """ + Initialize a new <message /> stanza with an optional 'id' value. + + Overrides StanzaBase.__init__. + """ + StanzaBase.__init__(self, *args, **kwargs) + if self['id'] == '': + if self.stream is not None and self.stream.use_message_ids: + self['id'] = self.stream.new_id() + def get_type(self): """ Return the message type. diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py index 7951f861..84bcd122 100644 --- a/sleekxmpp/stanza/presence.py +++ b/sleekxmpp/stanza/presence.py @@ -72,6 +72,17 @@ class Presence(RootStanza): 'subscribed', 'unsubscribe', 'unsubscribed']) showtypes = set(['dnd', 'chat', 'xa', 'away']) + def __init__(self, *args, **kwargs): + """ + Initialize a new <presence /> stanza with an optional 'id' value. + + Overrides StanzaBase.__init__. + """ + StanzaBase.__init__(self, *args, **kwargs) + if self['id'] == '': + if self.stream is not None and self.stream.use_presence_ids: + self['id'] = self.stream.new_id() + def exception(self, e): """ Override exception passback for presence. diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index a7c2b218..52b807e5 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -60,7 +60,9 @@ class RootStanza(StanzaBase): self.send() elif isinstance(e, XMPPError): # We raised this deliberately + keep_id = self['id'] self.reply(clear=e.clear) + self['id'] = keep_id self['error']['condition'] = e.condition self['error']['text'] = e.text self['error']['type'] = e.etype @@ -72,7 +74,9 @@ class RootStanza(StanzaBase): self.send() else: # We probably didn't raise this on purpose, so send an error stanza + keep_id = self['id'] self.reply() + self['id'] = keep_id self['error']['condition'] = 'undefined-condition' self['error']['text'] = "SleekXMPP got into trouble." self['error']['type'] = 'cancel' diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py index a415c482..681efd4f 100644 --- a/sleekxmpp/stanza/roster.py +++ b/sleekxmpp/stanza/roster.py @@ -130,7 +130,10 @@ class RosterItem(ElementBase): def get_groups(self): groups = [] for group in self.xml.findall('{%s}group' % self.namespace): - groups.append(group.text) + if group.text: + groups.append(group.text) + else: + groups.append('') return groups def set_groups(self, values): diff --git a/sleekxmpp/test/livesocket.py b/sleekxmpp/test/livesocket.py index 80d63307..d70ee4eb 100644 --- a/sleekxmpp/test/livesocket.py +++ b/sleekxmpp/test/livesocket.py @@ -8,10 +8,8 @@ import socket import threading -try: - import queue -except ImportError: - import Queue as queue + +from sleekxmpp.util import Queue class TestLiveSocket(object): @@ -39,8 +37,8 @@ class TestLiveSocket(object): """ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.recv_buffer = [] - self.recv_queue = queue.Queue() - self.send_queue = queue.Queue() + self.recv_queue = Queue() + self.send_queue = Queue() self.send_queue_lock = threading.Lock() self.recv_queue_lock = threading.Lock() self.is_live = True diff --git a/sleekxmpp/test/mocksocket.py b/sleekxmpp/test/mocksocket.py index 0920b7ea..4c9d1699 100644 --- a/sleekxmpp/test/mocksocket.py +++ b/sleekxmpp/test/mocksocket.py @@ -7,10 +7,8 @@ """ import socket -try: - import queue -except ImportError: - import Queue as queue + +from sleekxmpp.util import Queue class TestSocket(object): @@ -36,8 +34,8 @@ class TestSocket(object): Same as arguments for socket.socket """ self.socket = socket.socket(*args, **kwargs) - self.recv_queue = queue.Queue() - self.send_queue = queue.Queue() + self.recv_queue = Queue() + self.send_queue = Queue() self.is_live = False self.disconnected = False diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py index cac99f77..e26f99ce 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -8,20 +8,15 @@ import unittest from xml.parsers.expat import ExpatError -try: - import Queue as queue -except: - import queue -import sleekxmpp from sleekxmpp import ClientXMPP, ComponentXMPP +from sleekxmpp.util import Queue from sleekxmpp.stanza import Message, Iq, Presence from sleekxmpp.test import TestSocket, TestLiveSocket -from sleekxmpp.exceptions import XMPPError, IqTimeout, IqError -from sleekxmpp.xmlstream import ET, register_stanza_plugin -from sleekxmpp.xmlstream import ElementBase, StanzaBase +from sleekxmpp.xmlstream import ET +from sleekxmpp.xmlstream import ElementBase from sleekxmpp.xmlstream.tostring import tostring -from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId +from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId, MatchIDSender from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath @@ -217,6 +212,7 @@ class SleekTest(unittest.TestCase): matchers = {'stanzapath': StanzaPath, 'xpath': MatchXPath, 'mask': MatchXMLMask, + 'idsender': MatchIDSender, 'id': MatcherId} Matcher = matchers.get(method, None) if Matcher is None: @@ -292,11 +288,8 @@ class SleekTest(unittest.TestCase): if self.xmpp: self.xmpp.socket.disconnect_error() - def stream_start(self, mode='client', skip=True, header=None, - socket='mock', jid='tester@localhost', - password='test', server='localhost', - port=5222, sasl_mech=None, - plugins=None, plugin_config={}): + def stream_start(self, mode='client', skip=True, header=None, socket='mock', jid='tester@localhost', + password='test', server='localhost', port=5222, sasl_mech=None, plugins=None, plugin_config=None): """ Initialize an XMPP client or component using a dummy XML stream. @@ -319,6 +312,9 @@ class SleekTest(unittest.TestCase): plugins -- List of plugins to register. By default, all plugins are loaded. """ + if not plugin_config: + plugin_config = {} + if mode == 'client': self.xmpp = ClientXMPP(jid, password, sasl_mech=sasl_mech, @@ -338,7 +334,7 @@ class SleekTest(unittest.TestCase): # We will use this to wait for the session_start event # for live connections. - skip_queue = queue.Queue() + skip_queue = Queue() if socket == 'mock': self.xmpp.set_socket(TestSocket()) @@ -371,10 +367,16 @@ class SleekTest(unittest.TestCase): else: for plugin in plugins: self.xmpp.register_plugin(plugin) + + # Some plugins require messages to have ID values. Set + # this to True in tests related to those plugins. + self.xmpp.use_message_ids = False + self.xmpp.process(threaded=True) if skip: if socket != 'live': # Mark send queue as usable + self.xmpp.session_bind_event.set() self.xmpp.session_started_event.set() # Clear startup stanzas self.xmpp.socket.next_sent(timeout=1) @@ -423,8 +425,7 @@ class SleekTest(unittest.TestCase): parts.append('xmlns="%s"' % default_ns) return header % ' '.join(parts) - def recv(self, data, defaults=[], method='exact', - use_values=True, timeout=1): + def recv(self, data, defaults=None, method='exact', use_values=True, timeout=1): """ Pass data to the dummy XMPP client as if it came from an XMPP server. @@ -445,6 +446,9 @@ class SleekTest(unittest.TestCase): timeout -- Time to wait in seconds for data to be received by a live connection. """ + if not defaults: + defaults = [] + if self.xmpp.socket.is_live: # we are working with a live connection, so we should # verify what has been received instead of simulating diff --git a/sleekxmpp/thirdparty/__init__.py b/sleekxmpp/thirdparty/__init__.py index 7ec045a6..2a1db717 100644 --- a/sleekxmpp/thirdparty/__init__.py +++ b/sleekxmpp/thirdparty/__init__.py @@ -8,5 +8,5 @@ try: except: from sleekxmpp.thirdparty.gnupg import GPG -from sleekxmpp.thirdparty import suelta, socks +from sleekxmpp.thirdparty import socks from sleekxmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso diff --git a/sleekxmpp/thirdparty/mini_dateutil.py b/sleekxmpp/thirdparty/mini_dateutil.py index d0d3f2ea..e751a448 100644 --- a/sleekxmpp/thirdparty/mini_dateutil.py +++ b/sleekxmpp/thirdparty/mini_dateutil.py @@ -108,7 +108,7 @@ except: def __init__(self, name, offset): self._name = name - self._offset = datetime.timedelta(seconds=offset) + self._offset = datetime.timedelta(minutes=offset) def utcoffset(self, dt): return self._offset @@ -154,7 +154,7 @@ except: absoff = offsetmins name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60) - inst = tzoffset(offsetmins, name) + inst = tzoffset(name,offsetmins) _fixed_offset_tzs[offsetmins] = inst return _fixed_offset_tzs[offsetmins] @@ -166,32 +166,34 @@ except: (?P<month>[0-9]{2})?(?P=ymdsep)? (?P<day> [0-9]{2})? - (?: # time part... optional... at least hour must be specified - (?:T|\s+)? - (?P<hour>[0-9]{2}) - (?: - # minutes, separated with :, or none, from hours - (?P<hmssep>[:]?) - (?P<minute>[0-9]{2}) + (?P<time> + (?: # time part... optional... at least hour must be specified + (?:T|\s+)? + (?P<hour>[0-9]{2}) (?: - # same for seconds, separated with :, or none, from hours - (?P=hmssep) - (?P<second>[0-9]{2}) + # minutes, separated with :, or none, from hours + (?P<hmssep>[:]?) + (?P<minute>[0-9]{2}) + (?: + # same for seconds, separated with :, or none, from hours + (?P=hmssep) + (?P<second>[0-9]{2}) + )? )? - )? - - # fractions - (?: [,.] (?P<frac>[0-9]{1,10}))? - - # timezone, Z, +-hh or +-hh:?mm. MUST BE, but complain if not there. - ( - (?P<tzempty>Z) - | - (?P<tzh>[+-][0-9]{2}) - (?: :? # optional separator - (?P<tzm>[0-9]{2}) + + # fractions + (?: [,.] (?P<frac>[0-9]{1,10}))? + + # timezone, Z, +-hh or +-hh:?mm. MUST BE, but complain if not there. + ( + (?P<tzempty>Z) + | + (?P<tzh>[+-][0-9]{2}) + (?: :? # optional separator + (?P<tzm>[0-9]{2}) + )? )? - )? + ) )? $ """, re.X) # """ @@ -211,13 +213,16 @@ except: for key in vals: if vals[key] is None: vals[key] = def_vals.get(key, 0) - elif key not in ['ymdsep', 'hmssep', 'tzempty']: + elif key not in ['time', 'ymdsep', 'hmssep', 'tzempty']: vals[key] = int(vals[key]) year = vals['year'] month = vals['month'] day = vals['day'] + if m.group('time') is None: + return datetime.date(year, month, day) + h, min, s, us = None, None, None, 0 frac = 0 if m.group('tzempty') == None and m.group('tzh') == None: diff --git a/sleekxmpp/thirdparty/socks.py b/sleekxmpp/thirdparty/socks.py index a6c0d70e..34090d51 100644 --- a/sleekxmpp/thirdparty/socks.py +++ b/sleekxmpp/thirdparty/socks.py @@ -13,7 +13,7 @@ are permitted provided that the following conditions are met: 3. Neither the name of Dan Haim nor the names of his contributors may be used to endorse or promote products derived from this software without specific prior written permission. - + THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO @@ -38,6 +38,8 @@ for use in PyLoris (http://pyloris.sourceforge.net/) Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) mainly to merge bug fixes found in Sourceforge +Minor modifications made by Eugene Dementiev (http://www.dementiev.eu/) + """ import socket @@ -212,12 +214,12 @@ class socksocket(socket.socket): if self.__proxy[3]: # Resolve remotely ipaddr = None - req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr + req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr.encode() else: # Resolve locally ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) req = req + chr(0x01).encode() + ipaddr - req = req + struct.pack(">H", destport) + req += struct.pack(">H", destport) self.sendall(req) # Get the response resp = self.__recvall(4) @@ -286,7 +288,7 @@ class socksocket(socket.socket): # The username parameter is considered userid for SOCKS4 if self.__proxy[4] != None: req = req + self.__proxy[4] - req = req + chr(0x00).encode() + req += chr(0x00).encode() # DNS name if remote resolving is required # NOTE: This is actually an extension to the SOCKS4 protocol # called SOCKS4A and may not be supported in all cases. @@ -327,7 +329,10 @@ class socksocket(socket.socket): # We read the response until we get the string "\r\n\r\n" resp = self.recv(1) while resp.find("\r\n\r\n".encode()) == -1: - resp = resp + self.recv(1) + recv = self.recv(1) + if not recv: + raise GeneralProxyError((1, _generalerrors[1])) + resp = resp + recv # We just need the first line to check if the connection # was successful statusline = resp.splitlines()[0].split(" ".encode(), 2) diff --git a/sleekxmpp/thirdparty/statemachine.py b/sleekxmpp/thirdparty/statemachine.py index 33d9b828..6c504dce 100644 --- a/sleekxmpp/thirdparty/statemachine.py +++ b/sleekxmpp/thirdparty/statemachine.py @@ -15,7 +15,8 @@ log = logging.getLogger(__name__) class StateMachine(object): - def __init__(self, states=[]): + def __init__(self, states=None): + if not states: states = [] self.lock = threading.Condition() self.__states = [] self.addStates(states) @@ -29,11 +30,11 @@ class StateMachine(object): if state in self.__states: raise IndexError("The state '%s' is already in the StateMachine." % state) self.__states.append(state) - finally: + finally: self.lock.release() - def transition(self, from_state, to_state, wait=0.0, func=None, args=[], kwargs={}): + def transition(self, from_state, to_state, wait=0.0, func=None, args=None, kwargs=None): ''' Transition from the given `from_state` to the given `to_state`. This method will return `True` if the state machine is now in `to_state`. It @@ -64,15 +65,23 @@ class StateMachine(object): values for `args` and `kwargs` are provided, they are expanded and passed like so: `func( *args, **kwargs )`. ''' + if not args: + args = [] + if not kwargs: + kwargs = {} return self.transition_any((from_state,), to_state, wait=wait, func=func, args=args, kwargs=kwargs) - def transition_any(self, from_states, to_state, wait=0.0, func=None, args=[], kwargs={}): + def transition_any(self, from_states, to_state, wait=0.0, func=None, args=None, kwargs=None): ''' Transition from any of the given `from_states` to the given `to_state`. ''' + if not args: + args = [] + if not kwargs: + kwargs = {} if not isinstance(from_states, (tuple, list, set)): raise ValueError("from_states should be a list, tuple, or set") @@ -83,11 +92,14 @@ class StateMachine(object): if not to_state in self.__states: raise ValueError("StateMachine does not contain to_state %s." % to_state) + if self.__current_state == to_state: + return True + start = time.time() while not self.lock.acquire(False): time.sleep(.001) if (start + wait - time.time()) <= 0.0: - log.debug("Could not acquire lock") + log.debug("==== Could not acquire lock in %s sec: %s -> %s ", wait, self.__current_state, to_state) return False while not self.__current_state in from_states: @@ -108,7 +120,7 @@ class StateMachine(object): # some 'false' value returned from func, # indicating that transition should not occur: - if not return_val: + if not return_val: return return_val log.debug(' ==== TRANSITION %s -> %s', self.__current_state, to_state) @@ -193,9 +205,9 @@ class StateMachine(object): while not self.__current_state in states: # detect timeout: remainder = start + wait - time.time() - if remainder > 0: + if remainder > 0: self.lock.wait(remainder) - else: + else: self.lock.release() return False self.lock.release() @@ -241,7 +253,7 @@ class _StateCtx: while not self.state_machine[self.from_state] or not self.state_machine.lock.acquire(False): # detect timeout: remainder = start + self.wait - time.time() - if remainder > 0: + if remainder > 0: self.state_machine.lock.wait(remainder) else: log.debug('StateMachine timeout while waiting for state: %s', self.from_state) diff --git a/sleekxmpp/thirdparty/suelta/LICENSE b/sleekxmpp/thirdparty/suelta/LICENSE deleted file mode 100644 index 6eee4f33..00000000 --- a/sleekxmpp/thirdparty/suelta/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -This software is subject to "The MIT License" - -Copyright 2007-2010 David Alan Cridland - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/sleekxmpp/thirdparty/suelta/PLAYING-NICELY b/sleekxmpp/thirdparty/suelta/PLAYING-NICELY deleted file mode 100644 index 393b8078..00000000 --- a/sleekxmpp/thirdparty/suelta/PLAYING-NICELY +++ /dev/null @@ -1,27 +0,0 @@ -Hi. - -This is a short note explaining the license in non-legally-binding terms, and -describing how I hope to see people work with the licensing. - -First off, the license is permissive, and more or less allows you to do -anything, as long as you leave my credit and copyright intact. - -You can, and are very much welcome to, include this in commercial works, and -in code that has tightly controlled distribution, as well as open-source. - -If it doesn't work - and I have no doubt that there are bugs - then this is -largely your problem. - -If you do find a bug, though, do let me know - although you don't have to. - -And if you fix it, I'd greatly appreciate a patch, too. Please give me a -licensing statement, and a copyright statement, along with your patch. - -Similarly, any enhancements are welcome, and also will need copyright and -licensing. Please stick to a license which is compatible with the MIT license, -and consider assignment (as required) to me to simplify licensing. (Public -domain does not exist in the UK, sorry). - -Thanks, - -Dave. diff --git a/sleekxmpp/thirdparty/suelta/README b/sleekxmpp/thirdparty/suelta/README deleted file mode 100644 index c32463a4..00000000 --- a/sleekxmpp/thirdparty/suelta/README +++ /dev/null @@ -1,8 +0,0 @@ -Suelta - A pure-Python SASL client library - -Suelta is a SASL library, providing you with authentication and in some cases -security layers. - -It supports a wide range of typical SASL mechanisms, including the MTI for -all known protocols. - diff --git a/sleekxmpp/thirdparty/suelta/__init__.py b/sleekxmpp/thirdparty/suelta/__init__.py deleted file mode 100644 index 04f0cbad..00000000 --- a/sleekxmpp/thirdparty/suelta/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2007-2010 David Alan Cridland -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from sleekxmpp.thirdparty.suelta.saslprep import saslprep -from sleekxmpp.thirdparty.suelta.sasl import * -from sleekxmpp.thirdparty.suelta.mechanisms import * - -__version__ = '2.0' -__version_info__ = (2, 0, 0) diff --git a/sleekxmpp/thirdparty/suelta/exceptions.py b/sleekxmpp/thirdparty/suelta/exceptions.py deleted file mode 100644 index 40d8bad3..00000000 --- a/sleekxmpp/thirdparty/suelta/exceptions.py +++ /dev/null @@ -1,35 +0,0 @@ -class SASLError(Exception): - - def __init__(self, sasl, text, mech=None): - """ - :param sasl: The main `suelta.SASL` object. - :param text: Descpription of the error. - :param mech: Optional reference to the mechanism object. - - :type sasl: `suelta.SASL` - """ - self.sasl = sasl - self.text = text - self.mech = mech - - def __str__(self): - if self.mech is None: - return 'SASL Error: %s' % self.text - else: - return 'SASL Error (%s): %s' % (self.mech, self.text) - - -class SASLCancelled(SASLError): - - def __init__(self, sasl, mech=None): - """ - :param sasl: The main `suelta.SASL` object. - :param mech: Optional reference to the mechanism object. - - :type sasl: `suelta.SASL` - """ - super(SASLCancelled, self).__init__(sasl, "User cancelled", mech) - - -class SASLPrepFailure(UnicodeError): - pass diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py deleted file mode 100644 index 2044ff80..00000000 --- a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from sleekxmpp.thirdparty.suelta.mechanisms.anonymous import ANONYMOUS -from sleekxmpp.thirdparty.suelta.mechanisms.plain import PLAIN -from sleekxmpp.thirdparty.suelta.mechanisms.cram_md5 import CRAM_MD5 -from sleekxmpp.thirdparty.suelta.mechanisms.digest_md5 import DIGEST_MD5 -from sleekxmpp.thirdparty.suelta.mechanisms.scram_hmac import SCRAM_HMAC -from sleekxmpp.thirdparty.suelta.mechanisms.messenger_oauth2 import X_MESSENGER_OAUTH2 -from sleekxmpp.thirdparty.suelta.mechanisms.facebook_platform import X_FACEBOOK_PLATFORM -from sleekxmpp.thirdparty.suelta.mechanisms.google_token import X_GOOGLE_TOKEN diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py deleted file mode 100644 index e44e91a2..00000000 --- a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py +++ /dev/null @@ -1,36 +0,0 @@ -from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism -from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled - - -class ANONYMOUS(Mechanism): - - """ - """ - - def __init__(self, sasl, name): - """ - """ - super(ANONYMOUS, self).__init__(sasl, name, 0) - - def get_values(self): - """ - """ - return {} - - def process(self, challenge=None): - """ - """ - return b'Anonymous, Suelta' - - def okay(self): - """ - """ - return True - - def get_user(self): - """ - """ - return 'anonymous' - - -register_mechanism('ANONYMOUS', 0, ANONYMOUS, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py deleted file mode 100644 index e07bb883..00000000 --- a/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py +++ /dev/null @@ -1,63 +0,0 @@ -import sys -import hmac - -from sleekxmpp.thirdparty.suelta.util import hash, bytes -from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism -from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled - - -class CRAM_MD5(Mechanism): - - """ - """ - - def __init__(self, sasl, name): - """ - """ - super(CRAM_MD5, self).__init__(sasl, name, 2) - - self.hash = hash(name[5:]) - if self.hash is None: - raise SASLCancelled(self.sasl, self) - if not self.sasl.tls_active(): - if not self.sasl.sec_query(self, 'CRAM-MD5'): - raise SASLCancelled(self.sasl, self) - - def prep(self): - """ - """ - if 'savepass' not in self.values: - if self.sasl.sec_query(self, 'CLEAR-PASSWORD'): - self.values['savepass'] = True - - if 'savepass' not in self.values: - del self.values['password'] - - def process(self, challenge=None): - """ - """ - if challenge is None: - return None - - self.check_values(['username', 'password']) - username = bytes(self.values['username']) - password = bytes(self.values['password']) - - mac = hmac.HMAC(key=password, digestmod=self.hash) - - mac.update(challenge) - - return username + b' ' + bytes(mac.hexdigest()) - - def okay(self): - """ - """ - return True - - def get_user(self): - """ - """ - return self.values['username'] - - -register_mechanism('CRAM-', 20, CRAM_MD5) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py deleted file mode 100644 index 890f3e24..00000000 --- a/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py +++ /dev/null @@ -1,275 +0,0 @@ -import sys - -import random -import hmac - -from sleekxmpp.thirdparty.suelta.util import hash, bytes, quote -from sleekxmpp.thirdparty.suelta.util import num_to_bytes, bytes_to_num -from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism -from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled - - - -def parse_challenge(stuff): - """ - """ - ret = {} - var = b'' - val = b'' - in_var = True - in_quotes = False - new = False - escaped = False - for c in stuff: - if sys.version_info >= (3, 0): - c = bytes([c]) - if in_var: - if c.isspace(): - continue - if c == b'=': - in_var = False - new = True - else: - var += c - else: - if new: - if c == b'"': - in_quotes = True - else: - val += c - new = False - elif in_quotes: - if escaped: - escaped = False - val += c - else: - if c == b'\\': - escaped = True - elif c == b'"': - in_quotes = False - else: - val += c - else: - if c == b',': - if var: - ret[var] = val - var = b'' - val = b'' - in_var = True - else: - val += c - if var: - ret[var] = val - return ret - - -class DIGEST_MD5(Mechanism): - - """ - """ - - enc_magic = 'Digest session key to client-to-server signing key magic' - dec_magic = 'Digest session key to server-to-client signing key magic' - - def __init__(self, sasl, name): - """ - """ - super(DIGEST_MD5, self).__init__(sasl, name, 3) - - self.hash = hash(name[7:]) - if self.hash is None: - raise SASLCancelled(self.sasl, self) - - if not self.sasl.tls_active(): - if not self.sasl.sec_query(self, '-ENCRYPTION, DIGEST-MD5'): - raise SASLCancelled(self.sasl, self) - - self._rspauth_okay = False - self._digest_uri = None - self._a1 = None - self._enc_buf = b'' - self._enc_key = None - self._enc_seq = 0 - self._max_buffer = 65536 - self._dec_buf = b'' - self._dec_key = None - self._dec_seq = 0 - self._qops = [b'auth'] - self._qop = b'auth' - - def MAC(self, seq, msg, key): - """ - """ - mac = hmac.HMAC(key=key, digestmod=self.hash) - seqnum = num_to_bytes(seq) - mac.update(seqnum) - mac.update(msg) - return mac.digest()[:10] + b'\x00\x01' + seqnum - - - def encode(self, text): - """ - """ - self._enc_buf += text - - def flush(self): - """ - """ - result = b'' - # Leave buffer space for the MAC - mbuf = self._max_buffer - 10 - 2 - 4 - - while self._enc_buf: - msg = self._encbuf[:mbuf] - mac = self.MAC(self._enc_seq, msg, self._enc_key, self.hash) - self._enc_seq += 1 - msg += mac - result += num_to_bytes(len(msg)) + msg - self._enc_buf = self._enc_buf[mbuf:] - - return result - - def decode(self, text): - """ - """ - self._dec_buf += text - result = b'' - - while len(self._dec_buf) > 4: - num = bytes_to_num(self._dec_buf) - if len(self._dec_buf) < (num + 4): - return result - - mac = self._dec_buf[4:4 + num] - self._dec_buf = self._dec_buf[4 + num:] - msg = mac[:-16] - - mac_conf = self.MAC(self._dec_mac, msg, self._dec_key) - if mac[-16:] != mac_conf: - self._desc_sec = None - return result - - self._dec_seq += 1 - result += msg - - return result - - def response(self): - """ - """ - vitals = ['username'] - if not self.has_values(['key_hash']): - vitals.append('password') - self.check_values(vitals) - - resp = {} - if 'auth-int' in self._qops: - self._qop = b'auth-int' - resp['qop'] = self._qop - if 'realm' in self.values: - resp['realm'] = quote(self.values['realm']) - - resp['username'] = quote(bytes(self.values['username'])) - resp['nonce'] = quote(self.values['nonce']) - if self.values['nc']: - self._cnonce = self.values['cnonce'] - else: - self._cnonce = bytes('%s' % random.random())[2:] - resp['cnonce'] = quote(self._cnonce) - self.values['nc'] += 1 - resp['nc'] = bytes('%08x' % self.values['nc']) - - service = bytes(self.sasl.service) - host = bytes(self.sasl.host) - self._digest_uri = service + b'/' + host - resp['digest-uri'] = quote(self._digest_uri) - - a2 = b'AUTHENTICATE:' + self._digest_uri - if self._qop != b'auth': - a2 += b':00000000000000000000000000000000' - resp['maxbuf'] = b'16777215' # 2**24-1 - resp['response'] = self.gen_hash(a2) - return b','.join([bytes(k) + b'=' + bytes(v) for k, v in resp.items()]) - - def gen_hash(self, a2): - """ - """ - if not self.has_values(['key_hash']): - key_hash = self.hash() - user = bytes(self.values['username']) - password = bytes(self.values['password']) - realm = bytes(self.values['realm']) - kh = user + b':' + realm + b':' + password - key_hash.update(kh) - self.values['key_hash'] = key_hash.digest() - - a1 = self.hash(self.values['key_hash']) - a1h = b':' + self.values['nonce'] + b':' + self._cnonce - a1.update(a1h) - response = self.hash() - self._a1 = a1.digest() - rv = bytes(a1.hexdigest().lower()) - rv += b':' + self.values['nonce'] - rv += b':' + bytes('%08x' % self.values['nc']) - rv += b':' + self._cnonce - rv += b':' + self._qop - rv += b':' + bytes(self.hash(a2).hexdigest().lower()) - response.update(rv) - return bytes(response.hexdigest().lower()) - - def mutual_auth(self, cmp_hash): - """ - """ - a2 = b':' + self._digest_uri - if self._qop != b'auth': - a2 += b':00000000000000000000000000000000' - if self.gen_hash(a2) == cmp_hash: - self._rspauth_okay = True - - def prep(self): - """ - """ - if 'password' in self.values: - del self.values['password'] - self.values['cnonce'] = self._cnonce - - def process(self, challenge=None): - """ - """ - if challenge is None: - if self.has_values(['username', 'realm', 'nonce', 'key_hash', - 'nc', 'cnonce', 'qops']): - self._qops = self.values['qops'] - return self.response() - else: - return None - - d = parse_challenge(challenge) - if b'rspauth' in d: - self.mutual_auth(d[b'rspauth']) - else: - if b'realm' not in d: - d[b'realm'] = self.sasl.def_realm - for key in ['nonce', 'realm']: - if bytes(key) in d: - self.values[key] = d[bytes(key)] - self.values['nc'] = 0 - self._qops = [b'auth'] - if b'qop' in d: - self._qops = [x.strip() for x in d[b'qop'].split(b',')] - self.values['qops'] = self._qops - if b'maxbuf' in d: - self._max_buffer = int(d[b'maxbuf']) - return self.response() - - def okay(self): - """ - """ - if self._rspauth_okay and self._qop == b'auth-int': - self._enc_key = self.hash(self._a1 + self.enc_magic).digest() - self._dec_key = self.hash(self._a1 + self.dec_magic).digest() - self.encoding = True - return self._rspauth_okay - - -register_mechanism('DIGEST-', 30, DIGEST_MD5) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py b/sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py deleted file mode 100644 index af6a78eb..00000000 --- a/sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py +++ /dev/null @@ -1,43 +0,0 @@ -from sleekxmpp.thirdparty.suelta.util import bytes -from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism - -try: - import urlparse -except ImportError: - import urllib.parse as urlparse - - - -class X_FACEBOOK_PLATFORM(Mechanism): - - def __init__(self, sasl, name): - super(X_FACEBOOK_PLATFORM, self).__init__(sasl, name) - self.check_values(['access_token', 'api_key']) - - def process(self, challenge=None): - if challenge is not None: - values = {} - for kv in challenge.split(b'&'): - key, value = kv.split(b'=') - values[key] = value - - resp_data = { - 'method': values[b'method'], - 'v': '1.0', - 'call_id': '1.0', - 'nonce': values[b'nonce'], - 'access_token': self.values['access_token'], - 'api_key': self.values['api_key'] - } - - for k, v in resp_data.items(): - resp_data[k] = bytes(v).decode('utf-8') - - resp = '&'.join(['%s=%s' % (k, v) for k, v in resp_data.items()]) - return bytes(resp) - return b'' - - def okay(self): - return True - -register_mechanism('X-FACEBOOK-PLATFORM', 40, X_FACEBOOK_PLATFORM, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py b/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py deleted file mode 100644 index e641bb91..00000000 --- a/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py +++ /dev/null @@ -1,22 +0,0 @@ -from sleekxmpp.thirdparty.suelta.util import bytes -from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism -from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled - - - -class X_GOOGLE_TOKEN(Mechanism): - - def __init__(self, sasl, name): - super(X_GOOGLE_TOKEN, self).__init__(sasl, name) - self.check_values(['email', 'access_token']) - - def process(self, challenge=None): - email = bytes(self.values['email']) - token = bytes(self.values['access_token']) - return b'\x00' + email + b'\x00' + token - - def okay(self): - return True - - -register_mechanism('X-GOOGLE-TOKEN', 3, X_GOOGLE_TOKEN, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py b/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py deleted file mode 100644 index f5b0ddec..00000000 --- a/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py +++ /dev/null @@ -1,17 +0,0 @@ -from sleekxmpp.thirdparty.suelta.util import bytes -from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism - - -class X_MESSENGER_OAUTH2(Mechanism): - - def __init__(self, sasl, name): - super(X_MESSENGER_OAUTH2, self).__init__(sasl, name) - self.check_values(['access_token']) - - def process(self, challenge=None): - return bytes(self.values['access_token']) - - def okay(self): - return True - -register_mechanism('X-MESSENGER-OAUTH2', 10, X_MESSENGER_OAUTH2, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/plain.py b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py deleted file mode 100644 index accae54a..00000000 --- a/sleekxmpp/thirdparty/suelta/mechanisms/plain.py +++ /dev/null @@ -1,61 +0,0 @@ -import sys - -from sleekxmpp.thirdparty.suelta.util import bytes -from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism -from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled - - -class PLAIN(Mechanism): - - """ - """ - - def __init__(self, sasl, name): - """ - """ - super(PLAIN, self).__init__(sasl, name) - - if not self.sasl.tls_active(): - if not self.sasl.sec_query(self, '-ENCRYPTION, PLAIN'): - raise SASLCancelled(self.sasl, self) - else: - if not self.sasl.sec_query(self, '+ENCRYPTION, PLAIN'): - raise SASLCancelled(self.sasl, self) - - self.check_values(['username', 'password']) - - def prep(self): - """ - Prepare for processing by deleting the password if - the user has not approved storing it in the clear. - """ - if 'savepass' not in self.values: - if self.sasl.sec_query(self, 'CLEAR-PASSWORD'): - self.values['savepass'] = True - - if 'savepass' not in self.values: - del self.values['password'] - - return True - - def process(self, challenge=None): - """ - Process a challenge request and return the response. - - :param challenge: A challenge issued by the server that - must be answered for authentication. - """ - user = bytes(self.values['username']) - password = bytes(self.values['password']) - return b'\x00' + user + b'\x00' + password - - def okay(self): - """ - Mutual authentication is not supported by PLAIN. - - :returns: ``True`` - """ - return True - - -register_mechanism('PLAIN', 5, PLAIN, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py b/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py deleted file mode 100644 index b70ac9a4..00000000 --- a/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py +++ /dev/null @@ -1,176 +0,0 @@ -import sys -import hmac -import random -from base64 import b64encode, b64decode - -from sleekxmpp.thirdparty.suelta.util import hash, bytes, num_to_bytes, bytes_to_num, XOR -from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism -from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled - - -def parse_challenge(challenge): - """ - """ - items = {} - for key, value in [item.split(b'=', 1) for item in challenge.split(b',')]: - items[key] = value - return items - - -class SCRAM_HMAC(Mechanism): - - """ - """ - - def __init__(self, sasl, name): - """ - """ - super(SCRAM_HMAC, self).__init__(sasl, name, 0) - - self._cb = False - if name[-5:] == '-PLUS': - name = name[:-5] - self._cb = True - - self.hash = hash(name[6:]) - if self.hash is None: - raise SASLCancelled(self.sasl, self) - if not self.sasl.tls_active(): - if not self.sasl.sec_query(self, '-ENCRYPTION, SCRAM'): - raise SASLCancelled(self.sasl, self) - - self._step = 0 - self._rspauth = False - - def HMAC(self, key, msg): - """ - """ - return hmac.HMAC(key=key, msg=msg, digestmod=self.hash).digest() - - def Hi(self, text, salt, iterations): - """ - """ - text = bytes(text) - ui_1 = self.HMAC(text, salt + b'\0\0\0\01') - ui = ui_1 - for i in range(iterations - 1): - ui_1 = self.HMAC(text, ui_1) - ui = XOR(ui, ui_1) - return ui - - def H(self, text): - """ - """ - return self.hash(text).digest() - - def prep(self): - if 'password' in self.values: - del self.values['password'] - - def process(self, challenge=None): - """ - """ - steps = { - 0: self.process_one, - 1: self.process_two, - 2: self.process_three - } - return steps[self._step](challenge) - - def process_one(self, challenge): - """ - """ - vitals = ['username'] - if 'SaltedPassword' not in self.values: - vitals.append('password') - if 'Iterations' not in self.values: - vitals.append('password') - - self.check_values(vitals) - - username = bytes(self.values['username']) - - self._step = 1 - self._cnonce = bytes(('%s' % random.random())[2:]) - self._soup = b'n=' + username + b',r=' + self._cnonce - self._gs2header = b'' - - if not self.sasl.tls_active(): - if self._cb: - self._gs2header = b'p=tls-unique,,' - else: - self._gs2header = b'y,,' - else: - self._gs2header = b'n,,' - - return self._gs2header + self._soup - - def process_two(self, challenge): - """ - """ - data = parse_challenge(challenge) - - self._step = 2 - self._soup += b',' + challenge + b',' - self._nonce = data[b'r'] - self._salt = b64decode(data[b's']) - self._iter = int(data[b'i']) - - if self._nonce[:len(self._cnonce)] != self._cnonce: - raise SASLCancelled(self.sasl, self) - - cbdata = self.sasl.tls_active() - c = self._gs2header - if not cbdata and self._cb: - c += None - - r = b'c=' + b64encode(c).replace(b'\n', b'') - r += b',r=' + self._nonce - self._soup += r - - if 'Iterations' in self.values: - if self.values['Iterations'] != self._iter: - if 'SaltedPassword' in self.values: - del self.values['SaltedPassword'] - if 'Salt' in self.values: - if self.values['Salt'] != self._salt: - if 'SaltedPassword' in self.values: - del self.values['SaltedPassword'] - - self.values['Iterations'] = self._iter - self.values['Salt'] = self._salt - - if 'SaltedPassword' not in self.values: - self.check_values(['password']) - password = bytes(self.values['password']) - salted_pass = self.Hi(password, self._salt, self._iter) - self.values['SaltedPassword'] = salted_pass - - salted_pass = self.values['SaltedPassword'] - client_key = self.HMAC(salted_pass, b'Client Key') - stored_key = self.H(client_key) - client_sig = self.HMAC(stored_key, self._soup) - client_proof = XOR(client_key, client_sig) - r += b',p=' + b64encode(client_proof).replace(b'\n', b'') - server_key = self.HMAC(self.values['SaltedPassword'], b'Server Key') - self.server_sig = self.HMAC(server_key, self._soup) - return r - - def process_three(self, challenge=None): - """ - """ - data = parse_challenge(challenge) - if b64decode(data[b'v']) == self.server_sig: - self._rspauth = True - - def okay(self): - """ - """ - return self._rspauth - - def get_user(self): - return self.values['username'] - - -register_mechanism('SCRAM-', 60, SCRAM_HMAC) -register_mechanism('SCRAM-', 70, SCRAM_HMAC, extra='-PLUS') diff --git a/sleekxmpp/thirdparty/suelta/sasl.py b/sleekxmpp/thirdparty/suelta/sasl.py deleted file mode 100644 index 2ae9ae61..00000000 --- a/sleekxmpp/thirdparty/suelta/sasl.py +++ /dev/null @@ -1,402 +0,0 @@ -from sleekxmpp.thirdparty.suelta.util import hashes -from sleekxmpp.thirdparty.suelta.saslprep import saslprep - -#: Global session storage for user answers to requested mechanism values -#: and security questions. This allows the user's preferences to be -#: persisted across multiple SASL authentication attempts made by the -#: same process. -SESSION = {'answers': {}, - 'passwords': {}, - 'sec_queries': {}, - 'stash': {}, - 'stash_file': ''} - -#: Global registry mapping mechanism names to implementation classes. -MECHANISMS = {} - -#: Global registry mapping mechanism names to security scores. -MECH_SEC_SCORES = {} - - -def register_mechanism(basename, basescore, impl, extra=None, use_hashes=True): - """ - Add a SASL mechanism to the registry of available mechanisms. - - :param basename: The base name of the mechanism type, such as ``CRAM-``. - :param basescore: The base security score for this type of mechanism. - :param impl: The class implementing the mechanism. - :param extra: Any additional qualifiers to the mechanism name, - such as ``-PLUS``. - :param use_hashes: If ``True``, then register the mechanism for use with - all available hashes. - """ - n = 0 - if use_hashes: - for hashing_alg in hashes(): - n += 1 - name = basename + hashing_alg - if extra is not None: - name += extra - MECHANISMS[name] = impl - MECH_SEC_SCORES[name] = basescore + n - else: - MECHANISMS[basename] = impl - MECH_SEC_SCORES[basename] = basescore - - -def set_stash_file(filename): - """ - Enable or disable storing the stash to disk. - - If the filename is ``None``, then disable using a stash file. - - :param filename: The path to the file to store the stash data. - """ - SESSION['stash_file'] = filename - try: - import marshal - stash_file = file(filename) - SESSION['stash'] = marshal.load(stash_file) - except: - SESSION['stash'] = {} - - -def sec_query_allow(mech, query): - """ - Quick default to allow all feature combinations which could - negatively affect security. - - :param mech: The chosen SASL mechanism - :param query: An encoding of the combination of enabled and - disabled features which may affect security. - - :returns: ``True`` - """ - return True - - -class SASL(object): - - """ - """ - - def __init__(self, host, service, mech=None, username=None, - min_sec=0, request_values=None, sec_query=None, - tls_active=None, def_realm=None): - """ - :param string host: The host of the service requiring authentication. - :param string service: The name of the underlying protocol in use. - :param string mech: Optional name of the SASL mechanism to use. - If given, only this mechanism may be used for - authentication. - :param string username: The username to use when authenticating. - :param request_values: Reference to a function for supplying - values requested by mechanisms, such - as passwords. (See above) - :param sec_query: Reference to a function for approving or - denying feature combinations which could - negatively impact security. (See above) - :param tls_active: Function for indicating if TLS has been - negotiated. (See above) - :param integer min_sec: The minimum security level accepted. This - only allows for SASL mechanisms whose - security rating is greater than `min_sec`. - :param string def_realm: The default realm, if different than `host`. - - :type request_values: :func:`request_values` - :type sec_query: :func:`sec_query` - :type tls_active: :func:`tls_active` - """ - self.host = host - self.def_realm = def_realm or host - self.service = service - self.user = username - self.mech = mech - self.min_sec = min_sec - 1 - - self.request_values = request_values - self._sec_query = sec_query - if tls_active is not None: - self.tls_active = tls_active - else: - self.tls_active = lambda: False - - self.try_username = self.user - self.try_password = None - - self.stash_id = None - self.testkey = None - - def reset_stash_id(self, username): - """ - Reset the ID for the stash for persisting user data. - - :param username: The username to base the new ID on. - """ - username = saslprep(username) - self.user = username - self.try_username = self.user - self.testkey = [self.user, self.host, self.service] - self.stash_id = '\0'.join(self.testkey) - - def sec_query(self, mech, query): - """ - Request authorization from the user to use a combination - of features which could negatively affect security. - - The ``sec_query`` callback when creating the SASL object will - be called if the query has not been answered before. Otherwise, - the query response will be pulled from ``SESSION['sec_queries']``. - - If no ``sec_query`` callback was provided, then all queries - will be denied. - - :param mech: The chosen SASL mechanism - :param query: An encoding of the combination of enabled and - disabled features which may affect security. - :rtype: bool - """ - if self._sec_query is None: - return False - if query in SESSION['sec_queries']: - return SESSION['sec_queries'][query] - resp = self._sec_query(mech, query) - if resp: - SESSION['sec_queries'][query] = resp - - return resp - - def find_password(self, mech): - """ - Find and return the user's password, if it has been entered before - during this session. - - :param mech: The chosen SASL mechanism. - """ - if self.try_password is not None: - return self.try_password - if self.testkey is None: - return - - testkey = self.testkey[:] - lockout = 1 - - def find_username(self): - """Find and return user's username if known.""" - return self.try_username - - def success(self, mech): - mech.preprep() - if 'password' in mech.values: - testkey = self.testkey[:] - while len(testkey): - tk = '\0'.join(testkey) - if tk in SESSION['passwords']: - break - SESSION['passwords'][tk] = mech.values['password'] - testkey = testkey[:-1] - mech.prep() - mech.save_values() - - def failure(self, mech): - mech.clear() - self.testkey = self.testkey[:-1] - - def choose_mechanism(self, mechs, force_plain=False): - """ - Choose the most secure mechanism from a list of mechanisms. - - If ``force_plain`` is given, return the ``PLAIN`` mechanism. - - :param mechs: A list of mechanism names. - :param force_plain: If ``True``, force the selection of the - ``PLAIN`` mechanism. - :returns: A SASL mechanism object, or ``None`` if no mechanism - could be selected. - """ - # Handle selection of PLAIN and ANONYMOUS - if force_plain: - return MECHANISMS['PLAIN'](self, 'PLAIN') - - if self.user is not None: - requested_mech = '*' if self.mech is None else self.mech - else: - if self.mech is None: - requested_mech = 'ANONYMOUS' - else: - requested_mech = self.mech - if requested_mech == '*' and self.user in ['', 'anonymous', None]: - requested_mech = 'ANONYMOUS' - - # If a specific mechanism was requested, try it - if requested_mech != '*': - if requested_mech in MECHANISMS and \ - requested_mech in MECH_SEC_SCORES: - return MECHANISMS[requested_mech](self, requested_mech) - return None - - # Pick the best mechanism based on its security score - best_score = self.min_sec - best_mech = None - for name in mechs: - if name in MECH_SEC_SCORES: - if MECH_SEC_SCORES[name] > best_score: - best_score = MECH_SEC_SCORES[name] - best_mech = name - if best_mech is not None: - best_mech = MECHANISMS[best_mech](self, best_mech) - - return best_mech - - -class Mechanism(object): - - """ - """ - - def __init__(self, sasl, name, version=0, use_stash=True): - self.name = name - self.sasl = sasl - self.use_stash = use_stash - - self.encoding = False - self.values = {} - - if use_stash: - self.load_values() - - def load_values(self): - """Retrieve user data from the stash.""" - self.values = {} - if not self.use_stash: - return False - if self.sasl.stash_id is not None: - if self.sasl.stash_id in SESSION['stash']: - if SESSION['stash'][self.sasl.stash_id]['mech'] == self.name: - values = SESSION['stash'][self.sasl.stash_id]['values'] - self.values.update(values) - if self.sasl.user is not None: - if not self.has_values(['username']): - self.values['username'] = self.sasl.user - return None - - def save_values(self): - """ - Save user data to the session stash. - - If a stash file name has been set using ``SESSION['stash_file']``, - the saved values will be persisted to disk. - """ - if not self.use_stash: - return False - if self.sasl.stash_id is not None: - if self.sasl.stash_id not in SESSION['stash']: - SESSION['stash'][self.sasl.stash_id] = {} - SESSION['stash'][self.sasl.stash_id]['values'] = self.values - SESSION['stash'][self.sasl.stash_id]['mech'] = self.name - if SESSION['stash_file'] not in ['', None]: - import marshal - stash_file = file(SESSION['stash_file'], 'wb') - marshal.dump(SESSION['stash'], stash_file) - - def clear(self): - """Reset all user data, except the username.""" - username = None - if 'username' in self.values: - username = self.values['username'] - self.values = {} - if username is not None: - self.values['username'] = username - self.save_values() - self.values = {} - self.load_values() - - def okay(self): - """ - Indicate if mutual authentication has completed successfully. - - :rtype: bool - """ - return False - - def preprep(self): - """Ensure that the stash ID has been set before processing.""" - if self.sasl.stash_id is None: - if 'username' in self.values: - self.sasl.reset_stash_id(self.values['username']) - - def prep(self): - """ - Prepare stored values for processing. - - For example, by removing extra copies of passwords from memory. - """ - pass - - def process(self, challenge=None): - """ - Process a challenge request and return the response. - - :param challenge: A challenge issued by the server that - must be answered for authentication. - """ - raise NotImplemented - - def fulfill(self, values): - """ - Provide requested values to the mechanism. - - :param values: A dictionary of requested values. - """ - if 'password' in values: - values['password'] = saslprep(values['password']) - self.values.update(values) - - def missing_values(self, keys): - """ - Return a dictionary of value names that have not been given values - by the user, or retrieved from the stash. - - :param keys: A list of value names to check. - :rtype: dict - """ - vals = {} - for name in keys: - if name not in self.values or self.values[name] is None: - if self.use_stash: - if name == 'username': - value = self.sasl.find_username() - if value is not None: - self.sasl.reset_stash_id(value) - self.values[name] = value - break - if name == 'password': - value = self.sasl.find_password(self) - if value is not None: - self.values[name] = value - break - vals[name] = None - return vals - - def has_values(self, keys): - """ - Check that the given values have been retrieved from the user, - or from the stash. - - :param keys: A list of value names to check. - """ - return len(self.missing_values(keys)) == 0 - - def check_values(self, keys): - """ - Request missing values from the user. - - :param keys: A list of value names to request, if missing. - """ - vals = self.missing_values(keys) - if vals: - self.sasl.request_values(self, vals) - - def get_user(self): - """Return the username usd for this mechanism.""" - return self.values['username'] diff --git a/sleekxmpp/thirdparty/suelta/saslprep.py b/sleekxmpp/thirdparty/suelta/saslprep.py deleted file mode 100644 index 0e72fcb1..00000000 --- a/sleekxmpp/thirdparty/suelta/saslprep.py +++ /dev/null @@ -1,81 +0,0 @@ -from __future__ import unicode_literals - -import sys -import stringprep -import unicodedata - - -from sleekxmpp.thirdparty.suelta.exceptions import SASLPrepFailure - - -def saslprep(text, strict=True): - """ - Return a processed version of the given string, using the SASLPrep - profile of stringprep. - - :param text: The string to process, in UTF-8. - :param strict: If ``True``, prevent the use of unassigned code points. - """ - - if sys.version_info < (3, 0): - if type(text) == str: - text = text.decode('utf-8') - - # Mapping: - # - # - non-ASCII space characters [StringPrep, C.1.2] that can be - # mapped to SPACE (U+0020), and - # - # - the 'commonly mapped to nothing' characters [StringPrep, B.1] - # that can be mapped to nothing. - buffer = '' - for char in text: - if stringprep.in_table_c12(char): - buffer += ' ' - elif not stringprep.in_table_b1(char): - buffer += char - - # Normalization using form KC - text = unicodedata.normalize('NFKC', buffer) - - # Check for bidirectional string - buffer = '' - first_is_randal = False - if text: - first_is_randal = stringprep.in_table_d1(text[0]) - if first_is_randal and not stringprep.in_table_d1(text[-1]): - raise SASLPrepFailure('Section 6.3 [end]') - - # Check for prohibited characters - for x in range(len(text)): - if strict and stringprep.in_table_a1(text[x]): - raise SASLPrepFailure('Unassigned Codepoint') - if stringprep.in_table_c12(text[x]): - raise SASLPrepFailure('In table C.1.2') - if stringprep.in_table_c21(text[x]): - raise SASLPrepFailure('In table C.2.1') - if stringprep.in_table_c22(text[x]): - raise SASLPrepFailure('In table C.2.2') - if stringprep.in_table_c3(text[x]): - raise SASLPrepFailure('In table C.3') - if stringprep.in_table_c4(text[x]): - raise SASLPrepFailure('In table C.4') - if stringprep.in_table_c5(text[x]): - raise SASLPrepFailure('In table C.5') - if stringprep.in_table_c6(text[x]): - raise SASLPrepFailure('In table C.6') - if stringprep.in_table_c7(text[x]): - raise SASLPrepFailure('In table C.7') - if stringprep.in_table_c8(text[x]): - raise SASLPrepFailure('In table C.8') - if stringprep.in_table_c9(text[x]): - raise SASLPrepFailure('In table C.9') - if x: - if first_is_randal and stringprep.in_table_d2(text[x]): - raise SASLPrepFailure('Section 6.2') - if not first_is_randal and \ - x != len(text) - 1 and \ - stringprep.in_table_d1(text[x]): - raise SASLPrepFailure('Section 6.3') - - return text diff --git a/sleekxmpp/util/__init__.py b/sleekxmpp/util/__init__.py new file mode 100644 index 00000000..47a935af --- /dev/null +++ b/sleekxmpp/util/__init__.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" + sleekxmpp.util + ~~~~~~~~~~~~~~ + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout + :license: MIT, see LICENSE for more details +""" + + +from sleekxmpp.util.misc_ops import bytes, unicode, hashes, hash, \ + num_to_bytes, bytes_to_num, quote, \ + XOR, safedict + + +# ===================================================================== +# Standardize import of Queue class: + +import sys + +def _gevent_threads_enabled(): + if not 'gevent' in sys.modules: + return False + try: + from gevent import thread as green_thread + thread = __import__('thread') + return thread.LockType is green_thread.LockType + except ImportError: + return False + +if _gevent_threads_enabled(): + import gevent.queue as queue + _queue = queue.JoinableQueue +else: + try: + import queue + except ImportError: + import Queue as queue + _queue = queue.Queue +class Queue(_queue): + def put(self, item, block=True, timeout=None): + if _queue.full(self): + _queue.get(self) + return _queue.put(self, item, block, timeout) + +QueueEmpty = queue.Empty diff --git a/sleekxmpp/thirdparty/suelta/util.py b/sleekxmpp/util/misc_ops.py index cd2439d5..18c919a8 100644 --- a/sleekxmpp/thirdparty/suelta/util.py +++ b/sleekxmpp/util/misc_ops.py @@ -1,10 +1,19 @@ -""" -""" - import sys import hashlib +def unicode(text): + if sys.version_info < (3, 0): + if isinstance(text, str): + text = text.decode('utf-8') + import __builtin__ + return __builtin__.unicode(text) + elif not isinstance(text, str): + return text.decode('utf-8') + else: + return text + + def bytes(text): """ Convert Unicode text to UTF-8 encoded bytes. @@ -119,3 +128,38 @@ def hashes(): t += ['MD2'] hashes = ['SHA-' + h[3:] for h in dir(hashlib) if h.startswith('sha')] return t + hashes + + +def setdefaultencoding(encoding): + """ + Set the current default string encoding used by the Unicode implementation. + + Actually calls sys.setdefaultencoding under the hood - see the docs for that + for more details. This method exists only as a way to call find/call it + even after it has been 'deleted' when the site module is executed. + + :param string encoding: An encoding name, compatible with sys.setdefaultencoding + """ + func = getattr(sys, 'setdefaultencoding', None) + if func is None: + import gc + import types + for obj in gc.get_objects(): + if (isinstance(obj, types.BuiltinFunctionType) + and obj.__name__ == 'setdefaultencoding'): + func = obj + break + if func is None: + raise RuntimeError("Could not find setdefaultencoding") + sys.setdefaultencoding = func + return func(encoding) + + +def safedict(data): + if sys.version_info < (2, 7): + safe = {} + for key in data: + safe[key.encode('utf8')] = data[key] + return safe + else: + return data diff --git a/sleekxmpp/util/sasl/__init__.py b/sleekxmpp/util/sasl/__init__.py new file mode 100644 index 00000000..2d344e9b --- /dev/null +++ b/sleekxmpp/util/sasl/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" + sleekxmpp.util.sasl + ~~~~~~~~~~~~~~~~~~~ + + This module was originally based on Dave Cridland's Suelta library. + + Part of SleekXMPP: The Sleek XMPP Library + + :copryight: (c) 2004-2013 David Alan Cridland + :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout + + :license: MIT, see LICENSE for more details +""" + +from sleekxmpp.util.sasl.client import * +from sleekxmpp.util.sasl.mechanisms import * diff --git a/sleekxmpp/util/sasl/client.py b/sleekxmpp/util/sasl/client.py new file mode 100644 index 00000000..fd685547 --- /dev/null +++ b/sleekxmpp/util/sasl/client.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +""" + sleekxmpp.util.sasl.client + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + This module was originally based on Dave Cridland's Suelta library. + + Part of SleekXMPP: The Sleek XMPP Library + + :copryight: (c) 2004-2013 David Alan Cridland + :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout + + :license: MIT, see LICENSE for more details +""" + +import logging +import stringprep + +from sleekxmpp.util import hashes, bytes, stringprep_profiles + + +log = logging.getLogger(__name__) + + +#: Global registry mapping mechanism names to implementation classes. +MECHANISMS = {} + + +#: Global registry mapping mechanism names to security scores. +MECH_SEC_SCORES = {} + + +#: The SASLprep profile of stringprep used to validate simple username +#: and password credentials. +saslprep = stringprep_profiles.create( + nfkc=True, + bidi=True, + mappings=[ + stringprep_profiles.b1_mapping, + stringprep_profiles.c12_mapping], + prohibited=[ + stringprep.in_table_c12, + stringprep.in_table_c21, + stringprep.in_table_c22, + stringprep.in_table_c3, + stringprep.in_table_c4, + stringprep.in_table_c5, + stringprep.in_table_c6, + stringprep.in_table_c7, + stringprep.in_table_c8, + stringprep.in_table_c9], + unassigned=[stringprep.in_table_a1]) + + +def sasl_mech(score): + sec_score = score + def register(mech): + n = 0 + mech.score = sec_score + if mech.use_hashes: + for hashing_alg in hashes(): + n += 1 + score = mech.score + n + name = '%s-%s' % (mech.name, hashing_alg) + MECHANISMS[name] = mech + MECH_SEC_SCORES[name] = score + + if mech.channel_binding: + name += '-PLUS' + score += 10 + MECHANISMS[name] = mech + MECH_SEC_SCORES[name] = score + else: + MECHANISMS[mech.name] = mech + MECH_SEC_SCORES[mech.name] = mech.score + if mech.channel_binding: + MECHANISMS[mech.name + '-PLUS'] = mech + MECH_SEC_SCORES[name] = mech.score + 10 + return mech + return register + + +class SASLNoAppropriateMechanism(Exception): + def __init__(self, value=''): + self.message = value + + +class SASLCancelled(Exception): + def __init__(self, value=''): + self.message = value + + +class SASLFailed(Exception): + def __init__(self, value=''): + self.message = value + + +class SASLMutualAuthFailed(SASLFailed): + def __init__(self, value=''): + self.message = value + + +class Mech(object): + + name = 'GENERIC' + score = -1 + use_hashes = False + channel_binding = False + required_credentials = set() + optional_credentials = set() + security = set() + + def __init__(self, name, credentials, security_settings): + self.credentials = credentials + self.security_settings = security_settings + self.values = {} + self.base_name = self.name + self.name = name + self.setup(name) + + def setup(self, name): + pass + + def process(self, challenge=b''): + return b'' + + +def choose(mech_list, credentials, security_settings, limit=None, min_mech=None): + available_mechs = set(MECHANISMS.keys()) + if limit is None: + limit = set(mech_list) + if not isinstance(limit, set): + limit = set(limit) + if not isinstance(mech_list, set): + mech_list = set(mech_list) + + mech_list = mech_list.intersection(limit) + available_mechs = available_mechs.intersection(mech_list) + + best_score = MECH_SEC_SCORES.get(min_mech, -1) + best_mech = None + for name in available_mechs: + if name in MECH_SEC_SCORES: + if MECH_SEC_SCORES[name] > best_score: + best_score = MECH_SEC_SCORES[name] + best_mech = name + if best_mech is None: + raise SASLNoAppropriateMechanism() + + mech_class = MECHANISMS[best_mech] + + try: + creds = credentials(mech_class.required_credentials, + mech_class.optional_credentials) + for req in mech_class.required_credentials: + if req not in creds: + raise SASLCancelled('Missing credential: %s' % req) + for opt in mech_class.optional_credentials: + if opt not in creds: + creds[opt] = b'' + for cred in creds: + if cred in ('username', 'password', 'authzid'): + creds[cred] = bytes(saslprep(creds[cred])) + else: + creds[cred] = bytes(creds[cred]) + security_opts = security_settings(mech_class.security) + + return mech_class(best_mech, creds, security_opts) + except SASLCancelled as e: + log.info('SASL: %s: %s', best_mech, e.message) + mech_list.remove(best_mech) + return choose(mech_list, credentials, security_settings, + limit=limit, + min_mech=min_mech) diff --git a/sleekxmpp/util/sasl/mechanisms.py b/sleekxmpp/util/sasl/mechanisms.py new file mode 100644 index 00000000..7a7ebf7b --- /dev/null +++ b/sleekxmpp/util/sasl/mechanisms.py @@ -0,0 +1,550 @@ +# -*- coding: utf-8 -*- +""" + sleekxmpp.util.sasl.mechanisms + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + A collection of supported SASL mechanisms. + + This module was originally based on Dave Cridland's Suelta library. + + Part of SleekXMPP: The Sleek XMPP Library + + :copryight: (c) 2004-2013 David Alan Cridland + :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout + + :license: MIT, see LICENSE for more details +""" + +import sys +import hmac +import random + +from base64 import b64encode, b64decode + +from sleekxmpp.util import bytes, hash, XOR, quote, num_to_bytes +from sleekxmpp.util.sasl.client import sasl_mech, Mech, \ + SASLCancelled, SASLFailed, \ + SASLMutualAuthFailed + + +@sasl_mech(0) +class ANONYMOUS(Mech): + + name = 'ANONYMOUS' + + def process(self, challenge=b''): + return b'Anonymous, Suelta' + + +@sasl_mech(1) +class LOGIN(Mech): + + name = 'LOGIN' + required_credentials = set(['username', 'password']) + + def setup(self, name): + self.step = 0 + + def process(self, challenge=b''): + if not challenge: + return b'' + + if self.step == 0: + self.step = 1 + return self.credentials['username'] + else: + return self.credentials['password'] + + +@sasl_mech(2) +class PLAIN(Mech): + + name = 'PLAIN' + required_credentials = set(['username', 'password']) + optional_credentials = set(['authzid']) + security = set(['encrypted', 'encrypted_plain', 'unencrypted_plain']) + + def setup(self, name): + if not self.security_settings['encrypted']: + if not self.security_settings['unencrypted_plain']: + raise SASLCancelled('PLAIN without encryption') + else: + if not self.security_settings['encrypted_plain']: + raise SASLCancelled('PLAIN with encryption') + + def process(self, challenge=b''): + authzid = self.credentials['authzid'] + authcid = self.credentials['username'] + password = self.credentials['password'] + return authzid + b'\x00' + authcid + b'\x00' + password + + +@sasl_mech(100) +class EXTERNAL(Mech): + + name = 'EXTERNAL' + optional_credentials = set(['authzid']) + + def process(self, challenge=b''): + return self.credentials['authzid'] + + +@sasl_mech(31) +class X_FACEBOOK_PLATFORM(Mech): + + name = 'X-FACEBOOK-PLATFORM' + required_credentials = set(['api_key', 'access_token']) + + def process(self, challenge=b''): + if challenge: + values = {} + for kv in challenge.split(b'&'): + key, value = kv.split(b'=') + values[key] = value + + resp_data = { + b'method': values[b'method'], + b'v': b'1.0', + b'call_id': b'1.0', + b'nonce': values[b'nonce'], + b'access_token': self.credentials['access_token'], + b'api_key': self.credentials['api_key'] + } + + resp = '&'.join(['%s=%s' % (k.decode("utf-8"), v.decode("utf-8")) for k, v in resp_data.items()]) + return bytes(resp) + return b'' + + +@sasl_mech(10) +class X_MESSENGER_OAUTH2(Mech): + + name = 'X-MESSENGER-OAUTH2' + required_credentials = set(['access_token']) + + def process(self, challenge=b''): + return self.credentials['access_token'] + + +@sasl_mech(10) +class X_OAUTH2(Mech): + + name = 'X-OAUTH2' + required_credentials = set(['username', 'access_token']) + + def process(self, challenge=b''): + return b'\x00' + self.credentials['username'] + \ + b'\x00' + self.credentials['access_token'] + + +@sasl_mech(3) +class X_GOOGLE_TOKEN(Mech): + + name = 'X-GOOGLE-TOKEN' + required_credentials = set(['email', 'access_token']) + + def process(self, challenge=b''): + email = self.credentials['email'] + token = self.credentials['access_token'] + return b'\x00' + email + b'\x00' + token + + +@sasl_mech(20) +class CRAM(Mech): + + name = 'CRAM' + use_hashes = True + required_credentials = set(['username', 'password']) + security = set(['encrypted', 'unencrypted_cram']) + + def setup(self, name): + self.hash_name = name[5:] + self.hash = hash(self.hash_name) + if self.hash is None: + raise SASLCancelled('Unknown hash: %s' % self.hash_name) + if not self.security_settings['encrypted']: + if not self.security_settings['unencrypted_cram']: + raise SASLCancelled('Unecrypted CRAM-%s' % self.hash_name) + + def process(self, challenge=b''): + if not challenge: + return None + + username = self.credentials['username'] + password = self.credentials['password'] + + mac = hmac.HMAC(key=password, digestmod=self.hash) + mac.update(challenge) + + return username + b' ' + bytes(mac.hexdigest()) + + +@sasl_mech(60) +class SCRAM(Mech): + + name = 'SCRAM' + use_hashes = True + channel_binding = True + required_credentials = set(['username', 'password']) + optional_credentials = set(['authzid', 'channel_binding']) + security = set(['encrypted', 'unencrypted_scram']) + + def setup(self, name): + self.use_channel_binding = False + if name[-5:] == '-PLUS': + name = name[:-5] + self.use_channel_binding = True + + self.hash_name = name[6:] + self.hash = hash(self.hash_name) + + if self.hash is None: + raise SASLCancelled('Unknown hash: %s' % self.hash_name) + if not self.security_settings['encrypted']: + if not self.security_settings['unencrypted_scram']: + raise SASLCancelled('Unencrypted SCRAM') + + self.step = 0 + self._mutual_auth = False + + def HMAC(self, key, msg): + return hmac.HMAC(key=key, msg=msg, digestmod=self.hash).digest() + + def Hi(self, text, salt, iterations): + text = bytes(text) + ui1 = self.HMAC(text, salt + b'\0\0\0\01') + ui = ui1 + for i in range(iterations - 1): + ui1 = self.HMAC(text, ui1) + ui = XOR(ui, ui1) + return ui + + def H(self, text): + return self.hash(text).digest() + + def saslname(self, value): + value = value.decode("utf-8") + escaped = [] + for char in value: + if char == ',': + escaped += '=2C' + elif char == '=': + escaped += '=3D' + else: + escaped += char + return "".join(escaped).encode("utf-8") + + def parse(self, challenge): + items = {} + for key, value in [item.split(b'=', 1) for item in challenge.split(b',')]: + items[key] = value + return items + + def process(self, challenge=b''): + steps = [self.process_1, self.process_2, self.process_3] + return steps[self.step](challenge) + + def process_1(self, challenge): + self.step = 1 + data = {} + + self.cnonce = bytes(('%s' % random.random())[2:]) + + gs2_cbind_flag = b'n' + if self.credentials['channel_binding']: + if self.use_channel_binding: + gs2_cbind_flag = b'p=tls-unique' + else: + gs2_cbind_flag = b'y' + + authzid = b'' + if self.credentials['authzid']: + authzid = b'a=' + self.saslname(self.credentials['authzid']) + + self.gs2_header = gs2_cbind_flag + b',' + authzid + b',' + + nonce = b'r=' + self.cnonce + username = b'n=' + self.saslname(self.credentials['username']) + + self.client_first_message_bare = username + b',' + nonce + self.client_first_message = self.gs2_header + \ + self.client_first_message_bare + + return self.client_first_message + + def process_2(self, challenge): + self.step = 2 + + data = self.parse(challenge) + if b'm' in data: + raise SASLCancelled('Received reserved attribute.') + + salt = b64decode(data[b's']) + iteration_count = int(data[b'i']) + nonce = data[b'r'] + + if nonce[:len(self.cnonce)] != self.cnonce: + raise SASLCancelled('Invalid nonce') + + cbind_data = b'' + if self.use_channel_binding: + cbind_data = self.credentials['channel_binding'] + cbind_input = self.gs2_header + cbind_data + channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'') + + client_final_message_without_proof = channel_binding + b',' + \ + b'r=' + nonce + + salted_password = self.Hi(self.credentials['password'], + salt, + iteration_count) + client_key = self.HMAC(salted_password, b'Client Key') + stored_key = self.H(client_key) + auth_message = self.client_first_message_bare + b',' + \ + challenge + b',' + \ + client_final_message_without_proof + client_signature = self.HMAC(stored_key, auth_message) + client_proof = XOR(client_key, client_signature) + server_key = self.HMAC(salted_password, b'Server Key') + + self.server_signature = self.HMAC(server_key, auth_message) + + client_final_message = client_final_message_without_proof + \ + b',p=' + b64encode(client_proof) + + return client_final_message + + def process_3(self, challenge): + data = self.parse(challenge) + verifier = data.get(b'v', None) + error = data.get(b'e', 'Unknown error') + + if not verifier: + raise SASLFailed(error) + + if b64decode(verifier) != self.server_signature: + raise SASLMutualAuthFailed() + + self._mutual_auth = True + + return b'' + + +@sasl_mech(30) +class DIGEST(Mech): + + name = 'DIGEST' + use_hashes = True + required_credentials = set(['username', 'password', 'realm', 'service', 'host']) + optional_credentials = set(['authzid', 'service-name']) + security = set(['encrypted', 'unencrypted_digest']) + + def setup(self, name): + self.hash_name = name[7:] + self.hash = hash(self.hash_name) + if self.hash is None: + raise SASLCancelled('Unknown hash: %s' % self.hash_name) + if not self.security_settings['encrypted']: + if not self.security_settings['unencrypted_digest']: + raise SASLCancelled('Unencrypted DIGEST') + + self.qops = [b'auth'] + self.qop = b'auth' + self.maxbuf = b'65536' + self.nonce = b'' + self.cnonce = b'' + self.nonce_count = 1 + + def parse(self, challenge=b''): + data = {} + var_name = b'' + var_value = b'' + + # States: var, new_var, end, quote, escaped_quote + state = 'var' + + + for char in challenge: + if sys.version_info >= (3, 0): + char = bytes([char]) + + if state == 'var': + if char.isspace(): + continue + if char == b'=': + state = 'value' + else: + var_name += char + elif state == 'value': + if char == b'"': + state = 'quote' + elif char == b',': + if var_name: + data[var_name.decode('utf-8')] = var_value + var_name = b'' + var_value = b'' + state = 'var' + else: + var_value += char + elif state == 'escaped': + var_value += char + elif state == 'quote': + if char == b'\\': + state = 'escaped' + elif char == b'"': + state = 'end' + else: + var_value += char + else: + if char == b',': + if var_name: + data[var_name.decode('utf-8')] = var_value + var_name = b'' + var_value = b'' + state = 'var' + else: + var_value += char + + if var_name: + data[var_name.decode('utf-8')] = var_value + var_name = b'' + var_value = b'' + state = 'var' + return data + + def MAC(self, key, seq, msg): + mac = hmac.HMAC(key=key, digestmod=self.hash) + seqnum = num_to_bytes(seq) + mac.update(seqnum) + mac.update(msg) + return mac.digest()[:10] + b'\x00\x01' + seqnum + + def A1(self): + username = self.credentials['username'] + password = self.credentials['password'] + authzid = self.credentials['authzid'] + realm = self.credentials['realm'] + + a1 = self.hash() + a1.update(username + b':' + realm + b':' + password) + a1 = a1.digest() + a1 += b':' + self.nonce + b':' + self.cnonce + if authzid: + a1 += b':' + authzid + + return bytes(a1) + + def A2(self, prefix=b''): + a2 = prefix + b':' + self.digest_uri() + if self.qop in (b'auth-int', b'auth-conf'): + a2 += b':00000000000000000000000000000000' + return bytes(a2) + + def response(self, prefix=b''): + nc = bytes('%08x' % self.nonce_count) + + a1 = bytes(self.hash(self.A1()).hexdigest().lower()) + a2 = bytes(self.hash(self.A2(prefix)).hexdigest().lower()) + s = self.nonce + b':' + nc + b':' + self.cnonce + \ + b':' + self.qop + b':' + a2 + + return bytes(self.hash(a1 + b':' + s).hexdigest().lower()) + + def digest_uri(self): + serv_type = self.credentials['service'] + serv_name = self.credentials['service-name'] + host = self.credentials['host'] + + uri = serv_type + b'/' + host + if serv_name and host != serv_name: + uri += b'/' + serv_name + return uri + + def respond(self): + data = { + 'username': quote(self.credentials['username']), + 'authzid': quote(self.credentials['authzid']), + 'realm': quote(self.credentials['realm']), + 'nonce': quote(self.nonce), + 'cnonce': quote(self.cnonce), + 'nc': bytes('%08x' % self.nonce_count), + 'qop': self.qop, + 'digest-uri': quote(self.digest_uri()), + 'response': self.response(b'AUTHENTICATE'), + 'maxbuf': self.maxbuf, + 'charset': 'utf-8' + } + resp = b'' + for key, value in data.items(): + if value and value != b'""': + resp += b',' + bytes(key) + b'=' + bytes(value) + return resp[1:] + + def process(self, challenge=b''): + if not challenge: + if self.cnonce and self.nonce and self.nonce_count and self.qop: + self.nonce_count += 1 + return self.respond() + return None + + data = self.parse(challenge) + if 'rspauth' in data: + if data['rspauth'] != self.response(): + raise SASLMutualAuthFailed() + else: + self.nonce_count = 1 + self.cnonce = bytes('%s' % random.random())[2:] + self.qops = data.get('qop', [b'auth']) + self.qop = b'auth' + if 'nonce' in data: + self.nonce = data['nonce'] + if 'realm' in data and not self.credentials['realm']: + self.credentials['realm'] = data['realm'] + + return self.respond() + + +try: + import kerberos +except ImportError: + pass +else: + @sasl_mech(75) + class GSSAPI(Mech): + + name = 'GSSAPI' + required_credentials = set(['username', 'service-name']) + optional_credentials = set(['authzid']) + + def setup(self, name): + authzid = self.credentials['authzid'] + if not authzid: + authzid = 'xmpp@%s' % self.credentials['service-name'] + + _, self.gss = kerberos.authGSSClientInit(authzid) + self.step = 0 + + def process(self, challenge=b''): + b64_challenge = b64encode(challenge) + try: + if self.step == 0: + result = kerberos.authGSSClientStep(self.gss, b64_challenge) + if result != kerberos.AUTH_GSS_CONTINUE: + self.step = 1 + elif not challenge: + kerberos.authGSSClientClean(self.gss) + return b'' + elif self.step == 1: + username = self.credentials['username'] + + kerberos.authGSSClientUnwrap(self.gss, b64_challenge) + resp = kerberos.authGSSClientResponse(self.gss) + kerberos.authGSSClientWrap(self.gss, resp, username) + + resp = kerberos.authGSSClientResponse(self.gss) + except kerberos.GSSError as e: + raise SASLCancelled('Kerberos error: %s' % e) + if not resp: + return b'' + else: + return b64decode(resp) diff --git a/sleekxmpp/util/stringprep_profiles.py b/sleekxmpp/util/stringprep_profiles.py new file mode 100644 index 00000000..84326bc3 --- /dev/null +++ b/sleekxmpp/util/stringprep_profiles.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +""" + sleekxmpp.util.stringprep_profiles + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + This module makes it easier to define profiles of stringprep, + such as nodeprep and resourceprep for JID validation, and + SASLprep for SASL. + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout + :license: MIT, see LICENSE for more details +""" + + +from __future__ import unicode_literals + +import stringprep +from unicodedata import ucd_3_2_0 as unicodedata + +from sleekxmpp.util import unicode + + +class StringPrepError(UnicodeError): + pass + + +def b1_mapping(char): + """Map characters that are commonly mapped to nothing.""" + return '' if stringprep.in_table_b1(char) else None + + +def c12_mapping(char): + """Map non-ASCII whitespace to spaces.""" + return ' ' if stringprep.in_table_c12(char) else None + + +def map_input(data, tables=None): + """ + Each character in the input stream MUST be checked against + a mapping table. + """ + result = [] + for char in data: + replacement = None + + for mapping in tables: + replacement = mapping(char) + if replacement is not None: + break + + if replacement is None: + replacement = char + result.append(replacement) + return ''.join(result) + + +def normalize(data, nfkc=True): + """ + A profile can specify one of two options for Unicode normalization: + - no normalization + - Unicode normalization with form KC + """ + if nfkc: + data = unicodedata.normalize('NFKC', data) + return data + + +def prohibit_output(data, tables=None): + """ + Before the text can be emitted, it MUST be checked for prohibited + code points. + """ + for char in data: + for check in tables: + if check(char): + raise StringPrepError("Prohibited code point: %s" % char) + + +def check_bidi(data): + """ + 1) The characters in section 5.8 MUST be prohibited. + + 2) If a string contains any RandALCat character, the string MUST NOT + contain any LCat character. + + 3) If a string contains any RandALCat character, a RandALCat + character MUST be the first character of the string, and a + RandALCat character MUST be the last character of the string. + """ + if not data: + return data + + has_lcat = False + has_randal = False + + for c in data: + if stringprep.in_table_c8(c): + raise StringPrepError("BIDI violation: seciton 6 (1)") + if stringprep.in_table_d1(c): + has_randal = True + elif stringprep.in_table_d2(c): + has_lcat = True + + if has_randal and has_lcat: + raise StringPrepError("BIDI violation: section 6 (2)") + + first_randal = stringprep.in_table_d1(data[0]) + last_randal = stringprep.in_table_d1(data[-1]) + if has_randal and not (first_randal and last_randal): + raise StringPrepError("BIDI violation: section 6 (3)") + + +def create(nfkc=True, bidi=True, mappings=None, + prohibited=None, unassigned=None): + """Create a profile of stringprep. + + :param bool nfkc: + If `True`, perform NFKC Unicode normalization. Defaults to `True`. + :param bool bidi: + If `True`, perform bidirectional text checks. Defaults to `True`. + :param list mappings: + Optional list of functions for mapping characters to + suitable replacements. + :param list prohibited: + Optional list of functions which check for the presence of + prohibited characters. + :param list unassigned: + Optional list of functions for detecting the use of unassigned + code points. + + :raises: StringPrepError + :return: Unicode string of the resulting text passing the + profile's requirements. + """ + def profile(data, query=False): + try: + data = unicode(data) + except UnicodeError: + raise StringPrepError + + data = map_input(data, mappings) + data = normalize(data, nfkc) + prohibit_output(data, prohibited) + if bidi: + check_bidi(data) + if query and unassigned: + check_unassigned(data, unassigned) + return data + return profile diff --git a/sleekxmpp/version.py b/sleekxmpp/version.py index eb39fd68..acea9334 100644 --- a/sleekxmpp/version.py +++ b/sleekxmpp/version.py @@ -9,5 +9,5 @@ # We don't want to have to import the entire library # just to get the version info for setup.py -__version__ = '1.1.8' -__version_info__ = (1, 1, 8, '', 0) +__version__ = '1.4.0' +__version_info__ = (1, 4, 0, '', 0) diff --git a/sleekxmpp/xmlstream/__init__.py b/sleekxmpp/xmlstream/__init__.py index 67b20c56..5a1ea1be 100644 --- a/sleekxmpp/xmlstream/__init__.py +++ b/sleekxmpp/xmlstream/__init__.py @@ -6,7 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.xmlstream.jid import JID +from sleekxmpp.jid import JID from sleekxmpp.xmlstream.scheduler import Scheduler from sleekxmpp.xmlstream.stanzabase import StanzaBase, ElementBase, ET from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin diff --git a/sleekxmpp/xmlstream/cert.py b/sleekxmpp/xmlstream/cert.py index 339f872d..71146f36 100644 --- a/sleekxmpp/xmlstream/cert.py +++ b/sleekxmpp/xmlstream/cert.py @@ -1,6 +1,10 @@ import logging from datetime import datetime, timedelta +# Make a call to strptime before starting threads to +# prevent thread safety issues. +datetime.strptime('1970-01-01 12:00:00', "%Y-%m-%d %H:%M:%S") + try: from pyasn1.codec.der import decoder, encoder @@ -94,7 +98,7 @@ def extract_names(raw_cert): def extract_dates(raw_cert): if not HAVE_PYASN1: - log.warning("Could not find pyasn1 module. " + \ + log.warning("Could not find pyasn1 and pyasn1_modules. " + \ "SSL certificate expiration COULD NOT BE VERIFIED.") return None, None @@ -130,7 +134,7 @@ def get_ttl(raw_cert): def verify(expected, raw_cert): if not HAVE_PYASN1: - log.warning("Could not find pyasn1 module. " + \ + log.warning("Could not find pyasn1 and pyasn1_modules. " + \ "SSL certificate COULD NOT BE VERIFIED.") return @@ -147,7 +151,10 @@ def verify(expected, raw_cert): raise CertificateError( 'Certificate has expired.') - expected_wild = expected[expected.index('.'):] + if '.' in expected: + expected_wild = expected[expected.index('.'):] + else: + expected_wild = expected expected_srv = '_xmpp-client.%s' % expected for name in cert_names['XMPPAddr']: @@ -160,7 +167,10 @@ def verify(expected, raw_cert): if name == expected: return True if name.startswith('*'): - name_wild = name[name.index('.'):] + if '.' in name: + name_wild = name[name.index('.'):] + else: + name_wild = name if expected_wild == name_wild: return True for name in cert_names['URI']: diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py index 56554c73..53b83bc7 100644 --- a/sleekxmpp/xmlstream/filesocket.py +++ b/sleekxmpp/xmlstream/filesocket.py @@ -13,6 +13,7 @@ """ from socket import _fileobject +import errno import socket @@ -29,12 +30,18 @@ class FileSocket(_fileobject): """Read data from the socket as if it were a file.""" if self._sock is None: return None - data = self._sock.recv(size) + while True: + try: + data = self._sock.recv(size) + break + except socket.error as serr: + if serr.errno != errno.EINTR: + raise if data is not None: return data -class Socket26(socket._socketobject): +class Socket26(socket.socket): """A custom socket implementation that uses our own FileSocket class to work around issues in Python 2.6 when using sockets as files. diff --git a/sleekxmpp/xmlstream/handler/__init__.py b/sleekxmpp/xmlstream/handler/__init__.py index 7bcf0b71..83c87f01 100644 --- a/sleekxmpp/xmlstream/handler/__init__.py +++ b/sleekxmpp/xmlstream/handler/__init__.py @@ -7,6 +7,7 @@ """ from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.handler.collector import Collector from sleekxmpp.xmlstream.handler.waiter import Waiter from sleekxmpp.xmlstream.handler.xmlcallback import XMLCallback from sleekxmpp.xmlstream.handler.xmlwaiter import XMLWaiter diff --git a/sleekxmpp/xmlstream/handler/collector.py b/sleekxmpp/xmlstream/handler/collector.py new file mode 100644 index 00000000..8f02f8c3 --- /dev/null +++ b/sleekxmpp/xmlstream/handler/collector.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" + sleekxmpp.xmlstream.handler.collector + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout + :license: MIT, see LICENSE for more details +""" + +import logging + +from sleekxmpp.util import Queue, QueueEmpty +from sleekxmpp.xmlstream.handler.base import BaseHandler + + +log = logging.getLogger(__name__) + + +class Collector(BaseHandler): + + """ + The Collector handler allows for collecting a set of stanzas + that match a given pattern. Unlike the Waiter handler, a + Collector does not block execution, and will continue to + accumulate matching stanzas until told to stop. + + :param string name: The name of the handler. + :param matcher: A :class:`~sleekxmpp.xmlstream.matcher.base.MatcherBase` + derived object for matching stanza objects. + :param stream: The :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` + instance this handler should monitor. + """ + + def __init__(self, name, matcher, stream=None): + BaseHandler.__init__(self, name, matcher, stream=stream) + self._payload = Queue() + + def prerun(self, payload): + """Store the matched stanza when received during processing. + + :param payload: The matched + :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object. + """ + self._payload.put(payload) + + def run(self, payload): + """Do not process this handler during the main event loop.""" + pass + + def stop(self): + """ + Stop collection of matching stanzas, and return the ones that + have been stored so far. + """ + self._destroy = True + results = [] + try: + while True: + results.append(self._payload.get(False)) + except QueueEmpty: + pass + + self.stream().remove_handler(self.name) + return results diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py index 899df17c..66e14496 100644 --- a/sleekxmpp/xmlstream/handler/waiter.py +++ b/sleekxmpp/xmlstream/handler/waiter.py @@ -10,11 +10,8 @@ """ import logging -try: - import queue -except ImportError: - import Queue as queue +from sleekxmpp.util import Queue, QueueEmpty from sleekxmpp.xmlstream.handler.base import BaseHandler @@ -37,7 +34,7 @@ class Waiter(BaseHandler): def __init__(self, name, matcher, stream=None): BaseHandler.__init__(self, name, matcher, stream=stream) - self._payload = queue.Queue() + self._payload = Queue() def prerun(self, payload): """Store the matched stanza when received during processing. @@ -74,7 +71,7 @@ class Waiter(BaseHandler): try: stanza = self._payload.get(True, 1) break - except queue.Empty: + except QueueEmpty: elapsed_time += 1 if elapsed_time >= timeout: log.warning("Timed out waiting for %s", self.name) diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py index 281bf4ee..2b59db47 100644 --- a/sleekxmpp/xmlstream/jid.py +++ b/sleekxmpp/xmlstream/jid.py @@ -1,145 +1,5 @@ -# -*- coding: utf-8 -*- -""" - sleekxmpp.xmlstream.jid - ~~~~~~~~~~~~~~~~~~~~~~~ +import logging - This module allows for working with Jabber IDs (JIDs) by - providing accessors for the various components of a JID. +logging.warning('Deprecated: sleekxmpp.xmlstream.jid is moving to sleekxmpp.jid') - Part of SleekXMPP: The Sleek XMPP Library - - :copyright: (c) 2011 Nathanael C. Fritz - :license: MIT, see LICENSE for more details -""" - -from __future__ import unicode_literals - - -class JID(object): - - """ - A representation of a Jabber ID, or JID. - - Each JID may have three components: a user, a domain, and an optional - resource. For example: user@domain/resource - - When a resource is not used, the JID is called a bare JID. - The JID is a full JID otherwise. - - **JID Properties:** - :jid: Alias for ``full``. - :full: The value of the full JID. - :bare: The value of the bare JID. - :user: The username portion of the JID. - :domain: The domain name portion of the JID. - :server: Alias for ``domain``. - :resource: The resource portion of the JID. - - :param string jid: A string of the form ``'[user@]domain[/resource]'``. - """ - - def __init__(self, jid): - """Initialize a new JID""" - self.reset(jid) - - def reset(self, jid): - """Start fresh from a new JID string. - - :param string jid: A string of the form ``'[user@]domain[/resource]'``. - """ - if isinstance(jid, JID): - jid = jid.full - self._full = self._jid = jid - self._domain = None - self._resource = None - self._user = None - self._bare = None - - def __getattr__(self, name): - """Handle getting the JID values, using cache if available. - - :param name: One of: user, server, domain, resource, - full, or bare. - """ - if name == 'resource': - if self._resource is None and '/' in self._jid: - self._resource = self._jid.split('/', 1)[-1] - return self._resource or "" - elif name == 'user': - if self._user is None: - if '@' in self._jid: - self._user = self._jid.split('@', 1)[0] - else: - self._user = self._user - return self._user or "" - elif name in ('server', 'domain', 'host'): - if self._domain is None: - self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0] - return self._domain or "" - elif name in ('full', 'jid'): - return self._jid or "" - elif name == 'bare': - if self._bare is None: - self._bare = self._jid.split('/', 1)[0] - return self._bare or "" - - def __setattr__(self, name, value): - """Edit a JID by updating it's individual values, resetting the - generated JID in the end. - - Arguments: - name -- The name of the JID part. One of: user, domain, - server, resource, full, jid, or bare. - value -- The new value for the JID part. - """ - if name in ('resource', 'user', 'domain'): - object.__setattr__(self, "_%s" % name, value) - self.regenerate() - elif name in ('server', 'domain', 'host'): - self.domain = value - elif name in ('full', 'jid'): - self.reset(value) - self.regenerate() - elif name == 'bare': - if '@' in value: - u, d = value.split('@', 1) - object.__setattr__(self, "_user", u) - object.__setattr__(self, "_domain", d) - else: - object.__setattr__(self, "_user", '') - object.__setattr__(self, "_domain", value) - self.regenerate() - else: - object.__setattr__(self, name, value) - - def regenerate(self): - """Generate a new JID based on current values, useful after editing.""" - jid = "" - if self.user: - jid = "%s@" % self.user - jid += self.domain - if self.resource: - jid += "/%s" % self.resource - self.reset(jid) - - def __str__(self): - """Use the full JID as the string value.""" - return self.full - - def __repr__(self): - return self.full - - def __eq__(self, other): - """ - Two JIDs are considered equal if they have the same full JID value. - """ - other = JID(other) - return self.full == other.full - - def __ne__(self, other): - """Two JIDs are considered unequal if they are not equal.""" - return not self == other - - def __hash__(self): - """Hash a JID based on the string version of its full JID.""" - return hash(self.full) +from sleekxmpp.jid import JID diff --git a/sleekxmpp/xmlstream/matcher/__init__.py b/sleekxmpp/xmlstream/matcher/__init__.py index 1038d1bd..aa74c434 100644 --- a/sleekxmpp/xmlstream/matcher/__init__.py +++ b/sleekxmpp/xmlstream/matcher/__init__.py @@ -7,6 +7,7 @@ """ from sleekxmpp.xmlstream.matcher.id import MatcherId +from sleekxmpp.xmlstream.matcher.idsender import MatchIDSender from sleekxmpp.xmlstream.matcher.many import MatchMany from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask diff --git a/sleekxmpp/xmlstream/matcher/idsender.py b/sleekxmpp/xmlstream/matcher/idsender.py new file mode 100644 index 00000000..5c2c1f51 --- /dev/null +++ b/sleekxmpp/xmlstream/matcher/idsender.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" + sleekxmpp.xmlstream.matcher.id + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details +""" + +from sleekxmpp.xmlstream.matcher.base import MatcherBase + + +class MatchIDSender(MatcherBase): + + """ + The IDSender matcher selects stanzas that have the same stanza 'id' + interface value as the desired ID, and that the 'from' value is one + of a set of approved entities that can respond to a request. + """ + + def match(self, xml): + """Compare the given stanza's ``'id'`` attribute to the stored + ``id`` value, and verify the sender's JID. + + :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + stanza to compare against. + """ + + selfjid = self._criteria['self'] + peerjid = self._criteria['peer'] + + allowed = {} + allowed[''] = True + allowed[selfjid.bare] = True + allowed[selfjid.host] = True + allowed[peerjid.full] = True + allowed[peerjid.bare] = True + allowed[peerjid.host] = True + + _from = xml['from'] + + try: + return xml['id'] == self._criteria['id'] and allowed[_from] + except KeyError: + return False diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index a0568f08..56f728e1 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -14,12 +14,6 @@ from sleekxmpp.xmlstream.stanzabase import ET from sleekxmpp.xmlstream.matcher.base import MatcherBase -# Flag indicating if the builtin XPath matcher should be used, which -# uses namespaces, or a custom matcher that ignores namespaces. -# Changing this will affect ALL XMLMask matchers. -IGNORE_NS = False - - log = logging.getLogger(__name__) @@ -39,19 +33,15 @@ class MatchXMLMask(MatcherBase): :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` should be used instead. - The use of namespaces in the mask comparison is controlled by - ``IGNORE_NS``. Setting ``IGNORE_NS`` to ``True`` will disable namespace - based matching for ALL XMLMask matchers. - :param criteria: Either an :class:`~xml.etree.ElementTree.Element` XML object or XML string to use as a mask. """ - def __init__(self, criteria): + def __init__(self, criteria, default_ns='jabber:client'): MatcherBase.__init__(self, criteria) if isinstance(criteria, str): self._criteria = ET.fromstring(self._criteria) - self.default_ns = 'jabber:client' + self.default_ns = default_ns def setDefaultNS(self, ns): """Set the default namespace to use during comparisons. @@ -84,8 +74,6 @@ class MatchXMLMask(MatcherBase): do not have a specified namespace. Defaults to ``"__no_ns__"``. """ - use_ns = not IGNORE_NS - if source is None: # If the element was not found. May happend during recursive calls. return False @@ -96,17 +84,10 @@ class MatchXMLMask(MatcherBase): mask = ET.fromstring(mask) except ExpatError: log.warning("Expat error: %s\nIn parsing: %s", '', mask) - if not use_ns: - # Compare the element without using namespaces. - source_tag = source.tag.split('}', 1)[-1] - mask_tag = mask.tag.split('}', 1)[-1] - if source_tag != mask_tag: - return False - else: - # Compare the element using namespaces - mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag) - if source.tag not in [mask.tag, mask_ns_tag]: - return False + + mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag) + if source.tag not in [mask.tag, mask_ns_tag]: + return False # If the mask includes text, compare it. if mask.text and source.text and \ @@ -122,37 +103,15 @@ class MatchXMLMask(MatcherBase): # Recursively check subelements. matched_elements = {} for subelement in mask: - if use_ns: - matched = False - for other in source.findall(subelement.tag): - matched_elements[other] = False - if self._mask_cmp(other, subelement, use_ns): - if not matched_elements.get(other, False): - matched_elements[other] = True - matched = True - if not matched: - return False - else: - if not self._mask_cmp(self._get_child(source, subelement.tag), - subelement, use_ns): - return False + matched = False + for other in source.findall(subelement.tag): + matched_elements[other] = False + if self._mask_cmp(other, subelement, use_ns): + if not matched_elements.get(other, False): + matched_elements[other] = True + matched = True + if not matched: + return False # Everything matches. return True - - def _get_child(self, xml, tag): - """Return a child element given its tag, ignoring namespace values. - - Returns ``None`` if the child was not found. - - :param xml: The :class:`~xml.etree.ElementTree.Element` XML object - to search for the given child tag. - :param tag: The name of the subelement to find. - """ - tag = tag.split('}')[-1] - try: - children = [c.tag.split('}')[-1] for c in xml] - index = children.index(tag) - except ValueError: - return None - return list(xml)[index] diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py index 3f03e68e..f3d28429 100644 --- a/sleekxmpp/xmlstream/matcher/xpath.py +++ b/sleekxmpp/xmlstream/matcher/xpath.py @@ -9,16 +9,10 @@ :license: MIT, see LICENSE for more details """ -from sleekxmpp.xmlstream.stanzabase import ET +from sleekxmpp.xmlstream.stanzabase import ET, fix_ns from sleekxmpp.xmlstream.matcher.base import MatcherBase -# Flag indicating if the builtin XPath matcher should be used, which -# uses namespaces, or a custom matcher that ignores namespaces. -# Changing this will affect ALL XPath matchers. -IGNORE_NS = False - - class MatchXPath(MatcherBase): """ @@ -38,6 +32,9 @@ class MatchXPath(MatcherBase): expressions will be matched without using namespaces. """ + def __init__(self, criteria): + self._criteria = fix_ns(criteria) + def match(self, xml): """ Compare a stanza's XML contents to an XPath expression. @@ -59,28 +56,4 @@ class MatchXPath(MatcherBase): x = ET.Element('x') x.append(xml) - if not IGNORE_NS: - # Use builtin, namespace respecting, XPath matcher. - if x.find(self._criteria) is not None: - return True - return False - else: - # Remove namespaces from the XPath expression. - criteria = [] - for ns_block in self._criteria.split('{'): - criteria.extend(ns_block.split('}')[-1].split('/')) - - # Walk the XPath expression. - xml = x - for tag in criteria: - if not tag: - # Skip empty tag name artifacts from the cleanup phase. - continue - - children = [c.tag.split('}')[-1] for c in xml] - try: - index = children.index(tag) - except ValueError: - return False - xml = list(xml)[index] - return True + return x.find(self._criteria) is not None diff --git a/sleekxmpp/xmlstream/resolver.py b/sleekxmpp/xmlstream/resolver.py index 0d7a8c0d..188e5ac7 100644 --- a/sleekxmpp/xmlstream/resolver.py +++ b/sleekxmpp/xmlstream/resolver.py @@ -32,10 +32,10 @@ log = logging.getLogger(__name__) #: cd dnspython #: git checkout python3 #: python3 setup.py install -USE_DNSPYTHON = False +DNSPYTHON_AVAILABLE = False try: import dns.resolver - USE_DNSPYTHON = True + DNSPYTHON_AVAILABLE = True except ImportError as e: log.debug("Could not find dnspython package. " + \ "Not all features will be available") @@ -47,13 +47,13 @@ def default_resolver(): :returns: A :class:`dns.resolver.Resolver` object if dnspython is available. Otherwise, ``None``. """ - if USE_DNSPYTHON: + if DNSPYTHON_AVAILABLE: return dns.resolver.get_default_resolver() return None def resolve(host, port=None, service=None, proto='tcp', - resolver=None, use_ipv6=True): + resolver=None, use_ipv6=True, use_dnspython=True): """Peform DNS resolution for a given hostname. Resolution may perform SRV record lookups if a service and protocol @@ -77,6 +77,9 @@ def resolve(host, port=None, service=None, proto='tcp', :param use_ipv6: Optionally control the use of IPv6 in situations where it is either not available, or performance is degraded. Defaults to ``True``. + :param use_dnspython: Optionally control if dnspython is used to make + the DNS queries instead of the built-in DNS + library. :type host: string :type port: int @@ -84,14 +87,22 @@ def resolve(host, port=None, service=None, proto='tcp', :type proto: string :type resolver: :class:`dns.resolver.Resolver` :type use_ipv6: bool + :type use_dnspython: bool :return: An iterable of IP address, port pairs in the order dictated by SRV priorities and weights, if applicable. """ + + if not use_dnspython: + if DNSPYTHON_AVAILABLE: + log.debug("DNS: Not using dnspython, but dnspython is installed.") + else: + log.debug("DNS: Not using dnspython.") + if not use_ipv6: log.debug("DNS: Use of IPv6 has been disabled.") - if resolver is None and USE_DNSPYTHON: + if resolver is None and DNSPYTHON_AVAILABLE and use_dnspython: resolver = dns.resolver.get_default_resolver() # An IPv6 literal is allowed to be enclosed in square brackets, but @@ -102,7 +113,7 @@ def resolve(host, port=None, service=None, proto='tcp', try: # If `host` is an IPv4 literal, we can return it immediately. ipv4 = socket.inet_aton(host) - yield (host, port) + yield (host, host, port) except socket.error: pass @@ -112,8 +123,8 @@ def resolve(host, port=None, service=None, proto='tcp', # it immediately. if hasattr(socket, 'inet_pton'): ipv6 = socket.inet_pton(socket.AF_INET6, host) - yield (host, port) - except socket.error: + yield (host, host, port) + except (socket.error, ValueError): pass # If no service was provided, then we can just do A/AAAA lookups on the @@ -122,25 +133,29 @@ def resolve(host, port=None, service=None, proto='tcp', if not service: hosts = [(host, port)] else: - hosts = get_SRV(host, port, service, proto, resolver=resolver) + hosts = get_SRV(host, port, service, proto, + resolver=resolver, + use_dnspython=use_dnspython) for host, port in hosts: results = [] if host == 'localhost': if use_ipv6: - results.append(('::1', port)) - results.append(('127.0.0.1', port)) + results.append((host, '::1', port)) + results.append((host, '127.0.0.1', port)) if use_ipv6: - for address in get_AAAA(host, resolver=resolver): - results.append((address, port)) - for address in get_A(host, resolver=resolver): - results.append((address, port)) + for address in get_AAAA(host, resolver=resolver, + use_dnspython=use_dnspython): + results.append((host, address, port)) + for address in get_A(host, resolver=resolver, + use_dnspython=use_dnspython): + results.append((host, address, port)) - for address, port in results: - yield address, port + for host, address, port in results: + yield host, address, port -def get_A(host, resolver=None): +def get_A(host, resolver=None, use_dnspython=True): """Lookup DNS A records for a given host. If ``resolver`` is not provided, or is ``None``, then resolution will @@ -148,9 +163,13 @@ def get_A(host, resolver=None): :param host: The hostname to resolve for A record IPv4 addresses. :param resolver: Optional DNS resolver object to use for the query. + :param use_dnspython: Optionally control if dnspython is used to make + the DNS queries instead of the built-in DNS + library. :type host: string :type resolver: :class:`dns.resolver.Resolver` or ``None`` + :type use_dnspython: bool :return: A list of IPv4 literals. """ @@ -158,7 +177,7 @@ def get_A(host, resolver=None): # If not using dnspython, attempt lookup using the OS level # getaddrinfo() method. - if resolver is None: + if resolver is None or not use_dnspython: try: recs = socket.getaddrinfo(host, None, socket.AF_INET, socket.SOCK_STREAM) @@ -183,7 +202,7 @@ def get_A(host, resolver=None): return [] -def get_AAAA(host, resolver=None): +def get_AAAA(host, resolver=None, use_dnspython=True): """Lookup DNS AAAA records for a given host. If ``resolver`` is not provided, or is ``None``, then resolution will @@ -191,9 +210,13 @@ def get_AAAA(host, resolver=None): :param host: The hostname to resolve for AAAA record IPv6 addresses. :param resolver: Optional DNS resolver object to use for the query. + :param use_dnspython: Optionally control if dnspython is used to make + the DNS queries instead of the built-in DNS + library. :type host: string :type resolver: :class:`dns.resolver.Resolver` or ``None`` + :type use_dnspython: bool :return: A list of IPv6 literals. """ @@ -201,12 +224,15 @@ def get_AAAA(host, resolver=None): # If not using dnspython, attempt lookup using the OS level # getaddrinfo() method. - if resolver is None: + if resolver is None or not use_dnspython: + if not socket.has_ipv6: + log.debug("Unable to query %s for AAAA records: IPv6 is not supported", host) + return [] try: recs = socket.getaddrinfo(host, None, socket.AF_INET6, socket.SOCK_STREAM) return [rec[4][0] for rec in recs] - except socket.gaierror: + except (OSError, socket.gaierror): log.debug("DNS: Error retreiving AAAA address " + \ "info for %s." % host) return [] @@ -227,7 +253,7 @@ def get_AAAA(host, resolver=None): return [] -def get_SRV(host, port, service, proto='tcp', resolver=None): +def get_SRV(host, port, service, proto='tcp', resolver=None, use_dnspython=True): """Perform SRV record resolution for a given host. .. note:: @@ -253,7 +279,7 @@ def get_SRV(host, port, service, proto='tcp', resolver=None): :return: A list of hostname, port pairs in the order dictacted by SRV priorities and weights. """ - if resolver is None: + if resolver is None or not use_dnspython: log.warning("DNS: dnspython not found. Can not use SRV lookup.") return [(host, port)] @@ -297,7 +323,10 @@ def get_SRV(host, port, service, proto='tcp', resolver=None): for running_sum in sums: if running_sum >= selected: rec = sums[running_sum] - sorted_recs.append((rec.target.to_text(), rec.port)) + host = rec.target.to_text() + if host.endswith('.'): + host = host[:-1] + sorted_recs.append((host, rec.port)) answers[priority].remove(rec) break diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py index f68af081..e6fae37a 100644 --- a/sleekxmpp/xmlstream/scheduler.py +++ b/sleekxmpp/xmlstream/scheduler.py @@ -15,10 +15,14 @@ import time import threading import logging -try: - import queue -except ImportError: - import Queue as queue +import itertools + +from sleekxmpp.util import Queue, QueueEmpty + + +#: The time in seconds to wait for events from the event queue, and also the +#: time between checks for the process stop signal. +WAIT_TIMEOUT = 1.0 log = logging.getLogger(__name__) @@ -77,7 +81,7 @@ class Task(object): """ if self.qpointer is not None: self.qpointer.put(('schedule', self.callback, - self.args, self.name)) + self.args, self.kwargs, self.name)) else: self.callback(*self.args, **self.kwargs) self.reset() @@ -102,7 +106,7 @@ class Scheduler(object): def __init__(self, parentstop=None): #: A queue for storing tasks - self.addq = queue.Queue() + self.addq = Queue() #: A list of tasks in order of execution time. self.schedule = [] @@ -121,6 +125,10 @@ class Scheduler(object): #: Lock for accessing the task queue. self.schedule_lock = threading.RLock() + #: The time in seconds to wait for events from the event queue, + #: and also the time between checks for the process stop signal. + self.wait_timeout = WAIT_TIMEOUT + def process(self, threaded=True, daemon=False): """Begin accepting and processing scheduled tasks. @@ -140,44 +148,50 @@ class Scheduler(object): self.run = True try: while self.run and not self.stop.is_set(): - wait = 0.1 updated = False if self.schedule: wait = self.schedule[0].next - time.time() + else: + wait = self.wait_timeout try: if wait <= 0.0: newtask = self.addq.get(False) else: - if wait >= 3.0: - wait = 3.0 newtask = None - elapsed = 0 - while not self.stop.is_set() and \ + while self.run and \ + not self.stop.is_set() and \ newtask is None and \ - elapsed < wait: - newtask = self.addq.get(True, 0.1) - elapsed += 0.1 - except queue.Empty: - cleanup = [] + wait > 0: + try: + newtask = self.addq.get(True, min(wait, self.wait_timeout)) + except QueueEmpty: # Nothing to add, nothing to do. Check run flags and continue waiting. + wait -= self.wait_timeout + except QueueEmpty: # Time to run some tasks, and no new tasks to add. self.schedule_lock.acquire() - for task in self.schedule: - if time.time() >= task.next: - updated = True - if not task.run(): - cleanup.append(task) + # select only those tasks which are to be executed now + relevant = itertools.takewhile( + lambda task: time.time() >= task.next, self.schedule) + # run the tasks and keep the return value in a tuple + status = map(lambda task: (task, task.run()), relevant) + # remove non-repeating tasks + for task, doRepeat in status: + if not doRepeat: + try: + self.schedule.remove(task) + except ValueError: + pass else: - break - for task in cleanup: - self.schedule.pop(self.schedule.index(task)) - else: - updated = True + # only need to resort tasks if a repeated task has + # been kept in the list. + updated = True + else: # Add new task self.schedule_lock.acquire() if newtask is not None: self.schedule.append(newtask) + updated = True finally: if updated: - self.schedule = sorted(self.schedule, - key=lambda task: task.next) + self.schedule.sort(key=lambda task: task.next) self.schedule_lock.release() except KeyboardInterrupt: self.run = False diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 4af441cc..11c8dd67 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -3,7 +3,7 @@ sleekxmpp.xmlstream.stanzabase ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - This module implements a wrapper layer for XML objects + module implements a wrapper layer for XML objects that allows them to be treated like dictionaries. Part of SleekXMPP: The Sleek XMPP Library @@ -19,6 +19,7 @@ import logging import weakref from xml.etree import cElementTree as ET +from sleekxmpp.util import safedict from sleekxmpp.xmlstream import JID from sleekxmpp.xmlstream.tostring import tostring from sleekxmpp.thirdparty import OrderedDict @@ -141,7 +142,7 @@ def multifactory(stanza, plugin_attrib): parent.loaded_plugins.remove(plugin_attrib) try: parent.xml.remove(self.xml) - except: + except ValueError: pass else: for stanza in list(res): @@ -192,7 +193,7 @@ def fix_ns(xpath, split=False, propagate_ns=True, default_ns=''): for element in elements: if element: # Skip empty entry artifacts from splitting. - if propagate_ns: + if propagate_ns and element[0] != '*': tag = '{%s}%s' % (namespace, element) else: tag = element @@ -488,7 +489,7 @@ class ElementBase(object): """ return self.init_plugin(attrib, lang) - def _get_plugin(self, name, lang=None): + def _get_plugin(self, name, lang=None, check=False): if lang is None: lang = self.get_lang() @@ -501,12 +502,12 @@ class ElementBase(object): if (name, None) in self.plugins: return self.plugins[(name, None)] else: - return self.init_plugin(name, lang) + return None if check else self.init_plugin(name, lang) else: if (name, lang) in self.plugins: return self.plugins[(name, lang)] else: - return self.init_plugin(name, lang) + return None if check else self.init_plugin(name, lang) def init_plugin(self, attrib, lang=None, existing_xml=None, reuse=True): """Enable and initialize a stanza plugin. @@ -514,8 +515,9 @@ class ElementBase(object): :param string attrib: The :attr:`plugin_attrib` value of the plugin to enable. """ - if lang is None: - lang = self.get_lang() + default_lang = self.get_lang() + if not lang: + lang = default_lang plugin_class = self.plugin_attrib_map[attrib] @@ -524,19 +526,13 @@ class ElementBase(object): if reuse and (attrib, lang) in self.plugins: return self.plugins[(attrib, lang)] - if existing_xml is None: - existing_xml = self.xml.find(plugin_class.tag_name()) - - if existing_xml is not None: - if existing_xml.attrib.get('{%s}lang' % XML_NS, '') != lang: - existing_xml = None - plugin = plugin_class(parent=self, xml=existing_xml) if plugin.is_extension: self.plugins[(attrib, None)] = plugin else: - plugin['lang'] = lang + if lang != default_lang: + plugin['lang'] = lang self.plugins[(attrib, lang)] = plugin if plugin_class in self.plugin_iterables: @@ -570,13 +566,16 @@ class ElementBase(object): values = {} values['lang'] = self['lang'] for interface in self.interfaces: - values[interface] = self[interface] + if isinstance(self[interface], JID): + values[interface] = self[interface].jid + else: + values[interface] = self[interface] if interface in self.lang_interfaces: values['%s|*' % interface] = self['%s|*' % interface] for plugin, stanza in self.plugins.items(): lang = stanza['lang'] if lang: - values['%s|%s' % (plugin, lang)] = stanza.values + values['%s|%s' % (plugin[0], lang)] = stanza.values else: values[plugin[0]] = stanza.values if self.iterables: @@ -601,31 +600,39 @@ class ElementBase(object): iterable_interfaces = [p.plugin_attrib for \ p in self.plugin_iterables] + if 'lang' in values: + self['lang'] = values['lang'] + + if 'substanzas' in values: + # Remove existing substanzas + for stanza in self.iterables: + try: + self.xml.remove(stanza.xml) + except ValueError: + pass + self.iterables = [] + + # Add new substanzas + for subdict in values['substanzas']: + if '__childtag__' in subdict: + for subclass in self.plugin_iterables: + child_tag = "{%s}%s" % (subclass.namespace, + subclass.name) + if subdict['__childtag__'] == child_tag: + sub = subclass(parent=self) + sub.values = subdict + self.iterables.append(sub) + for interface, value in values.items(): full_interface = interface interface_lang = ('%s|' % interface).split('|') interface = interface_lang[0] lang = interface_lang[1] or self.get_lang() - if interface == 'substanzas': - # Remove existing substanzas - for stanza in self.iterables: - self.xml.remove(stanza.xml) - self.iterables = [] - - # Add new substanzas - for subdict in value: - if '__childtag__' in subdict: - for subclass in self.plugin_iterables: - child_tag = "{%s}%s" % (subclass.namespace, - subclass.name) - if subdict['__childtag__'] == child_tag: - sub = subclass(parent=self) - sub.values = subdict - self.iterables.append(sub) - break - elif interface == 'lang': - self[interface] = value + if interface == 'lang': + continue + elif interface == 'substanzas': + continue elif interface in self.interfaces: self[full_interface] = value elif interface in self.plugin_attrib_map: @@ -667,12 +674,14 @@ class ElementBase(object): full_attrib = attrib attrib_lang = ('%s|' % attrib).split('|') attrib = attrib_lang[0] - lang = attrib_lang[1] or '' + lang = attrib_lang[1] or None kwargs = {} if lang and attrib in self.lang_interfaces: kwargs['lang'] = lang + kwargs = safedict(kwargs) + if attrib == 'substanzas': return self.iterables elif attrib in self.interfaces or attrib == 'lang': @@ -743,12 +752,14 @@ class ElementBase(object): full_attrib = attrib attrib_lang = ('%s|' % attrib).split('|') attrib = attrib_lang[0] - lang = attrib_lang[1] or '' + lang = attrib_lang[1] or None kwargs = {} if lang and attrib in self.lang_interfaces: kwargs['lang'] = lang + kwargs = safedict(kwargs) + if attrib in self.interfaces or attrib == 'lang': if value is not None: set_method = "set_%s" % attrib.lower() @@ -829,12 +840,14 @@ class ElementBase(object): full_attrib = attrib attrib_lang = ('%s|' % attrib).split('|') attrib = attrib_lang[0] - lang = attrib_lang[1] or '' + lang = attrib_lang[1] or None kwargs = {} if lang and attrib in self.lang_interfaces: kwargs['lang'] = lang + kwargs = safedict(kwargs) + if attrib in self.interfaces or attrib == 'lang': del_method = "del_%s" % attrib.lower() del_method2 = "del%s" % attrib.title() @@ -860,18 +873,18 @@ class ElementBase(object): else: self._del_attr(attrib) elif attrib in self.plugin_attrib_map: - plugin = self._get_plugin(attrib, lang) + plugin = self._get_plugin(attrib, lang, check=True) if not plugin: return self if plugin.is_extension: del plugin[full_attrib] del self.plugins[(attrib, None)] else: - del self.plugins[(attrib, lang)] + del self.plugins[(attrib, plugin['lang'])] self.loaded_plugins.remove(attrib) try: self.xml.remove(plugin.xml) - except: + except ValueError: pass return self @@ -1222,6 +1235,10 @@ class ElementBase(object): if item.__class__ in self.plugin_iterables: if item.__class__.plugin_multi_attrib: self.init_plugin(item.__class__.plugin_multi_attrib) + elif item.__class__ == self.plugin_tag_map.get(item.tag_name(), None): + self.init_plugin(item.plugin_attrib, + existing_xml=item.xml, + reuse=False) return self def appendxml(self, xml): @@ -1398,10 +1415,8 @@ class ElementBase(object): :param bool top_level_ns: Display the top-most namespace. Defaults to True. """ - stanza_ns = '' if top_level_ns else self.namespace return tostring(self.xml, xmlns='', - stanza_ns=stanza_ns, - top_level=not top_level_ns) + top_level=True) def __repr__(self): """Use the stanza's serialized XML as its representation.""" @@ -1590,11 +1605,10 @@ class StanzaBase(ElementBase): :param bool top_level_ns: Display the top-most namespace. Defaults to ``False``. """ - stanza_ns = '' if top_level_ns else self.namespace - return tostring(self.xml, xmlns='', - stanza_ns=stanza_ns, + xmlns = self.stream.default_ns if self.stream else '' + return tostring(self.xml, xmlns=xmlns, stream=self.stream, - top_level=not top_level_ns) + top_level=(self.stream is None)) #: A JSON/dictionary version of the XML content exposed through diff --git a/sleekxmpp/xmlstream/tostring.py b/sleekxmpp/xmlstream/tostring.py index 2480f9b2..c49abd3e 100644 --- a/sleekxmpp/xmlstream/tostring.py +++ b/sleekxmpp/xmlstream/tostring.py @@ -24,25 +24,25 @@ if sys.version_info < (3, 0): XML_NS = 'http://www.w3.org/XML/1998/namespace' -def tostring(xml=None, xmlns='', stanza_ns='', stream=None, - outbuffer='', top_level=False, open_only=False): +def tostring(xml=None, xmlns='', stream=None, outbuffer='', + top_level=False, open_only=False, namespaces=None): """Serialize an XML object to a Unicode string. - If namespaces are provided using ``xmlns`` or ``stanza_ns``, then - elements that use those namespaces will not include the xmlns attribute - in the output. + If an outer xmlns is provided using ``xmlns``, then the current element's + namespace will not be included if it matches the outer namespace. An + exception is made for elements that have an attached stream, and appear + at the stream root. :param XML xml: The XML object to serialize. :param string xmlns: Optional namespace of an element wrapping the XML object. - :param string stanza_ns: The namespace of the stanza object that contains - the XML object. :param stream: The XML stream that generated the XML object. :param string outbuffer: Optional buffer for storing serializations during recursive calls. :param bool top_level: Indicates that the element is the outermost element. - + :param set namespaces: Track which namespaces are in active use so + that new ones can be declared when needed. :type xml: :py:class:`~xml.etree.ElementTree.Element` :type stream: :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` @@ -63,15 +63,19 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, default_ns = '' stream_ns = '' + use_cdata = False + if stream: default_ns = stream.default_ns stream_ns = stream.stream_ns + use_cdata = stream.use_cdata # Output the tag name and derived namespace of the element. namespace = '' - if top_level and tag_xmlns not in ['', default_ns, stream_ns] or \ - tag_xmlns not in ['', xmlns, stanza_ns, stream_ns]: - namespace = ' xmlns="%s"' % tag_xmlns + if tag_xmlns: + if top_level and tag_xmlns not in [default_ns, xmlns, stream_ns] \ + or not top_level and tag_xmlns != xmlns: + namespace = ' xmlns="%s"' % tag_xmlns if stream and tag_xmlns in stream.namespace_map: mapped_namespace = stream.namespace_map[tag_xmlns] if mapped_namespace: @@ -80,21 +84,28 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, output.append(namespace) # Output escaped attribute values. + new_namespaces = set() for attrib, value in xml.attrib.items(): - value = xml_escape(value) + value = escape(value, use_cdata) if '}' not in attrib: output.append(' %s="%s"' % (attrib, value)) else: attrib_ns = attrib.split('}')[0][1:] attrib = attrib.split('}')[1] - if stream and attrib_ns in stream.namespace_map: + if attrib_ns == XML_NS: + output.append(' xml:%s="%s"' % (attrib, value)) + elif stream and attrib_ns in stream.namespace_map: mapped_ns = stream.namespace_map[attrib_ns] if mapped_ns: - output.append(' %s:%s="%s"' % (mapped_ns, - attrib, - value)) - elif attrib_ns == XML_NS: - output.append(' xml:%s="%s"' % (attrib, value)) + if namespaces is None: + namespaces = set() + if attrib_ns not in namespaces: + namespaces.add(attrib_ns) + new_namespaces.add(attrib_ns) + output.append(' xmlns:%s="%s"' % ( + mapped_ns, attrib_ns)) + output.append(' %s:%s="%s"' % ( + mapped_ns, attrib, value)) if open_only: # Only output the opening tag, regardless of content. @@ -105,24 +116,30 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, # If there are additional child elements to serialize. output.append(">") if xml.text: - output.append(xml_escape(xml.text)) + output.append(escape(xml.text, use_cdata)) if len(xml): for child in xml: - output.append(tostring(child, tag_xmlns, stanza_ns, stream)) + output.append(tostring(child, tag_xmlns, stream, + namespaces=namespaces)) output.append("</%s>" % tag_name) elif xml.text: # If we only have text content. - output.append(">%s</%s>" % (xml_escape(xml.text), tag_name)) + output.append(">%s</%s>" % (escape(xml.text, use_cdata), tag_name)) else: # Empty element. output.append(" />") if xml.tail: # If there is additional text after the element. - output.append(xml_escape(xml.tail)) + output.append(escape(xml.tail, use_cdata)) + for ns in new_namespaces: + # Remove namespaces introduced in this context. This is necessary + # because the namespaces object continues to be shared with other + # contexts. + namespaces.remove(ns) return ''.join(output) -def xml_escape(text): +def escape(text, use_cdata=False): """Convert special characters in XML to escape sequences. :param string text: The XML text to convert. @@ -132,12 +149,24 @@ def xml_escape(text): if type(text) != types.UnicodeType: text = unicode(text, 'utf-8', 'ignore') - text = list(text) escapes = {'&': '&', '<': '<', '>': '>', "'": ''', '"': '"'} - for i, c in enumerate(text): - text[i] = escapes.get(c, c) - return ''.join(text) + + if not use_cdata: + text = list(text) + for i, c in enumerate(text): + text[i] = escapes.get(c, c) + return ''.join(text) + else: + escape_needed = False + for c in text: + if c in escapes: + escape_needed = True + break + if escape_needed: + escaped = map(lambda x : "<![CDATA[%s]]>" % x, text.split("]]>")) + return "<![CDATA[]]]><![CDATA[]>]]>".join(escaped) + return text diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 49f33933..f9ec4947 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -26,14 +26,12 @@ import time import random import weakref import uuid -try: - import queue -except ImportError: - import Queue as queue +import errno from xml.parsers.expat import ExpatError import sleekxmpp +from sleekxmpp.util import Queue, QueueEmpty, safedict from sleekxmpp.thirdparty.statemachine import StateMachine from sleekxmpp.xmlstream import Scheduler, tostring, cert from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET, ElementBase @@ -52,7 +50,7 @@ RESPONSE_TIMEOUT = 30 #: The time in seconds to wait for events from the event queue, and also the #: time between checks for the process stop signal. -WAIT_TIMEOUT = 0.1 +WAIT_TIMEOUT = 1.0 #: The number of threads to use to handle XML stream events. This is not the #: same as the number of custom event handling threads. @@ -61,9 +59,6 @@ WAIT_TIMEOUT = 0.1 #: a GIL increasing this value can provide better performance. HANDLER_THREADS = 1 -#: Flag indicating if the SSL library is available for use. -SSL_SUPPORT = True - #: The time in seconds to delay between attempts to resend data #: after an SSL error. SSL_RETRY_DELAY = 0.5 @@ -120,9 +115,6 @@ class XMLStream(object): """ def __init__(self, socket=None, host='', port=0): - #: Flag indicating if the SSL library is available for use. - self.ssl_support = SSL_SUPPORT - #: Most XMPP servers support TLSv1, but OpenFire in particular #: does not work well with it. For OpenFire, set #: :attr:`ssl_version` to use ``SSLv23``:: @@ -131,6 +123,11 @@ class XMLStream(object): #: xmpp.ssl_version = ssl.PROTOCOL_SSLv23 self.ssl_version = ssl.PROTOCOL_TLSv1 + #: The list of accepted ciphers, in OpenSSL Format. + #: It might be useful to override it for improved security + #: over the python defaults. + self.ciphers = None + #: Path to a file containing certificates for verifying the #: server SSL certificate. A non-``None`` value will trigger #: certificate checking. @@ -141,6 +138,17 @@ class XMLStream(object): #: be consulted, even if they are not in the provided file. self.ca_certs = None + #: Path to a file containing a client certificate to use for + #: authenticating via SASL EXTERNAL. If set, there must also + #: be a corresponding `:attr:keyfile` value. + self.certfile = None + + #: Path to a file containing the private key for the selected + #: client certificate to use for authenticating via SASL EXTERNAL. + self.keyfile = None + + self._der_cert = None + #: The time in seconds to wait for events from the event queue, #: and also the time between checks for the process stop signal. self.wait_timeout = WAIT_TIMEOUT @@ -184,6 +192,7 @@ class XMLStream(object): #: The expected name of the server, for validation. self._expected_server_name = '' + self._service_name = '' #: The desired, or actual, address of the connected server. self.address = (host, int(port)) @@ -215,6 +224,15 @@ class XMLStream(object): #: If set to ``True``, attempt to use IPv6. self.use_ipv6 = True + #: If set to ``True``, allow using the ``dnspython`` DNS library + #: if available. If set to ``False``, the builtin DNS resolver + #: will be used, even if ``dnspython`` is installed. + self.use_dnspython = True + + #: Use CDATA for escaping instead of XML entities. Defaults + #: to ``False``. + self.use_cdata = False + #: An optional dictionary of proxy settings. It may provide: #: :host: The host offering proxy services. #: :port: The port for the proxy service. @@ -270,10 +288,10 @@ class XMLStream(object): self.end_session_on_disconnect = True #: A queue of stream, custom, and scheduled events to be processed. - self.event_queue = queue.Queue() + self.event_queue = Queue() #: A queue of string data to be sent over the stream. - self.send_queue = queue.Queue() + self.send_queue = Queue(maxsize=256) self.send_queue_lock = threading.Lock() self.send_lock = threading.RLock() @@ -322,7 +340,7 @@ class XMLStream(object): #: ``_xmpp-client._tcp`` service. self.dns_service = None - self.add_event_handler('connected', self._handle_connected) + self.add_event_handler('connected', self._session_timeout_check) self.add_event_handler('disconnected', self._remove_schedules) self.add_event_handler('session_start', self._start_keepalive) self.add_event_handler('session_start', self._cert_expiration) @@ -407,6 +425,8 @@ class XMLStream(object): :param reattempt: Flag indicating if the socket should reconnect after disconnections. """ + self.stop.clear() + if host and port: self.address = (host, int(port)) try: @@ -439,11 +459,12 @@ class XMLStream(object): def _connect(self, reattempt=True): self.scheduler.remove('Session timeout check') - self.stop.clear() - if self.reconnect_delay is None or not reattempt: + if self.reconnect_delay is None: delay = 1.0 - else: + self.reconnect_delay = delay + + if reattempt: delay = min(self.reconnect_delay * 2, self.reconnect_max_delay) delay = random.normalvariate(delay, delay * 0.1) log.debug('Waiting %s seconds before connecting.', delay) @@ -453,16 +474,18 @@ class XMLStream(object): time.sleep(0.1) elapsed += 0.1 except KeyboardInterrupt: - self.stop.set() + self.set_stop() return False except SystemExit: - self.stop.set() + self.set_stop() return False if self.default_domain: try: - self.address = self.pick_dns_answer(self.default_domain, - self.address[1]) + host, address, port = self.pick_dns_answer(self.default_domain, + self.address[1]) + self.address = (address, port) + self._service_name = host except StopIteration: log.debug("No remaining DNS records to try.") self.dns_answers = None @@ -490,17 +513,26 @@ class XMLStream(object): self.reconnect_delay = delay return False - if self.use_ssl and self.ssl_support: + if self.use_ssl: log.debug("Socket Wrapped for SSL") if self.ca_certs is None: cert_policy = ssl.CERT_NONE else: cert_policy = ssl.CERT_REQUIRED - ssl_socket = ssl.wrap_socket(self.socket, - ca_certs=self.ca_certs, - cert_reqs=cert_policy, - do_handshake_on_connect=False) + ssl_args = safedict({ + 'certfile': self.certfile, + 'keyfile': self.keyfile, + 'ca_certs': self.ca_certs, + 'cert_reqs': cert_policy, + 'do_handshake_on_connect': False, + "ssl_version": self.ssl_version + }) + + if sys.version_info >= (2, 7): + ssl_args['ciphers'] = self.ciphers + + ssl_socket = ssl.wrap_socket(self.socket, **ssl_args) if hasattr(self.socket, 'socket'): # We are using a testing socket, so preserve the top @@ -517,7 +549,7 @@ class XMLStream(object): log.debug("Connecting to %s:%s", domain, self.address[1]) self.socket.connect(self.address) - if self.use_ssl and self.ssl_support: + if self.use_ssl: try: self.socket.do_handshake() except (Socket.error, ssl.SSLError): @@ -538,7 +570,7 @@ class XMLStream(object): cert.verify(self._expected_server_name, self._der_cert) except cert.CertificateError as err: if not self.event_handled('ssl_invalid_cert'): - log.error(err.message) + log.error(err) self.disconnect(send_close=False) else: self.event('ssl_invalid_cert', @@ -547,8 +579,7 @@ class XMLStream(object): self.set_socket(self.socket, ignore=True) #this event is where you should set your application state - self.event("connected", direct=True) - self.reconnect_delay = 1.0 + self.event('connected', direct=True) return True except (Socket.error, ssl.SSLError) as serr: error_msg = "Could not connect to %s:%s. Socket Error #%s: %s" @@ -588,7 +619,7 @@ class XMLStream(object): headers = '\r\n'.join(headers) + '\r\n\r\n' try: - log.debug("Connecting to proxy: %s:%s", address) + log.debug("Connecting to proxy: %s:%s", *address) self.socket.connect(address) self.send_raw(headers, now=True) resp = '' @@ -599,6 +630,7 @@ class XMLStream(object): lines = resp.split('\r\n') if '200' not in lines[0]: self.event('proxy_error', resp) + self.event('connection_failed', direct=True) log.error('Proxy Error: %s', lines[0]) return False @@ -612,7 +644,7 @@ class XMLStream(object): serr.errno, serr.strerror) return False - def _handle_connected(self, event=None): + def _session_timeout_check(self, event=None): """ Add check to ensure that a session is established within a reasonable amount of time. @@ -661,6 +693,9 @@ class XMLStream(object): args=(reconnect, wait, send_close)) def _disconnect(self, reconnect=False, wait=None, send_close=True): + if not reconnect: + self.auto_reconnect = False + if self.end_session_on_disconnect or send_close: self.event('session_end', direct=True) @@ -684,7 +719,6 @@ class XMLStream(object): # closed in the other direction. If we didn't # send a stream footer we don't need to wait # since the server won't know to respond. - self.auto_reconnect = reconnect if send_close: log.info('Waiting for %s from server', self.stream_footer) self.stream_end_event.wait(4) @@ -692,7 +726,7 @@ class XMLStream(object): self.stream_end_event.set() if not self.auto_reconnect: - self.stop.set() + self.set_stop() if self._disconnect_wait_for_threads: self._wait_for_threads() @@ -704,9 +738,23 @@ class XMLStream(object): self.event('socket_error', serr, direct=True) finally: #clear your application state - self.event("disconnected", direct=True) + self.event('disconnected', direct=True) return True + def abort(self): + self.session_started_event.clear() + self.set_stop() + if self._disconnect_wait_for_threads: + self._wait_for_threads() + try: + self.socket.shutdown(Socket.SHUT_RDWR) + self.socket.close() + self.filesocket.close() + except Socket.error: + pass + self.state.transition_any(['connected', 'disconnected'], 'disconnected', func=lambda: True) + self.event("killed", direct=True) + def reconnect(self, reattempt=True, wait=False, send_close=True): """Reset the stream's state and reconnect to the server.""" log.debug("reconnecting...") @@ -789,56 +837,62 @@ class XMLStream(object): If the handshake is successful, the XML stream will need to be restarted. """ - if self.ssl_support: - log.info("Negotiating TLS") - log.info("Using SSL version: %s", str(self.ssl_version)) - if self.ca_certs is None: - cert_policy = ssl.CERT_NONE - else: - cert_policy = ssl.CERT_REQUIRED - - ssl_socket = ssl.wrap_socket(self.socket, - ssl_version=self.ssl_version, - do_handshake_on_connect=False, - ca_certs=self.ca_certs, - cert_reqs=cert_policy) + log.info("Negotiating TLS") + ssl_versions = {3: 'TLS 1.0', 1: 'SSL 3', 2: 'SSL 2/3'} + log.info("Using SSL version: %s", ssl_versions[self.ssl_version]) + if self.ca_certs is None: + cert_policy = ssl.CERT_NONE + else: + cert_policy = ssl.CERT_REQUIRED + + ssl_args = safedict({ + 'certfile': self.certfile, + 'keyfile': self.keyfile, + 'ca_certs': self.ca_certs, + 'cert_reqs': cert_policy, + 'do_handshake_on_connect': False, + "ssl_version": self.ssl_version + }) + + if sys.version_info >= (2, 7): + ssl_args['ciphers'] = self.ciphers + + ssl_socket = ssl.wrap_socket(self.socket, **ssl_args) + + if hasattr(self.socket, 'socket'): + # We are using a testing socket, so preserve the top + # layer of wrapping. + self.socket.socket = ssl_socket + else: + self.socket = ssl_socket - if hasattr(self.socket, 'socket'): - # We are using a testing socket, so preserve the top - # layer of wrapping. - self.socket.socket = ssl_socket + try: + self.socket.do_handshake() + except (Socket.error, ssl.SSLError): + log.error('CERT: Invalid certificate trust chain.') + if not self.event_handled('ssl_invalid_chain'): + self.disconnect(self.auto_reconnect, send_close=False) else: - self.socket = ssl_socket - - try: - self.socket.do_handshake() - except (Socket.error, ssl.SSLError): - log.error('CERT: Invalid certificate trust chain.') - if not self.event_handled('ssl_invalid_chain'): - self.disconnect(self.auto_reconnect, send_close=False) - else: - self.event('ssl_invalid_chain', direct=True) - return False + self._der_cert = self.socket.getpeercert(binary_form=True) + self.event('ssl_invalid_chain', direct=True) + return False - self._der_cert = self.socket.getpeercert(binary_form=True) - pem_cert = ssl.DER_cert_to_PEM_cert(self._der_cert) - log.debug('CERT: %s', pem_cert) - self.event('ssl_cert', pem_cert, direct=True) + self._der_cert = self.socket.getpeercert(binary_form=True) + pem_cert = ssl.DER_cert_to_PEM_cert(self._der_cert) + log.debug('CERT: %s', pem_cert) + self.event('ssl_cert', pem_cert, direct=True) - try: - cert.verify(self._expected_server_name, self._der_cert) - except cert.CertificateError as err: - if not self.event_handled('ssl_invalid_cert'): - log.error(err.message) - self.disconnect(self.auto_reconnect, send_close=False) - else: - self.event('ssl_invalid_cert', pem_cert, direct=True) + try: + cert.verify(self._expected_server_name, self._der_cert) + except cert.CertificateError as err: + if not self.event_handled('ssl_invalid_cert'): + log.error(err) + self.disconnect(self.auto_reconnect, send_close=False) + else: + self.event('ssl_invalid_cert', pem_cert, direct=True) - self.set_socket(self.socket) - return True - else: - log.warning("Tried to enable TLS, but ssl module not found.") - return False + self.set_socket(self.socket) + return True def _cert_expiration(self, event): """Schedule an event for when the TLS certificate expires.""" @@ -866,9 +920,15 @@ class XMLStream(object): log.warn('CERT: Certificate has expired.') restart() + try: + total_seconds = cert_ttl.total_seconds() + except AttributeError: + # for Python < 2.7 + total_seconds = (cert_ttl.microseconds + (cert_ttl.seconds + cert_ttl.days * 24 * 3600) * 10**6) / 10**6 + log.info('CERT: Time until certificate expiration: %s' % cert_ttl) self.schedule('Certificate Expiration', - cert_ttl.seconds, + total_seconds, restart) def _start_keepalive(self, event): @@ -882,12 +942,13 @@ class XMLStream(object): self.whitespace_keepalive_interval = 300 """ - self.schedule('Whitespace Keepalive', - self.whitespace_keepalive_interval, - self.send_raw, - args=(' ',), - kwargs={'now': True}, - repeat=True) + if self.whitespace_keepalive: + self.schedule('Whitespace Keepalive', + self.whitespace_keepalive_interval, + self.send_raw, + args=(' ',), + kwargs={'now': True}, + repeat=True) def _remove_schedules(self, event): """Remove whitespace keepalive and certificate expiration schedules.""" @@ -983,9 +1044,13 @@ class XMLStream(object): # and handler classes here. if name is None: - name = 'add_handler_%s' % self.getNewId() - self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer, - once=disposable, instream=instream)) + name = 'add_handler_%s' % self.new_id() + self.register_handler( + XMLCallback(name, + MatchXMLMask(mask, self.default_ns), + pointer, + once=disposable, + instream=instream)) def register_handler(self, handler, before=None, after=None): """Add a stream event handler that will be executed when a matching @@ -1026,7 +1091,8 @@ class XMLStream(object): return resolve(domain, port, service=self.dns_service, resolver=resolver, - use_ipv6=self.use_ipv6) + use_ipv6=self.use_ipv6, + use_dnspython=self.use_dnspython) def pick_dns_answer(self, domain, port=None): """Pick a server and port from DNS answers. @@ -1087,7 +1153,7 @@ class XMLStream(object): """ return len(self.__event_handlers.get(name, [])) - def event(self, name, data={}, direct=False): + def event(self, name, data=None, direct=False): """Manually trigger a custom event. :param name: The name of the event to trigger. @@ -1098,6 +1164,11 @@ class XMLStream(object): event queue. All event handlers will run in the same thread. """ + if not data: + data = {} + + log.debug("Event triggered: " + name) + handlers = self.__event_handlers.get(name, []) for handler in handlers: #TODO: Data should not be copied, but should be read only, @@ -1202,7 +1273,9 @@ class XMLStream(object): data = filter(data) if data is None: return - str_data = str(data) + str_data = tostring(data.xml, xmlns=self.default_ns, + stream=self, + top_level=True) self.send_raw(str_data, now) else: self.send_raw(data, now) @@ -1267,6 +1340,9 @@ class XMLStream(object): if not self.stop.is_set(): time.sleep(self.ssl_retry_delay) tries += 1 + except Socket.error as serr: + if serr.errno != errno.EINTR: + raise if count > 1: log.debug('SENT: %d chunks', count) except (Socket.error, ssl.SSLError) as serr: @@ -1281,12 +1357,12 @@ class XMLStream(object): return True def _start_thread(self, name, target, track=True): - self.__active_threads.add(name) self.__thread[name] = threading.Thread(name=name, target=target) self.__thread[name].daemon = self._use_daemons self.__thread[name].start() if track: + self.__active_threads.add(name) with self.__thread_cond: self.__thread_count += 1 @@ -1315,6 +1391,13 @@ class XMLStream(object): if self.__thread_count == 0: self.__thread_cond.notify() + def set_stop(self): + self.stop.set() + + # Unlock queues + self.event_queue.put(None) + self.send_queue.put(None) + def _wait_for_threads(self): with self.__thread_cond: if self.__thread_count != 0: @@ -1458,6 +1541,10 @@ class XMLStream(object): # as handshakes. self.stream_end_event.clear() self.start_stream_handler(root) + + # We have a successful stream connection, so reset + # exponential backoff for new reconnect attempts. + self.reconnect_delay = 1.0 depth += 1 if event == b'end': depth -= 1 @@ -1583,11 +1670,7 @@ class XMLStream(object): log.debug("Loading event runner") try: while not self.stop.is_set(): - try: - wait = self.wait_timeout - event = self.event_queue.get(True, timeout=wait) - except queue.Empty: - event = None + event = self.event_queue.get() if event is None: continue @@ -1603,10 +1686,10 @@ class XMLStream(object): log.exception(error_msg, handler.name) orig.exception(e) elif etype == 'schedule': - name = args[1] + name = args[2] try: log.debug('Scheduled event: %s: %s', name, args[0]) - handler(*args[0]) + handler(*args[0], **args[1]) except Exception as e: log.exception('Error processing scheduled task') self.exception(e) @@ -1648,14 +1731,13 @@ class XMLStream(object): while not self.stop.is_set(): while not self.stop.is_set() and \ not self.session_started_event.is_set(): - self.session_started_event.wait(timeout=0.1) + self.session_started_event.wait(timeout=0.1) # Wait for session start if self.__failed_send_stanza is not None: data = self.__failed_send_stanza self.__failed_send_stanza = None else: - try: - data = self.send_queue.get(True, 1) - except queue.Empty: + data = self.send_queue.get() # Wait for data to send + if data is None: continue log.debug("SEND: %s", data) enc_data = data.encode('utf-8') @@ -1682,6 +1764,9 @@ class XMLStream(object): if not self.stop.is_set(): time.sleep(self.ssl_retry_delay) tries += 1 + except Socket.error as serr: + if serr.errno != errno.EINTR: + raise if count > 1: log.debug('SENT: %d chunks', count) self.send_queue.task_done() @@ -1,7 +1,11 @@ #!/usr/bin/env python -import os import sys +if len(sys.argv)>1 and sys.argv[1].lower() == 'gevent': + from gevent import monkey + monkey.patch_all() + +import os import logging import unittest import distutils.core @@ -57,7 +61,7 @@ class TestCommand(distutils.core.Command): if __name__ == '__main__': result = run_tests() - print("<tests %s ran='%s' errors='%s' fails='%s' success='%s' />" % ( + print("<tests %s ran='%s' errors='%s' fails='%s' success='%s' gevent_enabled=%s/>" % ( "xmlns='http//andyet.net/protocol/tests'", result.testsRun, len(result.errors), - len(result.failures), result.wasSuccessful())) + len(result.failures), result.wasSuccessful(),'gevent' in sys.modules)) diff --git a/tests/test_events.py b/tests/test_events.py index fb34be30..a41ed017 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,5 +1,6 @@ import time -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest class TestEvents(SleekTest): diff --git a/tests/test_jid.py b/tests/test_jid.py index ef1145d3..ed2aeea9 100644 --- a/tests/test_jid.py +++ b/tests/test_jid.py @@ -1,5 +1,9 @@ -from sleekxmpp.test import * -from sleekxmpp.xmlstream.jid import JID +# -*- encoding: utf8 -*- +from __future__ import unicode_literals +import unittest +from sleekxmpp.test import SleekTest +from sleekxmpp import JID, InvalidJID +from sleekxmpp.jid import nodeprep class TestJIDClass(SleekTest): @@ -137,5 +141,150 @@ class TestJIDClass(SleekTest): self.assertFalse(jid1 == jid2, "Same JIDs are not considered equal") self.assertTrue(jid1 != jid2, "Same JIDs are considered not equal") + def testZeroLengthDomain(self): + self.assertRaises(InvalidJID, JID, domain='') + self.assertRaises(InvalidJID, JID, 'user@/resource') + + def testZeroLengthLocalPart(self): + self.assertRaises(InvalidJID, JID, local='', domain='test.com') + self.assertRaises(InvalidJID, JID, '@/test.com') + + def testZeroLengthResource(self): + self.assertRaises(InvalidJID, JID, domain='test.com', resource='') + self.assertRaises(InvalidJID, JID, 'test.com/') + + def test1023LengthDomain(self): + domain = ('a.' * 509) + 'a.com' + jid1 = JID(domain=domain) + jid2 = JID('user@%s/resource' % domain) + + def test1023LengthLocalPart(self): + local = 'a' * 1023 + jid1 = JID(local=local, domain='test.com') + jid2 = JID('%s@test.com' % local) + + def test1023LengthResource(self): + resource = 'r' * 1023 + jid1 = JID(domain='test.com', resource=resource) + jid2 = JID('test.com/%s' % resource) + + def test1024LengthDomain(self): + domain = ('a.' * 509) + 'aa.com' + self.assertRaises(InvalidJID, JID, domain=domain) + self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) + + def test1024LengthLocalPart(self): + local = 'a' * 1024 + self.assertRaises(InvalidJID, JID, local=local, domain='test.com') + self.assertRaises(InvalidJID, JID, '%s@/test.com' % local) + + def test1024LengthResource(self): + resource = 'r' * 1024 + self.assertRaises(InvalidJID, JID, domain='test.com', resource=resource) + self.assertRaises(InvalidJID, JID, 'test.com/%s' % resource) + + def testTooLongDomainLabel(self): + domain = ('a' * 64) + '.com' + self.assertRaises(InvalidJID, JID, domain=domain) + self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) + + def testDomainEmptyLabel(self): + domain = 'aaa..bbb.com' + self.assertRaises(InvalidJID, JID, domain=domain) + self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) + + def testDomainIPv4(self): + domain = '127.0.0.1' + jid1 = JID(domain=domain) + jid2 = JID('user@%s/resource' % domain) + + def testDomainIPv6(self): + domain = '[::1]' + jid1 = JID(domain=domain) + jid2 = JID('user@%s/resource' % domain) + + def testDomainInvalidIPv6NoBrackets(self): + domain = '::1' + jid1 = JID(domain=domain) + jid2 = JID('user@%s/resource' % domain) + + self.assertEqual(jid1.domain, '[::1]') + self.assertEqual(jid2.domain, '[::1]') + + def testDomainInvalidIPv6MissingBracket(self): + domain = '[::1' + jid1 = JID(domain=domain) + jid2 = JID('user@%s/resource' % domain) + + self.assertEqual(jid1.domain, '[::1]') + self.assertEqual(jid2.domain, '[::1]') + + def testDomainWithPort(self): + domain = 'example.com:5555' + self.assertRaises(InvalidJID, JID, domain=domain) + self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) + + def testDomainWithTrailingDot(self): + domain = 'example.com.' + jid1 = JID(domain=domain) + jid2 = JID('user@%s/resource' % domain) + + self.assertEqual(jid1.domain, 'example.com') + self.assertEqual(jid2.domain, 'example.com') + + def testDomainWithDashes(self): + domain = 'example.com-' + self.assertRaises(InvalidJID, JID, domain=domain) + self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) + + domain = '-example.com' + self.assertRaises(InvalidJID, JID, domain=domain) + self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) + + def testACEDomain(self): + domain = 'xn--bcher-kva.ch' + jid1 = JID(domain=domain) + jid2 = JID('user@%s/resource' % domain) + + self.assertEqual(jid1.domain.encode('utf-8'), b'b\xc3\xbccher.ch') + self.assertEqual(jid2.domain.encode('utf-8'), b'b\xc3\xbccher.ch') + + def testJIDEscapeExistingSequences(self): + jid = JID(local='blah\\foo\\20bar', domain='example.com') + self.assertEqual(jid.local, 'blah\\foo\\5c20bar') + + def testJIDEscape(self): + jid = JID(local='here\'s_a_wild_&_/cr%zy/_address_for:<wv>("IMPS")', + domain='example.com') + self.assertEqual(jid.local, r'here\27s_a_wild_\26_\2fcr%zy\2f_address_for\3a\3cwv\3e(\22IMPS\22)') + + def testJIDUnescape(self): + jid = JID(local='here\'s_a_wild_&_/cr%zy/_address_for:<wv>("IMPS")', + domain='example.com') + ujid = jid.unescape() + self.assertEqual(ujid.local, 'here\'s_a_wild_&_/cr%zy/_address_for:<wv>("IMPS")') + + jid = JID(local='blah\\foo\\20bar', domain='example.com') + ujid = jid.unescape() + self.assertEqual(ujid.local, 'blah\\foo\\20bar') + + def testStartOrEndWithEscapedSpaces(self): + local = ' foo' + self.assertRaises(InvalidJID, JID, local=local, domain='example.com') + self.assertRaises(InvalidJID, JID, '%s@example.com' % local) + + local = 'bar ' + self.assertRaises(InvalidJID, JID, local=local, domain='example.com') + self.assertRaises(InvalidJID, JID, '%s@example.com' % local) + + # Need more input for these cases. A JID starting with \20 *is* valid + # according to RFC 6122, but is not according to XEP-0106. + #self.assertRaises(InvalidJID, JID, '%s@example.com' % '\\20foo2') + #self.assertRaises(InvalidJID, JID, '%s@example.com' % 'bar2\\20') + + def testNodePrepIdemptotent(self): + node = 'ᴹᴵᴷᴬᴱᴸ' + self.assertEqual(nodeprep(node), nodeprep(nodeprep(node))) + suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass) diff --git a/tests/test_stanza_base.py b/tests/test_stanza_base.py index 9bd326b6..deb7ab96 100644 --- a/tests/test_stanza_base.py +++ b/tests/test_stanza_base.py @@ -1,4 +1,5 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest from sleekxmpp.xmlstream.stanzabase import ET, StanzaBase diff --git a/tests/test_stanza_element.py b/tests/test_stanza_element.py index b7ccdb87..e678b56e 100644 --- a/tests/test_stanza_element.py +++ b/tests/test_stanza_element.py @@ -1,5 +1,6 @@ -from sleekxmpp.test import * -from sleekxmpp.xmlstream.stanzabase import ElementBase +import unittest +from sleekxmpp.test import SleekTest +from sleekxmpp.xmlstream.stanzabase import ElementBase, register_stanza_plugin, ET from sleekxmpp.thirdparty import OrderedDict @@ -384,7 +385,7 @@ class TestElementBase(SleekTest): interfaces = set(('bar', 'baz')) def setBar(self, value): - self._set_sub_text("path/to/only/bar", value); + self._set_sub_text("path/to/only/bar", value) def getBar(self): return self._get_sub_text("path/to/only/bar") @@ -393,7 +394,7 @@ class TestElementBase(SleekTest): self._del_sub("path/to/only/bar") def setBaz(self, value): - self._set_sub_text("path/to/just/baz", value); + self._set_sub_text("path/to/just/baz", value) def getBaz(self): return self._get_sub_text("path/to/just/baz") diff --git a/tests/test_stanza_error.py b/tests/test_stanza_error.py index a41bf4bf..d95a33ce 100644 --- a/tests/test_stanza_error.py +++ b/tests/test_stanza_error.py @@ -1,4 +1,5 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest class TestErrorStanzas(SleekTest): diff --git a/tests/test_stanza_gmail.py b/tests/test_stanza_gmail.py index 6190c608..a15fea20 100644 --- a/tests/test_stanza_gmail.py +++ b/tests/test_stanza_gmail.py @@ -1,5 +1,8 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp import Iq +from sleekxmpp.test import SleekTest import sleekxmpp.plugins.gmail_notify as gmail +from sleekxmpp.xmlstream import register_stanza_plugin, ET class TestGmail(SleekTest): diff --git a/tests/test_stanza_iq.py b/tests/test_stanza_iq.py index 42e4dcde..0f5e30b0 100644 --- a/tests/test_stanza_iq.py +++ b/tests/test_stanza_iq.py @@ -1,4 +1,5 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest from sleekxmpp.xmlstream.stanzabase import ET diff --git a/tests/test_stanza_message.py b/tests/test_stanza_message.py index e55971df..9968a630 100644 --- a/tests/test_stanza_message.py +++ b/tests/test_stanza_message.py @@ -1,6 +1,8 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest from sleekxmpp.stanza.message import Message from sleekxmpp.stanza.htmlim import HTMLIM +from sleekxmpp.xmlstream import register_stanza_plugin class TestMessageStanzas(SleekTest): @@ -30,9 +32,7 @@ class TestMessageStanzas(SleekTest): msg['to'] = "fritzy@netflint.net/sleekxmpp" msg['body'] = "this is the plaintext message" msg['type'] = 'chat' - p = ET.Element('{http://www.w3.org/1999/xhtml}p') - p.text = "This is the htmlim message" - msg['html']['body'] = p + msg['html']['body'] = '<p>This is the htmlim message</p>' self.check(msg, """ <message to="fritzy@netflint.net/sleekxmpp" type="chat"> <body>this is the plaintext message</body> diff --git a/tests/test_stanza_presence.py b/tests/test_stanza_presence.py index 2ec43b65..184dce96 100644 --- a/tests/test_stanza_presence.py +++ b/tests/test_stanza_presence.py @@ -1,6 +1,6 @@ -from sleekxmpp.test import * -from sleekxmpp.stanza.presence import Presence - +import unittest +import sleekxmpp +from sleekxmpp.test import SleekTest class TestPresenceStanzas(SleekTest): diff --git a/tests/test_stanza_roster.py b/tests/test_stanza_roster.py index 8ec2d32b..d121568b 100644 --- a/tests/test_stanza_roster.py +++ b/tests/test_stanza_roster.py @@ -1,5 +1,6 @@ -from sleekxmpp.test import * -from sleekxmpp.stanza.roster import Roster +import unittest +from sleekxmpp.test import SleekTest +from sleekxmpp.xmlstream import ET class TestRosterStanzas(SleekTest): diff --git a/tests/test_stanza_xep_0004.py b/tests/test_stanza_xep_0004.py index e183e5e9..9056c663 100644 --- a/tests/test_stanza_xep_0004.py +++ b/tests/test_stanza_xep_0004.py @@ -1,7 +1,10 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp import Message +from sleekxmpp.test import SleekTest from sleekxmpp.thirdparty import OrderedDict import sleekxmpp.plugins.xep_0004 as xep_0004 +from sleekxmpp.xmlstream import register_stanza_plugin class TestDataForms(SleekTest): diff --git a/tests/test_stanza_xep_0030.py b/tests/test_stanza_xep_0030.py index 2d64988d..986c1880 100644 --- a/tests/test_stanza_xep_0030.py +++ b/tests/test_stanza_xep_0030.py @@ -1,5 +1,8 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp import Iq +from sleekxmpp.test import SleekTest import sleekxmpp.plugins.xep_0030 as xep_0030 +from sleekxmpp.xmlstream import register_stanza_plugin class TestDisco(SleekTest): diff --git a/tests/test_stanza_xep_0033.py b/tests/test_stanza_xep_0033.py index ec9a5309..bf10cf6c 100644 --- a/tests/test_stanza_xep_0033.py +++ b/tests/test_stanza_xep_0033.py @@ -1,5 +1,8 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp import Message +from sleekxmpp.test import SleekTest import sleekxmpp.plugins.xep_0033 as xep_0033 +from sleekxmpp.xmlstream import register_stanza_plugin class TestAddresses(SleekTest): diff --git a/tests/test_stanza_xep_0047.py b/tests/test_stanza_xep_0047.py index 6aa2314b..9fd3c4d6 100644 --- a/tests/test_stanza_xep_0047.py +++ b/tests/test_stanza_xep_0047.py @@ -1,5 +1,9 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp.exceptions import XMPPError +from sleekxmpp import Iq +from sleekxmpp.test import SleekTest from sleekxmpp.plugins.xep_0047 import Data +from sleekxmpp.xmlstream import register_stanza_plugin, ET class TestIBB(SleekTest): diff --git a/tests/test_stanza_xep_0050.py b/tests/test_stanza_xep_0050.py index e02e86c3..9d49b3ee 100644 --- a/tests/test_stanza_xep_0050.py +++ b/tests/test_stanza_xep_0050.py @@ -1,6 +1,8 @@ from sleekxmpp import Iq -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest from sleekxmpp.plugins.xep_0050 import Command +from sleekxmpp.xmlstream import register_stanza_plugin class TestAdHocCommandStanzas(SleekTest): diff --git a/tests/test_stanza_xep_0059.py b/tests/test_stanza_xep_0059.py index 913436a6..860ec869 100644 --- a/tests/test_stanza_xep_0059.py +++ b/tests/test_stanza_xep_0059.py @@ -1,5 +1,7 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest from sleekxmpp.plugins.xep_0059 import Set +from sleekxmpp.xmlstream import ET class TestSetStanzas(SleekTest): diff --git a/tests/test_stanza_xep_0060.py b/tests/test_stanza_xep_0060.py index 16a7cb37..332b53ea 100644 --- a/tests/test_stanza_xep_0060.py +++ b/tests/test_stanza_xep_0060.py @@ -1,6 +1,8 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest import sleekxmpp.plugins.xep_0004 as xep_0004 import sleekxmpp.plugins.xep_0060.stanza as pubsub +from sleekxmpp.xmlstream.stanzabase import ET class TestPubsubStanzas(SleekTest): @@ -129,20 +131,6 @@ class TestPubsubStanzas(SleekTest): </pubsub> </iq>""") - def testState(self): - "Testing iq/psstate stanzas" - iq = self.Iq() - iq['psstate']['node']= 'mynode' - iq['psstate']['item']= 'myitem' - pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed') - iq['psstate']['payload'] = pl - self.check(iq, """ - <iq id="0"> - <state xmlns="http://jabber.org/protocol/psstate" node="mynode" item="myitem"> - <claimed xmlns="http://andyet.net/protocol/pubsubqueue" /> - </state> - </iq>""") - def testDefault(self): "Testing iq/pubsub_owner/default stanzas" iq = self.Iq() diff --git a/tests/test_stanza_xep_0085.py b/tests/test_stanza_xep_0085.py index 61784e47..303e6c5b 100644 --- a/tests/test_stanza_xep_0085.py +++ b/tests/test_stanza_xep_0085.py @@ -1,5 +1,9 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp import Message +from sleekxmpp.test import SleekTest import sleekxmpp.plugins.xep_0085 as xep_0085 +from sleekxmpp.xmlstream import register_stanza_plugin + class TestChatStates(SleekTest): diff --git a/tests/test_stanza_xep_0184.py b/tests/test_stanza_xep_0184.py index 13472373..0c340487 100644 --- a/tests/test_stanza_xep_0184.py +++ b/tests/test_stanza_xep_0184.py @@ -1,5 +1,8 @@ -from sleekxmpp.test import * +import unittest +from sleekxmpp import Message +from sleekxmpp.test import SleekTest import sleekxmpp.plugins.xep_0184 as xep_0184 +from sleekxmpp.xmlstream import register_stanza_plugin class TestReciept(SleekTest): diff --git a/tests/test_stanza_xep_0323.py b/tests/test_stanza_xep_0323.py new file mode 100644 index 00000000..7b1dfe42 --- /dev/null +++ b/tests/test_stanza_xep_0323.py @@ -0,0 +1,390 @@ +# -*- coding: utf-8 -*- + +from sleekxmpp.test import * +import sleekxmpp.plugins.xep_0323 as xep_0323 + +namespace='sn' + +class TestSensorDataStanzas(SleekTest): + + + def setUp(self): + pass + #register_stanza_plugin(Iq, xep_0323.stanza.Request) + #register_stanza_plugin(Iq, xep_0323.stanza.Accepted) + #register_stanza_plugin(Message, xep_0323.stanza.Failure) + #register_stanza_plugin(xep_0323.stanza.Failure, xep_0323.stanza.Error) + #register_stanza_plugin(Iq, xep_0323.stanza.Rejected) + #register_stanza_plugin(Message, xep_0323.stanza.Fields) + #register_stanza_plugin(Message, xep_0323.stanza.Request) + #register_stanza_plugin(Message, xep_0323.stanza.Accepted) + #register_stanza_plugin(Message, xep_0323.stanza.Failure) + # register_stanza_plugin(Message, xep_0323.stanza.Result) + # register_stanza_plugin(Message, xep_0323.stanza.Gone) + # register_stanza_plugin(Message, xep_0323.stanza.Inactive) + # register_stanza_plugin(Message, xep_0323.stanza.Paused) + + def testRequest(self): + """ + test of request stanza + """ + iq = self.Iq() + iq['type'] = 'get' + iq['from'] = 'master@clayster.com/amr' + iq['to'] = 'device@clayster.com' + iq['id'] = '1' + iq['req']['seqnr'] = '1' + iq['req']['momentary'] = 'true' + + self.check(iq,""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1' momentary='true'/> + </iq> + """ + ) + + def testRequestNodes(self): + """ + test of request nodes stanza + """ + iq = self.Iq() + iq['type'] = 'get' + iq['from'] = 'master@clayster.com/amr' + iq['to'] = 'device@clayster.com' + iq['id'] = '1' + iq['req']['seqnr'] = '1' + iq['req']['momentary'] = 'true' + + + iq['req'].add_node("Device02", "Source02", "CacheType") + iq['req'].add_node("Device44") + + self.check(iq,""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1' momentary='true'> + <node nodeId='Device02' sourceId='Source02' cacheType='CacheType'/> + <node nodeId='Device44'/> + </req> + </iq> + """ + ) + + iq['req'].del_node("Device02") + + self.check(iq,""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1' momentary='true'> + <node nodeId='Device44'/> + </req> + </iq> + """ + ) + + iq['req'].del_nodes() + + self.check(iq,""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1' momentary='true'> + </req> + </iq> + """ + ) + + def testRequestField(self): + """ + test of request field stanza + """ + iq = self.Iq() + iq['type'] = 'get' + iq['from'] = 'master@clayster.com/amr' + iq['to'] = 'device@clayster.com' + iq['id'] = '1' + iq['req']['seqnr'] = '1' + iq['req']['momentary'] = 'true' + + + iq['req'].add_field("Top temperature") + iq['req'].add_field("Bottom temperature") + + self.check(iq,""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1' momentary='true'> + <field name='Top temperature'/> + <field name='Bottom temperature'/> + </req> + </iq> + """ + ) + + iq['req'].del_field("Top temperature") + + self.check(iq,""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1' momentary='true'> + <field name='Bottom temperature'/> + </req> + </iq> + """ + ) + + iq['req'].del_fields() + + self.check(iq,""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1' momentary='true'> + </req> + </iq> + """ + ) + + + def testAccepted(self): + """ + test of request stanza + """ + iq = self.Iq() + iq['type'] = 'result' + iq['from'] = 'device@clayster.com' + iq['to'] = 'master@clayster.com/amr' + iq['id'] = '2' + iq['accepted']['seqnr'] = '2' + + self.check(iq,""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='2'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='2'/> + </iq> + """ + ) + + def testRejected(self): + """ + test of request stanza + """ + iq = self.Iq() + iq['type'] = 'error' + iq['from'] = 'device@clayster.com' + iq['to'] = 'master@clayster.com/amr' + iq['id'] = '4' + iq['rejected']['seqnr'] = '4' + iq['rejected']['error'] = 'Access denied.' + + self.check(iq,""" + <iq type='error' + from='device@clayster.com' + to='master@clayster.com/amr' + id='4'> + <rejected xmlns='urn:xmpp:iot:sensordata' seqnr='4'> + <error>Access denied.</error> + </rejected> + </iq> + """ + ) + + def testFailure(self): + """ + test of failure stanza + """ + msg = self.Message() + msg['from'] = 'device@clayster.com' + msg['to'] = 'master@clayster.com/amr' + msg['failure']['seqnr'] = '3' + msg['failure']['done'] = 'true' + msg['failure']['error']['nodeId'] = 'Device01' + msg['failure']['error']['timestamp'] = '2013-03-07T17:13:30' + msg['failure']['error']['text'] = 'Timeout.' + + self.check(msg,""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <failure xmlns='urn:xmpp:iot:sensordata' seqnr='3' done='true'> + <error nodeId='Device01' timestamp='2013-03-07T17:13:30'> + Timeout.</error> + </failure> + </message> + """ + ) + + def testFields(self): + """ + test of fields stanza + """ + msg = self.Message() + msg['from'] = 'device@clayster.com' + msg['to'] = 'master@clayster.com/amr' + msg['fields']['seqnr'] = '1' + + node = msg['fields'].add_node("Device02") + ts = node.add_timestamp("2013-03-07T16:24:30") + + data = ts.add_data(typename="numeric", name="Temperature", value="-12.42", unit='K') + data['momentary'] = 'true' + data['automaticReadout'] = 'true' + + self.check(msg,""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId='Device02'> + <timestamp value='2013-03-07T16:24:30'> + <numeric name='Temperature' momentary='true' automaticReadout='true' value='-12.42' unit='K'/> + </timestamp> + </node> + </fields> + </message> + """ + ) + + node = msg['fields'].add_node("EmptyDevice") + node = msg['fields'].add_node("Device04") + ts = node.add_timestamp("EmptyTimestamp") + + self.check(msg,""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId='Device02'> + <timestamp value='2013-03-07T16:24:30'> + <numeric name='Temperature' momentary='true' automaticReadout='true' value='-12.42' unit='K'/> + </timestamp> + </node> + <node nodeId='EmptyDevice'/> + <node nodeId='Device04'> + <timestamp value='EmptyTimestamp'/> + </node> + </fields> + </message> + """ + ) + + node = msg['fields'].add_node("Device77") + ts = node.add_timestamp("2013-05-03T12:00:01") + data = ts.add_data(typename="numeric", name="Temperature", value="-12.42", unit='K') + data['historicalDay'] = 'true' + data = ts.add_data(typename="numeric", name="Speed", value="312.42", unit='km/h') + data['historicalWeek'] = 'false' + data = ts.add_data(typename="string", name="Temperature name", value="Bottom oil") + data['historicalMonth'] = 'true' + data = ts.add_data(typename="string", name="Speed name", value="Top speed") + data['historicalQuarter'] = 'false' + data = ts.add_data(typename="dateTime", name="T1", value="1979-01-01T00:00:00") + data['historicalYear'] = 'true' + data = ts.add_data(typename="dateTime", name="T2", value="2000-01-01T01:02:03") + data['historicalOther'] = 'false' + data = ts.add_data(typename="timeSpan", name="TS1", value="P5Y") + data['missing'] = 'true' + data = ts.add_data(typename="timeSpan", name="TS2", value="PT2M1S") + data['manualEstimate'] = 'false' + data = ts.add_data(typename="enum", name="top color", value="red", dataType="string") + data['invoiced'] = 'true' + data = ts.add_data(typename="enum", name="bottom color", value="black", dataType="string") + data['powerFailure'] = 'false' + data = ts.add_data(typename="boolean", name="Temperature real", value="false") + data['historicalDay'] = 'true' + data = ts.add_data(typename="boolean", name="Speed real", value="true") + data['historicalWeek'] = 'false' + + self.check(msg,""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId='Device02'> + <timestamp value='2013-03-07T16:24:30'> + <numeric name='Temperature' momentary='true' automaticReadout='true' value='-12.42' unit='K'/> + </timestamp> + </node> + <node nodeId='EmptyDevice'/> + <node nodeId='Device04'> + <timestamp value='EmptyTimestamp'/> + </node> + <node nodeId='Device77'> + <timestamp value='2013-05-03T12:00:01'> + <numeric name='Temperature' historicalDay='true' value='-12.42' unit='K'/> + <numeric name='Speed' historicalWeek='false' value='312.42' unit='km/h'/> + <string name='Temperature name' historicalMonth='true' value='Bottom oil'/> + <string name='Speed name' historicalQuarter='false' value='Top speed'/> + <dateTime name='T1' historicalYear='true' value='1979-01-01T00:00:00'/> + <dateTime name='T2' historicalOther='false' value='2000-01-01T01:02:03'/> + <timeSpan name='TS1' missing='true' value='P5Y'/> + <timeSpan name='TS2' manualEstimate='false' value='PT2M1S'/> + <enum name='top color' invoiced='true' value='red' dataType='string'/> + <enum name='bottom color' powerFailure='false' value='black' dataType='string'/> + <boolean name='Temperature real' historicalDay='true' value='false'/> + <boolean name='Speed real' historicalWeek='false' value='true'/> + </timestamp> + </node> + </fields> + </message> + """ + ) + + + def testTimestamp(self): + msg = self.Message() + + msg['from'] = 'device@clayster.com' + msg['to'] = 'master@clayster.com/amr' + msg['fields']['seqnr'] = '1' + + node = msg['fields'].add_node("Device02") + node = msg['fields'].add_node("Device03") + + ts = node.add_timestamp("2013-03-07T16:24:30") + ts = node.add_timestamp("2013-03-07T16:24:31") + + self.check(msg,""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId='Device02'/> + <node nodeId='Device03'> + <timestamp value='2013-03-07T16:24:30'/> + <timestamp value='2013-03-07T16:24:31'/> + </node> + </fields> + </message> + """ + ) + + + def testStringIdsMatcher(self): + """ + test of StringIds follow spec + """ + emptyStringIdXML='<message xmlns="jabber:client"><fields xmlns="urn:xmpp:iot:sensordata" /></message>' + + msg = self.Message() + msg['fields']['stringIds'] = "Nisse" + self.check(msg,emptyStringIdXML) + msg['fields']['stringIds'] = "Nisse___nje#" + self.check(msg,emptyStringIdXML) + msg['fields']['stringIds'] = "1" + self.check(msg,emptyStringIdXML) + + + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestSensorDataStanzas) diff --git a/tests/test_stanza_xep_0325.py b/tests/test_stanza_xep_0325.py new file mode 100644 index 00000000..dc2e8efe --- /dev/null +++ b/tests/test_stanza_xep_0325.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.test import * +import sleekxmpp.plugins.xep_0325 as xep_0325 + +namespace='sn' + +class TestControlStanzas(SleekTest): + + + def setUp(self): + pass + + def testSetRequest(self): + """ + test of set request stanza + """ + iq = self.Iq() + iq['type'] = 'set' + iq['from'] = 'master@clayster.com/amr' + iq['to'] = 'device@clayster.com' + iq['id'] = '1' + iq['set'].add_node("Device02", "Source02", "MyCacheType") + iq['set'].add_node("Device15") + iq['set'].add_data("Tjohej", "boolean", "true") + + self.check(iq,""" + <iq type='set' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <set xmlns='urn:xmpp:iot:control'> + <node nodeId='Device02' sourceId='Source02' cacheType='MyCacheType'/> + <node nodeId='Device15'/> + <boolean name='Tjohej' value='true'/> + </set> + </iq> + """ + ) + + iq['set'].del_node("Device02") + + self.check(iq,""" + <iq type='set' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <set xmlns='urn:xmpp:iot:control'> + <node nodeId='Device15'/> + <boolean name='Tjohej' value='true'/> + </set> + </iq> + """ + ) + + iq['set'].del_nodes() + + self.check(iq,""" + <iq type='set' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <set xmlns='urn:xmpp:iot:control'> + <boolean name='Tjohej' value='true'/> + </set> + </iq> + """ + ) + + + def testDirectSet(self): + """ + test of direct set stanza + """ + msg = self.Message() + msg['from'] = 'master@clayster.com/amr' + msg['to'] = 'device@clayster.com' + msg['set'].add_node("Device02") + msg['set'].add_node("Device15") + msg['set'].add_data("Tjohej", "boolean", "true") + + self.check(msg,""" + <message + from='master@clayster.com/amr' + to='device@clayster.com'> + <set xmlns='urn:xmpp:iot:control'> + <node nodeId='Device02'/> + <node nodeId='Device15'/> + <boolean name='Tjohej' value='true'/> + </set> + </message> + """ + ) + + + def testSetResponse(self): + """ + test of set response stanza + """ + iq = self.Iq() + iq['type'] = 'result' + iq['from'] = 'master@clayster.com/amr' + iq['to'] = 'device@clayster.com' + iq['id'] = '8' + iq['setResponse']['responseCode'] = "OK" + + self.check(iq,""" + <iq type='result' + from='master@clayster.com/amr' + to='device@clayster.com' + id='8'> + <setResponse xmlns='urn:xmpp:iot:control' responseCode='OK' /> + </iq> + """ + ) + + iq = self.Iq() + iq['type'] = 'error' + iq['from'] = 'master@clayster.com/amr' + iq['to'] = 'device@clayster.com' + iq['id'] = '9' + iq['setResponse']['responseCode'] = "OtherError" + iq['setResponse']['error']['var'] = "Output" + iq['setResponse']['error']['text'] = "Test of other error.!" + + self.check(iq,""" + <iq type='error' + from='master@clayster.com/amr' + to='device@clayster.com' + id='9'> + <setResponse xmlns='urn:xmpp:iot:control' responseCode='OtherError'> + <error var='Output'>Test of other error.!</error> + </setResponse> + </iq> + """ + ) + + iq = self.Iq() + iq['type'] = 'error' + iq['from'] = 'master@clayster.com/amr' + iq['to'] = 'device@clayster.com' + iq['id'] = '9' + iq['setResponse']['responseCode'] = "NotFound" + iq['setResponse'].add_node("Device17", "Source09") + iq['setResponse'].add_node("Device18", "Source09") + iq['setResponse'].add_data("Tjohopp") + + self.check(iq,""" + <iq type='error' + from='master@clayster.com/amr' + to='device@clayster.com' + id='9'> + <setResponse xmlns='urn:xmpp:iot:control' responseCode='NotFound'> + <node nodeId='Device17' sourceId='Source09'/> + <node nodeId='Device18' sourceId='Source09'/> + <parameter name='Tjohopp' /> + </setResponse> + </iq> + """ + ) + + def testSetRequestDatas(self): + """ + test of set request data stanzas + """ + iq = self.Iq() + iq['type'] = 'set' + iq['from'] = 'master@clayster.com/amr' + iq['to'] = 'device@clayster.com' + iq['id'] = '1' + iq['set'].add_node("Device02", "Source02", "MyCacheType") + iq['set'].add_node("Device15") + + iq['set'].add_data("Tjohej", "boolean", "true") + iq['set'].add_data("Tjohej2", "boolean", "false") + + iq['set'].add_data("TjohejC", "color", "FF00FF") + iq['set'].add_data("TjohejC2", "color", "00FF00") + + iq['set'].add_data("TjohejS", "string", "String1") + iq['set'].add_data("TjohejS2", "string", "String2") + + iq['set'].add_data("TjohejDate", "date", "2012-01-01") + iq['set'].add_data("TjohejDate2", "date", "1900-12-03") + + iq['set'].add_data("TjohejDateT4", "dateTime", "1900-12-03 12:30") + iq['set'].add_data("TjohejDateT2", "dateTime", "1900-12-03 11:22") + + iq['set'].add_data("TjohejDouble2", "double", "200.22") + iq['set'].add_data("TjohejDouble3", "double", "-12232131.3333") + + iq['set'].add_data("TjohejDur", "duration", "P5Y") + iq['set'].add_data("TjohejDur2", "duration", "PT2M1S") + + iq['set'].add_data("TjohejInt", "int", "1") + iq['set'].add_data("TjohejInt2", "int", "-42") + + iq['set'].add_data("TjohejLong", "long", "123456789098") + iq['set'].add_data("TjohejLong2", "long", "-90983243827489374") + + iq['set'].add_data("TjohejTime", "time", "23:59") + iq['set'].add_data("TjohejTime2", "time", "12:00") + + self.check(iq,""" + <iq type='set' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <set xmlns='urn:xmpp:iot:control'> + <node nodeId='Device02' sourceId='Source02' cacheType='MyCacheType'/> + <node nodeId='Device15'/> + <boolean name='Tjohej' value='true'/> + <boolean name='Tjohej2' value='false'/> + <color name='TjohejC' value='FF00FF'/> + <color name='TjohejC2' value='00FF00'/> + <string name='TjohejS' value='String1'/> + <string name='TjohejS2' value='String2'/> + <date name='TjohejDate' value='2012-01-01'/> + <date name='TjohejDate2' value='1900-12-03'/> + <dateTime name='TjohejDateT4' value='1900-12-03 12:30'/> + <dateTime name='TjohejDateT2' value='1900-12-03 11:22'/> + <double name='TjohejDouble2' value='200.22'/> + <double name='TjohejDouble3' value='-12232131.3333'/> + <duration name='TjohejDur' value='P5Y'/> + <duration name='TjohejDur2' value='PT2M1S'/> + <int name='TjohejInt' value='1'/> + <int name='TjohejInt2' value='-42'/> + <long name='TjohejLong' value='123456789098'/> + <long name='TjohejLong2' value='-90983243827489374'/> + <time name='TjohejTime' value='23:59'/> + <time name='TjohejTime2' value='12:00'/> + </set> + </iq> + """ + ) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestControlStanzas) diff --git a/tests/test_stream.py b/tests/test_stream.py index deac24a5..f68f8426 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -1,5 +1,6 @@ import time -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest class TestStreamTester(SleekTest): diff --git a/tests/test_stream_exceptions.py b/tests/test_stream_exceptions.py index c41edbb2..d18d059a 100644 --- a/tests/test_stream_exceptions.py +++ b/tests/test_stream_exceptions.py @@ -1,9 +1,8 @@ -import sys -import sleekxmpp from sleekxmpp.xmlstream.matcher import MatchXPath from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.exceptions import XMPPError -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest class TestStreamExceptions(SleekTest): diff --git a/tests/test_stream_filters.py b/tests/test_stream_filters.py index ef4d5dc8..ee17ffdc 100644 --- a/tests/test_stream_filters.py +++ b/tests/test_stream_filters.py @@ -1,9 +1,8 @@ import time from sleekxmpp import Message -from sleekxmpp.test import * -from sleekxmpp.xmlstream.handler import * -from sleekxmpp.xmlstream.matcher import * +import unittest +from sleekxmpp.test import SleekTest class TestFilters(SleekTest): @@ -84,5 +83,5 @@ class TestFilters(SleekTest): """) - + suite = unittest.TestLoader().loadTestsFromTestCase(TestFilters) diff --git a/tests/test_stream_handlers.py b/tests/test_stream_handlers.py index 7fd4e648..0208cd16 100644 --- a/tests/test_stream_handlers.py +++ b/tests/test_stream_handlers.py @@ -1,9 +1,10 @@ import time +import threading -from sleekxmpp import Message -from sleekxmpp.test import * -from sleekxmpp.xmlstream.handler import * -from sleekxmpp.xmlstream.matcher import * +import unittest +from sleekxmpp.test import SleekTest +from sleekxmpp.exceptions import IqTimeout +from sleekxmpp import Callback, MatchXPath class TestHandlers(SleekTest): @@ -21,7 +22,7 @@ class TestHandlers(SleekTest): """Test using stream callback handlers.""" def callback_handler(stanza): - self.xmpp.sendRaw(""" + self.xmpp.send_raw(""" <message> <body>Success!</body> </message> @@ -31,7 +32,7 @@ class TestHandlers(SleekTest): MatchXPath('{test}tester'), callback_handler) - self.xmpp.registerHandler(callback) + self.xmpp.register_handler(callback) self.recv("""<tester xmlns="test" />""") @@ -49,7 +50,7 @@ class TestHandlers(SleekTest): iq['query'] = 'test' reply = iq.send(block=True) if reply: - self.xmpp.sendRaw(""" + self.xmpp.send_raw(""" <message> <body>Successful: %s</body> </message> @@ -112,7 +113,7 @@ class TestHandlers(SleekTest): time.sleep(0.1) # Check that the waiter is no longer registered - waiter_exists = self.xmpp.removeHandler('IqWait_test2') + waiter_exists = self.xmpp.remove_handler('IqWait_test2') self.failUnless(waiter_exists == False, "Waiter handler was not removed.") @@ -153,6 +154,35 @@ class TestHandlers(SleekTest): self.failUnless(events == ['foo'], "Iq callback was not executed: %s" % events) + def testIqTimeoutCallback(self): + """Test that iq.send(tcallback=handle_foo, timeout_callback=handle_timeout) works.""" + events = [] + + def handle_foo(iq): + events.append('foo') + + def handle_timeout(iq): + events.append('timeout') + + iq = self.Iq() + iq['type'] = 'get' + iq['id'] = 'test-foo' + iq['to'] = 'user@localhost' + iq['query'] = 'foo' + iq.send(callback=handle_foo, timeout_callback=handle_timeout, timeout=0.05) + + self.send(""" + <iq type="get" id="test-foo" to="user@localhost"> + <query xmlns="foo" /> + </iq> + """) + + # Give event queue time to process + time.sleep(1) + + self.failUnless(events == ['timeout'], + "Iq timeout was not executed: %s" % events) + def testMultipleHandlersForStanza(self): """ Test that multiple handlers for a single stanza work @@ -197,5 +227,57 @@ class TestHandlers(SleekTest): </message> """) + def testWrongSender(self): + """ + Test that using the wrong sender JID in a IQ result + doesn't trigger handlers. + """ + + events = [] + + def run_test(): + # Check that Iq was sent by waiter_handler + iq = self.Iq() + iq['id'] = 'test' + iq['to'] = 'tester@sleekxmpp.com/test' + iq['type'] = 'set' + iq['query'] = 'test' + result = iq.send() + events.append(result['from'].full) + + t = threading.Thread(name="sender_test", target=run_test) + t.start() + + self.recv(""" + <iq id="test" from="evil@sleekxmpp.com/bad" type="result"> + <query xmlns="test" /> + </iq> + """) + self.recv(""" + <iq id="test" from="evil2@sleekxmpp.com" type="result"> + <query xmlns="test" /> + </iq> + """) + self.recv(""" + <iq id="test" from="evil.com" type="result"> + <query xmlns="test" /> + </iq> + """) + + # Now for a good one + self.recv(""" + <iq id="test" from="tester@sleekxmpp.com/test" type="result"> + <query xmlns="test" /> + </iq> + """) + + t.join() + + time.sleep(0.1) + + self.assertEqual(events, ['tester@sleekxmpp.com/test'], "Did not timeout on bad sender") + + + suite = unittest.TestLoader().loadTestsFromTestCase(TestHandlers) diff --git a/tests/test_stream_presence.py b/tests/test_stream_presence.py index 4f2ede16..365a09ed 100644 --- a/tests/test_stream_presence.py +++ b/tests/test_stream_presence.py @@ -1,5 +1,6 @@ import time -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest class TestStreamPresence(SleekTest): diff --git a/tests/test_stream_roster.py b/tests/test_stream_roster.py index 652ea1ce..221954ab 100644 --- a/tests/test_stream_roster.py +++ b/tests/test_stream_roster.py @@ -1,8 +1,9 @@ # -*- encoding:utf-8 -*- - from __future__ import unicode_literals -from sleekxmpp.test import * +import unittest +from sleekxmpp.exceptions import IqTimeout +from sleekxmpp.test import SleekTest import time import threading @@ -19,16 +20,9 @@ class TestStreamRoster(SleekTest): """Test handling roster requests.""" self.stream_start(mode='client', jid='tester@localhost') - events = [] - - def roster_received(iq): - events.append('roster_received') + roster_updates = [] - def roster_update(iq): - events.append('roster_update') - - self.xmpp.add_event_handler('roster_received', roster_received) - self.xmpp.add_event_handler('roster_update', roster_update) + self.xmpp.add_event_handler('roster_update', roster_updates.append) # Since get_roster blocks, we need to run it in a thread. t = threading.Thread(name='get_roster', target=self.xmpp.get_roster) @@ -56,6 +50,9 @@ class TestStreamRoster(SleekTest): # Wait for get_roster to return. t.join() + # Give the event queue time to process. + time.sleep(.1) + self.check_roster('tester@localhost', 'user@localhost', name='User', subscription='from', @@ -63,11 +60,8 @@ class TestStreamRoster(SleekTest): pending_out=True, groups=['Friends', 'Examples']) - # Give the event queue time to process. - time.sleep(.1) - - self.failUnless(events == ['roster_received', 'roster_update'], - "Wrong roster events fired: %s" % events) + self.failUnless(len(roster_updates) == 1, + "Wrong number of roster_update events fired: %s (should be 1)" % len(roster_updates)) def testRosterSet(self): """Test handling pushed roster updates.""" @@ -156,7 +150,7 @@ class TestStreamRoster(SleekTest): """Test rejecting a roster push from an unauthorized source.""" self.stream_start() self.recv(""" - <iq to='tester@localhost' from="malicious_user@localhost" + <iq to='tester@localhost' from="malicious_user@localhost" type="set" id="1"> <query xmlns="jabber:iq:roster"> <item jid="user@localhost" diff --git a/tests/test_stream_xep_0030.py b/tests/test_stream_xep_0030.py index dd43778a..37d29d33 100644 --- a/tests/test_stream_xep_0030.py +++ b/tests/test_stream_xep_0030.py @@ -1,8 +1,8 @@ -import sys import time import threading -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest class TestStreamDisco(SleekTest): diff --git a/tests/test_stream_xep_0047.py b/tests/test_stream_xep_0047.py index d8cdd6a3..0515bca5 100644 --- a/tests/test_stream_xep_0047.py +++ b/tests/test_stream_xep_0047.py @@ -1,11 +1,12 @@ import threading import time -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest class TestInBandByteStreams(SleekTest): - + def setUp(self): self.stream_start(plugins=['xep_0047', 'xep_0030']) @@ -13,7 +14,7 @@ class TestInBandByteStreams(SleekTest): self.stream_close() def testOpenStream(self): - """Test requesting a stream, successfully""" + """Test requesting a stream, successfully""" events = [] @@ -22,7 +23,7 @@ class TestInBandByteStreams(SleekTest): self.xmpp.add_event_handler('ibb_stream_start', on_stream_start) - + t = threading.Thread(name='open_stream', target=self.xmpp['xep_0047'].open_stream, args=('tester@localhost/receiver',), @@ -31,7 +32,7 @@ class TestInBandByteStreams(SleekTest): self.send(""" <iq type="set" to="tester@localhost/receiver" id="1"> - <open xmlns="http://jabber.org/protocol/ibb" + <open xmlns="http://jabber.org/protocol/ibb" sid="testing" block-size="4096" stanza="iq" /> @@ -62,18 +63,18 @@ class TestInBandByteStreams(SleekTest): events.add('callback') self.xmpp.add_event_handler('ibb_stream_start', on_stream_start) - + t = threading.Thread(name='open_stream', target=self.xmpp['xep_0047'].open_stream, args=('tester@localhost/receiver',), - kwargs={'sid': 'testing', + kwargs={'sid': 'testing', 'block': False, 'callback': stream_callback}) t.start() self.send(""" <iq type="set" to="tester@localhost/receiver" id="1"> - <open xmlns="http://jabber.org/protocol/ibb" + <open xmlns="http://jabber.org/protocol/ibb" sid="testing" block-size="4096" stanza="iq" /> @@ -106,7 +107,7 @@ class TestInBandByteStreams(SleekTest): self.xmpp.add_event_handler('ibb_stream_start', on_stream_start) self.xmpp.add_event_handler('ibb_stream_data', on_stream_data) - + t = threading.Thread(name='open_stream', target=self.xmpp['xep_0047'].open_stream, args=('tester@localhost/receiver',), @@ -115,7 +116,7 @@ class TestInBandByteStreams(SleekTest): self.send(""" <iq type="set" to="tester@localhost/receiver" id="1"> - <open xmlns="http://jabber.org/protocol/ibb" + <open xmlns="http://jabber.org/protocol/ibb" sid="testing" block-size="4096" stanza="iq" /> @@ -142,8 +143,8 @@ class TestInBandByteStreams(SleekTest): <iq type="set" id="2" from="tester@localhost" to="tester@localhost/receiver"> - <data xmlns="http://jabber.org/protocol/ibb" - seq="0" + <data xmlns="http://jabber.org/protocol/ibb" + seq="0" sid="testing"> VGVzdGluZw== </data> @@ -161,8 +162,8 @@ class TestInBandByteStreams(SleekTest): <iq type="set" id="A" to="tester@localhost" from="tester@localhost/receiver"> - <data xmlns="http://jabber.org/protocol/ibb" - seq="0" + <data xmlns="http://jabber.org/protocol/ibb" + seq="0" sid="testing"> aXQgd29ya3Mh </data> @@ -174,7 +175,7 @@ class TestInBandByteStreams(SleekTest): to="tester@localhost/receiver" /> """) - self.assertEqual(data, ['it works!']) + self.assertEqual(data, [b'it works!']) suite = unittest.TestLoader().loadTestsFromTestCase(TestInBandByteStreams) diff --git a/tests/test_stream_xep_0050.py b/tests/test_stream_xep_0050.py index 5ad9d6ae..261a0057 100644 --- a/tests/test_stream_xep_0050.py +++ b/tests/test_stream_xep_0050.py @@ -1,8 +1,9 @@ import time import logging -import threading -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin class TestAdHocCommands(SleekTest): @@ -35,7 +36,7 @@ class TestAdHocCommands(SleekTest): logging.debug(initial) new_payload = TestPayload() if initial: - new_payload['bar'] = 'Received: %s' % initial['bar'] + new_payload['bar'] = 'Received: %s' % initial['bar'] else: new_payload['bar'] = 'Failed' diff --git a/tests/test_stream_xep_0059.py b/tests/test_stream_xep_0059.py index 3a99842b..5f3ea079 100644 --- a/tests/test_stream_xep_0059.py +++ b/tests/test_stream_xep_0059.py @@ -1,6 +1,7 @@ import threading -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.plugins.xep_0030 import DiscoItems from sleekxmpp.plugins.xep_0059 import ResultIterator, Set @@ -17,7 +18,7 @@ class TestStreamSet(SleekTest): def iter(self, rev=False): q = self.xmpp.Iq() q['type'] = 'get' - it = ResultIterator(q, 'disco_items', '1', reverse=rev) + it = ResultIterator(q, 'disco_items', amount='1', reverse=rev) for i in it: for j in i['disco_items']['items']: self.items.append(j[0]) diff --git a/tests/test_stream_xep_0060.py b/tests/test_stream_xep_0060.py index e0936660..581d5d00 100644 --- a/tests/test_stream_xep_0060.py +++ b/tests/test_stream_xep_0060.py @@ -1,8 +1,7 @@ -import sys -import time import threading -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest from sleekxmpp.stanza.atom import AtomEntry from sleekxmpp.xmlstream import register_stanza_plugin @@ -431,7 +430,7 @@ class TestStreamPubsub(SleekTest): </publish> </pubsub> </iq> - """) + """, use_values=False) def testPublishSingleOptions(self): """Test publishing a single item, with options.""" diff --git a/tests/test_stream_xep_0066.py b/tests/test_stream_xep_0066.py index e3f2ddfa..175026d2 100644 --- a/tests/test_stream_xep_0066.py +++ b/tests/test_stream_xep_0066.py @@ -1,7 +1,7 @@ -import time import threading -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest class TestOOB(SleekTest): diff --git a/tests/test_stream_xep_0085.py b/tests/test_stream_xep_0085.py index 2a814805..54e7e15f 100644 --- a/tests/test_stream_xep_0085.py +++ b/tests/test_stream_xep_0085.py @@ -1,7 +1,7 @@ -import threading import time -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest class TestStreamChatStates(SleekTest): diff --git a/tests/test_stream_xep_0092.py b/tests/test_stream_xep_0092.py index 4a038558..c0748697 100644 --- a/tests/test_stream_xep_0092.py +++ b/tests/test_stream_xep_0092.py @@ -1,6 +1,7 @@ import threading -from sleekxmpp.test import * +import unittest +from sleekxmpp.test import SleekTest class TestStreamSet(SleekTest): @@ -36,7 +37,9 @@ class TestStreamSet(SleekTest): def query(): r = self.xmpp['xep_0092'].get_version('foo@bar') - results.append(r) + results.append((r['software_version']['name'], + r['software_version']['version'], + r['software_version']['os'])) self.stream_start(mode='client', plugins=['xep_0030', 'xep_0092']) @@ -61,7 +64,7 @@ class TestStreamSet(SleekTest): t.join() - expected = [{'name': 'Foo', 'version': '1.0', 'os':'Linux'}] + expected = [('Foo', '1.0', 'Linux')] self.assertEqual(results, expected, "Did not receive expected results: %s" % results) diff --git a/tests/test_stream_xep_0128.py b/tests/test_stream_xep_0128.py index 42fc9143..10222d9b 100644 --- a/tests/test_stream_xep_0128.py +++ b/tests/test_stream_xep_0128.py @@ -1,9 +1,5 @@ -import sys -import time -import threading - -from sleekxmpp.test import * -from sleekxmpp.xmlstream import ElementBase +import unittest +from sleekxmpp.test import SleekTest class TestStreamExtendedDisco(SleekTest): diff --git a/tests/test_stream_xep_0249.py b/tests/test_stream_xep_0249.py index 9a25253f..8edea270 100644 --- a/tests/test_stream_xep_0249.py +++ b/tests/test_stream_xep_0249.py @@ -1,9 +1,7 @@ -import sys import time -import threading -from sleekxmpp.test import * -from sleekxmpp.xmlstream import ElementBase +import unittest +from sleekxmpp.test import SleekTest class TestStreamDirectInvite(SleekTest): diff --git a/tests/test_stream_xep_0323.py b/tests/test_stream_xep_0323.py new file mode 100644 index 00000000..94f1d638 --- /dev/null +++ b/tests/test_stream_xep_0323.py @@ -0,0 +1,1250 @@ +# -*- coding: utf-8 -*- + +import sys +import datetime +import time +import threading + +from sleekxmpp.test import * +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins.xep_0323.device import Device + + +class TestStreamSensorData(SleekTest): + + """ + Test using the XEP-0323 plugin. + """ + def setUp(self): + pass + + def _time_now(self): + return datetime.datetime.now().replace(microsecond=0).isoformat() + + def tearDown(self): + self.stream_close() + + def testRequestAccept(self): + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + myDevice = Device("Device22") + myDevice._add_field(name="Temperature", typename="numeric", unit="°C") + myDevice._set_momentary_timestamp("2013-03-07T16:24:30") + myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"}) + + self.xmpp['xep_0323'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1' momentary='true'/> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='1'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='1'/> + </iq> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='1' done='true'> + <node nodeId='Device22'> + <timestamp value='2013-03-07T16:24:30'> + <numeric name='Temperature' momentary='true' automaticReadout='true' value='23.4' unit='°C'/> + </timestamp> + </node> + </fields> + </message> + """) + + def testRequestRejectAuth(self): + + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + self.xmpp['xep_0323']._set_authenticated("darth@deathstar.com") + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='4'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='5' momentary='true'/> + </iq> + """) + + self.send(""" + <iq type='error' + from='device@clayster.com' + to='master@clayster.com/amr' + id='4'> + <rejected xmlns='urn:xmpp:iot:sensordata' seqnr='5'> + <error>Access denied</error> + </rejected> + </iq> + """) + + def testRequestNode(self): + + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + myDevice = Device("Device44") + self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) + + print("."), + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='77'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='66' momentary='true'> + <node nodeId='Device33'/> + </req> + </iq> + """) + + self.send(""" + <iq type='error' + from='device@clayster.com' + to='master@clayster.com/amr' + id='77'> + <rejected xmlns='urn:xmpp:iot:sensordata' seqnr='66'> + <error>Invalid nodeId Device33</error> + </rejected> + </iq> + """) + + print("."), + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='8'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='7' momentary='true'> + <node nodeId='Device44'/> + </req> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='8'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='7'/> + </iq> + """) + + + def testRequestField(self): + + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + myDevice = Device("Device44") + myDevice._add_field(name='Voltage', typename="numeric", unit="V") + myDevice._add_field_timestamp_data(name="Voltage", value="230.4", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) + + self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) + + print("."), + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='7'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='6'> + <field name='Current'/> + </req> + </iq> + """) + + self.send(""" + <iq type='error' + from='device@clayster.com' + to='master@clayster.com/amr' + id='7'> + <rejected xmlns='urn:xmpp:iot:sensordata' seqnr='6'> + <error>Invalid field Current</error> + </rejected> + </iq> + """) + + print("."), + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='8'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='7'> + <field name='Voltage'/> + </req> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='8'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='7'/> + </iq> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='7'> + <node nodeId='Device44'> + <timestamp value='2000-01-01T00:01:02'> + <numeric name='Voltage' invoiced='true' value='230.4' unit='V'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='7' done='true'> + </fields> + </message> + """) + + def testRequestMultiTimestampSingleField(self): + + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + myDevice = Device("Device44") + myDevice._add_field(name='Voltage', typename="numeric", unit="V") + myDevice._add_field_timestamp_data(name="Voltage", value="230.4", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) + myDevice._add_field(name='Current', typename="numeric", unit="A") + myDevice._add_field(name='Height', typename="string") + myDevice._add_field_timestamp_data(name="Voltage", value="230.6", timestamp="2000-01-01T01:01:02") + myDevice._add_field_timestamp_data(name="Height", value="115 m", timestamp="2000-01-01T01:01:02", flags={"invoiced": "true"}) + + self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) + + print("."), + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='8'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='7'> + <field name='Voltage'/> + </req> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='8'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='7'/> + </iq> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='7'> + <node nodeId='Device44'> + <timestamp value='2000-01-01T00:01:02'> + <numeric name='Voltage' invoiced='true' value='230.4' unit='V'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='7'> + <node nodeId='Device44'> + <timestamp value='2000-01-01T01:01:02'> + <numeric name='Voltage' value='230.6' unit='V'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='7' done='true'> + </fields> + </message> + """) + + def testRequestMultiTimestampAllFields(self): + + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + myDevice = Device("Device44") + myDevice._add_field(name='Voltage', typename="numeric", unit="V") + myDevice._add_field_timestamp_data(name="Voltage", value="230.4", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) + myDevice._add_field(name='Current', typename="numeric", unit="A") + myDevice._add_field(name='Height', typename="string") + myDevice._add_field_timestamp_data(name="Voltage", value="230.6", timestamp="2000-01-01T01:01:02") + myDevice._add_field_timestamp_data(name="Height", value="115 m", timestamp="2000-01-01T01:01:02", flags={"invoiced": "true"}) + + self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) + + print("."), + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='8'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='7'/> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='8'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='7'/> + </iq> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='7'> + <node nodeId='Device44'> + <timestamp value='2000-01-01T00:01:02'> + <numeric name='Voltage' invoiced='true' value='230.4' unit='V'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='7'> + <node nodeId='Device44'> + <timestamp value='2000-01-01T01:01:02'> + <numeric name='Voltage' value='230.6' unit='V'/> + <string name='Height' invoiced='true' value='115 m'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='7' done='true'> + </fields> + </message> + """) + + def testRequestAPI(self): + + self.stream_start(mode='client', + plugins=['xep_0030', + 'xep_0323']) + + self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", callback=None) + + self.send(""" + <iq type='get' + from='tester@localhost' + to='you@google.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1'/> + </iq> + """) + + self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33', 'Device22'], callback=None) + + self.send(""" + <iq type='get' + from='tester@localhost' + to='you@google.com' + id='2'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='2'> + <node nodeId="Device33"/> + <node nodeId="Device22"/> + </req> + </iq> + """) + + self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", fields=['Temperature', 'Voltage'], callback=None) + + self.send(""" + <iq type='get' + from='tester@localhost' + to='you@google.com' + id='3'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='3'> + <field name="Temperature"/> + <field name="Voltage"/> + </req> + </iq> + """) + + def testRequestRejectAPI(self): + + self.stream_start(mode='client', + plugins=['xep_0030', + 'xep_0323']) + + results = [] + + def my_callback(from_jid, result, nodeId=None, timestamp=None, fields=None, error_msg=None): + if (result == "rejected") and (error_msg == "Invalid device Device22"): + results.append("rejected") + + self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33', 'Device22'], callback=my_callback) + + self.send(""" + <iq type='get' + from='tester@localhost' + to='you@google.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId="Device33"/> + <node nodeId="Device22"/> + </req> + </iq> + """) + + self.recv(""" + <iq type='error' + from='you@google.com' + to='tester@localhost' + id='1'> + <rejected xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <error>Invalid device Device22</error> + </rejected> + </iq> + """) + + time.sleep(.1) + + self.failUnless(results == ["rejected"], + "Rejected callback was not properly executed") + + def testRequestAcceptedAPI(self): + + self.stream_start(mode='client', + plugins=['xep_0030', + 'xep_0323']) + + results = [] + + def my_callback(from_jid, result, nodeId=None, timestamp=None, fields=None, error_msg=None): + results.append(result) + + self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33', 'Device22'], callback=my_callback) + + self.send(""" + <iq type='get' + from='tester@localhost' + to='you@google.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId="Device33"/> + <node nodeId="Device22"/> + </req> + </iq> + """) + + self.recv(""" + <iq type='result' + from='you@google.com' + to='tester@localhost' + id='1'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='1'/> + </iq> + """) + + time.sleep(.1) + + self.failUnless(results == ["accepted"], + "Accepted callback was not properly executed") + + def testRequestFieldsAPI(self): + + self.stream_start(mode='client', + plugins=['xep_0030', + 'xep_0323']) + + results = [] + callback_data = {} + + def my_callback(from_jid, result, nodeId=None, timestamp=None, fields=None, error_msg=None): + results.append(result) + if result == "fields": + callback_data["nodeId"] = nodeId + callback_data["timestamp"] = timestamp + callback_data["error_msg"] = error_msg + for f in fields: + callback_data["field_" + f['name']] = f + + t1= threading.Thread(name="request_data", + target=self.xmpp['xep_0323'].request_data, + kwargs={"from_jid": "tester@localhost", + "to_jid": "you@google.com", + "nodeIds": ['Device33'], + "callback": my_callback}) + t1.start() + #self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33'], callback=my_callback); + + self.send(""" + <iq type='get' + from='tester@localhost' + to='you@google.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId="Device33"/> + </req> + </iq> + """) + + self.recv(""" + <iq type='result' + from='you@google.com' + to='tester@localhost' + id='1'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='1'/> + </iq> + """) + + self.recv(""" + <message from='you@google.com' + to='tester@localhost'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId='Device33'> + <timestamp value='2000-01-01T00:01:02'> + <numeric name='Voltage' invoiced='true' value='230.4' unit='V'/> + <boolean name='TestBool' value='true'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.recv(""" + <message from='you@google.com' + to='tester@localhost'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='1' done='true'/> + </message> + """) + + t1.join() + time.sleep(.5) + + self.failUnlessEqual(results, ["accepted","fields","done"]) + # self.assertIn("nodeId", callback_data); + self.assertTrue("nodeId" in callback_data) + self.failUnlessEqual(callback_data["nodeId"], "Device33") + # self.assertIn("timestamp", callback_data); + self.assertTrue("timestamp" in callback_data) + self.failUnlessEqual(callback_data["timestamp"], "2000-01-01T00:01:02") + #self.assertIn("field_Voltage", callback_data); + self.assertTrue("field_Voltage" in callback_data) + self.failUnlessEqual(callback_data["field_Voltage"], {"name": "Voltage", "value": "230.4", "typename": "numeric", "unit": "V", "flags": {"invoiced": "true"}}) + #self.assertIn("field_TestBool", callback_data); + self.assertTrue("field_TestBool" in callback_data) + self.failUnlessEqual(callback_data["field_TestBool"], {"name": "TestBool", "value": "true", "typename": "boolean" }) + + def testServiceDiscoveryClient(self): + self.stream_start(mode='client', + plugins=['xep_0030', + 'xep_0323']) + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='tester@localhost' + id='disco1'> + <query xmlns='http://jabber.org/protocol/disco#info'/> + </iq> + """) + + self.send(""" + <iq type='result' + to='master@clayster.com/amr' + id='disco1'> + <query xmlns='http://jabber.org/protocol/disco#info'> + <identity category='client' type='bot'/> + <feature var='urn:xmpp:iot:sensordata'/> + </query> + </iq> + """) + + def testServiceDiscoveryComponent(self): + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='tester@localhost' + id='disco1'> + <query xmlns='http://jabber.org/protocol/disco#info'/> + </iq> + """) + + self.send(""" + <iq type='result' + from='tester@localhost' + to='master@clayster.com/amr' + id='disco1'> + <query xmlns='http://jabber.org/protocol/disco#info'> + <identity category='component' type='generic'/> + <feature var='urn:xmpp:iot:sensordata'/> + </query> + </iq> + """) + + def testRequestTimeout(self): + + self.stream_start(mode='client', + plugins=['xep_0030', + 'xep_0323']) + + results = [] + callback_data = {} + + def my_callback(from_jid, result, nodeId=None, timestamp=None, error_msg=None): + results.append(result) + if result == "failure": + callback_data["nodeId"] = nodeId + callback_data["timestamp"] = timestamp + callback_data["error_msg"] = error_msg + + t1= threading.Thread(name="request_data", + target=self.xmpp['xep_0323'].request_data, + kwargs={"from_jid": "tester@localhost", + "to_jid": "you@google.com", + "nodeIds": ['Device33'], + "callback": my_callback}) + t1.start() + + self.send(""" + <iq type='get' + from='tester@localhost' + to='you@google.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId="Device33"/> + </req> + </iq> + """) + + self.recv(""" + <iq type='result' + from='you@google.com' + to='tester@localhost' + id='1'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='1'/> + </iq> + """) + + self.recv(""" + <message from='you@google.com' + to='tester@localhost'> + <failure xmlns='urn:xmpp:iot:sensordata' seqnr='1' done='true'> + <error nodeId='Device33' timestamp='2013-03-07T17:13:30'>Timeout.</error> + </failure> + </message> + """) + + t1.join() + time.sleep(.5) + + self.failUnlessEqual(results, ["accepted","failure"]) + # self.assertIn("nodeId", callback_data); + self.assertTrue("nodeId" in callback_data) + self.failUnlessEqual(callback_data["nodeId"], "Device33") + # self.assertIn("timestamp", callback_data); + self.assertTrue("timestamp" in callback_data) + self.failUnlessEqual(callback_data["timestamp"], "2013-03-07T17:13:30") + # self.assertIn("error_msg", callback_data); + self.assertTrue("error_msg" in callback_data) + self.failUnlessEqual(callback_data["error_msg"], "Timeout.") + + def testDelayedRequest(self): + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + myDevice = Device("Device22") + myDevice._add_field(name="Temperature", typename="numeric", unit="°C") + myDevice._set_momentary_timestamp("2013-03-07T16:24:30") + myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"}) + + self.xmpp['xep_0323'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) + + dtnow = datetime.datetime.now() + ts_2sec = datetime.timedelta(0,2) + dtnow_plus_2sec = dtnow + ts_2sec + when_flag = dtnow_plus_2sec.replace(microsecond=0).isoformat() + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1' momentary='true' when='""" + when_flag + """'/> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='1'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='1' queued='true' /> + </iq> + """) + + time.sleep(2) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <started xmlns='urn:xmpp:iot:sensordata' seqnr='1' /> + </message> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='1' done='true'> + <node nodeId='Device22'> + <timestamp value='2013-03-07T16:24:30'> + <numeric name='Temperature' momentary='true' automaticReadout='true' value='23.4' unit='°C'/> + </timestamp> + </node> + </fields> + </message> + """) + + def testDelayedRequestFail(self): + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + myDevice = Device("Device22") + myDevice._add_field(name="Temperature", typename="numeric", unit="°C") + myDevice._set_momentary_timestamp("2013-03-07T16:24:30") + myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"}) + + self.xmpp['xep_0323'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) + + dtnow = datetime.datetime.now() + ts_2sec = datetime.timedelta(0,2) + dtnow_minus_2sec = dtnow - ts_2sec + when_flag = dtnow_minus_2sec.replace(microsecond=0).isoformat() + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1' momentary='true' when='""" + when_flag + """'/> + </iq> + """) + + # Remove the returned datetime to allow predictable test + xml_stanza = self._filtered_stanza_prepare() + error_text = xml_stanza['rejected']['error'] #['text'] + error_text = error_text[:error_text.find(':')] + xml_stanza['rejected']['error'] = error_text + + self._filtered_stanza_check(""" + <iq type='error' + from='device@clayster.com' + to='master@clayster.com/amr' + id='1'> + <rejected xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <error>Invalid datetime in 'when' flag, cannot set a time in the past. Current time</error> + </rejected> + </iq> + """, xml_stanza) + + + def _filtered_stanza_prepare(self, timeout=.5): + sent = self.xmpp.socket.next_sent(timeout) + if sent is None: + self.fail("No stanza was sent.") + + xml = self.parse_xml(sent) + self.fix_namespaces(xml, 'jabber:client') + sent = self.xmpp._build_stanza(xml, 'jabber:client') + return sent + + def _filtered_stanza_check(self, data, filtered, defaults=None, use_values=True, method='exact'): + self.check(filtered, data, + method=method, + defaults=defaults, + use_values=use_values) + + def testRequestFieldFrom(self): + + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + myDevice = Device("Device44") + myDevice._add_field(name='Voltage', typename="numeric", unit="V") + myDevice._add_field_timestamp_data(name="Voltage", value="230.1", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) + myDevice._add_field_timestamp_data(name="Voltage", value="230.2", timestamp="2000-02-01T00:01:02", flags={"invoiced": "true"}) + myDevice._add_field_timestamp_data(name="Voltage", value="230.3", timestamp="2000-03-01T00:01:02", flags={"invoiced": "true"}) + + self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) + + print("."), + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='6'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='6' from='2000-01-02T00:00:01'> + <field name='Voltage'/> + </req> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='6'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='6'/> + </iq> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='6'> + <node nodeId='Device44'> + <timestamp value='2000-02-01T00:01:02'> + <numeric name='Voltage' invoiced='true' value='230.2' unit='V'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='6'> + <node nodeId='Device44'> + <timestamp value='2000-03-01T00:01:02'> + <numeric name='Voltage' invoiced='true' value='230.3' unit='V'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='6' done='true'> + </fields> + </message> + """) + + def testRequestFieldTo(self): + + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + myDevice = Device("Device44") + myDevice._add_field(name='Voltage', typename="numeric", unit="V") + myDevice._add_field_timestamp_data(name="Voltage", value="230.1", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) + myDevice._add_field_timestamp_data(name="Voltage", value="230.2", timestamp="2000-02-01T00:01:02", flags={"invoiced": "true"}) + myDevice._add_field_timestamp_data(name="Voltage", value="230.3", timestamp="2000-03-01T00:01:02", flags={"invoiced": "true"}) + + self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) + + print("."), + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='6'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='6' to='2000-02-02T00:00:01'> + <field name='Voltage'/> + </req> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='6'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='6'/> + </iq> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='6'> + <node nodeId='Device44'> + <timestamp value='2000-01-01T00:01:02'> + <numeric name='Voltage' invoiced='true' value='230.1' unit='V'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='6'> + <node nodeId='Device44'> + <timestamp value='2000-02-01T00:01:02'> + <numeric name='Voltage' invoiced='true' value='230.2' unit='V'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='6' done='true'> + </fields> + </message> + """) + + def testRequestFieldFromTo(self): + + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + myDevice = Device("Device44") + myDevice._add_field(name='Voltage', typename="numeric", unit="V") + myDevice._add_field_timestamp_data(name="Voltage", value="230.1", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) + myDevice._add_field_timestamp_data(name="Voltage", value="230.2", timestamp="2000-02-01T00:01:02", flags={"invoiced": "true"}) + myDevice._add_field_timestamp_data(name="Voltage", value="230.3", timestamp="2000-03-01T00:01:02", flags={"invoiced": "true"}) + + self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) + + print("."), + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='6'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='6' from='2000-01-01T00:01:03' to='2000-02-02T00:00:01'> + <field name='Voltage'/> + </req> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='6'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='6'/> + </iq> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='6'> + <node nodeId='Device44'> + <timestamp value='2000-02-01T00:01:02'> + <numeric name='Voltage' invoiced='true' value='230.2' unit='V'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.send(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='6' done='true'> + </fields> + </message> + """) + + def testDelayedRequestClient(self): + self.stream_start(mode='client', + plugins=['xep_0030', + 'xep_0323']) + + results = [] + callback_data = {} + + def my_callback(from_jid, result, nodeId=None, timestamp=None, fields=None, error_msg=None): + results.append(result) + if result == "fields": + callback_data["nodeId"] = nodeId + callback_data["timestamp"] = timestamp + callback_data["error_msg"] = error_msg + for f in fields: + callback_data["field_" + f['name']] = f + + t1= threading.Thread(name="request_data", + target=self.xmpp['xep_0323'].request_data, + kwargs={"from_jid": "tester@localhost", + "to_jid": "you@google.com", + "nodeIds": ['Device33'], + "callback": my_callback}) + t1.start() + #self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33'], callback=my_callback); + + self.send(""" + <iq type='get' + from='tester@localhost' + to='you@google.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId="Device33"/> + </req> + </iq> + """) + + self.recv(""" + <iq type='result' + from='you@google.com' + to='tester@localhost' + id='1'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='1' queued='true'/> + </iq> + """) + + self.recv(""" + <message from='device@clayster.com' + to='master@clayster.com/amr'> + <started xmlns='urn:xmpp:iot:sensordata' seqnr='1' /> + </message> + """) + + self.recv(""" + <message from='you@google.com' + to='tester@localhost'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId='Device33'> + <timestamp value='2000-01-01T00:01:02'> + <numeric name='Voltage' invoiced='true' value='230.4' unit='V'/> + <boolean name='TestBool' value='true'/> + </timestamp> + </node> + </fields> + </message> + """) + + self.recv(""" + <message from='you@google.com' + to='tester@localhost'> + <fields xmlns='urn:xmpp:iot:sensordata' seqnr='1' done='true'/> + </message> + """) + + t1.join() + time.sleep(.5) + + self.failUnlessEqual(results, ["queued","started","fields","done"]) + # self.assertIn("nodeId", callback_data); + self.assertTrue("nodeId" in callback_data) + self.failUnlessEqual(callback_data["nodeId"], "Device33") + # self.assertIn("timestamp", callback_data); + self.assertTrue("timestamp" in callback_data) + self.failUnlessEqual(callback_data["timestamp"], "2000-01-01T00:01:02") + # self.assertIn("field_Voltage", callback_data); + self.assertTrue("field_Voltage" in callback_data) + self.failUnlessEqual(callback_data["field_Voltage"], {"name": "Voltage", "value": "230.4", "typename": "numeric", "unit": "V", "flags": {"invoiced": "true"}}) + # self.assertIn("field_TestBool", callback_data); + self.assertTrue("field_TestBool" in callback_data) + self.failUnlessEqual(callback_data["field_TestBool"], {"name": "TestBool", "value": "true", "typename": "boolean" }) + + + def testRequestFieldsCancelAPI(self): + + self.stream_start(mode='client', + plugins=['xep_0030', + 'xep_0323']) + + results = [] + + def my_callback(from_jid, result, nodeId=None, timestamp=None, fields=None, error_msg=None): + results.append(result) + + session = self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33'], callback=my_callback) + + self.send(""" + <iq type='get' + from='tester@localhost' + to='you@google.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <node nodeId="Device33"/> + </req> + </iq> + """) + + self.recv(""" + <iq type='result' + from='you@google.com' + to='tester@localhost' + id='1'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='1'/> + </iq> + """) + + self.xmpp['xep_0323'].cancel_request(session=session) + + self.send(""" + <iq type='get' + from='tester@localhost' + to='you@google.com' + id='1'> + <cancel xmlns='urn:xmpp:iot:sensordata' seqnr='1' /> + </iq> + """) + + self.recv(""" + <iq type='result' + from='tester@localhost' + to='you@google.com' + id='1'> + <cancelled xmlns='urn:xmpp:iot:sensordata' seqnr='1' /> + </iq> + """) + + time.sleep(.5) + + self.failUnlessEqual(results, ["accepted","cancelled"]) + + def testDelayedRequestCancel(self): + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0323']) + + myDevice = Device("Device22") + myDevice._add_field(name="Temperature", typename="numeric", unit="°C") + myDevice._set_momentary_timestamp("2013-03-07T16:24:30") + myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"}) + + self.xmpp['xep_0323'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) + + dtnow = datetime.datetime.now() + ts_2sec = datetime.timedelta(0,2) + dtnow_plus_2sec = dtnow + ts_2sec + when_flag = dtnow_plus_2sec.replace(microsecond=0).isoformat() + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <req xmlns='urn:xmpp:iot:sensordata' seqnr='1' momentary='true' when='""" + when_flag + """'/> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='1'> + <accepted xmlns='urn:xmpp:iot:sensordata' seqnr='1' queued='true' /> + </iq> + """) + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <cancel xmlns='urn:xmpp:iot:sensordata' seqnr='1' /> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='1'> + <cancelled xmlns='urn:xmpp:iot:sensordata' seqnr='1' /> + </iq> + """) + + # Test cancel of non-existing request + self.recv(""" + <iq type='get' + from='tester@localhost' + to='you@google.com' + id='1'> + <cancel xmlns='urn:xmpp:iot:sensordata' seqnr='1' /> + </iq> + """) + + self.send(""" + <iq type='error' + from='you@google.com' + to='tester@localhost' + id='1'> + <rejected xmlns='urn:xmpp:iot:sensordata' seqnr='1'> + <error>Cancel request received, no matching request is active.</error> + </rejected> + </iq> + """) + + time.sleep(2) + + # Ensure we don't get anything after cancellation + self.send(None) + + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamSensorData) + diff --git a/tests/test_stream_xep_0325.py b/tests/test_stream_xep_0325.py new file mode 100644 index 00000000..2ebdd121 --- /dev/null +++ b/tests/test_stream_xep_0325.py @@ -0,0 +1,365 @@ +# -*- coding: utf-8 -*- +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import sys +import datetime +import time +import threading + +from sleekxmpp.test import * +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins.xep_0325.device import Device + + +class TestStreamControl(SleekTest): + + """ + Test using the XEP-0325 plugin. + """ + def setUp(self): + pass + + def _time_now(self): + return datetime.datetime.now().replace(microsecond=0).isoformat() + + def tearDown(self): + self.stream_close() + + def testRequestSetOk(self): + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0325']) + + myDevice = Device("Device22") + myDevice._add_control_field(name="Temperature", typename="int", value="15") + + self.xmpp['xep_0325'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) + + self.recv(""" + <iq type='set' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <set xmlns='urn:xmpp:iot:control'> + <int name="Temperature" value="17"/> + </set> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='1'> + <setResponse xmlns='urn:xmpp:iot:control' responseCode="OK" /> + </iq> + """) + + self.assertEqual(myDevice._get_field_value("Temperature"), "17") + + def testRequestSetMulti(self): + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0325']) + + myDevice = Device("Device22") + myDevice._add_control_field(name="Temperature", typename="int", value="15") + myDevice._add_control_field(name="Startup", typename="date", value="2013-01-03") + + myDevice2 = Device("Device23") + myDevice2._add_control_field(name="Temperature", typename="int", value="19") + myDevice2._add_control_field(name="Startup", typename="date", value="2013-01-09") + + self.xmpp['xep_0325'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) + self.xmpp['xep_0325'].register_node(nodeId="Device23", device=myDevice2, commTimeout=0.5) + + self.recv(""" + <iq type='set' + from='master@clayster.com/amr' + to='device@clayster.com' + id='1'> + <set xmlns='urn:xmpp:iot:control'> + <node nodeId='Device22' /> + <int name="Temperature" value="17"/> + </set> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='1'> + <setResponse xmlns='urn:xmpp:iot:control' responseCode="OK" /> + </iq> + """) + + self.assertEqual(myDevice._get_field_value("Temperature"), "17") + self.assertEqual(myDevice2._get_field_value("Temperature"), "19") + + self.recv(""" + <iq type='set' + from='master@clayster.com/amr' + to='device@clayster.com' + id='2'> + <set xmlns='urn:xmpp:iot:control'> + <node nodeId='Device23' /> + <node nodeId='Device22' /> + <date name="Startup" value="2013-02-01"/> + <int name="Temperature" value="20"/> + </set> + </iq> + """) + + self.send(""" + <iq type='result' + from='device@clayster.com' + to='master@clayster.com/amr' + id='2'> + <setResponse xmlns='urn:xmpp:iot:control' responseCode="OK" /> + </iq> + """) + + self.assertEqual(myDevice._get_field_value("Temperature"), "20") + self.assertEqual(myDevice2._get_field_value("Temperature"), "20") + self.assertEqual(myDevice._get_field_value("Startup"), "2013-02-01") + self.assertEqual(myDevice2._get_field_value("Startup"), "2013-02-01") + + def testRequestSetFail(self): + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0325']) + + myDevice = Device("Device23") + myDevice._add_control_field(name="Temperature", typename="int", value="15") + + self.xmpp['xep_0325'].register_node(nodeId="Device23", device=myDevice, commTimeout=0.5) + + self.recv(""" + <iq type='set' + from='master@clayster.com/amr' + to='device@clayster.com' + id='9'> + <set xmlns='urn:xmpp:iot:control'> + <int name="Voltage" value="17"/> + </set> + </iq> + """) + + self.send(""" + <iq type='error' + from='device@clayster.com' + to='master@clayster.com/amr' + id='9'> + <setResponse xmlns='urn:xmpp:iot:control' responseCode='NotFound'> + <parameter name='Voltage' /> + <error var='Output'>Invalid field Voltage</error> + </setResponse> + </iq> + """) + + self.assertEqual(myDevice._get_field_value("Temperature"), "15") + self.assertFalse(myDevice.has_control_field("Voltage", "int")) + + def testDirectSetOk(self): + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0325']) + + myDevice = Device("Device22") + myDevice._add_control_field(name="Temperature", typename="int", value="15") + + self.xmpp['xep_0325'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) + + self.recv(""" + <message + from='master@clayster.com/amr' + to='device@clayster.com'> + <set xmlns='urn:xmpp:iot:control'> + <int name="Temperature" value="17"/> + </set> + </message> + """) + + time.sleep(.5) + + self.assertEqual(myDevice._get_field_value("Temperature"), "17") + + def testDirectSetFail(self): + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0325']) + + myDevice = Device("Device22") + myDevice._add_control_field(name="Temperature", typename="int", value="15") + + self.xmpp['xep_0325'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) + + self.recv(""" + <message + from='master@clayster.com/amr' + to='device@clayster.com'> + <set xmlns='urn:xmpp:iot:control'> + <int name="Voltage" value="17"/> + </set> + </message> + """) + + time.sleep(.5) + + self.assertEqual(myDevice._get_field_value("Temperature"), "15") + self.assertFalse(myDevice.has_control_field("Voltage", "int")) + + + def testRequestSetOkAPI(self): + + self.stream_start(mode='client', + plugins=['xep_0030', + 'xep_0325']) + + results = [] + + def my_callback(from_jid, result, nodeIds=None, fields=None, error_msg=None): + results.append(result) + + fields = [] + fields.append(("Temperature", "double", "20.5")) + fields.append(("TemperatureAlarmSetting", "string", "High")) + + self.xmpp['xep_0325'].set_request(from_jid="tester@localhost", to_jid="you@google.com", fields=fields, nodeIds=['Device33', 'Device22'], callback=my_callback) + + self.send(""" + <iq type='set' + from='tester@localhost' + to='you@google.com' + id='1'> + <set xmlns='urn:xmpp:iot:control'> + <node nodeId='Device33' /> + <node nodeId='Device22' /> + <double name="Temperature" value="20.5" /> + <string name="TemperatureAlarmSetting" value="High" /> + </set> + </iq> + """) + + self.recv(""" + <iq type='result' + from='you@google.com' + to='tester@localhost' + id='1'> + <setResponse xmlns='urn:xmpp:iot:control' responseCode="OK" /> + </iq> + """) + + time.sleep(.5) + + self.assertEqual(results, ["OK"]) + + def testRequestSetErrorAPI(self): + + self.stream_start(mode='client', + plugins=['xep_0030', + 'xep_0325']) + + results = [] + + def my_callback(from_jid, result, nodeIds=None, fields=None, error_msg=None): + results.append(result) + + fields = [] + fields.append(("Temperature", "double", "20.5")) + fields.append(("TemperatureAlarmSetting", "string", "High")) + + self.xmpp['xep_0325'].set_request(from_jid="tester@localhost", to_jid="you@google.com", fields=fields, nodeIds=['Device33', 'Device22'], callback=my_callback) + + self.send(""" + <iq type='set' + from='tester@localhost' + to='you@google.com' + id='1'> + <set xmlns='urn:xmpp:iot:control'> + <node nodeId='Device33' /> + <node nodeId='Device22' /> + <double name="Temperature" value="20.5" /> + <string name="TemperatureAlarmSetting" value="High" /> + </set> + </iq> + """) + + self.recv(""" + <iq type='error' + from='you@google.com' + to='tester@localhost' + id='1'> + <setResponse xmlns='urn:xmpp:iot:control' responseCode="OtherError" > + <error var='Temperature'>Sensor error</error> + </setResponse> + </iq> + """) + + time.sleep(.5) + + self.assertEqual(results, ["OtherError"]) + + def testServiceDiscoveryClient(self): + self.stream_start(mode='client', + plugins=['xep_0030', + 'xep_0325']) + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='tester@localhost' + id='disco1'> + <query xmlns='http://jabber.org/protocol/disco#info'/> + </iq> + """) + + self.send(""" + <iq type='result' + to='master@clayster.com/amr' + id='disco1'> + <query xmlns='http://jabber.org/protocol/disco#info'> + <identity category='client' type='bot'/> + <feature var='urn:xmpp:iot:control'/> + </query> + </iq> + """) + + def testServiceDiscoveryComponent(self): + self.stream_start(mode='component', + plugins=['xep_0030', + 'xep_0325']) + + self.recv(""" + <iq type='get' + from='master@clayster.com/amr' + to='tester@localhost' + id='disco1'> + <query xmlns='http://jabber.org/protocol/disco#info'/> + </iq> + """) + + self.send(""" + <iq type='result' + from='tester@localhost' + to='master@clayster.com/amr' + id='disco1'> + <query xmlns='http://jabber.org/protocol/disco#info'> + <identity category='component' type='generic'/> + <feature var='urn:xmpp:iot:control'/> + </query> + </iq> + """) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamControl) + diff --git a/tests/test_tostring.py b/tests/test_tostring.py index e456d28e..e6148533 100644 --- a/tests/test_tostring.py +++ b/tests/test_tostring.py @@ -1,7 +1,7 @@ -from sleekxmpp.test import * -from sleekxmpp.stanza import Message -from sleekxmpp.xmlstream.stanzabase import ET, ElementBase -from sleekxmpp.xmlstream.tostring import tostring, xml_escape +import unittest +from sleekxmpp.test import SleekTest +from sleekxmpp.xmlstream.stanzabase import ET +from sleekxmpp.xmlstream.tostring import tostring, escape class TestToString(SleekTest): @@ -30,7 +30,7 @@ class TestToString(SleekTest): def testXMLEscape(self): """Test escaping XML special characters.""" original = """<foo bar="baz">'Hi & welcome!'</foo>""" - escaped = xml_escape(original) + escaped = escape(original) desired = """<foo bar="baz">'Hi""" desired += """ & welcome!'</foo>""" @@ -85,19 +85,6 @@ class TestToString(SleekTest): original='<a>foo <b>bar</b> baz</a>', message='Element tail content is incorrect.') - - def testStanzaNs(self): - """ - Test using the stanza_ns tostring parameter, which will prevent - adding an xmlns attribute to the serialized element if the - element's namespace is the same. - """ - self.tryTostring( - original='<bar xmlns="foo" />', - expected='<bar />', - message="The stanza_ns parameter was not used properly.", - stanza_ns='foo') - def testStanzaStr(self): """ Test that stanza objects are serialized properly. @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py31,py32 +envlist = py26,py27,py34 [testenv] deps = nose commands = nosetests --where=tests --exclude=live -i sleektest.py |