Merge branch 'master' into develop
Conflicts: sleekxmpp/thirdparty/__init__.py
This commit is contained in:
commit
06a9d9fc30
5
setup.py
5
setup.py
@ -50,6 +50,7 @@ packages = [ 'sleekxmpp',
|
|||||||
'sleekxmpp/test',
|
'sleekxmpp/test',
|
||||||
'sleekxmpp/roster',
|
'sleekxmpp/roster',
|
||||||
'sleekxmpp/util',
|
'sleekxmpp/util',
|
||||||
|
'sleekxmpp/util/sasl',
|
||||||
'sleekxmpp/xmlstream',
|
'sleekxmpp/xmlstream',
|
||||||
'sleekxmpp/xmlstream/matcher',
|
'sleekxmpp/xmlstream/matcher',
|
||||||
'sleekxmpp/xmlstream/handler',
|
'sleekxmpp/xmlstream/handler',
|
||||||
@ -59,6 +60,7 @@ packages = [ 'sleekxmpp',
|
|||||||
'sleekxmpp/plugins/xep_0009',
|
'sleekxmpp/plugins/xep_0009',
|
||||||
'sleekxmpp/plugins/xep_0009/stanza',
|
'sleekxmpp/plugins/xep_0009/stanza',
|
||||||
'sleekxmpp/plugins/xep_0012',
|
'sleekxmpp/plugins/xep_0012',
|
||||||
|
'sleekxmpp/plugins/xep_0016',
|
||||||
'sleekxmpp/plugins/xep_0027',
|
'sleekxmpp/plugins/xep_0027',
|
||||||
'sleekxmpp/plugins/xep_0030',
|
'sleekxmpp/plugins/xep_0030',
|
||||||
'sleekxmpp/plugins/xep_0030/stanza',
|
'sleekxmpp/plugins/xep_0030/stanza',
|
||||||
@ -105,9 +107,8 @@ packages = [ 'sleekxmpp',
|
|||||||
'sleekxmpp/features/feature_bind',
|
'sleekxmpp/features/feature_bind',
|
||||||
'sleekxmpp/features/feature_session',
|
'sleekxmpp/features/feature_session',
|
||||||
'sleekxmpp/features/feature_rosterver',
|
'sleekxmpp/features/feature_rosterver',
|
||||||
|
'sleekxmpp/features/feature_preapproval',
|
||||||
'sleekxmpp/thirdparty',
|
'sleekxmpp/thirdparty',
|
||||||
'sleekxmpp/thirdparty/suelta',
|
|
||||||
'sleekxmpp/thirdparty/suelta/mechanisms',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
@ -113,9 +113,10 @@ class ClientXMPP(BaseXMPP):
|
|||||||
self.register_plugin('feature_starttls')
|
self.register_plugin('feature_starttls')
|
||||||
self.register_plugin('feature_bind')
|
self.register_plugin('feature_bind')
|
||||||
self.register_plugin('feature_session')
|
self.register_plugin('feature_session')
|
||||||
|
self.register_plugin('feature_rosterver')
|
||||||
|
self.register_plugin('feature_preapproval')
|
||||||
self.register_plugin('feature_mechanisms',
|
self.register_plugin('feature_mechanisms',
|
||||||
pconfig={'use_mech': sasl_mech} if sasl_mech else None)
|
pconfig={'use_mech': sasl_mech} if sasl_mech else None)
|
||||||
self.register_plugin('feature_rosterver')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def password(self):
|
def password(self):
|
||||||
@ -286,15 +287,17 @@ class ClientXMPP(BaseXMPP):
|
|||||||
if iq['roster']['ver']:
|
if iq['roster']['ver']:
|
||||||
roster.version = iq['roster']['ver']
|
roster.version = iq['roster']['ver']
|
||||||
items = iq['roster']['items']
|
items = iq['roster']['items']
|
||||||
for jid in items:
|
|
||||||
item = items[jid]
|
|
||||||
roster[jid]['name'] = item['name']
|
|
||||||
roster[jid]['groups'] = item['groups']
|
|
||||||
roster[jid]['from'] = item['subscription'] in ['from', 'both']
|
|
||||||
roster[jid]['to'] = item['subscription'] in ['to', 'both']
|
|
||||||
roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
|
|
||||||
|
|
||||||
roster[jid].save(remove=(item['subscription'] == 'remove'))
|
valid_subscriptions = ('to', 'from', 'both', 'none', 'remove')
|
||||||
|
for jid, item in items.items():
|
||||||
|
if item['subscription'] in valid_subscriptions:
|
||||||
|
roster[jid]['name'] = item['name']
|
||||||
|
roster[jid]['groups'] = item['groups']
|
||||||
|
roster[jid]['from'] = item['subscription'] in ('from', 'both')
|
||||||
|
roster[jid]['to'] = item['subscription'] in ('to', 'both')
|
||||||
|
roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
|
||||||
|
|
||||||
|
roster[jid].save(remove=(item['subscription'] == 'remove'))
|
||||||
|
|
||||||
self.event("roster_update", iq)
|
self.event("roster_update", iq)
|
||||||
if iq['type'] == 'set':
|
if iq['type'] == 'set':
|
||||||
|
@ -11,5 +11,6 @@ __all__ = [
|
|||||||
'feature_mechanisms',
|
'feature_mechanisms',
|
||||||
'feature_bind',
|
'feature_bind',
|
||||||
'feature_session',
|
'feature_session',
|
||||||
'feature_rosterver'
|
'feature_rosterver',
|
||||||
|
'feature_preapproval'
|
||||||
]
|
]
|
||||||
|
@ -6,12 +6,11 @@
|
|||||||
See the file LICENSE for copying permission.
|
See the file LICENSE for copying permission.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from sleekxmpp.thirdparty import suelta
|
from sleekxmpp.util import sasl
|
||||||
from sleekxmpp.thirdparty.suelta.exceptions import SASLCancelled, SASLError
|
from sleekxmpp.util.stringprep_profiles import StringPrepError
|
||||||
from sleekxmpp.thirdparty.suelta.exceptions import SASLPrepFailure
|
|
||||||
|
|
||||||
from sleekxmpp.stanza import StreamFeatures
|
from sleekxmpp.stanza import StreamFeatures
|
||||||
from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
|
from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
|
||||||
from sleekxmpp.plugins import BasePlugin
|
from sleekxmpp.plugins import BasePlugin
|
||||||
@ -31,7 +30,15 @@ class FeatureMechanisms(BasePlugin):
|
|||||||
stanza = stanza
|
stanza = stanza
|
||||||
default_config = {
|
default_config = {
|
||||||
'use_mech': None,
|
'use_mech': None,
|
||||||
|
'use_mechs': None,
|
||||||
|
'min_mech': None,
|
||||||
'sasl_callback': None,
|
'sasl_callback': None,
|
||||||
|
'security_callback': None,
|
||||||
|
'encrypted_plain': True,
|
||||||
|
'unencrypted_plain': False,
|
||||||
|
'unencrypted_digest': False,
|
||||||
|
'unencrypted_cram': False,
|
||||||
|
'unencrypted_scram': True,
|
||||||
'order': 100
|
'order': 100
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,34 +46,13 @@ class FeatureMechanisms(BasePlugin):
|
|||||||
if not self.use_mech and not self.xmpp.boundjid.user:
|
if not self.use_mech and not self.xmpp.boundjid.user:
|
||||||
self.use_mech = 'ANONYMOUS'
|
self.use_mech = 'ANONYMOUS'
|
||||||
|
|
||||||
def tls_active():
|
|
||||||
return 'starttls' in self.xmpp.features
|
|
||||||
|
|
||||||
def basic_callback(mech, values):
|
|
||||||
creds = self.xmpp.credentials
|
|
||||||
for value in values:
|
|
||||||
if value == 'username':
|
|
||||||
values['username'] = self.xmpp.boundjid.user
|
|
||||||
elif value == 'password':
|
|
||||||
values['password'] = creds['password']
|
|
||||||
elif value == 'email':
|
|
||||||
jid = self.xmpp.boundjid.bare
|
|
||||||
values['email'] = creds.get('email', jid)
|
|
||||||
elif value in creds:
|
|
||||||
values[value] = creds[value]
|
|
||||||
mech.fulfill(values)
|
|
||||||
|
|
||||||
if self.sasl_callback is None:
|
if self.sasl_callback is None:
|
||||||
self.sasl_callback = basic_callback
|
self.sasl_callback = self._default_credentials
|
||||||
|
|
||||||
|
if self.security_callback is None:
|
||||||
|
self.security_callback = self._default_security
|
||||||
|
|
||||||
self.mech = None
|
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=self.sasl_callback,
|
|
||||||
tls_active=tls_active,
|
|
||||||
mech=self.use_mech)
|
|
||||||
|
|
||||||
self.mech_list = set()
|
self.mech_list = set()
|
||||||
self.attempted_mechs = set()
|
self.attempted_mechs = set()
|
||||||
|
|
||||||
@ -99,6 +85,44 @@ class FeatureMechanisms(BasePlugin):
|
|||||||
restart=True,
|
restart=True,
|
||||||
order=self.order)
|
order=self.order)
|
||||||
|
|
||||||
|
def _default_credentials(self, required_values, optional_values):
|
||||||
|
creds = self.xmpp.credentials
|
||||||
|
result = {}
|
||||||
|
values = required_values.union(optional_values)
|
||||||
|
for value in values:
|
||||||
|
if value == 'username':
|
||||||
|
result[value] = self.xmpp.boundjid.user
|
||||||
|
elif value == 'password':
|
||||||
|
result[value] = creds['password']
|
||||||
|
elif value == 'email':
|
||||||
|
jid = self.xmpp.boundjid.bare
|
||||||
|
result[value] = creds.get('email', jid)
|
||||||
|
elif value == 'channel_binding':
|
||||||
|
if sys.version_info >= (3, 3):
|
||||||
|
result[value] = self.xmpp.socket.channel_binding()
|
||||||
|
else:
|
||||||
|
result[value] = None
|
||||||
|
elif value == 'host':
|
||||||
|
result[value] = self.xmpp.boundjid.domain
|
||||||
|
elif value == 'realm':
|
||||||
|
result[value] = self.xmpp.boundjid.domain
|
||||||
|
elif value == 'service-name':
|
||||||
|
result[value] = self.xmpp.address[0]
|
||||||
|
elif value == 'service':
|
||||||
|
result[value] = 'xmpp'
|
||||||
|
elif value in creds:
|
||||||
|
result[value] = creds[value]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _default_security(self, values):
|
||||||
|
result = {}
|
||||||
|
for value in values:
|
||||||
|
if value == 'encrypted':
|
||||||
|
result[value] = 'starttls' in self.xmpp.features
|
||||||
|
else:
|
||||||
|
result[value] = self.config.get(value, False)
|
||||||
|
return result
|
||||||
|
|
||||||
def _handle_sasl_auth(self, features):
|
def _handle_sasl_auth(self, features):
|
||||||
"""
|
"""
|
||||||
Handle authenticating using SASL.
|
Handle authenticating using SASL.
|
||||||
@ -111,37 +135,61 @@ class FeatureMechanisms(BasePlugin):
|
|||||||
# server has incorrectly offered it again.
|
# server has incorrectly offered it again.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not self.use_mech:
|
enforce_limit = False
|
||||||
self.mech_list = set(features['mechanisms'])
|
limited_mechs = self.use_mechs
|
||||||
else:
|
|
||||||
self.mech_list = set([self.use_mech])
|
if limited_mechs is None:
|
||||||
|
limited_mechs = set()
|
||||||
|
elif limited_mechs and not isinstance(limited_mechs, set):
|
||||||
|
limited_mechs = set(limited_mechs)
|
||||||
|
enforce_limit = True
|
||||||
|
|
||||||
|
if self.use_mech:
|
||||||
|
limited_mechs.add(self.use_mech)
|
||||||
|
enforce_limit = True
|
||||||
|
|
||||||
|
if enforce_limit:
|
||||||
|
self.use_mechs = limited_mechs
|
||||||
|
|
||||||
|
self.mech_list = set(features['mechanisms'])
|
||||||
|
|
||||||
return self._send_auth()
|
return self._send_auth()
|
||||||
|
|
||||||
def _send_auth(self):
|
def _send_auth(self):
|
||||||
mech_list = self.mech_list - self.attempted_mechs
|
mech_list = self.mech_list - self.attempted_mechs
|
||||||
self.mech = self.sasl.choose_mechanism(mech_list)
|
try:
|
||||||
|
self.mech = sasl.choose(mech_list,
|
||||||
if mech_list and self.mech is not None:
|
self.sasl_callback,
|
||||||
resp = stanza.Auth(self.xmpp)
|
self.security_callback,
|
||||||
resp['mechanism'] = self.mech.name
|
limit=self.use_mechs,
|
||||||
try:
|
min_mech=self.min_mech)
|
||||||
resp['value'] = self.mech.process()
|
except sasl.SASLNoAppropriateMechanism:
|
||||||
except SASLCancelled:
|
|
||||||
self.attempted_mechs.add(self.mech.name)
|
|
||||||
self._send_auth()
|
|
||||||
except SASLError:
|
|
||||||
self.attempted_mechs.add(self.mech.name)
|
|
||||||
self._send_auth()
|
|
||||||
except SASLPrepFailure:
|
|
||||||
log.exception("A credential value did not pass SASLprep.")
|
|
||||||
self.xmpp.disconnect()
|
|
||||||
else:
|
|
||||||
resp.send(now=True)
|
|
||||||
else:
|
|
||||||
log.error("No appropriate login method.")
|
log.error("No appropriate login method.")
|
||||||
self.xmpp.event("no_auth", direct=True)
|
self.xmpp.event("no_auth", direct=True)
|
||||||
self.attempted_mechs = set()
|
self.attempted_mechs = set()
|
||||||
|
return self.xmpp.disconnect()
|
||||||
|
|
||||||
|
resp = stanza.Auth(self.xmpp)
|
||||||
|
resp['mechanism'] = self.mech.name
|
||||||
|
try:
|
||||||
|
resp['value'] = self.mech.process()
|
||||||
|
except sasl.SASLCancelled:
|
||||||
|
self.attempted_mechs.add(self.mech.name)
|
||||||
|
self._send_auth()
|
||||||
|
except sasl.SASLFailed:
|
||||||
|
self.attempted_mechs.add(self.mech.name)
|
||||||
|
self._send_auth()
|
||||||
|
except sasl.SASLMutualAuthFailed:
|
||||||
|
log.error("Mutual authentication failed! " + \
|
||||||
|
"A security breach is possible.")
|
||||||
|
self.attempted_mechs.add(self.mech.name)
|
||||||
self.xmpp.disconnect()
|
self.xmpp.disconnect()
|
||||||
|
except StringPrepError:
|
||||||
|
log.exception("A credential value did not pass SASLprep.")
|
||||||
|
self.xmpp.disconnect()
|
||||||
|
else:
|
||||||
|
resp.send(now=True)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _handle_challenge(self, stanza):
|
def _handle_challenge(self, stanza):
|
||||||
@ -149,20 +197,33 @@ class FeatureMechanisms(BasePlugin):
|
|||||||
resp = self.stanza.Response(self.xmpp)
|
resp = self.stanza.Response(self.xmpp)
|
||||||
try:
|
try:
|
||||||
resp['value'] = self.mech.process(stanza['value'])
|
resp['value'] = self.mech.process(stanza['value'])
|
||||||
except SASLCancelled:
|
except sasl.SASLCancelled:
|
||||||
self.stanza.Abort(self.xmpp).send()
|
self.stanza.Abort(self.xmpp).send()
|
||||||
except SASLError:
|
except sasl.SASLFailed:
|
||||||
self.stanza.Abort(self.xmpp).send()
|
self.stanza.Abort(self.xmpp).send()
|
||||||
|
except sasl.SASLMutualAuthFailed:
|
||||||
|
log.error("Mutual authentication failed! " + \
|
||||||
|
"A security breach is possible.")
|
||||||
|
self.attempted_mechs.add(self.mech.name)
|
||||||
|
self.xmpp.disconnect()
|
||||||
else:
|
else:
|
||||||
resp.send(now=True)
|
resp.send(now=True)
|
||||||
|
|
||||||
def _handle_success(self, stanza):
|
def _handle_success(self, stanza):
|
||||||
"""SASL authentication succeeded. Restart the stream."""
|
"""SASL authentication succeeded. Restart the stream."""
|
||||||
self.attempted_mechs = set()
|
try:
|
||||||
self.xmpp.authenticated = True
|
final = self.mech.process(stanza['value'])
|
||||||
self.xmpp.features.add('mechanisms')
|
except sasl.SASLMutualAuthFailed:
|
||||||
self.xmpp.event('auth_success', stanza, direct=True)
|
log.error("Mutual authentication failed! " + \
|
||||||
raise RestartStream()
|
"A security breach is possible.")
|
||||||
|
self.attempted_mechs.add(self.mech.name)
|
||||||
|
self.xmpp.disconnect()
|
||||||
|
else:
|
||||||
|
self.attempted_mechs = set()
|
||||||
|
self.xmpp.authenticated = True
|
||||||
|
self.xmpp.features.add('mechanisms')
|
||||||
|
self.xmpp.event('auth_success', stanza, direct=True)
|
||||||
|
raise RestartStream()
|
||||||
|
|
||||||
def _handle_fail(self, stanza):
|
def _handle_fail(self, stanza):
|
||||||
"""SASL authentication failed. Disconnect and shutdown."""
|
"""SASL authentication failed. Disconnect and shutdown."""
|
||||||
|
@ -8,8 +8,7 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
from sleekxmpp.util import bytes
|
||||||
|
|
||||||
from sleekxmpp.xmlstream import StanzaBase
|
from sleekxmpp.xmlstream import StanzaBase
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,8 +8,7 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
from sleekxmpp.util import bytes
|
||||||
|
|
||||||
from sleekxmpp.xmlstream import StanzaBase
|
from sleekxmpp.xmlstream import StanzaBase
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,8 +8,7 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
from sleekxmpp.util import bytes
|
||||||
|
|
||||||
from sleekxmpp.xmlstream import StanzaBase
|
from sleekxmpp.xmlstream import StanzaBase
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,8 +6,10 @@
|
|||||||
See the file LICENSE for copying permission.
|
See the file LICENSE for copying permission.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sleekxmpp.xmlstream import StanzaBase
|
import base64
|
||||||
|
|
||||||
|
from sleekxmpp.util import bytes
|
||||||
|
from sleekxmpp.xmlstream import StanzaBase
|
||||||
|
|
||||||
class Success(StanzaBase):
|
class Success(StanzaBase):
|
||||||
|
|
||||||
@ -16,9 +18,21 @@ class Success(StanzaBase):
|
|||||||
|
|
||||||
name = 'success'
|
name = 'success'
|
||||||
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||||
interfaces = set()
|
interfaces = set(['value'])
|
||||||
plugin_attrib = name
|
plugin_attrib = name
|
||||||
|
|
||||||
def setup(self, xml):
|
def setup(self, xml):
|
||||||
StanzaBase.setup(self, xml)
|
StanzaBase.setup(self, xml)
|
||||||
self.xml.tag = self.tag_name()
|
self.xml.tag = self.tag_name()
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
return base64.b64decode(bytes(self.xml.text))
|
||||||
|
|
||||||
|
def set_value(self, values):
|
||||||
|
if values:
|
||||||
|
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
|
||||||
|
else:
|
||||||
|
self.xml.text = '='
|
||||||
|
|
||||||
|
def del_value(self):
|
||||||
|
self.xml.text = ''
|
||||||
|
15
sleekxmpp/features/feature_preapproval/__init__.py
Normal file
15
sleekxmpp/features/feature_preapproval/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2012 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.base import register_plugin
|
||||||
|
|
||||||
|
from sleekxmpp.features.feature_preapproval.preapproval import FeaturePreApproval
|
||||||
|
from sleekxmpp.features.feature_preapproval.stanza import PreApproval
|
||||||
|
|
||||||
|
|
||||||
|
register_plugin(FeaturePreApproval)
|
42
sleekxmpp/features/feature_preapproval/preapproval.py
Normal file
42
sleekxmpp/features/feature_preapproval/preapproval.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2012 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import Iq, StreamFeatures
|
||||||
|
from sleekxmpp.features.feature_preapproval import stanza
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.plugins.base import BasePlugin
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FeaturePreApproval(BasePlugin):
|
||||||
|
|
||||||
|
name = 'feature_preapproval'
|
||||||
|
description = 'RFC 6121: Stream Feature: Subscription Pre-Approval'
|
||||||
|
dependences = set()
|
||||||
|
stanza = stanza
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xmpp.register_feature('preapproval',
|
||||||
|
self._handle_preapproval,
|
||||||
|
restart=False,
|
||||||
|
order=9001)
|
||||||
|
|
||||||
|
register_stanza_plugin(StreamFeatures, stanza.PreApproval)
|
||||||
|
|
||||||
|
def _handle_preapproval(self, features):
|
||||||
|
"""Save notice that the server support subscription pre-approvals.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
features -- The stream features stanza.
|
||||||
|
"""
|
||||||
|
log.debug("Server supports subscription pre-approvals.")
|
||||||
|
self.xmpp.features.add('preapproval')
|
17
sleekxmpp/features/feature_preapproval/stanza.py
Normal file
17
sleekxmpp/features/feature_preapproval/stanza.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2012 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.xmlstream import ElementBase
|
||||||
|
|
||||||
|
|
||||||
|
class PreApproval(ElementBase):
|
||||||
|
|
||||||
|
name = 'sub'
|
||||||
|
namespace = 'urn:xmpp:features:pre-approval'
|
||||||
|
interfaces = set()
|
||||||
|
plugin_attrib = 'preapproval'
|
@ -18,6 +18,7 @@ __all__ = [
|
|||||||
'xep_0004', # Data Forms
|
'xep_0004', # Data Forms
|
||||||
'xep_0009', # Jabber-RPC
|
'xep_0009', # Jabber-RPC
|
||||||
'xep_0012', # Last Activity
|
'xep_0012', # Last Activity
|
||||||
|
'xep_0016', # Privacy Lists
|
||||||
'xep_0027', # Current Jabber OpenPGP Usage
|
'xep_0027', # Current Jabber OpenPGP Usage
|
||||||
'xep_0030', # Service Discovery
|
'xep_0030', # Service Discovery
|
||||||
'xep_0033', # Extended Stanza Addresses
|
'xep_0033', # Extended Stanza Addresses
|
||||||
@ -60,6 +61,7 @@ __all__ = [
|
|||||||
'xep_0223', # Persistent Storage of Private Data via Pubsub
|
'xep_0223', # Persistent Storage of Private Data via Pubsub
|
||||||
'xep_0224', # Attention
|
'xep_0224', # Attention
|
||||||
'xep_0231', # Bits of Binary
|
'xep_0231', # Bits of Binary
|
||||||
|
'xep_0242', # XMPP Client Compliance 2009
|
||||||
'xep_0249', # Direct MUC Invitations
|
'xep_0249', # Direct MUC Invitations
|
||||||
'xep_0256', # Last Activity in Presence
|
'xep_0256', # Last Activity in Presence
|
||||||
'xep_0258', # Security Labels in XMPP
|
'xep_0258', # Security Labels in XMPP
|
||||||
|
16
sleekxmpp/plugins/xep_0016/__init__.py
Normal file
16
sleekxmpp/plugins/xep_0016/__init__.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.base import register_plugin
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0016 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0016.stanza import Privacy
|
||||||
|
from sleekxmpp.plugins.xep_0016.privacy import XEP_0016
|
||||||
|
|
||||||
|
|
||||||
|
register_plugin(XEP_0016)
|
110
sleekxmpp/plugins/xep_0016/privacy.py
Normal file
110
sleekxmpp/plugins/xep_0016/privacy.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp import Iq
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.plugins import BasePlugin
|
||||||
|
from sleekxmpp.plugins.xep_0016 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0016.stanza import Privacy, Item
|
||||||
|
|
||||||
|
|
||||||
|
class XEP_0016(BasePlugin):
|
||||||
|
|
||||||
|
name = 'xep_0016'
|
||||||
|
description = 'XEP-0016: Privacy Lists'
|
||||||
|
dependencies = set(['xep_0030'])
|
||||||
|
stanza = stanza
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
register_stanza_plugin(Iq, Privacy)
|
||||||
|
|
||||||
|
def plugin_end(self):
|
||||||
|
self.xmpp['xep_0030'].del_feature(feature=Privacy.namespace)
|
||||||
|
|
||||||
|
def session_bind(self, jid):
|
||||||
|
self.xmpp['xep_0030'].add_feature(Privacy.namespace)
|
||||||
|
|
||||||
|
def get_privacy_lists(self, block=True, timeout=None, callback=None):
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq.enable('privacy')
|
||||||
|
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||||
|
|
||||||
|
def get_list(self, name, block=True, timeout=None, callback=None):
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq['privacy']['list']['name'] = name
|
||||||
|
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||||
|
|
||||||
|
def get_active(self, block=True, timeout=None, callback=None):
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq['privacy'].enable('active')
|
||||||
|
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||||
|
|
||||||
|
def get_default(self, block=True, timeout=None, callback=None):
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq['privacy'].enable('default')
|
||||||
|
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||||
|
|
||||||
|
def activate(self, name, block=True, timeout=None, callback=None):
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['privacy']['active']['name'] = name
|
||||||
|
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||||
|
|
||||||
|
def deactivate(self, block=True, timeout=None, callback=None):
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['privacy'].enable('active')
|
||||||
|
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||||
|
|
||||||
|
def make_default(self, name, block=True, timeout=None, callback=None):
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['privacy']['default']['name'] = name
|
||||||
|
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||||
|
|
||||||
|
def remove_default(self, block=True, timeout=None, callback=None):
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['privacy'].enable('default')
|
||||||
|
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||||
|
|
||||||
|
def edit_list(self, name, rules, block=True, timeout=None, callback=None):
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['privacy']['list']['name'] = name
|
||||||
|
priv_list = iq['privacy']['list']
|
||||||
|
|
||||||
|
if not rules:
|
||||||
|
rules = []
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
if isinstance(rule, Item):
|
||||||
|
priv_list.append(rule)
|
||||||
|
continue
|
||||||
|
|
||||||
|
priv_list.add_item(
|
||||||
|
rule['value'],
|
||||||
|
rule['action'],
|
||||||
|
rule['order'],
|
||||||
|
itype=rule.get('type', None),
|
||||||
|
iq=rule.get('iq', None),
|
||||||
|
message=rule.get('message', None),
|
||||||
|
presence_in=rule.get('presence_in',
|
||||||
|
rule.get('presence-in', None)),
|
||||||
|
presence_out=rule.get('presence_out',
|
||||||
|
rule.get('presence-out', None)))
|
||||||
|
|
||||||
|
def remove_list(self, name, block=True, timeout=None, callback=None):
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['privacy']['list']['name'] = name
|
||||||
|
return iq.send(block=block, timeout=timeout, callback=callback)
|
103
sleekxmpp/plugins/xep_0016/stanza.py
Normal file
103
sleekxmpp/plugins/xep_0016/stanza.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class Privacy(ElementBase):
|
||||||
|
name = 'query'
|
||||||
|
namespace = 'jabber:iq:privacy'
|
||||||
|
plugin_attrib = 'privacy'
|
||||||
|
interfaces = set()
|
||||||
|
|
||||||
|
def add_list(self, name):
|
||||||
|
priv_list = List()
|
||||||
|
priv_list['name'] = name
|
||||||
|
self.append(priv_list)
|
||||||
|
return priv_list
|
||||||
|
|
||||||
|
|
||||||
|
class Active(ElementBase):
|
||||||
|
name = 'active'
|
||||||
|
namespace = 'jabber:iq:privacy'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(['name'])
|
||||||
|
|
||||||
|
|
||||||
|
class Default(ElementBase):
|
||||||
|
name = 'default'
|
||||||
|
namespace = 'jabber:iq:privacy'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(['name'])
|
||||||
|
|
||||||
|
|
||||||
|
class List(ElementBase):
|
||||||
|
name = 'list'
|
||||||
|
namespace = 'jabber:iq:privacy'
|
||||||
|
plugin_attrib = name
|
||||||
|
plugin_multi_attrib = 'lists'
|
||||||
|
interfaces = set(['name'])
|
||||||
|
|
||||||
|
def add_item(self, value, action, order, itype=None, iq=False,
|
||||||
|
message=False, presence_in=False, presence_out=False):
|
||||||
|
item = Item()
|
||||||
|
item.values = {'type': itype,
|
||||||
|
'value': value,
|
||||||
|
'action': action,
|
||||||
|
'order': order,
|
||||||
|
'message': message,
|
||||||
|
'iq': iq,
|
||||||
|
'presence_in': presence_in,
|
||||||
|
'presence_out': presence_out}
|
||||||
|
self.append(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
class Item(ElementBase):
|
||||||
|
name = 'item'
|
||||||
|
namespace = 'jabber:iq:privacy'
|
||||||
|
plugin_attrib = name
|
||||||
|
plugin_multi_attrib = 'items'
|
||||||
|
interfaces = set(['type', 'value', 'action', 'order', 'iq',
|
||||||
|
'message', 'presence_in', 'presence_out'])
|
||||||
|
bool_interfaces = set(['message', 'iq', 'presence_in', 'presence_out'])
|
||||||
|
|
||||||
|
type_values = ('', 'jid', 'group', 'subscription')
|
||||||
|
action_values = ('allow', 'deny')
|
||||||
|
|
||||||
|
def set_type(self, value):
|
||||||
|
if value and value not in self.type_values:
|
||||||
|
raise ValueError('Unknown type value: %s' % value)
|
||||||
|
else:
|
||||||
|
self._set_attr('type', value)
|
||||||
|
|
||||||
|
def set_action(self, value):
|
||||||
|
if value not in self.action_values:
|
||||||
|
raise ValueError('Unknown action value: %s' % value)
|
||||||
|
else:
|
||||||
|
self._set_attr('action', value)
|
||||||
|
|
||||||
|
def set_presence_in(self, value):
|
||||||
|
keep = True if value else False
|
||||||
|
self._set_sub_text('presence-in', '', keep=keep)
|
||||||
|
|
||||||
|
def get_presence_in(self):
|
||||||
|
pres = self.xml.find('{%s}presence-in' % self.namespace)
|
||||||
|
return pres is not None
|
||||||
|
|
||||||
|
def del_presence_in(self):
|
||||||
|
self._del_sub('{%s}presence-in' % self.namespace)
|
||||||
|
|
||||||
|
def set_presence_out(self, value):
|
||||||
|
keep = True if value else False
|
||||||
|
self._set_sub_text('presence-in', '', keep=keep)
|
||||||
|
|
||||||
|
def get_presence_out(self):
|
||||||
|
pres = self.xml.find('{%s}presence-in' % self.namespace)
|
||||||
|
return pres is not None
|
||||||
|
|
||||||
|
def del_presence_out(self):
|
||||||
|
self._del_sub('{%s}presence-in' % self.namespace)
|
||||||
|
|
||||||
|
|
||||||
|
register_stanza_plugin(Privacy, Active)
|
||||||
|
register_stanza_plugin(Privacy, Default)
|
||||||
|
register_stanza_plugin(Privacy, List, iterable=True)
|
||||||
|
register_stanza_plugin(List, Item, iterable=True)
|
@ -1,9 +1,9 @@
|
|||||||
import re
|
import re
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
from sleekxmpp.util import bytes
|
||||||
from sleekxmpp.exceptions import XMPPError
|
from sleekxmpp.exceptions import XMPPError
|
||||||
from sleekxmpp.xmlstream import ElementBase
|
from sleekxmpp.xmlstream import ElementBase
|
||||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
|
||||||
|
|
||||||
|
|
||||||
VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*')
|
VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*')
|
||||||
|
@ -187,12 +187,6 @@ class XEP_0050(BasePlugin):
|
|||||||
jid = JID(jid)
|
jid = JID(jid)
|
||||||
item_jid = jid.full
|
item_jid = jid.full
|
||||||
|
|
||||||
# Client disco uses only the bare JID
|
|
||||||
if self.xmpp.is_component:
|
|
||||||
jid = jid.full
|
|
||||||
else:
|
|
||||||
jid = jid.bare
|
|
||||||
|
|
||||||
self.xmpp['xep_0030'].add_identity(category='automation',
|
self.xmpp['xep_0030'].add_identity(category='automation',
|
||||||
itype='command-list',
|
itype='command-list',
|
||||||
name='Ad-Hoc commands',
|
name='Ad-Hoc commands',
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import base64
|
import base64
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
||||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
from sleekxmpp.util import bytes
|
||||||
|
|
||||||
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin, JID
|
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin, JID
|
||||||
from sleekxmpp.plugins import xep_0082
|
from sleekxmpp.plugins import xep_0082
|
||||||
|
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from base64 import b64encode, b64decode
|
from base64 import b64encode, b64decode
|
||||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
|
||||||
|
|
||||||
|
from sleekxmpp.util import bytes
|
||||||
from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
|
from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
21
sleekxmpp/plugins/xep_0242.py
Normal file
21
sleekxmpp/plugins/xep_0242.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins import BasePlugin, register_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class XEP_0242(BasePlugin):
|
||||||
|
|
||||||
|
name = 'xep_0242'
|
||||||
|
description = 'XEP-0242: XMPP Client Compliance 2009'
|
||||||
|
dependencies = set(['xep_0030', 'xep_0115', 'xep_0054',
|
||||||
|
'xep_0045', 'xep_0085', 'xep_0016',
|
||||||
|
'xep_0191'])
|
||||||
|
|
||||||
|
|
||||||
|
register_plugin(XEP_0242)
|
@ -8,8 +8,7 @@
|
|||||||
|
|
||||||
from base64 import b64encode, b64decode
|
from base64 import b64encode, b64decode
|
||||||
|
|
||||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
from sleekxmpp.util import bytes
|
||||||
|
|
||||||
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
|
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
2
sleekxmpp/thirdparty/__init__.py
vendored
2
sleekxmpp/thirdparty/__init__.py
vendored
@ -8,5 +8,5 @@ try:
|
|||||||
except:
|
except:
|
||||||
from sleekxmpp.thirdparty.gnupg import GPG
|
from sleekxmpp.thirdparty.gnupg import GPG
|
||||||
|
|
||||||
from sleekxmpp.thirdparty import suelta, socks
|
from sleekxmpp.thirdparty import socks
|
||||||
from sleekxmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso
|
from sleekxmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso
|
||||||
|
21
sleekxmpp/thirdparty/suelta/LICENSE
vendored
21
sleekxmpp/thirdparty/suelta/LICENSE
vendored
@ -1,21 +0,0 @@
|
|||||||
This software is subject to "The MIT License"
|
|
||||||
|
|
||||||
Copyright 2007-2010 David Alan Cridland
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
27
sleekxmpp/thirdparty/suelta/PLAYING-NICELY
vendored
27
sleekxmpp/thirdparty/suelta/PLAYING-NICELY
vendored
@ -1,27 +0,0 @@
|
|||||||
Hi.
|
|
||||||
|
|
||||||
This is a short note explaining the license in non-legally-binding terms, and
|
|
||||||
describing how I hope to see people work with the licensing.
|
|
||||||
|
|
||||||
First off, the license is permissive, and more or less allows you to do
|
|
||||||
anything, as long as you leave my credit and copyright intact.
|
|
||||||
|
|
||||||
You can, and are very much welcome to, include this in commercial works, and
|
|
||||||
in code that has tightly controlled distribution, as well as open-source.
|
|
||||||
|
|
||||||
If it doesn't work - and I have no doubt that there are bugs - then this is
|
|
||||||
largely your problem.
|
|
||||||
|
|
||||||
If you do find a bug, though, do let me know - although you don't have to.
|
|
||||||
|
|
||||||
And if you fix it, I'd greatly appreciate a patch, too. Please give me a
|
|
||||||
licensing statement, and a copyright statement, along with your patch.
|
|
||||||
|
|
||||||
Similarly, any enhancements are welcome, and also will need copyright and
|
|
||||||
licensing. Please stick to a license which is compatible with the MIT license,
|
|
||||||
and consider assignment (as required) to me to simplify licensing. (Public
|
|
||||||
domain does not exist in the UK, sorry).
|
|
||||||
|
|
||||||
Thanks,
|
|
||||||
|
|
||||||
Dave.
|
|
8
sleekxmpp/thirdparty/suelta/README
vendored
8
sleekxmpp/thirdparty/suelta/README
vendored
@ -1,8 +0,0 @@
|
|||||||
Suelta - A pure-Python SASL client library
|
|
||||||
|
|
||||||
Suelta is a SASL library, providing you with authentication and in some cases
|
|
||||||
security layers.
|
|
||||||
|
|
||||||
It supports a wide range of typical SASL mechanisms, including the MTI for
|
|
||||||
all known protocols.
|
|
||||||
|
|
26
sleekxmpp/thirdparty/suelta/__init__.py
vendored
26
sleekxmpp/thirdparty/suelta/__init__.py
vendored
@ -1,26 +0,0 @@
|
|||||||
# Copyright 2007-2010 David Alan Cridland
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in
|
|
||||||
# all copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
# THE SOFTWARE.
|
|
||||||
|
|
||||||
from sleekxmpp.thirdparty.suelta.saslprep import saslprep
|
|
||||||
from sleekxmpp.thirdparty.suelta.sasl import *
|
|
||||||
from sleekxmpp.thirdparty.suelta.mechanisms import *
|
|
||||||
|
|
||||||
__version__ = '2.0'
|
|
||||||
__version_info__ = (2, 0, 0)
|
|
35
sleekxmpp/thirdparty/suelta/exceptions.py
vendored
35
sleekxmpp/thirdparty/suelta/exceptions.py
vendored
@ -1,35 +0,0 @@
|
|||||||
class SASLError(Exception):
|
|
||||||
|
|
||||||
def __init__(self, sasl, text, mech=None):
|
|
||||||
"""
|
|
||||||
:param sasl: The main `suelta.SASL` object.
|
|
||||||
:param text: Descpription of the error.
|
|
||||||
:param mech: Optional reference to the mechanism object.
|
|
||||||
|
|
||||||
:type sasl: `suelta.SASL`
|
|
||||||
"""
|
|
||||||
self.sasl = sasl
|
|
||||||
self.text = text
|
|
||||||
self.mech = mech
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.mech is None:
|
|
||||||
return 'SASL Error: %s' % self.text
|
|
||||||
else:
|
|
||||||
return 'SASL Error (%s): %s' % (self.mech, self.text)
|
|
||||||
|
|
||||||
|
|
||||||
class SASLCancelled(SASLError):
|
|
||||||
|
|
||||||
def __init__(self, sasl, mech=None):
|
|
||||||
"""
|
|
||||||
:param sasl: The main `suelta.SASL` object.
|
|
||||||
:param mech: Optional reference to the mechanism object.
|
|
||||||
|
|
||||||
:type sasl: `suelta.SASL`
|
|
||||||
"""
|
|
||||||
super(SASLCancelled, self).__init__(sasl, "User cancelled", mech)
|
|
||||||
|
|
||||||
|
|
||||||
class SASLPrepFailure(UnicodeError):
|
|
||||||
pass
|
|
@ -1,8 +0,0 @@
|
|||||||
from sleekxmpp.thirdparty.suelta.mechanisms.anonymous import ANONYMOUS
|
|
||||||
from sleekxmpp.thirdparty.suelta.mechanisms.plain import PLAIN
|
|
||||||
from sleekxmpp.thirdparty.suelta.mechanisms.cram_md5 import CRAM_MD5
|
|
||||||
from sleekxmpp.thirdparty.suelta.mechanisms.digest_md5 import DIGEST_MD5
|
|
||||||
from sleekxmpp.thirdparty.suelta.mechanisms.scram_hmac import SCRAM_HMAC
|
|
||||||
from sleekxmpp.thirdparty.suelta.mechanisms.messenger_oauth2 import X_MESSENGER_OAUTH2
|
|
||||||
from sleekxmpp.thirdparty.suelta.mechanisms.facebook_platform import X_FACEBOOK_PLATFORM
|
|
||||||
from sleekxmpp.thirdparty.suelta.mechanisms.google_token import X_GOOGLE_TOKEN
|
|
@ -1,36 +0,0 @@
|
|||||||
from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
|
|
||||||
from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
|
|
||||||
|
|
||||||
|
|
||||||
class ANONYMOUS(Mechanism):
|
|
||||||
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, sasl, name):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
super(ANONYMOUS, self).__init__(sasl, name, 0)
|
|
||||||
|
|
||||||
def get_values(self):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def process(self, challenge=None):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
return b'Anonymous, Suelta'
|
|
||||||
|
|
||||||
def okay(self):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_user(self):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
return 'anonymous'
|
|
||||||
|
|
||||||
|
|
||||||
register_mechanism('ANONYMOUS', 0, ANONYMOUS, use_hashes=False)
|
|
@ -1,63 +0,0 @@
|
|||||||
import sys
|
|
||||||
import hmac
|
|
||||||
|
|
||||||
from sleekxmpp.thirdparty.suelta.util import hash, bytes
|
|
||||||
from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
|
|
||||||
from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
|
|
||||||
|
|
||||||
|
|
||||||
class CRAM_MD5(Mechanism):
|
|
||||||
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, sasl, name):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
super(CRAM_MD5, self).__init__(sasl, name, 2)
|
|
||||||
|
|
||||||
self.hash = hash(name[5:])
|
|
||||||
if self.hash is None:
|
|
||||||
raise SASLCancelled(self.sasl, self)
|
|
||||||
if not self.sasl.tls_active():
|
|
||||||
if not self.sasl.sec_query(self, 'CRAM-MD5'):
|
|
||||||
raise SASLCancelled(self.sasl, self)
|
|
||||||
|
|
||||||
def prep(self):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
if 'savepass' not in self.values:
|
|
||||||
if self.sasl.sec_query(self, 'CLEAR-PASSWORD'):
|
|
||||||
self.values['savepass'] = True
|
|
||||||
|
|
||||||
if 'savepass' not in self.values:
|
|
||||||
del self.values['password']
|
|
||||||
|
|
||||||
def process(self, challenge=None):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
if challenge is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
self.check_values(['username', 'password'])
|
|
||||||
username = bytes(self.values['username'])
|
|
||||||
password = bytes(self.values['password'])
|
|
||||||
|
|
||||||
mac = hmac.HMAC(key=password, digestmod=self.hash)
|
|
||||||
|
|
||||||
mac.update(challenge)
|
|
||||||
|
|
||||||
return username + b' ' + bytes(mac.hexdigest())
|
|
||||||
|
|
||||||
def okay(self):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_user(self):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
return self.values['username']
|
|
||||||
|
|
||||||
|
|
||||||
register_mechanism('CRAM-', 20, CRAM_MD5)
|
|
275
sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
vendored
275
sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
vendored
@ -1,275 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
import random
|
|
||||||
import hmac
|
|
||||||
|
|
||||||
from sleekxmpp.thirdparty.suelta.util import hash, bytes, quote
|
|
||||||
from sleekxmpp.thirdparty.suelta.util import num_to_bytes, bytes_to_num
|
|
||||||
from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
|
|
||||||
from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def parse_challenge(stuff):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
ret = {}
|
|
||||||
var = b''
|
|
||||||
val = b''
|
|
||||||
in_var = True
|
|
||||||
in_quotes = False
|
|
||||||
new = False
|
|
||||||
escaped = False
|
|
||||||
for c in stuff:
|
|
||||||
if sys.version_info >= (3, 0):
|
|
||||||
c = bytes([c])
|
|
||||||
if in_var:
|
|
||||||
if c.isspace():
|
|
||||||
continue
|
|
||||||
if c == b'=':
|
|
||||||
in_var = False
|
|
||||||
new = True
|
|
||||||
else:
|
|
||||||
var += c
|
|
||||||
else:
|
|
||||||
if new:
|
|
||||||
if c == b'"':
|
|
||||||
in_quotes = True
|
|
||||||
else:
|
|
||||||
val += c
|
|
||||||
new = False
|
|
||||||
elif in_quotes:
|
|
||||||
if escaped:
|
|
||||||
escaped = False
|
|
||||||
val += c
|
|
||||||
else:
|
|
||||||
if c == b'\\':
|
|
||||||
escaped = True
|
|
||||||
elif c == b'"':
|
|
||||||
in_quotes = False
|
|
||||||
else:
|
|
||||||
val += c
|
|
||||||
else:
|
|
||||||
if c == b',':
|
|
||||||
if var:
|
|
||||||
ret[var] = val
|
|
||||||
var = b''
|
|
||||||
val = b''
|
|
||||||
in_var = True
|
|
||||||
else:
|
|
||||||
val += c
|
|
||||||
if var:
|
|
||||||
ret[var] = val
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class DIGEST_MD5(Mechanism):
|
|
||||||
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
|
|
||||||
enc_magic = 'Digest session key to client-to-server signing key magic'
|
|
||||||
dec_magic = 'Digest session key to server-to-client signing key magic'
|
|
||||||
|
|
||||||
def __init__(self, sasl, name):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
super(DIGEST_MD5, self).__init__(sasl, name, 3)
|
|
||||||
|
|
||||||
self.hash = hash(name[7:])
|
|
||||||
if self.hash is None:
|
|
||||||
raise SASLCancelled(self.sasl, self)
|
|
||||||
|
|
||||||
if not self.sasl.tls_active():
|
|
||||||
if not self.sasl.sec_query(self, '-ENCRYPTION, DIGEST-MD5'):
|
|
||||||
raise SASLCancelled(self.sasl, self)
|
|
||||||
|
|
||||||
self._rspauth_okay = False
|
|
||||||
self._digest_uri = None
|
|
||||||
self._a1 = None
|
|
||||||
self._enc_buf = b''
|
|
||||||
self._enc_key = None
|
|
||||||
self._enc_seq = 0
|
|
||||||
self._max_buffer = 65536
|
|
||||||
self._dec_buf = b''
|
|
||||||
self._dec_key = None
|
|
||||||
self._dec_seq = 0
|
|
||||||
self._qops = [b'auth']
|
|
||||||
self._qop = b'auth'
|
|
||||||
|
|
||||||
def MAC(self, seq, msg, key):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
mac = hmac.HMAC(key=key, digestmod=self.hash)
|
|
||||||
seqnum = num_to_bytes(seq)
|
|
||||||
mac.update(seqnum)
|
|
||||||
mac.update(msg)
|
|
||||||
return mac.digest()[:10] + b'\x00\x01' + seqnum
|
|
||||||
|
|
||||||
|
|
||||||
def encode(self, text):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
self._enc_buf += text
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
result = b''
|
|
||||||
# Leave buffer space for the MAC
|
|
||||||
mbuf = self._max_buffer - 10 - 2 - 4
|
|
||||||
|
|
||||||
while self._enc_buf:
|
|
||||||
msg = self._encbuf[:mbuf]
|
|
||||||
mac = self.MAC(self._enc_seq, msg, self._enc_key, self.hash)
|
|
||||||
self._enc_seq += 1
|
|
||||||
msg += mac
|
|
||||||
result += num_to_bytes(len(msg)) + msg
|
|
||||||
self._enc_buf = self._enc_buf[mbuf:]
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def decode(self, text):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
self._dec_buf += text
|
|
||||||
result = b''
|
|
||||||
|
|
||||||
while len(self._dec_buf) > 4:
|
|
||||||
num = bytes_to_num(self._dec_buf)
|
|
||||||
if len(self._dec_buf) < (num + 4):
|
|
||||||
return result
|
|
||||||
|
|
||||||
mac = self._dec_buf[4:4 + num]
|
|
||||||
self._dec_buf = self._dec_buf[4 + num:]
|
|
||||||
msg = mac[:-16]
|
|
||||||
|
|
||||||
mac_conf = self.MAC(self._dec_mac, msg, self._dec_key)
|
|
||||||
if mac[-16:] != mac_conf:
|
|
||||||
self._desc_sec = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
self._dec_seq += 1
|
|
||||||
result += msg
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def response(self):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
vitals = ['username']
|
|
||||||
if not self.has_values(['key_hash']):
|
|
||||||
vitals.append('password')
|
|
||||||
self.check_values(vitals)
|
|
||||||
|
|
||||||
resp = {}
|
|
||||||
if 'auth-int' in self._qops:
|
|
||||||
self._qop = b'auth-int'
|
|
||||||
resp['qop'] = self._qop
|
|
||||||
if 'realm' in self.values:
|
|
||||||
resp['realm'] = quote(self.values['realm'])
|
|
||||||
|
|
||||||
resp['username'] = quote(bytes(self.values['username']))
|
|
||||||
resp['nonce'] = quote(self.values['nonce'])
|
|
||||||
if self.values['nc']:
|
|
||||||
self._cnonce = self.values['cnonce']
|
|
||||||
else:
|
|
||||||
self._cnonce = bytes('%s' % random.random())[2:]
|
|
||||||
resp['cnonce'] = quote(self._cnonce)
|
|
||||||
self.values['nc'] += 1
|
|
||||||
resp['nc'] = bytes('%08x' % self.values['nc'])
|
|
||||||
|
|
||||||
service = bytes(self.sasl.service)
|
|
||||||
host = bytes(self.sasl.host)
|
|
||||||
self._digest_uri = service + b'/' + host
|
|
||||||
resp['digest-uri'] = quote(self._digest_uri)
|
|
||||||
|
|
||||||
a2 = b'AUTHENTICATE:' + self._digest_uri
|
|
||||||
if self._qop != b'auth':
|
|
||||||
a2 += b':00000000000000000000000000000000'
|
|
||||||
resp['maxbuf'] = b'16777215' # 2**24-1
|
|
||||||
resp['response'] = self.gen_hash(a2)
|
|
||||||
return b','.join([bytes(k) + b'=' + bytes(v) for k, v in resp.items()])
|
|
||||||
|
|
||||||
def gen_hash(self, a2):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
if not self.has_values(['key_hash']):
|
|
||||||
key_hash = self.hash()
|
|
||||||
user = bytes(self.values['username'])
|
|
||||||
password = bytes(self.values['password'])
|
|
||||||
realm = bytes(self.values['realm'])
|
|
||||||
kh = user + b':' + realm + b':' + password
|
|
||||||
key_hash.update(kh)
|
|
||||||
self.values['key_hash'] = key_hash.digest()
|
|
||||||
|
|
||||||
a1 = self.hash(self.values['key_hash'])
|
|
||||||
a1h = b':' + self.values['nonce'] + b':' + self._cnonce
|
|
||||||
a1.update(a1h)
|
|
||||||
response = self.hash()
|
|
||||||
self._a1 = a1.digest()
|
|
||||||
rv = bytes(a1.hexdigest().lower())
|
|
||||||
rv += b':' + self.values['nonce']
|
|
||||||
rv += b':' + bytes('%08x' % self.values['nc'])
|
|
||||||
rv += b':' + self._cnonce
|
|
||||||
rv += b':' + self._qop
|
|
||||||
rv += b':' + bytes(self.hash(a2).hexdigest().lower())
|
|
||||||
response.update(rv)
|
|
||||||
return bytes(response.hexdigest().lower())
|
|
||||||
|
|
||||||
def mutual_auth(self, cmp_hash):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
a2 = b':' + self._digest_uri
|
|
||||||
if self._qop != b'auth':
|
|
||||||
a2 += b':00000000000000000000000000000000'
|
|
||||||
if self.gen_hash(a2) == cmp_hash:
|
|
||||||
self._rspauth_okay = True
|
|
||||||
|
|
||||||
def prep(self):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
if 'password' in self.values:
|
|
||||||
del self.values['password']
|
|
||||||
self.values['cnonce'] = self._cnonce
|
|
||||||
|
|
||||||
def process(self, challenge=None):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
if challenge is None:
|
|
||||||
if self.has_values(['username', 'realm', 'nonce', 'key_hash',
|
|
||||||
'nc', 'cnonce', 'qops']):
|
|
||||||
self._qops = self.values['qops']
|
|
||||||
return self.response()
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
d = parse_challenge(challenge)
|
|
||||||
if b'rspauth' in d:
|
|
||||||
self.mutual_auth(d[b'rspauth'])
|
|
||||||
else:
|
|
||||||
if b'realm' not in d:
|
|
||||||
d[b'realm'] = self.sasl.def_realm
|
|
||||||
for key in ['nonce', 'realm']:
|
|
||||||
if bytes(key) in d:
|
|
||||||
self.values[key] = d[bytes(key)]
|
|
||||||
self.values['nc'] = 0
|
|
||||||
self._qops = [b'auth']
|
|
||||||
if b'qop' in d:
|
|
||||||
self._qops = [x.strip() for x in d[b'qop'].split(b',')]
|
|
||||||
self.values['qops'] = self._qops
|
|
||||||
if b'maxbuf' in d:
|
|
||||||
self._max_buffer = int(d[b'maxbuf'])
|
|
||||||
return self.response()
|
|
||||||
|
|
||||||
def okay(self):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
if self._rspauth_okay and self._qop == b'auth-int':
|
|
||||||
self._enc_key = self.hash(self._a1 + self.enc_magic).digest()
|
|
||||||
self._dec_key = self.hash(self._a1 + self.dec_magic).digest()
|
|
||||||
self.encoding = True
|
|
||||||
return self._rspauth_okay
|
|
||||||
|
|
||||||
|
|
||||||
register_mechanism('DIGEST-', 30, DIGEST_MD5)
|
|
@ -1,43 +0,0 @@
|
|||||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
|
||||||
from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
|
|
||||||
|
|
||||||
try:
|
|
||||||
import urlparse
|
|
||||||
except ImportError:
|
|
||||||
import urllib.parse as urlparse
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class X_FACEBOOK_PLATFORM(Mechanism):
|
|
||||||
|
|
||||||
def __init__(self, sasl, name):
|
|
||||||
super(X_FACEBOOK_PLATFORM, self).__init__(sasl, name)
|
|
||||||
self.check_values(['access_token', 'api_key'])
|
|
||||||
|
|
||||||
def process(self, challenge=None):
|
|
||||||
if challenge is not None:
|
|
||||||
values = {}
|
|
||||||
for kv in challenge.split(b'&'):
|
|
||||||
key, value = kv.split(b'=')
|
|
||||||
values[key] = value
|
|
||||||
|
|
||||||
resp_data = {
|
|
||||||
'method': values[b'method'],
|
|
||||||
'v': '1.0',
|
|
||||||
'call_id': '1.0',
|
|
||||||
'nonce': values[b'nonce'],
|
|
||||||
'access_token': self.values['access_token'],
|
|
||||||
'api_key': self.values['api_key']
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v in resp_data.items():
|
|
||||||
resp_data[k] = bytes(v).decode('utf-8')
|
|
||||||
|
|
||||||
resp = '&'.join(['%s=%s' % (k, v) for k, v in resp_data.items()])
|
|
||||||
return bytes(resp)
|
|
||||||
return b''
|
|
||||||
|
|
||||||
def okay(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
register_mechanism('X-FACEBOOK-PLATFORM', 40, X_FACEBOOK_PLATFORM, use_hashes=False)
|
|
@ -1,22 +0,0 @@
|
|||||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
|
||||||
from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
|
|
||||||
from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class X_GOOGLE_TOKEN(Mechanism):
|
|
||||||
|
|
||||||
def __init__(self, sasl, name):
|
|
||||||
super(X_GOOGLE_TOKEN, self).__init__(sasl, name)
|
|
||||||
self.check_values(['email', 'access_token'])
|
|
||||||
|
|
||||||
def process(self, challenge=None):
|
|
||||||
email = bytes(self.values['email'])
|
|
||||||
token = bytes(self.values['access_token'])
|
|
||||||
return b'\x00' + email + b'\x00' + token
|
|
||||||
|
|
||||||
def okay(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
register_mechanism('X-GOOGLE-TOKEN', 3, X_GOOGLE_TOKEN, use_hashes=False)
|
|
@ -1,17 +0,0 @@
|
|||||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
|
||||||
from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
|
|
||||||
|
|
||||||
|
|
||||||
class X_MESSENGER_OAUTH2(Mechanism):
|
|
||||||
|
|
||||||
def __init__(self, sasl, name):
|
|
||||||
super(X_MESSENGER_OAUTH2, self).__init__(sasl, name)
|
|
||||||
self.check_values(['access_token'])
|
|
||||||
|
|
||||||
def process(self, challenge=None):
|
|
||||||
return bytes(self.values['access_token'])
|
|
||||||
|
|
||||||
def okay(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
register_mechanism('X-MESSENGER-OAUTH2', 10, X_MESSENGER_OAUTH2, use_hashes=False)
|
|
61
sleekxmpp/thirdparty/suelta/mechanisms/plain.py
vendored
61
sleekxmpp/thirdparty/suelta/mechanisms/plain.py
vendored
@ -1,61 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
|
||||||
from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
|
|
||||||
from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
|
|
||||||
|
|
||||||
|
|
||||||
class PLAIN(Mechanism):
|
|
||||||
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, sasl, name):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
super(PLAIN, self).__init__(sasl, name)
|
|
||||||
|
|
||||||
if not self.sasl.tls_active():
|
|
||||||
if not self.sasl.sec_query(self, '-ENCRYPTION, PLAIN'):
|
|
||||||
raise SASLCancelled(self.sasl, self)
|
|
||||||
else:
|
|
||||||
if not self.sasl.sec_query(self, '+ENCRYPTION, PLAIN'):
|
|
||||||
raise SASLCancelled(self.sasl, self)
|
|
||||||
|
|
||||||
self.check_values(['username', 'password'])
|
|
||||||
|
|
||||||
def prep(self):
|
|
||||||
"""
|
|
||||||
Prepare for processing by deleting the password if
|
|
||||||
the user has not approved storing it in the clear.
|
|
||||||
"""
|
|
||||||
if 'savepass' not in self.values:
|
|
||||||
if self.sasl.sec_query(self, 'CLEAR-PASSWORD'):
|
|
||||||
self.values['savepass'] = True
|
|
||||||
|
|
||||||
if 'savepass' not in self.values:
|
|
||||||
del self.values['password']
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def process(self, challenge=None):
|
|
||||||
"""
|
|
||||||
Process a challenge request and return the response.
|
|
||||||
|
|
||||||
:param challenge: A challenge issued by the server that
|
|
||||||
must be answered for authentication.
|
|
||||||
"""
|
|
||||||
user = bytes(self.values['username'])
|
|
||||||
password = bytes(self.values['password'])
|
|
||||||
return b'\x00' + user + b'\x00' + password
|
|
||||||
|
|
||||||
def okay(self):
|
|
||||||
"""
|
|
||||||
Mutual authentication is not supported by PLAIN.
|
|
||||||
|
|
||||||
:returns: ``True``
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
register_mechanism('PLAIN', 5, PLAIN, use_hashes=False)
|
|
176
sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py
vendored
176
sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py
vendored
@ -1,176 +0,0 @@
|
|||||||
import sys
|
|
||||||
import hmac
|
|
||||||
import random
|
|
||||||
from base64 import b64encode, b64decode
|
|
||||||
|
|
||||||
from sleekxmpp.thirdparty.suelta.util import hash, bytes, num_to_bytes, bytes_to_num, XOR
|
|
||||||
from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
|
|
||||||
from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
|
|
||||||
|
|
||||||
|
|
||||||
def parse_challenge(challenge):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
items = {}
|
|
||||||
for key, value in [item.split(b'=', 1) for item in challenge.split(b',')]:
|
|
||||||
items[key] = value
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
class SCRAM_HMAC(Mechanism):
|
|
||||||
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, sasl, name):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
super(SCRAM_HMAC, self).__init__(sasl, name, 0)
|
|
||||||
|
|
||||||
self._cb = False
|
|
||||||
if name[-5:] == '-PLUS':
|
|
||||||
name = name[:-5]
|
|
||||||
self._cb = True
|
|
||||||
|
|
||||||
self.hash = hash(name[6:])
|
|
||||||
if self.hash is None:
|
|
||||||
raise SASLCancelled(self.sasl, self)
|
|
||||||
if not self.sasl.tls_active():
|
|
||||||
if not self.sasl.sec_query(self, '-ENCRYPTION, SCRAM'):
|
|
||||||
raise SASLCancelled(self.sasl, self)
|
|
||||||
|
|
||||||
self._step = 0
|
|
||||||
self._rspauth = False
|
|
||||||
|
|
||||||
def HMAC(self, key, msg):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
return hmac.HMAC(key=key, msg=msg, digestmod=self.hash).digest()
|
|
||||||
|
|
||||||
def Hi(self, text, salt, iterations):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
text = bytes(text)
|
|
||||||
ui_1 = self.HMAC(text, salt + b'\0\0\0\01')
|
|
||||||
ui = ui_1
|
|
||||||
for i in range(iterations - 1):
|
|
||||||
ui_1 = self.HMAC(text, ui_1)
|
|
||||||
ui = XOR(ui, ui_1)
|
|
||||||
return ui
|
|
||||||
|
|
||||||
def H(self, text):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
return self.hash(text).digest()
|
|
||||||
|
|
||||||
def prep(self):
|
|
||||||
if 'password' in self.values:
|
|
||||||
del self.values['password']
|
|
||||||
|
|
||||||
def process(self, challenge=None):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
steps = {
|
|
||||||
0: self.process_one,
|
|
||||||
1: self.process_two,
|
|
||||||
2: self.process_three
|
|
||||||
}
|
|
||||||
return steps[self._step](challenge)
|
|
||||||
|
|
||||||
def process_one(self, challenge):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
vitals = ['username']
|
|
||||||
if 'SaltedPassword' not in self.values:
|
|
||||||
vitals.append('password')
|
|
||||||
if 'Iterations' not in self.values:
|
|
||||||
vitals.append('password')
|
|
||||||
|
|
||||||
self.check_values(vitals)
|
|
||||||
|
|
||||||
username = bytes(self.values['username'])
|
|
||||||
|
|
||||||
self._step = 1
|
|
||||||
self._cnonce = bytes(('%s' % random.random())[2:])
|
|
||||||
self._soup = b'n=' + username + b',r=' + self._cnonce
|
|
||||||
self._gs2header = b''
|
|
||||||
|
|
||||||
if not self.sasl.tls_active():
|
|
||||||
if self._cb:
|
|
||||||
self._gs2header = b'p=tls-unique,,'
|
|
||||||
else:
|
|
||||||
self._gs2header = b'y,,'
|
|
||||||
else:
|
|
||||||
self._gs2header = b'n,,'
|
|
||||||
|
|
||||||
return self._gs2header + self._soup
|
|
||||||
|
|
||||||
def process_two(self, challenge):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
data = parse_challenge(challenge)
|
|
||||||
|
|
||||||
self._step = 2
|
|
||||||
self._soup += b',' + challenge + b','
|
|
||||||
self._nonce = data[b'r']
|
|
||||||
self._salt = b64decode(data[b's'])
|
|
||||||
self._iter = int(data[b'i'])
|
|
||||||
|
|
||||||
if self._nonce[:len(self._cnonce)] != self._cnonce:
|
|
||||||
raise SASLCancelled(self.sasl, self)
|
|
||||||
|
|
||||||
cbdata = self.sasl.tls_active()
|
|
||||||
c = self._gs2header
|
|
||||||
if not cbdata and self._cb:
|
|
||||||
c += None
|
|
||||||
|
|
||||||
r = b'c=' + b64encode(c).replace(b'\n', b'')
|
|
||||||
r += b',r=' + self._nonce
|
|
||||||
self._soup += r
|
|
||||||
|
|
||||||
if 'Iterations' in self.values:
|
|
||||||
if self.values['Iterations'] != self._iter:
|
|
||||||
if 'SaltedPassword' in self.values:
|
|
||||||
del self.values['SaltedPassword']
|
|
||||||
if 'Salt' in self.values:
|
|
||||||
if self.values['Salt'] != self._salt:
|
|
||||||
if 'SaltedPassword' in self.values:
|
|
||||||
del self.values['SaltedPassword']
|
|
||||||
|
|
||||||
self.values['Iterations'] = self._iter
|
|
||||||
self.values['Salt'] = self._salt
|
|
||||||
|
|
||||||
if 'SaltedPassword' not in self.values:
|
|
||||||
self.check_values(['password'])
|
|
||||||
password = bytes(self.values['password'])
|
|
||||||
salted_pass = self.Hi(password, self._salt, self._iter)
|
|
||||||
self.values['SaltedPassword'] = salted_pass
|
|
||||||
|
|
||||||
salted_pass = self.values['SaltedPassword']
|
|
||||||
client_key = self.HMAC(salted_pass, b'Client Key')
|
|
||||||
stored_key = self.H(client_key)
|
|
||||||
client_sig = self.HMAC(stored_key, self._soup)
|
|
||||||
client_proof = XOR(client_key, client_sig)
|
|
||||||
r += b',p=' + b64encode(client_proof).replace(b'\n', b'')
|
|
||||||
server_key = self.HMAC(self.values['SaltedPassword'], b'Server Key')
|
|
||||||
self.server_sig = self.HMAC(server_key, self._soup)
|
|
||||||
return r
|
|
||||||
|
|
||||||
def process_three(self, challenge=None):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
data = parse_challenge(challenge)
|
|
||||||
if b64decode(data[b'v']) == self.server_sig:
|
|
||||||
self._rspauth = True
|
|
||||||
|
|
||||||
def okay(self):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
return self._rspauth
|
|
||||||
|
|
||||||
def get_user(self):
|
|
||||||
return self.values['username']
|
|
||||||
|
|
||||||
|
|
||||||
register_mechanism('SCRAM-', 60, SCRAM_HMAC)
|
|
||||||
register_mechanism('SCRAM-', 70, SCRAM_HMAC, extra='-PLUS')
|
|
402
sleekxmpp/thirdparty/suelta/sasl.py
vendored
402
sleekxmpp/thirdparty/suelta/sasl.py
vendored
@ -1,402 +0,0 @@
|
|||||||
from sleekxmpp.thirdparty.suelta.util import hashes
|
|
||||||
from sleekxmpp.thirdparty.suelta.saslprep import saslprep
|
|
||||||
|
|
||||||
#: Global session storage for user answers to requested mechanism values
|
|
||||||
#: and security questions. This allows the user's preferences to be
|
|
||||||
#: persisted across multiple SASL authentication attempts made by the
|
|
||||||
#: same process.
|
|
||||||
SESSION = {'answers': {},
|
|
||||||
'passwords': {},
|
|
||||||
'sec_queries': {},
|
|
||||||
'stash': {},
|
|
||||||
'stash_file': ''}
|
|
||||||
|
|
||||||
#: Global registry mapping mechanism names to implementation classes.
|
|
||||||
MECHANISMS = {}
|
|
||||||
|
|
||||||
#: Global registry mapping mechanism names to security scores.
|
|
||||||
MECH_SEC_SCORES = {}
|
|
||||||
|
|
||||||
|
|
||||||
def register_mechanism(basename, basescore, impl, extra=None, use_hashes=True):
|
|
||||||
"""
|
|
||||||
Add a SASL mechanism to the registry of available mechanisms.
|
|
||||||
|
|
||||||
:param basename: The base name of the mechanism type, such as ``CRAM-``.
|
|
||||||
:param basescore: The base security score for this type of mechanism.
|
|
||||||
:param impl: The class implementing the mechanism.
|
|
||||||
:param extra: Any additional qualifiers to the mechanism name,
|
|
||||||
such as ``-PLUS``.
|
|
||||||
:param use_hashes: If ``True``, then register the mechanism for use with
|
|
||||||
all available hashes.
|
|
||||||
"""
|
|
||||||
n = 0
|
|
||||||
if use_hashes:
|
|
||||||
for hashing_alg in hashes():
|
|
||||||
n += 1
|
|
||||||
name = basename + hashing_alg
|
|
||||||
if extra is not None:
|
|
||||||
name += extra
|
|
||||||
MECHANISMS[name] = impl
|
|
||||||
MECH_SEC_SCORES[name] = basescore + n
|
|
||||||
else:
|
|
||||||
MECHANISMS[basename] = impl
|
|
||||||
MECH_SEC_SCORES[basename] = basescore
|
|
||||||
|
|
||||||
|
|
||||||
def set_stash_file(filename):
|
|
||||||
"""
|
|
||||||
Enable or disable storing the stash to disk.
|
|
||||||
|
|
||||||
If the filename is ``None``, then disable using a stash file.
|
|
||||||
|
|
||||||
:param filename: The path to the file to store the stash data.
|
|
||||||
"""
|
|
||||||
SESSION['stash_file'] = filename
|
|
||||||
try:
|
|
||||||
import marshal
|
|
||||||
stash_file = file(filename)
|
|
||||||
SESSION['stash'] = marshal.load(stash_file)
|
|
||||||
except:
|
|
||||||
SESSION['stash'] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def sec_query_allow(mech, query):
|
|
||||||
"""
|
|
||||||
Quick default to allow all feature combinations which could
|
|
||||||
negatively affect security.
|
|
||||||
|
|
||||||
:param mech: The chosen SASL mechanism
|
|
||||||
:param query: An encoding of the combination of enabled and
|
|
||||||
disabled features which may affect security.
|
|
||||||
|
|
||||||
:returns: ``True``
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class SASL(object):
|
|
||||||
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, host, service, mech=None, username=None,
|
|
||||||
min_sec=0, request_values=None, sec_query=None,
|
|
||||||
tls_active=None, def_realm=None):
|
|
||||||
"""
|
|
||||||
:param string host: The host of the service requiring authentication.
|
|
||||||
:param string service: The name of the underlying protocol in use.
|
|
||||||
:param string mech: Optional name of the SASL mechanism to use.
|
|
||||||
If given, only this mechanism may be used for
|
|
||||||
authentication.
|
|
||||||
:param string username: The username to use when authenticating.
|
|
||||||
:param request_values: Reference to a function for supplying
|
|
||||||
values requested by mechanisms, such
|
|
||||||
as passwords. (See above)
|
|
||||||
:param sec_query: Reference to a function for approving or
|
|
||||||
denying feature combinations which could
|
|
||||||
negatively impact security. (See above)
|
|
||||||
:param tls_active: Function for indicating if TLS has been
|
|
||||||
negotiated. (See above)
|
|
||||||
:param integer min_sec: The minimum security level accepted. This
|
|
||||||
only allows for SASL mechanisms whose
|
|
||||||
security rating is greater than `min_sec`.
|
|
||||||
:param string def_realm: The default realm, if different than `host`.
|
|
||||||
|
|
||||||
:type request_values: :func:`request_values`
|
|
||||||
:type sec_query: :func:`sec_query`
|
|
||||||
:type tls_active: :func:`tls_active`
|
|
||||||
"""
|
|
||||||
self.host = host
|
|
||||||
self.def_realm = def_realm or host
|
|
||||||
self.service = service
|
|
||||||
self.user = username
|
|
||||||
self.mech = mech
|
|
||||||
self.min_sec = min_sec - 1
|
|
||||||
|
|
||||||
self.request_values = request_values
|
|
||||||
self._sec_query = sec_query
|
|
||||||
if tls_active is not None:
|
|
||||||
self.tls_active = tls_active
|
|
||||||
else:
|
|
||||||
self.tls_active = lambda: False
|
|
||||||
|
|
||||||
self.try_username = self.user
|
|
||||||
self.try_password = None
|
|
||||||
|
|
||||||
self.stash_id = None
|
|
||||||
self.testkey = None
|
|
||||||
|
|
||||||
def reset_stash_id(self, username):
|
|
||||||
"""
|
|
||||||
Reset the ID for the stash for persisting user data.
|
|
||||||
|
|
||||||
:param username: The username to base the new ID on.
|
|
||||||
"""
|
|
||||||
username = saslprep(username)
|
|
||||||
self.user = username
|
|
||||||
self.try_username = self.user
|
|
||||||
self.testkey = [self.user, self.host, self.service]
|
|
||||||
self.stash_id = '\0'.join(self.testkey)
|
|
||||||
|
|
||||||
def sec_query(self, mech, query):
|
|
||||||
"""
|
|
||||||
Request authorization from the user to use a combination
|
|
||||||
of features which could negatively affect security.
|
|
||||||
|
|
||||||
The ``sec_query`` callback when creating the SASL object will
|
|
||||||
be called if the query has not been answered before. Otherwise,
|
|
||||||
the query response will be pulled from ``SESSION['sec_queries']``.
|
|
||||||
|
|
||||||
If no ``sec_query`` callback was provided, then all queries
|
|
||||||
will be denied.
|
|
||||||
|
|
||||||
:param mech: The chosen SASL mechanism
|
|
||||||
:param query: An encoding of the combination of enabled and
|
|
||||||
disabled features which may affect security.
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
if self._sec_query is None:
|
|
||||||
return False
|
|
||||||
if query in SESSION['sec_queries']:
|
|
||||||
return SESSION['sec_queries'][query]
|
|
||||||
resp = self._sec_query(mech, query)
|
|
||||||
if resp:
|
|
||||||
SESSION['sec_queries'][query] = resp
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def find_password(self, mech):
|
|
||||||
"""
|
|
||||||
Find and return the user's password, if it has been entered before
|
|
||||||
during this session.
|
|
||||||
|
|
||||||
:param mech: The chosen SASL mechanism.
|
|
||||||
"""
|
|
||||||
if self.try_password is not None:
|
|
||||||
return self.try_password
|
|
||||||
if self.testkey is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
testkey = self.testkey[:]
|
|
||||||
lockout = 1
|
|
||||||
|
|
||||||
def find_username(self):
|
|
||||||
"""Find and return user's username if known."""
|
|
||||||
return self.try_username
|
|
||||||
|
|
||||||
def success(self, mech):
|
|
||||||
mech.preprep()
|
|
||||||
if 'password' in mech.values:
|
|
||||||
testkey = self.testkey[:]
|
|
||||||
while len(testkey):
|
|
||||||
tk = '\0'.join(testkey)
|
|
||||||
if tk in SESSION['passwords']:
|
|
||||||
break
|
|
||||||
SESSION['passwords'][tk] = mech.values['password']
|
|
||||||
testkey = testkey[:-1]
|
|
||||||
mech.prep()
|
|
||||||
mech.save_values()
|
|
||||||
|
|
||||||
def failure(self, mech):
|
|
||||||
mech.clear()
|
|
||||||
self.testkey = self.testkey[:-1]
|
|
||||||
|
|
||||||
def choose_mechanism(self, mechs, force_plain=False):
|
|
||||||
"""
|
|
||||||
Choose the most secure mechanism from a list of mechanisms.
|
|
||||||
|
|
||||||
If ``force_plain`` is given, return the ``PLAIN`` mechanism.
|
|
||||||
|
|
||||||
:param mechs: A list of mechanism names.
|
|
||||||
:param force_plain: If ``True``, force the selection of the
|
|
||||||
``PLAIN`` mechanism.
|
|
||||||
:returns: A SASL mechanism object, or ``None`` if no mechanism
|
|
||||||
could be selected.
|
|
||||||
"""
|
|
||||||
# Handle selection of PLAIN and ANONYMOUS
|
|
||||||
if force_plain:
|
|
||||||
return MECHANISMS['PLAIN'](self, 'PLAIN')
|
|
||||||
|
|
||||||
if self.user is not None:
|
|
||||||
requested_mech = '*' if self.mech is None else self.mech
|
|
||||||
else:
|
|
||||||
if self.mech is None:
|
|
||||||
requested_mech = 'ANONYMOUS'
|
|
||||||
else:
|
|
||||||
requested_mech = self.mech
|
|
||||||
if requested_mech == '*' and self.user in ['', 'anonymous', None]:
|
|
||||||
requested_mech = 'ANONYMOUS'
|
|
||||||
|
|
||||||
# If a specific mechanism was requested, try it
|
|
||||||
if requested_mech != '*':
|
|
||||||
if requested_mech in MECHANISMS and \
|
|
||||||
requested_mech in MECH_SEC_SCORES:
|
|
||||||
return MECHANISMS[requested_mech](self, requested_mech)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Pick the best mechanism based on its security score
|
|
||||||
best_score = self.min_sec
|
|
||||||
best_mech = None
|
|
||||||
for name in mechs:
|
|
||||||
if name in MECH_SEC_SCORES:
|
|
||||||
if MECH_SEC_SCORES[name] > best_score:
|
|
||||||
best_score = MECH_SEC_SCORES[name]
|
|
||||||
best_mech = name
|
|
||||||
if best_mech is not None:
|
|
||||||
best_mech = MECHANISMS[best_mech](self, best_mech)
|
|
||||||
|
|
||||||
return best_mech
|
|
||||||
|
|
||||||
|
|
||||||
class Mechanism(object):
|
|
||||||
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, sasl, name, version=0, use_stash=True):
|
|
||||||
self.name = name
|
|
||||||
self.sasl = sasl
|
|
||||||
self.use_stash = use_stash
|
|
||||||
|
|
||||||
self.encoding = False
|
|
||||||
self.values = {}
|
|
||||||
|
|
||||||
if use_stash:
|
|
||||||
self.load_values()
|
|
||||||
|
|
||||||
def load_values(self):
|
|
||||||
"""Retrieve user data from the stash."""
|
|
||||||
self.values = {}
|
|
||||||
if not self.use_stash:
|
|
||||||
return False
|
|
||||||
if self.sasl.stash_id is not None:
|
|
||||||
if self.sasl.stash_id in SESSION['stash']:
|
|
||||||
if SESSION['stash'][self.sasl.stash_id]['mech'] == self.name:
|
|
||||||
values = SESSION['stash'][self.sasl.stash_id]['values']
|
|
||||||
self.values.update(values)
|
|
||||||
if self.sasl.user is not None:
|
|
||||||
if not self.has_values(['username']):
|
|
||||||
self.values['username'] = self.sasl.user
|
|
||||||
return None
|
|
||||||
|
|
||||||
def save_values(self):
|
|
||||||
"""
|
|
||||||
Save user data to the session stash.
|
|
||||||
|
|
||||||
If a stash file name has been set using ``SESSION['stash_file']``,
|
|
||||||
the saved values will be persisted to disk.
|
|
||||||
"""
|
|
||||||
if not self.use_stash:
|
|
||||||
return False
|
|
||||||
if self.sasl.stash_id is not None:
|
|
||||||
if self.sasl.stash_id not in SESSION['stash']:
|
|
||||||
SESSION['stash'][self.sasl.stash_id] = {}
|
|
||||||
SESSION['stash'][self.sasl.stash_id]['values'] = self.values
|
|
||||||
SESSION['stash'][self.sasl.stash_id]['mech'] = self.name
|
|
||||||
if SESSION['stash_file'] not in ['', None]:
|
|
||||||
import marshal
|
|
||||||
stash_file = file(SESSION['stash_file'], 'wb')
|
|
||||||
marshal.dump(SESSION['stash'], stash_file)
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""Reset all user data, except the username."""
|
|
||||||
username = None
|
|
||||||
if 'username' in self.values:
|
|
||||||
username = self.values['username']
|
|
||||||
self.values = {}
|
|
||||||
if username is not None:
|
|
||||||
self.values['username'] = username
|
|
||||||
self.save_values()
|
|
||||||
self.values = {}
|
|
||||||
self.load_values()
|
|
||||||
|
|
||||||
def okay(self):
|
|
||||||
"""
|
|
||||||
Indicate if mutual authentication has completed successfully.
|
|
||||||
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def preprep(self):
|
|
||||||
"""Ensure that the stash ID has been set before processing."""
|
|
||||||
if self.sasl.stash_id is None:
|
|
||||||
if 'username' in self.values:
|
|
||||||
self.sasl.reset_stash_id(self.values['username'])
|
|
||||||
|
|
||||||
def prep(self):
|
|
||||||
"""
|
|
||||||
Prepare stored values for processing.
|
|
||||||
|
|
||||||
For example, by removing extra copies of passwords from memory.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def process(self, challenge=None):
|
|
||||||
"""
|
|
||||||
Process a challenge request and return the response.
|
|
||||||
|
|
||||||
:param challenge: A challenge issued by the server that
|
|
||||||
must be answered for authentication.
|
|
||||||
"""
|
|
||||||
raise NotImplemented
|
|
||||||
|
|
||||||
def fulfill(self, values):
|
|
||||||
"""
|
|
||||||
Provide requested values to the mechanism.
|
|
||||||
|
|
||||||
:param values: A dictionary of requested values.
|
|
||||||
"""
|
|
||||||
if 'password' in values:
|
|
||||||
values['password'] = saslprep(values['password'])
|
|
||||||
self.values.update(values)
|
|
||||||
|
|
||||||
def missing_values(self, keys):
|
|
||||||
"""
|
|
||||||
Return a dictionary of value names that have not been given values
|
|
||||||
by the user, or retrieved from the stash.
|
|
||||||
|
|
||||||
:param keys: A list of value names to check.
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
vals = {}
|
|
||||||
for name in keys:
|
|
||||||
if name not in self.values or self.values[name] is None:
|
|
||||||
if self.use_stash:
|
|
||||||
if name == 'username':
|
|
||||||
value = self.sasl.find_username()
|
|
||||||
if value is not None:
|
|
||||||
self.sasl.reset_stash_id(value)
|
|
||||||
self.values[name] = value
|
|
||||||
break
|
|
||||||
if name == 'password':
|
|
||||||
value = self.sasl.find_password(self)
|
|
||||||
if value is not None:
|
|
||||||
self.values[name] = value
|
|
||||||
break
|
|
||||||
vals[name] = None
|
|
||||||
return vals
|
|
||||||
|
|
||||||
def has_values(self, keys):
|
|
||||||
"""
|
|
||||||
Check that the given values have been retrieved from the user,
|
|
||||||
or from the stash.
|
|
||||||
|
|
||||||
:param keys: A list of value names to check.
|
|
||||||
"""
|
|
||||||
return len(self.missing_values(keys)) == 0
|
|
||||||
|
|
||||||
def check_values(self, keys):
|
|
||||||
"""
|
|
||||||
Request missing values from the user.
|
|
||||||
|
|
||||||
:param keys: A list of value names to request, if missing.
|
|
||||||
"""
|
|
||||||
vals = self.missing_values(keys)
|
|
||||||
if vals:
|
|
||||||
self.sasl.request_values(self, vals)
|
|
||||||
|
|
||||||
def get_user(self):
|
|
||||||
"""Return the username usd for this mechanism."""
|
|
||||||
return self.values['username']
|
|
81
sleekxmpp/thirdparty/suelta/saslprep.py
vendored
81
sleekxmpp/thirdparty/suelta/saslprep.py
vendored
@ -1,81 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import stringprep
|
|
||||||
import unicodedata
|
|
||||||
|
|
||||||
|
|
||||||
from sleekxmpp.thirdparty.suelta.exceptions import SASLPrepFailure
|
|
||||||
|
|
||||||
|
|
||||||
def saslprep(text, strict=True):
|
|
||||||
"""
|
|
||||||
Return a processed version of the given string, using the SASLPrep
|
|
||||||
profile of stringprep.
|
|
||||||
|
|
||||||
:param text: The string to process, in UTF-8.
|
|
||||||
:param strict: If ``True``, prevent the use of unassigned code points.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
if type(text) == str:
|
|
||||||
text = text.decode('utf-8')
|
|
||||||
|
|
||||||
# Mapping:
|
|
||||||
#
|
|
||||||
# - non-ASCII space characters [StringPrep, C.1.2] that can be
|
|
||||||
# mapped to SPACE (U+0020), and
|
|
||||||
#
|
|
||||||
# - the 'commonly mapped to nothing' characters [StringPrep, B.1]
|
|
||||||
# that can be mapped to nothing.
|
|
||||||
buffer = ''
|
|
||||||
for char in text:
|
|
||||||
if stringprep.in_table_c12(char):
|
|
||||||
buffer += ' '
|
|
||||||
elif not stringprep.in_table_b1(char):
|
|
||||||
buffer += char
|
|
||||||
|
|
||||||
# Normalization using form KC
|
|
||||||
text = unicodedata.normalize('NFKC', buffer)
|
|
||||||
|
|
||||||
# Check for bidirectional string
|
|
||||||
buffer = ''
|
|
||||||
first_is_randal = False
|
|
||||||
if text:
|
|
||||||
first_is_randal = stringprep.in_table_d1(text[0])
|
|
||||||
if first_is_randal and not stringprep.in_table_d1(text[-1]):
|
|
||||||
raise SASLPrepFailure('Section 6.3 [end]')
|
|
||||||
|
|
||||||
# Check for prohibited characters
|
|
||||||
for x in range(len(text)):
|
|
||||||
if strict and stringprep.in_table_a1(text[x]):
|
|
||||||
raise SASLPrepFailure('Unassigned Codepoint')
|
|
||||||
if stringprep.in_table_c12(text[x]):
|
|
||||||
raise SASLPrepFailure('In table C.1.2')
|
|
||||||
if stringprep.in_table_c21(text[x]):
|
|
||||||
raise SASLPrepFailure('In table C.2.1')
|
|
||||||
if stringprep.in_table_c22(text[x]):
|
|
||||||
raise SASLPrepFailure('In table C.2.2')
|
|
||||||
if stringprep.in_table_c3(text[x]):
|
|
||||||
raise SASLPrepFailure('In table C.3')
|
|
||||||
if stringprep.in_table_c4(text[x]):
|
|
||||||
raise SASLPrepFailure('In table C.4')
|
|
||||||
if stringprep.in_table_c5(text[x]):
|
|
||||||
raise SASLPrepFailure('In table C.5')
|
|
||||||
if stringprep.in_table_c6(text[x]):
|
|
||||||
raise SASLPrepFailure('In table C.6')
|
|
||||||
if stringprep.in_table_c7(text[x]):
|
|
||||||
raise SASLPrepFailure('In table C.7')
|
|
||||||
if stringprep.in_table_c8(text[x]):
|
|
||||||
raise SASLPrepFailure('In table C.8')
|
|
||||||
if stringprep.in_table_c9(text[x]):
|
|
||||||
raise SASLPrepFailure('In table C.9')
|
|
||||||
if x:
|
|
||||||
if first_is_randal and stringprep.in_table_d2(text[x]):
|
|
||||||
raise SASLPrepFailure('Section 6.2')
|
|
||||||
if not first_is_randal and \
|
|
||||||
x != len(text) - 1 and \
|
|
||||||
stringprep.in_table_d1(text[x]):
|
|
||||||
raise SASLPrepFailure('Section 6.3')
|
|
||||||
|
|
||||||
return text
|
|
@ -10,6 +10,10 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from sleekxmpp.util.misc_ops import bytes, unicode, hashes, hash, \
|
||||||
|
num_to_bytes, bytes_to_num, quote, XOR
|
||||||
|
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
# Standardize import of Queue class:
|
# Standardize import of Queue class:
|
||||||
|
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
"""
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
|
||||||
|
def unicode(text):
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
import __builtin__
|
||||||
|
return __builtin__.unicode(text)
|
||||||
|
return str(text)
|
||||||
|
|
||||||
|
|
||||||
def bytes(text):
|
def bytes(text):
|
||||||
"""
|
"""
|
||||||
Convert Unicode text to UTF-8 encoded bytes.
|
Convert Unicode text to UTF-8 encoded bytes.
|
||||||
@ -15,9 +19,6 @@ def bytes(text):
|
|||||||
:param text: Unicode text to convert to bytes
|
:param text: Unicode text to convert to bytes
|
||||||
:rtype: bytes (Python3), str (Python2.6+)
|
:rtype: bytes (Python3), str (Python2.6+)
|
||||||
"""
|
"""
|
||||||
if text is None:
|
|
||||||
return b''
|
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
import __builtin__
|
import __builtin__
|
||||||
return __builtin__.bytes(text)
|
return __builtin__.bytes(text)
|
15
sleekxmpp/util/sasl/__init__.py
Normal file
15
sleekxmpp/util/sasl/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
sleekxmpp.util.sasl
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module was originally based on Dave Cridland's Suelta library.
|
||||||
|
|
||||||
|
Part of SleekXMPP: The Sleek XMPP Library
|
||||||
|
|
||||||
|
:copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
:license: MIT, see LICENSE for more details
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.util.sasl.client import *
|
||||||
|
from sleekxmpp.util.sasl.mechanisms import *
|
168
sleekxmpp/util/sasl/client.py
Normal file
168
sleekxmpp/util/sasl/client.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
sleekxmpp.util.sasl.client
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module was originally based on Dave Cridland's Suelta library.
|
||||||
|
|
||||||
|
Part of SleekXMPP: The Sleek XMPP Library
|
||||||
|
|
||||||
|
:copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
:license: MIT, see LICENSE for more details
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import stringprep
|
||||||
|
|
||||||
|
from sleekxmpp.util import hashes, bytes, stringprep_profiles
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
#: Global registry mapping mechanism names to implementation classes.
|
||||||
|
MECHANISMS = {}
|
||||||
|
|
||||||
|
|
||||||
|
#: Global registry mapping mechanism names to security scores.
|
||||||
|
MECH_SEC_SCORES = {}
|
||||||
|
|
||||||
|
|
||||||
|
#: The SASLprep profile of stringprep used to validate simple username
|
||||||
|
#: and password credentials.
|
||||||
|
saslprep = stringprep_profiles.create(
|
||||||
|
nfkc=True,
|
||||||
|
bidi=True,
|
||||||
|
mappings=[
|
||||||
|
stringprep_profiles.b1_mapping,
|
||||||
|
stringprep_profiles.c12_mapping],
|
||||||
|
prohibited=[
|
||||||
|
stringprep.in_table_c12,
|
||||||
|
stringprep.in_table_c21,
|
||||||
|
stringprep.in_table_c22,
|
||||||
|
stringprep.in_table_c3,
|
||||||
|
stringprep.in_table_c4,
|
||||||
|
stringprep.in_table_c5,
|
||||||
|
stringprep.in_table_c6,
|
||||||
|
stringprep.in_table_c7,
|
||||||
|
stringprep.in_table_c8,
|
||||||
|
stringprep.in_table_c9],
|
||||||
|
unassigned=[stringprep.in_table_a1])
|
||||||
|
|
||||||
|
|
||||||
|
def sasl_mech(score):
|
||||||
|
sec_score = score
|
||||||
|
def register(mech):
|
||||||
|
n = 0
|
||||||
|
mech.score = sec_score
|
||||||
|
if mech.use_hashes:
|
||||||
|
for hashing_alg in hashes():
|
||||||
|
n += 1
|
||||||
|
score = mech.score + n
|
||||||
|
name = '%s-%s' % (mech.name, hashing_alg)
|
||||||
|
MECHANISMS[name] = mech
|
||||||
|
MECH_SEC_SCORES[name] = score
|
||||||
|
|
||||||
|
if mech.channel_binding:
|
||||||
|
name += '-PLUS'
|
||||||
|
score += 10
|
||||||
|
MECHANISMS[name] = mech
|
||||||
|
MECH_SEC_SCORES[name] = score
|
||||||
|
else:
|
||||||
|
MECHANISMS[mech.name] = mech
|
||||||
|
MECH_SEC_SCORES[mech.name] = mech.score
|
||||||
|
if mech.channel_binding:
|
||||||
|
MECHANISMS[mech.name + '-PLUS'] = mech
|
||||||
|
MECH_SEC_SCORES[name] = mech.score + 10
|
||||||
|
return mech
|
||||||
|
return register
|
||||||
|
|
||||||
|
|
||||||
|
class SASLNoAppropriateMechanism(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SASLCancelled(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SASLFailed(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SASLMutualAuthFailed(SASLFailed):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Mech(object):
|
||||||
|
|
||||||
|
name = 'GENERIC'
|
||||||
|
score = -1
|
||||||
|
use_hashes = False
|
||||||
|
channel_binding = False
|
||||||
|
required_credentials = set()
|
||||||
|
optional_credentials = set()
|
||||||
|
security = set()
|
||||||
|
|
||||||
|
def __init__(self, name, credentials, security_settings):
|
||||||
|
self.credentials = credentials
|
||||||
|
self.security_settings = security_settings
|
||||||
|
self.values = {}
|
||||||
|
self.base_name = self.name
|
||||||
|
self.name = name
|
||||||
|
self.setup(name)
|
||||||
|
|
||||||
|
def setup(self, name):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
return b''
|
||||||
|
|
||||||
|
|
||||||
|
def choose(mech_list, credentials, security_settings, limit=None, min_mech=None):
|
||||||
|
available_mechs = set(MECHANISMS.keys())
|
||||||
|
if limit is None:
|
||||||
|
limit = set(mech_list)
|
||||||
|
if not isinstance(limit, set):
|
||||||
|
limit = set(limit)
|
||||||
|
if not isinstance(mech_list, set):
|
||||||
|
mech_list = set(mech_list)
|
||||||
|
|
||||||
|
mech_list = mech_list.intersection(limit)
|
||||||
|
available_mechs = available_mechs.intersection(mech_list)
|
||||||
|
|
||||||
|
best_score = MECH_SEC_SCORES.get(min_mech, -1)
|
||||||
|
best_mech = None
|
||||||
|
for name in available_mechs:
|
||||||
|
if name in MECH_SEC_SCORES:
|
||||||
|
if MECH_SEC_SCORES[name] > best_score:
|
||||||
|
best_score = MECH_SEC_SCORES[name]
|
||||||
|
best_mech = name
|
||||||
|
if best_mech is None:
|
||||||
|
raise SASLNoAppropriateMechanism()
|
||||||
|
|
||||||
|
mech_class = MECHANISMS[best_mech]
|
||||||
|
|
||||||
|
try:
|
||||||
|
creds = credentials(mech_class.required_credentials,
|
||||||
|
mech_class.optional_credentials)
|
||||||
|
for req in mech_class.required_credentials:
|
||||||
|
if req not in creds:
|
||||||
|
raise SASLCancelled('Missing credential: %s' % req)
|
||||||
|
for opt in mech_class.optional_credentials:
|
||||||
|
if opt not in creds:
|
||||||
|
creds[opt] = b''
|
||||||
|
for cred in creds:
|
||||||
|
if cred in ('username', 'password', 'authzid'):
|
||||||
|
creds[cred] = bytes(saslprep(creds[cred]))
|
||||||
|
else:
|
||||||
|
creds[cred] = bytes(creds[cred])
|
||||||
|
security_opts = security_settings(mech_class.security)
|
||||||
|
|
||||||
|
return mech_class(best_mech, creds, security_opts)
|
||||||
|
except SASLCancelled as e:
|
||||||
|
log.info('SASL: %s: %s', best_mech, e.message)
|
||||||
|
mech_list.remove(best_mech)
|
||||||
|
return choose(mech_list, credentials, security_settings,
|
||||||
|
limit=limit,
|
||||||
|
min_mech=min_mech)
|
531
sleekxmpp/util/sasl/mechanisms.py
Normal file
531
sleekxmpp/util/sasl/mechanisms.py
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
sleekxmpp.util.sasl.mechanisms
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A collection of supported SASL mechanisms.
|
||||||
|
|
||||||
|
This module was originally based on Dave Cridland's Suelta library.
|
||||||
|
|
||||||
|
Part of SleekXMPP: The Sleek XMPP Library
|
||||||
|
|
||||||
|
:copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
:license: MIT, see LICENSE for more details
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import hmac
|
||||||
|
import random
|
||||||
|
|
||||||
|
from base64 import b64encode, b64decode
|
||||||
|
|
||||||
|
from sleekxmpp.util import bytes, hash, XOR, quote, num_to_bytes
|
||||||
|
from sleekxmpp.util.sasl.client import sasl_mech, Mech, \
|
||||||
|
SASLCancelled, SASLFailed
|
||||||
|
|
||||||
|
|
||||||
|
@sasl_mech(0)
|
||||||
|
class ANONYMOUS(Mech):
|
||||||
|
|
||||||
|
name = 'ANONYMOUS'
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
return b'Anonymous, Suelta'
|
||||||
|
|
||||||
|
|
||||||
|
@sasl_mech(1)
|
||||||
|
class LOGIN(Mech):
|
||||||
|
|
||||||
|
name = 'LOGIN'
|
||||||
|
required_credentials = set(['username', 'password'])
|
||||||
|
|
||||||
|
def setup(self, name):
|
||||||
|
self.step = 0
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
if not challenge:
|
||||||
|
return b''
|
||||||
|
|
||||||
|
if self.step == 0:
|
||||||
|
self.step = 1
|
||||||
|
return self.credentials['username']
|
||||||
|
else:
|
||||||
|
return self.credentials['password']
|
||||||
|
|
||||||
|
|
||||||
|
@sasl_mech(2)
|
||||||
|
class PLAIN(Mech):
|
||||||
|
|
||||||
|
name = 'PLAIN'
|
||||||
|
required_credentials = set(['username', 'password'])
|
||||||
|
optional_credentials = set(['authzid'])
|
||||||
|
security = set(['encrypted', 'encrypted_plain', 'unencrypted_plain'])
|
||||||
|
|
||||||
|
def setup(self, name):
|
||||||
|
if not self.security_settings['encrypted']:
|
||||||
|
if not self.security_settings['unencrypted_plain']:
|
||||||
|
raise SASLCancelled('PLAIN without encryption')
|
||||||
|
else:
|
||||||
|
if not self.security_settings['encrypted_plain']:
|
||||||
|
raise SASLCancelled('PLAIN with encryption')
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
authzid = self.credentials['authzid']
|
||||||
|
authcid = self.credentials['username']
|
||||||
|
password = self.credentials['password']
|
||||||
|
return authzid + b'\x00' + authcid + b'\x00' + password
|
||||||
|
|
||||||
|
|
||||||
|
@sasl_mech(100)
|
||||||
|
class EXTERNAL(Mech):
|
||||||
|
|
||||||
|
name = 'EXTERNAL'
|
||||||
|
optional_credentials = set(['authzid'])
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
return self.credentials['authzid']
|
||||||
|
|
||||||
|
|
||||||
|
@sasl_mech(30)
|
||||||
|
class X_FACEBOOK_PLATFORM(Mech):
|
||||||
|
|
||||||
|
name = 'X-FACEBOOK-PLATFORM'
|
||||||
|
required_credentials = set(['api_key', 'access_token'])
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
if challenge:
|
||||||
|
values = {}
|
||||||
|
for kv in challenge.split(b'&'):
|
||||||
|
key, value = kv.split(b'=')
|
||||||
|
values[key] = value
|
||||||
|
|
||||||
|
resp_data = {
|
||||||
|
b'method': values[b'method'],
|
||||||
|
b'v': b'1.0',
|
||||||
|
b'call_id': b'1.0',
|
||||||
|
b'nonce': values[b'nonce'],
|
||||||
|
b'access_token': self.credentials['access_token'],
|
||||||
|
b'api_key': self.credentials['api_key']
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = '&'.join(['%s=%s' % (k, v) for k, v in resp_data.items()])
|
||||||
|
return bytes(resp)
|
||||||
|
return b''
|
||||||
|
|
||||||
|
|
||||||
|
@sasl_mech(10)
|
||||||
|
class X_MESSENGER_OAUTH2(Mech):
|
||||||
|
|
||||||
|
name = 'X-MESSENGER-OAUTH2'
|
||||||
|
required_credentials = set(['access_token'])
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
return self.credentials['access_token']
|
||||||
|
|
||||||
|
|
||||||
|
@sasl_mech(3)
|
||||||
|
class X_GOOGLE_TOKEN(Mech):
|
||||||
|
|
||||||
|
name = 'X-GOOGLE-TOKEN'
|
||||||
|
required_credentials = set(['email', 'access_token'])
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
email = self.credentials['email']
|
||||||
|
token = self.credentials['access_token']
|
||||||
|
return b'\x00' + email + b'\x00' + token
|
||||||
|
|
||||||
|
|
||||||
|
@sasl_mech(20)
|
||||||
|
class CRAM(Mech):
|
||||||
|
|
||||||
|
name = 'CRAM'
|
||||||
|
use_hashes = True
|
||||||
|
required_credentials = set(['username', 'password'])
|
||||||
|
security = set(['encrypted', 'unencrypted_cram'])
|
||||||
|
|
||||||
|
def setup(self, name):
|
||||||
|
self.hash_name = name[5:]
|
||||||
|
self.hash = hash(self.hash_name)
|
||||||
|
if self.hash is None:
|
||||||
|
raise SASLCancelled('Unknown hash: %s' % self.hash_name)
|
||||||
|
if not self.security_settings['encrypted']:
|
||||||
|
if not self.security_settings['unencrypted_cram']:
|
||||||
|
raise SASLCancelled('Unecrypted CRAM-%s' % self.hash_name)
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
if not challenge:
|
||||||
|
return None
|
||||||
|
|
||||||
|
username = self.credentials['username']
|
||||||
|
password = self.credentials['password']
|
||||||
|
|
||||||
|
mac = hmac.HMAC(key=password, digestmod=self.hash)
|
||||||
|
mac.update(challenge)
|
||||||
|
|
||||||
|
return username + b' ' + bytes(mac.hexdigest())
|
||||||
|
|
||||||
|
|
||||||
|
@sasl_mech(60)
|
||||||
|
class SCRAM(Mech):
|
||||||
|
|
||||||
|
name = 'SCRAM'
|
||||||
|
use_hashes = True
|
||||||
|
channel_binding = True
|
||||||
|
required_credentials = set(['username', 'password'])
|
||||||
|
optional_credentials = set(['authzid', 'channel_binding'])
|
||||||
|
security = set(['encrypted', 'unencrypted_scram'])
|
||||||
|
|
||||||
|
def setup(self, name):
|
||||||
|
self.use_channel_binding = False
|
||||||
|
if name[-5:] == '-PLUS':
|
||||||
|
name = name[:-5]
|
||||||
|
self.use_channel_binding = True
|
||||||
|
|
||||||
|
self.hash_name = name[6:]
|
||||||
|
self.hash = hash(self.hash_name)
|
||||||
|
|
||||||
|
if self.hash is None:
|
||||||
|
raise SASLCancelled('Unknown hash: %s' % self.hash_name)
|
||||||
|
if not self.security_settings['encrypted']:
|
||||||
|
if not self.security_settings['unencrypted_scram']:
|
||||||
|
raise SASLCancelled('Unencrypted SCRAM')
|
||||||
|
|
||||||
|
self.step = 0
|
||||||
|
self._mutual_auth = False
|
||||||
|
|
||||||
|
def HMAC(self, key, msg):
|
||||||
|
return hmac.HMAC(key=key, msg=msg, digestmod=self.hash).digest()
|
||||||
|
|
||||||
|
def Hi(self, text, salt, iterations):
|
||||||
|
text = bytes(text)
|
||||||
|
ui1 = self.HMAC(text, salt + b'\0\0\0\01')
|
||||||
|
ui = ui1
|
||||||
|
for i in range(iterations - 1):
|
||||||
|
ui1 = self.HMAC(text, ui1)
|
||||||
|
ui = XOR(ui, ui1)
|
||||||
|
return ui
|
||||||
|
|
||||||
|
def H(self, text):
|
||||||
|
return self.hash(text).digest()
|
||||||
|
|
||||||
|
def saslname(self, value):
|
||||||
|
escaped = b''
|
||||||
|
for char in bytes(value):
|
||||||
|
if char == b',':
|
||||||
|
escaped += b'=2C'
|
||||||
|
elif char == b'=':
|
||||||
|
escaped += b'=3D'
|
||||||
|
else:
|
||||||
|
if isinstance(char, int):
|
||||||
|
char = chr(char)
|
||||||
|
escaped += bytes(char)
|
||||||
|
return escaped
|
||||||
|
|
||||||
|
def parse(self, challenge):
|
||||||
|
items = {}
|
||||||
|
for key, value in [item.split(b'=', 1) for item in challenge.split(b',')]:
|
||||||
|
items[key] = value
|
||||||
|
return items
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
steps = [self.process_1, self.process_2, self.process_3]
|
||||||
|
return steps[self.step](challenge)
|
||||||
|
|
||||||
|
def process_1(self, challenge):
|
||||||
|
self.step = 1
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
self.cnonce = bytes(('%s' % random.random())[2:])
|
||||||
|
|
||||||
|
gs2_cbind_flag = b'n'
|
||||||
|
if self.credentials['channel_binding']:
|
||||||
|
if self.use_channel_binding:
|
||||||
|
gs2_cbind_flag = b'p=tls-unique'
|
||||||
|
else:
|
||||||
|
gs2_cbind_flag = b'y'
|
||||||
|
|
||||||
|
authzid = b''
|
||||||
|
if self.credentials['authzid']:
|
||||||
|
authzid = b'a=' + self.saslname(self.credentials['authzid'])
|
||||||
|
|
||||||
|
self.gs2_header = gs2_cbind_flag + b',' + authzid + b','
|
||||||
|
|
||||||
|
nonce = b'r=' + self.cnonce
|
||||||
|
username = b'n=' + self.saslname(self.credentials['username'])
|
||||||
|
|
||||||
|
self.client_first_message_bare = username + b',' + nonce
|
||||||
|
self.client_first_message = self.gs2_header + \
|
||||||
|
self.client_first_message_bare
|
||||||
|
|
||||||
|
return self.client_first_message
|
||||||
|
|
||||||
|
def process_2(self, challenge):
|
||||||
|
self.step = 2
|
||||||
|
|
||||||
|
data = self.parse(challenge)
|
||||||
|
if b'm' in data:
|
||||||
|
raise SASLCancelled('Received reserved attribute.')
|
||||||
|
|
||||||
|
salt = b64decode(data[b's'])
|
||||||
|
iteration_count = int(data[b'i'])
|
||||||
|
nonce = data[b'r']
|
||||||
|
|
||||||
|
if nonce[:len(self.cnonce)] != self.cnonce:
|
||||||
|
raise SASLCancelled('Invalid nonce')
|
||||||
|
|
||||||
|
cbind_data = self.credentials['channel_binding']
|
||||||
|
cbind_input = self.gs2_header + cbind_data
|
||||||
|
channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'')
|
||||||
|
|
||||||
|
client_final_message_without_proof = channel_binding + b',' + \
|
||||||
|
b'r=' + nonce
|
||||||
|
|
||||||
|
salted_password = self.Hi(self.credentials['password'],
|
||||||
|
salt,
|
||||||
|
iteration_count)
|
||||||
|
client_key = self.HMAC(salted_password, b'Client Key')
|
||||||
|
stored_key = self.H(client_key)
|
||||||
|
auth_message = self.client_first_message_bare + b',' + \
|
||||||
|
challenge + b',' + \
|
||||||
|
client_final_message_without_proof
|
||||||
|
client_signature = self.HMAC(stored_key, auth_message)
|
||||||
|
client_proof = XOR(client_key, client_signature)
|
||||||
|
server_key = self.HMAC(salted_password, b'Server Key')
|
||||||
|
|
||||||
|
self.server_signature = self.HMAC(server_key, auth_message)
|
||||||
|
|
||||||
|
client_final_message = client_final_message_without_proof + \
|
||||||
|
b',p=' + b64encode(client_proof)
|
||||||
|
|
||||||
|
return client_final_message
|
||||||
|
|
||||||
|
def process_3(self, challenge):
|
||||||
|
data = self.parse(challenge)
|
||||||
|
verifier = data.get(b'v', None)
|
||||||
|
error = data.get(b'e', 'Unknown error')
|
||||||
|
|
||||||
|
if not verifier:
|
||||||
|
raise SASLFailed(error)
|
||||||
|
|
||||||
|
if b64decode(verifier) != self.server_signature:
|
||||||
|
raise SASLMutualAuthFailed()
|
||||||
|
|
||||||
|
self._mutual_auth = True
|
||||||
|
|
||||||
|
return b''
|
||||||
|
|
||||||
|
|
||||||
|
@sasl_mech(30)
|
||||||
|
class DIGEST(Mech):
|
||||||
|
|
||||||
|
name = 'DIGEST'
|
||||||
|
use_hashes = True
|
||||||
|
required_credentials = set(['username', 'password', 'realm', 'service', 'host'])
|
||||||
|
optional_credentials = set(['authzid', 'service-name'])
|
||||||
|
security = set(['encrypted', 'unencrypted_digest'])
|
||||||
|
|
||||||
|
def setup(self, name):
|
||||||
|
self.hash_name = name[7:]
|
||||||
|
self.hash = hash(self.hash_name)
|
||||||
|
if self.hash is None:
|
||||||
|
raise SASLCancelled('Unknown hash: %s' % self.hash_name)
|
||||||
|
if not self.security_settings['encrypted']:
|
||||||
|
if not self.security_settings['unencrypted_digest']:
|
||||||
|
raise SASLCancelled('Unencrypted DIGEST')
|
||||||
|
|
||||||
|
self.qops = [b'auth']
|
||||||
|
self.qop = b'auth'
|
||||||
|
self.maxbuf = b'65536'
|
||||||
|
self.nonce = b''
|
||||||
|
self.cnonce = b''
|
||||||
|
self.nonce_count = 1
|
||||||
|
|
||||||
|
def parse(self, challenge=b''):
|
||||||
|
data = {}
|
||||||
|
var_name = b''
|
||||||
|
var_value = b''
|
||||||
|
|
||||||
|
# States: var, new_var, end, quote, escaped_quote
|
||||||
|
state = 'var'
|
||||||
|
|
||||||
|
|
||||||
|
for char in challenge:
|
||||||
|
if sys.version_info >= (3, 0):
|
||||||
|
char = bytes([char])
|
||||||
|
|
||||||
|
if state == 'var':
|
||||||
|
if char.isspace():
|
||||||
|
continue
|
||||||
|
if char == b'=':
|
||||||
|
state = 'value'
|
||||||
|
else:
|
||||||
|
var_name += char
|
||||||
|
elif state == 'value':
|
||||||
|
if char == b'"':
|
||||||
|
state = 'quote'
|
||||||
|
elif char == b',':
|
||||||
|
if var_name:
|
||||||
|
data[var_name.decode('utf-8')] = var_value
|
||||||
|
var_name = b''
|
||||||
|
var_value = b''
|
||||||
|
state = 'var'
|
||||||
|
else:
|
||||||
|
var_value += char
|
||||||
|
elif state == 'escaped':
|
||||||
|
var_value += char
|
||||||
|
elif state == 'quote':
|
||||||
|
if char == b'\\':
|
||||||
|
state = 'escaped'
|
||||||
|
elif char == b'"':
|
||||||
|
state = 'end'
|
||||||
|
else:
|
||||||
|
var_value += char
|
||||||
|
else:
|
||||||
|
if char == b',':
|
||||||
|
if var_name:
|
||||||
|
data[var_name.decode('utf-8')] = var_value
|
||||||
|
var_name = b''
|
||||||
|
var_value = b''
|
||||||
|
state = 'var'
|
||||||
|
else:
|
||||||
|
var_value += char
|
||||||
|
|
||||||
|
if var_name:
|
||||||
|
data[var_name.decode('utf-8')] = var_value
|
||||||
|
var_name = b''
|
||||||
|
var_value = b''
|
||||||
|
state = 'var'
|
||||||
|
return data
|
||||||
|
|
||||||
|
def MAC(self, key, seq, msg):
|
||||||
|
mac = hmac.HMAC(key=key, digestmod=self.hash)
|
||||||
|
seqnum = num_to_bytes(seq)
|
||||||
|
mac.update(seqnum)
|
||||||
|
mac.update(msg)
|
||||||
|
return mac.digest()[:10] + b'\x00\x01' + seqnum
|
||||||
|
|
||||||
|
def A1(self):
|
||||||
|
username = self.credentials['username']
|
||||||
|
password = self.credentials['password']
|
||||||
|
authzid = self.credentials['authzid']
|
||||||
|
realm = self.credentials['realm']
|
||||||
|
|
||||||
|
a1 = self.hash()
|
||||||
|
a1.update(username + b':' + realm + b':' + password)
|
||||||
|
a1 = a1.digest()
|
||||||
|
a1 += b':' + self.nonce + b':' + self.cnonce
|
||||||
|
if authzid:
|
||||||
|
a1 += b':' + authzid
|
||||||
|
|
||||||
|
return bytes(a1)
|
||||||
|
|
||||||
|
def A2(self, prefix=b''):
|
||||||
|
a2 = prefix + b':' + self.digest_uri()
|
||||||
|
if self.qop in (b'auth-int', b'auth-conf'):
|
||||||
|
a2 += b':00000000000000000000000000000000'
|
||||||
|
return bytes(a2)
|
||||||
|
|
||||||
|
def response(self, prefix=b''):
|
||||||
|
nc = bytes('%08x' % self.nonce_count)
|
||||||
|
|
||||||
|
a1 = bytes(self.hash(self.A1()).hexdigest().lower())
|
||||||
|
a2 = bytes(self.hash(self.A2(prefix)).hexdigest().lower())
|
||||||
|
s = self.nonce + b':' + nc + b':' + self.cnonce + \
|
||||||
|
b':' + self.qop + b':' + a2
|
||||||
|
|
||||||
|
return bytes(self.hash(a1 + b':' + s).hexdigest().lower())
|
||||||
|
|
||||||
|
def digest_uri(self):
|
||||||
|
serv_type = self.credentials['service']
|
||||||
|
serv_name = self.credentials['service-name']
|
||||||
|
host = self.credentials['host']
|
||||||
|
|
||||||
|
uri = serv_type + b'/' + host
|
||||||
|
if serv_name and host != serv_name:
|
||||||
|
uri += b'/' + serv_name
|
||||||
|
return uri
|
||||||
|
|
||||||
|
def respond(self):
|
||||||
|
data = {
|
||||||
|
'username': quote(self.credentials['username']),
|
||||||
|
'authzid': quote(self.credentials['authzid']),
|
||||||
|
'realm': quote(self.credentials['realm']),
|
||||||
|
'nonce': quote(self.nonce),
|
||||||
|
'cnonce': quote(self.cnonce),
|
||||||
|
'nc': bytes('%08x' % self.nonce_count),
|
||||||
|
'qop': self.qop,
|
||||||
|
'digest-uri': quote(self.digest_uri()),
|
||||||
|
'response': self.response(b'AUTHENTICATE'),
|
||||||
|
'maxbuf': self.maxbuf
|
||||||
|
}
|
||||||
|
resp = b''
|
||||||
|
for key, value in data.items():
|
||||||
|
if value and value != b'""':
|
||||||
|
resp += b',' + bytes(key) + b'=' + bytes(value)
|
||||||
|
return resp[1:]
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
if not challenge:
|
||||||
|
if self.cnonce and self.nonce and self.nonce_count and self.qop:
|
||||||
|
self.nonce_count += 1
|
||||||
|
return self.respond()
|
||||||
|
return b''
|
||||||
|
|
||||||
|
data = self.parse(challenge)
|
||||||
|
if 'rspauth' in data:
|
||||||
|
if data['rspauth'] != self.response():
|
||||||
|
raise SASLMutualAuthFailed()
|
||||||
|
else:
|
||||||
|
self.nonce_count = 1
|
||||||
|
self.cnonce = bytes('%s' % random.random())[2:]
|
||||||
|
self.qops = data.get('qop', [b'auth'])
|
||||||
|
self.qop = b'auth'
|
||||||
|
if 'nonce' in data:
|
||||||
|
self.nonce = data['nonce']
|
||||||
|
if 'realm' in data and not self.credentials['realm']:
|
||||||
|
self.credentials['realm'] = data['realm']
|
||||||
|
|
||||||
|
return self.respond()
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import kerberos
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
@sasl_mech(75)
|
||||||
|
class GSSAPI(Mech):
|
||||||
|
|
||||||
|
name = 'GSSAPI'
|
||||||
|
required_credentials = set(['username', 'service-name'])
|
||||||
|
optional_credentials = set(['authzid'])
|
||||||
|
|
||||||
|
def setup(self, name):
|
||||||
|
authzid = self.credentials['authzid']
|
||||||
|
if not authzid:
|
||||||
|
authzid = 'xmpp@%s' % self.credentials['service-name']
|
||||||
|
|
||||||
|
_, self.gss = kerberos.authGSSClientInit(authzid)
|
||||||
|
self.step = 0
|
||||||
|
|
||||||
|
def process(self, challenge=b''):
|
||||||
|
b64_challenge = b64encode(challenge)
|
||||||
|
try:
|
||||||
|
if self.step == 0:
|
||||||
|
result = kerberos.authGSSClientStep(self.gss, b64_challenge)
|
||||||
|
if result != kerberos.AUTH_GSS_CONTINUE:
|
||||||
|
self.step = 1
|
||||||
|
elif self.step == 1:
|
||||||
|
username = self.credentials['username']
|
||||||
|
|
||||||
|
kerberos.authGSSClientUnwrap(self.gss, b64_challenge)
|
||||||
|
resp = kerberos.authGSSClientResponse(self.gss)
|
||||||
|
kerberos.authGSSClientWrap(self.gss, resp, username)
|
||||||
|
|
||||||
|
resp = kerberos.authGSSClientResponse(self.gss)
|
||||||
|
except kerberos.GSSError as e:
|
||||||
|
raise SASLCancelled('Kerberos error: %s' % e.message)
|
||||||
|
if not resp:
|
||||||
|
return b''
|
||||||
|
else:
|
||||||
|
return b64decode(resp)
|
@ -20,19 +20,13 @@ import sys
|
|||||||
import stringprep
|
import stringprep
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
|
from sleekxmpp.util import unicode
|
||||||
|
|
||||||
|
|
||||||
class StringPrepError(UnicodeError):
|
class StringPrepError(UnicodeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def to_unicode(data):
|
|
||||||
"""Ensure that a given string is Unicode, regardless of Python version."""
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
return unicode(data)
|
|
||||||
else:
|
|
||||||
return str(data)
|
|
||||||
|
|
||||||
|
|
||||||
def b1_mapping(char):
|
def b1_mapping(char):
|
||||||
"""Map characters that are commonly mapped to nothing."""
|
"""Map characters that are commonly mapped to nothing."""
|
||||||
return '' if stringprep.in_table_b1(char) else None
|
return '' if stringprep.in_table_b1(char) else None
|
||||||
@ -143,7 +137,7 @@ def create(nfkc=True, bidi=True, mappings=None,
|
|||||||
"""
|
"""
|
||||||
def profile(data, query=False):
|
def profile(data, query=False):
|
||||||
try:
|
try:
|
||||||
data = to_unicode(data)
|
data = unicode(data)
|
||||||
except UnicodeError:
|
except UnicodeError:
|
||||||
raise StringPrepError
|
raise StringPrepError
|
||||||
|
|
||||||
|
@ -138,6 +138,15 @@ class XMLStream(object):
|
|||||||
#: be consulted, even if they are not in the provided file.
|
#: be consulted, even if they are not in the provided file.
|
||||||
self.ca_certs = None
|
self.ca_certs = None
|
||||||
|
|
||||||
|
#: Path to a file containing a client certificate to use for
|
||||||
|
#: authenticating via SASL EXTERNAL. If set, there must also
|
||||||
|
#: be a corresponding `:attr:keyfile` value.
|
||||||
|
self.certfile = None
|
||||||
|
|
||||||
|
#: Path to a file containing the private key for the selected
|
||||||
|
#: client certificate to use for authenticating via SASL EXTERNAL.
|
||||||
|
self.keyfile = None
|
||||||
|
|
||||||
#: The time in seconds to wait for events from the event queue,
|
#: The time in seconds to wait for events from the event queue,
|
||||||
#: and also the time between checks for the process stop signal.
|
#: and also the time between checks for the process stop signal.
|
||||||
self.wait_timeout = WAIT_TIMEOUT
|
self.wait_timeout = WAIT_TIMEOUT
|
||||||
@ -499,6 +508,8 @@ class XMLStream(object):
|
|||||||
cert_policy = ssl.CERT_REQUIRED
|
cert_policy = ssl.CERT_REQUIRED
|
||||||
|
|
||||||
ssl_socket = ssl.wrap_socket(self.socket,
|
ssl_socket = ssl.wrap_socket(self.socket,
|
||||||
|
certfile=self.certfile,
|
||||||
|
keyfile=self.keyfile,
|
||||||
ca_certs=self.ca_certs,
|
ca_certs=self.ca_certs,
|
||||||
cert_reqs=cert_policy,
|
cert_reqs=cert_policy,
|
||||||
do_handshake_on_connect=False)
|
do_handshake_on_connect=False)
|
||||||
@ -799,6 +810,8 @@ class XMLStream(object):
|
|||||||
cert_policy = ssl.CERT_REQUIRED
|
cert_policy = ssl.CERT_REQUIRED
|
||||||
|
|
||||||
ssl_socket = ssl.wrap_socket(self.socket,
|
ssl_socket = ssl.wrap_socket(self.socket,
|
||||||
|
certfile=self.certfile,
|
||||||
|
keyfile=self.keyfile,
|
||||||
ssl_version=self.ssl_version,
|
ssl_version=self.ssl_version,
|
||||||
do_handshake_on_connect=False,
|
do_handshake_on_connect=False,
|
||||||
ca_certs=self.ca_certs,
|
ca_certs=self.ca_certs,
|
||||||
|
Loading…
Reference in New Issue
Block a user