def _make_command(command, attrs, dataform): command_node = Node('command', attrs=attrs) if dataform is not None: command_node.addChild(node=dataform) iq = Iq('set', to=command.jid) iq.addChild(node=command_node) return iq
def _make_keylist(keylist): item = Node('public-keys-list', {'xmlns': Namespace.OPENPGP}) if keylist is not None: for key in keylist: date = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(key.date)) attrs = {'v4-fingerprint': key.fingerprint, 'date': date} item.addChild('pubkey-metadata', attrs=attrs) return item
def _make_devicelist(devicelist): if devicelist is None: devicelist = [] devicelist_node = Node('list', attrs={'xmlns': Namespace.OMEMO_TEMP}) for device in devicelist: devicelist_node.addChild('device').setAttr('id', device) return devicelist_node
def _make_search_query(jid, dataform, items_per_page=50, after=None): search = Node('search', attrs={'xmlns': Namespace.MUCLUMBUS}) search.addChild(node=dataform) rsm = search.addChild('set', namespace=Namespace.RSM) rsm.addChild('max').setData(items_per_page) if after is not None: rsm.addChild('after').setData(after) query = Iq(to=jid, typ='get') query.addChild(node=search) return query
def build_conference_node(bookmark): attrs = {'xmlns': Namespace.BOOKMARKS_1} if bookmark.autojoin: attrs['autojoin'] = 'true' if bookmark.name: attrs['name'] = bookmark.name conference = Node(tag='conference', attrs=attrs) if bookmark.nick: conference.setTagData('nick', bookmark.nick) return conference
def _make_publish_options(options): data = Node(Namespace.DATA + ' x', attrs={'type': 'submit'}) field = data.addChild('field', attrs={ 'var': 'FORM_TYPE', 'type': 'hidden' }) field.setTagData('value', Namespace.PUBSUB_PUBLISH_OPTIONS) for var, value in options.items(): field = data.addChild('field', attrs={'var': var}) field.setTagData('value', value) return data
def set_location(self, data): task = yield item = Node('geoloc', {'xmlns': Namespace.LOCATION}) if data is not None: data = data._asdict() for tag, value in data.items(): if value is not None: item.addChild(tag, payload=value) result = yield self.publish(Namespace.LOCATION, item, id_='current') yield finalize(task, result)
def build_storage_node(bookmarks): storage_node = Node(tag='storage', attrs={'xmlns': Namespace.BOOKMARKS}) for bookmark in bookmarks: conf_node = storage_node.addChild(name="conference") conf_node.setAttr('jid', bookmark.jid) conf_node.setAttr('autojoin', to_xs_boolean(bookmark.autojoin)) if bookmark.name: conf_node.setAttr('name', bookmark.name) if bookmark.nick: conf_node.setTagData('nick', bookmark.nick) if bookmark.password: conf_node.setTagData('password', bookmark.password) return storage_node
def get_publish_options(config): options = Node(Namespace.DATA + ' x', attrs={'type': 'submit'}) field = options.addChild('field', attrs={ 'var': 'FORM_TYPE', 'type': 'hidden' }) field.setTagData('value', Namespace.PUBSUB_PUBLISH_OPTIONS) for var, value in config.items(): field = options.addChild('field', attrs={'var': var}) field.setTagData('value', value) return options
def set_activity(self, data): task = yield item = Node('activity', {'xmlns': Namespace.ACTIVITY}) if data is not None and data.activity: activity_node = item.addChild(data.activity) if data.subactivity: activity_node.addChild(data.subactivity) if data.text: item.addChild('text', payload=data.text) result = yield self.publish(Namespace.ACTIVITY, item, id_='current') yield finalize(task, result)
def initiate(self): node = Node('auth', attrs={ 'xmlns': Namespace.SASL, 'mechanism': 'ANONYMOUS' }) self._client.send_nonza(node)
def create_message_stanza(stanza, encrypted_payload, with_fallback_text): b64encoded_payload = b64encode(encrypted_payload) openpgp_node = Node('openpgp', attrs={'xmlns': Namespace.OPENPGP}) openpgp_node.addData(b64encoded_payload) stanza.addChild(node=openpgp_node) eme_node = Node('encryption', attrs={ 'xmlns': Namespace.EME, 'namespace': Namespace.OPENPGP }) stanza.addChild(node=eme_node) if with_fallback_text: stanza.setBody( '[This message is *encrypted* with OpenPGP (See :XEP:`0373`]')
def StreamInit(self): """ Send an initial stream header """ self._owner.Connection.sendqueue = [] self.Stream = NodeBuilder() self.Stream.dispatch = self.dispatch self.Stream._dispatch_depth = 2 self.Stream.stream_header_received = self._check_stream_start self.Stream.features = None self._metastream = Node('stream:stream') self._metastream.setNamespace(self._owner.Namespace) self._metastream.setAttr('version', '1.0') self._metastream.setAttr('xmlns:stream', Namespace.STREAMS) self._metastream.setAttr('to', self._owner.Server) self._metastream.setAttr('xml:lang', self._owner.lang) self._owner.send("%s%s>" % (XML_DECLARATION, str(self._metastream)[:-2]))
def initiate(self, username, password): payload = b64encode('\x00%s\x00%s' % (username, password)) node = Node('auth', attrs={ 'xmlns': Namespace.SASL, 'mechanism': 'PLAIN' }, payload=[payload]) self._client.send_nonza(node)
def initiate(self, username, server): payload = b64encode('%s@%s' % (username, server)) node = Node('auth', attrs={ 'xmlns': Namespace.SASL, 'mechanism': 'EXTERNAL' }, payload=[payload]) self._client.send_nonza(node)
def StreamInit(self): """ Send an initial stream header """ self.Stream = NodeBuilder() self.Stream.dispatch = self.dispatch self.Stream._dispatch_depth = 2 self.Stream.stream_header_received = self._check_stream_start self.Stream.features = self.old_features self._metastream = Node('stream:stream') self._metastream.setNamespace(self._owner.Namespace) self._metastream.setAttr('version', '1.0') self._metastream.setAttr('xmlns:stream', Namespace.STREAMS) self._metastream.setAttr('to', self._owner.Server) self._metastream.setAttr('xml:lang', self._owner.lang) self.restart = True self._owner.Connection.send_init(after_SASL=self.after_SASL)
def get_key_transport_message(typ, jid, omemo_message): message = Message(typ=typ, to=jid) encrypted = Node('encrypted', attrs={'xmlns': Namespace.OMEMO_TEMP}) header = Node('header', attrs={'sid': omemo_message.sid}) for rid, (key, prekey) in omemo_message.keys.items(): attrs = {'rid': rid} if prekey: attrs['prekey'] = 'true' child = header.addChild('key', attrs=attrs) child.addData(b64encode(key)) header.addChild('iv').addData(b64encode(omemo_message.iv)) encrypted.addChild(node=header) message.addChild(node=encrypted) return message
def set_nickname(self, nickname, public=False): task = yield access_model = 'open' if public else 'presence' options = { 'pubsub#persist_items': 'true', 'pubsub#access_model': access_model, } item = Node('nick', {'xmlns': Namespace.NICK}) if nickname is not None: item.addData(nickname) result = yield self.publish(Namespace.NICK, item, id_='current', options=options, force_node_options=True) yield finalize(task, result)
def _make_rsm_query(max_, after): rsm_set = Node('set', attrs={'xmlns': Namespace.RSM}) if max_ is not None: rsm_set.setTagData('max', max_) if after is not None: rsm_set.setTagData('after', after) return rsm_set
def initiate(self, username, password): self._password = password self._client_first_message_bare = 'n=%s,r=%s' % (username, self._client_nonce) client_first_message = '%s%s' % (self._channel_binding, self._client_first_message_bare) payload = b64encode(client_first_message) node = Node('auth', attrs={ 'xmlns': Namespace.SASL, 'mechanism': self._mechanism }, payload=[payload]) self._client.send_nonza(node)
def create_signcrypt_node(stanza, recipients, not_encrypted_nodes): ''' <signcrypt xmlns='urn:xmpp:openpgp:0'> <to jid='*****@*****.**'/> <time stamp='2014-07-10T17:06:00+02:00'/> <rpad> f0rm1l4n4-mT8y33j!Y%fRSrcd^ZE4Q7VDt1L%WEgR!kv </rpad> <payload> <body xmlns='jabber:client'> This is a secret message. </body> </payload> </signcrypt> ''' encrypted_nodes = [] child_nodes = list(stanza.getChildren()) for node in child_nodes: if (node.getName(), node.getNamespace()) not in not_encrypted_nodes: if not node.getNamespace(): node.setNamespace(Namespace.CLIENT) encrypted_nodes.append(node) stanza.delChild(node) signcrypt = Node('signcrypt', attrs={'xmlns': Namespace.OPENPGP}) for recipient in recipients: signcrypt.addChild('to', attrs={'jid': str(recipient)}) timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) signcrypt.addChild('time', attrs={'stamp': timestamp}) signcrypt.addChild('rpad').addData(get_rpad()) payload = signcrypt.addChild('payload') for node in encrypted_nodes: payload.addChild(node=node) return signcrypt
def response(self, server_message, *args, **kwargs): server_message = b64decode(server_message, bytes) try: if not self.ctx.complete: output_token = self.ctx.step(server_message) else: result = self.ctx.unwrap(server_message) # TODO(jelmer): Log result.message data = b'\x00\x00\x00\x00' + bytes(self.ctx.initiator_name) output_token = self.ctx.wrap(data, False).message except (gssapi.exceptions.GeneralError, gssapi.raw.misc.GSSError) as e: raise AuthFail(e) response = b64encode(output_token) node = Node('response', attrs={'xmlns': Namespace.SASL}, payload=response) self._client.send_nonza(node)
def get_node(self): identity = Node('identity', attrs={ 'category': self.category, 'type': self.type }) if self.name is not None: identity.setAttr('name', self.name) if self.lang is not None: identity.setAttr('xml:lang', self.lang) return identity
def initiate(self, hostname): service = gssapi.Name('xmpp@%s' % hostname, name_type=gssapi.NameType.hostbased_service) try: self.ctx = gssapi.SecurityContext( name=service, usage="initiate", flags=gssapi.RequirementFlag.integrity) token = self.ctx.step() except (gssapi.exceptions.GeneralError, gssapi.raw.misc.GSSError) as e: raise AuthFail(e) node = Node('auth', attrs={ 'xmlns': Namespace.SASL, 'mechanism': 'GSSAPI' }, payload=b64encode(token)) self._client.send_nonza(node)
def set_mood(self, data): task = yield item = Node('mood', {'xmlns': Namespace.MOOD}) if data is not None and data.mood: item.addChild(data.mood) if data.text: item.addChild('text', payload=data.text) result = yield self.publish(Namespace.MOOD, item, id_='current') yield finalize(task, result)
def response(self, server_first_message): server_first_message = b64decode(server_first_message) challenge = self._scram_parse(server_first_message) client_nonce = challenge['r'][:self.nonce_length] if client_nonce != self._client_nonce: raise AuthFail('Invalid client nonce received from server') salt = b64decode(challenge['s'], bytes) iteration_count = int(challenge['i']) if iteration_count < 4096: raise AuthFail('Salt iteration count to low: %s' % iteration_count) salted_password = pbkdf2_hmac(self._hash_method, self._password.encode('utf8'), salt, iteration_count) client_final_message_wo_proof = 'c=%s,r=%s' % ( self._b64_channel_binding_data, challenge['r']) client_key = self._hmac(salted_password, 'Client Key') stored_key = self._h(client_key) auth_message = '%s,%s,%s' % (self._client_first_message_bare, server_first_message, client_final_message_wo_proof) client_signature = self._hmac(stored_key, auth_message) client_proof = self._xor(client_key, client_signature) client_finale_message = 'c=%s,r=%s,p=%s' % ( self._b64_channel_binding_data, challenge['r'], b64encode(client_proof)) server_key = self._hmac(salted_password, 'Server Key') self._server_signature = self._hmac(server_key, auth_message) payload = b64encode(client_finale_message) node = Node('response', attrs={'xmlns': Namespace.SASL}, payload=[payload]) self._client.send_nonza(node)
def _make_bundle(bundle): ''' <publish node='eu.siacs.conversations.axolotl.bundles:31415'> <item id='current'> <bundle xmlns='eu.siacs.conversations.axolotl'> <signedPreKeyPublic signedPreKeyId='1'> BASE64ENCODED... </signedPreKeyPublic> <signedPreKeySignature> BASE64ENCODED... </signedPreKeySignature> <identityKey> BASE64ENCODED... </identityKey> <prekeys> <preKeyPublic preKeyId='1'> BASE64ENCODED... </preKeyPublic> <preKeyPublic preKeyId='2'> BASE64ENCODED... </preKeyPublic> <preKeyPublic preKeyId='3'> BASE64ENCODED... </preKeyPublic> <!-- ... --> </prekeys> </bundle> </item> </publish> ''' bundle_node = Node('bundle', attrs={'xmlns': Namespace.OMEMO_TEMP}) prekey_pub_node = bundle_node.addChild( 'signedPreKeyPublic', attrs={'signedPreKeyId': bundle.spk['id']}) prekey_pub_node.addData(b64encode(bundle.spk['key'])) prekey_sig_node = bundle_node.addChild('signedPreKeySignature') prekey_sig_node.addData(b64encode(bundle.spk_signature)) identity_key_node = bundle_node.addChild('identityKey') identity_key_node.addData(b64encode(bundle.ik)) prekeys = bundle_node.addChild('prekeys') for key in bundle.otpks: pre_key_public = prekeys.addChild('preKeyPublic', attrs={'preKeyId': key['id']}) pre_key_public.addData(b64encode(key['key'])) return bundle_node
def test_parse_delay(self): node = """ <message> <delay xmlns='urn:xmpp:delay' from='capulet.com' stamp='2002-09-10T23:08:25Z' /> <delay xmlns='urn:xmpp:delay' from='romeo.com' stamp='2010-09-10T23:08:25Z' /> <delay xmlns='urn:xmpp:delay' stamp='2015-09-10T23:08:25Z' /> </message> """ message = Node(node=node) timestamp = parse_delay(message) self.assertEqual(timestamp, 1031699305.0) timestamp = parse_delay(message, from_=['capulet.com']) self.assertEqual(timestamp, 1031699305.0) timestamp = parse_delay(message, from_=['romeo.com']) self.assertEqual(timestamp, 1284160105.0) timestamp = parse_delay(message, not_from=['romeo.com']) self.assertEqual(timestamp, 1031699305.0)
class BOSHDispatcher(XMPPDispatcher): def PlugIn(self, owner, after_SASL=False, old_features=None): self.old_features = old_features self.after_SASL = after_SASL XMPPDispatcher.PlugIn(self, owner) def StreamInit(self): """ Send an initial stream header """ self.Stream = NodeBuilder() self.Stream.dispatch = self.dispatch self.Stream._dispatch_depth = 2 self.Stream.stream_header_received = self._check_stream_start self.Stream.features = self.old_features self._metastream = Node('stream:stream') self._metastream.setNamespace(self._owner.Namespace) self._metastream.setAttr('version', '1.0') self._metastream.setAttr('xmlns:stream', Namespace.STREAMS) self._metastream.setAttr('to', self._owner.Server) self._metastream.setAttr('xml:lang', self._owner.lang) self.restart = True self._owner.Connection.send_init(after_SASL=self.after_SASL) def StreamTerminate(self): """ Send a stream terminator """ self._owner.Connection.send_terminator() def ProcessNonBlocking(self, data=None): if self.restart: fromstream = self._metastream fromstream.setAttr('from', fromstream.getAttr('to')) fromstream.delAttr('to') data = '%s%s>%s' % (XML_DECLARATION, str(fromstream)[:-2], data) self.restart = False return XMPPDispatcher.ProcessNonBlocking(self, data) def dispatch(self, stanza): if stanza.getName() == 'body' and stanza.getNamespace( ) == Namespace.HTTP_BIND: stanza_attrs = stanza.getAttrs() if 'authid' in stanza_attrs: # should be only in init response # auth module expects id of stream in document attributes self.Stream._document_attrs['id'] = stanza_attrs['authid'] self._owner.Connection.handle_body_attrs(stanza_attrs) children = stanza.getChildren() if children: for child in children: # if child doesn't have any ns specified, simplexml # (or expat) thinks it's of parent's (BOSH body) namespace, # so we have to rewrite it to jabber:client if child.getNamespace() == Namespace.HTTP_BIND: child.setNamespace(self._owner.defaultNamespace) XMPPDispatcher.dispatch(self, child) else: XMPPDispatcher.dispatch(self, stanza)
class XMPPDispatcher(PlugIn): """ Handles XMPP stream and is the first who takes control over a fresh stanza Is plugged into NonBlockingClient but can be replugged to restart handled stream headers (used by SASL f.e.). """ def __init__(self): PlugIn.__init__(self) self.handlers = {} self._modules = {} self._expected = {} self._defaultHandler = None self._pendingExceptions = [] self._eventHandler = None self._cycleHandlers = [] self._exported_methods = [ self.RegisterHandler, self.RegisterDefaultHandler, self.RegisterEventHandler, self.UnregisterCycleHandler, self.RegisterCycleHandler, self.RegisterHandlerOnce, self.UnregisterHandler, self.RegisterProtocol, self.SendAndCallForResponse, self.getAnID, self.Event, self.send, self.get_module ] # \ufddo -> \ufdef range c = '\ufdd0' r = c while c < '\ufdef': c = chr(ord(c) + 1) r += '|' + c # \ufffe-\uffff, \u1fffe-\u1ffff, ..., \u10fffe-\u10ffff c = '\ufffe' r += '|' + c r += '|' + chr(ord(c) + 1) while c < '\U0010fffe': c = chr(ord(c) + 0x10000) r += '|' + c r += '|' + chr(ord(c) + 1) self.invalid_chars_re = re.compile(r) def getAnID(self): return str(uuid.uuid4()) def dumpHandlers(self): """ Return set of user-registered callbacks in it's internal format. Used within the library to carry user handlers set over Dispatcher replugins """ return self.handlers def restoreHandlers(self, handlers): """ Restore user-registered callbacks structure from dump previously obtained via dumpHandlers. Used within the library to carry user handlers set over Dispatcher replugins. """ self.handlers = handlers def get_module(self, name): return self._modules[name] def _register_modules(self): self._modules['BasePresence'] = BasePresence(self._owner) self._modules['BaseMessage'] = BaseMessage(self._owner) self._modules['BaseIq'] = BaseIq(self._owner) self._modules['EME'] = EME(self._owner) self._modules['HTTPAuth'] = HTTPAuth(self._owner) self._modules['Nickname'] = Nickname(self._owner) self._modules['MUC'] = MUC(self._owner) self._modules['Delay'] = Delay(self._owner) self._modules['Captcha'] = Captcha(self._owner) self._modules['Idle'] = Idle(self._owner) self._modules['PGPLegacy'] = PGPLegacy(self._owner) self._modules['VCardAvatar'] = VCardAvatar(self._owner) self._modules['EntityCaps'] = EntityCaps(self._owner) self._modules['Blocking'] = Blocking(self._owner) self._modules['PubSub'] = PubSub(self._owner) self._modules['Mood'] = Mood(self._owner) self._modules['Activity'] = Activity(self._owner) self._modules['Tune'] = Tune(self._owner) self._modules['Location'] = Location(self._owner) self._modules['UserAvatar'] = UserAvatar(self._owner) self._modules['OpenPGP'] = OpenPGP(self._owner) self._modules['OMEMO'] = OMEMO(self._owner) self._modules['Annotations'] = Annotations(self._owner) self._modules['Muclumbus'] = Muclumbus(self._owner) self._modules['SoftwareVersion'] = SoftwareVersion(self._owner) self._modules['AdHoc'] = AdHoc(self._owner) self._modules['IBB'] = IBB(self._owner) self._modules['Discovery'] = Discovery(self._owner) self._modules['ChatMarkers'] = ChatMarkers(self._owner) self._modules['Receipts'] = Receipts(self._owner) self._modules['OOB'] = OOB(self._owner) self._modules['Correction'] = Correction(self._owner) self._modules['Attention'] = Attention(self._owner) self._modules['SecurityLabels'] = SecurityLabels(self._owner) self._modules['Chatstates'] = Chatstates(self._owner) self._modules['Register'] = Register(self._owner) self._modules['HTTPUpload'] = HTTPUpload(self._owner) for instance in self._modules.values(): for handler in instance.handlers: self.RegisterHandler(*handler) def _init(self): """ Register default namespaces/protocols/handlers. Used internally """ # FIXME: inject dependencies, do not rely that they are defined by our # owner self.RegisterNamespace('unknown') self.RegisterNamespace(Namespace.STREAMS) self.RegisterNamespace(self._owner.defaultNamespace) self.RegisterProtocol('iq', Iq) self.RegisterProtocol('presence', Presence) self.RegisterProtocol('message', Message) self.RegisterDefaultHandler(self.returnStanzaHandler) self.RegisterEventHandler(self._owner._caller._event_dispatcher) self._register_modules() def plugin(self, _owner): """ Plug the Dispatcher instance into Client class instance and send initial stream header. Used internally """ self._init() self._owner.lastErrNode = None self._owner.lastErr = None self._owner.lastErrCode = None if hasattr(self._owner, 'StreamInit'): self._owner.StreamInit() else: self.StreamInit() def plugout(self): """ Prepare instance to be destructed """ self._modules = {} self.Stream.dispatch = None self.Stream.features = None self.Stream.destroy() self._owner = None self.Stream = None def StreamInit(self): """ Send an initial stream header """ self._owner.Connection.sendqueue = [] self.Stream = NodeBuilder() self.Stream.dispatch = self.dispatch self.Stream._dispatch_depth = 2 self.Stream.stream_header_received = self._check_stream_start self.Stream.features = None self._metastream = Node('stream:stream') self._metastream.setNamespace(self._owner.Namespace) self._metastream.setAttr('version', '1.0') self._metastream.setAttr('xmlns:stream', Namespace.STREAMS) self._metastream.setAttr('to', self._owner.Server) self._metastream.setAttr('xml:lang', self._owner.lang) self._owner.send("%s%s>" % (XML_DECLARATION, str(self._metastream)[:-2])) def _check_stream_start(self, ns, tag, attrs): if ns != Namespace.STREAMS or tag != 'stream': raise ValueError('Incorrect stream start: ' '(%s,%s). Terminating.' % (tag, ns)) def replace_non_character(self, data): return re.sub(self.invalid_chars_re, '\ufffd', data) def ProcessNonBlocking(self, data): """ Check incoming stream for data waiting :param data: data received from transports/IO sockets :return: 1) length of processed data if some data were processed; 2) '0' string if no data were processed but link is alive; 3) 0 (zero) if underlying connection is closed. """ # FIXME: # When an error occurs we disconnect the transport directly. Client's # disconnect method will never be called. # Is this intended? # also look at transports start_disconnect() data = self.replace_non_character(data) for handler in self._cycleHandlers: handler(self) if len(self._pendingExceptions) > 0: _pendingException = self._pendingExceptions.pop() sys.excepthook(*_pendingException) return None try: self.Stream.Parse(data) # end stream:stream tag received if self.Stream and self.Stream.has_received_endtag(): self._owner.disconnect(self.Stream.streamError) return 0 except ExpatError as error: log.error('Invalid XML received from server. Forcing disconnect.') log.error(error) self._owner.Connection.disconnect() return 0 except ValueError as error: log.debug('ValueError: %s', error) self._owner.Connection.pollend() return 0 if len(self._pendingExceptions) > 0: _pendingException = self._pendingExceptions.pop() sys.excepthook(*_pendingException) return None if len(data) == 0: return '0' return len(data) def RegisterNamespace(self, xmlns): """ Create internal structures for newly registered namespace You can register handlers for this namespace afterwards. By default one namespace is already registered (jabber:client or jabber:component:accept depending on context. """ log.debug('Registering namespace "%s"', xmlns) self.handlers[xmlns] = {} self.RegisterProtocol('unknown', Protocol, xmlns=xmlns) self.RegisterProtocol('default', Protocol, xmlns=xmlns) def RegisterProtocol(self, tag_name, proto, xmlns=None): """ Used to declare some top-level stanza name to dispatcher Needed to start registering handlers for such stanzas. Iq, message and presence protocols are registered by default. """ if not xmlns: xmlns = self._owner.defaultNamespace log.debug('Registering protocol "%s" as %s(%s)', tag_name, proto, xmlns) self.handlers[xmlns][tag_name] = {'type': proto, 'default': []} def RegisterNamespaceHandler(self, xmlns, handler, typ='', ns='', system=0): """ Register handler for processing all stanzas for specified namespace """ self.RegisterHandler('default', handler, typ, ns, xmlns, system) def RegisterHandler(self, name, handler, typ='', ns='', xmlns=None, system=False, priority=50): """ Register user callback as stanzas handler of declared type Callback arguments: dispatcher instance (for replying), incoming return of previous handlers. The callback must raise xmpp.NodeProcessed just before return if it wants to prevent other callbacks to be called with the same stanza as argument _and_, more importantly library from returning stanza to sender with error set. :param name: name of stanza. F.e. "iq". :param handler: user callback. :param typ: value of stanza's "type" attribute. If not specified any value will match :param ns: namespace of child that stanza must contain. :param xmlns: xml namespace :param system: call handler even if NodeProcessed Exception were raised already. :param priority: The priority of the handler, higher get called later """ if not xmlns: xmlns = self._owner.defaultNamespace if not typ and not ns: typ = 'default' log.debug( 'Registering handler %s for "%s" type->%s ns->%s(%s) priority->%s', handler, name, typ, ns, xmlns, priority) if xmlns not in self.handlers: self.RegisterNamespace(xmlns) if name not in self.handlers[xmlns]: self.RegisterProtocol(name, Protocol, xmlns) specific = typ + ns if specific not in self.handlers[xmlns][name]: self.handlers[xmlns][name][specific] = [] self.handlers[xmlns][name][specific].append({ 'func': handler, 'system': system, 'priority': priority, 'specific': specific }) def RegisterHandlerOnce(self, name, handler, typ='', ns='', xmlns=None, system=0): """ Unregister handler after first call (not implemented yet) """ # FIXME Drop or implement if not xmlns: xmlns = self._owner.defaultNamespace self.RegisterHandler(name, handler, typ, ns, xmlns, system) def UnregisterHandler(self, name, handler, typ='', ns='', xmlns=None): """ Unregister handler. "typ" and "ns" must be specified exactly the same as with registering. """ if not xmlns: xmlns = self._owner.defaultNamespace if not typ and not ns: typ = 'default' if xmlns not in self.handlers: return if name not in self.handlers[xmlns]: return specific = typ + ns if specific not in self.handlers[xmlns][name]: return for handler_dict in self.handlers[xmlns][name][specific]: if handler_dict['func'] == handler: try: self.handlers[xmlns][name][specific].remove(handler_dict) log.debug( 'Unregister handler %s for "%s" type->%s ns->%s(%s)', handler, name, typ, ns, xmlns) except ValueError: log.warning( 'Unregister failed: %s for "%s" type->%s ns->%s(%s)', handler, name, typ, ns, xmlns) def RegisterDefaultHandler(self, handler): """ Specify the handler that will be used if no NodeProcessed exception were raised. This is returnStanzaHandler by default. """ self._defaultHandler = handler def RegisterEventHandler(self, handler): """ Register handler that will process events. F.e. "FILERECEIVED" event. See common/connection: _event_dispatcher() """ self._eventHandler = handler def returnStanzaHandler(self, conn, stanza): """ Return stanza back to the sender with <feature-not-implemented/> error set """ if stanza.getType() in ('get', 'set'): conn._owner.send(Error(stanza, ERR_FEATURE_NOT_IMPLEMENTED)) def RegisterCycleHandler(self, handler): """ Register handler that will be called on every Dispatcher.Process() call """ if handler not in self._cycleHandlers: self._cycleHandlers.append(handler) def UnregisterCycleHandler(self, handler): """ Unregister handler that will be called on every Dispatcher.Process() call """ if handler in self._cycleHandlers: self._cycleHandlers.remove(handler) def Event(self, realm, event, data=None): """ Raise some event :param realm: scope of event. Usually a namespace. :param event: the event itself. F.e. "SUCCESSFUL SEND". :param data: data that comes along with event. Depends on event. """ if self._eventHandler: self._eventHandler(realm, event, data) else: log.warning('Received unhandled event: %s', event) def dispatch(self, stanza): """ Main procedure that performs XMPP stanza recognition and calling apppropriate handlers for it. Called by simplexml """ self.Event('', 'STANZA RECEIVED', stanza) self.Stream._mini_dom = None # Count stanza self._owner.Smacks.count_incoming(stanza.getName()) name = stanza.getName() if name == 'features': self._owner.got_features = True self.Stream.features = stanza elif name == 'error': if stanza.getTag('see-other-host'): self._owner.got_see_other_host = stanza xmlns = stanza.getNamespace() if xmlns not in self.handlers: log.warning('Unknown namespace: %s', xmlns) xmlns = 'unknown' # features stanza has been handled before if name not in self.handlers[xmlns]: if name not in ('features', 'stream'): log.warning('Unknown stanza: %s', stanza) else: log.debug('Got %s / %s stanza', xmlns, name) name = 'unknown' else: log.debug('Got %s / %s stanza', xmlns, name) # Convert simplexml to Protocol object try: stanza = self.handlers[xmlns][name]['type'](node=stanza) except InvalidJid: log.warning('Invalid JID, ignoring stanza') log.warning(stanza) return own_jid = self._owner.get_bound_jid() properties = get_properties_struct(name, own_jid) if name == 'iq': if stanza.getFrom() is None and own_jid is not None: stanza.setFrom(own_jid.bare) if name == 'message': # https://tools.ietf.org/html/rfc6120#section-8.1.1.1 # If the stanza does not include a 'to' address then the client MUST # treat it as if the 'to' address were included with a value of the # client's full JID. to = stanza.getTo() if to is None: stanza.setTo(own_jid) elif not to.bare_match(own_jid): log.warning('Message addressed to someone else: %s', stanza) return if stanza.getFrom() is None: stanza.setFrom(own_jid.bare) # Unwrap carbon try: stanza, properties.carbon = unwrap_carbon(stanza, own_jid) except (InvalidFrom, InvalidJid) as exc: log.warning(exc) log.warning(stanza) return except NodeProcessed as exc: log.info(exc) return # Unwrap mam try: stanza, properties.mam = unwrap_mam(stanza, own_jid) except (InvalidStanza, InvalidJid) as exc: log.warning(exc) log.warning(stanza) return typ = stanza.getType() if name == 'message' and not typ: typ = 'normal' elif not typ: typ = '' stanza.props = stanza.getProperties() log.debug('type: %s, properties: %s', typ, stanza.props) _id = stanza.getID() processed = False if _id in self._expected: cb, args = self._expected[_id] log.debug('Expected stanza arrived. Callback %s(%s) found', cb, args) try: if args is None: cb(self, stanza) else: cb(self, stanza, **args) except NodeProcessed: pass return # Gather specifics depending on stanza properties specifics = ['default'] if typ and typ in self.handlers[xmlns][name]: specifics.append(typ) for prop in stanza.props: if prop in self.handlers[xmlns][name]: specifics.append(prop) if typ and typ + prop in self.handlers[xmlns][name]: specifics.append(typ + prop) # Create the handler chain chain = [] chain += self.handlers[xmlns]['default']['default'] for specific in specifics: chain += self.handlers[xmlns][name][specific] # Sort chain with priority chain.sort(key=lambda x: x['priority']) for handler in chain: if not processed or handler['system']: try: log.info('Call handler: %s', handler['func'].__qualname__) # Backwards compatibility until all handlers support # properties signature = inspect.signature(handler['func']) if len(signature.parameters) > 2: handler['func'](self, stanza, properties) else: handler['func'](self, stanza) except NodeProcessed: processed = True except Exception: self._pendingExceptions.insert(0, sys.exc_info()) return # Stanza was not processed call default handler if not processed and self._defaultHandler: self._defaultHandler(self, stanza) def SendAndCallForResponse(self, stanza, func=None, args=None): """ Put stanza on the wire and call back when recipient replies. Additional callback arguments can be specified in args """ _waitid = self.send(stanza) self._expected[_waitid] = (func, args) return _waitid def send(self, stanza, now=False): """ Wrap transports send method when plugged into NonBlockingClient. Makes sure stanzas get ID and from tag. """ ID = None if isinstance(stanza, Protocol): ID = stanza.getID() if ID is None: stanza.setID(self.getAnID()) ID = stanza.getID() if self._owner._registered_name and not stanza.getAttr('from'): stanza.setAttr('from', self._owner._registered_name) self._owner.Connection.send(stanza, now) # If no ID then it is a whitespace if hasattr(self._owner, 'Smacks') and ID: self._owner.Smacks.save_in_queue(stanza) return ID