diff options
-rw-r--r-- | sleekxmpp/__init__.py | 593 | ||||
-rw-r--r-- | sleekxmpp/basexmpp.py | 1 | ||||
-rw-r--r-- | sleekxmpp/componentxmpp.py | 2 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/xmlstream.py | 3 |
4 files changed, 370 insertions, 229 deletions
diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index 933f661f..26982a54 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - """ SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz @@ -7,245 +5,384 @@ See the file LICENSE for copying permission. """ + from __future__ import absolute_import, unicode_literals -from . basexmpp import BaseXMPP, basexmpp -from xml.etree import cElementTree as ET -from . xmlstream.xmlstream import XMLStream -from . xmlstream.xmlstream import RestartStream -from . xmlstream.matcher.xmlmask import MatchXMLMask -from . xmlstream.matcher.xpath import MatchXPath -from . xmlstream.matcher.many import MatchMany -from . xmlstream.handler.callback import Callback -from . xmlstream.stanzabase import StanzaBase -from . xmlstream import xmlstream as xmlstreammod -from . stanza.message import Message -from . stanza.iq import Iq -import time + import logging import base64 import sys +import hashlib import random -import copy -from . import plugins -#from . import stanza -srvsupport = True + +from sleekxmpp import plugins +from sleekxmpp import stanza +from sleekxmpp.basexmpp import BaseXMPP +from sleekxmpp.stanza import Message, Presence, Iq +from sleekxmpp.xmlstream import XMLStream, RestartStream +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET + +# Flag indicating if DNS SRV records are available for use. +SRV_SUPPORT = True try: - import dns.resolver -except ImportError: - srvsupport = False + import dns.resolver +except: + SRV_SUPPORT = False +class ClientXMPP(BaseXMPP): + """ + SleekXMPP's client class. -#class PresenceStanzaType(object): -# -# def fromXML(self, xml): + Use only for good, not for evil. -# self.ptype = xml.get('type') + Attributes: + Methods: + connect -- Overrides XMLStream.connect. + del_roster_item -- Delete a roster item. + get_roster -- Retrieve the roster from the server. + register_feature -- Register a stream feature. + update_roster -- Update a roster item. + """ -class ClientXMPP(BaseXMPP): - """SleekXMPP's client class. Use only for good, not evil.""" - - def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], escape_quotes=True): - BaseXMPP.__init__(self, 'jabber:client') - global srvsupport - self.default_ns = 'jabber:client' - self.plugin_config = plugin_config - self.escape_quotes = escape_quotes - self.set_jid(jid) - self.plugin_whitelist = plugin_whitelist - self.auto_reconnect = True - self.srvsupport = srvsupport - self.password = password - self.registered_features = [] - self.stream_header = """<stream:stream to='%s' xmlns:stream='http://etherx.jabber.org/streams' xmlns='%s' version='1.0'>""" % (self.server,self.default_ns) - self.stream_footer = "</stream:stream>" - #self.map_namespace('http://etherx.jabber.org/streams', 'stream') - #self.map_namespace('jabber:client', '') - self.features = [] - #TODO: Use stream state here - self.authenticated = False - self.sessionstarted = False - self.bound = False - self.bindfail = False - self.is_component = False - self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures)) - self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster)) - #self.registerHandler(Callback('Roster Update', MatchXMLMask("<presence xmlns='%s' type='subscribe' />" % self.default_ns), self._handlePresenceSubscribe, thread=True)) - self.registerFeature("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_starttls, True) - self.registerFeature("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_sasl_auth, True) - self.registerFeature("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />", self.handler_bind_resource) - self.registerFeature("<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", self.handler_start_session) - - #self.registerStanzaExtension('PresenceStanza', PresenceStanzaType) - #self.register_plugins() - - def connect(self, address=tuple()): - """Connect to the Jabber Server. Attempts SRV lookup, and if it fails, uses - the JID server.""" - if not address or len(address) < 2: - if not self.srvsupport: - logging.debug("Did not supply (address, port) to connect to and no SRV support is installed (http://www.dnspython.org). Continuing to attempt connection, using server hostname from JID.") - else: - logging.debug("Since no address is supplied, attempting SRV lookup.") - try: - answers = dns.resolver.query("_xmpp-client._tcp.%s" % self.server, dns.rdatatype.SRV) - except dns.resolver.NXDOMAIN: - logging.debug("No appropriate SRV record found. Using JID server name.") - else: - # pick a random answer, weighted by priority - # there are less verbose ways of doing this (random.choice() with answer * priority), but I chose this way anyway - # suggestions are welcome - addresses = {} - intmax = 0 - priorities = [] - for answer in answers: - intmax += answer.priority - addresses[intmax] = (answer.target.to_text()[:-1], answer.port) - priorities.append(intmax) # sure, I could just do priorities = addresses.keys()\n priorities.sort() - picked = random.randint(0, intmax) - for priority in priorities: - if picked <= priority: - address = addresses[priority] - break - if not address: - # if all else fails take server from JID. - address = (self.server, 5222) - result = XMLStream.connect(self, address[0], address[1], use_tls=True) - if result: - self.event("connected") + def __init__(self, jid, password, ssl=False, plugin_config={}, + plugin_whitelist=[], escape_quotes=True): + """ + Create a new SleekXMPP client. + + Arguments: + jid -- The JID of the XMPP user account. + password -- The password for the XMPP user account. + ssl -- Deprecated. + plugin_config -- A dictionary of plugin configurations. + plugin_whitelist -- A list of approved plugins that will be loaded + when calling register_plugins. + escape_quotes -- Deprecated. + """ + BaseXMPP.__init__(self, 'jabber:client') + + # To comply with PEP8, method names now use underscores. + # Deprecated method names are re-mapped for backwards compatibility. + self.updateRoster = self.update_roster + self.delRosterItem = self.del_roster_item + self.getRoster = self.get_roster + self.registerFeature = self.register_feature + + self.set_jid(jid) + self.password = password + self.escape_quotes = escape_quotes + self.plugin_config = plugin_config + self.plugin_whitelist = plugin_whitelist + self.srv_support = SRV_SUPPORT + + self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % ( + self.server, + "xmlns:stream='%s'" % self.stream_ns, + "xmlns='%s'" % self.default_ns) + self.stream_footer = "</stream:stream>" + + self.features = [] + self.registered_features = [] + + #TODO: Use stream state here + self.authenticated = False + self.sessionstarted = False + self.bound = False + self.bindfail = False + + self.registerHandler( + Callback('Stream Features', + MatchXPath('{%s}features' % self.stream_ns), + self._handle_stream_features)) + self.registerHandler( + Callback('Roster Update', + MatchXPath('{%s}iq/{%s}query' % ( + self.default_ns, + 'jabber:iq:roster')), + self._handle_roster)) + + self.registerFeature( + "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", + self._handle_starttls, True) + self.registerFeature( + "<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", + self._handle_sasl_auth, True) + self.registerFeature( + "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />", + self._handle_bind_resource) + self.registerFeature( + "<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", + self._handle_start_session) + + def connect(self, address=tuple()): + """ + Connect to the XMPP server. + + When no address is given, a SRV lookup for the server will + be attempted. If that fails, the server user in the JID + will be used. + + Arguments: + address -- A tuple containing the server's host and port. + """ + if not address or len(address) < 2: + if not self.srv_support: + logging.debug("Did not supply (address, port) to connect" + \ + " to and no SRV support is installed" + \ + " (http://www.dnspython.org)." + \ + " Continuing to attempt connection, using" + \ + " server hostname from JID.") + else: + logging.debug("Since no address is supplied," + \ + "attempting SRV lookup.") + try: + xmpp_srv = "_xmpp-client._tcp.%s" % self.server + answers = dns.resolver.query(xmpp_srv, dns.rdatatype.SRV) + except dns.resolver.NXDOMAIN: + logging.debug("No appropriate SRV record found." + \ + " Using JID server name.") else: - logging.warning("Failed to connect") - self.event("disconnected") - return result - - # overriding reconnect and disconnect so that we can get some events - # should events be part of or required by xmlstream? Maybe that would be cleaner - def reconnect(self): - logging.info("Reconnecting") - self.event("disconnected") - XMLStream.reconnect(self) - - def disconnect(self, init=True, close=False, reconnect=False): - self.event("disconnected") - XMLStream.disconnect(self, reconnect) - - def registerFeature(self, mask, pointer, breaker = False): - """Register a stream feature.""" - self.registered_features.append((MatchXMLMask(mask), pointer, breaker)) - - def updateRoster(self, jid, name=None, subscription=None, groups=[]): - """Add or change a roster item.""" - iq = self.Iq().setStanzaValues({'type': 'set'}) - iq['roster']['items'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}} - #self.send(iq, self.Iq().setValues({'id': iq['id']})) - r = iq.send() - return r['type'] == 'result' - - def delRosterItem(self, jid): - iq = self.Iq() - iq['type'] = 'set' - iq['roster']['items'] = {jid: {'subscription': 'remove'}} - return iq.send()['type'] == 'result' - - def getRoster(self): - """Request the roster be sent.""" - iq = self.Iq().setStanzaValues({'type': 'get'}).enable('roster').send() - self._handleRoster(iq, request=True) - - def _handleStreamFeatures(self, features): - self.features = [] - for sub in features.xml: - self.features.append(sub.tag) - for subelement in features.xml: - for feature in self.registered_features: - if feature[0].match(subelement): - #if self.maskcmp(subelement, feature[0], True): - if feature[1](subelement) and feature[2]: #if breaker, don't continue - return True - - def handler_starttls(self, xml): - if not self.authenticated and self.ssl_support: - self.add_handler("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_tls_start, name='TLS Proceed', instream=True) - self.sendXML(xml) + # Pick a random server, weighted by priority. + + addresses = {} + intmax = 0 + for answer in answers: + intmax += answer.priority + addresses[intmax] = (answer.target.to_text()[:-1], + answer.port) + priorities = addresses.keys() + priorities.sort() + + picked = random.randint(0, intmax) + for priority in priorities: + if picked <= priority: + address = addresses[priority] + break + + if not address: + # If all else fails, use the server from the JID. + address = (self.server, 5222) + + return XMLStream.connect(self, address[0], address[1], use_tls=True) + + def register_feature(self, mask, pointer, breaker=False): + """ + Register a stream feature. + + Arguments: + mask -- An XML string matching the feature's element. + pointer -- The function to execute if the feature is received. + breaker -- Indicates if feature processing should halt with + this feature. Defaults to False. + """ + self.registered_features.append((MatchXMLMask(mask), + pointer, + breaker)) + + def update_roster(self, jid, name=None, subscription=None, groups=[]): + """ + Add or change a roster item. + + Arguments: + jid -- The JID of the entry to modify. + name -- The user's nickname for this JID. + subscription -- The subscription status. May be one of + 'to', 'from', 'both', or 'none'. If set + to 'remove', the entry will be deleted. + groups -- The roster groups that contain this item. + """ + iq = self.Iq().setStanzaValues({'type': 'set'}) + iq['roster']['items'] = {jid: {'name': name, + 'subscription': subscription, + 'groups': groups}} + resp = iq.send() + return resp['type'] == 'result' + + def del_roster_item(self, jid): + """ + Remove an item from the roster by setting its subscription + status to 'remove'. + + Arguments: + jid -- The JID of the item to remove. + """ + return self.update_roster(jid, subscription='remove') + + def get_roster(self): + """Request the roster from the server.""" + iq = self.Iq().setStanzaValues({'type': 'get'}).enable('roster') + iq.send() + self._handle_roster(iq, request=True) + + def _handle_stream_features(self, features): + """ + Process the received stream features. + + Arguments: + features -- The features stanza. + """ + # Record all of the features. + self.features = [] + for sub in features.xml: + self.features.append(sub.tag) + + # Process the features. + for sub in features.xml: + for feature in self.registered_features: + mask, handler, halt = feature + if mask.match(sub): + if handler(sub) and halt: + # Don't continue if the feature was + # marked as a breaker. return True + + def _handle_starttls(self, xml): + if not self.authenticated and self.ssl_support: + tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls' + self.add_handler("<proceed xmlns='%s' />" % tls_ns, + self._handle_tls_start, + name='TLS Proceed', + instream=True) + self.send_xml(xml) + return True + else: + logging.warning("The module tlslite is required to log in" +\ + " to some servers, and has not been found.") + return False + + def _handle_tls_start(self, xml): + logging.debug("Starting TLS") + if self.start_tls(): + raise RestartStream() + + def _handle_sasl_auth(self, xml): + if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features: + return False + + logging.debug("Starting SASL Auth") + sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl' + self.add_handler("<success xmlns='%s' />" % sasl_ns, + self._handle_auth_success, + name='SASL Sucess', + instream=True) + self.add_handler("<failure xmlns='%s' />" % sasl_ns, + self._handle_auth_fail, + name='SASL Failure', + instream=True) + + sasl_mechs = xml.findall('{%s}mechanism' % sasl_ns) + if sasl_mechs: + for sasl_mech in sasl_mechs: + self.features.append("sasl:%s" % sasl_mech.text) + if 'sasl:PLAIN' in self.features: + if sys.version_info < (3, 0): + user = bytes(self.username) + password = bytes(self.password) else: - logging.warning("The module tlslite is required in to some servers, and has not been found.") - return False - - def handler_tls_start(self, xml): - logging.debug("Starting TLS") - if self.startTLS(): - raise RestartStream() - - def handler_sasl_auth(self, xml): - if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features: - return False - logging.debug("Starting SASL Auth") - self.add_handler("<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_success, name='SASL Sucess', instream=True) - self.add_handler("<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_fail, name='SASL Failure', instream=True) - sasl_mechs = xml.findall('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism') - if len(sasl_mechs): - for sasl_mech in sasl_mechs: - self.features.append("sasl:%s" % sasl_mech.text) - if 'sasl:PLAIN' in self.features: - if sys.version_info < (3,0): - self.send("""<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>""" % base64.b64encode(b'\x00' + bytes(self.username) + b'\x00' + bytes(self.password)).decode('utf-8')) - else: - self.send("""<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>""" % base64.b64encode(b'\x00' + bytes(self.username, 'utf-8') + b'\x00' + bytes(self.password, 'utf-8')).decode('utf-8')) - else: - logging.error("No appropriate login method.") - self.disconnect() - #if 'sasl:DIGEST-MD5' in self.features: - # self._auth_digestmd5() - return True - - def handler_auth_success(self, xml): - self.authenticated = True - self.features = [] - raise RestartStream() - - def handler_auth_fail(self, xml): - logging.info("Authentication failed.") + user = bytes(self.username, 'utf-8') + password = bytes(self.password, 'utf-8') + + auth = base64.b64encode(b'\x00' + user + \ + b'\x00' + password).decode('utf-8') + + self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % ( + sasl_ns, + auth)) + else: + logging.error("No appropriate login method.") self.disconnect() - self.event("failed_auth") - - def handler_bind_resource(self, xml): - logging.debug("Requesting resource: %s" % self.resource) - xml.clear() - iq = self.Iq(stype='set') - if self.resource: - res = ET.Element('resource') - res.text = self.resource - xml.append(res) - iq.append(xml) - response = iq.send() - #response = self.send(iq, self.Iq(sid=iq['id'])) - self.set_jid(response.xml.find('{urn:ietf:params:xml:ns:xmpp-bind}bind/{urn:ietf:params:xml:ns:xmpp-bind}jid').text) - self.bound = True - logging.info("Node set to: %s" % self.fulljid) - if "{urn:ietf:params:xml:ns:xmpp-session}session" not in self.features or self.bindfail: - logging.debug("Established Session") - self.sessionstarted = True - self.event("session_start") - - def handler_start_session(self, xml): - if self.authenticated and self.bound: - iq = self.makeIqSet(xml) - response = iq.send() - logging.debug("Established Session") - self.sessionstarted = True - self.event("session_start") - else: - #bind probably hasn't happened yet - self.bindfail = True - - def _handleRoster(self, iq, request=False): - if iq['type'] == 'set' or (iq['type'] == 'result' and request): - for jid in iq['roster']['items']: - if not jid in self.roster: - self.roster[jid] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': True} - self.roster[jid].update(iq['roster']['items'][jid]) - if iq['type'] == 'set': - self.send(self.Iq().setStanzaValues({'type': 'result', 'id': iq['id']}).enable('roster')) - self.event("roster_update", iq) + return True + + def _handle_auth_success(self, xml): + """ + SASL authentication succeeded. Restart the stream. + + Arguments: + xml -- The SASL authentication success element. + """ + self.authenticated = True + self.features = [] + raise RestartStream() + + def _handle_auth_fail(self, xml): + """ + SASL authentication failed. Disconnect and shutdown. + + Arguments: + xml -- The SASL authentication failure element. + """ + logging.info("Authentication failed.") + self.disconnect() + self.event("failed_auth") + + def _handle_bind_resource(self, xml): + """ + Handle requesting a specific resource. + + Arguments: + xml -- The bind feature element. + """ + logging.debug("Requesting resource: %s" % self.resource) + xml.clear() + iq = self.Iq(stype='set') + if self.resource: + res = ET.Element('resource') + res.text = self.resource + xml.append(res) + iq.append(xml) + response = iq.send() + + bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind' + self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns, + bind_ns)).text) + self.bound = True + logging.info("Node set to: %s" % self.fulljid) + session_ns = 'urn:ietf:params:xml:ns:xmpp-session' + if "{%s}session" % session_ns not in self.features or self.bindfail: + logging.debug("Established Session") + self.sessionstarted = True + self.event("session_start") + + def _handle_start_session(self, xml): + """ + Handle the start of the session. + + Arguments: + xml -- The session feature element. + """ + if self.authenticated and self.bound: + iq = self.makeIqSet(xml) + response = iq.send() + logging.debug("Established Session") + self.sessionstarted = True + self.event("session_start") + else: + # Bind probably hasn't happened yet. + self.bindfail = True + + def _handle_roster(self, iq, request=False): + """ + Update the roster after receiving a roster stanza. + + Arguments: + iq -- The roster stanza. + request -- Indicates if this stanza is a response + to a request for the roster. + """ + if iq['type'] == 'set' or (iq['type'] == 'result' and request): + for jid in iq['roster']['items']: + if not jid in self.roster: + self.roster[jid] = {'groups': [], + 'name': '', + 'subscription': 'none', + 'presence': {}, + 'in_roster': True} + self.roster[jid].update(iq['roster']['items'][jid]) + + self.event("roster_update", iq) + if iq['type'] == 'set': + iq.reply() + iq.enable('roster') + iq.send() diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 2548934e..c18dda1a 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -111,6 +111,7 @@ class BaseXMPP(XMLStream): self.sendPresenceSubscription = self.send_presence_subscription self.default_ns = default_ns + self.stream_ns = 'http://etherx.jabber.org/streams' self.jid = '' self.fulljid = '' diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index abb20d95..523e75af 100644 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -60,7 +60,7 @@ class ComponentXMPP(BaseXMPP): self.auto_authorize = None self.stream_header = "<stream:stream %s %s to='%s'>" % ( 'xmlns="jabber:component:accept"', - 'xmlns:stream="http://etherx.jabber.org/streams"', + 'xmlns:stream="%s"' % self.stream_ns, jid) self.stream_footer = "</stream:stream>" self.server_host = host diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 40218dd5..239eab8f 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -269,6 +269,7 @@ class XMLStream(object): and processing should be restarted. Defaults to False. """ + self.event("disconnected") self.state.set('reconnect', reconnect) if self.state['disconnecting']: return @@ -294,6 +295,8 @@ class XMLStream(object): """ Reset the stream's state and reconnect to the server. """ + logging.info("Reconnecting") + self.event("disconnected") self.state.set('tls', False) self.state.set('ssl', False) time.sleep(1) |