diff options
Diffstat (limited to 'sleekxmpp')
45 files changed, 992 insertions, 170 deletions
diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index f0dc2ce2..542bb4b6 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -6,14 +6,14 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.basexmpp import BaseXMPP -from sleekxmpp.clientxmpp import ClientXMPP -from sleekxmpp.componentxmpp import ComponentXMPP from sleekxmpp.stanza import Message, Presence, Iq from sleekxmpp.jid import JID, InvalidJID +from sleekxmpp.xmlstream.stanzabase import ET, ElementBase, register_stanza_plugin from sleekxmpp.xmlstream.handler import * from sleekxmpp.xmlstream import XMLStream, RestartStream from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET +from sleekxmpp.basexmpp import BaseXMPP +from sleekxmpp.clientxmpp import ClientXMPP +from sleekxmpp.componentxmpp import ComponentXMPP from sleekxmpp.version import __version__, __version_info__ diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index a54e4bb6..bf0ae4df 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -18,8 +18,7 @@ import sys import logging import threading -import sleekxmpp -from sleekxmpp import plugins, features, roster +from sleekxmpp import plugins, roster, stanza from sleekxmpp.api import APIRegistry from sleekxmpp.exceptions import IqError, IqTimeout @@ -34,8 +33,7 @@ from sleekxmpp.xmlstream.matcher import MatchXPath from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.stanzabase import XML_NS -from sleekxmpp.features import * -from sleekxmpp.plugins import PluginManager, register_plugin, load_plugin +from sleekxmpp.plugins import PluginManager, load_plugin log = logging.getLogger(__name__) @@ -148,7 +146,7 @@ class BaseXMPP(XMLStream): #: A reference to :mod:`sleekxmpp.stanza` to make accessing #: stanza classes easier. - self.stanza = sleekxmpp.stanza + self.stanza = stanza self.register_handler( Callback('IM', diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 905e1944..6bcb36ba 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -96,6 +96,7 @@ class ClientXMPP(BaseXMPP): self.add_event_handler('connected', self._reset_connection_state) self.add_event_handler('session_bind', self._handle_session_bind) + self.add_event_handler('roster_update', self._handle_roster) self.register_stanza(StreamFeatures) @@ -106,7 +107,7 @@ class ClientXMPP(BaseXMPP): self.register_handler( Callback('Roster Update', StanzaPath('iq@type=set/roster'), - self._handle_roster)) + lambda iq: self.event('roster_update', iq))) # Setup default stream features self.register_plugin('feature_starttls') @@ -154,8 +155,6 @@ class ClientXMPP(BaseXMPP): address = (self.boundjid.host, 5222) self.dns_service = 'xmpp-client' - self._expected_server_name = self.boundjid.host - return XMLStream.connect(self, address[0], address[1], use_tls=use_tls, use_ssl=use_ssl, reattempt=reattempt) @@ -244,13 +243,22 @@ class ClientXMPP(BaseXMPP): if 'rosterver' in self.features: iq['roster']['ver'] = self.client_roster.version - if not block and callback is None: - callback = lambda resp: self._handle_roster(resp) + + if not block or callback is not None: + block = False + if callback is None: + callback = lambda resp: self.event('roster_update', resp) + else: + orig_cb = callback + def wrapped(resp): + self.event('roster_update', resp) + orig_cb(resp) + callback = wrapped response = iq.send(block, timeout, callback) if block: - self._handle_roster(response) + self.event('roster_update', response) return response def _reset_connection_state(self, event=None): @@ -301,7 +309,6 @@ class ClientXMPP(BaseXMPP): roster[jid].save(remove=(item['subscription'] == 'remove')) - self.event("roster_update", iq) if iq['type'] == 'set': resp = self.Iq(stype='result', sto=iq['from'], diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index 5284f9d4..bac455e2 100644 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -123,12 +123,6 @@ class ComponentXMPP(BaseXMPP): """ if xml.tag.startswith('{jabber:client}'): xml.tag = xml.tag.replace('jabber:client', self.default_ns) - - # The incoming_filter call is only made on top level stanza - # elements. So we manually continue filtering on sub-elements. - for sub in xml: - self.incoming_filter(sub) - return xml def start_stream_handler(self, xml): diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py index bc145620..ee4c1e9b 100644 --- a/sleekxmpp/features/feature_bind/bind.py +++ b/sleekxmpp/features/feature_bind/bind.py @@ -12,7 +12,7 @@ from sleekxmpp.jid import JID from sleekxmpp.stanza import Iq, StreamFeatures from sleekxmpp.features.feature_bind import stanza from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.plugins import BasePlugin, register_plugin +from sleekxmpp.plugins import BasePlugin log = logging.getLogger(__name__) diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index 58ed9b91..ca94bce1 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -6,7 +6,6 @@ See the file LICENSE for copying permission. """ -import sys import ssl import logging diff --git a/sleekxmpp/features/feature_preapproval/preapproval.py b/sleekxmpp/features/feature_preapproval/preapproval.py index 3823c472..c7106ed3 100644 --- a/sleekxmpp/features/feature_preapproval/preapproval.py +++ b/sleekxmpp/features/feature_preapproval/preapproval.py @@ -8,7 +8,7 @@ import logging -from sleekxmpp.stanza import Iq, StreamFeatures +from sleekxmpp.stanza import StreamFeatures from sleekxmpp.features.feature_preapproval import stanza from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.plugins.base import BasePlugin diff --git a/sleekxmpp/features/feature_rosterver/rosterver.py b/sleekxmpp/features/feature_rosterver/rosterver.py index 9e0bb8e8..2991f587 100644 --- a/sleekxmpp/features/feature_rosterver/rosterver.py +++ b/sleekxmpp/features/feature_rosterver/rosterver.py @@ -8,7 +8,7 @@ import logging -from sleekxmpp.stanza import Iq, StreamFeatures +from sleekxmpp.stanza import StreamFeatures from sleekxmpp.features.feature_rosterver import stanza from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.plugins.base import BasePlugin diff --git a/sleekxmpp/jid.py b/sleekxmpp/jid.py index 752c3210..620d4160 100644 --- a/sleekxmpp/jid.py +++ b/sleekxmpp/jid.py @@ -228,7 +228,7 @@ def _validate_domain(domain): for char in label: if char in ILLEGAL_CHARS: - raise InvalidJID('Domain contains illegar characters') + raise InvalidJID('Domain contains illegal characters') if '-' in (label[0], label[-1]): raise InvalidJID('Domain started or ended with -') @@ -506,50 +506,100 @@ class JID(object): """ self._jid = JID(data)._jid - # pylint: disable=R0911 - def __getattr__(self, name): - """Retrieve the given JID component. + @property + def resource(self): + return self._jid[2] or '' - :param name: one of: user, server, domain, resource, - full, or bare. - """ - if name == 'resource': - return self._jid[2] or '' - elif name in ('user', 'username', 'local', 'node'): - return self._jid[0] or '' - elif name in ('server', 'domain', 'host'): - return self._jid[1] or '' - elif name in ('full', 'jid'): - return _format_jid(*self._jid) - elif name == 'bare': - return _format_jid(self._jid[0], self._jid[1]) - elif name == '_jid': - return getattr(super(JID, self), '_jid') - else: - return None + @property + def user(self): + return self._jid[0] or '' - # pylint: disable=W0212 - def __setattr__(self, name, value): - """Update the given JID component. + @property + def local(self): + return self._jid[0] or '' + + @property + def node(self): + return self._jid[0] or '' + + @property + def username(self): + return self._jid[0] or '' + + @property + def bare(self): + return _format_jid(self._jid[0], self._jid[1]) + + @property + def server(self): + return self._jid[1] or '' + + @property + def domain(self): + return self._jid[1] or '' + + @property + def host(self): + return self._jid[1] or '' + + @property + def full(self): + return _format_jid(*self._jid) + + @property + def jid(self): + return _format_jid(*self._jid) + + @property + def bare(self): + return _format_jid(self._jid[0], self._jid[1]) + + + @resource.setter + def resource(self, value): + self._jid = JID(self, resource=value)._jid + + @user.setter + def user(self, value): + self._jid = JID(self, local=value)._jid + + @username.setter + def username(self, value): + self._jid = JID(self, local=value)._jid + + @local.setter + def local(self, value): + self._jid = JID(self, local=value)._jid + + @node.setter + def node(self, value): + self._jid = JID(self, local=value)._jid + + @server.setter + def server(self, value): + self._jid = JID(self, domain=value)._jid + + @domain.setter + def domain(self, value): + self._jid = JID(self, domain=value)._jid + + @host.setter + def host(self, value): + self._jid = JID(self, domain=value)._jid + + @full.setter + def full(self, value): + self._jid = JID(value)._jid + + @jid.setter + def jid(self, value): + self._jid = JID(value)._jid + + @bare.setter + def bare(self, value): + parsed = JID(value)._jid + self._jid = (parsed[0], parsed[1], self._jid[2]) - :param name: one of: ``user``, ``username``, ``local``, - ``node``, ``server``, ``domain``, ``host``, - ``resource``, ``full``, ``jid``, or ``bare``. - :param value: The new string value of the JID component. - """ - if name == '_jid': - super(JID, self).__setattr__('_jid', value) - elif name == 'resource': - self._jid = JID(self, resource=value)._jid - elif name in ('user', 'username', 'local', 'node'): - self._jid = JID(self, local=value)._jid - elif name in ('server', 'domain', 'host'): - self._jid = JID(self, domain=value)._jid - elif name in ('full', 'jid'): - self._jid = JID(value)._jid - elif name == 'bare': - parsed = JID(value)._jid - self._jid = (parsed[0], parsed[1], self._jid[2]) def __str__(self): """Use the full JID as the string value.""" diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index bdd06d50..951f31eb 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -50,6 +50,7 @@ __all__ = [ 'xep_0128', # Extended Service Discovery 'xep_0131', # Standard Headers and Internet Metadata 'xep_0133', # Service Administration + 'xep_0152', # Reachability Addresses 'xep_0153', # vCard-Based Avatars 'xep_0163', # Personal Eventing Protocol 'xep_0172', # User Nickname @@ -79,6 +80,7 @@ __all__ = [ 'xep_0302', # XMPP Compliance Suites 2012 'xep_0308', # Last Message Correction 'xep_0313', # Message Archive Management + 'xep_0319', # Last User Interaction in Presence 'xep_0323', # IoT Systems Sensor Data 'xep_0325', # IoT Systems Control ] diff --git a/sleekxmpp/plugins/xep_0027/gpg.py b/sleekxmpp/plugins/xep_0027/gpg.py index 2aa6e5a0..52c1c461 100644 --- a/sleekxmpp/plugins/xep_0027/gpg.py +++ b/sleekxmpp/plugins/xep_0027/gpg.py @@ -24,7 +24,7 @@ def _extract_data(data, kind): if not begin_headers and 'BEGIN PGP %s' % kind in line: begin_headers = True continue - if begin_headers and line.stripped() == '': + if begin_headers and line.strip() == '': begin_data = True continue if 'END PGP %s' % kind in line: diff --git a/sleekxmpp/plugins/xep_0027/stanza.py b/sleekxmpp/plugins/xep_0027/stanza.py index 3170ca6e..08f2032b 100644 --- a/sleekxmpp/plugins/xep_0027/stanza.py +++ b/sleekxmpp/plugins/xep_0027/stanza.py @@ -39,7 +39,7 @@ class Encrypted(ElementBase): def set_encrypted(self, value): parent = self.parent() xmpp = parent.stream - data = xmpp['xep_0027'].encrypt(value, parent['to'].bare) + data = xmpp['xep_0027'].encrypt(value, parent['to']) if data: self.xml.text = data else: diff --git a/sleekxmpp/plugins/xep_0047/ibb.py b/sleekxmpp/plugins/xep_0047/ibb.py index e341433f..62dddac2 100644 --- a/sleekxmpp/plugins/xep_0047/ibb.py +++ b/sleekxmpp/plugins/xep_0047/ibb.py @@ -21,18 +21,21 @@ class XEP_0047(BasePlugin): dependencies = set(['xep_0030']) stanza = stanza default_config = { + 'block_size': 4096, 'max_block_size': 8192, 'window_size': 1, - 'auto_accept': True, - 'accept_stream': None + 'auto_accept': False, } def plugin_init(self): - self.streams = {} - self.pending_streams = {} - self.pending_close_streams = {} + self._streams = {} + self._pending_streams = {} + self._pending_lock = threading.Lock() self._stream_lock = threading.Lock() + self._preauthed_sids_lock = threading.Lock() + self._preauthed_sids = {} + register_stanza_plugin(Iq, Open) register_stanza_plugin(Iq, Close) register_stanza_plugin(Iq, Data) @@ -58,6 +61,13 @@ class XEP_0047(BasePlugin): StanzaPath('message/ibb_data'), self._handle_data)) + self.api.register(self._authorized, 'authorized', default=True) + self.api.register(self._authorized_sid, 'authorized_sid', default=True) + self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True) + self.api.register(self._get_stream, 'get_stream', default=True) + self.api.register(self._set_stream, 'set_stream', default=True) + self.api.register(self._del_stream, 'del_stream', default=True) + def plugin_end(self): self.xmpp.remove_handler('IBB Open') self.xmpp.remove_handler('IBB Close') @@ -68,18 +78,49 @@ class XEP_0047(BasePlugin): def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb') + def _get_stream(self, jid, sid, peer_jid, data): + return self._streams.get((jid, sid, peer_jid), None) + + def _set_stream(self, jid, sid, peer_jid, stream): + self._streams[(jid, sid, peer_jid)] = stream + + def _del_stream(self, jid, sid, peer_jid, data): + with self._stream_lock: + if (jid, sid, peer_jid) in self._streams: + del self._streams[(jid, sid, peer_jid)] + def _accept_stream(self, iq): - if self.accept_stream is not None: - return self.accept_stream(iq) + receiver = iq['to'] + sender = iq['from'] + sid = iq['ibb_open']['sid'] + + if self.api['authorized_sid'](receiver, sid, sender, iq): + return True + return self.api['authorized'](receiver, sid, sender, iq) + + def _authorized(self, jid, sid, ifrom, iq): if self.auto_accept: if iq['ibb_open']['block_size'] <= self.max_block_size: return True return False - def open_stream(self, jid, block_size=4096, sid=None, window=1, use_messages=False, + def _authorized_sid(self, jid, sid, ifrom, iq): + with self._preauthed_sids_lock: + if (jid, sid, ifrom) in self._preauthed_sids: + del self._preauthed_sids[(jid, sid, ifrom)] + return True + return False + + def _preauthorize_sid(self, jid, sid, ifrom, data): + with self._preauthed_sids_lock: + self._preauthed_sids[(jid, sid, ifrom)] = True + + def open_stream(self, jid, block_size=None, sid=None, window=1, use_messages=False, ifrom=None, block=True, timeout=None, callback=None): if sid is None: sid = str(uuid.uuid4()) + if block_size is None: + block_size = self.block_size iq = self.xmpp.Iq() iq['type'] = 'set' @@ -90,13 +131,13 @@ class XEP_0047(BasePlugin): iq['ibb_open']['stanza'] = 'iq' stream = IBBytestream(self.xmpp, sid, block_size, - iq['to'], iq['from'], window, + iq['from'], iq['to'], window, use_messages) with self._stream_lock: - self.pending_streams[iq['id']] = stream + self._pending_streams[iq['id']] = stream - self.pending_streams[iq['id']] = stream + self._pending_streams[iq['id']] = stream if block: resp = iq.send(timeout=timeout) @@ -116,49 +157,59 @@ class XEP_0047(BasePlugin): def _handle_opened_stream(self, iq): if iq['type'] == 'result': with self._stream_lock: - stream = self.pending_streams.get(iq['id'], None) - if stream is not None: - stream.sender = iq['to'] - stream.receiver = iq['from'] - stream.stream_started.set() - self.streams[stream.sid] = stream - self.xmpp.event('ibb_stream_start', stream) + stream = self._pending_streams.get(iq['id'], None) + if stream is not None: + log.debug('IBB stream (%s) accepted by %s', stream.sid, iq['from']) + stream.self_jid = iq['to'] + stream.peer_jid = iq['from'] + stream.stream_started.set() + self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream) + self.xmpp.event('ibb_stream_start', stream) + self.xmpp.event('stream:%s:%s' % (stream.sid, stream.peer_jid), stream) with self._stream_lock: - if iq['id'] in self.pending_streams: - del self.pending_streams[iq['id']] + if iq['id'] in self._pending_streams: + del self._pending_streams[iq['id']] def _handle_open_request(self, iq): sid = iq['ibb_open']['sid'] - size = iq['ibb_open']['block_size'] + size = iq['ibb_open']['block_size'] or self.block_size + + log.debug('Received IBB stream request from %s', iq['from']) + + if not sid: + raise XMPPError(etype='modify', condition='bad-request') + if not self._accept_stream(iq): - raise XMPPError('not-acceptable') + raise XMPPError(etype='modify', condition='not-acceptable') if size > self.max_block_size: raise XMPPError('resource-constraint') stream = IBBytestream(self.xmpp, sid, size, - iq['from'], iq['to'], + iq['to'], iq['from'], self.window_size) stream.stream_started.set() - self.streams[sid] = stream + self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream) iq.reply() iq.send() self.xmpp.event('ibb_stream_start', stream) + self.xmpp.event('stream:%s:%s' % (sid, stream.peer_jid), stream) def _handle_data(self, stanza): sid = stanza['ibb_data']['sid'] - stream = self.streams.get(sid, None) - if stream is not None and stanza['from'] != stream.sender: + stream = self.api['get_stream'](stanza['to'], sid, stanza['from']) + if stream is not None and stanza['from'] == stream.peer_jid: stream._recv_data(stanza) else: raise XMPPError('item-not-found') def _handle_close(self, iq): sid = iq['ibb_close']['sid'] - stream = self.streams.get(sid, None) - if stream is not None and iq['from'] != stream.sender: + stream = self.api['get_stream'](iq['to'], sid, iq['from']) + if stream is not None and iq['from'] == stream.peer_jid: stream._closed(iq) + self.api['del_stream'](stream.self_jid, stream.sid, stream.peer_jid) else: raise XMPPError('item-not-found') diff --git a/sleekxmpp/plugins/xep_0047/stream.py b/sleekxmpp/plugins/xep_0047/stream.py index adc86450..9651edf8 100644 --- a/sleekxmpp/plugins/xep_0047/stream.py +++ b/sleekxmpp/plugins/xep_0047/stream.py @@ -12,15 +12,17 @@ log = logging.getLogger(__name__) class IBBytestream(object): - def __init__(self, xmpp, sid, block_size, to, ifrom, window_size=1, use_messages=False): + def __init__(self, xmpp, sid, block_size, jid, peer, window_size=1, use_messages=False): self.xmpp = xmpp self.sid = sid self.block_size = block_size self.window_size = window_size self.use_messages = use_messages - self.receiver = to - self.sender = ifrom + if jid is None: + jid = xmpp.boundjid + self.self_jid = jid + self.peer_jid = peer self.send_seq = -1 self.recv_seq = -1 @@ -50,8 +52,8 @@ class IBBytestream(object): seq = self.send_seq if self.use_messages: msg = self.xmpp.Message() - msg['to'] = self.receiver - msg['from'] = self.sender + msg['to'] = self.peer_jid + msg['from'] = self.self_jid msg['id'] = self.xmpp.new_id() msg['ibb_data']['sid'] = self.sid msg['ibb_data']['seq'] = seq @@ -61,8 +63,8 @@ class IBBytestream(object): else: iq = self.xmpp.Iq() iq['type'] = 'set' - iq['to'] = self.receiver - iq['from'] = self.sender + iq['to'] = self.peer_jid + iq['from'] = self.self_jid iq['ibb_data']['sid'] = self.sid iq['ibb_data']['seq'] = seq iq['ibb_data']['data'] = data @@ -121,8 +123,8 @@ class IBBytestream(object): def close(self): iq = self.xmpp.Iq() iq['type'] = 'set' - iq['to'] = self.receiver - iq['from'] = self.sender + iq['to'] = self.peer_jid + iq['from'] = self.self_jid iq['ibb_close']['sid'] = self.sid self.stream_out_closed.set() iq.send(block=False, @@ -132,9 +134,6 @@ class IBBytestream(object): def _closed(self, iq): self.stream_in_closed.set() self.stream_out_closed.set() - while not self.window_empty.is_set(): - log.info('waiting for send window to empty') - self.window_empty.wait(timeout=1) iq.reply() iq.send() self.xmpp.event('ibb_stream_end', self) diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py index c10ac762..d975a46d 100644 --- a/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py @@ -94,7 +94,9 @@ class OwnerRedirect(ElementBase): class OwnerSubscriptions(Subscriptions): + name = 'subscriptions' namespace = 'http://jabber.org/protocol/pubsub#owner' + plugin_attrib = name interfaces = set(('node',)) def append(self, subscription): diff --git a/sleekxmpp/plugins/xep_0065/proxy.py b/sleekxmpp/plugins/xep_0065/proxy.py index 473dd033..265d3030 100644 --- a/sleekxmpp/plugins/xep_0065/proxy.py +++ b/sleekxmpp/plugins/xep_0065/proxy.py @@ -25,6 +25,9 @@ class XEP_0065(base_plugin): name = 'xep_0065' description = "Socks5 Bytestreams" dependencies = set(['xep_0030']) + default_config = { + 'auto_accept': False + } def plugin_init(self): register_stanza_plugin(Iq, Socks5) @@ -33,11 +36,18 @@ class XEP_0065(base_plugin): self._sessions = {} self._sessions_lock = threading.Lock() + self._preauthed_sids_lock = threading.Lock() + self._preauthed_sids = {} + self.xmpp.register_handler( Callback('Socks5 Bytestreams', StanzaPath('iq@type=set/socks/streamhost'), self._handle_streamhost)) + self.api.register(self._authorized, 'authorized', default=True) + self.api.register(self._authorized_sid, 'authorized_sid', default=True) + self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True) + def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Socks5.namespace) @@ -50,14 +60,15 @@ class XEP_0065(base_plugin): """Returns the socket associated to the SID.""" return self._sessions.get(sid, None) - def handshake(self, to, ifrom=None, timeout=None): + def handshake(self, to, ifrom=None, sid=None, timeout=None): """ Starts the handshake to establish the socks5 bytestreams connection. """ if not self._proxies: self._proxies = self.discover_proxies() - sid = uuid4().hex + if sid is None: + sid = uuid4().hex used = self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout) proxy = used['socks']['streamhost_used']['jid'] @@ -72,10 +83,12 @@ class XEP_0065(base_plugin): self.xmpp.boundjid, to, self._proxies[proxy][0], - self._proxies[proxy][1]) + self._proxies[proxy][1], + peer=to) # Request that the proxy activate the session with the target. self.activate(proxy, sid, to, timeout=timeout) + self.xmpp.event('stream:%s:%s' % (sid, conn.peer_jid), conn) return self.get_socket(sid) def request_stream(self, to, sid=None, ifrom=None, block=True, timeout=None, callback=None): @@ -139,19 +152,24 @@ class XEP_0065(base_plugin): """Handle incoming SOCKS5 session request.""" sid = iq['socks']['sid'] if not sid: + raise XMPPError(etype='modify', condition='bad-request') + + if not self._accept_stream(iq): raise XMPPError(etype='modify', condition='not-acceptable') streamhosts = iq['socks']['streamhosts'] conn = None used_streamhost = None + sender = iq['from'] for streamhost in streamhosts: try: conn = self._connect_proxy(sid, - iq['from'], + sender, self.xmpp.boundjid, streamhost['host'], - streamhost['port']) + streamhost['port'], + peer=sender) used_streamhost = streamhost['jid'] break except socket.error: @@ -165,6 +183,8 @@ class XEP_0065(base_plugin): iq['socks']['sid'] = sid iq['socks']['streamhost_used']['jid'] = used_streamhost iq.send() + self.xmpp.event('socks5_stream', conn) + self.xmpp.event('stream:%s:%s' % (sid, conn.peer_jid), conn) def activate(self, proxy, sid, target, ifrom=None, block=True, timeout=None, callback=None): """Activate the socks5 session that has been negotiated.""" @@ -191,7 +211,7 @@ class XEP_0065(base_plugin): with self._sessions_lock: self._sessions = {} - def _connect_proxy(self, sid, requester, target, proxy, proxy_port): + def _connect_proxy(self, sid, requester, target, proxy, proxy_port, peer=None): """ Establishes a connection between the client and the server-side Socks5 proxy. @@ -200,6 +220,8 @@ class XEP_0065(base_plugin): target : The JID of the target. <str> proxy_host : The hostname or the IP of the proxy. <str> proxy_port : The port of the proxy. <str> or <int> + peer : The JID for the other side of the stream, regardless + of target or requester status. """ # Because the xep_0065 plugin uses the proxy_port as string, # the Proxy class accepts the proxy_port argument as a string @@ -230,6 +252,34 @@ class XEP_0065(base_plugin): _close() sock.close = close - self.xmpp.event('socks_connected', sid) + sock.peer_jid = peer + sock.self_jid = target if requester == peer else requester + self.xmpp.event('socks_connected', sid) return sock + + def _accept_stream(self, iq): + receiver = iq['to'] + sender = iq['from'] + sid = iq['socks']['sid'] + + if self.api['authorized_sid'](receiver, sid, sender, iq): + return True + return self.api['authorized'](receiver, sid, sender, iq) + + def _authorized(self, jid, sid, ifrom, iq): + return self.auto_accept + + def _authorized_sid(self, jid, sid, ifrom, iq): + with self._preauthed_sids_lock: + log.debug('>>> authed sids: %s', self._preauthed_sids) + log.debug('>>> lookup: %s %s %s', jid, sid, ifrom) + if (jid, sid, ifrom) in self._preauthed_sids: + del self._preauthed_sids[(jid, sid, ifrom)] + return True + return False + + def _preauthorize_sid(self, jid, sid, ifrom, data): + log.debug('>>>> %s %s %s %s', jid, sid, ifrom, data) + with self._preauthed_sids_lock: + self._preauthed_sids[(jid, sid, ifrom)] = True diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py index 02571fa7..26eb68fa 100644 --- a/sleekxmpp/plugins/xep_0082.py +++ b/sleekxmpp/plugins/xep_0082.py @@ -6,7 +6,6 @@ See the file LICENSE for copying permission. """ -import logging import datetime as dt from sleekxmpp.plugins import BasePlugin, register_plugin diff --git a/sleekxmpp/plugins/xep_0084/avatar.py b/sleekxmpp/plugins/xep_0084/avatar.py index 2454afc7..677a888d 100644 --- a/sleekxmpp/plugins/xep_0084/avatar.py +++ b/sleekxmpp/plugins/xep_0084/avatar.py @@ -82,6 +82,7 @@ class XEP_0084(BasePlugin): metadata.add_pointer(pointer) return self.xmpp['xep_0163'].publish(metadata, + id=info['id'], ifrom=ifrom, block=block, callback=callback, diff --git a/sleekxmpp/plugins/xep_0095/__init__.py b/sleekxmpp/plugins/xep_0095/__init__.py new file mode 100644 index 00000000..4465ef5c --- /dev/null +++ b/sleekxmpp/plugins/xep_0095/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0095 import stanza +from sleekxmpp.plugins.xep_0095.stanza import SI +from sleekxmpp.plugins.xep_0095.stream_initiation import XEP_0095 + + +register_plugin(XEP_0095) diff --git a/sleekxmpp/plugins/xep_0095/stanza.py b/sleekxmpp/plugins/xep_0095/stanza.py new file mode 100644 index 00000000..34999a11 --- /dev/null +++ b/sleekxmpp/plugins/xep_0095/stanza.py @@ -0,0 +1,25 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase + + +class SI(ElementBase): + name = 'si' + namespace = 'http://jabber.org/protocol/si' + plugin_attrib = 'si' + interfaces = set(['id', 'mime_type', 'profile']) + + def get_mime_type(self): + return self._get_attr('mime-type', 'application/octet-stream') + + def set_mime_type(self, value): + self._set_attr('mime-type', value) + + def del_mime_type(self): + self._del_attr('mime-type') diff --git a/sleekxmpp/plugins/xep_0095/stream_initiation.py b/sleekxmpp/plugins/xep_0095/stream_initiation.py new file mode 100644 index 00000000..927248a5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0095/stream_initiation.py @@ -0,0 +1,214 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import threading + +from uuid import uuid4 + +from sleekxmpp import Iq, Message +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins.xep_0095 import stanza, SI + + +log = logging.getLogger(__name__) + + +SOCKS5 = 'http://jabber.org/protocol/bytestreams' +IBB = 'http://jabber.org/protocol/ibb' + + +class XEP_0095(BasePlugin): + + name = 'xep_0095' + description = 'XEP-0095: Stream Initiation' + dependencies = set(['xep_0020', 'xep_0030', 'xep_0047', 'xep_0065']) + stanza = stanza + + def plugin_init(self): + self._profiles = {} + self._methods = {} + self._methods_order = [] + self._pending_lock = threading.Lock() + self._pending= {} + + self.register_method(SOCKS5, 'xep_0065', 100) + self.register_method(IBB, 'xep_0047', 50) + + register_stanza_plugin(Iq, SI) + register_stanza_plugin(SI, self.xmpp['xep_0020'].stanza.FeatureNegotiation) + + self.xmpp.register_handler( + Callback('SI Request', + StanzaPath('iq@type=set/si'), + self._handle_request)) + + self.api.register(self._add_pending, 'add_pending', default=True) + self.api.register(self._get_pending, 'get_pending', default=True) + self.api.register(self._del_pending, 'del_pending', default=True) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(SI.namespace) + + def plugin_end(self): + self.xmpp.remove_handler('SI Request') + self.xmpp['xep_0030'].del_feature(feature=SI.namespace) + + def register_profile(self, profile_name, plugin): + self._profiles[profile_name] = plugin + + def unregister_profile(self, profile_name): + try: + del self._profiles[profile_name] + except KeyError: + pass + + def register_method(self, method, plugin_name, order=50): + self._methods[method] = (plugin_name, order) + self._methods_order.append((order, method, plugin_name)) + self._methods_order.sort() + + def unregister_method(self, method): + if method in self._methods: + plugin_name, order = self._methods[method] + del self._methods[method] + self._methods_order.remove((order, method, plugin_name)) + self._methods_order.sort() + + def _handle_request(self, iq): + profile = iq['si']['profile'] + sid = iq['si']['id'] + + if not sid: + raise XMPPError(etype='modify', condition='bad-request') + if profile not in self._profiles: + raise XMPPError( + etype='modify', + condition='bad-request', + extension='bad-profile', + extension_ns=SI.namespace) + + neg = iq['si']['feature_neg']['form']['fields'] + options = neg['stream-method']['options'] or [] + methods = [] + for opt in options: + methods.append(opt['value']) + for method in methods: + if method in self._methods: + supported = True + break + else: + raise XMPPError('bad-request', + extension='no-valid-streams', + extension_ns=SI.namespace) + + selected_method = None + log.debug('Available: %s', methods) + for order, method, plugin in self._methods_order: + log.debug('Testing: %s', method) + if method in methods: + selected_method = method + break + + receiver = iq['to'] + sender = iq['from'] + + self.api['add_pending'](receiver, sid, sender, { + 'response_id': iq['id'], + 'method': selected_method, + 'profile': profile + }) + self.xmpp.event('si_request', iq) + + def offer(self, jid, sid=None, mime_type=None, profile=None, + methods=None, payload=None, ifrom=None, + **iqargs): + if sid is None: + sid = uuid4().hex + if methods is None: + methods = list(self._methods.keys()) + if not isinstance(methods, (list, tuple, set)): + methods = [methods] + + si = self.xmpp.Iq() + si['to'] = jid + si['from'] = ifrom + si['type'] = 'set' + si['si']['id'] = sid + si['si']['mime_type'] = mime_type + si['si']['profile'] = profile + if not isinstance(payload, (list, tuple, set)): + payload = [payload] + for item in payload: + si['si'].append(item) + si['si']['feature_neg']['form'].add_field( + var='stream-method', + ftype='list-single', + options=methods) + return si.send(**iqargs) + + def accept(self, jid, sid, payload=None, ifrom=None, stream_handler=None): + stream = self.api['get_pending'](ifrom, sid, jid) + iq = self.xmpp.Iq() + iq['id'] = stream['response_id'] + iq['to'] = jid + iq['from'] = ifrom + iq['type'] = 'result' + if payload: + iq['si'].append(payload) + iq['si']['feature_neg']['form']['type'] = 'submit' + iq['si']['feature_neg']['form'].add_field( + var='stream-method', + ftype='list-single', + value=stream['method']) + + if ifrom is None: + ifrom = self.xmpp.boundjid + + method_plugin = self._methods[stream['method']][0] + self.xmpp[method_plugin].api['preauthorize_sid'](ifrom, sid, jid) + + self.api['del_pending'](ifrom, sid, jid) + + if stream_handler: + self.xmpp.add_event_handler('stream:%s:%s' % (sid, jid), + stream_handler, + threaded=True, + disposable=True) + return iq.send() + + def decline(self, jid, sid, ifrom=None): + stream = self.api['get_pending'](ifrom, sid, jid) + if not stream: + return + iq = self.xmpp.Iq() + iq['id'] = stream['response_id'] + iq['to'] = jid + iq['from'] = ifrom + iq['type'] = 'error' + iq['error']['condition'] = 'forbidden' + iq['error']['text'] = 'Offer declined' + self.api['del_pending'](ifrom, sid, jid) + return iq.send() + + def _add_pending(self, jid, node, ifrom, data): + with self._pending_lock: + self._pending[(jid, node, ifrom)] = data + + def _get_pending(self, jid, node, ifrom, data): + with self._pending_lock: + return self._pending.get((jid, node, ifrom), None) + + def _del_pending(self, jid, node, ifrom, data): + with self._pending_lock: + if (jid, node, ifrom) in self._pending: + del self._pending[(jid, node, ifrom)] diff --git a/sleekxmpp/plugins/xep_0096/__init__.py b/sleekxmpp/plugins/xep_0096/__init__.py new file mode 100644 index 00000000..5f836169 --- /dev/null +++ b/sleekxmpp/plugins/xep_0096/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0096 import stanza +from sleekxmpp.plugins.xep_0096.stanza import File +from sleekxmpp.plugins.xep_0096.file_transfer import XEP_0096 + + +register_plugin(XEP_0096) diff --git a/sleekxmpp/plugins/xep_0096/file_transfer.py b/sleekxmpp/plugins/xep_0096/file_transfer.py new file mode 100644 index 00000000..6873c7f5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0096/file_transfer.py @@ -0,0 +1,58 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp import Iq, Message +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins.xep_0096 import stanza, File + + +log = logging.getLogger(__name__) + + +class XEP_0096(BasePlugin): + + name = 'xep_0096' + description = 'XEP-0096: SI File Transfer' + dependencies = set(['xep_0095']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(self.xmpp['xep_0095'].stanza.SI, File) + + self.xmpp['xep_0095'].register_profile(File.namespace, self) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(File.namespace) + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=File.namespace) + self.xmpp['xep_0095'].unregister_profile(File.namespace, self) + + def request_file_transfer(self, jid, sid=None, name=None, size=None, + desc=None, hash=None, date=None, + allow_ranged=False, mime_type=None, + **iqargs): + data = File() + data['name'] = name + data['size'] = size + data['date'] = date + data['desc'] = desc + if allow_ranged: + data.enable('range') + + return self.xmpp['xep_0095'].offer(jid, + sid=sid, + mime_type=mime_type, + profile=File.namespace, + payload=data, + **iqargs) diff --git a/sleekxmpp/plugins/xep_0096/stanza.py b/sleekxmpp/plugins/xep_0096/stanza.py new file mode 100644 index 00000000..65eb5bc5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0096/stanza.py @@ -0,0 +1,48 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt + +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin +from sleekxmpp.plugins import xep_0082 + + +class File(ElementBase): + name = 'file' + namespace = 'http://jabber.org/protocol/si/profile/file-transfer' + plugin_attrib = 'file' + interfaces = set(['name', 'size', 'date', 'hash', 'desc']) + sub_interfaces = set(['desc']) + + def set_size(self, value): + self._set_attr('size', str(value)) + + def get_date(self): + timestamp = self._get_attr('date') + return xep_0082.parse(timestamp) + + def set_date(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('date', value) + + +class Range(ElementBase): + name = 'range' + namespace = 'http://jabber.org/protocol/si/profile/file-transfer' + plugin_attrib = 'range' + interfaces = set(['length', 'offset']) + + def set_length(self, value): + self._set_attr('length', str(value)) + + def set_offset(self, value): + self._set_attr('offset', str(value)) + + +register_stanza_plugin(File, Range) diff --git a/sleekxmpp/plugins/xep_0152/__init__.py b/sleekxmpp/plugins/xep_0152/__init__.py new file mode 100644 index 00000000..7de031b7 --- /dev/null +++ b/sleekxmpp/plugins/xep_0152/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0152 import stanza +from sleekxmpp.plugins.xep_0152.stanza import Reachability +from sleekxmpp.plugins.xep_0152.reachability import XEP_0152 + + +register_plugin(XEP_0152) diff --git a/sleekxmpp/plugins/xep_0152/reachability.py b/sleekxmpp/plugins/xep_0152/reachability.py new file mode 100644 index 00000000..4cf81739 --- /dev/null +++ b/sleekxmpp/plugins/xep_0152/reachability.py @@ -0,0 +1,93 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0152 import stanza, Reachability + + +log = logging.getLogger(__name__) + + +class XEP_0152(BasePlugin): + + """ + XEP-0152: Reachability Addresses + """ + + name = 'xep_0152' + description = 'XEP-0152: Reachability Addresses' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=Reachability.namespace) + self.xmpp['xep_0163'].remove_interest(Reachability.namespace) + + def session_bind(self, jid): + self.xmpp['xep_0163'].register_pep('reachability', Reachability) + + def publish_reachability(self, addresses, options=None, + ifrom=None, block=True, callback=None, timeout=None): + """ + Publish alternative addresses where the user can be reached. + + Arguments: + addresses -- A list of dictionaries containing the URI and + optional description for each address. + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + if not isinstance(addresses, (list, tuple)): + addresses = [addresses] + reach = Reachability() + for address in addresses: + if not hasattr(address, 'items'): + address = {'uri': address} + + addr = stanza.Address() + for key, val in address.items(): + addr[key] = val + reach.append(addr) + return self.xmpp['xep_0163'].publish(reach, + node=Reachability.namespace, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user activity information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + reach = Reachability() + return self.xmpp['xep_0163'].publish(reach, + node=Reachability.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0152/stanza.py b/sleekxmpp/plugins/xep_0152/stanza.py new file mode 100644 index 00000000..bd173ce1 --- /dev/null +++ b/sleekxmpp/plugins/xep_0152/stanza.py @@ -0,0 +1,29 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin + + +class Reachability(ElementBase): + name = 'reach' + namespace = 'urn:xmpp:reach:0' + plugin_attrib = 'reach' + interfaces = set() + + +class Address(ElementBase): + name = 'addr' + namespace = 'urn:xmpp:reach:0' + plugin_attrib = 'address' + plugin_multi_attrib = 'addresses' + interfaces = set(['uri', 'desc']) + lang_interfaces = set(['desc']) + sub_interfaces = set(['desc']) + + +register_stanza_plugin(Reachability, Address, iterable=True) diff --git a/sleekxmpp/plugins/xep_0319/__init__.py b/sleekxmpp/plugins/xep_0319/__init__.py new file mode 100644 index 00000000..4756e63e --- /dev/null +++ b/sleekxmpp/plugins/xep_0319/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0319 import stanza +from sleekxmpp.plugins.xep_0319.stanza import Idle +from sleekxmpp.plugins.xep_0319.idle import XEP_0319 + + +register_plugin(XEP_0319) diff --git a/sleekxmpp/plugins/xep_0319/idle.py b/sleekxmpp/plugins/xep_0319/idle.py new file mode 100644 index 00000000..e541e0ad --- /dev/null +++ b/sleekxmpp/plugins/xep_0319/idle.py @@ -0,0 +1,75 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from datetime import datetime, timedelta + +from sleekxmpp.stanza import Presence +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.xep_0319 import stanza + + +class XEP_0319(BasePlugin): + name = 'xep_0319' + description = 'XEP-0319: Last User Interaction in Presence' + dependencies = set(['xep_0012']) + stanza = stanza + + def plugin_init(self): + self._idle_stamps = {} + register_stanza_plugin(Presence, stanza.Idle) + self.api.register(self._set_idle, + 'set_idle', + default=True) + self.api.register(self._get_idle, + 'get_idle', + default=True) + self.xmpp.register_handler( + Callback('Idle Presence', + StanzaPath('presence/idle'), + self._idle_presence)) + self.xmpp.add_filter('out', self._stamp_idle_presence) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature('urn:xmpp:idle:0') + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:idle:0') + self.xmpp.del_filter('out', self._stamp_idle_presence) + self.xmpp.remove_handler('Idle Presence') + + def idle(self, jid=None, since=None): + seconds = None + if since is None: + since = datetime.now() + else: + seconds = datetime.now() - since + self.api['set_idle'](jid, None, None, since) + self.xmpp['xep_0012'].set_last_activity(jid=jid, seconds=seconds) + + def active(self, jid=None): + self.api['set_idle'](jid, None, None, None) + self.xmpp['xep_0012'].del_last_activity(jid) + + def _set_idle(self, jid, node, ifrom, data): + self._idle_stamps[jid] = data + + def _get_idle(self, jid, node, ifrom, data): + return self._idle_stamps.get(jid, None) + + def _idle_presence(self, pres): + self.xmpp.event('presence_idle', pres) + + def _stamp_idle_presence(self, stanza): + if isinstance(stanza, Presence): + since = self.api['get_idle'](stanza['from'] or self.xmpp.boundjid) + if since: + stanza['idle']['since'] = since + return stanza diff --git a/sleekxmpp/plugins/xep_0319/stanza.py b/sleekxmpp/plugins/xep_0319/stanza.py new file mode 100644 index 00000000..a0bb7272 --- /dev/null +++ b/sleekxmpp/plugins/xep_0319/stanza.py @@ -0,0 +1,28 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt + +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class Idle(ElementBase): + name = 'idle' + namespace = 'urn:xmpp:idle:0' + plugin_attrib = 'idle' + interfaces = set(['since']) + + def get_since(self): + timestamp = self._get_attr('since') + return xep_0082.parse(timestamp) + + def set_since(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('since', value) diff --git a/sleekxmpp/roster/__init__.py b/sleekxmpp/roster/__init__.py index 4335d367..18b380c9 100644 --- a/sleekxmpp/roster/__init__.py +++ b/sleekxmpp/roster/__init__.py @@ -6,7 +6,6 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.xmlstream import JID from sleekxmpp.roster.item import RosterItem from sleekxmpp.roster.single import RosterNode from sleekxmpp.roster.multi import Roster diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py index 901c3a56..04fb106d 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -9,14 +9,12 @@ import unittest from xml.parsers.expat import ExpatError -import sleekxmpp from sleekxmpp import ClientXMPP, ComponentXMPP from sleekxmpp.util import Queue from sleekxmpp.stanza import Message, Iq, Presence from sleekxmpp.test import TestSocket, TestLiveSocket -from sleekxmpp.exceptions import XMPPError, IqTimeout, IqError -from sleekxmpp.xmlstream import ET, register_stanza_plugin -from sleekxmpp.xmlstream import ElementBase, StanzaBase +from sleekxmpp.xmlstream import ET +from sleekxmpp.xmlstream import ElementBase from sleekxmpp.xmlstream.tostring import tostring from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath diff --git a/sleekxmpp/thirdparty/mini_dateutil.py b/sleekxmpp/thirdparty/mini_dateutil.py index 93f26312..e751a448 100644 --- a/sleekxmpp/thirdparty/mini_dateutil.py +++ b/sleekxmpp/thirdparty/mini_dateutil.py @@ -108,7 +108,7 @@ except: def __init__(self, name, offset): self._name = name - self._offset = datetime.timedelta(seconds=offset) + self._offset = datetime.timedelta(minutes=offset) def utcoffset(self, dt): return self._offset @@ -154,7 +154,7 @@ except: absoff = offsetmins name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60) - inst = tzoffset(offsetmins, name) + inst = tzoffset(name,offsetmins) _fixed_offset_tzs[offsetmins] = inst return _fixed_offset_tzs[offsetmins] diff --git a/sleekxmpp/thirdparty/socks.py b/sleekxmpp/thirdparty/socks.py index a6c0d70e..9239a7b9 100644 --- a/sleekxmpp/thirdparty/socks.py +++ b/sleekxmpp/thirdparty/socks.py @@ -13,7 +13,7 @@ are permitted provided that the following conditions are met: 3. Neither the name of Dan Haim nor the names of his contributors may be used to endorse or promote products derived from this software without specific prior written permission. - + THIS SOFTWARE IS PROVIDED BY DAN HAIM "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 @@ -28,9 +28,6 @@ OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. This module provides a standard socket-like interface for Python for tunneling connections through SOCKS proxies. -""" - -""" Minor modifications made by Christopher Gilbert (http://motomastyle.com/) for use in PyLoris (http://pyloris.sourceforge.net/) @@ -42,7 +39,6 @@ mainly to merge bug fixes found in Sourceforge import socket import struct -import sys PROXY_TYPE_SOCKS4 = 1 PROXY_TYPE_SOCKS5 = 2 diff --git a/sleekxmpp/thirdparty/statemachine.py b/sleekxmpp/thirdparty/statemachine.py index 4b5ecd6b..113320fa 100644 --- a/sleekxmpp/thirdparty/statemachine.py +++ b/sleekxmpp/thirdparty/statemachine.py @@ -15,7 +15,8 @@ log = logging.getLogger(__name__) class StateMachine(object): - def __init__(self, states=[]): + def __init__(self, states=None): + if not states: states = [] self.lock = threading.Condition() self.__states = [] self.addStates(states) diff --git a/sleekxmpp/util/sasl/__init__.py b/sleekxmpp/util/sasl/__init__.py index d054ce09..2d344e9b 100644 --- a/sleekxmpp/util/sasl/__init__.py +++ b/sleekxmpp/util/sasl/__init__.py @@ -7,7 +7,9 @@ Part of SleekXMPP: The Sleek XMPP Library - :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout + :copryight: (c) 2004-2013 David Alan Cridland + :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout + :license: MIT, see LICENSE for more details """ diff --git a/sleekxmpp/util/sasl/client.py b/sleekxmpp/util/sasl/client.py index 0bfb63f8..fd685547 100644 --- a/sleekxmpp/util/sasl/client.py +++ b/sleekxmpp/util/sasl/client.py @@ -7,7 +7,9 @@ Part of SleekXMPP: The Sleek XMPP Library - :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout + :copryight: (c) 2004-2013 David Alan Cridland + :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout + :license: MIT, see LICENSE for more details """ diff --git a/sleekxmpp/util/sasl/mechanisms.py b/sleekxmpp/util/sasl/mechanisms.py index 373ae039..1eb6af83 100644 --- a/sleekxmpp/util/sasl/mechanisms.py +++ b/sleekxmpp/util/sasl/mechanisms.py @@ -9,7 +9,9 @@ Part of SleekXMPP: The Sleek XMPP Library - :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout + :copryight: (c) 2004-2013 David Alan Cridland + :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout + :license: MIT, see LICENSE for more details """ diff --git a/sleekxmpp/util/stringprep_profiles.py b/sleekxmpp/util/stringprep_profiles.py index ad89d4cc..84326bc3 100644 --- a/sleekxmpp/util/stringprep_profiles.py +++ b/sleekxmpp/util/stringprep_profiles.py @@ -16,9 +16,8 @@ from __future__ import unicode_literals -import sys import stringprep -import unicodedata +from unicodedata import ucd_3_2_0 as unicodedata from sleekxmpp.util import unicode diff --git a/sleekxmpp/xmlstream/cert.py b/sleekxmpp/xmlstream/cert.py index fa12f794..71146f36 100644 --- a/sleekxmpp/xmlstream/cert.py +++ b/sleekxmpp/xmlstream/cert.py @@ -1,6 +1,10 @@ import logging from datetime import datetime, timedelta +# Make a call to strptime before starting threads to +# prevent thread safety issues. +datetime.strptime('1970-01-01 12:00:00', "%Y-%m-%d %H:%M:%S") + try: from pyasn1.codec.der import decoder, encoder diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py index d4537998..53b83bc7 100644 --- a/sleekxmpp/xmlstream/filesocket.py +++ b/sleekxmpp/xmlstream/filesocket.py @@ -13,6 +13,7 @@ """ from socket import _fileobject +import errno import socket @@ -29,7 +30,13 @@ class FileSocket(_fileobject): """Read data from the socket as if it were a file.""" if self._sock is None: return None - data = self._sock.recv(size) + while True: + try: + data = self._sock.recv(size) + break + except socket.error as serr: + if serr.errno != errno.EINTR: + raise if data is not None: return data diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index cb202448..56f728e1 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -37,11 +37,11 @@ class MatchXMLMask(MatcherBase): object or XML string to use as a mask. """ - def __init__(self, criteria): + def __init__(self, criteria, default_ns='jabber:client'): MatcherBase.__init__(self, criteria) if isinstance(criteria, str): self._criteria = ET.fromstring(self._criteria) - self.default_ns = 'jabber:client' + self.default_ns = default_ns def setDefaultNS(self, ns): """Set the default namespace to use during comparisons. diff --git a/sleekxmpp/xmlstream/resolver.py b/sleekxmpp/xmlstream/resolver.py index 16f8a7ad..6f26797f 100644 --- a/sleekxmpp/xmlstream/resolver.py +++ b/sleekxmpp/xmlstream/resolver.py @@ -202,11 +202,14 @@ def get_AAAA(host, resolver=None): # If not using dnspython, attempt lookup using the OS level # getaddrinfo() method. if resolver is None: + if not socket.has_ipv6: + log.debug("Unable to query %s for AAAA records: IPv6 is not supported", host) + return [] try: recs = socket.getaddrinfo(host, None, socket.AF_INET6, socket.SOCK_STREAM) return [rec[4][0] for rec in recs] - except socket.gaierror: + except (OSError, socket.gaierror): log.debug("DNS: Error retreiving AAAA address " + \ "info for %s." % host) return [] diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py index b3e50983..e6fae37a 100644 --- a/sleekxmpp/xmlstream/scheduler.py +++ b/sleekxmpp/xmlstream/scheduler.py @@ -20,6 +20,11 @@ import itertools from sleekxmpp.util import Queue, QueueEmpty +#: The time in seconds to wait for events from the event queue, and also the +#: time between checks for the process stop signal. +WAIT_TIMEOUT = 1.0 + + log = logging.getLogger(__name__) @@ -76,7 +81,7 @@ class Task(object): """ if self.qpointer is not None: self.qpointer.put(('schedule', self.callback, - self.args, self.name)) + self.args, self.kwargs, self.name)) else: self.callback(*self.args, **self.kwargs) self.reset() @@ -120,6 +125,10 @@ class Scheduler(object): #: Lock for accessing the task queue. self.schedule_lock = threading.RLock() + #: The time in seconds to wait for events from the event queue, + #: and also the time between checks for the process stop signal. + self.wait_timeout = WAIT_TIMEOUT + def process(self, threaded=True, daemon=False): """Begin accepting and processing scheduled tasks. @@ -139,24 +148,25 @@ class Scheduler(object): self.run = True try: while self.run and not self.stop.is_set(): - wait = 0.1 updated = False if self.schedule: wait = self.schedule[0].next - time.time() + else: + wait = self.wait_timeout try: if wait <= 0.0: newtask = self.addq.get(False) else: - if wait >= 3.0: - wait = 3.0 newtask = None - elapsed = 0 - while not self.stop.is_set() and \ + while self.run and \ + not self.stop.is_set() and \ newtask is None and \ - elapsed < wait: - newtask = self.addq.get(True, 0.1) - elapsed += 0.1 - except QueueEmpty: + wait > 0: + try: + newtask = self.addq.get(True, min(wait, self.wait_timeout)) + except QueueEmpty: # Nothing to add, nothing to do. Check run flags and continue waiting. + wait -= self.wait_timeout + except QueueEmpty: # Time to run some tasks, and no new tasks to add. self.schedule_lock.acquire() # select only those tasks which are to be executed now relevant = itertools.takewhile( @@ -174,11 +184,11 @@ class Scheduler(object): # only need to resort tasks if a repeated task has # been kept in the list. updated = True - else: - updated = True + else: # Add new task self.schedule_lock.acquire() if newtask is not None: self.schedule.append(newtask) + updated = True finally: if updated: self.schedule.sort(key=lambda task: task.next) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 1c0b84b9..8242a127 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -26,6 +26,7 @@ import time import random import weakref import uuid +import errno from xml.parsers.expat import ExpatError @@ -49,7 +50,7 @@ RESPONSE_TIMEOUT = 30 #: The time in seconds to wait for events from the event queue, and also the #: time between checks for the process stop signal. -WAIT_TIMEOUT = 0.1 +WAIT_TIMEOUT = 1.0 #: The number of threads to use to handle XML stream events. This is not the #: same as the number of custom event handling threads. @@ -461,10 +462,10 @@ class XMLStream(object): time.sleep(0.1) elapsed += 0.1 except KeyboardInterrupt: - self.stop.set() + self.set_stop() return False except SystemExit: - self.stop.set() + self.set_stop() return False if self.default_domain: @@ -706,7 +707,7 @@ class XMLStream(object): self.stream_end_event.set() if not self.auto_reconnect: - self.stop.set() + self.set_stop() if self._disconnect_wait_for_threads: self._wait_for_threads() @@ -723,7 +724,7 @@ class XMLStream(object): def abort(self): self.session_started_event.clear() - self.stop.set() + self.set_stop() if self._disconnect_wait_for_threads: self._wait_for_threads() try: @@ -1017,8 +1018,12 @@ class XMLStream(object): if name is None: name = 'add_handler_%s' % self.new_id() - self.register_handler(XMLCallback(name, MatchXMLMask(mask), pointer, - once=disposable, instream=instream)) + self.register_handler( + XMLCallback(name, + MatchXMLMask(mask, self.default_ns), + pointer, + once=disposable, + instream=instream)) def register_handler(self, handler, before=None, after=None): """Add a stream event handler that will be executed when a matching @@ -1290,6 +1295,9 @@ class XMLStream(object): try: sent += self.socket.send(data[sent:]) count += 1 + except Socket.error as serr: + if serr.errno != errno.EINTR: + raise except ssl.SSLError as serr: if tries >= self.ssl_retry_max: log.debug('SSL error: max retries reached') @@ -1352,6 +1360,13 @@ class XMLStream(object): if self.__thread_count == 0: self.__thread_cond.notify() + def set_stop(self): + self.stop.set() + + # Unlock queues + self.event_queue.put(None) + self.send_queue.put(None) + def _wait_for_threads(self): with self.__thread_cond: if self.__thread_count != 0: @@ -1624,11 +1639,7 @@ class XMLStream(object): log.debug("Loading event runner") try: while not self.stop.is_set(): - try: - wait = self.wait_timeout - event = self.event_queue.get(True, timeout=wait) - except QueueEmpty: - event = None + event = self.event_queue.get() if event is None: continue @@ -1644,10 +1655,10 @@ class XMLStream(object): log.exception(error_msg, handler.name) orig.exception(e) elif etype == 'schedule': - name = args[1] + name = args[2] try: log.debug('Scheduled event: %s: %s', name, args[0]) - handler(*args[0]) + handler(*args[0], **args[1]) except Exception as e: log.exception('Error processing scheduled task') self.exception(e) @@ -1689,14 +1700,13 @@ class XMLStream(object): while not self.stop.is_set(): while not self.stop.is_set() and \ not self.session_started_event.is_set(): - self.session_started_event.wait(timeout=0.1) + self.session_started_event.wait(timeout=0.1) # Wait for session start if self.__failed_send_stanza is not None: data = self.__failed_send_stanza self.__failed_send_stanza = None else: - try: - data = self.send_queue.get(True, 1) - except QueueEmpty: + data = self.send_queue.get() # Wait for data to send + if data is None: continue log.debug("SEND: %s", data) enc_data = data.encode('utf-8') @@ -1711,6 +1721,9 @@ class XMLStream(object): try: sent += self.socket.send(enc_data[sent:]) count += 1 + except Socket.error as serr: + if serr.errno != errno.EINTR: + raise except ssl.SSLError as serr: if tries >= self.ssl_retry_max: log.debug('SSL error: max retries reached') |