mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-11-27 19:19:54 +00:00
Merge branch 'stream_features' into develop
This commit is contained in:
commit
9591cd3a7e
46 changed files with 2256 additions and 231 deletions
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.
|
||||||
|
|
10
setup.py
10
setup.py
|
@ -45,7 +45,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',
|
||||||
|
@ -59,6 +58,15 @@ packages = [ 'sleekxmpp',
|
||||||
'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/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):
|
if sys.version_info < (3, 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,
|
||||||
|
@ -162,9 +165,14 @@ 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__(str("%s.%s" % (module.__name__, plugin)),
|
||||||
globals(), locals(), [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().
|
||||||
|
@ -173,12 +181,14 @@ class BaseXMPP(XMLStream):
|
||||||
# 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)
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
||||||
|
@ -81,15 +83,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 +108,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):
|
||||||
"""
|
"""
|
||||||
|
@ -194,19 +179,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):
|
||||||
|
@ -278,6 +266,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.
|
||||||
|
@ -285,173 +288,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.
|
||||||
|
|
11
sleekxmpp/features/__init__.py
Normal file
11
sleekxmpp/features/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
"""
|
||||||
|
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', 'feature_session',
|
||||||
|
'sasl_plain', 'sasl_anonymous']
|
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
|
127
sleekxmpp/features/feature_mechanisms/mechanisms.py
Normal file
127
sleekxmpp/features/feature_mechanisms/mechanisms.py
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
52
sleekxmpp/stanza/stream_features.py
Normal file
52
sleekxmpp/stanza/stream_features.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
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']]
|
2
sleekxmpp/thirdparty/__init__.py
vendored
2
sleekxmpp/thirdparty/__init__.py
vendored
|
@ -2,3 +2,5 @@ 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
|
||||||
|
|
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__(self, 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 == 'anonymous':
|
||||||
|
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 != 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
|
|
@ -1064,7 +1064,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 +1284,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.
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
|
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 +27,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 +42,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]
|
||||||
|
|
|
@ -10,7 +10,8 @@ from __future__ import unicode_literals
|
||||||
import types
|
import types
|
||||||
|
|
||||||
|
|
||||||
def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
|
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.
|
||||||
|
|
||||||
|
@ -29,6 +30,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]
|
||||||
|
@ -42,9 +45,16 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
|
||||||
else:
|
else:
|
||||||
tag_xmlns = u''
|
tag_xmlns = u''
|
||||||
|
|
||||||
|
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 = u''
|
namespace = u''
|
||||||
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 = u' xmlns="%s"' % tag_xmlns
|
namespace = u' 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]
|
||||||
|
|
|
@ -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__()
|
||||||
|
|
Loading…
Reference in a new issue