mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-11-27 19:19:54 +00:00
Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop
This commit is contained in:
commit
a189cb8333
97 changed files with 5157 additions and 1197 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
build/
|
build/
|
||||||
|
dist/
|
||||||
|
MANIFEST
|
||||||
|
|
121
LICENSE
121
LICENSE
|
@ -17,3 +17,124 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Licences of Bundled Third Pary Code
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
dateutil - Extensions to the standard python 2.3+ datetime module.
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"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 EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
fixed_datetime
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Copyright (c) 2008, Red Innovation Ltd., Finland
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of Red Innovation nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``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 EVENT SHALL RED INNOVATION BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
OrderedDict - A port of the Python 2.7+ OrderedDict to Python 2.6
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Copyright (c) 2009 Raymond Hettinger
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
SUELTA – A PURE-PYTHON SASL CLIENT LIBRARY
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
3
README
3
README
|
@ -42,6 +42,9 @@ Main Author: Nathan Fritz fritz@netflint.net
|
||||||
Contributors: Kevin Smith & Lance Stout
|
Contributors: Kevin Smith & Lance Stout
|
||||||
Patches: Remko Tronçon
|
Patches: Remko Tronçon
|
||||||
|
|
||||||
|
Dave Cridland, for his Suelta SASL library.
|
||||||
|
|
||||||
|
|
||||||
Feel free to add fritzy@netflint.net to your roster for direct support and comments.
|
Feel free to add fritzy@netflint.net to your roster for direct support and comments.
|
||||||
Join sleekxmpp-discussion@googlegroups.com / http://groups.google.com/group/sleekxmpp-discussion for email discussion.
|
Join sleekxmpp-discussion@googlegroups.com / http://groups.google.com/group/sleekxmpp-discussion for email discussion.
|
||||||
Join sleek@conference.jabber.org for groupchat discussion.
|
Join sleek@conference.jabber.org for groupchat discussion.
|
||||||
|
|
167
examples/proxy_echo_client.py
Executable file
167
examples/proxy_echo_client.py
Executable file
|
@ -0,0 +1,167 @@
|
||||||
|
#!/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 time
|
||||||
|
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):
|
||||||
|
reload(sys)
|
||||||
|
sys.setdefaultencoding('utf8')
|
||||||
|
|
||||||
|
|
||||||
|
class EchoBot(sleekxmpp.ClientXMPP):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A simple SleekXMPP bot that will echo messages it
|
||||||
|
receives, along with a short thank you message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, jid, password):
|
||||||
|
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||||
|
|
||||||
|
# The session_start event will be triggered when
|
||||||
|
# the bot establishes its connection with the server
|
||||||
|
# and the XML streams are ready for use. We want to
|
||||||
|
# listen for this event so that we we can intialize
|
||||||
|
# our roster.
|
||||||
|
self.add_event_handler("session_start", self.start)
|
||||||
|
|
||||||
|
# The message event is triggered whenever a message
|
||||||
|
# stanza is received. Be aware that that includes
|
||||||
|
# MUC messages and error messages.
|
||||||
|
self.add_event_handler("message", self.message)
|
||||||
|
|
||||||
|
def start(self, event):
|
||||||
|
"""
|
||||||
|
Process the session_start event.
|
||||||
|
|
||||||
|
Typical actions for the session_start event are
|
||||||
|
requesting the roster and broadcasting an intial
|
||||||
|
presence stanza.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
event -- An empty dictionary. The session_start
|
||||||
|
event does not provide any additional
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
self.send_presence()
|
||||||
|
self.get_roster()
|
||||||
|
|
||||||
|
def message(self, msg):
|
||||||
|
"""
|
||||||
|
Process incoming message stanzas. Be aware that this also
|
||||||
|
includes MUC messages and error messages. It is usually
|
||||||
|
a good idea to check the messages's type before processing
|
||||||
|
or sending replies.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
msg -- The received message stanza. See the documentation
|
||||||
|
for stanza objects and the Message stanza to see
|
||||||
|
how it may be used.
|
||||||
|
"""
|
||||||
|
msg.reply("Thanks for sending\n%(body)s" % msg).send()
|
||||||
|
|
||||||
|
|
||||||
|
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("--phost", dest="proxy_host",
|
||||||
|
help="Proxy hostname")
|
||||||
|
optp.add_option("--pport", dest="proxy_port",
|
||||||
|
help="Proxy port")
|
||||||
|
optp.add_option("--puser", dest="proxy_user",
|
||||||
|
help="Proxy username")
|
||||||
|
optp.add_option("--ppass", dest="proxy_pass",
|
||||||
|
help="Proxy password")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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.proxy_host is None:
|
||||||
|
opts.proxy_host = raw_input("Proxy host: ")
|
||||||
|
if opts.proxy_port is None:
|
||||||
|
opts.proxy_port = raw_input("Proxy port: ")
|
||||||
|
if opts.proxy_user is None:
|
||||||
|
opts.proxy_user = raw_input("Proxy username: ")
|
||||||
|
if opts.proxy_pass is None and opts.proxy_user:
|
||||||
|
opts.proxy_pass = getpass.getpass("Proxy password: ")
|
||||||
|
|
||||||
|
# Setup the EchoBot and register plugins. Note that while plugins may
|
||||||
|
# have interdependencies, the order in which you register them does
|
||||||
|
# not matter.
|
||||||
|
xmpp = EchoBot(opts.jid, opts.password)
|
||||||
|
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||||
|
xmpp.register_plugin('xep_0004') # Data Forms
|
||||||
|
xmpp.register_plugin('xep_0060') # PubSub
|
||||||
|
xmpp.register_plugin('xep_0199') # XMPP Ping
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
xmpp.use_proxy = True
|
||||||
|
xmpp.proxy_config = {
|
||||||
|
'host': opts.proxy_host,
|
||||||
|
'port': int(opts.proxy_port),
|
||||||
|
'username': opts.proxy_user,
|
||||||
|
'password': opts.proxy_pass}
|
||||||
|
|
||||||
|
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||||
|
if xmpp.connect():
|
||||||
|
# If you do not have the pydns 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(threaded=False)
|
||||||
|
print("Done")
|
||||||
|
else:
|
||||||
|
print("Unable to connect.")
|
39
setup.py
39
setup.py
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007-2008 Nathanael C. Fritz
|
# Copyright (C) 2007-2011 Nathanael C. Fritz
|
||||||
# All Rights Reserved
|
# All Rights Reserved
|
||||||
#
|
#
|
||||||
# This software is licensed as described in the README file,
|
# This software is licensed as described in the README file,
|
||||||
|
@ -29,13 +29,16 @@ import sleekxmpp
|
||||||
|
|
||||||
VERSION = sleekxmpp.__version__
|
VERSION = sleekxmpp.__version__
|
||||||
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
|
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
|
||||||
LONG_DESCRIPTION = """
|
with open('README') as readme:
|
||||||
SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).
|
LONG_DESCRIPTION = '\n'.join(readme)
|
||||||
"""
|
|
||||||
|
|
||||||
CLASSIFIERS = [ 'Intended Audience :: Developers',
|
CLASSIFIERS = [ 'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: MIT',
|
'License :: OSI Approved :: MIT License',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
|
'Programming Language :: Python 2.6',
|
||||||
|
'Programming Language :: Python 2.7',
|
||||||
|
'Programming Language :: Python 3.1',
|
||||||
|
'Programming Language :: Python 3.2',
|
||||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -45,7 +48,6 @@ packages = [ 'sleekxmpp',
|
||||||
'sleekxmpp/xmlstream',
|
'sleekxmpp/xmlstream',
|
||||||
'sleekxmpp/xmlstream/matcher',
|
'sleekxmpp/xmlstream/matcher',
|
||||||
'sleekxmpp/xmlstream/handler',
|
'sleekxmpp/xmlstream/handler',
|
||||||
'sleekxmpp/thirdparty',
|
|
||||||
'sleekxmpp/plugins',
|
'sleekxmpp/plugins',
|
||||||
'sleekxmpp/plugins/xep_0009',
|
'sleekxmpp/plugins/xep_0009',
|
||||||
'sleekxmpp/plugins/xep_0009/stanza',
|
'sleekxmpp/plugins/xep_0009/stanza',
|
||||||
|
@ -53,18 +55,30 @@ packages = [ 'sleekxmpp',
|
||||||
'sleekxmpp/plugins/xep_0030/stanza',
|
'sleekxmpp/plugins/xep_0030/stanza',
|
||||||
'sleekxmpp/plugins/xep_0050',
|
'sleekxmpp/plugins/xep_0050',
|
||||||
'sleekxmpp/plugins/xep_0059',
|
'sleekxmpp/plugins/xep_0059',
|
||||||
|
'sleekxmpp/plugins/xep_0060',
|
||||||
|
'sleekxmpp/plugins/xep_0060/stanza',
|
||||||
|
'sleekxmpp/plugins/xep_0066',
|
||||||
|
'sleekxmpp/plugins/xep_0078',
|
||||||
'sleekxmpp/plugins/xep_0085',
|
'sleekxmpp/plugins/xep_0085',
|
||||||
'sleekxmpp/plugins/xep_0086',
|
'sleekxmpp/plugins/xep_0086',
|
||||||
'sleekxmpp/plugins/xep_0092',
|
'sleekxmpp/plugins/xep_0092',
|
||||||
'sleekxmpp/plugins/xep_0128',
|
'sleekxmpp/plugins/xep_0128',
|
||||||
'sleekxmpp/plugins/xep_0199',
|
'sleekxmpp/plugins/xep_0199',
|
||||||
|
'sleekxmpp/plugins/xep_0202',
|
||||||
|
'sleekxmpp/plugins/xep_0203',
|
||||||
|
'sleekxmpp/plugins/xep_0224',
|
||||||
|
'sleekxmpp/plugins/xep_0249',
|
||||||
|
'sleekxmpp/features',
|
||||||
|
'sleekxmpp/features/feature_mechanisms',
|
||||||
|
'sleekxmpp/features/feature_mechanisms/stanza',
|
||||||
|
'sleekxmpp/features/feature_starttls',
|
||||||
|
'sleekxmpp/features/feature_bind',
|
||||||
|
'sleekxmpp/features/feature_session',
|
||||||
|
'sleekxmpp/thirdparty',
|
||||||
|
'sleekxmpp/thirdparty/suelta',
|
||||||
|
'sleekxmpp/thirdparty/suelta/mechanisms',
|
||||||
]
|
]
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
py_modules = ['sleekxmpp.xmlstream.tostring.tostring26']
|
|
||||||
else:
|
|
||||||
py_modules = ['sleekxmpp.xmlstream.tostring.tostring']
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name = "sleekxmpp",
|
name = "sleekxmpp",
|
||||||
version = VERSION,
|
version = VERSION,
|
||||||
|
@ -72,11 +86,10 @@ setup(
|
||||||
long_description = LONG_DESCRIPTION,
|
long_description = LONG_DESCRIPTION,
|
||||||
author = 'Nathanael Fritz',
|
author = 'Nathanael Fritz',
|
||||||
author_email = 'fritzy [at] netflint.net',
|
author_email = 'fritzy [at] netflint.net',
|
||||||
url = 'http://code.google.com/p/sleekxmpp',
|
url = 'http://github.com/fritzy/SleekXMPP',
|
||||||
license = 'MIT',
|
license = 'MIT',
|
||||||
platforms = [ 'any' ],
|
platforms = [ 'any' ],
|
||||||
packages = packages,
|
packages = packages,
|
||||||
py_modules = py_modules,
|
|
||||||
requires = [ 'tlslite', 'pythondns' ],
|
requires = [ 'tlslite', 'pythondns' ],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -15,5 +15,5 @@ from sleekxmpp.xmlstream import XMLStream, RestartStream
|
||||||
from sleekxmpp.xmlstream.matcher import *
|
from sleekxmpp.xmlstream.matcher import *
|
||||||
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
|
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
|
||||||
|
|
||||||
__version__ = '1.0beta5'
|
__version__ = '1.0rc1'
|
||||||
__version_info__ = (1, 0, 0, 'beta5', 0)
|
__version_info__ = (1, 0, 0, 'rc1', 0)
|
||||||
|
|
|
@ -92,6 +92,7 @@ class BaseXMPP(XMLStream):
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
self.default_ns = default_ns
|
self.default_ns = default_ns
|
||||||
self.stream_ns = 'http://etherx.jabber.org/streams'
|
self.stream_ns = 'http://etherx.jabber.org/streams'
|
||||||
|
self.namespace_map[self.stream_ns] = 'stream'
|
||||||
|
|
||||||
self.boundjid = JID("")
|
self.boundjid = JID("")
|
||||||
|
|
||||||
|
@ -105,6 +106,8 @@ class BaseXMPP(XMLStream):
|
||||||
|
|
||||||
self.sentpresence = False
|
self.sentpresence = False
|
||||||
|
|
||||||
|
self.stanza = sleekxmpp.stanza
|
||||||
|
|
||||||
self.register_handler(
|
self.register_handler(
|
||||||
Callback('IM',
|
Callback('IM',
|
||||||
MatchXPath('{%s}message/{%s}body' % (self.default_ns,
|
MatchXPath('{%s}message/{%s}body' % (self.default_ns,
|
||||||
|
@ -135,12 +138,41 @@ class BaseXMPP(XMLStream):
|
||||||
register_stanza_plugin(Message, Nick)
|
register_stanza_plugin(Message, Nick)
|
||||||
register_stanza_plugin(Message, HTMLIM)
|
register_stanza_plugin(Message, HTMLIM)
|
||||||
|
|
||||||
|
def start_stream_handler(self, xml):
|
||||||
|
"""
|
||||||
|
Save the stream ID once the streams have been established.
|
||||||
|
|
||||||
|
Overrides XMLStream.start_stream_handler.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
xml -- The incoming stream's root element.
|
||||||
|
"""
|
||||||
|
self.stream_id = xml.get('id', '')
|
||||||
|
|
||||||
def process(self, *args, **kwargs):
|
def process(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Ensure that plugin inter-dependencies are handled before starting
|
|
||||||
event processing.
|
|
||||||
|
|
||||||
Overrides XMLStream.process.
|
Overrides XMLStream.process.
|
||||||
|
|
||||||
|
Initialize the XML streams and begin processing events.
|
||||||
|
|
||||||
|
The number of threads used for processing stream events is determined
|
||||||
|
by HANDLER_THREADS.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
block -- If block=False then event dispatcher will run
|
||||||
|
in a separate thread, allowing for the stream to be
|
||||||
|
used in the background for another application.
|
||||||
|
Otherwise, process(block=True) blocks the current thread.
|
||||||
|
Defaults to False.
|
||||||
|
|
||||||
|
**threaded is deprecated and included for API compatibility**
|
||||||
|
threaded -- If threaded=True then event dispatcher will run
|
||||||
|
in a separate thread, allowing for the stream to be
|
||||||
|
used in the background for another application.
|
||||||
|
Defaults to True.
|
||||||
|
|
||||||
|
Event handlers and the send queue will be threaded
|
||||||
|
regardless of these parameters.
|
||||||
"""
|
"""
|
||||||
for name in self.plugin:
|
for name in self.plugin:
|
||||||
if not self.plugin[name].post_inited:
|
if not self.plugin[name].post_inited:
|
||||||
|
@ -162,23 +194,36 @@ class BaseXMPP(XMLStream):
|
||||||
try:
|
try:
|
||||||
# Import the given module that contains the plugin.
|
# Import the given module that contains the plugin.
|
||||||
if not module:
|
if not module:
|
||||||
|
try:
|
||||||
module = sleekxmpp.plugins
|
module = sleekxmpp.plugins
|
||||||
module = __import__("%s.%s" % (module.__name__, plugin),
|
module = __import__(
|
||||||
globals(), locals(), [plugin])
|
str("%s.%s" % (module.__name__, plugin)),
|
||||||
|
globals(), locals(), [str(plugin)])
|
||||||
|
except ImportError:
|
||||||
|
module = sleekxmpp.features
|
||||||
|
module = __import__(
|
||||||
|
str("%s.%s" % (module.__name__, plugin)),
|
||||||
|
globals(), locals(), [str(plugin)])
|
||||||
if isinstance(module, str):
|
if isinstance(module, str):
|
||||||
# We probably want to load a module from outside
|
# We probably want to load a module from outside
|
||||||
# the sleekxmpp package, so leave out the globals().
|
# the sleekxmpp package, so leave out the globals().
|
||||||
module = __import__(module, fromlist=[plugin])
|
module = __import__(module, fromlist=[plugin])
|
||||||
|
|
||||||
|
# Use the global plugin config cache, if applicable
|
||||||
|
if not pconfig:
|
||||||
|
pconfig = self.plugin_config.get(plugin, {})
|
||||||
|
|
||||||
# Load the plugin class from the module.
|
# Load the plugin class from the module.
|
||||||
self.plugin[plugin] = getattr(module, plugin)(self, pconfig)
|
self.plugin[plugin] = getattr(module, plugin)(self, pconfig)
|
||||||
|
|
||||||
# Let XEP implementing plugins have some extra logging info.
|
# Let XEP/RFC implementing plugins have some extra logging info.
|
||||||
xep = ''
|
spec = '(CUSTOM) '
|
||||||
if hasattr(self.plugin[plugin], 'xep'):
|
if self.plugin[plugin].xep:
|
||||||
xep = "(XEP-%s) " % self.plugin[plugin].xep
|
spec = "(XEP-%s) " % self.plugin[plugin].xep
|
||||||
|
elif self.plugin[plugin].rfc:
|
||||||
|
spec = "(RFC-%s) " % self.plugin[plugin].rfc
|
||||||
|
|
||||||
desc = (xep, self.plugin[plugin].description)
|
desc = (spec, self.plugin[plugin].description)
|
||||||
log.debug("Loaded Plugin %s%s" % desc)
|
log.debug("Loaded Plugin %s%s" % desc)
|
||||||
except:
|
except:
|
||||||
log.exception("Unable to load plugin: %s", plugin)
|
log.exception("Unable to load plugin: %s", plugin)
|
||||||
|
@ -640,7 +685,8 @@ class BaseXMPP(XMLStream):
|
||||||
log.debug("%s %s got offline" % (jid, resource))
|
log.debug("%s %s got offline" % (jid, resource))
|
||||||
del connections[resource]
|
del connections[resource]
|
||||||
|
|
||||||
if not connections and not self.roster[jid]['in_roster']:
|
if not connections and \
|
||||||
|
not self.roster[jid].get('in_roster', False):
|
||||||
del self.roster[jid]
|
del self.roster[jid]
|
||||||
if not was_offline:
|
if not was_offline:
|
||||||
self.event("got_offline", presence)
|
self.event("got_offline", presence)
|
||||||
|
|
|
@ -15,12 +15,14 @@ import hashlib
|
||||||
import random
|
import random
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
from sleekxmpp import plugins
|
from sleekxmpp import plugins
|
||||||
from sleekxmpp import stanza
|
from sleekxmpp import stanza
|
||||||
|
from sleekxmpp import features
|
||||||
from sleekxmpp.basexmpp import BaseXMPP
|
from sleekxmpp.basexmpp import BaseXMPP
|
||||||
from sleekxmpp.stanza import Message, Presence, Iq
|
from sleekxmpp.stanza import *
|
||||||
from sleekxmpp.xmlstream import XMLStream, RestartStream
|
from sleekxmpp.xmlstream import XMLStream, RestartStream
|
||||||
from sleekxmpp.xmlstream import StanzaBase, ET
|
from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
|
||||||
from sleekxmpp.xmlstream.matcher import *
|
from sleekxmpp.xmlstream.matcher import *
|
||||||
from sleekxmpp.xmlstream.handler import *
|
from sleekxmpp.xmlstream.handler import *
|
||||||
|
|
||||||
|
@ -38,9 +40,12 @@ log = logging.getLogger(__name__)
|
||||||
class ClientXMPP(BaseXMPP):
|
class ClientXMPP(BaseXMPP):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
SleekXMPP's client class.
|
SleekXMPP's client class. ( Use only for good, not for evil.)
|
||||||
|
|
||||||
Use only for good, not for evil.
|
Typical Use:
|
||||||
|
xmpp = ClientXMPP('user@server.tld/resource', 'password')
|
||||||
|
xmpp.process(block=False) // when block is True, it blocks the current
|
||||||
|
// thread. False by default.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
|
||||||
|
@ -81,15 +86,19 @@ class ClientXMPP(BaseXMPP):
|
||||||
"xmlns='%s'" % self.default_ns)
|
"xmlns='%s'" % self.default_ns)
|
||||||
self.stream_footer = "</stream:stream>"
|
self.stream_footer = "</stream:stream>"
|
||||||
|
|
||||||
self.features = []
|
self.features = set()
|
||||||
self.registered_features = []
|
self._stream_feature_handlers = {}
|
||||||
|
self._stream_feature_order = []
|
||||||
|
|
||||||
#TODO: Use stream state here
|
#TODO: Use stream state here
|
||||||
self.authenticated = False
|
self.authenticated = False
|
||||||
self.sessionstarted = False
|
self.sessionstarted = False
|
||||||
self.bound = False
|
self.bound = False
|
||||||
self.bindfail = False
|
self.bindfail = False
|
||||||
self.add_event_handler('connected', self.handle_connected)
|
|
||||||
|
self.add_event_handler('connected', self._handle_connected)
|
||||||
|
|
||||||
|
self.register_stanza(StreamFeatures)
|
||||||
|
|
||||||
self.register_handler(
|
self.register_handler(
|
||||||
Callback('Stream Features',
|
Callback('Stream Features',
|
||||||
|
@ -102,32 +111,11 @@ class ClientXMPP(BaseXMPP):
|
||||||
'jabber:iq:roster')),
|
'jabber:iq:roster')),
|
||||||
self._handle_roster))
|
self._handle_roster))
|
||||||
|
|
||||||
self.register_feature(
|
# Setup default stream features
|
||||||
"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />",
|
self.register_plugin('feature_starttls')
|
||||||
self._handle_starttls, True)
|
self.register_plugin('feature_mechanisms')
|
||||||
self.register_feature(
|
self.register_plugin('feature_bind')
|
||||||
"<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />",
|
self.register_plugin('feature_session')
|
||||||
self._handle_sasl_auth, True)
|
|
||||||
self.register_feature(
|
|
||||||
"<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />",
|
|
||||||
self._handle_bind_resource)
|
|
||||||
self.register_feature(
|
|
||||||
"<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />",
|
|
||||||
self._handle_start_session)
|
|
||||||
|
|
||||||
def handle_connected(self, event=None):
|
|
||||||
#TODO: Use stream state here
|
|
||||||
self.authenticated = False
|
|
||||||
self.sessionstarted = False
|
|
||||||
self.bound = False
|
|
||||||
self.bindfail = False
|
|
||||||
self.schedule("session timeout checker", 15,
|
|
||||||
self._session_timeout_check)
|
|
||||||
|
|
||||||
def _session_timeout_check(self):
|
|
||||||
if not self.session_started_event.isSet():
|
|
||||||
log.debug("Session start has taken more than 15 seconds")
|
|
||||||
self.disconnect(reconnect=self.auto_reconnect)
|
|
||||||
|
|
||||||
def connect(self, address=tuple(), reattempt=True, use_tls=True):
|
def connect(self, address=tuple(), reattempt=True, use_tls=True):
|
||||||
"""
|
"""
|
||||||
|
@ -168,18 +156,23 @@ class ClientXMPP(BaseXMPP):
|
||||||
|
|
||||||
addresses = {}
|
addresses = {}
|
||||||
intmax = 0
|
intmax = 0
|
||||||
|
topprio = 65535
|
||||||
for answer in answers:
|
for answer in answers:
|
||||||
intmax += answer.priority
|
topprio = min(topprio, answer.priority)
|
||||||
|
for answer in answers:
|
||||||
|
if answer.priority == topprio:
|
||||||
|
intmax += answer.weight
|
||||||
addresses[intmax] = (answer.target.to_text()[:-1],
|
addresses[intmax] = (answer.target.to_text()[:-1],
|
||||||
answer.port)
|
answer.port)
|
||||||
|
|
||||||
#python3 returns a generator for dictionary keys
|
#python3 returns a generator for dictionary keys
|
||||||
priorities = [x for x in addresses.keys()]
|
items = [x for x in addresses.keys()]
|
||||||
priorities.sort()
|
items.sort()
|
||||||
|
|
||||||
picked = random.randint(0, intmax)
|
picked = random.randint(0, intmax)
|
||||||
for priority in priorities:
|
for item in items:
|
||||||
if picked <= priority:
|
if picked <= item:
|
||||||
address = addresses[priority]
|
address = addresses[item]
|
||||||
break
|
break
|
||||||
|
|
||||||
if not address:
|
if not address:
|
||||||
|
@ -189,19 +182,22 @@ class ClientXMPP(BaseXMPP):
|
||||||
return XMLStream.connect(self, address[0], address[1],
|
return XMLStream.connect(self, address[0], address[1],
|
||||||
use_tls=use_tls, reattempt=reattempt)
|
use_tls=use_tls, reattempt=reattempt)
|
||||||
|
|
||||||
def register_feature(self, mask, pointer, breaker=False):
|
def register_feature(self, name, handler, restart=False, order=5000):
|
||||||
"""
|
"""
|
||||||
Register a stream feature.
|
Register a stream feature.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
mask -- An XML string matching the feature's element.
|
name -- The name of the stream feature.
|
||||||
pointer -- The function to execute if the feature is received.
|
handler -- The function to execute if the feature is received.
|
||||||
breaker -- Indicates if feature processing should halt with
|
restart -- Indicates if feature processing should halt with
|
||||||
this feature. Defaults to False.
|
this feature. Defaults to False.
|
||||||
|
order -- The relative ordering in which the feature should
|
||||||
|
be negotiated. Lower values will be attempted
|
||||||
|
earlier when available.
|
||||||
"""
|
"""
|
||||||
self.registered_features.append((MatchXMLMask(mask),
|
self._stream_feature_handlers[name] = (handler, restart)
|
||||||
pointer,
|
self._stream_feature_order.append((order, name))
|
||||||
breaker))
|
self._stream_feature_order.sort()
|
||||||
|
|
||||||
def update_roster(self, jid, name=None, subscription=None, groups=[],
|
def update_roster(self, jid, name=None, subscription=None, groups=[],
|
||||||
block=True, timeout=None, callback=None):
|
block=True, timeout=None, callback=None):
|
||||||
|
@ -273,6 +269,21 @@ class ClientXMPP(BaseXMPP):
|
||||||
else:
|
else:
|
||||||
return self._handle_roster(response, request=True)
|
return self._handle_roster(response, request=True)
|
||||||
|
|
||||||
|
def _handle_connected(self, event=None):
|
||||||
|
#TODO: Use stream state here
|
||||||
|
self.authenticated = False
|
||||||
|
self.sessionstarted = False
|
||||||
|
self.bound = False
|
||||||
|
self.bindfail = False
|
||||||
|
self.features = set()
|
||||||
|
|
||||||
|
def session_timeout():
|
||||||
|
if not self.session_started_event.isSet():
|
||||||
|
log.debug("Session start has taken more than 15 seconds")
|
||||||
|
self.disconnect(reconnect=self.auto_reconnect)
|
||||||
|
|
||||||
|
self.schedule("session timeout checker", 15, session_timeout)
|
||||||
|
|
||||||
def _handle_stream_features(self, features):
|
def _handle_stream_features(self, features):
|
||||||
"""
|
"""
|
||||||
Process the received stream features.
|
Process the received stream features.
|
||||||
|
@ -280,173 +291,14 @@ class ClientXMPP(BaseXMPP):
|
||||||
Arguments:
|
Arguments:
|
||||||
features -- The features stanza.
|
features -- The features stanza.
|
||||||
"""
|
"""
|
||||||
# Record all of the features.
|
for order, name in self._stream_feature_order:
|
||||||
self.features = []
|
if name in features['features']:
|
||||||
for sub in features.xml:
|
handler, restart = self._stream_feature_handlers[name]
|
||||||
self.features.append(sub.tag)
|
if handler(features) and restart:
|
||||||
|
# Don't continue if the feature requires
|
||||||
# Process the features.
|
# restarting the XML stream.
|
||||||
for sub in features.xml:
|
|
||||||
for feature in self.registered_features:
|
|
||||||
mask, handler, halt = feature
|
|
||||||
if mask.match(sub):
|
|
||||||
if handler(sub) and halt:
|
|
||||||
# Don't continue if the feature was
|
|
||||||
# marked as a breaker.
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _handle_starttls(self, xml):
|
|
||||||
"""
|
|
||||||
Handle notification that the server supports TLS.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- The STARTLS proceed element.
|
|
||||||
"""
|
|
||||||
if not self.use_tls:
|
|
||||||
return False
|
|
||||||
elif not self.authenticated and self.ssl_support:
|
|
||||||
tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls'
|
|
||||||
self.add_handler("<proceed xmlns='%s' />" % tls_ns,
|
|
||||||
self._handle_tls_start,
|
|
||||||
name='TLS Proceed',
|
|
||||||
instream=True)
|
|
||||||
self.send_xml(xml, 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_tls_start(self, xml):
|
|
||||||
"""
|
|
||||||
Handle encrypting the stream using TLS.
|
|
||||||
|
|
||||||
Restarts the stream.
|
|
||||||
"""
|
|
||||||
log.debug("Starting TLS")
|
|
||||||
if self.start_tls():
|
|
||||||
raise RestartStream()
|
|
||||||
|
|
||||||
def _handle_sasl_auth(self, xml):
|
|
||||||
"""
|
|
||||||
Handle authenticating using SASL.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- The SASL mechanisms stanza.
|
|
||||||
"""
|
|
||||||
if self.use_tls and \
|
|
||||||
'{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.debug("Starting SASL Auth")
|
|
||||||
sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
|
||||||
self.add_handler("<success xmlns='%s' />" % sasl_ns,
|
|
||||||
self._handle_auth_success,
|
|
||||||
name='SASL Sucess',
|
|
||||||
instream=True)
|
|
||||||
self.add_handler("<failure xmlns='%s' />" % sasl_ns,
|
|
||||||
self._handle_auth_fail,
|
|
||||||
name='SASL Failure',
|
|
||||||
instream=True)
|
|
||||||
|
|
||||||
sasl_mechs = xml.findall('{%s}mechanism' % sasl_ns)
|
|
||||||
if sasl_mechs:
|
|
||||||
for sasl_mech in sasl_mechs:
|
|
||||||
self.features.append("sasl:%s" % sasl_mech.text)
|
|
||||||
if 'sasl:PLAIN' in self.features and self.boundjid.user:
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
user = bytes(self.boundjid.user)
|
|
||||||
password = bytes(self.password)
|
|
||||||
else:
|
|
||||||
user = bytes(self.boundjid.user, 'utf-8')
|
|
||||||
password = bytes(self.password, 'utf-8')
|
|
||||||
|
|
||||||
auth = base64.b64encode(b'\x00' + user + \
|
|
||||||
b'\x00' + password).decode('utf-8')
|
|
||||||
|
|
||||||
self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % (
|
|
||||||
sasl_ns,
|
|
||||||
auth),
|
|
||||||
now=True)
|
|
||||||
elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user:
|
|
||||||
self.send("<auth xmlns='%s' mechanism='%s' />" % (
|
|
||||||
sasl_ns,
|
|
||||||
'ANONYMOUS'),
|
|
||||||
now=True)
|
|
||||||
else:
|
|
||||||
log.error("No appropriate login method.")
|
|
||||||
self.disconnect()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _handle_auth_success(self, xml):
|
|
||||||
"""
|
|
||||||
SASL authentication succeeded. Restart the stream.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- The SASL authentication success element.
|
|
||||||
"""
|
|
||||||
self.authenticated = True
|
|
||||||
self.features = []
|
|
||||||
raise RestartStream()
|
|
||||||
|
|
||||||
def _handle_auth_fail(self, xml):
|
|
||||||
"""
|
|
||||||
SASL authentication failed. Disconnect and shutdown.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- The SASL authentication failure element.
|
|
||||||
"""
|
|
||||||
log.info("Authentication failed.")
|
|
||||||
self.event("failed_auth", direct=True)
|
|
||||||
self.disconnect()
|
|
||||||
|
|
||||||
def _handle_bind_resource(self, xml):
|
|
||||||
"""
|
|
||||||
Handle requesting a specific resource.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- The bind feature element.
|
|
||||||
"""
|
|
||||||
log.debug("Requesting resource: %s" % self.boundjid.resource)
|
|
||||||
xml.clear()
|
|
||||||
iq = self.Iq(stype='set')
|
|
||||||
if self.boundjid.resource:
|
|
||||||
res = ET.Element('resource')
|
|
||||||
res.text = self.boundjid.resource
|
|
||||||
xml.append(res)
|
|
||||||
iq.append(xml)
|
|
||||||
response = iq.send(now=True)
|
|
||||||
|
|
||||||
bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind'
|
|
||||||
self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns,
|
|
||||||
bind_ns)).text)
|
|
||||||
self.bound = True
|
|
||||||
log.info("Node set to: %s" % self.boundjid.full)
|
|
||||||
session_ns = 'urn:ietf:params:xml:ns:xmpp-session'
|
|
||||||
if "{%s}session" % session_ns not in self.features or self.bindfail:
|
|
||||||
log.debug("Established Session")
|
|
||||||
self.sessionstarted = True
|
|
||||||
self.session_started_event.set()
|
|
||||||
self.event("session_start")
|
|
||||||
|
|
||||||
def _handle_start_session(self, xml):
|
|
||||||
"""
|
|
||||||
Handle the start of the session.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- The session feature element.
|
|
||||||
"""
|
|
||||||
if self.authenticated and self.bound:
|
|
||||||
iq = self.makeIqSet(xml)
|
|
||||||
response = iq.send(now=True)
|
|
||||||
log.debug("Established Session")
|
|
||||||
self.sessionstarted = True
|
|
||||||
self.session_started_event.set()
|
|
||||||
self.event("session_start")
|
|
||||||
else:
|
|
||||||
# Bind probably hasn't happened yet.
|
|
||||||
self.bindfail = True
|
|
||||||
|
|
||||||
def _handle_roster(self, iq, request=False):
|
def _handle_roster(self, iq, request=False):
|
||||||
"""
|
"""
|
||||||
Update the roster after receiving a roster stanza.
|
Update the roster after receiving a roster stanza.
|
||||||
|
|
|
@ -115,11 +115,13 @@ class ComponentXMPP(BaseXMPP):
|
||||||
Once the streams are established, attempt to handshake
|
Once the streams are established, attempt to handshake
|
||||||
with the server to be accepted as a component.
|
with the server to be accepted as a component.
|
||||||
|
|
||||||
Overrides XMLStream.start_stream_handler.
|
Overrides BaseXMPP.start_stream_handler.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
xml -- The incoming stream's root element.
|
xml -- The incoming stream's root element.
|
||||||
"""
|
"""
|
||||||
|
BaseXMPP.start_stream_handler(self, xml)
|
||||||
|
|
||||||
# Construct a hash of the stream ID and the component secret.
|
# Construct a hash of the stream ID and the component secret.
|
||||||
sid = xml.get('id', '')
|
sid = xml.get('id', '')
|
||||||
pre_hash = '%s%s' % (sid, self.secret)
|
pre_hash = '%s%s' % (sid, self.secret)
|
||||||
|
|
9
sleekxmpp/features/__init__.py
Normal file
9
sleekxmpp/features/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = ['feature_starttls', 'feature_mechanisms', 'feature_bind']
|
10
sleekxmpp/features/feature_bind/__init__.py
Normal file
10
sleekxmpp/features/feature_bind/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.features.feature_bind.bind import feature_bind
|
||||||
|
from sleekxmpp.features.feature_bind.stanza import Bind
|
64
sleekxmpp/features/feature_bind/bind.py
Normal file
64
sleekxmpp/features/feature_bind/bind.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import Iq, StreamFeatures
|
||||||
|
from sleekxmpp.features.feature_bind import stanza
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.xmlstream.matcher import *
|
||||||
|
from sleekxmpp.xmlstream.handler import *
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class feature_bind(base_plugin):
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.name = 'Bind Resource'
|
||||||
|
self.rfc = '6120'
|
||||||
|
self.description = 'Resource Binding Stream Feature'
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
self.xmpp.register_feature('bind',
|
||||||
|
self._handle_bind_resource,
|
||||||
|
restart=False,
|
||||||
|
order=10000)
|
||||||
|
|
||||||
|
register_stanza_plugin(Iq, stanza.Bind)
|
||||||
|
register_stanza_plugin(StreamFeatures, stanza.Bind)
|
||||||
|
|
||||||
|
def _handle_bind_resource(self, features):
|
||||||
|
"""
|
||||||
|
Handle requesting a specific resource.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
features -- The stream features stanza.
|
||||||
|
"""
|
||||||
|
log.debug("Requesting resource: %s" % self.xmpp.boundjid.resource)
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq.enable('bind')
|
||||||
|
if self.xmpp.boundjid.resource:
|
||||||
|
iq['bind']['resource'] = self.xmpp.boundjid.resource
|
||||||
|
response = iq.send(now=True)
|
||||||
|
|
||||||
|
self.xmpp.set_jid(response['bind']['jid'])
|
||||||
|
self.xmpp.bound = True
|
||||||
|
|
||||||
|
self.xmpp.features.add('bind')
|
||||||
|
|
||||||
|
log.info("Node 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")
|
22
sleekxmpp/features/feature_bind/stanza.py
Normal file
22
sleekxmpp/features/feature_bind/stanza.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import Iq, StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class Bind(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'bind'
|
||||||
|
namespace = 'urn:ietf:params:xml:ns:xmpp-bind'
|
||||||
|
interfaces = set(('resource', 'jid'))
|
||||||
|
sub_interfaces = interfaces
|
||||||
|
plugin_attrib = 'bind'
|
13
sleekxmpp/features/feature_mechanisms/__init__.py
Normal file
13
sleekxmpp/features/feature_mechanisms/__init__.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.features.feature_mechanisms.mechanisms import feature_mechanisms
|
||||||
|
from sleekxmpp.features.feature_mechanisms.stanza import Mechanisms
|
||||||
|
from sleekxmpp.features.feature_mechanisms.stanza import Auth
|
||||||
|
from sleekxmpp.features.feature_mechanisms.stanza import Success
|
||||||
|
from sleekxmpp.features.feature_mechanisms.stanza import Failure
|
129
sleekxmpp/features/feature_mechanisms/mechanisms.py
Normal file
129
sleekxmpp/features/feature_mechanisms/mechanisms.py
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sleekxmpp.thirdparty import suelta
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
|
||||||
|
from sleekxmpp.xmlstream.matcher import *
|
||||||
|
from sleekxmpp.xmlstream.handler import *
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.features.feature_mechanisms import stanza
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class feature_mechanisms(base_plugin):
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.name = 'SASL Mechanisms'
|
||||||
|
self.rfc = '6120'
|
||||||
|
self.description = "SASL Stream Feature"
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
self.use_mech = self.config.get('use_mech', None)
|
||||||
|
|
||||||
|
def tls_active():
|
||||||
|
return 'starttls' in self.xmpp.features
|
||||||
|
|
||||||
|
def basic_callback(mech, values):
|
||||||
|
if 'username' in values:
|
||||||
|
values['username'] = self.xmpp.boundjid.user
|
||||||
|
if 'password' in values:
|
||||||
|
values['password'] = self.xmpp.password
|
||||||
|
mech.fulfill(values)
|
||||||
|
|
||||||
|
sasl_callback = self.config.get('sasl_callback', None)
|
||||||
|
if sasl_callback is None:
|
||||||
|
sasl_callback = basic_callback
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
register_stanza_plugin(StreamFeatures, stanza.Mechanisms)
|
||||||
|
|
||||||
|
self.xmpp.register_stanza(stanza.Success)
|
||||||
|
self.xmpp.register_stanza(stanza.Failure)
|
||||||
|
self.xmpp.register_stanza(stanza.Auth)
|
||||||
|
self.xmpp.register_stanza(stanza.Challenge)
|
||||||
|
self.xmpp.register_stanza(stanza.Response)
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('SASL Success',
|
||||||
|
MatchXPath(stanza.Success.tag_name()),
|
||||||
|
self._handle_success,
|
||||||
|
instream=True,
|
||||||
|
once=True))
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('SASL Failure',
|
||||||
|
MatchXPath(stanza.Failure.tag_name()),
|
||||||
|
self._handle_fail,
|
||||||
|
instream=True,
|
||||||
|
once=True))
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('SASL Challenge',
|
||||||
|
MatchXPath(stanza.Challenge.tag_name()),
|
||||||
|
self._handle_challenge))
|
||||||
|
|
||||||
|
self.xmpp.register_feature('mechanisms',
|
||||||
|
self._handle_sasl_auth,
|
||||||
|
restart=True,
|
||||||
|
order=self.config.get('order', 100))
|
||||||
|
|
||||||
|
def _handle_sasl_auth(self, features):
|
||||||
|
"""
|
||||||
|
Handle authenticating using SASL.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
features -- The stream features stanza.
|
||||||
|
"""
|
||||||
|
if 'mechanisms' in self.xmpp.features:
|
||||||
|
# SASL authentication has already succeeded, but the
|
||||||
|
# server has incorrectly offered it again.
|
||||||
|
return False
|
||||||
|
|
||||||
|
mech_list = features['mechanisms']
|
||||||
|
self.mech = self.sasl.choose_mechanism(mech_list)
|
||||||
|
|
||||||
|
if self.mech is not None:
|
||||||
|
resp = stanza.Auth(self.xmpp)
|
||||||
|
resp['mechanism'] = self.mech.name
|
||||||
|
resp['value'] = self.mech.process()
|
||||||
|
resp.send(now=True)
|
||||||
|
else:
|
||||||
|
log.error("No appropriate login method.")
|
||||||
|
self.xmpp.event("no_auth", direct=True)
|
||||||
|
self.xmpp.disconnect()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _handle_challenge(self, stanza):
|
||||||
|
"""SASL challenge received. Process and send response."""
|
||||||
|
resp = self.stanza.Response(self.xmpp)
|
||||||
|
resp['value'] = self.mech.process(stanza['value'])
|
||||||
|
resp.send(now=True)
|
||||||
|
|
||||||
|
def _handle_success(self, stanza):
|
||||||
|
"""SASL authentication succeeded. Restart the stream."""
|
||||||
|
self.xmpp.authenticated = True
|
||||||
|
self.xmpp.features.add('mechanisms')
|
||||||
|
raise RestartStream()
|
||||||
|
|
||||||
|
def _handle_fail(self, stanza):
|
||||||
|
"""SASL authentication failed. Disconnect and shutdown."""
|
||||||
|
log.info("Authentication failed: %s" % stanza['condition'])
|
||||||
|
self.xmpp.event("failed_auth", stanza, direct=True)
|
||||||
|
self.xmpp.disconnect()
|
||||||
|
return True
|
15
sleekxmpp/features/feature_mechanisms/stanza/__init__.py
Normal file
15
sleekxmpp/features/feature_mechanisms/stanza/__init__.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from sleekxmpp.features.feature_mechanisms.stanza.mechanisms import Mechanisms
|
||||||
|
from sleekxmpp.features.feature_mechanisms.stanza.auth import Auth
|
||||||
|
from sleekxmpp.features.feature_mechanisms.stanza.success import Success
|
||||||
|
from sleekxmpp.features.feature_mechanisms.stanza.failure import Failure
|
||||||
|
from sleekxmpp.features.feature_mechanisms.stanza.challenge import Challenge
|
||||||
|
from sleekxmpp.features.feature_mechanisms.stanza.response import Response
|
39
sleekxmpp/features/feature_mechanisms/stanza/auth.py
Normal file
39
sleekxmpp/features/feature_mechanisms/stanza/auth.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from sleekxmpp.thirdparty.suelta.util import bytes
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class Auth(StanzaBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'auth'
|
||||||
|
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||||
|
interfaces = set(('mechanism', '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):
|
||||||
|
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
|
||||||
|
|
||||||
|
def del_value(self):
|
||||||
|
self.xml.text = ''
|
39
sleekxmpp/features/feature_mechanisms/stanza/challenge.py
Normal file
39
sleekxmpp/features/feature_mechanisms/stanza/challenge.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from sleekxmpp.thirdparty.suelta.util import bytes
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class Challenge(StanzaBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'challenge'
|
||||||
|
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||||
|
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):
|
||||||
|
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
|
||||||
|
|
||||||
|
def del_value(self):
|
||||||
|
self.xml.text = ''
|
78
sleekxmpp/features/feature_mechanisms/stanza/failure.py
Normal file
78
sleekxmpp/features/feature_mechanisms/stanza/failure.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class Failure(StanzaBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'failure'
|
||||||
|
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||||
|
interfaces = set(('condition', 'text'))
|
||||||
|
plugin_attrib = name
|
||||||
|
sub_interfaces = set(('text',))
|
||||||
|
conditions = set(('aborted', 'account-disabled', 'credentials-expired',
|
||||||
|
'encryption-required', 'incorrect-encoding', 'invalid-authzid',
|
||||||
|
'invalid-mechanism', 'malformed-request', 'mechansism-too-weak',
|
||||||
|
'not-authorized', 'temporary-auth-failure'))
|
||||||
|
|
||||||
|
def setup(self, xml=None):
|
||||||
|
"""
|
||||||
|
Populate the stanza object using an optional XML object.
|
||||||
|
|
||||||
|
Overrides ElementBase.setup.
|
||||||
|
|
||||||
|
Sets a default error type and condition, and changes the
|
||||||
|
parent stanza's type to 'error'.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
xml -- Use an existing XML object for the stanza's values.
|
||||||
|
"""
|
||||||
|
# StanzaBase overrides self.namespace
|
||||||
|
self.namespace = Failure.namespace
|
||||||
|
|
||||||
|
if StanzaBase.setup(self, xml):
|
||||||
|
#If we had to generate XML then set default values.
|
||||||
|
self['condition'] = 'not-authorized'
|
||||||
|
|
||||||
|
self.xml.tag = self.tag_name()
|
||||||
|
|
||||||
|
def get_condition(self):
|
||||||
|
"""Return the condition element's name."""
|
||||||
|
for child in self.xml.getchildren():
|
||||||
|
if "{%s}" % self.namespace in child.tag:
|
||||||
|
cond = child.tag.split('}', 1)[-1]
|
||||||
|
if cond in self.conditions:
|
||||||
|
return cond
|
||||||
|
return 'not-authorized'
|
||||||
|
|
||||||
|
def set_condition(self, value):
|
||||||
|
"""
|
||||||
|
Set the tag name of the condition element.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
value -- The tag name of the condition element.
|
||||||
|
"""
|
||||||
|
if value in self.conditions:
|
||||||
|
del self['condition']
|
||||||
|
self.xml.append(ET.Element("{%s}%s" % (self.namespace, value)))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def del_condition(self):
|
||||||
|
"""Remove the condition element."""
|
||||||
|
for child in self.xml.getchildren():
|
||||||
|
if "{%s}" % self.condition_ns in child.tag:
|
||||||
|
tag = child.tag.split('}', 1)[-1]
|
||||||
|
if tag in self.conditions:
|
||||||
|
self.xml.remove(child)
|
||||||
|
return self
|
55
sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py
Normal file
55
sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class Mechanisms(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'mechanisms'
|
||||||
|
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||||
|
interfaces = set(('mechanisms', 'required'))
|
||||||
|
plugin_attrib = name
|
||||||
|
is_extension = True
|
||||||
|
|
||||||
|
def get_required(self):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_mechanisms(self):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
mechs = self.findall('{%s}mechanism' % self.namespace)
|
||||||
|
if mechs:
|
||||||
|
for mech in mechs:
|
||||||
|
results.append(mech.text)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def set_mechanisms(self, values):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
self.del_mechanisms()
|
||||||
|
for val in values:
|
||||||
|
mech = ET.Element('{%s}mechanism' % self.namespace)
|
||||||
|
mech.text = val
|
||||||
|
self.append(mech)
|
||||||
|
|
||||||
|
def del_mechanisms(self):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
mechs = self.findall('{%s}mechanism' % self.namespace)
|
||||||
|
if mechs:
|
||||||
|
for mech in mechs:
|
||||||
|
self.xml.remove(mech)
|
39
sleekxmpp/features/feature_mechanisms/stanza/response.py
Normal file
39
sleekxmpp/features/feature_mechanisms/stanza/response.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from sleekxmpp.thirdparty.suelta.util import bytes
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class Response(StanzaBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'response'
|
||||||
|
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||||
|
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):
|
||||||
|
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
|
||||||
|
|
||||||
|
def del_value(self):
|
||||||
|
self.xml.text = ''
|
26
sleekxmpp/features/feature_mechanisms/stanza/success.py
Normal file
26
sleekxmpp/features/feature_mechanisms/stanza/success.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class Success(StanzaBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'success'
|
||||||
|
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||||
|
interfaces = set()
|
||||||
|
plugin_attrib = name
|
||||||
|
|
||||||
|
def setup(self, xml):
|
||||||
|
StanzaBase.setup(self, xml)
|
||||||
|
self.xml.tag = self.tag_name()
|
10
sleekxmpp/features/feature_session/__init__.py
Normal file
10
sleekxmpp/features/feature_session/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.features.feature_session.session import feature_session
|
||||||
|
from sleekxmpp.features.feature_session.stanza import Session
|
56
sleekxmpp/features/feature_session/session.py
Normal file
56
sleekxmpp/features/feature_session/session.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import Iq, StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.xmlstream.matcher import *
|
||||||
|
from sleekxmpp.xmlstream.handler import *
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
|
||||||
|
from sleekxmpp.features.feature_session import stanza
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class feature_session(base_plugin):
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.name = 'Start Session'
|
||||||
|
self.rfc = '3920'
|
||||||
|
self.description = 'Start Session Stream Feature'
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
self.xmpp.register_feature('session',
|
||||||
|
self._handle_start_session,
|
||||||
|
restart=False,
|
||||||
|
order=10001)
|
||||||
|
|
||||||
|
register_stanza_plugin(Iq, stanza.Session)
|
||||||
|
register_stanza_plugin(StreamFeatures, stanza.Session)
|
||||||
|
|
||||||
|
def _handle_start_session(self, features):
|
||||||
|
"""
|
||||||
|
Handle the start of the session.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
feature -- The stream features element.
|
||||||
|
"""
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq.enable('session')
|
||||||
|
response = iq.send(now=True)
|
||||||
|
|
||||||
|
self.xmpp.features.add('session')
|
||||||
|
|
||||||
|
log.debug("Established Session")
|
||||||
|
self.xmpp.sessionstarted = True
|
||||||
|
self.xmpp.session_started_event.set()
|
||||||
|
self.xmpp.event("session_start")
|
21
sleekxmpp/features/feature_session/stanza.py
Normal file
21
sleekxmpp/features/feature_session/stanza.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import Iq, StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class Session(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'session'
|
||||||
|
namespace = 'urn:ietf:params:xml:ns:xmpp-session'
|
||||||
|
interfaces = set()
|
||||||
|
plugin_attrib = 'session'
|
10
sleekxmpp/features/feature_starttls/__init__.py
Normal file
10
sleekxmpp/features/feature_starttls/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.features.feature_starttls.starttls import feature_starttls
|
||||||
|
from sleekxmpp.features.feature_starttls.stanza import *
|
47
sleekxmpp/features/feature_starttls/stanza.py
Normal file
47
sleekxmpp/features/feature_starttls/stanza.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import StanzaBase, ElementBase
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class STARTTLS(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'starttls'
|
||||||
|
namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
|
||||||
|
interfaces = set(('required',))
|
||||||
|
plugin_attrib = name
|
||||||
|
|
||||||
|
def get_required(self):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Proceed(StanzaBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'proceed'
|
||||||
|
namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
|
||||||
|
interfaces = set()
|
||||||
|
|
||||||
|
|
||||||
|
class Failure(StanzaBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'failure'
|
||||||
|
namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
|
||||||
|
interfaces = set()
|
70
sleekxmpp/features/feature_starttls/starttls.py
Normal file
70
sleekxmpp/features/feature_starttls/starttls.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 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.xmlstream import RestartStream, register_stanza_plugin
|
||||||
|
from sleekxmpp.xmlstream.matcher import *
|
||||||
|
from sleekxmpp.xmlstream.handler import *
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.features.feature_starttls import stanza
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class feature_starttls(base_plugin):
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.name = "STARTTLS"
|
||||||
|
self.rfc = '6120'
|
||||||
|
self.description = "STARTTLS Stream Feature"
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('STARTTLS Proceed',
|
||||||
|
MatchXPath(stanza.Proceed.tag_name()),
|
||||||
|
self._handle_starttls_proceed,
|
||||||
|
instream=True))
|
||||||
|
self.xmpp.register_feature('starttls',
|
||||||
|
self._handle_starttls,
|
||||||
|
restart=True,
|
||||||
|
order=self.config.get('order', 0))
|
||||||
|
|
||||||
|
self.xmpp.register_stanza(stanza.Proceed)
|
||||||
|
self.xmpp.register_stanza(stanza.Failure)
|
||||||
|
register_stanza_plugin(StreamFeatures, stanza.STARTTLS)
|
||||||
|
|
||||||
|
def _handle_starttls(self, features):
|
||||||
|
"""
|
||||||
|
Handle notification that the server supports TLS.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
features -- The stream:features element.
|
||||||
|
"""
|
||||||
|
if 'starttls' in self.xmpp.features:
|
||||||
|
# We have already negotiated TLS, but the server is
|
||||||
|
# offering it again, against spec.
|
||||||
|
return False
|
||||||
|
elif not self.xmpp.use_tls:
|
||||||
|
return False
|
||||||
|
elif self.xmpp.ssl_support:
|
||||||
|
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."""
|
||||||
|
log.debug("Starting TLS")
|
||||||
|
if self.xmpp.start_tls():
|
||||||
|
self.xmpp.features.add('starttls')
|
||||||
|
raise RestartStream()
|
|
@ -6,5 +6,8 @@
|
||||||
See the file LICENSE for copying permission.
|
See the file LICENSE for copying permission.
|
||||||
"""
|
"""
|
||||||
__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
|
__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
|
||||||
'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086',
|
'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082',
|
||||||
'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify']
|
'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199',
|
||||||
|
'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify']
|
||||||
|
|
||||||
|
# Don't automatically load xep_0078
|
||||||
|
|
|
@ -66,7 +66,8 @@ class base_plugin(object):
|
||||||
"""
|
"""
|
||||||
if config is None:
|
if config is None:
|
||||||
config = {}
|
config = {}
|
||||||
self.xep = 'base'
|
self.xep = None
|
||||||
|
self.rfc = None
|
||||||
self.description = 'Base Plugin'
|
self.description = 'Base Plugin'
|
||||||
self.xmpp = xmpp
|
self.xmpp = xmpp
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
|
@ -1,557 +0,0 @@
|
||||||
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
|
||||||
from .. stanza.iq import Iq
|
|
||||||
from .. stanza.message import Message
|
|
||||||
from .. basexmpp import basexmpp
|
|
||||||
from .. xmlstream.xmlstream import XMLStream
|
|
||||||
import logging
|
|
||||||
from . import xep_0004
|
|
||||||
|
|
||||||
|
|
||||||
class PubsubState(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/psstate'
|
|
||||||
name = 'state'
|
|
||||||
plugin_attrib = 'psstate'
|
|
||||||
interfaces = set(('node', 'item', 'payload'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def setPayload(self, value):
|
|
||||||
self.xml.append(value)
|
|
||||||
|
|
||||||
def getPayload(self):
|
|
||||||
childs = self.xml.getchildren()
|
|
||||||
if len(childs) > 0:
|
|
||||||
return childs[0]
|
|
||||||
|
|
||||||
def delPayload(self):
|
|
||||||
for child in self.xml.getchildren():
|
|
||||||
self.xml.remove(child)
|
|
||||||
|
|
||||||
registerStanzaPlugin(Iq, PubsubState)
|
|
||||||
|
|
||||||
class PubsubStateEvent(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/psstate#event'
|
|
||||||
name = 'event'
|
|
||||||
plugin_attrib = 'psstate_event'
|
|
||||||
intefaces = set(tuple())
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(Message, PubsubStateEvent)
|
|
||||||
registerStanzaPlugin(PubsubStateEvent, PubsubState)
|
|
||||||
|
|
||||||
class Pubsub(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'pubsub'
|
|
||||||
plugin_attrib = 'pubsub'
|
|
||||||
interfaces = set(tuple())
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(Iq, Pubsub)
|
|
||||||
|
|
||||||
class PubsubOwner(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
|
||||||
name = 'pubsub'
|
|
||||||
plugin_attrib = 'pubsub_owner'
|
|
||||||
interfaces = set(tuple())
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(Iq, PubsubOwner)
|
|
||||||
|
|
||||||
class Affiliation(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'affiliation'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node', 'affiliation'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
class Affiliations(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'affiliations'
|
|
||||||
plugin_attrib = 'affiliations'
|
|
||||||
interfaces = set(tuple())
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
subitem = (Affiliation,)
|
|
||||||
|
|
||||||
def append(self, affiliation):
|
|
||||||
if not isinstance(affiliation, Affiliation):
|
|
||||||
raise TypeError
|
|
||||||
self.xml.append(affiliation.xml)
|
|
||||||
return self.iterables.append(affiliation)
|
|
||||||
|
|
||||||
registerStanzaPlugin(Pubsub, Affiliations)
|
|
||||||
|
|
||||||
|
|
||||||
class Subscription(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'subscription'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('jid', 'node', 'subscription', 'subid'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def setjid(self, value):
|
|
||||||
self._setattr('jid', str(value))
|
|
||||||
|
|
||||||
def getjid(self):
|
|
||||||
return jid(self._getattr('jid'))
|
|
||||||
|
|
||||||
registerStanzaPlugin(Pubsub, Subscription)
|
|
||||||
|
|
||||||
class Subscriptions(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'subscriptions'
|
|
||||||
plugin_attrib = 'subscriptions'
|
|
||||||
interfaces = set(tuple())
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
subitem = (Subscription,)
|
|
||||||
|
|
||||||
registerStanzaPlugin(Pubsub, Subscriptions)
|
|
||||||
|
|
||||||
class OptionalSetting(object):
|
|
||||||
interfaces = set(('required',))
|
|
||||||
|
|
||||||
def setRequired(self, value):
|
|
||||||
value = bool(value)
|
|
||||||
if value and not self['required']:
|
|
||||||
self.xml.append(ET.Element("{%s}required" % self.namespace))
|
|
||||||
elif not value and self['required']:
|
|
||||||
self.delRequired()
|
|
||||||
|
|
||||||
def getRequired(self):
|
|
||||||
required = self.xml.find("{%s}required" % self.namespace)
|
|
||||||
if required is not None:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def delRequired(self):
|
|
||||||
required = self.xml.find("{%s}required" % self.namespace)
|
|
||||||
if required is not None:
|
|
||||||
self.xml.remove(required)
|
|
||||||
|
|
||||||
|
|
||||||
class SubscribeOptions(ElementBase, OptionalSetting):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'subscribe-options'
|
|
||||||
plugin_attrib = 'suboptions'
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
interfaces = set(('required',))
|
|
||||||
|
|
||||||
registerStanzaPlugin(Subscription, SubscribeOptions)
|
|
||||||
|
|
||||||
class Item(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'item'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('id', 'payload'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def setPayload(self, value):
|
|
||||||
self.xml.append(value)
|
|
||||||
|
|
||||||
def getPayload(self):
|
|
||||||
childs = self.xml.getchildren()
|
|
||||||
if len(childs) > 0:
|
|
||||||
return childs[0]
|
|
||||||
|
|
||||||
def delPayload(self):
|
|
||||||
for child in self.xml.getchildren():
|
|
||||||
self.xml.remove(child)
|
|
||||||
|
|
||||||
class Items(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'items'
|
|
||||||
plugin_attrib = 'items'
|
|
||||||
interfaces = set(('node',))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
subitem = (Item,)
|
|
||||||
|
|
||||||
registerStanzaPlugin(Pubsub, Items)
|
|
||||||
|
|
||||||
class Create(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'create'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node',))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(Pubsub, Create)
|
|
||||||
|
|
||||||
#class Default(ElementBase):
|
|
||||||
# namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
# name = 'default'
|
|
||||||
# plugin_attrib = name
|
|
||||||
# interfaces = set(('node', 'type'))
|
|
||||||
# plugin_attrib_map = {}
|
|
||||||
# plugin_tag_map = {}
|
|
||||||
#
|
|
||||||
# def getType(self):
|
|
||||||
# t = self._getAttr('type')
|
|
||||||
# if not t: t == 'leaf'
|
|
||||||
# return t
|
|
||||||
#
|
|
||||||
#registerStanzaPlugin(Pubsub, Default)
|
|
||||||
|
|
||||||
class Publish(Items):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'publish'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node',))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
subitem = (Item,)
|
|
||||||
|
|
||||||
registerStanzaPlugin(Pubsub, Publish)
|
|
||||||
|
|
||||||
class Retract(Items):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'retract'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node', 'notify'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(Pubsub, Retract)
|
|
||||||
|
|
||||||
class Unsubscribe(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'unsubscribe'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node', 'jid'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def setJid(self, value):
|
|
||||||
self._setAttr('jid', str(value))
|
|
||||||
|
|
||||||
def getJid(self):
|
|
||||||
return JID(self._getAttr('jid'))
|
|
||||||
|
|
||||||
registerStanzaPlugin(Pubsub, Unsubscribe)
|
|
||||||
|
|
||||||
class Subscribe(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'subscribe'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node', 'jid'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def setJid(self, value):
|
|
||||||
self._setAttr('jid', str(value))
|
|
||||||
|
|
||||||
def getJid(self):
|
|
||||||
return JID(self._getAttr('jid'))
|
|
||||||
|
|
||||||
registerStanzaPlugin(Pubsub, Subscribe)
|
|
||||||
|
|
||||||
class Configure(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'configure'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node', 'type'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def getType(self):
|
|
||||||
t = self._getAttr('type')
|
|
||||||
if not t: t == 'leaf'
|
|
||||||
return t
|
|
||||||
|
|
||||||
registerStanzaPlugin(Pubsub, Configure)
|
|
||||||
registerStanzaPlugin(Configure, xep_0004.Form)
|
|
||||||
|
|
||||||
class DefaultConfig(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
|
||||||
name = 'default'
|
|
||||||
plugin_attrib = 'default'
|
|
||||||
interfaces = set(('node', 'type', 'config'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
ElementBase.__init__(self, *args, **kwargs)
|
|
||||||
|
|
||||||
def getType(self):
|
|
||||||
t = self._getAttr('type')
|
|
||||||
if not t: t = 'leaf'
|
|
||||||
return t
|
|
||||||
|
|
||||||
def getConfig(self):
|
|
||||||
return self['form']
|
|
||||||
|
|
||||||
def setConfig(self, value):
|
|
||||||
self['form'].setStanzaValues(value.getStanzaValues())
|
|
||||||
return self
|
|
||||||
|
|
||||||
registerStanzaPlugin(PubsubOwner, DefaultConfig)
|
|
||||||
registerStanzaPlugin(DefaultConfig, xep_0004.Form)
|
|
||||||
|
|
||||||
class Options(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
|
||||||
name = 'options'
|
|
||||||
plugin_attrib = 'options'
|
|
||||||
interfaces = set(('jid', 'node', 'options'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
ElementBase.__init__(self, *args, **kwargs)
|
|
||||||
|
|
||||||
def getOptions(self):
|
|
||||||
config = self.xml.find('{jabber:x:data}x')
|
|
||||||
form = xep_0004.Form()
|
|
||||||
if config is not None:
|
|
||||||
form.fromXML(config)
|
|
||||||
return form
|
|
||||||
|
|
||||||
def setOptions(self, value):
|
|
||||||
self.xml.append(value.getXML())
|
|
||||||
return self
|
|
||||||
|
|
||||||
def delOptions(self):
|
|
||||||
config = self.xml.find('{jabber:x:data}x')
|
|
||||||
self.xml.remove(config)
|
|
||||||
|
|
||||||
def setJid(self, value):
|
|
||||||
self._setAttr('jid', str(value))
|
|
||||||
|
|
||||||
def getJid(self):
|
|
||||||
return JID(self._getAttr('jid'))
|
|
||||||
|
|
||||||
registerStanzaPlugin(Pubsub, Options)
|
|
||||||
registerStanzaPlugin(Subscribe, Options)
|
|
||||||
|
|
||||||
class OwnerAffiliations(Affiliations):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
|
||||||
interfaces = set(('node'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def append(self, affiliation):
|
|
||||||
if not isinstance(affiliation, OwnerAffiliation):
|
|
||||||
raise TypeError
|
|
||||||
self.xml.append(affiliation.xml)
|
|
||||||
return self.affiliations.append(affiliation)
|
|
||||||
|
|
||||||
registerStanzaPlugin(PubsubOwner, OwnerAffiliations)
|
|
||||||
|
|
||||||
class OwnerAffiliation(Affiliation):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
|
||||||
interfaces = set(('affiliation', 'jid'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
class OwnerConfigure(Configure):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
|
||||||
interfaces = set(('node', 'config'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(PubsubOwner, OwnerConfigure)
|
|
||||||
|
|
||||||
class OwnerDefault(OwnerConfigure):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
|
||||||
interfaces = set(('node', 'config'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def getConfig(self):
|
|
||||||
return self['form']
|
|
||||||
|
|
||||||
def setConfig(self, value):
|
|
||||||
self['form'].setStanzaValues(value.getStanzaValues())
|
|
||||||
return self
|
|
||||||
|
|
||||||
registerStanzaPlugin(PubsubOwner, OwnerDefault)
|
|
||||||
registerStanzaPlugin(OwnerDefault, xep_0004.Form)
|
|
||||||
|
|
||||||
class OwnerDelete(ElementBase, OptionalSetting):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
|
||||||
name = 'delete'
|
|
||||||
plugin_attrib = 'delete'
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
interfaces = set(('node',))
|
|
||||||
|
|
||||||
registerStanzaPlugin(PubsubOwner, OwnerDelete)
|
|
||||||
|
|
||||||
class OwnerPurge(ElementBase, OptionalSetting):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
|
||||||
name = 'purge'
|
|
||||||
plugin_attrib = name
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(PubsubOwner, OwnerPurge)
|
|
||||||
|
|
||||||
class OwnerRedirect(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
|
||||||
name = 'redirect'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node', 'jid'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def setJid(self, value):
|
|
||||||
self._setAttr('jid', str(value))
|
|
||||||
|
|
||||||
def getJid(self):
|
|
||||||
return JID(self._getAttr('jid'))
|
|
||||||
|
|
||||||
registerStanzaPlugin(OwnerDelete, OwnerRedirect)
|
|
||||||
|
|
||||||
class OwnerSubscriptions(Subscriptions):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
|
||||||
interfaces = set(('node',))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def append(self, subscription):
|
|
||||||
if not isinstance(subscription, OwnerSubscription):
|
|
||||||
raise TypeError
|
|
||||||
self.xml.append(subscription.xml)
|
|
||||||
return self.subscriptions.append(subscription)
|
|
||||||
|
|
||||||
registerStanzaPlugin(PubsubOwner, OwnerSubscriptions)
|
|
||||||
|
|
||||||
class OwnerSubscription(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
|
||||||
name = 'subscription'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('jid', 'subscription'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def setJid(self, value):
|
|
||||||
self._setAttr('jid', str(value))
|
|
||||||
|
|
||||||
def getJid(self):
|
|
||||||
return JID(self._getAttr('from'))
|
|
||||||
|
|
||||||
class Event(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
|
||||||
name = 'event'
|
|
||||||
plugin_attrib = 'pubsub_event'
|
|
||||||
interfaces = set(('node',))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(Message, Event)
|
|
||||||
|
|
||||||
class EventItem(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
|
||||||
name = 'item'
|
|
||||||
plugin_attrib = 'item'
|
|
||||||
interfaces = set(('id', 'payload'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def setPayload(self, value):
|
|
||||||
self.xml.append(value)
|
|
||||||
|
|
||||||
def getPayload(self):
|
|
||||||
childs = self.xml.getchildren()
|
|
||||||
if len(childs) > 0:
|
|
||||||
return childs[0]
|
|
||||||
|
|
||||||
def delPayload(self):
|
|
||||||
for child in self.xml.getchildren():
|
|
||||||
self.xml.remove(child)
|
|
||||||
|
|
||||||
|
|
||||||
class EventRetract(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
|
||||||
name = 'retract'
|
|
||||||
plugin_attrib = 'retract'
|
|
||||||
interfaces = set(('id',))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
class EventItems(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
|
||||||
name = 'items'
|
|
||||||
plugin_attrib = 'items'
|
|
||||||
interfaces = set(('node',))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
subitem = (EventItem, EventRetract)
|
|
||||||
|
|
||||||
registerStanzaPlugin(Event, EventItems)
|
|
||||||
|
|
||||||
class EventCollection(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
|
||||||
name = 'collection'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node',))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(Event, EventCollection)
|
|
||||||
|
|
||||||
class EventAssociate(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
|
||||||
name = 'associate'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node',))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(EventCollection, EventAssociate)
|
|
||||||
|
|
||||||
class EventDisassociate(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
|
||||||
name = 'disassociate'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node',))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(EventCollection, EventDisassociate)
|
|
||||||
|
|
||||||
class EventConfiguration(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
|
||||||
name = 'configuration'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node', 'config'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(Event, EventConfiguration)
|
|
||||||
registerStanzaPlugin(EventConfiguration, xep_0004.Form)
|
|
||||||
|
|
||||||
class EventPurge(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
|
||||||
name = 'purge'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node',))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
registerStanzaPlugin(Event, EventPurge)
|
|
||||||
|
|
||||||
class EventSubscription(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
|
||||||
name = 'subscription'
|
|
||||||
plugin_attrib = name
|
|
||||||
interfaces = set(('node','expiry', 'jid', 'subid', 'subscription'))
|
|
||||||
plugin_attrib_map = {}
|
|
||||||
plugin_tag_map = {}
|
|
||||||
|
|
||||||
def setJid(self, value):
|
|
||||||
self._setAttr('jid', str(value))
|
|
||||||
|
|
||||||
def getJid(self):
|
|
||||||
return JID(self._getAttr('jid'))
|
|
||||||
|
|
||||||
registerStanzaPlugin(Event, EventSubscription)
|
|
|
@ -463,7 +463,7 @@ class RemoteSession(object):
|
||||||
key = "%s.%s" % (endpoint, name)
|
key = "%s.%s" % (endpoint, name)
|
||||||
log.debug("Registering call handler for %s (%s)." % (key, method))
|
log.debug("Registering call handler for %s (%s)." % (key, method))
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if self._entries.has_key(key):
|
if key in self._entries:
|
||||||
raise KeyError("A handler for %s has already been regisered!" % endpoint)
|
raise KeyError("A handler for %s has already been regisered!" % endpoint)
|
||||||
self._entries[key] = JabberRPCEntry(endpoint, method)
|
self._entries[key] = JabberRPCEntry(endpoint, method)
|
||||||
return key
|
return key
|
||||||
|
|
2
sleekxmpp/plugins/xep_0060/__init__.py
Normal file
2
sleekxmpp/plugins/xep_0060/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from sleekxmpp.plugins.xep_0060.pubsub import xep_0060
|
||||||
|
from sleekxmpp.plugins.xep_0060 import stanza
|
313
sleekxmpp/plugins/xep_0060/pubsub.py
Normal file
313
sleekxmpp/plugins/xep_0060/pubsub.py
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
from __future__ import with_statement
|
||||||
|
from sleekxmpp.plugins import base
|
||||||
|
import logging
|
||||||
|
#from xml.etree import cElementTree as ET
|
||||||
|
from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET
|
||||||
|
from sleekxmpp.plugins.xep_0060 import stanza
|
||||||
|
from sleekxmpp.plugins.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, '')
|
||||||
|
|
3
sleekxmpp/plugins/xep_0060/stanza/__init__.py
Normal file
3
sleekxmpp/plugins/xep_0060/stanza/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from sleekxmpp.plugins.xep_0060.stanza.pubsub import Pubsub, Affiliation, Affiliations, Subscription, Subscriptions, SubscribeOptions, Item, Items, Create, Publish, Retract, Unsubscribe, Subscribe, Configure, Options, PubsubState, PubsubStateEvent
|
||||||
|
from sleekxmpp.plugins.xep_0060.stanza.pubsub_owner import PubsubOwner, DefaultConfig, OwnerAffiliations, OwnerAffiliation, OwnerConfigure, OwnerDefault, OwnerDelete, OwnerPurge, OwnerRedirect, OwnerSubscriptions, OwnerSubscription
|
||||||
|
from sleekxmpp.plugins.xep_0060.stanza.pubsub_event import Event, EventItem, EventRetract, EventItems, EventCollection, EventAssociate, EventDisassociate, EventConfiguration, EventPurge, EventSubscription
|
24
sleekxmpp/plugins/xep_0060/stanza/base.py
Normal file
24
sleekxmpp/plugins/xep_0060/stanza/base.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
|
||||||
|
class OptionalSetting(object):
|
||||||
|
interfaces = set(('required',))
|
||||||
|
|
||||||
|
def setRequired(self, value):
|
||||||
|
value = bool(value)
|
||||||
|
if value and not self['required']:
|
||||||
|
self.xml.append(ET.Element("{%s}required" % self.namespace))
|
||||||
|
elif not value and self['required']:
|
||||||
|
self.delRequired()
|
||||||
|
|
||||||
|
def getRequired(self):
|
||||||
|
required = self.xml.find("{%s}required" % self.namespace)
|
||||||
|
if required is not None:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delRequired(self):
|
||||||
|
required = self.xml.find("{%s}required" % self.namespace)
|
||||||
|
if required is not None:
|
||||||
|
self.xml.remove(required)
|
||||||
|
|
277
sleekxmpp/plugins/xep_0060/stanza/pubsub.py
Normal file
277
sleekxmpp/plugins/xep_0060/stanza/pubsub.py
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
||||||
|
from sleekxmpp.stanza.iq import Iq
|
||||||
|
from sleekxmpp.stanza.message import Message
|
||||||
|
from sleekxmpp.basexmpp import basexmpp
|
||||||
|
from sleekxmpp.xmlstream.xmlstream import XMLStream
|
||||||
|
import logging
|
||||||
|
from sleekxmpp.plugins import xep_0004
|
||||||
|
from sleekxmpp.plugins.xep_0060.stanza.base import OptionalSetting
|
||||||
|
|
||||||
|
|
||||||
|
class Pubsub(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'pubsub'
|
||||||
|
plugin_attrib = 'pubsub'
|
||||||
|
interfaces = set(tuple())
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(Iq, Pubsub)
|
||||||
|
|
||||||
|
|
||||||
|
class Affiliation(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'affiliation'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node', 'affiliation'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
class Affiliations(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'affiliations'
|
||||||
|
plugin_attrib = 'affiliations'
|
||||||
|
interfaces = set(tuple())
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
subitem = (Affiliation,)
|
||||||
|
|
||||||
|
def append(self, affiliation):
|
||||||
|
if not isinstance(affiliation, Affiliation):
|
||||||
|
raise TypeError
|
||||||
|
self.xml.append(affiliation.xml)
|
||||||
|
return self.iterables.append(affiliation)
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Affiliations)
|
||||||
|
|
||||||
|
|
||||||
|
class Subscription(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'subscription'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('jid', 'node', 'subscription', 'subid'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def setjid(self, value):
|
||||||
|
self._setattr('jid', str(value))
|
||||||
|
|
||||||
|
def getjid(self):
|
||||||
|
return jid(self._getattr('jid'))
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Subscription)
|
||||||
|
|
||||||
|
class Subscriptions(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'subscriptions'
|
||||||
|
plugin_attrib = 'subscriptions'
|
||||||
|
interfaces = set(tuple())
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
subitem = (Subscription,)
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Subscriptions)
|
||||||
|
|
||||||
|
|
||||||
|
class SubscribeOptions(ElementBase, OptionalSetting):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'subscribe-options'
|
||||||
|
plugin_attrib = 'suboptions'
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
interfaces = set(('required',))
|
||||||
|
|
||||||
|
registerStanzaPlugin(Subscription, SubscribeOptions)
|
||||||
|
|
||||||
|
class Item(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'item'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('id', 'payload'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def setPayload(self, value):
|
||||||
|
self.xml.append(value)
|
||||||
|
|
||||||
|
def getPayload(self):
|
||||||
|
childs = self.xml.getchildren()
|
||||||
|
if len(childs) > 0:
|
||||||
|
return childs[0]
|
||||||
|
|
||||||
|
def delPayload(self):
|
||||||
|
for child in self.xml.getchildren():
|
||||||
|
self.xml.remove(child)
|
||||||
|
|
||||||
|
class Items(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'items'
|
||||||
|
plugin_attrib = 'items'
|
||||||
|
interfaces = set(('node',))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
subitem = (Item,)
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Items)
|
||||||
|
|
||||||
|
class Create(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'create'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node',))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Create)
|
||||||
|
|
||||||
|
#class Default(ElementBase):
|
||||||
|
# namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
# name = 'default'
|
||||||
|
# plugin_attrib = name
|
||||||
|
# interfaces = set(('node', 'type'))
|
||||||
|
# plugin_attrib_map = {}
|
||||||
|
# plugin_tag_map = {}
|
||||||
|
#
|
||||||
|
# def getType(self):
|
||||||
|
# t = self._getAttr('type')
|
||||||
|
# if not t: t == 'leaf'
|
||||||
|
# return t
|
||||||
|
#
|
||||||
|
#registerStanzaPlugin(Pubsub, Default)
|
||||||
|
|
||||||
|
class Publish(Items):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'publish'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node',))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
subitem = (Item,)
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Publish)
|
||||||
|
|
||||||
|
class Retract(Items):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'retract'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node', 'notify'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Retract)
|
||||||
|
|
||||||
|
class Unsubscribe(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'unsubscribe'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node', 'jid'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def setJid(self, value):
|
||||||
|
self._setAttr('jid', str(value))
|
||||||
|
|
||||||
|
def getJid(self):
|
||||||
|
return JID(self._getAttr('jid'))
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Unsubscribe)
|
||||||
|
|
||||||
|
class Subscribe(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'subscribe'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node', 'jid'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def setJid(self, value):
|
||||||
|
self._setAttr('jid', str(value))
|
||||||
|
|
||||||
|
def getJid(self):
|
||||||
|
return JID(self._getAttr('jid'))
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Subscribe)
|
||||||
|
|
||||||
|
class Configure(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'configure'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node', 'type'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def getType(self):
|
||||||
|
t = self._getAttr('type')
|
||||||
|
if not t: t == 'leaf'
|
||||||
|
return t
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Configure)
|
||||||
|
registerStanzaPlugin(Configure, xep_0004.Form)
|
||||||
|
|
||||||
|
class Options(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
|
name = 'options'
|
||||||
|
plugin_attrib = 'options'
|
||||||
|
interfaces = set(('jid', 'node', 'options'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
ElementBase.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def getOptions(self):
|
||||||
|
config = self.xml.find('{jabber:x:data}x')
|
||||||
|
form = xep_0004.Form()
|
||||||
|
if config is not None:
|
||||||
|
form.fromXML(config)
|
||||||
|
return form
|
||||||
|
|
||||||
|
def setOptions(self, value):
|
||||||
|
self.xml.append(value.getXML())
|
||||||
|
return self
|
||||||
|
|
||||||
|
def delOptions(self):
|
||||||
|
config = self.xml.find('{jabber:x:data}x')
|
||||||
|
self.xml.remove(config)
|
||||||
|
|
||||||
|
def setJid(self, value):
|
||||||
|
self._setAttr('jid', str(value))
|
||||||
|
|
||||||
|
def getJid(self):
|
||||||
|
return JID(self._getAttr('jid'))
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Options)
|
||||||
|
registerStanzaPlugin(Subscribe, Options)
|
||||||
|
|
||||||
|
class PubsubState(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/psstate'
|
||||||
|
name = 'state'
|
||||||
|
plugin_attrib = 'psstate'
|
||||||
|
interfaces = set(('node', 'item', 'payload'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def setPayload(self, value):
|
||||||
|
self.xml.append(value)
|
||||||
|
|
||||||
|
def getPayload(self):
|
||||||
|
childs = self.xml.getchildren()
|
||||||
|
if len(childs) > 0:
|
||||||
|
return childs[0]
|
||||||
|
|
||||||
|
def delPayload(self):
|
||||||
|
for child in self.xml.getchildren():
|
||||||
|
self.xml.remove(child)
|
||||||
|
|
||||||
|
registerStanzaPlugin(Iq, PubsubState)
|
||||||
|
|
||||||
|
class PubsubStateEvent(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/psstate#event'
|
||||||
|
name = 'event'
|
||||||
|
plugin_attrib = 'psstate_event'
|
||||||
|
intefaces = set(tuple())
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(Message, PubsubStateEvent)
|
||||||
|
registerStanzaPlugin(PubsubStateEvent, PubsubState)
|
124
sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py
Normal file
124
sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
||||||
|
from sleekxmpp.stanza.iq import Iq
|
||||||
|
from sleekxmpp.stanza.message import Message
|
||||||
|
from sleekxmpp.basexmpp import basexmpp
|
||||||
|
from sleekxmpp.xmlstream.xmlstream import XMLStream
|
||||||
|
import logging
|
||||||
|
from sleekxmpp.plugins import xep_0004
|
||||||
|
|
||||||
|
class Event(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||||
|
name = 'event'
|
||||||
|
plugin_attrib = 'pubsub_event'
|
||||||
|
interfaces = set(('node',))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(Message, Event)
|
||||||
|
|
||||||
|
class EventItem(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||||
|
name = 'item'
|
||||||
|
plugin_attrib = 'item'
|
||||||
|
interfaces = set(('id', 'payload'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def setPayload(self, value):
|
||||||
|
self.xml.append(value)
|
||||||
|
|
||||||
|
def getPayload(self):
|
||||||
|
childs = self.xml.getchildren()
|
||||||
|
if len(childs) > 0:
|
||||||
|
return childs[0]
|
||||||
|
|
||||||
|
def delPayload(self):
|
||||||
|
for child in self.xml.getchildren():
|
||||||
|
self.xml.remove(child)
|
||||||
|
|
||||||
|
|
||||||
|
class EventRetract(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||||
|
name = 'retract'
|
||||||
|
plugin_attrib = 'retract'
|
||||||
|
interfaces = set(('id',))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
class EventItems(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||||
|
name = 'items'
|
||||||
|
plugin_attrib = 'items'
|
||||||
|
interfaces = set(('node',))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
subitem = (EventItem, EventRetract)
|
||||||
|
|
||||||
|
registerStanzaPlugin(Event, EventItems)
|
||||||
|
|
||||||
|
class EventCollection(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||||
|
name = 'collection'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node',))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(Event, EventCollection)
|
||||||
|
|
||||||
|
class EventAssociate(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||||
|
name = 'associate'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node',))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(EventCollection, EventAssociate)
|
||||||
|
|
||||||
|
class EventDisassociate(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||||
|
name = 'disassociate'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node',))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(EventCollection, EventDisassociate)
|
||||||
|
|
||||||
|
class EventConfiguration(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||||
|
name = 'configuration'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node', 'config'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(Event, EventConfiguration)
|
||||||
|
registerStanzaPlugin(EventConfiguration, xep_0004.Form)
|
||||||
|
|
||||||
|
class EventPurge(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||||
|
name = 'purge'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node',))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(Event, EventPurge)
|
||||||
|
|
||||||
|
class EventSubscription(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||||
|
name = 'subscription'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node','expiry', 'jid', 'subid', 'subscription'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def setJid(self, value):
|
||||||
|
self._setAttr('jid', str(value))
|
||||||
|
|
||||||
|
def getJid(self):
|
||||||
|
return JID(self._getAttr('jid'))
|
||||||
|
|
||||||
|
registerStanzaPlugin(Event, EventSubscription)
|
152
sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py
Normal file
152
sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
||||||
|
from sleekxmpp.stanza.iq import Iq
|
||||||
|
from sleekxmpp.stanza.message import Message
|
||||||
|
from sleekxmpp.basexmpp import basexmpp
|
||||||
|
from sleekxmpp.xmlstream.xmlstream import XMLStream
|
||||||
|
import logging
|
||||||
|
from sleekxmpp.plugins import xep_0004
|
||||||
|
from sleekxmpp.plugins.xep_0060.stanza.base import OptionalSetting
|
||||||
|
from sleekxmpp.plugins.xep_0060.stanza.pubsub import Affiliations, Affiliation, Configure, Subscriptions
|
||||||
|
|
||||||
|
class PubsubOwner(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||||
|
name = 'pubsub'
|
||||||
|
plugin_attrib = 'pubsub_owner'
|
||||||
|
interfaces = set(tuple())
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(Iq, PubsubOwner)
|
||||||
|
|
||||||
|
class DefaultConfig(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||||
|
name = 'default'
|
||||||
|
plugin_attrib = 'default'
|
||||||
|
interfaces = set(('node', 'type', 'config'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
ElementBase.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def getType(self):
|
||||||
|
t = self._getAttr('type')
|
||||||
|
if not t: t = 'leaf'
|
||||||
|
return t
|
||||||
|
|
||||||
|
def getConfig(self):
|
||||||
|
return self['form']
|
||||||
|
|
||||||
|
def setConfig(self, value):
|
||||||
|
self['form'].setStanzaValues(value.getStanzaValues())
|
||||||
|
return self
|
||||||
|
|
||||||
|
registerStanzaPlugin(PubsubOwner, DefaultConfig)
|
||||||
|
registerStanzaPlugin(DefaultConfig, xep_0004.Form)
|
||||||
|
|
||||||
|
class OwnerAffiliations(Affiliations):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||||
|
interfaces = set(('node'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def append(self, affiliation):
|
||||||
|
if not isinstance(affiliation, OwnerAffiliation):
|
||||||
|
raise TypeError
|
||||||
|
self.xml.append(affiliation.xml)
|
||||||
|
return self.affiliations.append(affiliation)
|
||||||
|
|
||||||
|
registerStanzaPlugin(PubsubOwner, OwnerAffiliations)
|
||||||
|
|
||||||
|
class OwnerAffiliation(Affiliation):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||||
|
interfaces = set(('affiliation', 'jid'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
class OwnerConfigure(Configure):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||||
|
interfaces = set(('node', 'config'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(PubsubOwner, OwnerConfigure)
|
||||||
|
|
||||||
|
class OwnerDefault(OwnerConfigure):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||||
|
interfaces = set(('node', 'config'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def getConfig(self):
|
||||||
|
return self['form']
|
||||||
|
|
||||||
|
def setConfig(self, value):
|
||||||
|
self['form'].setStanzaValues(value.getStanzaValues())
|
||||||
|
return self
|
||||||
|
|
||||||
|
registerStanzaPlugin(PubsubOwner, OwnerDefault)
|
||||||
|
registerStanzaPlugin(OwnerDefault, xep_0004.Form)
|
||||||
|
|
||||||
|
class OwnerDelete(ElementBase, OptionalSetting):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||||
|
name = 'delete'
|
||||||
|
plugin_attrib = 'delete'
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
interfaces = set(('node',))
|
||||||
|
|
||||||
|
registerStanzaPlugin(PubsubOwner, OwnerDelete)
|
||||||
|
|
||||||
|
class OwnerPurge(ElementBase, OptionalSetting):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||||
|
name = 'purge'
|
||||||
|
plugin_attrib = name
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
registerStanzaPlugin(PubsubOwner, OwnerPurge)
|
||||||
|
|
||||||
|
class OwnerRedirect(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||||
|
name = 'redirect'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('node', 'jid'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def setJid(self, value):
|
||||||
|
self._setAttr('jid', str(value))
|
||||||
|
|
||||||
|
def getJid(self):
|
||||||
|
return JID(self._getAttr('jid'))
|
||||||
|
|
||||||
|
registerStanzaPlugin(OwnerDelete, OwnerRedirect)
|
||||||
|
|
||||||
|
class OwnerSubscriptions(Subscriptions):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||||
|
interfaces = set(('node',))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def append(self, subscription):
|
||||||
|
if not isinstance(subscription, OwnerSubscription):
|
||||||
|
raise TypeError
|
||||||
|
self.xml.append(subscription.xml)
|
||||||
|
return self.subscriptions.append(subscription)
|
||||||
|
|
||||||
|
registerStanzaPlugin(PubsubOwner, OwnerSubscriptions)
|
||||||
|
|
||||||
|
class OwnerSubscription(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||||
|
name = 'subscription'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('jid', 'subscription'))
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def setJid(self, value):
|
||||||
|
self._setAttr('jid', str(value))
|
||||||
|
|
||||||
|
def getJid(self):
|
||||||
|
return JID(self._getAttr('from'))
|
11
sleekxmpp/plugins/xep_0066/__init__.py
Normal file
11
sleekxmpp/plugins/xep_0066/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
"""
|
||||||
|
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.plugins.xep_0066 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0066.stanza import OOB, OOBTransfer
|
||||||
|
from sleekxmpp.plugins.xep_0066.oob import xep_0066
|
154
sleekxmpp/plugins/xep_0066/oob.py
Normal file
154
sleekxmpp/plugins/xep_0066/oob.py
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
"""
|
||||||
|
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.stanza import Message, Presence, 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.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0066 import stanza
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0066(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0066: Out-of-Band Data
|
||||||
|
|
||||||
|
Out-of-Band Data is a basic method for transferring files between
|
||||||
|
XMPP agents. The URL of the resource in question is sent to the receiving
|
||||||
|
entity, which then downloads the resource before responding to the OOB
|
||||||
|
request. OOB is also used as a generic means to transmit URLs in other
|
||||||
|
stanzas to indicate where to find additional information.
|
||||||
|
|
||||||
|
Also see <http://www.xmpp.org/extensions/xep-0066.html>.
|
||||||
|
|
||||||
|
Events:
|
||||||
|
oob_transfer -- Raised when a request to download a resource
|
||||||
|
has been received.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
send_oob -- Send a request to another entity to download a file
|
||||||
|
or other addressable resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""Start the XEP-0066 plugin."""
|
||||||
|
self.xep = '0066'
|
||||||
|
self.description = 'Out-of-Band Transfer'
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
self.url_handlers = {'global': self._default_handler,
|
||||||
|
'jid': {}}
|
||||||
|
|
||||||
|
register_stanza_plugin(Iq, stanza.OOBTransfer)
|
||||||
|
register_stanza_plugin(Message, stanza.OOB)
|
||||||
|
register_stanza_plugin(Presence, stanza.OOB)
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('OOB Transfer',
|
||||||
|
StanzaPath('iq@type=set/oob_transfer'),
|
||||||
|
self._handle_transfer))
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
"""Handle cross-plugin dependencies."""
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace)
|
||||||
|
self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace)
|
||||||
|
|
||||||
|
def register_url_handler(self, jid=None, handler=None):
|
||||||
|
"""
|
||||||
|
Register a handler to process download requests, either for all
|
||||||
|
JIDs or a single JID.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- If None, then set the handler as a global default.
|
||||||
|
handler -- If None, then remove the existing handler for the
|
||||||
|
given JID, or reset the global handler if the JID
|
||||||
|
is None.
|
||||||
|
"""
|
||||||
|
if jid is None:
|
||||||
|
if handler is not None:
|
||||||
|
self.url_handlers['global'] = handler
|
||||||
|
else:
|
||||||
|
self.url_handlers['global'] = self._default_handler
|
||||||
|
else:
|
||||||
|
if handler is not None:
|
||||||
|
self.url_handlers['jid'][jid] = handler
|
||||||
|
else:
|
||||||
|
del self.url_handlers['jid'][jid]
|
||||||
|
|
||||||
|
def send_oob(self, to, url, desc=None, ifrom=None, **iqargs):
|
||||||
|
"""
|
||||||
|
Initiate a basic file transfer by sending the URL of
|
||||||
|
a file or other resource.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
url -- The URL of the resource to transfer.
|
||||||
|
desc -- An optional human readable description of the item
|
||||||
|
that is to be transferred.
|
||||||
|
ifrom -- Specifiy the sender's JID.
|
||||||
|
block -- If true, block and wait for the stanzas' reply.
|
||||||
|
timeout -- The time in seconds to block while waiting for
|
||||||
|
a reply. If None, then wait indefinitely.
|
||||||
|
callback -- Optional callback to execute when a reply is
|
||||||
|
received instead of blocking and waiting for
|
||||||
|
the reply.
|
||||||
|
"""
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['to'] = to
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
|
iq['oob_transfer']['url'] = url
|
||||||
|
iq['oob_transfer']['desc'] = desc
|
||||||
|
return iq.send(**iqargs)
|
||||||
|
|
||||||
|
def _run_url_handler(self, iq):
|
||||||
|
"""
|
||||||
|
Execute the appropriate handler for a transfer request.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The Iq stanza containing the OOB transfer request.
|
||||||
|
"""
|
||||||
|
if iq['to'] in self.url_handlers['jid']:
|
||||||
|
return self.url_handlers['jid'][jid](iq)
|
||||||
|
else:
|
||||||
|
if self.url_handlers['global']:
|
||||||
|
self.url_handlers['global'](iq)
|
||||||
|
else:
|
||||||
|
raise XMPPError('service-unavailable')
|
||||||
|
|
||||||
|
def _default_handler(self, iq):
|
||||||
|
"""
|
||||||
|
As a safe default, don't actually download files.
|
||||||
|
|
||||||
|
Register a new handler using self.register_url_handler to
|
||||||
|
screen requests and download files.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The Iq stanza containing the OOB transfer request.
|
||||||
|
"""
|
||||||
|
raise XMPPError('service-unavailable')
|
||||||
|
|
||||||
|
def _handle_transfer(self, iq):
|
||||||
|
"""
|
||||||
|
Handle receiving an out-of-band transfer request.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- An Iq stanza containing an OOB transfer request.
|
||||||
|
"""
|
||||||
|
log.debug('Received out-of-band data request for %s from %s:' % (
|
||||||
|
iq['oob_transfer']['url'], iq['from']))
|
||||||
|
self._run_url_handler(iq)
|
||||||
|
iq.reply().send()
|
33
sleekxmpp/plugins/xep_0066/stanza.py
Normal file
33
sleekxmpp/plugins/xep_0066/stanza.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
"""
|
||||||
|
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.xmlstream import ElementBase
|
||||||
|
|
||||||
|
|
||||||
|
class OOBTransfer(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'query'
|
||||||
|
namespace = 'jabber:iq:oob'
|
||||||
|
plugin_attrib = 'oob_transfer'
|
||||||
|
interfaces = set(('url', 'desc', 'sid'))
|
||||||
|
sub_interfaces = set(('url', 'desc'))
|
||||||
|
|
||||||
|
|
||||||
|
class OOB(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'x'
|
||||||
|
namespace = 'jabber:x:oob'
|
||||||
|
plugin_attrib = 'oob'
|
||||||
|
interfaces = set(('url', 'desc'))
|
||||||
|
sub_interfaces = interfaces
|
|
@ -1,72 +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 xml.etree import cElementTree as ET
|
|
||||||
import logging
|
|
||||||
import hashlib
|
|
||||||
from . import base
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class xep_0078(base.base_plugin):
|
|
||||||
"""
|
|
||||||
XEP-0078 NON-SASL Authentication
|
|
||||||
"""
|
|
||||||
def plugin_init(self):
|
|
||||||
self.description = "Non-SASL Authentication (broken)"
|
|
||||||
self.xep = "0078"
|
|
||||||
self.xmpp.add_event_handler("session_start", self.check_stream)
|
|
||||||
#disabling until I fix conflict with PLAIN
|
|
||||||
#self.xmpp.registerFeature("<auth xmlns='http://jabber.org/features/iq-auth'/>", self.auth)
|
|
||||||
self.streamid = ''
|
|
||||||
|
|
||||||
def check_stream(self, xml):
|
|
||||||
self.streamid = xml.attrib['id']
|
|
||||||
if xml.get('version', '0') != '1.0':
|
|
||||||
self.auth()
|
|
||||||
|
|
||||||
def auth(self, xml=None):
|
|
||||||
log.debug("Starting jabber:iq:auth Authentication")
|
|
||||||
auth_request = self.xmpp.makeIqGet()
|
|
||||||
auth_request_query = ET.Element('{jabber:iq:auth}query')
|
|
||||||
auth_request.attrib['to'] = self.xmpp.boundjid.host
|
|
||||||
username = ET.Element('username')
|
|
||||||
username.text = self.xmpp.username
|
|
||||||
auth_request_query.append(username)
|
|
||||||
auth_request.append(auth_request_query)
|
|
||||||
result = auth_request.send()
|
|
||||||
rquery = result.find('{jabber:iq:auth}query')
|
|
||||||
attempt = self.xmpp.makeIqSet()
|
|
||||||
query = ET.Element('{jabber:iq:auth}query')
|
|
||||||
resource = ET.Element('resource')
|
|
||||||
resource.text = self.xmpp.resource
|
|
||||||
query.append(username)
|
|
||||||
query.append(resource)
|
|
||||||
if rquery.find('{jabber:iq:auth}digest') is None:
|
|
||||||
log.warning("Authenticating via jabber:iq:auth Plain.")
|
|
||||||
password = ET.Element('password')
|
|
||||||
password.text = self.xmpp.password
|
|
||||||
query.append(password)
|
|
||||||
else:
|
|
||||||
log.debug("Authenticating via jabber:iq:auth Digest")
|
|
||||||
digest = ET.Element('digest')
|
|
||||||
digest.text = hashlib.sha1(b"%s%s" % (self.streamid, self.xmpp.password)).hexdigest()
|
|
||||||
query.append(digest)
|
|
||||||
attempt.append(query)
|
|
||||||
result = attempt.send()
|
|
||||||
if result.attrib['type'] == 'result':
|
|
||||||
with self.xmpp.lock:
|
|
||||||
self.xmpp.authenticated = True
|
|
||||||
self.xmpp.sessionstarted = True
|
|
||||||
self.xmpp.event("session_start")
|
|
||||||
else:
|
|
||||||
log.info("Authentication failed")
|
|
||||||
self.xmpp.disconnect()
|
|
||||||
self.xmpp.event("failed_auth")
|
|
12
sleekxmpp/plugins/xep_0078/__init__.py
Normal file
12
sleekxmpp/plugins/xep_0078/__init__.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"""
|
||||||
|
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.plugins.xep_0078 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0078.stanza import IqAuth, AuthFeature
|
||||||
|
from sleekxmpp.plugins.xep_0078.legacyauth import xep_0078
|
||||||
|
|
108
sleekxmpp/plugins/xep_0078/legacyauth.py
Normal file
108
sleekxmpp/plugins/xep_0078/legacyauth.py
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import Iq, StreamFeatures
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0078 import stanza
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0078(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0078 NON-SASL Authentication
|
||||||
|
|
||||||
|
This XEP is OBSOLETE in favor of using SASL, so DO NOT use this plugin
|
||||||
|
unless you are forced to use an old XMPP server implementation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xep = "0078"
|
||||||
|
self.description = "Non-SASL Authentication"
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
self.xmpp.register_feature('auth',
|
||||||
|
self._handle_auth,
|
||||||
|
restart=False,
|
||||||
|
order=self.config.get('order', 15))
|
||||||
|
|
||||||
|
register_stanza_plugin(Iq, stanza.IqAuth)
|
||||||
|
register_stanza_plugin(StreamFeatures, stanza.AuthFeature)
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_auth(self, features):
|
||||||
|
# If we can or have already authenticated with SASL, do nothing.
|
||||||
|
if 'mechanisms' in features['features']:
|
||||||
|
return False
|
||||||
|
if self.xmpp.authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.debug("Starting jabber:iq:auth Authentication")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
resp = iq.send(now=True)
|
||||||
|
|
||||||
|
if resp is None or resp['type'] != 'result':
|
||||||
|
log.info("Authentication failed: %s" % resp['error']['condition'])
|
||||||
|
self.xmpp.event('failed_auth', resp, direct=True)
|
||||||
|
self.xmpp.disconnect()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
if 'digest' in resp['auth']['fields']:
|
||||||
|
log.debug('Authenticating via jabber:iq:auth Digest')
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
stream_id = bytes(self.xmpp.stream_id)
|
||||||
|
password = bytes(self.xmpp.password)
|
||||||
|
else:
|
||||||
|
stream_id = bytes(self.xmpp.stream_id, encoding='utf-8')
|
||||||
|
password = bytes(self.xmpp.password, encoding='utf-8')
|
||||||
|
|
||||||
|
digest = hashlib.sha1(b'%s%s' % (stream_id, password)).hexdigest()
|
||||||
|
iq['auth']['digest'] = digest
|
||||||
|
else:
|
||||||
|
log.warning('Authenticating via jabber:iq:auth Plain.')
|
||||||
|
iq['auth']['password'] = self.xmpp.password
|
||||||
|
|
||||||
|
# Step 3: Send credentials
|
||||||
|
result = iq.send(now=True)
|
||||||
|
if result is not None and result.attrib['type'] == 'result':
|
||||||
|
self.xmpp.features.add('auth')
|
||||||
|
|
||||||
|
self.xmpp.authenticated = True
|
||||||
|
log.debug("Established Session")
|
||||||
|
self.xmpp.sessionstarted = True
|
||||||
|
self.xmpp.session_started_event.set()
|
||||||
|
self.xmpp.event('session_start')
|
||||||
|
else:
|
||||||
|
log.info("Authentication failed")
|
||||||
|
self.xmpp.disconnect()
|
||||||
|
self.xmpp.event("failed_auth")
|
||||||
|
|
||||||
|
return True
|
43
sleekxmpp/plugins/xep_0078/stanza.py
Normal file
43
sleekxmpp/plugins/xep_0078/stanza.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class IqAuth(ElementBase):
|
||||||
|
namespace = 'jabber:iq:auth'
|
||||||
|
name = 'query'
|
||||||
|
plugin_attrib = 'auth'
|
||||||
|
interfaces = set(('fields', 'username', 'password', 'resource', 'digest'))
|
||||||
|
sub_interfaces = set(('username', 'password', 'resource', 'digest'))
|
||||||
|
plugin_tag_map = {}
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
|
||||||
|
def get_fields(self):
|
||||||
|
fields = set()
|
||||||
|
for field in self.sub_interfaces:
|
||||||
|
if self.xml.find('{%s}%s' % (self.namespace, field)) is not None:
|
||||||
|
fields.add(field)
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def set_resource(self, value):
|
||||||
|
self._set_sub_text('resource', value, keep=True)
|
||||||
|
|
||||||
|
def set_password(self, value):
|
||||||
|
self._set_sub_text('password', value, keep=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthFeature(ElementBase):
|
||||||
|
namespace = 'http://jabber.org/features/iq-auth'
|
||||||
|
name = 'auth'
|
||||||
|
plugin_attrib = 'auth'
|
||||||
|
interfaces = set()
|
||||||
|
plugin_tag_map = {}
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
|
||||||
|
|
206
sleekxmpp/plugins/xep_0082.py
Normal file
206
sleekxmpp/plugins/xep_0082.py
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.thirdparty import tzutc, tzoffset, parse_iso
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# To make it easier for stanzas without direct access to plugin objects
|
||||||
|
# to use the XEP-0082 utility methods, we will define them as top-level
|
||||||
|
# functions and then just reference them in the plugin itself.
|
||||||
|
|
||||||
|
def parse(time_str):
|
||||||
|
"""
|
||||||
|
Convert a string timestamp into a datetime object.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
time_str -- A formatted timestamp string.
|
||||||
|
"""
|
||||||
|
return parse_iso(time_str)
|
||||||
|
|
||||||
|
|
||||||
|
def format_date(time_obj):
|
||||||
|
"""
|
||||||
|
Return a formatted string version of a date object.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
YYYY-MM-DD
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
time_obj -- A date or datetime object.
|
||||||
|
"""
|
||||||
|
if isinstance(time_obj, dt.datetime):
|
||||||
|
time_obj = time_obj.date()
|
||||||
|
return time_obj.isoformat()
|
||||||
|
|
||||||
|
def format_time(time_obj):
|
||||||
|
"""
|
||||||
|
Return a formatted string version of a time object.
|
||||||
|
|
||||||
|
format:
|
||||||
|
hh:mm:ss[.sss][TZD]
|
||||||
|
|
||||||
|
arguments:
|
||||||
|
time_obj -- A time or datetime object.
|
||||||
|
"""
|
||||||
|
if isinstance(time_obj, dt.datetime):
|
||||||
|
time_obj = time_obj.timetz()
|
||||||
|
timestamp = time_obj.isoformat()
|
||||||
|
if time_obj.tzinfo == tzutc():
|
||||||
|
timestamp = timestamp[:-6]
|
||||||
|
return '%sZ' % timestamp
|
||||||
|
return timestamp
|
||||||
|
|
||||||
|
def format_datetime(time_obj):
|
||||||
|
"""
|
||||||
|
Return a formatted string version of a datetime object.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
YYYY-MM-DDThh:mm:ss[.sss]TZD
|
||||||
|
|
||||||
|
arguments:
|
||||||
|
time_obj -- A datetime object.
|
||||||
|
"""
|
||||||
|
timestamp = time_obj.isoformat('T')
|
||||||
|
if time_obj.tzinfo == tzutc():
|
||||||
|
timestamp = timestamp[:-6]
|
||||||
|
return '%sZ' % timestamp
|
||||||
|
return timestamp
|
||||||
|
|
||||||
|
def date(year=None, month=None, day=None):
|
||||||
|
"""
|
||||||
|
Create a date only timestamp for the given instant.
|
||||||
|
|
||||||
|
Unspecified components default to their current counterparts.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
year -- Integer value of the year (4 digits)
|
||||||
|
month -- Integer value of the month
|
||||||
|
day -- Integer value of the day of the month.
|
||||||
|
"""
|
||||||
|
today = dt.datetime.today()
|
||||||
|
if year is None:
|
||||||
|
year = today.year
|
||||||
|
if month is None:
|
||||||
|
month = today.month
|
||||||
|
if day is None:
|
||||||
|
day = today.day
|
||||||
|
return format_date(dt.date(year, month, day))
|
||||||
|
|
||||||
|
def time(hour=None, min=None, sec=None, micro=None, offset=None):
|
||||||
|
"""
|
||||||
|
Create a time only timestamp for the given instant.
|
||||||
|
|
||||||
|
Unspecified components default to their current counterparts.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
hour -- Integer value of the hour.
|
||||||
|
min -- Integer value of the number of minutes.
|
||||||
|
sec -- Integer value of the number of seconds.
|
||||||
|
micro -- Integer value of the number of microseconds.
|
||||||
|
offset -- Either a positive or negative number of seconds
|
||||||
|
to offset from UTC to match a desired timezone,
|
||||||
|
or a tzinfo object.
|
||||||
|
"""
|
||||||
|
now = dt.datetime.utcnow()
|
||||||
|
if hour is None:
|
||||||
|
hour = now.hour
|
||||||
|
if min is None:
|
||||||
|
min = now.minute
|
||||||
|
if sec is None:
|
||||||
|
sec = now.second
|
||||||
|
if micro is None:
|
||||||
|
micro = now.microsecond
|
||||||
|
if offset is None:
|
||||||
|
offset = tzutc()
|
||||||
|
elif not isinstance(offset, dt.tzinfo):
|
||||||
|
offset = tzoffset(None, offset)
|
||||||
|
time = dt.time(hour, min, sec, micro, offset)
|
||||||
|
return format_time(time)
|
||||||
|
|
||||||
|
def datetime(year=None, month=None, day=None, hour=None,
|
||||||
|
min=None, sec=None, micro=None, offset=None,
|
||||||
|
separators=True):
|
||||||
|
"""
|
||||||
|
Create a datetime timestamp for the given instant.
|
||||||
|
|
||||||
|
Unspecified components default to their current counterparts.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
year -- Integer value of the year (4 digits)
|
||||||
|
month -- Integer value of the month
|
||||||
|
day -- Integer value of the day of the month.
|
||||||
|
hour -- Integer value of the hour.
|
||||||
|
min -- Integer value of the number of minutes.
|
||||||
|
sec -- Integer value of the number of seconds.
|
||||||
|
micro -- Integer value of the number of microseconds.
|
||||||
|
offset -- Either a positive or negative number of seconds
|
||||||
|
to offset from UTC to match a desired timezone,
|
||||||
|
or a tzinfo object.
|
||||||
|
"""
|
||||||
|
now = dt.datetime.utcnow()
|
||||||
|
if year is None:
|
||||||
|
year = now.year
|
||||||
|
if month is None:
|
||||||
|
month = now.month
|
||||||
|
if day is None:
|
||||||
|
day = now.day
|
||||||
|
if hour is None:
|
||||||
|
hour = now.hour
|
||||||
|
if min is None:
|
||||||
|
min = now.minute
|
||||||
|
if sec is None:
|
||||||
|
sec = now.second
|
||||||
|
if micro is None:
|
||||||
|
micro = now.microsecond
|
||||||
|
if offset is None:
|
||||||
|
offset = tzutc()
|
||||||
|
elif not isinstance(offset, dt.tzinfo):
|
||||||
|
offset = tzoffset(None, offset)
|
||||||
|
|
||||||
|
date = dt.datetime(year, month, day, hour,
|
||||||
|
min, sec, micro, offset)
|
||||||
|
return format_datetime(date)
|
||||||
|
|
||||||
|
class xep_0082(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0082: XMPP Date and Time Profiles
|
||||||
|
|
||||||
|
XMPP uses a subset of the formats allowed by ISO 8601 as a matter of
|
||||||
|
pragmatism based on the relatively few formats historically used by
|
||||||
|
the XMPP.
|
||||||
|
|
||||||
|
Also see <http://www.xmpp.org/extensions/xep-0082.html>.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
date -- Create a time stamp using the Date profile.
|
||||||
|
datetime -- Create a time stamp using the DateTime profile.
|
||||||
|
time -- Create a time stamp using the Time profile.
|
||||||
|
format_date -- Format an existing date object.
|
||||||
|
format_datetime -- Format an existing datetime object.
|
||||||
|
format_time -- Format an existing time object.
|
||||||
|
parse -- Convert a time string into a Python datetime object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""Start the XEP-0082 plugin."""
|
||||||
|
self.xep = '0082'
|
||||||
|
self.description = 'XMPP Date and Time Profiles'
|
||||||
|
|
||||||
|
self.date = date
|
||||||
|
self.datetime = datetime
|
||||||
|
self.time = time
|
||||||
|
self.format_date = format_date
|
||||||
|
self.format_datetime = format_datetime
|
||||||
|
self.format_time = format_time
|
||||||
|
self.parse = parse
|
|
@ -35,7 +35,7 @@ class xep_0092(base_plugin):
|
||||||
self.stanza = sleekxmpp.plugins.xep_0092.stanza
|
self.stanza = sleekxmpp.plugins.xep_0092.stanza
|
||||||
|
|
||||||
self.name = self.config.get('name', 'SleekXMPP')
|
self.name = self.config.get('name', 'SleekXMPP')
|
||||||
self.version = self.config.get('version', '0.1-dev')
|
self.version = self.config.get('version', sleekxmpp.__version__)
|
||||||
self.os = self.config.get('os', '')
|
self.os = self.config.get('os', '')
|
||||||
|
|
||||||
self.getVersion = self.get_version
|
self.getVersion = self.get_version
|
||||||
|
|
|
@ -108,7 +108,7 @@ class xep_0199(base_plugin):
|
||||||
iq -- The ping request.
|
iq -- The ping request.
|
||||||
"""
|
"""
|
||||||
log.debug("Pinged by %s" % iq['from'])
|
log.debug("Pinged by %s" % iq['from'])
|
||||||
iq.reply().enable('ping').send()
|
iq.reply().send()
|
||||||
|
|
||||||
def send_ping(self, jid, timeout=None, errorfalse=False,
|
def send_ping(self, jid, timeout=None, errorfalse=False,
|
||||||
ifrom=None, block=True, callback=None):
|
ifrom=None, block=True, callback=None):
|
||||||
|
|
|
@ -1,117 +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 datetime import datetime, tzinfo
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
|
|
||||||
from . import base
|
|
||||||
from .. stanza.iq import Iq
|
|
||||||
from .. xmlstream.handler.callback import Callback
|
|
||||||
from .. xmlstream.matcher.xpath import MatchXPath
|
|
||||||
from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class EntityTime(ElementBase):
|
|
||||||
name = 'time'
|
|
||||||
namespace = 'urn:xmpp:time'
|
|
||||||
plugin_attrib = 'entity_time'
|
|
||||||
interfaces = set(('tzo', 'utc'))
|
|
||||||
sub_interfaces = set(('tzo', 'utc'))
|
|
||||||
|
|
||||||
#def get_tzo(self):
|
|
||||||
# TODO: Right now it returns a string but maybe it should
|
|
||||||
# return a datetime.tzinfo object or maybe a datetime.timedelta?
|
|
||||||
#pass
|
|
||||||
|
|
||||||
def set_tzo(self, tzo):
|
|
||||||
if isinstance(tzo, tzinfo):
|
|
||||||
td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here'
|
|
||||||
seconds = td.seconds + td.days * 24 * 3600
|
|
||||||
sign = ('+' if seconds >= 0 else '-')
|
|
||||||
minutes = abs(seconds // 60)
|
|
||||||
tzo = '{sign}{hours:02d}:{minutes:02d}'.format(sign=sign, hours=minutes//60, minutes=minutes%60)
|
|
||||||
elif not isinstance(tzo, str):
|
|
||||||
raise TypeError('The time should be a string or a datetime.tzinfo object.')
|
|
||||||
self._set_sub_text('tzo', tzo)
|
|
||||||
|
|
||||||
def get_utc(self):
|
|
||||||
# Returns a datetime object instead the string. Is this a good idea?
|
|
||||||
value = self._get_sub_text('utc')
|
|
||||||
if '.' in value:
|
|
||||||
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
|
|
||||||
else:
|
|
||||||
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
|
|
||||||
def set_utc(self, tim=None):
|
|
||||||
if isinstance(tim, datetime):
|
|
||||||
if tim.utcoffset():
|
|
||||||
tim = tim - tim.utcoffset()
|
|
||||||
tim = tim.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
elif isinstance(tim, time.struct_time):
|
|
||||||
tim = time.strftime('%Y-%m-%dT%H:%M:%SZ', tim)
|
|
||||||
elif not isinstance(tim, str):
|
|
||||||
raise TypeError('The time should be a string or a datetime.datetime or time.struct_time object.')
|
|
||||||
|
|
||||||
self._set_sub_text('utc', tim)
|
|
||||||
|
|
||||||
|
|
||||||
class xep_0202(base.base_plugin):
|
|
||||||
"""
|
|
||||||
XEP-0202 Entity Time
|
|
||||||
"""
|
|
||||||
def plugin_init(self):
|
|
||||||
self.description = "Entity Time"
|
|
||||||
self.xep = "0202"
|
|
||||||
|
|
||||||
self.xmpp.registerHandler(
|
|
||||||
Callback('Time Request',
|
|
||||||
MatchXPath('{%s}iq/{%s}time' % (self.xmpp.default_ns,
|
|
||||||
EntityTime.namespace)),
|
|
||||||
self.handle_entity_time_query))
|
|
||||||
register_stanza_plugin(Iq, EntityTime)
|
|
||||||
|
|
||||||
self.xmpp.add_event_handler('entity_time_request', self.handle_entity_time)
|
|
||||||
|
|
||||||
|
|
||||||
def post_init(self):
|
|
||||||
base.base_plugin.post_init(self)
|
|
||||||
|
|
||||||
self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:time')
|
|
||||||
|
|
||||||
def handle_entity_time_query(self, iq):
|
|
||||||
if iq['type'] == 'get':
|
|
||||||
log.debug("Entity time requested by %s" % iq['from'])
|
|
||||||
self.xmpp.event('entity_time_request', iq)
|
|
||||||
elif iq['type'] == 'result':
|
|
||||||
log.debug("Entity time result from %s" % iq['from'])
|
|
||||||
self.xmpp.event('entity_time', iq)
|
|
||||||
|
|
||||||
def handle_entity_time(self, iq):
|
|
||||||
iq = iq.reply()
|
|
||||||
iq.enable('entity_time')
|
|
||||||
tzo = time.strftime('%z') # %z is not on all ANSI C libraries
|
|
||||||
tzo = tzo[:3] + ':' + tzo[3:]
|
|
||||||
iq['entity_time']['tzo'] = tzo
|
|
||||||
iq['entity_time']['utc'] = datetime.utcnow()
|
|
||||||
iq.send()
|
|
||||||
|
|
||||||
def get_entity_time(self, jid):
|
|
||||||
iq = self.xmpp.makeIqGet()
|
|
||||||
iq.enable('entity_time')
|
|
||||||
iq.attrib['to'] = jid
|
|
||||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
|
||||||
id = iq.get('id')
|
|
||||||
result = iq.send()
|
|
||||||
if result and result is not None and result.get('type', 'error') != 'error':
|
|
||||||
return {'utc': result['entity_time']['utc'], 'tzo': result['entity_time']['tzo']}
|
|
||||||
else:
|
|
||||||
return False
|
|
12
sleekxmpp/plugins/xep_0202/__init__.py
Normal file
12
sleekxmpp/plugins/xep_0202/__init__.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"""
|
||||||
|
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.plugins.xep_0202 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0202.stanza import EntityTime
|
||||||
|
from sleekxmpp.plugins.xep_0202.time import xep_0202
|
127
sleekxmpp/plugins/xep_0202/stanza.py
Normal file
127
sleekxmpp/plugins/xep_0202/stanza.py
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
"""
|
||||||
|
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 logging
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from sleekxmpp.xmlstream import ElementBase
|
||||||
|
from sleekxmpp.plugins import xep_0082
|
||||||
|
from sleekxmpp.thirdparty import tzutc, tzoffset
|
||||||
|
|
||||||
|
|
||||||
|
class EntityTime(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
The <time> element represents the local time for an XMPP agent.
|
||||||
|
The time is expressed in UTC to make synchronization easier
|
||||||
|
between entities, but the offset for the local timezone is also
|
||||||
|
included.
|
||||||
|
|
||||||
|
Example <time> stanzas:
|
||||||
|
<iq type="result">
|
||||||
|
<time xmlns="urn:xmpp:time">
|
||||||
|
<utc>2011-07-03T11:37:12.234569</utc>
|
||||||
|
<tzo>-07:00</tzo>
|
||||||
|
</time>
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
Stanza Interface:
|
||||||
|
time -- The local time for the entity (updates utc and tzo).
|
||||||
|
utc -- The UTC equivalent to local time.
|
||||||
|
tzo -- The local timezone offset from UTC.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
get_time -- Return local time datetime object.
|
||||||
|
set_time -- Set UTC and TZO fields.
|
||||||
|
del_time -- Remove both UTC and TZO fields.
|
||||||
|
get_utc -- Return datetime object of UTC time.
|
||||||
|
set_utc -- Set the UTC time.
|
||||||
|
get_tzo -- Return tzinfo object.
|
||||||
|
set_tzo -- Set the local timezone offset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'time'
|
||||||
|
namespace = 'urn:xmpp:time'
|
||||||
|
plugin_attrib = 'entity_time'
|
||||||
|
interfaces = set(('tzo', 'utc', 'time'))
|
||||||
|
sub_interfaces = interfaces
|
||||||
|
|
||||||
|
def set_time(self, value):
|
||||||
|
"""
|
||||||
|
Set both the UTC and TZO fields given a time object.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
value -- A datetime object or properly formatted
|
||||||
|
string equivalent.
|
||||||
|
"""
|
||||||
|
date = value
|
||||||
|
if not isinstance(value, dt.datetime):
|
||||||
|
date = xep_0082.parse(value)
|
||||||
|
self['utc'] = date
|
||||||
|
self['tzo'] = date.tzinfo
|
||||||
|
|
||||||
|
def get_time(self):
|
||||||
|
"""
|
||||||
|
Return the entity's local time based on the UTC and TZO data.
|
||||||
|
"""
|
||||||
|
date = self['utc']
|
||||||
|
tz = self['tzo']
|
||||||
|
return date.astimezone(tz)
|
||||||
|
|
||||||
|
def del_time(self):
|
||||||
|
"""Remove both the UTC and TZO fields."""
|
||||||
|
del self['utc']
|
||||||
|
del self['tzo']
|
||||||
|
|
||||||
|
def get_tzo(self):
|
||||||
|
"""
|
||||||
|
Return the timezone offset from UTC as a tzinfo object.
|
||||||
|
"""
|
||||||
|
tzo = self._get_sub_text('tzo')
|
||||||
|
if tzo == '':
|
||||||
|
tzo = 'Z'
|
||||||
|
time = xep_0082.parse('00:00:00%s' % tzo)
|
||||||
|
return time.tzinfo
|
||||||
|
|
||||||
|
def set_tzo(self, value):
|
||||||
|
"""
|
||||||
|
Set the timezone offset from UTC.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
value -- Either a tzinfo object or the number of
|
||||||
|
seconds (positive or negative) to offset.
|
||||||
|
"""
|
||||||
|
time = xep_0082.time(offset=value)
|
||||||
|
if xep_0082.parse(time).tzinfo == tzutc():
|
||||||
|
self._set_sub_text('tzo', 'Z')
|
||||||
|
else:
|
||||||
|
self._set_sub_text('tzo', time[-6:])
|
||||||
|
|
||||||
|
def get_utc(self):
|
||||||
|
"""
|
||||||
|
Return the time in UTC as a datetime object.
|
||||||
|
"""
|
||||||
|
value = self._get_sub_text('utc')
|
||||||
|
if value == '':
|
||||||
|
return xep_0082.parse(xep_0082.datetime())
|
||||||
|
return xep_0082.parse('%sZ' % value)
|
||||||
|
|
||||||
|
def set_utc(self, value):
|
||||||
|
"""
|
||||||
|
Set the time in UTC.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
value -- A datetime object or properly formatted
|
||||||
|
string equivalent.
|
||||||
|
"""
|
||||||
|
date = value
|
||||||
|
if not isinstance(value, dt.datetime):
|
||||||
|
date = xep_0082.parse(value)
|
||||||
|
date = date.astimezone(tzutc())
|
||||||
|
value = xep_0082.format_datetime(date)[:-1]
|
||||||
|
self._set_sub_text('utc', value)
|
92
sleekxmpp/plugins/xep_0202/time.py
Normal file
92
sleekxmpp/plugins/xep_0202/time.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
"""
|
||||||
|
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 logging
|
||||||
|
|
||||||
|
from sleekxmpp.stanza.iq import Iq
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.xmlstream.handler import Callback
|
||||||
|
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins import xep_0082
|
||||||
|
from sleekxmpp.plugins.xep_0202 import stanza
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0202(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0202: Entity Time
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""Start the XEP-0203 plugin."""
|
||||||
|
self.xep = '0202'
|
||||||
|
self.description = 'Entity Time'
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
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)
|
||||||
|
if not self.local_time:
|
||||||
|
self.local_time = lambda x: xep_0082.datetime(offset=self.tz_offset)
|
||||||
|
|
||||||
|
self.xmpp.registerHandler(
|
||||||
|
Callback('Entity Time',
|
||||||
|
StanzaPath('iq/entity_time'),
|
||||||
|
self._handle_time_request))
|
||||||
|
register_stanza_plugin(Iq, stanza.EntityTime)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
"""Handle cross-plugin interactions."""
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
self.xmpp['xep_0030'].add_feature('urn:xmpp:time')
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_time_request(self, iq):
|
||||||
|
"""
|
||||||
|
Respond to a request for the local time.
|
||||||
|
|
||||||
|
The time is taken from self.local_time(), which may be replaced
|
||||||
|
during plugin configuration with a function that maps JIDs to
|
||||||
|
times.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The Iq time request stanza.
|
||||||
|
"""
|
||||||
|
iq.reply()
|
||||||
|
iq['entity_time']['time'] = self.local_time(iq['to'])
|
||||||
|
iq.send()
|
||||||
|
|
||||||
|
def get_entity_time(self, to, ifrom=None, **iqargs):
|
||||||
|
"""
|
||||||
|
Request the time from another entity.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
to -- JID of the entity to query.
|
||||||
|
ifrom -- Specifiy the sender's JID.
|
||||||
|
block -- If true, block and wait for the stanzas' reply.
|
||||||
|
timeout -- The time in seconds to block while waiting for
|
||||||
|
a reply. If None, then wait indefinitely.
|
||||||
|
callback -- Optional callback to execute when a reply is
|
||||||
|
received instead of blocking and waiting for
|
||||||
|
the reply.
|
||||||
|
"""
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq['to'] = 'to'
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = 'ifrom'
|
||||||
|
iq.enable('entity_time')
|
||||||
|
return iq.send(**iqargs)
|
12
sleekxmpp/plugins/xep_0203/__init__.py
Normal file
12
sleekxmpp/plugins/xep_0203/__init__.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"""
|
||||||
|
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.plugins.xep_0203 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0203.stanza import Delay
|
||||||
|
from sleekxmpp.plugins.xep_0203.delay import xep_0203
|
||||||
|
|
36
sleekxmpp/plugins/xep_0203/delay.py
Normal file
36
sleekxmpp/plugins/xep_0203/delay.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
"""
|
||||||
|
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.stanza import Message, Presence
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0203 import stanza
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0203(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0203: Delayed Delivery
|
||||||
|
|
||||||
|
XMPP stanzas are sometimes withheld for delivery due to the recipient
|
||||||
|
being offline, or are resent in order to establish recent history as
|
||||||
|
is the case with MUCS. In any case, it is important to know when the
|
||||||
|
stanza was originally sent, not just when it was last received.
|
||||||
|
|
||||||
|
Also see <http://www.xmpp.org/extensions/xep-0203.html>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""Start the XEP-0203 plugin."""
|
||||||
|
self.xep = '0203'
|
||||||
|
self.description = 'Delayed Delivery'
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
register_stanza_plugin(Message, stanza.Delay)
|
||||||
|
register_stanza_plugin(Presence, stanza.Delay)
|
41
sleekxmpp/plugins/xep_0203/stanza.py
Normal file
41
sleekxmpp/plugins/xep_0203/stanza.py
Normal file
|
@ -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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
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_stamp(self):
|
||||||
|
timestamp = self._get_attr('stamp')
|
||||||
|
return xep_0082.parse(timestamp)
|
||||||
|
|
||||||
|
def set_stamp(self, value):
|
||||||
|
if isinstance(value, dt.datetime):
|
||||||
|
value = xep_0082.format_datetime(value)
|
||||||
|
self._set_attr('stamp', value)
|
||||||
|
|
||||||
|
def get_text(self):
|
||||||
|
return self.xml.text
|
||||||
|
|
||||||
|
def set_text(self, value):
|
||||||
|
self.xml.text = value
|
||||||
|
|
||||||
|
def del_text(self):
|
||||||
|
self.xml.text = ''
|
11
sleekxmpp/plugins/xep_0224/__init__.py
Normal file
11
sleekxmpp/plugins/xep_0224/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
"""
|
||||||
|
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.plugins.xep_0224 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0224.stanza import Attention
|
||||||
|
from sleekxmpp.plugins.xep_0224.attention import xep_0224
|
72
sleekxmpp/plugins/xep_0224/attention.py
Normal file
72
sleekxmpp/plugins/xep_0224/attention.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
"""
|
||||||
|
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.stanza import Message
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.xmlstream.handler import Callback
|
||||||
|
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0224 import stanza
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0224(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0224: Attention
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""Start the XEP-0224 plugin."""
|
||||||
|
self.xep = '0224'
|
||||||
|
self.description = 'Attention'
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
register_stanza_plugin(Message, stanza.Attention)
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('Attention',
|
||||||
|
StanzaPath('message/attention'),
|
||||||
|
self._handle_attention))
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
"""Handle cross-plugin dependencies."""
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
self.xmpp['xep_0030'].add_feature(stanza.Attention.namespace)
|
||||||
|
|
||||||
|
def request_attention(self, to, mfrom=None, mbody=''):
|
||||||
|
"""
|
||||||
|
Send an attention message with an optional body.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
to -- The attention request recipient's JID.
|
||||||
|
mfrom -- Optionally specify the sender of the attention request.
|
||||||
|
mbody -- An optional message body to include in the request.
|
||||||
|
"""
|
||||||
|
m = self.xmpp.Message()
|
||||||
|
m['to'] = to
|
||||||
|
m['type'] = 'headline'
|
||||||
|
m['attention'] = True
|
||||||
|
if mfrom:
|
||||||
|
m['from'] = mfrom
|
||||||
|
m['body'] = mbody
|
||||||
|
m.send()
|
||||||
|
|
||||||
|
def _handle_attention(self, msg):
|
||||||
|
"""
|
||||||
|
Raise an event after receiving a message with an attention request.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
msg -- A message stanza with an attention element.
|
||||||
|
"""
|
||||||
|
log.debug("Received attention request from: %s" % msg['from'])
|
||||||
|
self.xmpp.event('attention', msg)
|
40
sleekxmpp/plugins/xep_0224/stanza.py
Normal file
40
sleekxmpp/plugins/xep_0224/stanza.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
"""
|
||||||
|
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.xmlstream import ElementBase, ET
|
||||||
|
|
||||||
|
|
||||||
|
class Attention(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'attention'
|
||||||
|
namespace = 'urn:xmpp:attention:0'
|
||||||
|
plugin_attrib = 'attention'
|
||||||
|
interfaces = set(('attention',))
|
||||||
|
is_extension = True
|
||||||
|
|
||||||
|
def setup(self, xml):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_attention(self, value):
|
||||||
|
if value:
|
||||||
|
xml = ET.Element(self.tag_name())
|
||||||
|
self.parent().xml.append(xml)
|
||||||
|
else:
|
||||||
|
self.del_attention()
|
||||||
|
|
||||||
|
def get_attention(self):
|
||||||
|
xml = self.parent().xml.find(self.tag_name())
|
||||||
|
return xml is not None
|
||||||
|
|
||||||
|
def del_attention(self):
|
||||||
|
xml = self.parent().xml.find(self.tag_name())
|
||||||
|
if xml is not None:
|
||||||
|
self.parent().xml.remove(xml)
|
|
@ -8,7 +8,8 @@
|
||||||
|
|
||||||
|
|
||||||
from sleekxmpp.stanza.error import Error
|
from sleekxmpp.stanza.error import Error
|
||||||
from sleekxmpp.stanza.stream_error import StreamError
|
|
||||||
from sleekxmpp.stanza.iq import Iq
|
from sleekxmpp.stanza.iq import Iq
|
||||||
from sleekxmpp.stanza.message import Message
|
from sleekxmpp.stanza.message import Message
|
||||||
from sleekxmpp.stanza.presence import Presence
|
from sleekxmpp.stanza.presence import Presence
|
||||||
|
from sleekxmpp.stanza.stream_features import StreamFeatures
|
||||||
|
from sleekxmpp.stanza.stream_error import StreamError
|
||||||
|
|
|
@ -88,7 +88,9 @@ class Error(ElementBase):
|
||||||
"""Return the condition element's name."""
|
"""Return the condition element's name."""
|
||||||
for child in self.xml.getchildren():
|
for child in self.xml.getchildren():
|
||||||
if "{%s}" % self.condition_ns in child.tag:
|
if "{%s}" % self.condition_ns in child.tag:
|
||||||
return child.tag.split('}', 1)[-1]
|
cond = child.tag.split('}', 1)[-1]
|
||||||
|
if cond in self.conditions:
|
||||||
|
return cond
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def set_condition(self, value):
|
def set_condition(self, value):
|
||||||
|
|
|
@ -97,7 +97,7 @@ class Message(RootStanza):
|
||||||
clear -- Indicates if existing content should be removed
|
clear -- Indicates if existing content should be removed
|
||||||
before replying. Defaults to True.
|
before replying. Defaults to True.
|
||||||
"""
|
"""
|
||||||
StanzaBase.reply(self)
|
StanzaBase.reply(self, clear)
|
||||||
if self['type'] == 'groupchat':
|
if self['type'] == 'groupchat':
|
||||||
self['to'] = self['to'].bare
|
self['to'] = self['to'].bare
|
||||||
|
|
||||||
|
|
|
@ -64,8 +64,7 @@ class RootStanza(StanzaBase):
|
||||||
# log the error
|
# log the error
|
||||||
log.exception('Error handling {%s}%s stanza' %
|
log.exception('Error handling {%s}%s stanza' %
|
||||||
(self.namespace, self.name))
|
(self.namespace, self.name))
|
||||||
# Finally raise the exception, so it can be handled (or not)
|
# Finally raise the exception to a global exception handler
|
||||||
# at a higher level by using sys.excepthook.
|
self.stream.exception(e)
|
||||||
raise e
|
|
||||||
|
|
||||||
register_stanza_plugin(RootStanza, Error)
|
register_stanza_plugin(RootStanza, Error)
|
||||||
|
|
54
sleekxmpp/stanza/stream_features.py
Normal file
54
sleekxmpp/stanza/stream_features.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
"""
|
||||||
|
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 sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class StreamFeatures(StanzaBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'features'
|
||||||
|
namespace = 'http://etherx.jabber.org/streams'
|
||||||
|
interfaces = set(('features', 'required', 'optional'))
|
||||||
|
sub_interfaces = interfaces
|
||||||
|
plugin_tag_map = {}
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
|
||||||
|
def setup(self, xml):
|
||||||
|
StanzaBase.setup(self, xml)
|
||||||
|
self.values = self.values
|
||||||
|
|
||||||
|
def get_features(self):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
return self.plugins
|
||||||
|
|
||||||
|
def set_features(self, value):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def del_features(self):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_required(self):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
features = self['features']
|
||||||
|
return [f for n, f in features.items() if f['required']]
|
||||||
|
|
||||||
|
def get_optional(self):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
features = self['features']
|
||||||
|
return [f for n, f in features.items() if not f['required']]
|
|
@ -318,9 +318,11 @@ class SleekTest(unittest.TestCase):
|
||||||
self.xmpp.socket.recv_data(header)
|
self.xmpp.socket.recv_data(header)
|
||||||
elif socket == 'live':
|
elif socket == 'live':
|
||||||
self.xmpp.socket_class = TestLiveSocket
|
self.xmpp.socket_class = TestLiveSocket
|
||||||
|
|
||||||
def wait_for_session(x):
|
def wait_for_session(x):
|
||||||
self.xmpp.socket.clear()
|
self.xmpp.socket.clear()
|
||||||
skip_queue.put('started')
|
skip_queue.put('started')
|
||||||
|
|
||||||
self.xmpp.add_event_handler('session_start', wait_for_session)
|
self.xmpp.add_event_handler('session_start', wait_for_session)
|
||||||
self.xmpp.connect()
|
self.xmpp.connect()
|
||||||
else:
|
else:
|
||||||
|
|
3
sleekxmpp/thirdparty/__init__.py
vendored
3
sleekxmpp/thirdparty/__init__.py
vendored
|
@ -2,3 +2,6 @@ try:
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
except:
|
except:
|
||||||
from sleekxmpp.thirdparty.ordereddict import OrderedDict
|
from sleekxmpp.thirdparty.ordereddict import OrderedDict
|
||||||
|
|
||||||
|
from sleekxmpp.thirdparty import suelta
|
||||||
|
from sleekxmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso
|
||||||
|
|
267
sleekxmpp/thirdparty/mini_dateutil.py
vendored
Normal file
267
sleekxmpp/thirdparty/mini_dateutil.py
vendored
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
# This module is a very stripped down version of the dateutil
|
||||||
|
# package for when dateutil has not been installed. As a replacement
|
||||||
|
# for dateutil.parser.parse, the parsing methods from
|
||||||
|
# http://blog.mfabrik.com/2008/06/30/relativity-of-time-shortcomings-in-python-datetime-and-workaround/
|
||||||
|
|
||||||
|
#As such, the following copyrights and licenses applies:
|
||||||
|
|
||||||
|
|
||||||
|
# dateutil - Extensions to the standard python 2.3+ datetime module.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||||
|
#
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of the copyright holder nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "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 EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
# fixed_dateime
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008, Red Innovation Ltd., Finland
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of Red Innovation nor the names of its contributors
|
||||||
|
# may be used to endorse or promote products derived from this software
|
||||||
|
# without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``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 EVENT SHALL RED INNOVATION BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
ZERO = datetime.timedelta(0)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from dateutil.parser import parse as parse_iso
|
||||||
|
from dateutil.tz import tzoffset, tzutc
|
||||||
|
except:
|
||||||
|
# As a stopgap, define the two timezones here based
|
||||||
|
# on the dateutil code.
|
||||||
|
|
||||||
|
class tzutc(datetime.tzinfo):
|
||||||
|
|
||||||
|
def utcoffset(self, dt):
|
||||||
|
return ZERO
|
||||||
|
|
||||||
|
def dst(self, dt):
|
||||||
|
return ZERO
|
||||||
|
|
||||||
|
def tzname(self, dt):
|
||||||
|
return "UTC"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (isinstance(other, tzutc) or
|
||||||
|
(isinstance(other, tzoffset) and other._offset == ZERO))
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%s()" % self.__class__.__name__
|
||||||
|
|
||||||
|
__reduce__ = object.__reduce__
|
||||||
|
|
||||||
|
class tzoffset(datetime.tzinfo):
|
||||||
|
|
||||||
|
def __init__(self, name, offset):
|
||||||
|
self._name = name
|
||||||
|
self._offset = datetime.timedelta(seconds=offset)
|
||||||
|
|
||||||
|
def utcoffset(self, dt):
|
||||||
|
return self._offset
|
||||||
|
|
||||||
|
def dst(self, dt):
|
||||||
|
return ZERO
|
||||||
|
|
||||||
|
def tzname(self, dt):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (isinstance(other, tzoffset) and
|
||||||
|
self._offset == other._offset)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%s(%s, %s)" % (self.__class__.__name__,
|
||||||
|
repr(self._name),
|
||||||
|
self._offset.days*86400+self._offset.seconds)
|
||||||
|
|
||||||
|
__reduce__ = object.__reduce__
|
||||||
|
|
||||||
|
|
||||||
|
_fixed_offset_tzs = { }
|
||||||
|
UTC = tzutc()
|
||||||
|
|
||||||
|
def _get_fixed_offset_tz(offsetmins):
|
||||||
|
"""For internal use only: Returns a tzinfo with
|
||||||
|
the given fixed offset. This creates only one instance
|
||||||
|
for each offset; the zones are kept in a dictionary"""
|
||||||
|
|
||||||
|
if offsetmins == 0:
|
||||||
|
return UTC
|
||||||
|
|
||||||
|
if not offsetmins in _fixed_offset_tzs:
|
||||||
|
if offsetmins < 0:
|
||||||
|
sign = '-'
|
||||||
|
absoff = -offsetmins
|
||||||
|
else:
|
||||||
|
sign = '+'
|
||||||
|
absoff = offsetmins
|
||||||
|
|
||||||
|
name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60)
|
||||||
|
inst = tzoffset(offsetmins, name)
|
||||||
|
_fixed_offset_tzs[offsetmins] = inst
|
||||||
|
|
||||||
|
return _fixed_offset_tzs[offsetmins]
|
||||||
|
|
||||||
|
|
||||||
|
_iso8601_parser = re.compile("""
|
||||||
|
^
|
||||||
|
(?P<year> [0-9]{4})?(?P<ymdsep>-?)?
|
||||||
|
(?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})
|
||||||
|
(?:
|
||||||
|
# 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})
|
||||||
|
)?
|
||||||
|
)?
|
||||||
|
)?
|
||||||
|
$
|
||||||
|
""", re.X) # """
|
||||||
|
|
||||||
|
def parse_iso(timestamp):
|
||||||
|
"""Internal function for parsing a timestamp in
|
||||||
|
ISO 8601 format"""
|
||||||
|
|
||||||
|
timestamp = timestamp.strip()
|
||||||
|
|
||||||
|
m = _iso8601_parser.match(timestamp)
|
||||||
|
if not m:
|
||||||
|
raise ValueError("Not a proper ISO 8601 timestamp!: %s" % timestamp)
|
||||||
|
|
||||||
|
vals = m.groupdict()
|
||||||
|
def_vals = {'year': 1970, 'month': 1, 'day': 1}
|
||||||
|
for key in vals:
|
||||||
|
if vals[key] is None:
|
||||||
|
vals[key] = def_vals.get(key, 0)
|
||||||
|
elif key not in ['ymdsep', 'hmssep', 'tzempty']:
|
||||||
|
vals[key] = int(vals[key])
|
||||||
|
|
||||||
|
year = vals['year']
|
||||||
|
month = vals['month']
|
||||||
|
day = vals['day']
|
||||||
|
|
||||||
|
h, min, s, us = None, None, None, 0
|
||||||
|
frac = 0
|
||||||
|
if m.group('tzempty') == None and m.group('tzh') == None:
|
||||||
|
raise ValueError("Not a proper ISO 8601 timestamp: " +
|
||||||
|
"missing timezone (Z or +hh[:mm])!")
|
||||||
|
|
||||||
|
if m.group('frac'):
|
||||||
|
frac = m.group('frac')
|
||||||
|
power = len(frac)
|
||||||
|
frac = int(frac) / 10.0 ** power
|
||||||
|
|
||||||
|
if m.group('hour'):
|
||||||
|
h = vals['hour']
|
||||||
|
|
||||||
|
if m.group('minute'):
|
||||||
|
min = vals['minute']
|
||||||
|
|
||||||
|
if m.group('second'):
|
||||||
|
s = vals['second']
|
||||||
|
|
||||||
|
if frac != None:
|
||||||
|
# ok, fractions of hour?
|
||||||
|
if min == None:
|
||||||
|
frac, min = _math.modf(frac * 60.0)
|
||||||
|
min = int(min)
|
||||||
|
|
||||||
|
# fractions of second?
|
||||||
|
if s == None:
|
||||||
|
frac, s = _math.modf(frac * 60.0)
|
||||||
|
s = int(s)
|
||||||
|
|
||||||
|
# and extract microseconds...
|
||||||
|
us = int(frac * 1000000)
|
||||||
|
|
||||||
|
if m.group('tzempty') == 'Z':
|
||||||
|
offsetmins = 0
|
||||||
|
else:
|
||||||
|
# timezone: hour diff with sign
|
||||||
|
offsetmins = vals['tzh'] * 60
|
||||||
|
tzm = m.group('tzm')
|
||||||
|
|
||||||
|
# add optional minutes
|
||||||
|
if tzm != None:
|
||||||
|
tzm = int(tzm)
|
||||||
|
offsetmins += tzm if offsetmins > 0 else -tzm
|
||||||
|
|
||||||
|
tz = _get_fixed_offset_tz(offsetmins)
|
||||||
|
return datetime.datetime(year, month, day, h, min, s, us, tz)
|
21
sleekxmpp/thirdparty/suelta/LICENSE
vendored
Normal file
21
sleekxmpp/thirdparty/suelta/LICENSE
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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.
|
27
sleekxmpp/thirdparty/suelta/PLAYING-NICELY
vendored
Normal file
27
sleekxmpp/thirdparty/suelta/PLAYING-NICELY
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
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.
|
8
sleekxmpp/thirdparty/suelta/README
vendored
Normal file
8
sleekxmpp/thirdparty/suelta/README
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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.
|
||||||
|
|
26
sleekxmpp/thirdparty/suelta/__init__.py
vendored
Normal file
26
sleekxmpp/thirdparty/suelta/__init__.py
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# 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)
|
31
sleekxmpp/thirdparty/suelta/exceptions.py
vendored
Normal file
31
sleekxmpp/thirdparty/suelta/exceptions.py
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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)
|
5
sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
vendored
Normal file
5
sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
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
|
36
sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py
vendored
Normal file
36
sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
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)
|
63
sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py
vendored
Normal file
63
sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py
vendored
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
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)
|
273
sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
vendored
Normal file
273
sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
vendored
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from sleekxmpp.thirdparty.suelta.util import hash, bytes, quote
|
||||||
|
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)
|
61
sleekxmpp/thirdparty/suelta/mechanisms/plain.py
vendored
Normal file
61
sleekxmpp/thirdparty/suelta/mechanisms/plain.py
vendored
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
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', 1, PLAIN, use_hashes=False)
|
176
sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py
vendored
Normal file
176
sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py
vendored
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
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(self.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')
|
402
sleekxmpp/thirdparty/suelta/sasl.py
vendored
Normal file
402
sleekxmpp/thirdparty/suelta/sasl.py
vendored
Normal file
|
@ -0,0 +1,402 @@
|
||||||
|
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']
|
78
sleekxmpp/thirdparty/suelta/saslprep.py
vendored
Normal file
78
sleekxmpp/thirdparty/suelta/saslprep.py
vendored
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import stringprep
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
|
|
||||||
|
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('us-ascii')
|
||||||
|
|
||||||
|
# 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 UnicodeError('Section 6.3 [end]')
|
||||||
|
|
||||||
|
# Check for prohibited characters
|
||||||
|
for x in range(len(text)):
|
||||||
|
if strict and stringprep.in_table_a1(text[x]):
|
||||||
|
raise UnicodeError('Unassigned Codepoint')
|
||||||
|
if stringprep.in_table_c12(text[x]):
|
||||||
|
raise UnicodeError('In table C.1.2')
|
||||||
|
if stringprep.in_table_c21(text[x]):
|
||||||
|
raise UnicodeError('In table C.2.1')
|
||||||
|
if stringprep.in_table_c22(text[x]):
|
||||||
|
raise UnicodeError('In table C.2.2')
|
||||||
|
if stringprep.in_table_c3(text[x]):
|
||||||
|
raise UnicodeError('In table C.3')
|
||||||
|
if stringprep.in_table_c4(text[x]):
|
||||||
|
raise UnicodeError('In table C.4')
|
||||||
|
if stringprep.in_table_c5(text[x]):
|
||||||
|
raise UnicodeError('In table C.5')
|
||||||
|
if stringprep.in_table_c6(text[x]):
|
||||||
|
raise UnicodeError('In table C.6')
|
||||||
|
if stringprep.in_table_c7(text[x]):
|
||||||
|
raise UnicodeError('In table C.7')
|
||||||
|
if stringprep.in_table_c8(text[x]):
|
||||||
|
raise UnicodeError('In table C.8')
|
||||||
|
if stringprep.in_table_c9(text[x]):
|
||||||
|
raise UnicodeError('In table C.9')
|
||||||
|
if x:
|
||||||
|
if first_is_randal and stringprep.in_table_d2(text[x]):
|
||||||
|
raise UnicodeError('Section 6.2')
|
||||||
|
if not first_is_randal and \
|
||||||
|
x != len(text) - 1 and \
|
||||||
|
stringprep.in_table_d1(text[x]):
|
||||||
|
raise UnicodeError('Section 6.3')
|
||||||
|
|
||||||
|
return text
|
118
sleekxmpp/thirdparty/suelta/util.py
vendored
Normal file
118
sleekxmpp/thirdparty/suelta/util.py
vendored
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
|
||||||
|
def bytes(text):
|
||||||
|
"""
|
||||||
|
Convert Unicode text to UTF-8 encoded bytes.
|
||||||
|
|
||||||
|
Since Python 2.6+ and Python 3+ have similar but incompatible
|
||||||
|
signatures, this function unifies the two to keep code sane.
|
||||||
|
|
||||||
|
:param text: Unicode text to convert to bytes
|
||||||
|
:rtype: bytes (Python3), str (Python2.6+)
|
||||||
|
"""
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
import __builtin__
|
||||||
|
return __builtin__.bytes(text)
|
||||||
|
else:
|
||||||
|
import builtins
|
||||||
|
if isinstance(text, builtins.bytes):
|
||||||
|
# We already have bytes, so do nothing
|
||||||
|
return text
|
||||||
|
if isinstance(text, list):
|
||||||
|
# Convert a list of integers to bytes
|
||||||
|
return builtins.bytes(text)
|
||||||
|
else:
|
||||||
|
# Convert UTF-8 text to bytes
|
||||||
|
return builtins.bytes(text, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def quote(text):
|
||||||
|
"""
|
||||||
|
Enclose in quotes and escape internal slashes and double quotes.
|
||||||
|
|
||||||
|
:param text: A Unicode or byte string.
|
||||||
|
"""
|
||||||
|
text = bytes(text)
|
||||||
|
return b'"' + text.replace(b'\\', b'\\\\').replace(b'"', b'\\"') + b'"'
|
||||||
|
|
||||||
|
|
||||||
|
def num_to_bytes(num):
|
||||||
|
"""
|
||||||
|
Convert an integer into a four byte sequence.
|
||||||
|
|
||||||
|
:param integer num: An integer to convert to its byte representation.
|
||||||
|
"""
|
||||||
|
bval = b''
|
||||||
|
bval += bytes(chr(0xFF & (num >> 24)))
|
||||||
|
bval += bytes(chr(0xFF & (num >> 16)))
|
||||||
|
bval += bytes(chr(0xFF & (num >> 8)))
|
||||||
|
bval += bytes(chr(0xFF & (num >> 0)))
|
||||||
|
return bval
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_to_num(bval):
|
||||||
|
"""
|
||||||
|
Convert a four byte sequence to an integer.
|
||||||
|
|
||||||
|
:param bytes bval: A four byte sequence to turn into an integer.
|
||||||
|
"""
|
||||||
|
num = 0
|
||||||
|
num += ord(bval[0] << 24)
|
||||||
|
num += ord(bval[1] << 16)
|
||||||
|
num += ord(bval[2] << 8)
|
||||||
|
num += ord(bval[3])
|
||||||
|
return num
|
||||||
|
|
||||||
|
|
||||||
|
def XOR(x, y):
|
||||||
|
"""
|
||||||
|
Return the results of an XOR operation on two equal length byte strings.
|
||||||
|
|
||||||
|
:param bytes x: A byte string
|
||||||
|
:param bytes y: A byte string
|
||||||
|
:rtype: bytes
|
||||||
|
"""
|
||||||
|
result = b''
|
||||||
|
for a, b in zip(x, y):
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
result += chr((ord(a) ^ ord(b)))
|
||||||
|
else:
|
||||||
|
result += bytes([a ^ b])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def hash(name):
|
||||||
|
"""
|
||||||
|
Return a hash function implementing the given algorithm.
|
||||||
|
|
||||||
|
:param name: The name of the hashing algorithm to use.
|
||||||
|
:type name: string
|
||||||
|
|
||||||
|
:rtype: function
|
||||||
|
"""
|
||||||
|
name = name.lower()
|
||||||
|
if name.startswith('sha-'):
|
||||||
|
name = 'sha' + name[4:]
|
||||||
|
if name in dir(hashlib):
|
||||||
|
return getattr(hashlib, name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def hashes():
|
||||||
|
"""
|
||||||
|
Return a list of available hashing algorithms.
|
||||||
|
|
||||||
|
:rtype: list of strings
|
||||||
|
"""
|
||||||
|
t = []
|
||||||
|
if 'md5' in dir(hashlib):
|
||||||
|
t = ['MD5']
|
||||||
|
if 'md2' in dir(hashlib):
|
||||||
|
t += ['MD2']
|
||||||
|
hashes = ['SHA-' + h[3:] for h in dir(hashlib) if h.startswith('sha')]
|
||||||
|
return t + hashes
|
|
@ -482,7 +482,8 @@ class ElementBase(object):
|
||||||
if plugin:
|
if plugin:
|
||||||
if plugin not in self.plugins:
|
if plugin not in self.plugins:
|
||||||
self.init_plugin(plugin)
|
self.init_plugin(plugin)
|
||||||
handler = getattr(self.plugins[plugin], set_method, None)
|
handler = getattr(self.plugins[plugin],
|
||||||
|
set_method, None)
|
||||||
if handler:
|
if handler:
|
||||||
return handler(value)
|
return handler(value)
|
||||||
|
|
||||||
|
@ -1064,7 +1065,9 @@ class ElementBase(object):
|
||||||
Defaults to True.
|
Defaults to True.
|
||||||
"""
|
"""
|
||||||
stanza_ns = '' if top_level_ns else self.namespace
|
stanza_ns = '' if top_level_ns else self.namespace
|
||||||
return tostring(self.xml, xmlns='', stanza_ns=stanza_ns)
|
return tostring(self.xml, xmlns='',
|
||||||
|
stanza_ns=stanza_ns,
|
||||||
|
top_level=not top_level_ns)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1282,7 +1285,8 @@ class StanzaBase(ElementBase):
|
||||||
stanza_ns = '' if top_level_ns else self.namespace
|
stanza_ns = '' if top_level_ns else self.namespace
|
||||||
return tostring(self.xml, xmlns='',
|
return tostring(self.xml, xmlns='',
|
||||||
stanza_ns=stanza_ns,
|
stanza_ns=stanza_ns,
|
||||||
stream=self.stream)
|
stream=self.stream,
|
||||||
|
top_level=not top_level_ns)
|
||||||
|
|
||||||
|
|
||||||
# To comply with PEP8, method names now use underscores.
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
|
|
@ -6,8 +6,14 @@
|
||||||
See the file LICENSE for copying permission.
|
See the file LICENSE for copying permission.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
|
if sys.version_info < (3, 0):
|
||||||
|
import types
|
||||||
|
|
||||||
|
|
||||||
|
def tostring(xml=None, xmlns='', stanza_ns='', stream=None,
|
||||||
|
outbuffer='', top_level=False):
|
||||||
"""
|
"""
|
||||||
Serialize an XML object to a Unicode string.
|
Serialize an XML object to a Unicode string.
|
||||||
|
|
||||||
|
@ -26,6 +32,8 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
|
||||||
stream -- The XML stream that generated the XML object.
|
stream -- The XML stream that generated the XML object.
|
||||||
outbuffer -- Optional buffer for storing serializations during
|
outbuffer -- Optional buffer for storing serializations during
|
||||||
recursive calls.
|
recursive calls.
|
||||||
|
top_level -- Indicates that the element is the outermost
|
||||||
|
element.
|
||||||
"""
|
"""
|
||||||
# Add previous results to the start of the output.
|
# Add previous results to the start of the output.
|
||||||
output = [outbuffer]
|
output = [outbuffer]
|
||||||
|
@ -39,9 +47,16 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
|
||||||
else:
|
else:
|
||||||
tag_xmlns = ''
|
tag_xmlns = ''
|
||||||
|
|
||||||
|
default_ns = ''
|
||||||
|
stream_ns = ''
|
||||||
|
if stream:
|
||||||
|
default_ns = stream.default_ns
|
||||||
|
stream_ns = stream.stream_ns
|
||||||
|
|
||||||
# Output the tag name and derived namespace of the element.
|
# Output the tag name and derived namespace of the element.
|
||||||
namespace = ''
|
namespace = ''
|
||||||
if tag_xmlns not in ['', xmlns, stanza_ns]:
|
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
|
namespace = ' xmlns="%s"' % tag_xmlns
|
||||||
if stream and tag_xmlns in stream.namespace_map:
|
if stream and tag_xmlns in stream.namespace_map:
|
||||||
mapped_namespace = stream.namespace_map[tag_xmlns]
|
mapped_namespace = stream.namespace_map[tag_xmlns]
|
||||||
|
@ -93,6 +108,10 @@ def xml_escape(text):
|
||||||
Arguments:
|
Arguments:
|
||||||
text -- The XML text to convert.
|
text -- The XML text to convert.
|
||||||
"""
|
"""
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
if type(text) != types.UnicodeType:
|
||||||
|
text = unicode(text, 'utf-8', 'ignore')
|
||||||
|
|
||||||
text = list(text)
|
text = list(text)
|
||||||
escapes = {'&': '&',
|
escapes = {'&': '&',
|
||||||
'<': '<',
|
'<': '<',
|
|
@ -1,19 +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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Import the correct tostring and xml_escape functions based on the Python
|
|
||||||
# version in order to properly handle Unicode.
|
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
from sleekxmpp.xmlstream.tostring.tostring26 import tostring, xml_escape
|
|
||||||
else:
|
|
||||||
from sleekxmpp.xmlstream.tostring.tostring import tostring, xml_escape
|
|
||||||
|
|
||||||
__all__ = ['tostring', 'xml_escape']
|
|
|
@ -1,110 +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 unicode_literals
|
|
||||||
import types
|
|
||||||
|
|
||||||
|
|
||||||
def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
|
|
||||||
"""
|
|
||||||
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.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- The XML object to serialize. If the value is None,
|
|
||||||
then the XML object contained in this stanza
|
|
||||||
object will be used.
|
|
||||||
xmlns -- Optional namespace of an element wrapping the XML
|
|
||||||
object.
|
|
||||||
stanza_ns -- The namespace of the stanza object that contains
|
|
||||||
the XML object.
|
|
||||||
stream -- The XML stream that generated the XML object.
|
|
||||||
outbuffer -- Optional buffer for storing serializations during
|
|
||||||
recursive calls.
|
|
||||||
"""
|
|
||||||
# Add previous results to the start of the output.
|
|
||||||
output = [outbuffer]
|
|
||||||
|
|
||||||
# Extract the element's tag name.
|
|
||||||
tag_name = xml.tag.split('}', 1)[-1]
|
|
||||||
|
|
||||||
# Extract the element's namespace if it is defined.
|
|
||||||
if '}' in xml.tag:
|
|
||||||
tag_xmlns = xml.tag.split('}', 1)[0][1:]
|
|
||||||
else:
|
|
||||||
tag_xmlns = u''
|
|
||||||
|
|
||||||
# Output the tag name and derived namespace of the element.
|
|
||||||
namespace = u''
|
|
||||||
if tag_xmlns not in ['', xmlns, stanza_ns]:
|
|
||||||
namespace = u' xmlns="%s"' % tag_xmlns
|
|
||||||
if stream and tag_xmlns in stream.namespace_map:
|
|
||||||
mapped_namespace = stream.namespace_map[tag_xmlns]
|
|
||||||
if mapped_namespace:
|
|
||||||
tag_name = u"%s:%s" % (mapped_namespace, tag_name)
|
|
||||||
output.append(u"<%s" % tag_name)
|
|
||||||
output.append(namespace)
|
|
||||||
|
|
||||||
# Output escaped attribute values.
|
|
||||||
for attrib, value in xml.attrib.items():
|
|
||||||
value = xml_escape(value)
|
|
||||||
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:
|
|
||||||
mapped_ns = stream.namespace_map[attrib_ns]
|
|
||||||
if mapped_ns:
|
|
||||||
output.append(' %s:%s="%s"' % (mapped_ns,
|
|
||||||
attrib,
|
|
||||||
value))
|
|
||||||
|
|
||||||
if len(xml) or xml.text:
|
|
||||||
# If there are additional child elements to serialize.
|
|
||||||
output.append(u">")
|
|
||||||
if xml.text:
|
|
||||||
output.append(xml_escape(xml.text))
|
|
||||||
if len(xml):
|
|
||||||
for child in xml.getchildren():
|
|
||||||
output.append(tostring(child, tag_xmlns, stanza_ns, stream))
|
|
||||||
output.append(u"</%s>" % tag_name)
|
|
||||||
elif xml.text:
|
|
||||||
# If we only have text content.
|
|
||||||
output.append(u">%s</%s>" % (xml_escape(xml.text), tag_name))
|
|
||||||
else:
|
|
||||||
# Empty element.
|
|
||||||
output.append(u" />")
|
|
||||||
if xml.tail:
|
|
||||||
# If there is additional text after the element.
|
|
||||||
output.append(xml_escape(xml.tail))
|
|
||||||
return u''.join(output)
|
|
||||||
|
|
||||||
|
|
||||||
def xml_escape(text):
|
|
||||||
"""
|
|
||||||
Convert special characters in XML to escape sequences.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
text -- The XML text to convert.
|
|
||||||
"""
|
|
||||||
if type(text) != types.UnicodeType:
|
|
||||||
text = list(unicode(text, 'utf-8', 'ignore'))
|
|
||||||
else:
|
|
||||||
text = list(text)
|
|
||||||
escapes = {u'&': u'&',
|
|
||||||
u'<': u'<',
|
|
||||||
u'>': u'>',
|
|
||||||
u"'": u''',
|
|
||||||
u'"': u'"'}
|
|
||||||
for i, c in enumerate(text):
|
|
||||||
text[i] = escapes.get(c, c)
|
|
||||||
return u''.join(text)
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
from __future__ import with_statement, unicode_literals
|
from __future__ import with_statement, unicode_literals
|
||||||
|
|
||||||
|
import base64
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import signal
|
import signal
|
||||||
|
@ -23,6 +24,7 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import Queue as queue
|
import Queue as queue
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
from sleekxmpp.thirdparty.statemachine import StateMachine
|
from sleekxmpp.thirdparty.statemachine import StateMachine
|
||||||
from sleekxmpp.xmlstream import Scheduler, tostring
|
from sleekxmpp.xmlstream import Scheduler, tostring
|
||||||
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
|
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
|
||||||
|
@ -107,7 +109,13 @@ class XMLStream(object):
|
||||||
stream_header -- The closing tag of the stream's root element.
|
stream_header -- The closing tag of the stream's root element.
|
||||||
use_ssl -- Flag indicating if SSL should be used.
|
use_ssl -- Flag indicating if SSL should be used.
|
||||||
use_tls -- Flag indicating if TLS should be used.
|
use_tls -- Flag indicating if TLS should be used.
|
||||||
|
use_proxy -- Flag indicating that an HTTP Proxy should be used.
|
||||||
stop -- threading Event used to stop all threads.
|
stop -- threading Event used to stop all threads.
|
||||||
|
proxy_config -- An optional dictionary with the following entries:
|
||||||
|
host -- The host offering proxy services.
|
||||||
|
port -- The port for the proxy service.
|
||||||
|
username -- Optional username for the proxy.
|
||||||
|
password -- Optional password for the proxy.
|
||||||
|
|
||||||
auto_reconnect -- Flag to determine whether we auto reconnect.
|
auto_reconnect -- Flag to determine whether we auto reconnect.
|
||||||
reconnect_max_delay -- Maximum time to delay between connection
|
reconnect_max_delay -- Maximum time to delay between connection
|
||||||
|
@ -180,6 +188,9 @@ class XMLStream(object):
|
||||||
|
|
||||||
self.use_ssl = False
|
self.use_ssl = False
|
||||||
self.use_tls = False
|
self.use_tls = False
|
||||||
|
self.use_proxy = False
|
||||||
|
|
||||||
|
self.proxy_config = {}
|
||||||
|
|
||||||
self.default_ns = ''
|
self.default_ns = ''
|
||||||
self.stream_header = "<stream>"
|
self.stream_header = "<stream>"
|
||||||
|
@ -322,6 +333,12 @@ class XMLStream(object):
|
||||||
log.debug('Waiting %s seconds before connecting.' % delay)
|
log.debug('Waiting %s seconds before connecting.' % delay)
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
|
if self.use_proxy:
|
||||||
|
connected = self._connect_proxy()
|
||||||
|
if not connected:
|
||||||
|
self.reconnect_delay = delay
|
||||||
|
return False
|
||||||
|
|
||||||
if self.use_ssl and self.ssl_support:
|
if self.use_ssl and self.ssl_support:
|
||||||
log.debug("Socket Wrapped for SSL")
|
log.debug("Socket Wrapped for SSL")
|
||||||
if self.ca_certs is None:
|
if self.ca_certs is None:
|
||||||
|
@ -341,8 +358,10 @@ class XMLStream(object):
|
||||||
self.socket = ssl_socket
|
self.socket = ssl_socket
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not self.use_proxy:
|
||||||
log.debug("Connecting to %s:%s" % self.address)
|
log.debug("Connecting to %s:%s" % self.address)
|
||||||
self.socket.connect(self.address)
|
self.socket.connect(self.address)
|
||||||
|
|
||||||
self.set_socket(self.socket, ignore=True)
|
self.set_socket(self.socket, ignore=True)
|
||||||
#this event is where you should set your application state
|
#this event is where you should set your application state
|
||||||
self.event("connected", direct=True)
|
self.event("connected", direct=True)
|
||||||
|
@ -356,22 +375,86 @@ class XMLStream(object):
|
||||||
self.reconnect_delay = delay
|
self.reconnect_delay = delay
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def disconnect(self, reconnect=False):
|
def _connect_proxy(self):
|
||||||
|
"""Attempt to connect using an HTTP Proxy."""
|
||||||
|
|
||||||
|
# Extract the proxy address, and optional credentials
|
||||||
|
address = (self.proxy_config['host'], int(self.proxy_config['port']))
|
||||||
|
cred = None
|
||||||
|
if self.proxy_config['username']:
|
||||||
|
username = self.proxy_config['username']
|
||||||
|
password = self.proxy_config['password']
|
||||||
|
|
||||||
|
cred = '%s:%s' % (username, password)
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
cred = bytes(cred)
|
||||||
|
else:
|
||||||
|
cred = bytes(cred, 'utf-8')
|
||||||
|
cred = base64.b64encode(cred).decode('utf-8')
|
||||||
|
|
||||||
|
# Build the HTTP headers for connecting to the XMPP server
|
||||||
|
headers = ['CONNECT %s:%s HTTP/1.0' % self.address,
|
||||||
|
'Host: %s:%s' % self.address,
|
||||||
|
'Proxy-Connection: Keep-Alive',
|
||||||
|
'Pragma: no-cache',
|
||||||
|
'User-Agent: SleekXMPP/%s' % sleekxmpp.__version__]
|
||||||
|
if cred:
|
||||||
|
headers.append('Proxy-Authorization: Basic %s' % cred)
|
||||||
|
headers = '\r\n'.join(headers) + '\r\n\r\n'
|
||||||
|
|
||||||
|
try:
|
||||||
|
log.debug("Connecting to proxy: %s:%s" % address)
|
||||||
|
self.socket.connect(address)
|
||||||
|
self.send_raw(headers, now=True)
|
||||||
|
resp = ''
|
||||||
|
while '\r\n\r\n' not in resp:
|
||||||
|
resp += self.socket.recv(1024).decode('utf-8')
|
||||||
|
log.debug('RECV: %s' % resp)
|
||||||
|
|
||||||
|
lines = resp.split('\r\n')
|
||||||
|
if '200' not in lines[0]:
|
||||||
|
self.event('proxy_error', resp)
|
||||||
|
log.error('Proxy Error: %s' % lines[0])
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Proxy connection established, continue connecting
|
||||||
|
# with the XMPP server.
|
||||||
|
return True
|
||||||
|
except Socket.error as serr:
|
||||||
|
error_msg = "Could not connect to %s:%s. Socket Error #%s: %s"
|
||||||
|
self.event('socket_error', serr)
|
||||||
|
log.error(error_msg % (self.address[0], self.address[1],
|
||||||
|
serr.errno, serr.strerror))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disconnect(self, reconnect=False, wait=False):
|
||||||
"""
|
"""
|
||||||
Terminate processing and close the XML streams.
|
Terminate processing and close the XML streams.
|
||||||
|
|
||||||
Optionally, the connection may be reconnected and
|
Optionally, the connection may be reconnected and
|
||||||
resume processing afterwards.
|
resume processing afterwards.
|
||||||
|
|
||||||
|
If the disconnect should take place after all items
|
||||||
|
in the send queue have been sent, use wait=True. However,
|
||||||
|
take note: If you are constantly adding items to the queue
|
||||||
|
such that it is never empty, then the disconnect will
|
||||||
|
not occur and the call will continue to block.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
reconnect -- Flag indicating if the connection
|
reconnect -- Flag indicating if the connection
|
||||||
and processing should be restarted.
|
and processing should be restarted.
|
||||||
Defaults to False.
|
Defaults to False.
|
||||||
|
wait -- Flag indicating if the send queue should
|
||||||
|
be emptied before disconnecting.
|
||||||
"""
|
"""
|
||||||
self.state.transition('connected', 'disconnected', wait=0.0,
|
self.state.transition('connected', 'disconnected', wait=0.0,
|
||||||
func=self._disconnect, args=(reconnect,))
|
func=self._disconnect, args=(reconnect, wait))
|
||||||
|
|
||||||
|
def _disconnect(self, reconnect=False, wait=False):
|
||||||
|
# Wait for the send queue to empty.
|
||||||
|
if wait:
|
||||||
|
self.send_queue.join()
|
||||||
|
|
||||||
def _disconnect(self, reconnect=False):
|
|
||||||
# Send the end of stream marker.
|
# Send the end of stream marker.
|
||||||
self.send_raw(self.stream_footer, now=True)
|
self.send_raw(self.stream_footer, now=True)
|
||||||
self.session_started_event.clear()
|
self.session_started_event.clear()
|
||||||
|
@ -748,7 +831,7 @@ class XMLStream(object):
|
||||||
self.send_queue.put(data)
|
self.send_queue.put(data)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def process(self, threaded=True):
|
def process(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize the XML streams and begin processing events.
|
Initialize the XML streams and begin processing events.
|
||||||
|
|
||||||
|
@ -756,15 +839,29 @@ class XMLStream(object):
|
||||||
by HANDLER_THREADS.
|
by HANDLER_THREADS.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
|
block -- If block=False then event dispatcher will run
|
||||||
|
in a separate thread, allowing for the stream to be
|
||||||
|
used in the background for another application.
|
||||||
|
Otherwise, process(block=True) blocks the current thread.
|
||||||
|
Defaults to False.
|
||||||
|
|
||||||
|
**threaded is deprecated and included for API compatibility**
|
||||||
threaded -- If threaded=True then event dispatcher will run
|
threaded -- If threaded=True then event dispatcher will run
|
||||||
in a separate thread, allowing for the stream to be
|
in a separate thread, allowing for the stream to be
|
||||||
used in the background for another application.
|
used in the background for another application.
|
||||||
Defaults to True.
|
Defaults to True.
|
||||||
|
|
||||||
Event handlers and the send queue will be threaded
|
Event handlers and the send queue will be threaded
|
||||||
regardless of this parameter's value.
|
regardless of these parameters.
|
||||||
"""
|
"""
|
||||||
self._thread_excepthook()
|
if 'threaded' in kwargs and 'block' in kwargs:
|
||||||
|
raise ValueError("process() called with both " + \
|
||||||
|
"block and threaded arguments")
|
||||||
|
elif 'block' in kwargs:
|
||||||
|
threaded = not(kwargs.get('block', False))
|
||||||
|
else:
|
||||||
|
threaded = kwargs.get('threaded', True)
|
||||||
|
|
||||||
self.scheduler.process(threaded=True)
|
self.scheduler.process(threaded=True)
|
||||||
|
|
||||||
def start_thread(name, target):
|
def start_thread(name, target):
|
||||||
|
@ -944,13 +1041,14 @@ class XMLStream(object):
|
||||||
func -- The event handler to execute.
|
func -- The event handler to execute.
|
||||||
args -- Arguments to the event handler.
|
args -- Arguments to the event handler.
|
||||||
"""
|
"""
|
||||||
|
orig = copy.copy(args[0])
|
||||||
try:
|
try:
|
||||||
func(*args)
|
func(*args)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = 'Error processing event handler: %s'
|
error_msg = 'Error processing event handler: %s'
|
||||||
log.exception(error_msg % str(func))
|
log.exception(error_msg % str(func))
|
||||||
if hasattr(args[0], 'exception'):
|
if hasattr(orig, 'exception'):
|
||||||
args[0].exception(e)
|
orig.exception(e)
|
||||||
|
|
||||||
def _event_runner(self):
|
def _event_runner(self):
|
||||||
"""
|
"""
|
||||||
|
@ -973,6 +1071,7 @@ class XMLStream(object):
|
||||||
|
|
||||||
etype, handler = event[0:2]
|
etype, handler = event[0:2]
|
||||||
args = event[2:]
|
args = event[2:]
|
||||||
|
orig = copy.copy(args[0])
|
||||||
|
|
||||||
if etype == 'stanza':
|
if etype == 'stanza':
|
||||||
try:
|
try:
|
||||||
|
@ -980,7 +1079,7 @@ class XMLStream(object):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = 'Error processing stream handler: %s'
|
error_msg = 'Error processing stream handler: %s'
|
||||||
log.exception(error_msg % handler.name)
|
log.exception(error_msg % handler.name)
|
||||||
args[0].exception(e)
|
orig.exception(e)
|
||||||
elif etype == 'schedule':
|
elif etype == 'schedule':
|
||||||
try:
|
try:
|
||||||
log.debug('Scheduled event: %s' % args)
|
log.debug('Scheduled event: %s' % args)
|
||||||
|
@ -989,6 +1088,7 @@ class XMLStream(object):
|
||||||
log.exception('Error processing scheduled task')
|
log.exception('Error processing scheduled task')
|
||||||
elif etype == 'event':
|
elif etype == 'event':
|
||||||
func, threaded, disposable = handler
|
func, threaded, disposable = handler
|
||||||
|
orig = copy.copy(args[0])
|
||||||
try:
|
try:
|
||||||
if threaded:
|
if threaded:
|
||||||
x = threading.Thread(
|
x = threading.Thread(
|
||||||
|
@ -1001,8 +1101,8 @@ class XMLStream(object):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = 'Error processing event handler: %s'
|
error_msg = 'Error processing event handler: %s'
|
||||||
log.exception(error_msg % str(func))
|
log.exception(error_msg % str(func))
|
||||||
if hasattr(args[0], 'exception'):
|
if hasattr(orig, 'exception'):
|
||||||
args[0].exception(e)
|
orig.exception(e)
|
||||||
elif etype == 'quit':
|
elif etype == 'quit':
|
||||||
log.debug("Quitting event runner thread")
|
log.debug("Quitting event runner thread")
|
||||||
return False
|
return False
|
||||||
|
@ -1034,6 +1134,7 @@ class XMLStream(object):
|
||||||
log.debug("SEND: %s" % data)
|
log.debug("SEND: %s" % data)
|
||||||
try:
|
try:
|
||||||
self.socket.send(data.encode('utf-8'))
|
self.socket.send(data.encode('utf-8'))
|
||||||
|
self.send_queue.task_done()
|
||||||
except Socket.error as serr:
|
except Socket.error as serr:
|
||||||
self.event('socket_error', serr)
|
self.event('socket_error', serr)
|
||||||
log.warning("Failed to send %s" % data)
|
log.warning("Failed to send %s" % data)
|
||||||
|
@ -1049,30 +1150,16 @@ class XMLStream(object):
|
||||||
self.event_queue.put(('quit', None, None))
|
self.event_queue.put(('quit', None, None))
|
||||||
return
|
return
|
||||||
|
|
||||||
def _thread_excepthook(self):
|
def exception(self, exception):
|
||||||
"""
|
"""
|
||||||
If a threaded event handler raises an exception, there is no way to
|
Process an unknown exception.
|
||||||
catch it except with an excepthook. Currently, each thread has its own
|
|
||||||
excepthook, but ideally we could use the main sys.excepthook.
|
|
||||||
|
|
||||||
Modifies threading.Thread to use sys.excepthook when an exception
|
Meant to be overridden.
|
||||||
is not caught.
|
|
||||||
|
Arguments:
|
||||||
|
exception -- An unhandled exception object.
|
||||||
"""
|
"""
|
||||||
init_old = threading.Thread.__init__
|
pass
|
||||||
|
|
||||||
def init(self, *args, **kwargs):
|
|
||||||
init_old(self, *args, **kwargs)
|
|
||||||
run_old = self.run
|
|
||||||
|
|
||||||
def run_with_except_hook(*args, **kw):
|
|
||||||
try:
|
|
||||||
run_old(*args, **kw)
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
|
||||||
raise
|
|
||||||
except:
|
|
||||||
sys.excepthook(*sys.exc_info())
|
|
||||||
self.run = run_with_except_hook
|
|
||||||
threading.Thread.__init__ = init
|
|
||||||
|
|
||||||
|
|
||||||
# To comply with PEP8, method names now use underscores.
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from sleekxmpp.test import *
|
from sleekxmpp.test import *
|
||||||
import sleekxmpp.plugins.xep_0004 as xep_0004
|
import sleekxmpp.plugins.xep_0004 as xep_0004
|
||||||
import sleekxmpp.plugins.stanza_pubsub as pubsub
|
import sleekxmpp.plugins.xep_0060.stanza as pubsub
|
||||||
|
|
||||||
|
|
||||||
class TestPubsubStanzas(SleekTest):
|
class TestPubsubStanzas(SleekTest):
|
||||||
|
|
|
@ -12,9 +12,78 @@ class TestStreamExceptions(SleekTest):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
sys.excepthook = sys.__excepthook__
|
|
||||||
self.stream_close()
|
self.stream_close()
|
||||||
|
|
||||||
|
def testExceptionReply(self):
|
||||||
|
"""Test that raising an exception replies with the original stanza."""
|
||||||
|
|
||||||
|
def message(msg):
|
||||||
|
msg.reply()
|
||||||
|
msg['body'] = 'Body changed'
|
||||||
|
raise XMPPError(clear=False)
|
||||||
|
|
||||||
|
self.stream_start()
|
||||||
|
self.xmpp.add_event_handler('message', message)
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<message>
|
||||||
|
<body>This is going to cause an error.</body>
|
||||||
|
</message>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<message type="error">
|
||||||
|
<body>This is going to cause an error.</body>
|
||||||
|
<error type="cancel" code="500">
|
||||||
|
<undefined-condition
|
||||||
|
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||||
|
</error>
|
||||||
|
</message>
|
||||||
|
""")
|
||||||
|
|
||||||
|
def testExceptionContinueWorking(self):
|
||||||
|
"""Test that Sleek continues to respond after an XMPPError is raised."""
|
||||||
|
|
||||||
|
def message(msg):
|
||||||
|
msg.reply()
|
||||||
|
msg['body'] = 'Body changed'
|
||||||
|
raise XMPPError(clear=False)
|
||||||
|
|
||||||
|
self.stream_start()
|
||||||
|
self.xmpp.add_event_handler('message', message)
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<message>
|
||||||
|
<body>This is going to cause an error.</body>
|
||||||
|
</message>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<message type="error">
|
||||||
|
<body>This is going to cause an error.</body>
|
||||||
|
<error type="cancel" code="500">
|
||||||
|
<undefined-condition
|
||||||
|
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||||
|
</error>
|
||||||
|
</message>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<message>
|
||||||
|
<body>This is going to cause an error.</body>
|
||||||
|
</message>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<message type="error">
|
||||||
|
<body>This is going to cause an error.</body>
|
||||||
|
<error type="cancel" code="500">
|
||||||
|
<undefined-condition
|
||||||
|
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||||
|
</error>
|
||||||
|
</message>
|
||||||
|
""")
|
||||||
|
|
||||||
def testXMPPErrorException(self):
|
def testXMPPErrorException(self):
|
||||||
"""Test raising an XMPPError exception."""
|
"""Test raising an XMPPError exception."""
|
||||||
|
|
||||||
|
@ -124,9 +193,8 @@ class TestStreamExceptions(SleekTest):
|
||||||
def catch_error(*args, **kwargs):
|
def catch_error(*args, **kwargs):
|
||||||
raised_errors.append(True)
|
raised_errors.append(True)
|
||||||
|
|
||||||
sys.excepthook = catch_error
|
|
||||||
|
|
||||||
self.stream_start()
|
self.stream_start()
|
||||||
|
self.xmpp.exception = catch_error
|
||||||
self.xmpp.add_event_handler('message', message)
|
self.xmpp.add_event_handler('message', message)
|
||||||
|
|
||||||
self.recv("""
|
self.recv("""
|
||||||
|
@ -149,6 +217,58 @@ class TestStreamExceptions(SleekTest):
|
||||||
|
|
||||||
self.assertEqual(raised_errors, [True], "Exception was not raised: %s" % raised_errors)
|
self.assertEqual(raised_errors, [True], "Exception was not raised: %s" % raised_errors)
|
||||||
|
|
||||||
|
def testUnknownException(self):
|
||||||
|
"""Test Sleek continues to respond after an unknown exception."""
|
||||||
|
|
||||||
|
raised_errors = []
|
||||||
|
|
||||||
|
def message(msg):
|
||||||
|
raise ValueError("Did something wrong")
|
||||||
|
|
||||||
|
def catch_error(*args, **kwargs):
|
||||||
|
raised_errors.append(True)
|
||||||
|
|
||||||
|
self.stream_start()
|
||||||
|
self.xmpp.exception = catch_error
|
||||||
|
self.xmpp.add_event_handler('message', message)
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<message>
|
||||||
|
<body>This is going to cause an error.</body>
|
||||||
|
</message>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<message type="error">
|
||||||
|
<error type="cancel" code="500">
|
||||||
|
<undefined-condition
|
||||||
|
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||||
|
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
|
||||||
|
SleekXMPP got into trouble.
|
||||||
|
</text>
|
||||||
|
</error>
|
||||||
|
</message>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<message>
|
||||||
|
<body>This is going to cause an error.</body>
|
||||||
|
</message>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<message type="error">
|
||||||
|
<error type="cancel" code="500">
|
||||||
|
<undefined-condition
|
||||||
|
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||||
|
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
|
||||||
|
SleekXMPP got into trouble.
|
||||||
|
</text>
|
||||||
|
</error>
|
||||||
|
</message>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.assertEqual(raised_errors, [True, True], "Exceptions were not raised: %s" % raised_errors)
|
||||||
|
|
||||||
|
|
||||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExceptions)
|
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExceptions)
|
||||||
|
|
|
@ -184,5 +184,56 @@ class TestStreamPresence(SleekTest):
|
||||||
self.assertEqual(events, expected,
|
self.assertEqual(events, expected,
|
||||||
"Incorrect events triggered: %s" % events)
|
"Incorrect events triggered: %s" % events)
|
||||||
|
|
||||||
|
def test_presence_events(self):
|
||||||
|
"""Test that presence events are raised."""
|
||||||
|
|
||||||
|
events = []
|
||||||
|
|
||||||
|
self.stream_start()
|
||||||
|
|
||||||
|
ptypes = ['available', 'away', 'dnd', 'xa', 'chat',
|
||||||
|
'unavailable', 'subscribe', 'subscribed',
|
||||||
|
'unsubscribe', 'unsubscribed']
|
||||||
|
|
||||||
|
for ptype in ptypes:
|
||||||
|
handler = lambda p: events.append(p['type'])
|
||||||
|
self.xmpp.add_event_handler('presence_%s' % ptype, handler)
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<presence />
|
||||||
|
""")
|
||||||
|
self.recv("""
|
||||||
|
<presence><show>away</show></presence>
|
||||||
|
""")
|
||||||
|
self.recv("""
|
||||||
|
<presence><show>dnd</show></presence>
|
||||||
|
""")
|
||||||
|
self.recv("""
|
||||||
|
<presence><show>xa</show></presence>
|
||||||
|
""")
|
||||||
|
self.recv("""
|
||||||
|
<presence><show>chat</show></presence>
|
||||||
|
""")
|
||||||
|
self.recv("""
|
||||||
|
<presence type="unavailable" />
|
||||||
|
""")
|
||||||
|
self.recv("""
|
||||||
|
<presence type="subscribe" />
|
||||||
|
""")
|
||||||
|
self.recv("""
|
||||||
|
<presence type="subscribed" />
|
||||||
|
""")
|
||||||
|
self.recv("""
|
||||||
|
<presence type="unsubscribe" />
|
||||||
|
""")
|
||||||
|
self.recv("""
|
||||||
|
<presence type="unsubscribed" />
|
||||||
|
""")
|
||||||
|
|
||||||
|
time.sleep(.5)
|
||||||
|
|
||||||
|
self.assertEqual(events, ptypes,
|
||||||
|
"Not all events raised: %s" % events)
|
||||||
|
|
||||||
|
|
||||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPresence)
|
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPresence)
|
||||||
|
|
|
@ -12,7 +12,6 @@ class TestStreamDisco(SleekTest):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
sys.excepthook = sys.__excepthook__
|
|
||||||
self.stream_close()
|
self.stream_close()
|
||||||
|
|
||||||
def testInfoEmptyDefaultNode(self):
|
def testInfoEmptyDefaultNode(self):
|
||||||
|
@ -531,11 +530,6 @@ class TestStreamDisco(SleekTest):
|
||||||
|
|
||||||
raised_exceptions = []
|
raised_exceptions = []
|
||||||
|
|
||||||
def catch_exception(*args, **kwargs):
|
|
||||||
raised_exceptions.append(True)
|
|
||||||
|
|
||||||
sys.excepthook = catch_exception
|
|
||||||
|
|
||||||
self.stream_start(mode='client',
|
self.stream_start(mode='client',
|
||||||
plugins=['xep_0030', 'xep_0059'])
|
plugins=['xep_0030', 'xep_0059'])
|
||||||
|
|
||||||
|
@ -544,8 +538,14 @@ class TestStreamDisco(SleekTest):
|
||||||
iterator=True)
|
iterator=True)
|
||||||
results.amount = 10
|
results.amount = 10
|
||||||
|
|
||||||
|
def run_test():
|
||||||
|
try:
|
||||||
|
results.next()
|
||||||
|
except StopIteration:
|
||||||
|
raised_exceptions.append(True)
|
||||||
|
|
||||||
t = threading.Thread(name="get_items_iterator",
|
t = threading.Thread(name="get_items_iterator",
|
||||||
target=results.next)
|
target=run_test)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
self.send("""
|
self.send("""
|
||||||
|
|
44
tests/test_stream_xep_0066.py
Normal file
44
tests/test_stream_xep_0066.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from sleekxmpp.test import *
|
||||||
|
|
||||||
|
|
||||||
|
class TestOOB(SleekTest):
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.stream_close()
|
||||||
|
|
||||||
|
def testSendOOB(self):
|
||||||
|
"""Test sending an OOB transfer request."""
|
||||||
|
self.stream_start(plugins=['xep_0066', 'xep_0030'])
|
||||||
|
|
||||||
|
url = 'http://github.com/fritzy/SleekXMPP/blob/master/README'
|
||||||
|
|
||||||
|
t = threading.Thread(
|
||||||
|
name='send_oob',
|
||||||
|
target=self.xmpp['xep_0066'].send_oob,
|
||||||
|
args=('user@example.com', url),
|
||||||
|
kwargs={'desc': 'SleekXMPP README'})
|
||||||
|
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq to="user@example.com" type="set" id="1">
|
||||||
|
<query xmlns="jabber:iq:oob">
|
||||||
|
<url>http://github.com/fritzy/SleekXMPP/blob/master/README</url>
|
||||||
|
<desc>SleekXMPP README</desc>
|
||||||
|
</query>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="1" type="result"
|
||||||
|
to="tester@localhost"
|
||||||
|
from="user@example.com" />
|
||||||
|
""")
|
||||||
|
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
|
||||||
|
suite = unittest.TestLoader().loadTestsFromTestCase(TestOOB)
|
|
@ -13,7 +13,6 @@ class TestStreamExtendedDisco(SleekTest):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
sys.excepthook = sys.__excepthook__
|
|
||||||
self.stream_close()
|
self.stream_close()
|
||||||
|
|
||||||
def testUsingExtendedInfo(self):
|
def testUsingExtendedInfo(self):
|
||||||
|
|
|
@ -13,7 +13,6 @@ class TestStreamDirectInvite(SleekTest):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
sys.excepthook = sys.__excepthook__
|
|
||||||
self.stream_close()
|
self.stream_close()
|
||||||
|
|
||||||
def testReceiveInvite(self):
|
def testReceiveInvite(self):
|
||||||
|
|
|
@ -102,11 +102,13 @@ class TestToString(SleekTest):
|
||||||
"""
|
"""
|
||||||
Test that stanza objects are serialized properly.
|
Test that stanza objects are serialized properly.
|
||||||
"""
|
"""
|
||||||
|
self.stream_start()
|
||||||
|
|
||||||
utf8_message = '\xe0\xb2\xa0_\xe0\xb2\xa0'
|
utf8_message = '\xe0\xb2\xa0_\xe0\xb2\xa0'
|
||||||
if not hasattr(utf8_message, 'decode'):
|
if not hasattr(utf8_message, 'decode'):
|
||||||
# Python 3
|
# Python 3
|
||||||
utf8_message = bytes(utf8_message, encoding='utf-8')
|
utf8_message = bytes(utf8_message, encoding='utf-8')
|
||||||
msg = Message()
|
msg = self.Message()
|
||||||
msg['body'] = utf8_message.decode('utf-8')
|
msg['body'] = utf8_message.decode('utf-8')
|
||||||
expected = '<message><body>\xe0\xb2\xa0_\xe0\xb2\xa0</body></message>'
|
expected = '<message><body>\xe0\xb2\xa0_\xe0\xb2\xa0</body></message>'
|
||||||
result = msg.__str__()
|
result = msg.__str__()
|
||||||
|
|
10
todo1.0
10
todo1.0
|
@ -18,8 +18,6 @@ Plugins:
|
||||||
PEP8
|
PEP8
|
||||||
Documentation
|
Documentation
|
||||||
Stream/Unit tests
|
Stream/Unit tests
|
||||||
0050
|
|
||||||
Review replacement in github.com/legastero/adhoc
|
|
||||||
0060
|
0060
|
||||||
PEP8
|
PEP8
|
||||||
Documentation
|
Documentation
|
||||||
|
@ -29,14 +27,6 @@ Plugins:
|
||||||
PEP8
|
PEP8
|
||||||
Documentation
|
Documentation
|
||||||
Stream/Unit tests
|
Stream/Unit tests
|
||||||
0086
|
|
||||||
PEP8
|
|
||||||
Documentation
|
|
||||||
Consider any simplifications.
|
|
||||||
0202
|
|
||||||
PEP8
|
|
||||||
Documentation
|
|
||||||
Stream/Unit tests
|
|
||||||
gmail_notify
|
gmail_notify
|
||||||
PEP8
|
PEP8
|
||||||
Documentation
|
Documentation
|
||||||
|
|
Loading…
Reference in a new issue