From d4091dbde641dc9796b51e032ea23a0ba5c1fcbb Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 3 Aug 2011 17:00:51 -0700 Subject: Integrate a modified version of Dave Cridland's Suelta SASL library. --- sleekxmpp/thirdparty/__init__.py | 2 + sleekxmpp/thirdparty/suelta/__init__.py | 26 ++ sleekxmpp/thirdparty/suelta/exceptions.py | 31 ++ sleekxmpp/thirdparty/suelta/mechanisms/__init__.py | 5 + .../thirdparty/suelta/mechanisms/anonymous.py | 36 ++ sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py | 63 ++++ .../thirdparty/suelta/mechanisms/digest_md5.py | 273 ++++++++++++++ sleekxmpp/thirdparty/suelta/mechanisms/plain.py | 61 ++++ .../thirdparty/suelta/mechanisms/scram_hmac.py | 176 +++++++++ sleekxmpp/thirdparty/suelta/sasl.py | 402 +++++++++++++++++++++ sleekxmpp/thirdparty/suelta/saslprep.py | 78 ++++ sleekxmpp/thirdparty/suelta/util.py | 118 ++++++ 12 files changed, 1271 insertions(+) create mode 100644 sleekxmpp/thirdparty/suelta/__init__.py create mode 100644 sleekxmpp/thirdparty/suelta/exceptions.py create mode 100644 sleekxmpp/thirdparty/suelta/mechanisms/__init__.py create mode 100644 sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py create mode 100644 sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py create mode 100644 sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py create mode 100644 sleekxmpp/thirdparty/suelta/mechanisms/plain.py create mode 100644 sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py create mode 100644 sleekxmpp/thirdparty/suelta/sasl.py create mode 100644 sleekxmpp/thirdparty/suelta/saslprep.py create mode 100644 sleekxmpp/thirdparty/suelta/util.py (limited to 'sleekxmpp/thirdparty') diff --git a/sleekxmpp/thirdparty/__init__.py b/sleekxmpp/thirdparty/__init__.py index 276ac3cc..3eb6ad73 100644 --- a/sleekxmpp/thirdparty/__init__.py +++ b/sleekxmpp/thirdparty/__init__.py @@ -2,3 +2,5 @@ try: from collections import OrderedDict except: from sleekxmpp.thirdparty.ordereddict import OrderedDict + +from sleekxmpp.thirdparty import suelta diff --git a/sleekxmpp/thirdparty/suelta/__init__.py b/sleekxmpp/thirdparty/suelta/__init__.py new file mode 100644 index 00000000..04f0cbad --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/__init__.py @@ -0,0 +1,26 @@ +# Copyright 2007-2010 David Alan Cridland +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from sleekxmpp.thirdparty.suelta.saslprep import saslprep +from sleekxmpp.thirdparty.suelta.sasl import * +from sleekxmpp.thirdparty.suelta.mechanisms import * + +__version__ = '2.0' +__version_info__ = (2, 0, 0) diff --git a/sleekxmpp/thirdparty/suelta/exceptions.py b/sleekxmpp/thirdparty/suelta/exceptions.py new file mode 100644 index 00000000..625cca0e --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/exceptions.py @@ -0,0 +1,31 @@ +class SASLError(Exception): + + def __init__(self, sasl, text, mech=None): + """ + :param sasl: The main `suelta.SASL` object. + :param text: Descpription of the error. + :param mech: Optional reference to the mechanism object. + + :type sasl: `suelta.SASL` + """ + self.sasl = sasl + self.text = text + self.mech = mech + + def __str__(self): + if self.mech is None: + return 'SASL Error: %s' % self.text + else: + return 'SASL Error (%s): %s' % (self.mech, self.text) + + +class SASLCancelled(SASLError): + + def __init__(self, sasl, mech=None): + """ + :param sasl: The main `suelta.SASL` object. + :param mech: Optional reference to the mechanism object. + + :type sasl: `suelta.SASL` + """ + super(SASLCancelled, self).__init__(sasl, "User cancelled", mech) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py new file mode 100644 index 00000000..5cb2ee3d --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py @@ -0,0 +1,5 @@ +from sleekxmpp.thirdparty.suelta.mechanisms.anonymous import ANONYMOUS +from sleekxmpp.thirdparty.suelta.mechanisms.plain import PLAIN +from sleekxmpp.thirdparty.suelta.mechanisms.cram_md5 import CRAM_MD5 +from sleekxmpp.thirdparty.suelta.mechanisms.digest_md5 import DIGEST_MD5 +from sleekxmpp.thirdparty.suelta.mechanisms.scram_hmac import SCRAM_HMAC diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py new file mode 100644 index 00000000..de89eef2 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py @@ -0,0 +1,36 @@ +from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism +from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled + + +class ANONYMOUS(Mechanism): + + """ + """ + + def __init__(self, sasl, name): + """ + """ + super(ANONYMOUS, self).__init__(self, sasl, name, 0) + + def get_values(self): + """ + """ + return {} + + def process(self, challenge=None): + """ + """ + return b'Anonymous, Suelta' + + def okay(self): + """ + """ + return True + + def get_user(self): + """ + """ + return 'anonymous' + + +register_mechanism('ANONYMOUS', 0, ANONYMOUS, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py new file mode 100644 index 00000000..ba44befe --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py @@ -0,0 +1,63 @@ +import sys +import hmac + +from sleekxmpp.thirdparty.suelta.util import hash, bytes +from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism +from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled + + +class CRAM_MD5(Mechanism): + + """ + """ + + def __init__(self, sasl, name): + """ + """ + super(CRAM_MD5, self).__init__(sasl, name, 2) + + self.hash = hash(name[5:]) + if self.hash is None: + raise SASLCancelled(self.sasl, self) + if not self.sasl.tls_active(): + if not self.sasl.sec_query(self, 'CRAM-MD5'): + raise SASLCancelled(self.sasl, self) + + def prep(self): + """ + """ + if 'savepass' not in self.values: + if self.sasl.sec_query(self, 'CLEAR-PASSWORD'): + self.values['savepass'] = True + + if 'savepass' not in self.values: + del self.values['password'] + + def process(self, challenge): + """ + """ + if challenge is None: + return None + + self.check_values(['username', 'password']) + username = bytes(self.values['username']) + password = bytes(self.values['password']) + + mac = hmac.HMAC(key=password, digestmod=self.hash) + + mac.update(challenge) + + return username + b' ' + bytes(mac.hexdigest()) + + def okay(self): + """ + """ + return True + + def get_user(self): + """ + """ + return self.values['username'] + + +register_mechanism('CRAM-', 20, CRAM_MD5) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py new file mode 100644 index 00000000..5492c553 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py @@ -0,0 +1,273 @@ +import sys + +import random + +from sleekxmpp.thirdparty.suelta.util import hash, bytes, quote +from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism +from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled + + + +def parse_challenge(stuff): + """ + """ + ret = {} + var = b'' + val = b'' + in_var = True + in_quotes = False + new = False + escaped = False + for c in stuff: + if sys.version_info >= (3, 0): + c = bytes([c]) + if in_var: + if c.isspace(): + continue + if c == b'=': + in_var = False + new = True + else: + var += c + else: + if new: + if c == b'"': + in_quotes = True + else: + val += c + new = False + elif in_quotes: + if escaped: + escaped = False + val += c + else: + if c == b'\\': + escaped = True + elif c == b'"': + in_quotes = False + else: + val += c + else: + if c == b',': + if var: + ret[var] = val + var = b'' + val = b'' + in_var = True + else: + val += c + if var: + ret[var] = val + return ret + + +class DIGEST_MD5(Mechanism): + + """ + """ + + enc_magic = 'Digest session key to client-to-server signing key magic' + dec_magic = 'Digest session key to server-to-client signing key magic' + + def __init__(self, sasl, name): + """ + """ + super(DIGEST_MD5, self).__init__(sasl, name, 3) + + self.hash = hash(name[7:]) + if self.hash is None: + raise SASLCancelled(self.sasl, self) + + if not self.sasl.tls_active(): + if not self.sasl.sec_query(self, '-ENCRYPTION, DIGEST-MD5'): + raise SASLCancelled(self.sasl, self) + + self._rspauth_okay = False + self._digest_uri = None + self._a1 = None + self._enc_buf = b'' + self._enc_key = None + self._enc_seq = 0 + self._max_buffer = 65536 + self._dec_buf = b'' + self._dec_key = None + self._dec_seq = 0 + self._qops = [b'auth'] + self._qop = b'auth' + + def MAC(self, seq, msg, key): + """ + """ + mac = hmac.HMAC(key=key, digestmod=self.hash) + seqnum = num_to_bytes(seq) + mac.update(seqnum) + mac.update(msg) + return mac.digest()[:10] + b'\x00\x01' + seqnum + + + def encode(self, text): + """ + """ + self._enc_buf += text + + def flush(self): + """ + """ + result = b'' + # Leave buffer space for the MAC + mbuf = self._max_buffer - 10 - 2 - 4 + + while self._enc_buf: + msg = self._encbuf[:mbuf] + mac = self.MAC(self._enc_seq, msg, self._enc_key, self.hash) + self._enc_seq += 1 + msg += mac + result += num_to_bytes(len(msg)) + msg + self._enc_buf = self._enc_buf[mbuf:] + + return result + + def decode(self, text): + """ + """ + self._dec_buf += text + result = b'' + + while len(self._dec_buf) > 4: + num = bytes_to_num(self._dec_buf) + if len(self._dec_buf) < (num + 4): + return result + + mac = self._dec_buf[4:4 + num] + self._dec_buf = self._dec_buf[4 + num:] + msg = mac[:-16] + + mac_conf = self.MAC(self._dec_mac, msg, self._dec_key) + if mac[-16:] != mac_conf: + self._desc_sec = None + return result + + self._dec_seq += 1 + result += msg + + return result + + def response(self): + """ + """ + vitals = ['username'] + if not self.has_values(['key_hash']): + vitals.append('password') + self.check_values(vitals) + + resp = {} + if 'auth-int' in self._qops: + self._qop = b'auth-int' + resp['qop'] = self._qop + if 'realm' in self.values: + resp['realm'] = quote(self.values['realm']) + + resp['username'] = quote(bytes(self.values['username'])) + resp['nonce'] = quote(self.values['nonce']) + if self.values['nc']: + self._cnonce = self.values['cnonce'] + else: + self._cnonce = bytes('%s' % random.random())[2:] + resp['cnonce'] = quote(self._cnonce) + self.values['nc'] += 1 + resp['nc'] = bytes('%08x' % self.values['nc']) + + service = bytes(self.sasl.service) + host = bytes(self.sasl.host) + self._digest_uri = service + b'/' + host + resp['digest-uri'] = quote(self._digest_uri) + + a2 = b'AUTHENTICATE:' + self._digest_uri + if self._qop != b'auth': + a2 += b':00000000000000000000000000000000' + resp['maxbuf'] = b'16777215' # 2**24-1 + resp['response'] = self.gen_hash(a2) + return b','.join([bytes(k) + b'=' + bytes(v) for k, v in resp.items()]) + + def gen_hash(self, a2): + """ + """ + if not self.has_values(['key_hash']): + key_hash = self.hash() + user = bytes(self.values['username']) + password = bytes(self.values['password']) + realm = bytes(self.values['realm']) + kh = user + b':' + realm + b':' + password + key_hash.update(kh) + self.values['key_hash'] = key_hash.digest() + + a1 = self.hash(self.values['key_hash']) + a1h = b':' + self.values['nonce'] + b':' + self._cnonce + a1.update(a1h) + response = self.hash() + self._a1 = a1.digest() + rv = bytes(a1.hexdigest().lower()) + rv += b':' + self.values['nonce'] + rv += b':' + bytes('%08x' % self.values['nc']) + rv += b':' + self._cnonce + rv += b':' + self._qop + rv += b':' + bytes(self.hash(a2).hexdigest().lower()) + response.update(rv) + return bytes(response.hexdigest().lower()) + + def mutual_auth(self, cmp_hash): + """ + """ + a2 = b':' + self._digest_uri + if self._qop != b'auth': + a2 += b':00000000000000000000000000000000' + if self.gen_hash(a2) == cmp_hash: + self._rspauth_okay = True + + def prep(self): + """ + """ + if 'password' in self.values: + del self.values['password'] + self.values['cnonce'] = self._cnonce + + def process(self, challenge=None): + """ + """ + if challenge is None: + if self.has_values(['username', 'realm', 'nonce', 'key_hash', + 'nc', 'cnonce', 'qops']): + self._qops = self.values['qops'] + return self.response() + else: + return None + + d = parse_challenge(challenge) + if b'rspauth' in d: + self.mutual_auth(d[b'rspauth']) + else: + if b'realm' not in d: + d[b'realm'] = self.sasl.def_realm + for key in ['nonce', 'realm']: + if bytes(key) in d: + self.values[key] = d[bytes(key)] + self.values['nc'] = 0 + self._qops = [b'auth'] + if b'qop' in d: + self._qops = [x.strip() for x in d[b'qop'].split(b',')] + self.values['qops'] = self._qops + if b'maxbuf' in d: + self._max_buffer = int(d[b'maxbuf']) + return self.response() + + def okay(self): + """ + """ + if self._rspauth_okay and self._qop == b'auth-int': + self._enc_key = self.hash(self._a1 + self.enc_magic).digest() + self._dec_key = self.hash(self._a1 + self.dec_magic).digest() + self.encoding = True + return self._rspauth_okay + + +register_mechanism('DIGEST-', 30, DIGEST_MD5) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/plain.py b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py new file mode 100644 index 00000000..ab17095e --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py @@ -0,0 +1,61 @@ +import sys + +from sleekxmpp.thirdparty.suelta.util import bytes +from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism +from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled + + +class PLAIN(Mechanism): + + """ + """ + + def __init__(self, sasl, name): + """ + """ + super(PLAIN, self).__init__(sasl, name) + + if not self.sasl.tls_active(): + if not self.sasl.sec_query(self, '-ENCRYPTION, PLAIN'): + raise SASLCancelled(self.sasl, self) + else: + if not self.sasl.sec_query(self, '+ENCRYPTION, PLAIN'): + raise SASLCancelled(self.sasl, self) + + self.check_values(['username', 'password']) + + def prep(self): + """ + Prepare for processing by deleting the password if + the user has not approved storing it in the clear. + """ + if 'savepass' not in self.values: + if self.sasl.sec_query(self, 'CLEAR-PASSWORD'): + self.values['savepass'] = True + + if 'savepass' not in self.values: + del self.values['password'] + + return True + + def process(self, challenge=None): + """ + Process a challenge request and return the response. + + :param challenge: A challenge issued by the server that + must be answered for authentication. + """ + user = bytes(self.values['username']) + password = bytes(self.values['password']) + return b'\x00' + user + b'\x00' + password + + def okay(self): + """ + Mutual authentication is not supported by PLAIN. + + :returns: ``True`` + """ + return True + + +register_mechanism('PLAIN', 1, PLAIN, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py b/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py new file mode 100644 index 00000000..e0020329 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py @@ -0,0 +1,176 @@ +import sys +import hmac +import random +from base64 import b64encode, b64decode + +from sleekxmpp.thirdparty.suelta.util import hash, bytes, num_to_bytes, bytes_to_num, XOR +from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism +from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled + + +def parse_challenge(challenge): + """ + """ + items = {} + for key, value in [item.split(b'=', 1) for item in challenge.split(b',')]: + items[key] = value + return items + + +class SCRAM_HMAC(Mechanism): + + """ + """ + + def __init__(self, sasl, name): + """ + """ + super(SCRAM_HMAC, self).__init__(sasl, name, 0) + + self._cb = False + if name[-5:] == '-PLUS': + name = name[:-5] + self._cb = True + + self.hash = hash(self.name[6:]) + if self.hash is None: + raise SASLCancelled(self.sasl, self) + if not self.sasl.tls_active(): + if not self.sasl.sec_query(self, '-ENCRYPTION, SCRAM'): + raise SASLCancelled(self.sasl, self) + + self._step = 0 + self._rspauth = False + + def HMAC(self, key, msg): + """ + """ + return hmac.HMAC(key=key, msg=msg, digestmod=self.hash).digest() + + def Hi(self, text, salt, iterations): + """ + """ + text = bytes(text) + ui_1 = self.HMAC(text, salt + b'\0\0\0\01') + ui = ui_1 + for i in range(iterations - 1): + ui_1 = self.HMAC(text, ui_1) + ui = XOR(ui, ui_1) + return ui + + def H(self, text): + """ + """ + return self.hash(text).digest() + + def prep(self): + if 'password' in self.values: + del self.values['password'] + + def process(self, challenge=None): + """ + """ + steps = { + 0: self.process_one, + 1: self.process_two, + 2: self.process_three + } + return steps[self._step](challenge) + + def process_one(self, challenge): + """ + """ + vitals = ['username'] + if 'SaltedPassword' not in self.values: + vitals.append('password') + if 'Iterations' not in self.values: + vitals.append('password') + + self.check_values(vitals) + + username = bytes(self.values['username']) + + self._step = 1 + self._cnonce = bytes(('%s' % random.random())[2:]) + self._soup = b'n=' + username + b',r=' + self._cnonce + self._gs2header = b'' + + if not self.sasl.tls_active(): + if self._cb: + self._gs2header = b'p=tls-unique,,' + else: + self._gs2header = b'y,,' + else: + self._gs2header = b'n,,' + + return self._gs2header + self._soup + + def process_two(self, challenge): + """ + """ + data = parse_challenge(challenge) + + self._step = 2 + self._soup += b',' + challenge + b',' + self._nonce = data[b'r'] + self._salt = b64decode(data[b's']) + self._iter = int(data[b'i']) + + if self._nonce[:len(self._cnonce)] != self._cnonce: + raise SASLCancelled(self.sasl, self) + + cbdata = self.sasl.tls_active() + c = self._gs2header + if not cbdata and self._cb: + c += None + + r = b'c=' + b64encode(c).replace(b'\n', b'') + r += b',r=' + self._nonce + self._soup += r + + if 'Iterations' in self.values: + if self.values['Iterations'] != self._iter: + if 'SaltedPassword' in self.values: + del self.values['SaltedPassword'] + if 'Salt' in self.values: + if self.values['Salt'] != self._salt: + if 'SaltedPassword' in self.values: + del self.values['SaltedPassword'] + + self.values['Iterations'] = self._iter + self.values['Salt'] = self._salt + + if 'SaltedPassword' not in self.values: + self.check_values(['password']) + password = bytes(self.values['password']) + salted_pass = self.Hi(password, self._salt, self._iter) + self.values['SaltedPassword'] = salted_pass + + salted_pass = self.values['SaltedPassword'] + client_key = self.HMAC(salted_pass, b'Client Key') + stored_key = self.H(client_key) + client_sig = self.HMAC(stored_key, self._soup) + client_proof = XOR(client_key, client_sig) + r += b',p=' + b64encode(client_proof).replace(b'\n', b'') + server_key = self.HMAC(self.values['SaltedPassword'], b'Server Key') + self.server_sig = self.HMAC(server_key, self._soup) + return r + + def process_three(self, challenge=None): + """ + """ + data = parse_challenge(challenge) + if b64decode(data[b'v']) == self.server_sig: + self._rspauth = True + + def okay(self): + """ + """ + return self._rspauth + + def get_user(self): + return self.values['username'] + + +register_mechanism('SCRAM-', 60, SCRAM_HMAC) +register_mechanism('SCRAM-', 70, SCRAM_HMAC, extra='-PLUS') diff --git a/sleekxmpp/thirdparty/suelta/sasl.py b/sleekxmpp/thirdparty/suelta/sasl.py new file mode 100644 index 00000000..ec7afe9d --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/sasl.py @@ -0,0 +1,402 @@ +from sleekxmpp.thirdparty.suelta.util import hashes +from sleekxmpp.thirdparty.suelta.saslprep import saslprep + +#: Global session storage for user answers to requested mechanism values +#: and security questions. This allows the user's preferences to be +#: persisted across multiple SASL authentication attempts made by the +#: same process. +SESSION = {'answers': {}, + 'passwords': {}, + 'sec_queries': {}, + 'stash': {}, + 'stash_file': ''} + +#: Global registry mapping mechanism names to implementation classes. +MECHANISMS = {} + +#: Global registry mapping mechanism names to security scores. +MECH_SEC_SCORES = {} + + +def register_mechanism(basename, basescore, impl, extra=None, use_hashes=True): + """ + Add a SASL mechanism to the registry of available mechanisms. + + :param basename: The base name of the mechanism type, such as ``CRAM-``. + :param basescore: The base security score for this type of mechanism. + :param impl: The class implementing the mechanism. + :param extra: Any additional qualifiers to the mechanism name, + such as ``-PLUS``. + :param use_hashes: If ``True``, then register the mechanism for use with + all available hashes. + """ + n = 0 + if use_hashes: + for hashing_alg in hashes(): + n += 1 + name = basename + hashing_alg + if extra is not None: + name += extra + MECHANISMS[name] = impl + MECH_SEC_SCORES[name] = basescore + n + else: + MECHANISMS[basename] = impl + MECH_SEC_SCORES[basename] = basescore + + +def set_stash_file(filename): + """ + Enable or disable storing the stash to disk. + + If the filename is ``None``, then disable using a stash file. + + :param filename: The path to the file to store the stash data. + """ + SESSION['stash_file'] = filename + try: + import marshal + stash_file = file(filename) + SESSION['stash'] = marshal.load(stash_file) + except: + SESSION['stash'] = {} + + +def sec_query_allow(mech, query): + """ + Quick default to allow all feature combinations which could + negatively affect security. + + :param mech: The chosen SASL mechanism + :param query: An encoding of the combination of enabled and + disabled features which may affect security. + + :returns: ``True`` + """ + return True + + +class SASL(object): + + """ + """ + + def __init__(self, host, service, mech=None, username=None, + min_sec=0, request_values=None, sec_query=None, + tls_active=None, def_realm=None): + """ + :param string host: The host of the service requiring authentication. + :param string service: The name of the underlying protocol in use. + :param string mech: Optional name of the SASL mechanism to use. + If given, only this mechanism may be used for + authentication. + :param string username: The username to use when authenticating. + :param request_values: Reference to a function for supplying + values requested by mechanisms, such + as passwords. (See above) + :param sec_query: Reference to a function for approving or + denying feature combinations which could + negatively impact security. (See above) + :param tls_active: Function for indicating if TLS has been + negotiated. (See above) + :param integer min_sec: The minimum security level accepted. This + only allows for SASL mechanisms whose + security rating is greater than `min_sec`. + :param string def_realm: The default realm, if different than `host`. + + :type request_values: :func:`request_values` + :type sec_query: :func:`sec_query` + :type tls_active: :func:`tls_active` + """ + self.host = host + self.def_realm = def_realm or host + self.service = service + self.user = username + self.mech = mech + self.min_sec = min_sec - 1 + + self.request_values = request_values + self._sec_query = sec_query + if tls_active is not None: + self.tls_active = tls_active + else: + self.tls_active = lambda: False + + self.try_username = self.user + self.try_password = None + + self.stash_id = None + self.testkey = None + + def reset_stash_id(self, username): + """ + Reset the ID for the stash for persisting user data. + + :param username: The username to base the new ID on. + """ + username = saslprep(username) + self.user = username + self.try_username = self.user + self.testkey = [self.user, self.host, self.service] + self.stash_id = '\0'.join(self.testkey) + + def sec_query(self, mech, query): + """ + Request authorization from the user to use a combination + of features which could negatively affect security. + + The ``sec_query`` callback when creating the SASL object will + be called if the query has not been answered before. Otherwise, + the query response will be pulled from ``SESSION['sec_queries']``. + + If no ``sec_query`` callback was provided, then all queries + will be denied. + + :param mech: The chosen SASL mechanism + :param query: An encoding of the combination of enabled and + disabled features which may affect security. + :rtype: bool + """ + if self._sec_query is None: + return False + if query in SESSION['sec_queries']: + return SESSION['sec_queries'][query] + resp = self._sec_query(mech, query) + if resp: + SESSION['sec_queries'][query] = resp + + return resp + + def find_password(self, mech): + """ + Find and return the user's password, if it has been entered before + during this session. + + :param mech: The chosen SASL mechanism. + """ + if self.try_password is not None: + return self.try_password + if self.testkey is None: + return + + testkey = self.testkey[:] + lockout = 1 + + def find_username(self): + """Find and return user's username if known.""" + return self.try_username + + def success(self, mech): + mech.preprep() + if 'password' in mech.values: + testkey = self.testkey[:] + while len(testkey): + tk = '\0'.join(testkey) + if tk in SESSION['passwords']: + break + SESSION['passwords'][tk] = mech.values['password'] + testkey = testkey[:-1] + mech.prep() + mech.save_values() + + def failure(self, mech): + mech.clear() + self.testkey = self.testkey[:-1] + + def choose_mechanism(self, mechs, force_plain=False): + """ + Choose the most secure mechanism from a list of mechanisms. + + If ``force_plain`` is given, return the ``PLAIN`` mechanism. + + :param mechs: A list of mechanism names. + :param force_plain: If ``True``, force the selection of the + ``PLAIN`` mechanism. + :returns: A SASL mechanism object, or ``None`` if no mechanism + could be selected. + """ + # Handle selection of PLAIN and ANONYMOUS + if force_plain: + return MECHANISMS['PLAIN'](self, 'PLAIN') + + if self.user is not None: + requested_mech = '*' if self.mech is None else self.mech + else: + if self.mech is None: + requested_mech = 'ANONYMOUS' + else: + requested_mech = self.mech + if requested_mech == '*' and self.user == 'anonymous': + requested_mech = 'ANONYMOUS' + + # If a specific mechanism was requested, try it + if requested_mech != '*': + if requested_mech in MECHANISMS and \ + requested_mech in MECH_SEC_SCORES: + return MECHANISMS[requested_mech](self, requested_mech) + return None + + # Pick the best mechanism based on its security score + best_score = self.min_sec + best_mech = None + for name in mechs: + if name in MECH_SEC_SCORES: + if MECH_SEC_SCORES[name] > best_score: + best_score = MECH_SEC_SCORES[name] + best_mech = name + if best_mech != None: + best_mech = MECHANISMS[best_mech](self, best_mech) + + return best_mech + + +class Mechanism(object): + + """ + """ + + def __init__(self, sasl, name, version=0, use_stash=True): + self.name = name + self.sasl = sasl + self.use_stash = use_stash + + self.encoding = False + self.values = {} + + if use_stash: + self.load_values() + + def load_values(self): + """Retrieve user data from the stash.""" + self.values = {} + if not self.use_stash: + return False + if self.sasl.stash_id is not None: + if self.sasl.stash_id in SESSION['stash']: + if SESSION['stash'][self.sasl.stash_id]['mech'] == self.name: + values = SESSION['stash'][self.sasl.stash_id]['values'] + self.values.update(values) + if self.sasl.user is not None: + if not self.has_values(['username']): + self.values['username'] = self.sasl.user + return None + + def save_values(self): + """ + Save user data to the session stash. + + If a stash file name has been set using ``SESSION['stash_file']``, + the saved values will be persisted to disk. + """ + if not self.use_stash: + return False + if self.sasl.stash_id is not None: + if self.sasl.stash_id not in SESSION['stash']: + SESSION['stash'][self.sasl.stash_id] = {} + SESSION['stash'][self.sasl.stash_id]['values'] = self.values + SESSION['stash'][self.sasl.stash_id]['mech'] = self.name + if SESSION['stash_file'] not in ['', None]: + import marshal + stash_file = file(SESSION['stash_file'], 'wb') + marshal.dump(SESSION['stash'], stash_file) + + def clear(self): + """Reset all user data, except the username.""" + username = None + if 'username' in self.values: + username = self.values['username'] + self.values = {} + if username is not None: + self.values['username'] = username + self.save_values() + self.values = {} + self.load_values() + + def okay(self): + """ + Indicate if mutual authentication has completed successfully. + + :rtype: bool + """ + return False + + def preprep(self): + """Ensure that the stash ID has been set before processing.""" + if self.sasl.stash_id is None: + if 'username' in self.values: + self.sasl.reset_stash_id(self.values['username']) + + def prep(self): + """ + Prepare stored values for processing. + + For example, by removing extra copies of passwords from memory. + """ + pass + + def process(self, challenge=None): + """ + Process a challenge request and return the response. + + :param challenge: A challenge issued by the server that + must be answered for authentication. + """ + raise NotImplemented + + def fulfill(self, values): + """ + Provide requested values to the mechanism. + + :param values: A dictionary of requested values. + """ + if 'password' in values: + values['password'] = saslprep(values['password']) + self.values.update(values) + + def missing_values(self, keys): + """ + Return a dictionary of value names that have not been given values + by the user, or retrieved from the stash. + + :param keys: A list of value names to check. + :rtype: dict + """ + vals = {} + for name in keys: + if name not in self.values or self.values[name] is None: + if self.use_stash: + if name == 'username': + value = self.sasl.find_username() + if value is not None: + self.sasl.reset_stash_id(value) + self.values[name] = value + break + if name == 'password': + value = self.sasl.find_password(self) + if value is not None: + self.values[name] = value + break + vals[name] = None + return vals + + def has_values(self, keys): + """ + Check that the given values have been retrieved from the user, + or from the stash. + + :param keys: A list of value names to check. + """ + return len(self.missing_values(keys)) == 0 + + def check_values(self, keys): + """ + Request missing values from the user. + + :param keys: A list of value names to request, if missing. + """ + vals = self.missing_values(keys) + if vals: + self.sasl.request_values(self, vals) + + def get_user(self): + """Return the username usd for this mechanism.""" + return self.values['username'] diff --git a/sleekxmpp/thirdparty/suelta/saslprep.py b/sleekxmpp/thirdparty/suelta/saslprep.py new file mode 100644 index 00000000..fe58d58b --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/saslprep.py @@ -0,0 +1,78 @@ +from __future__ import unicode_literals + +import sys +import stringprep +import unicodedata + + +def saslprep(text, strict=True): + """ + Return a processed version of the given string, using the SASLPrep + profile of stringprep. + + :param text: The string to process, in UTF-8. + :param strict: If ``True``, prevent the use of unassigned code points. + """ + + if sys.version_info < (3, 0): + if type(text) == str: + text = text.decode('us-ascii') + + # Mapping: + # + # - non-ASCII space characters [StringPrep, C.1.2] that can be + # mapped to SPACE (U+0020), and + # + # - the 'commonly mapped to nothing' characters [StringPrep, B.1] + # that can be mapped to nothing. + buffer = '' + for char in text: + if stringprep.in_table_c12(char): + buffer += ' ' + elif not stringprep.in_table_b1(char): + buffer += char + + # Normalization using form KC + text = unicodedata.normalize('NFKC', buffer) + + # Check for bidirectional string + buffer = '' + first_is_randal = False + if text: + first_is_randal = stringprep.in_table_d1(text[0]) + if first_is_randal and not stringprep.in_table_d1(text[-1]): + raise UnicodeError('Section 6.3 [end]') + + # Check for prohibited characters + for x in range(len(text)): + if strict and stringprep.in_table_a1(text[x]): + raise UnicodeError('Unassigned Codepoint') + if stringprep.in_table_c12(text[x]): + raise UnicodeError('In table C.1.2') + if stringprep.in_table_c21(text[x]): + raise UnicodeError('In table C.2.1') + if stringprep.in_table_c22(text[x]): + raise UnicodeError('In table C.2.2') + if stringprep.in_table_c3(text[x]): + raise UnicodeError('In table C.3') + if stringprep.in_table_c4(text[x]): + raise UnicodeError('In table C.4') + if stringprep.in_table_c5(text[x]): + raise UnicodeError('In table C.5') + if stringprep.in_table_c6(text[x]): + raise UnicodeError('In table C.6') + if stringprep.in_table_c7(text[x]): + raise UnicodeError('In table C.7') + if stringprep.in_table_c8(text[x]): + raise UnicodeError('In table C.8') + if stringprep.in_table_c9(text[x]): + raise UnicodeError('In table C.9') + if x: + if first_is_randal and stringprep.in_table_d2(text[x]): + raise UnicodeError('Section 6.2') + if not first_is_randal and \ + x != len(text) - 1 and \ + stringprep.in_table_d1(text[x]): + raise UnicodeError('Section 6.3') + + return text diff --git a/sleekxmpp/thirdparty/suelta/util.py b/sleekxmpp/thirdparty/suelta/util.py new file mode 100644 index 00000000..7d822a81 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/util.py @@ -0,0 +1,118 @@ +""" +""" + +import sys +import hashlib + + +def bytes(text): + """ + Convert Unicode text to UTF-8 encoded bytes. + + Since Python 2.6+ and Python 3+ have similar but incompatible + signatures, this function unifies the two to keep code sane. + + :param text: Unicode text to convert to bytes + :rtype: bytes (Python3), str (Python2.6+) + """ + if sys.version_info < (3, 0): + import __builtin__ + return __builtin__.bytes(text) + else: + import builtins + if isinstance(text, builtins.bytes): + # We already have bytes, so do nothing + return text + if isinstance(text, list): + # Convert a list of integers to bytes + return builtins.bytes(text) + else: + # Convert UTF-8 text to bytes + return builtins.bytes(text, encoding='utf-8') + + +def quote(text): + """ + Enclose in quotes and escape internal slashes and double quotes. + + :param text: A Unicode or byte string. + """ + text = bytes(text) + return b'"' + text.replace(b'\\', b'\\\\').replace(b'"', b'\\"') + b'"' + + +def num_to_bytes(num): + """ + Convert an integer into a four byte sequence. + + :param integer num: An integer to convert to its byte representation. + """ + bval = b'' + bval += bytes(chr(0xFF & (num >> 24))) + bval += bytes(chr(0xFF & (num >> 16))) + bval += bytes(chr(0xFF & (num >> 8))) + bval += bytes(chr(0xFF & (num >> 0))) + return bval + + +def bytes_to_num(bval): + """ + Convert a four byte sequence to an integer. + + :param bytes bval: A four byte sequence to turn into an integer. + """ + num = 0 + num += ord(bval[0] << 24) + num += ord(bval[1] << 16) + num += ord(bval[2] << 8) + num += ord(bval[3]) + return num + + +def XOR(x, y): + """ + Return the results of an XOR operation on two equal length byte strings. + + :param bytes x: A byte string + :param bytes y: A byte string + :rtype: bytes + """ + result = b'' + for a, b in zip(x, y): + if sys.version_info < (3, 0): + result += chr((ord(a) ^ ord(b))) + else: + result += bytes([a ^ b]) + return result + + +def hash(name): + """ + Return a hash function implementing the given algorithm. + + :param name: The name of the hashing algorithm to use. + :type name: string + + :rtype: function + """ + name = name.lower() + if name.startswith('sha-'): + name = 'sha' + name[4:] + if name in dir(hashlib): + return getattr(hashlib, name) + return None + + +def hashes(): + """ + Return a list of available hashing algorithms. + + :rtype: list of strings + """ + t = [] + if 'md5' in dir(hashlib): + t = ['MD5'] + if 'md2' in dir(hashlib): + t += ['MD2'] + hashes = ['SHA-' + h[3:] for h in dir(hashlib) if h.startswith('sha')] + return t + hashes -- cgit v1.2.3 From 0bec040cfed632c40b820eec326f6af262ccde4a Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 3 Aug 2011 17:08:45 -0700 Subject: Make sure to keep David Cridland's license and copyright. --- sleekxmpp/thirdparty/suelta/LICENSE | 21 +++++++++++++++++++++ sleekxmpp/thirdparty/suelta/PLAYING-NICELY | 27 +++++++++++++++++++++++++++ sleekxmpp/thirdparty/suelta/README | 8 ++++++++ 3 files changed, 56 insertions(+) create mode 100644 sleekxmpp/thirdparty/suelta/LICENSE create mode 100644 sleekxmpp/thirdparty/suelta/PLAYING-NICELY create mode 100644 sleekxmpp/thirdparty/suelta/README (limited to 'sleekxmpp/thirdparty') diff --git a/sleekxmpp/thirdparty/suelta/LICENSE b/sleekxmpp/thirdparty/suelta/LICENSE new file mode 100644 index 00000000..6eee4f33 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/LICENSE @@ -0,0 +1,21 @@ +This software is subject to "The MIT License" + +Copyright 2007-2010 David Alan Cridland + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/sleekxmpp/thirdparty/suelta/PLAYING-NICELY b/sleekxmpp/thirdparty/suelta/PLAYING-NICELY new file mode 100644 index 00000000..393b8078 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/PLAYING-NICELY @@ -0,0 +1,27 @@ +Hi. + +This is a short note explaining the license in non-legally-binding terms, and +describing how I hope to see people work with the licensing. + +First off, the license is permissive, and more or less allows you to do +anything, as long as you leave my credit and copyright intact. + +You can, and are very much welcome to, include this in commercial works, and +in code that has tightly controlled distribution, as well as open-source. + +If it doesn't work - and I have no doubt that there are bugs - then this is +largely your problem. + +If you do find a bug, though, do let me know - although you don't have to. + +And if you fix it, I'd greatly appreciate a patch, too. Please give me a +licensing statement, and a copyright statement, along with your patch. + +Similarly, any enhancements are welcome, and also will need copyright and +licensing. Please stick to a license which is compatible with the MIT license, +and consider assignment (as required) to me to simplify licensing. (Public +domain does not exist in the UK, sorry). + +Thanks, + +Dave. diff --git a/sleekxmpp/thirdparty/suelta/README b/sleekxmpp/thirdparty/suelta/README new file mode 100644 index 00000000..c32463a4 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/README @@ -0,0 +1,8 @@ +Suelta - A pure-Python SASL client library + +Suelta is a SASL library, providing you with authentication and in some cases +security layers. + +It supports a wide range of typical SASL mechanisms, including the MTI for +all known protocols. + -- cgit v1.2.3 From 4d8933abdf4a190493f2762d423f469f6d8b30a9 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 4 Aug 2011 20:20:22 -0700 Subject: Actually, we can work around needing dateutil. If dateutil is present, we'll use that. If not, we'll use some regexes from the fixed_datetime module. --- sleekxmpp/thirdparty/__init__.py | 1 + sleekxmpp/thirdparty/mini_dateutil.py | 267 ++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 sleekxmpp/thirdparty/mini_dateutil.py (limited to 'sleekxmpp/thirdparty') diff --git a/sleekxmpp/thirdparty/__init__.py b/sleekxmpp/thirdparty/__init__.py index 3eb6ad73..1c7bf651 100644 --- a/sleekxmpp/thirdparty/__init__.py +++ b/sleekxmpp/thirdparty/__init__.py @@ -4,3 +4,4 @@ except: from sleekxmpp.thirdparty.ordereddict import OrderedDict from sleekxmpp.thirdparty import suelta +from sleekxmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso diff --git a/sleekxmpp/thirdparty/mini_dateutil.py b/sleekxmpp/thirdparty/mini_dateutil.py new file mode 100644 index 00000000..bfb86305 --- /dev/null +++ b/sleekxmpp/thirdparty/mini_dateutil.py @@ -0,0 +1,267 @@ +# This module is a very stripped down version of the dateutil +# package for when dateutil has not been installed. As a replacement +# for dateutil.parser.parse, the parsing methods from +# http://blog.mfabrik.com/2008/06/30/relativity-of-time-shortcomings-in-python-datetime-and-workaround/ + +#As such, the following copyrights and licenses applies: + + +# dateutil - Extensions to the standard python 2.3+ datetime module. +# +# Copyright (c) 2003-2011 - Gustavo Niemeyer +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# fixed_dateime +# +# Copyright (c) 2008, Red Innovation Ltd., Finland +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Red Innovation nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +import re +import datetime + + +ZERO = datetime.timedelta(0) + + +try: + from dateutil.parser import parse as parse_iso + from dateutil.tz import tzoffset, tzutc +except: + # As a stopgap, define the two timezones here based + # on the dateutil code. + + class tzutc(datetime.tzinfo): + + def utcoffset(self, dt): + return ZERO + + def dst(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def __eq__(self, other): + return (isinstance(other, tzutc) or + (isinstance(other, tzoffset) and other._offset == ZERO)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + class tzoffset(datetime.tzinfo): + + def __init__(self, name, offset): + self._name = name + self._offset = datetime.timedelta(seconds=offset) + + def utcoffset(self, dt): + return self._offset + + def dst(self, dt): + return ZERO + + def tzname(self, dt): + return self._name + + def __eq__(self, other): + return (isinstance(other, tzoffset) and + self._offset == other._offset) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s(%s, %s)" % (self.__class__.__name__, + repr(self._name), + self._offset.days*86400+self._offset.seconds) + + __reduce__ = object.__reduce__ + + + _fixed_offset_tzs = { } + UTC = tzutc() + + def _get_fixed_offset_tz(offsetmins): + """For internal use only: Returns a tzinfo with + the given fixed offset. This creates only one instance + for each offset; the zones are kept in a dictionary""" + + if offsetmins == 0: + return UTC + + if not _fixed_offset_tzs.has_key(offsetmins): + if offsetmins < 0: + sign = '-' + absoff = -offsetmins + else: + sign = '+' + absoff = offsetmins + + name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60) + inst = tzoffset(offsetmins, name) + _fixed_offset_tzs[offsetmins] = inst + + return _fixed_offset_tzs[offsetmins] + + + _iso8601_parser = re.compile(""" + ^ + (?P [0-9]{4})?(?P-?)? + (?P[0-9]{2})?(?P=ymdsep)? + (?P [0-9]{2})? + + (?: # time part... optional... at least hour must be specified + (?:T|\s+)? + (?P[0-9]{2}) + (?: + # minutes, separated with :, or none, from hours + (?P[:]?) + (?P[0-9]{2}) + (?: + # same for seconds, separated with :, or none, from hours + (?P=hmssep) + (?P[0-9]{2}) + )? + )? + + # fractions + (?: [,.] (?P[0-9]{1,10}))? + + # timezone, Z, +-hh or +-hh:?mm. MUST BE, but complain if not there. + ( + (?PZ) + | + (?P[+-][0-9]{2}) + (?: :? # optional separator + (?P[0-9]{2}) + )? + )? + )? + $ + """, re.X) # """ + + def parse_iso(timestamp): + """Internal function for parsing a timestamp in + ISO 8601 format""" + + timestamp = timestamp.strip() + + m = _iso8601_parser.match(timestamp) + if not m: + raise ValueError("Not a proper ISO 8601 timestamp!: %s" % timestamp) + + vals = m.groupdict() + def_vals = {'year': 1970, 'month': 1, 'day': 1} + for key in vals: + if vals[key] is None: + vals[key] = def_vals.get(key, 0) + elif key not in ['ymdsep', 'hmssep', 'tzempty']: + vals[key] = int(vals[key]) + + year = vals['year'] + month = vals['month'] + day = vals['day'] + + h, min, s, us = None, None, None, 0 + frac = 0 + if m.group('tzempty') == None and m.group('tzh') == None: + raise ValueError("Not a proper ISO 8601 timestamp: " + + "missing timezone (Z or +hh[:mm])!") + + if m.group('frac'): + frac = m.group('frac') + power = len(frac) + frac = long(frac) / 10.0 ** power + + if m.group('hour'): + h = vals['hour'] + + if m.group('minute'): + min = vals['minute'] + + if m.group('second'): + s = vals['second'] + + if frac != None: + # ok, fractions of hour? + if min == None: + frac, min = _math.modf(frac * 60.0) + min = int(min) + + # fractions of second? + if s == None: + frac, s = _math.modf(frac * 60.0) + s = int(s) + + # and extract microseconds... + us = int(frac * 1000000) + + if m.group('tzempty') == 'Z': + offsetmins = 0 + else: + # timezone: hour diff with sign + offsetmins = vals['tzh'] * 60 + tzm = m.group('tzm') + + # add optional minutes + if tzm != None: + tzm = long(tzm) + offsetmins += tzm if offsetmins > 0 else -tzm + + tz = _get_fixed_offset_tz(offsetmins) + return datetime.datetime(year, month, day, h, min, s, us, tz) -- cgit v1.2.3 From caec2976d7788dba75a82dfb9079b57c96ef7b47 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 4 Aug 2011 20:34:23 -0700 Subject: Fix Python3 bug. Use int() instead of long() --- sleekxmpp/thirdparty/mini_dateutil.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'sleekxmpp/thirdparty') diff --git a/sleekxmpp/thirdparty/mini_dateutil.py b/sleekxmpp/thirdparty/mini_dateutil.py index bfb86305..5b3af9a7 100644 --- a/sleekxmpp/thirdparty/mini_dateutil.py +++ b/sleekxmpp/thirdparty/mini_dateutil.py @@ -226,7 +226,7 @@ except: if m.group('frac'): frac = m.group('frac') power = len(frac) - frac = long(frac) / 10.0 ** power + frac = int(frac) / 10.0 ** power if m.group('hour'): h = vals['hour'] @@ -260,7 +260,7 @@ except: # add optional minutes if tzm != None: - tzm = long(tzm) + tzm = int(tzm) offsetmins += tzm if offsetmins > 0 else -tzm tz = _get_fixed_offset_tz(offsetmins) -- cgit v1.2.3 From 93a4a3f8a0f64eed846895365fa3da059bbf5ea1 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 4 Aug 2011 22:34:34 -0700 Subject: Fix Python3 issue with dict.has_key() --- sleekxmpp/thirdparty/mini_dateutil.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) (limited to 'sleekxmpp/thirdparty') diff --git a/sleekxmpp/thirdparty/mini_dateutil.py b/sleekxmpp/thirdparty/mini_dateutil.py index 5b3af9a7..6af5ffde 100644 --- a/sleekxmpp/thirdparty/mini_dateutil.py +++ b/sleekxmpp/thirdparty/mini_dateutil.py @@ -144,17 +144,17 @@ except: if offsetmins == 0: return UTC - if not _fixed_offset_tzs.has_key(offsetmins): - if offsetmins < 0: - sign = '-' - absoff = -offsetmins - else: - sign = '+' - absoff = offsetmins - - name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60) - inst = tzoffset(offsetmins, name) - _fixed_offset_tzs[offsetmins] = inst + if not offsetmins in _fixed_offset_tzs: + if offsetmins < 0: + sign = '-' + absoff = -offsetmins + else: + sign = '+' + absoff = offsetmins + + name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60) + inst = tzoffset(offsetmins, name) + _fixed_offset_tzs[offsetmins] = inst return _fixed_offset_tzs[offsetmins] @@ -240,13 +240,13 @@ except: if frac != None: # ok, fractions of hour? if min == None: - frac, min = _math.modf(frac * 60.0) - min = int(min) + frac, min = _math.modf(frac * 60.0) + min = int(min) # fractions of second? if s == None: - frac, s = _math.modf(frac * 60.0) - s = int(s) + frac, s = _math.modf(frac * 60.0) + s = int(s) # and extract microseconds... us = int(frac * 1000000) -- cgit v1.2.3 From 148a23579c199d218325c88d7b6f567f42c7a36a Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 5 Aug 2011 14:06:58 -0700 Subject: Hotfix for ANONYMOUS mech support. Updates version to 1.0-Beta6.1 --- sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py | 2 +- sleekxmpp/thirdparty/suelta/sasl.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'sleekxmpp/thirdparty') diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py index de89eef2..e44e91a2 100644 --- a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py +++ b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py @@ -10,7 +10,7 @@ class ANONYMOUS(Mechanism): def __init__(self, sasl, name): """ """ - super(ANONYMOUS, self).__init__(self, sasl, name, 0) + super(ANONYMOUS, self).__init__(sasl, name, 0) def get_values(self): """ diff --git a/sleekxmpp/thirdparty/suelta/sasl.py b/sleekxmpp/thirdparty/suelta/sasl.py index ec7afe9d..2ae9ae61 100644 --- a/sleekxmpp/thirdparty/suelta/sasl.py +++ b/sleekxmpp/thirdparty/suelta/sasl.py @@ -225,7 +225,7 @@ class SASL(object): requested_mech = 'ANONYMOUS' else: requested_mech = self.mech - if requested_mech == '*' and self.user == 'anonymous': + if requested_mech == '*' and self.user in ['', 'anonymous', None]: requested_mech = 'ANONYMOUS' # If a specific mechanism was requested, try it @@ -243,7 +243,7 @@ class SASL(object): if MECH_SEC_SCORES[name] > best_score: best_score = MECH_SEC_SCORES[name] best_mech = name - if best_mech != None: + if best_mech is not None: best_mech = MECHANISMS[best_mech](self, best_mech) return best_mech -- cgit v1.2.3