1##   protocol.py
2##
3##   Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
4##
5##   This program is free software; you can redistribute it and/or modify
6##   it under the terms of the GNU General Public License as published by
7##   the Free Software Foundation; either version 2, or (at your option)
8##   any later version.
9##
10##   This program is distributed in the hope that it will be useful,
11##   but WITHOUT ANY WARRANTY; without even the implied warranty of
12##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13##   GNU General Public License for more details.
14
15"""
16Protocol module contains tools that are needed for processing of xmpp-related
17data structures, including jabber-objects like JID or different stanzas and
18sub- stanzas) handling routines
19"""
20
21import time
22import hashlib
23import functools
24import warnings
25from base64 import b64encode
26from collections import namedtuple
27
28from gi.repository import GLib
29
30import idna
31from precis_i18n import get_profile
32from nbxmpp.simplexml import Node
33from nbxmpp.namespaces import Namespace
34
35def ascii_upper(s):
36    return s.upper()
37
38SASL_AUTH_MECHS = [
39    'SCRAM-SHA-256-PLUS',
40    'SCRAM-SHA-256',
41    'SCRAM-SHA-1-PLUS',
42    'SCRAM-SHA-1',
43    'GSSAPI',
44    'PLAIN',
45    'EXTERNAL',
46    'ANONYMOUS',
47]
48
49SASL_ERROR_CONDITIONS = [
50    'aborted',
51    'account-disabled',
52    'credentials-expired',
53    'encryption-required',
54    'incorrect-encoding',
55    'invalid-authzid',
56    'invalid-mechanism',
57    'mechanism-too-weak',
58    'malformed-request',
59    'not-authorized',
60    'temporary-auth-failure',
61]
62
63ERRORS = {
64    'urn:ietf:params:xml:ns:xmpp-sasl aborted': ['',
65        '',
66        'The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element.'],
67    'urn:ietf:params:xml:ns:xmpp-sasl incorrect-encoding': ['',
68        '',
69        'The data provided by the initiating entity could not be processed because the [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003. encoding is incorrect (e.g., because the encoding does not adhere to the definition in Section 3 of [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003.); sent in reply to a <response/> element or an <auth/> element with initial response data.'],
70    'urn:ietf:params:xml:ns:xmpp-sasl invalid-authzid': ['',
71        '',
72        'The authzid provided by the initiating entity is invalid, either because it is incorrectly formatted or because the initiating entity does not have permissions to authorize that ID; sent in reply to a <response/> element or an <auth/> element with initial response data.'],
73    'urn:ietf:params:xml:ns:xmpp-sasl invalid-mechanism': ['',
74        '',
75        'The initiating entity did not provide a mechanism or requested a mechanism that is not supported by the receiving entity; sent in reply to an <auth/> element.'],
76    'urn:ietf:params:xml:ns:xmpp-sasl mechanism-too-weak': ['',
77        '',
78        'The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity; sent in reply to a <response/> element or an <auth/> element with initial response data.'],
79    'urn:ietf:params:xml:ns:xmpp-sasl not-authorized': ['',
80        '',
81        'The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username); sent in reply to a <response/> element or an <auth/> element with initial response data.'],
82    'urn:ietf:params:xml:ns:xmpp-sasl temporary-auth-failure': ['',
83        '',
84        'The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an <auth/> element or <response/> element.'],
85    'urn:ietf:params:xml:ns:xmpp-stanzas bad-request': ['400',
86        'modify',
87        'The sender has sent XML that is malformed or that cannot be processed.'],
88    'urn:ietf:params:xml:ns:xmpp-stanzas conflict': ['409',
89        'cancel',
90        'Access cannot be granted because an existing resource or session exists with the same name or address.'],
91    'urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented': ['501',
92        'cancel',
93        'The feature requested is not implemented by the recipient or server and therefore cannot be processed.'],
94    'urn:ietf:params:xml:ns:xmpp-stanzas forbidden': ['403',
95        'auth',
96        'The requesting entity does not possess the required permissions to perform the action.'],
97    'urn:ietf:params:xml:ns:xmpp-stanzas gone': ['302',
98        'modify',
99        'The recipient or server can no longer be contacted at this address.'],
100    'urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error': ['500',
101        'wait',
102        'The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error.'],
103    'urn:ietf:params:xml:ns:xmpp-stanzas item-not-found': ['404',
104        'cancel',
105        'The addressed JID or item requested cannot be found.'],
106    'urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed': ['400',
107        'modify',
108        "The value of the        'to' attribute in the sender's stanza does not adhere to the syntax defined in Addressing Scheme."],
109    'urn:ietf:params:xml:ns:xmpp-stanzas not-acceptable': ['406',
110        'cancel',
111        'The recipient or server understands the request but is refusing to process it because it does not meet criteria defined by the recipient or server.'],
112    'urn:ietf:params:xml:ns:xmpp-stanzas not-allowed': ['405',
113        'cancel',
114        'The recipient or server does not allow any entity to perform the action.'],
115    'urn:ietf:params:xml:ns:xmpp-stanzas not-authorized': ['401',
116        'auth',
117        'The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials.'],
118    'urn:ietf:params:xml:ns:xmpp-stanzas payment-required': ['402',
119        'auth',
120        'The requesting entity is not authorized to access the requested service because payment is required.'],
121    'urn:ietf:params:xml:ns:xmpp-stanzas recipient-unavailable': ['404',
122        'wait',
123        'The intended recipient is temporarily unavailable.'],
124    'urn:ietf:params:xml:ns:xmpp-stanzas redirect': ['302',
125        'modify',
126        'The recipient or server is redirecting requests for this information to another entity.'],
127    'urn:ietf:params:xml:ns:xmpp-stanzas registration-required': ['407',
128        'auth',
129        'The requesting entity is not authorized to access the requested service because registration is required.'],
130    'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-not-found': ['404',
131        'cancel',
132        'A remote server or service specified as part or all of the JID of the intended recipient does not exist.'],
133    'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-timeout': ['504',
134        'wait',
135        'A remote server or service specified as part or all of the JID of the intended recipient could not be contacted within a reasonable amount of time.'],
136    'urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint': ['500',
137        'wait',
138        'The server or recipient lacks the system resources necessary to service the request.'],
139    'urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable': ['503',
140        'cancel',
141        'The server or recipient does not currently provide the requested service.'],
142    'urn:ietf:params:xml:ns:xmpp-stanzas subscription-required': ['407',
143        'auth',
144        'The requesting entity is not authorized to access the requested service because a subscription is required.'],
145    'urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition': ['500',
146        '',
147        'Undefined Condition'],
148    'urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request': ['400',
149        'wait',
150        'The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order).'],
151    'urn:ietf:params:xml:ns:xmpp-streams bad-format': ['',
152        '',
153        'The entity has sent XML that cannot be processed.'],
154    'urn:ietf:params:xml:ns:xmpp-streams bad-namespace-prefix': ['',
155        '',
156        'The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix.'],
157    'urn:ietf:params:xml:ns:xmpp-streams conflict': ['',
158        '',
159        'The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream.'],
160    'urn:ietf:params:xml:ns:xmpp-streams connection-timeout': ['',
161        '',
162        'The entity has not generated any traffic over the stream for some period of time.'],
163    'urn:ietf:params:xml:ns:xmpp-streams host-gone': ['',
164        '',
165        "The value of the        'to' attribute provided by the initiating entity in the stream header corresponds to a hostname that is no longer hosted by the server."],
166    'urn:ietf:params:xml:ns:xmpp-streams host-unknown': ['',
167        '',
168        "The value of the        'to' attribute provided by the initiating entity in the stream header does not correspond to a hostname that is hosted by the server."],
169    'urn:ietf:params:xml:ns:xmpp-streams improper-addressing': ['',
170        '',
171        "A stanza sent between two servers lacks a        'to' or 'from' attribute (or the attribute has no value)."],
172    'urn:ietf:params:xml:ns:xmpp-streams internal-server-error': ['',
173        '',
174        'The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream.'],
175    'urn:ietf:params:xml:ns:xmpp-streams invalid-from': ['cancel',
176        '',
177        "The JID or hostname provided in a        'from' address does not match an authorized JID or validated domain negotiated between servers via SASL or dialback, or between a client and a server via authentication and resource authorization."],
178    'urn:ietf:params:xml:ns:xmpp-streams invalid-id': ['',
179        '',
180        'The stream ID or dialback ID is invalid or does not match an ID previously provided.'],
181    'urn:ietf:params:xml:ns:xmpp-streams invalid-namespace': ['',
182        '',
183        'The streams namespace name is something other than        "http://etherx.jabber.org/streams" or the dialback namespace name is something other than "jabber:server:dialback".'],
184    'urn:ietf:params:xml:ns:xmpp-streams invalid-xml': ['',
185        '',
186        'The entity has sent invalid XML over the stream to a server that performs validation.'],
187    'urn:ietf:params:xml:ns:xmpp-streams not-authorized': ['',
188        '',
189        'The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action related to stream negotiation.'],
190    'urn:ietf:params:xml:ns:xmpp-streams policy-violation': ['',
191        '',
192        'The entity has violated some local service policy.'],
193    'urn:ietf:params:xml:ns:xmpp-streams remote-connection-failed': ['',
194        '',
195        'The server is unable to properly connect to a remote resource that is required for authentication or authorization.'],
196    'urn:ietf:params:xml:ns:xmpp-streams resource-constraint': ['',
197        '',
198        'The server lacks the system resources necessary to service the stream.'],
199    'urn:ietf:params:xml:ns:xmpp-streams restricted-xml': ['',
200        '',
201        'The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character.'],
202    'urn:ietf:params:xml:ns:xmpp-streams see-other-host': ['',
203        '',
204        'The server will not provide service to the initiating entity but is redirecting traffic to another host.'],
205    'urn:ietf:params:xml:ns:xmpp-streams system-shutdown': ['',
206        '',
207        'The server is being shut down and all active streams are being closed.'],
208    'urn:ietf:params:xml:ns:xmpp-streams undefined-condition': ['',
209        '',
210        'The error condition is not one of those defined by the other conditions in this list.'],
211    'urn:ietf:params:xml:ns:xmpp-streams unsupported-encoding': ['',
212        '',
213        'The initiating entity has encoded the stream in an encoding that is not supported by the server.'],
214    'urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type': ['',
215        '',
216        'The initiating entity has sent a first-level child of the stream that is not supported by the server.'],
217    'urn:ietf:params:xml:ns:xmpp-streams unsupported-version': ['',
218        '',
219        "The value of the        'version' attribute provided by the initiating entity in the stream header specifies a version of XMPP that is not supported by the server."],
220    'urn:ietf:params:xml:ns:xmpp-streams xml-not-well-formed': ['',
221        '',
222        'The initiating entity has sent XML that is not well-formed.']
223}
224
225_errorcodes = {
226    '302': 'redirect',
227    '400': 'unexpected-request',
228    '401': 'not-authorized',
229    '402': 'payment-required',
230    '403': 'forbidden',
231    '404': 'remote-server-not-found',
232    '405': 'not-allowed',
233    '406': 'not-acceptable',
234    '407': 'subscription-required',
235    '409': 'conflict',
236    '500': 'undefined-condition',
237    '501': 'feature-not-implemented',
238    '503': 'service-unavailable',
239    '504': 'remote-server-timeout',
240    'cancel': 'invalid-from'
241}
242
243_status_conditions = {
244    'realjid-public': 100,
245    'affiliation-changed': 101,
246    'unavailable-shown': 102,
247    'unavailable-not-shown': 103,
248    'configuration-changed': 104,
249    'self-presence': 110,
250    'logging-enabled': 170,
251    'logging-disabled': 171,
252    'non-anonymous': 172,
253    'semi-anonymous': 173,
254    'fully-anonymous': 174,
255    'room-created': 201,
256    'nick-assigned': 210,
257    'banned': 301,
258    'new-nick': 303,
259    'kicked': 307,
260    'removed-affiliation': 321,
261    'removed-membership': 322,
262    'removed-shutdown': 332,
263}
264
265_localpart_disallowed_chars = set('"&\'/:<>@')
266_localpart_escape_chars = ' "&\'/:<>@'
267
268
269STREAM_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-streams not-authorized'
270STREAM_REMOTE_CONNECTION_FAILED = 'urn:ietf:params:xml:ns:xmpp-streams remote-connection-failed'
271SASL_MECHANISM_TOO_WEAK = 'urn:ietf:params:xml:ns:xmpp-sasl mechanism-too-weak'
272STREAM_XML_NOT_WELL_FORMED = 'urn:ietf:params:xml:ns:xmpp-streams xml-not-well-formed'
273ERR_JID_MALFORMED = 'urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed'
274STREAM_SEE_OTHER_HOST = 'urn:ietf:params:xml:ns:xmpp-streams see-other-host'
275STREAM_BAD_NAMESPACE_PREFIX = 'urn:ietf:params:xml:ns:xmpp-streams bad-namespace-prefix'
276ERR_SERVICE_UNAVAILABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable'
277STREAM_CONNECTION_TIMEOUT = 'urn:ietf:params:xml:ns:xmpp-streams connection-timeout'
278STREAM_UNSUPPORTED_VERSION = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-version'
279STREAM_IMPROPER_ADDRESSING = 'urn:ietf:params:xml:ns:xmpp-streams improper-addressing'
280STREAM_UNDEFINED_CONDITION = 'urn:ietf:params:xml:ns:xmpp-streams undefined-condition'
281SASL_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-sasl not-authorized'
282ERR_GONE = 'urn:ietf:params:xml:ns:xmpp-stanzas gone'
283SASL_TEMPORARY_AUTH_FAILURE = 'urn:ietf:params:xml:ns:xmpp-sasl temporary-auth-failure'
284ERR_REMOTE_SERVER_NOT_FOUND = 'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-not-found'
285ERR_UNEXPECTED_REQUEST = 'urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request'
286ERR_RECIPIENT_UNAVAILABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas recipient-unavailable'
287ERR_CONFLICT = 'urn:ietf:params:xml:ns:xmpp-stanzas conflict'
288STREAM_SYSTEM_SHUTDOWN = 'urn:ietf:params:xml:ns:xmpp-streams system-shutdown'
289STREAM_BAD_FORMAT = 'urn:ietf:params:xml:ns:xmpp-streams bad-format'
290ERR_SUBSCRIPTION_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas subscription-required'
291STREAM_INTERNAL_SERVER_ERROR = 'urn:ietf:params:xml:ns:xmpp-streams internal-server-error'
292ERR_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-stanzas not-authorized'
293SASL_ABORTED = 'urn:ietf:params:xml:ns:xmpp-sasl aborted'
294ERR_REGISTRATION_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas registration-required'
295ERR_INTERNAL_SERVER_ERROR = 'urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error'
296SASL_INCORRECT_ENCODING = 'urn:ietf:params:xml:ns:xmpp-sasl incorrect-encoding'
297STREAM_HOST_GONE = 'urn:ietf:params:xml:ns:xmpp-streams host-gone'
298STREAM_POLICY_VIOLATION = 'urn:ietf:params:xml:ns:xmpp-streams policy-violation'
299STREAM_INVALID_XML = 'urn:ietf:params:xml:ns:xmpp-streams invalid-xml'
300STREAM_CONFLICT = 'urn:ietf:params:xml:ns:xmpp-streams conflict'
301STREAM_RESOURCE_CONSTRAINT = 'urn:ietf:params:xml:ns:xmpp-streams resource-constraint'
302STREAM_UNSUPPORTED_ENCODING = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-encoding'
303ERR_NOT_ALLOWED = 'urn:ietf:params:xml:ns:xmpp-stanzas not-allowed'
304ERR_ITEM_NOT_FOUND = 'urn:ietf:params:xml:ns:xmpp-stanzas item-not-found'
305ERR_NOT_ACCEPTABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas not-acceptable'
306STREAM_INVALID_FROM = 'urn:ietf:params:xml:ns:xmpp-streams invalid-from'
307ERR_FEATURE_NOT_IMPLEMENTED = 'urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented'
308ERR_BAD_REQUEST = 'urn:ietf:params:xml:ns:xmpp-stanzas bad-request'
309STREAM_INVALID_ID = 'urn:ietf:params:xml:ns:xmpp-streams invalid-id'
310STREAM_HOST_UNKNOWN = 'urn:ietf:params:xml:ns:xmpp-streams host-unknown'
311ERR_UNDEFINED_CONDITION = 'urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition'
312SASL_INVALID_MECHANISM = 'urn:ietf:params:xml:ns:xmpp-sasl invalid-mechanism'
313STREAM_RESTRICTED_XML = 'urn:ietf:params:xml:ns:xmpp-streams restricted-xml'
314ERR_RESOURCE_CONSTRAINT = 'urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint'
315ERR_REMOTE_SERVER_TIMEOUT = 'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-timeout'
316SASL_INVALID_AUTHZID = 'urn:ietf:params:xml:ns:xmpp-sasl invalid-authzid'
317ERR_PAYMENT_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas payment-required'
318STREAM_INVALID_NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-streams invalid-namespace'
319ERR_REDIRECT = 'urn:ietf:params:xml:ns:xmpp-stanzas redirect'
320STREAM_UNSUPPORTED_STANZA_TYPE = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type'
321ERR_FORBIDDEN = 'urn:ietf:params:xml:ns:xmpp-stanzas forbidden'
322
323def isResultNode(node):
324    """
325    Return true if the node is a positive reply
326    """
327    return node and node.getType() == 'result'
328
329def isErrorNode(node):
330    """
331    Return true if the node is a negative reply
332    """
333    return node and node.getType() == 'error'
334
335def isMucPM(message):
336    muc_user = message.getTag('x', namespace=Namespace.MUC_USER)
337    return (message.getType() in ('chat', 'error') and
338            muc_user is not None and
339            not muc_user.getChildren())
340
341class NodeProcessed(Exception):
342    """
343    Exception that should be raised by handler when the handling should be
344    stopped
345    """
346
347class StreamError(Exception):
348    """
349    Base exception class for stream errors
350    """
351
352class BadFormat(StreamError):
353    pass
354
355class BadNamespacePrefix(StreamError):
356    pass
357
358class Conflict(StreamError):
359    pass
360
361class ConnectionTimeout(StreamError):
362    pass
363
364class HostGone(StreamError):
365    pass
366
367class HostUnknown(StreamError):
368    pass
369
370class ImproperAddressing(StreamError):
371    pass
372
373class InternalServerError(StreamError):
374    pass
375
376class InvalidFrom(StreamError):
377    pass
378
379class InvalidID(StreamError):
380    pass
381
382class InvalidNamespace(StreamError):
383    pass
384
385class InvalidXML(StreamError):
386    pass
387
388class NotAuthorized(StreamError):
389    pass
390
391class PolicyViolation(StreamError):
392    pass
393
394class RemoteConnectionFailed(StreamError):
395    pass
396
397class ResourceConstraint(StreamError):
398    pass
399
400class RestrictedXML(StreamError):
401    pass
402
403class SeeOtherHost(StreamError):
404    pass
405
406class SystemShutdown(StreamError):
407    pass
408
409class UndefinedCondition(StreamError):
410    pass
411
412class UnsupportedEncoding(StreamError):
413    pass
414
415class UnsupportedStanzaType(StreamError):
416    pass
417
418class UnsupportedVersion(StreamError):
419    pass
420
421class XMLNotWellFormed(StreamError):
422    pass
423
424class InvalidStanza(Exception):
425    pass
426
427class InvalidJid(Exception):
428    pass
429
430class LocalpartByteLimit(InvalidJid):
431    def __init__(self):
432        InvalidJid.__init__(self, 'Localpart must be between 1 and 1023 bytes')
433
434class LocalpartNotAllowedChar(InvalidJid):
435    def __init__(self):
436        InvalidJid.__init__(self, 'Not allowed character in localpart')
437
438class ResourcepartByteLimit(InvalidJid):
439    def __init__(self):
440        InvalidJid.__init__(self,
441                            'Resourcepart must be between 1 and 1023 bytes')
442
443class ResourcepartNotAllowedChar(InvalidJid):
444    def __init__(self):
445        InvalidJid.__init__(self, 'Not allowed character in resourcepart')
446
447class DomainpartByteLimit(InvalidJid):
448    def __init__(self):
449        InvalidJid.__init__(self, 'Domainpart must be between 1 and 1023 bytes')
450
451class DomainpartNotAllowedChar(InvalidJid):
452    def __init__(self):
453        InvalidJid.__init__(self, 'Not allowed character in domainpart')
454
455class StanzaMalformed(Exception):
456    pass
457
458class DiscoInfoMalformed(Exception):
459    pass
460
461stream_exceptions = {'bad-format': BadFormat,
462                     'bad-namespace-prefix': BadNamespacePrefix,
463                     'conflict': Conflict,
464                     'connection-timeout': ConnectionTimeout,
465                     'host-gone': HostGone,
466                     'host-unknown': HostUnknown,
467                     'improper-addressing': ImproperAddressing,
468                     'internal-server-error': InternalServerError,
469                     'invalid-from': InvalidFrom,
470                     'invalid-id': InvalidID,
471                     'invalid-namespace': InvalidNamespace,
472                     'invalid-xml': InvalidXML,
473                     'not-authorized': NotAuthorized,
474                     'policy-violation': PolicyViolation,
475                     'remote-connection-failed': RemoteConnectionFailed,
476                     'resource-constraint': ResourceConstraint,
477                     'restricted-xml': RestrictedXML,
478                     'see-other-host': SeeOtherHost,
479                     'system-shutdown': SystemShutdown,
480                     'undefined-condition': UndefinedCondition,
481                     'unsupported-encoding': UnsupportedEncoding,
482                     'unsupported-stanza-type': UnsupportedStanzaType,
483                     'unsupported-version': UnsupportedVersion,
484                     'xml-not-well-formed': XMLNotWellFormed}
485
486
487def deprecation_warning(message):
488    warnings.warn(message, DeprecationWarning)
489
490
491@functools.lru_cache(maxsize=None)
492def validate_localpart(localpart):
493    if not localpart or len(localpart.encode()) > 1023:
494        raise LocalpartByteLimit
495
496    if _localpart_disallowed_chars & set(localpart):
497        raise LocalpartNotAllowedChar
498
499    try:
500        username = get_profile('UsernameCaseMapped')
501        return username.enforce(localpart)
502    except Exception:
503        raise LocalpartNotAllowedChar
504
505
506@functools.lru_cache(maxsize=None)
507def validate_resourcepart(resourcepart):
508    if not resourcepart or len(resourcepart.encode()) > 1023:
509        raise ResourcepartByteLimit
510
511    try:
512        opaque = get_profile('OpaqueString')
513        return opaque.enforce(resourcepart)
514    except Exception:
515        raise ResourcepartNotAllowedChar
516
517
518@functools.lru_cache(maxsize=None)
519def validate_domainpart(domainpart):
520    if not domainpart:
521        raise DomainpartByteLimit
522
523    ip_address = domainpart.strip('[]')
524    if GLib.hostname_is_ip_address(ip_address):
525        return ip_address
526
527    length = len(domainpart.encode())
528    if length == 0 or length > 1023:
529        raise DomainpartByteLimit
530
531    if domainpart.endswith('.'):  # RFC7622, 3.2
532        domainpart = domainpart[:-1]
533
534    try:
535        idna_encode(domainpart)
536    except Exception:
537        raise DomainpartNotAllowedChar
538
539    return domainpart
540
541
542@functools.lru_cache(maxsize=None)
543def idna_encode(domain):
544    return idna.encode(domain, uts46=True).decode()
545
546
547@functools.lru_cache(maxsize=None)
548def escape_localpart(localpart):
549    # https://xmpp.org/extensions/xep-0106.html#bizrules-algorithm
550    #
551    # If there are any instances of character sequences that correspond
552    # to escapings of the disallowed characters
553    # (e.g., the character sequence "\27") or the escaping character
554    # (i.e., the character sequence "\5c") in the source address,
555    # the leading backslash character MUST be escaped to the character
556    # sequence "\5c"
557
558    for char in '\\' + _localpart_escape_chars:
559        seq = "\\{:02x}".format(ord(char))
560        localpart = localpart.replace(seq, "\\5c{:02x}".format(ord(char)))
561
562    # Escape all other chars
563    for char in _localpart_escape_chars:
564        localpart = localpart.replace(char, "\\{:02x}".format(ord(char)))
565
566    return localpart
567
568
569@functools.lru_cache(maxsize=None)
570def unescape_localpart(localpart):
571    if localpart.startswith('\\20') or localpart.endswith('\\20'):
572        # Escaped JIDs are not allowed to start or end with \20
573        # so this localpart must be already unescaped
574        return localpart
575
576    for char in _localpart_escape_chars:
577        seq = "\\{:02x}".format(ord(char))
578        localpart = localpart.replace(seq, char)
579
580    for char in _localpart_escape_chars + "\\":
581        seq = "\\5c{:02x}".format(ord(char))
582        localpart = localpart.replace(seq, "\\{:02x}".format(ord(char)))
583
584    return localpart
585
586
587class JID(namedtuple('JID',
588                     ['jid', 'localpart', 'domain', 'resource'])):
589
590    __slots__ = []
591
592    def __new__(cls, jid=None, localpart=None, domain=None, resource=None):
593        if jid is not None:
594            deprecation_warning('JID(jid) is deprecated, use from_string()')
595            return JID.from_string(str(jid))
596
597        if localpart is not None:
598            localpart = validate_localpart(localpart)
599
600        domain = validate_domainpart(domain)
601
602        if resource is not None:
603            resource = validate_resourcepart(resource)
604
605        return super().__new__(cls, None, localpart, domain, resource)
606
607    @classmethod
608    @functools.lru_cache(maxsize=None)
609    def from_string(cls, jid_string):
610        # https://tools.ietf.org/html/rfc7622#section-3.2
611
612        # Remove any portion from the first '/' character to the end of the
613        # string (if there is a '/' character present).
614
615        # Remove any portion from the beginning of the string to the first
616        # '@' character (if there is an '@' character present).
617
618        if jid_string.find('/') != -1:
619            rest, resourcepart = jid_string.split('/', 1)
620        else:
621            rest, resourcepart = jid_string, None
622
623        if rest.find('@') != -1:
624            localpart, domainpart = rest.split('@', 1)
625        else:
626            localpart, domainpart = None, rest
627
628        return cls(jid=None,
629                   localpart=localpart,
630                   domain=domainpart,
631                   resource=resourcepart)
632
633    @classmethod
634    @functools.lru_cache(maxsize=None)
635    def from_user_input(cls, user_input, escaped=False):
636        # Use this if we want JIDs to be escaped according to XEP-0106
637        # The standard JID parsing cannot be applied because user_input is
638        # not a valid JID.
639
640        # Only user_input which after escaping result in a bare JID can be
641        # successfully parsed.
642
643        # The assumpution is user_input is a bare JID so we start with an
644        # rsplit on @ because we assume there is no resource, so the char @
645        # in the localpart can later be escaped.
646
647        if escaped:
648            # for convenience
649            return cls.from_string(user_input)
650
651        if '@' in user_input:
652            localpart, domainpart = user_input.rsplit('@', 1)
653            if localpart.startswith(' ') or localpart.endswith(' '):
654                raise LocalpartNotAllowedChar
655
656            localpart = escape_localpart(localpart)
657
658        else:
659            localpart = None
660            domainpart = user_input
661
662        return cls(jid=None,
663                   localpart=localpart,
664                   domain=domainpart,
665                   resource=None)
666
667    def __str__(self):
668        if self.localpart:
669            jid = f'{self.localpart}@{self.domain}'
670        else:
671            jid = self.domain
672
673        if self.resource is not None:
674            return f'{jid}/{self.resource}'
675        return jid
676
677    def __hash__(self):
678        return hash(str(self))
679
680    def __eq__(self, other):
681        if isinstance(other, str):
682            deprecation_warning('comparing string with JID is deprected')
683            try:
684                return JID.from_string(other) == self
685            except Exception:
686                return False
687        return super().__eq__(other)
688
689    def __ne__(self, other):
690        return not self.__eq__(other)
691
692    def domain_to_ascii(self):
693        return idna_encode(self.domain)
694
695    @property
696    def bare(self):
697        if self.localpart is not None:
698            return f'{self.localpart}@{self.domain}'
699        return self.domain
700
701    @property
702    def is_bare(self):
703        return self.resource is None
704
705    def new_as_bare(self):
706        if self.resource is None:
707            return self
708        return self._replace(resource=None)
709
710    def bare_match(self, other):
711        if isinstance(other, str):
712            other = JID.from_string(other)
713        return self.bare == other.bare
714
715    @property
716    def is_domain(self):
717        return self.localpart is None and self.resource is None
718
719    @property
720    def is_full(self):
721        return (self.localpart is not None and
722                self.domain is not None and
723                self.resource is not None)
724
725    def new_with(self, **kwargs):
726        return self._replace(**kwargs)
727
728    def to_user_string(self, show_punycode=True):
729        domain = self.domain_to_ascii()
730        if domain.startswith('xn--') and show_punycode:
731            domain_encoded = f' ({domain})'
732        else:
733            domain_encoded = ''
734
735        if self.localpart is None:
736            return f'{self}{domain_encoded}'
737
738        localpart = unescape_localpart(self.localpart)
739
740        if self.resource is None:
741            return f'{localpart}@{self.domain}{domain_encoded}'
742        return f'{localpart}@{self.domain}/{self.resource}{domain_encoded}'
743
744    def bareMatch(self, other):
745        deprecation_warning('bareMatch() is deprected use bare_match()')
746        return self.bare_match(other)
747
748    @property
749    def isBare(self):
750        deprecation_warning('isBare() is deprected use '
751                            'the attribute is_bare')
752        return self.is_bare
753
754    @property
755    def isDomain(self):
756        deprecation_warning('isDomain() is deprected use '
757                            'the attribute is_domain')
758        return self.is_domain
759
760    @property
761    def isFull(self):
762        deprecation_warning('isFull() is deprected use '
763                            'the attribute is_full')
764        return self.is_full
765
766    def copy(self):
767        deprecation_warning('copy() is not needed, JID is immutable')
768        return self
769
770    def getNode(self):
771        deprecation_warning('getNode() is deprected use '
772                            'the attribute localpart')
773        return self.localpart
774
775    def getDomain(self):
776        deprecation_warning('getDomain() is deprected use '
777                            'the attribute domain')
778        return self.domain
779
780    def getResource(self):
781        deprecation_warning('getResource() is deprected use '
782                            'the attribute resource')
783        return self.resource
784
785    def getStripped(self):
786        deprecation_warning('getStripped() is deprected use '
787                            'the attribute bare')
788        return self.bare
789
790    def getBare(self):
791        deprecation_warning('getBare() is deprected use '
792                            'the attribute bare')
793        return self.bare
794
795
796class StreamErrorNode(Node):
797    def __init__(self, node):
798        Node.__init__(self, node=node)
799
800        self._text = {}
801
802        text_elements = self.getTags('text', namespace=Namespace.XMPP_STREAMS)
803        for element in text_elements:
804            lang = element.getXmlLang()
805            text = element.getData()
806            self._text[lang] = text
807
808    def get_condition(self):
809        for tag in self.getChildren():
810            if (tag.getName() != 'text' and
811                    tag.getNamespace() == Namespace.XMPP_STREAMS):
812                return tag.getName()
813        return None
814
815    def get_text(self, pref_lang=None):
816        if pref_lang is not None:
817            text = self._text.get(pref_lang)
818            if text is not None:
819                return text
820
821        if self._text:
822            text = self._text.get('en')
823            if text is not None:
824                return text
825
826            text = self._text.get(None)
827            if text is not None:
828                return text
829            return self._text.popitem()[1]
830        return ''
831
832
833class Protocol(Node):
834    """
835    A "stanza" object class. Contains methods that are common for presences, iqs
836    and messages
837    """
838
839    def __init__(self,
840                 name=None,
841                 to=None,
842                 typ=None,
843                 frm=None,
844                 attrs=None,
845                 payload=None,
846                 timestamp=None,
847                 xmlns=None,
848                 node=None):
849        """
850        Constructor, name is the name of the stanza
851        i.e. 'message' or 'presence'or 'iq'
852
853        to is the value of 'to' attribure, 'typ' - 'type' attribute
854        frn - from attribure, attrs - other attributes mapping,
855        payload - same meaning as for simplexml payload definition
856        timestamp - the time value that needs to be stamped over stanza
857        xmlns - namespace of top stanza node
858        node - parsed or unparsed stana to be taken as prototype.
859        """
860        if not attrs:
861            attrs = {}
862        if to:
863            attrs['to'] = to
864        if frm:
865            attrs['from'] = frm
866        if typ:
867            attrs['type'] = typ
868        Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node)
869        if not node and xmlns:
870            self.setNamespace(xmlns)
871        if self['to']:
872            self.setTo(self['to'])
873        if self['from']:
874            self.setFrom(self['from'])
875        if (node and
876                isinstance(node, Protocol) and
877                self.__class__ == node.__class__
878                and 'id' in self.attrs):
879            del self.attrs['id']
880        self.timestamp = None
881        for d in self.getTags('delay', namespace=Namespace.DELAY2):
882            try:
883                if d.getAttr('stamp') < self.getTimestamp2():
884                    self.setTimestamp(d.getAttr('stamp'))
885            except Exception:
886                pass
887        if not self.timestamp:
888            for x in self.getTags('x', namespace=Namespace.DELAY):
889                try:
890                    if x.getAttr('stamp') < self.getTimestamp():
891                        self.setTimestamp(x.getAttr('stamp'))
892                except Exception:
893                    pass
894        if timestamp is not None:
895            self.setTimestamp(timestamp)
896
897    def isError(self):
898        return self.getAttr('type') == 'error'
899
900    def isResult(self):
901        return self.getAttr('type') == 'result'
902
903    def getTo(self):
904        """
905        Return value of the 'to' attribute
906        """
907        try:
908            return self['to']
909        except Exception:
910            pass
911        return None
912
913    def getFrom(self):
914        """
915        Return value of the 'from' attribute
916        """
917        try:
918            return self['from']
919        except Exception:
920            pass
921        return None
922
923    def getTimestamp(self):
924        """
925        Return the timestamp in the 'yyyymmddThhmmss' format
926        """
927        if self.timestamp:
928            return self.timestamp
929        return time.strftime('%Y%m%dT%H:%M:%S', time.gmtime())
930
931    def getTimestamp2(self):
932        """
933        Return the timestamp in the 'yyyymmddThhmmss' format
934        """
935        if self.timestamp:
936            return self.timestamp
937        return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
938
939    def getJid(self):
940        """
941        Return the value of the 'jid' attribute
942        """
943        attr = self.getAttr('jid')
944        if attr:
945            return JID.from_string(attr)
946        return attr
947
948    def getID(self):
949        """
950        Return the value of the 'id' attribute
951        """
952        return self.getAttr('id')
953
954    def setTo(self, val):
955        """
956        Set the value of the 'to' attribute
957        """
958        if not isinstance(val, JID):
959            val = JID.from_string(val)
960        self.setAttr('to', val)
961
962    def getType(self):
963        """
964        Return the value of the 'type' attribute
965        """
966        return self.getAttr('type')
967
968    def setFrom(self, val):
969        """
970        Set the value of the 'from' attribute
971        """
972        if not isinstance(val, JID):
973            val = JID.from_string(val)
974        self.setAttr('from', val)
975
976    def setType(self, val):
977        """
978        Set the value of the 'type' attribute
979        """
980        self.setAttr('type', val)
981
982    def setID(self, val):
983        """
984        Set the value of the 'id' attribute
985        """
986        self.setAttr('id', val)
987
988    def getError(self):
989        """
990        Return the error-condition (if present) or the textual description
991        of the error (otherwise)
992        """
993        errtag = self.getTag('error')
994        if errtag is None:
995            return None
996        for tag in errtag.getChildren():
997            if (tag.getName() != 'text' and
998                    tag.getNamespace() == Namespace.STANZAS):
999                return tag.getName()
1000        return None
1001
1002    def getAppError(self):
1003        errtag = self.getTag('error')
1004        if errtag is None:
1005            return None
1006        for tag in errtag.getChildren():
1007            if (tag.getName() != 'text' and
1008                    tag.getNamespace() != Namespace.STANZAS):
1009                return tag.getName()
1010        return None
1011
1012    def getAppErrorNamespace(self):
1013        errtag = self.getTag('error')
1014        if errtag is None:
1015            return None
1016        for tag in errtag.getChildren():
1017            if (tag.getName() != 'text' and
1018                    tag.getNamespace() != Namespace.STANZAS):
1019                return tag.getNamespace()
1020        return None
1021
1022    def getErrorMsg(self):
1023        """
1024        Return the textual description of the error (if present)
1025        or the error condition
1026        """
1027        errtag = self.getTag('error')
1028        if errtag:
1029            for tag in errtag.getChildren():
1030                if tag.getName() == 'text':
1031                    return tag.getData()
1032            return self.getError()
1033        return None
1034
1035    def getErrorCode(self):
1036        """
1037        Return the error code. Obsolete.
1038        """
1039        return self.getTagAttr('error', 'code')
1040
1041    def getErrorType(self):
1042        """
1043        Return the error code. Obsolete.
1044        """
1045        return self.getTagAttr('error', 'type')
1046
1047    def getStatusConditions(self, as_code=False):
1048        """
1049        Return the status conditions list as defined in XEP-0306.
1050        """
1051        result = set()
1052        status_tags = self.getTags('status')
1053        for status in status_tags:
1054            if as_code:
1055                code = status.getAttr('code')
1056                if code is not None:
1057                    result.add(code)
1058            else:
1059                for condition in status.getChildren():
1060                    result.add(condition.getName())
1061        return list(result)
1062
1063    def setError(self, error, code=None):
1064        """
1065        Set the error code. Obsolete. Use error-conditions instead
1066        """
1067        if code:
1068            if str(code) in _errorcodes.keys():
1069                error = ErrorNode(_errorcodes[str(code)], text=error)
1070            else:
1071                error = ErrorNode(ERR_UNDEFINED_CONDITION, code=code,
1072                                  typ='cancel', text=error)
1073        elif isinstance(error, str):
1074            error = ErrorNode(error)
1075        self.setType('error')
1076        self.addChild(node=error)
1077
1078    def setTimestamp(self, val=None):
1079        """
1080        Set the timestamp. timestamp should be the yyyymmddThhmmss string
1081        """
1082        if not val:
1083            val = time.strftime('%Y%m%dT%H:%M:%S', time.gmtime())
1084        self.timestamp=val
1085        self.setTag('x', {'stamp': self.timestamp}, namespace=Namespace.DELAY)
1086
1087    def getProperties(self):
1088        """
1089        Return the list of namespaces to which belongs the
1090        direct childs of element
1091        """
1092        props = []
1093        for child in self.getChildren():
1094            prop = child.getNamespace()
1095            if prop not in props:
1096                props.append(prop)
1097        return props
1098
1099    def getTag(self, name, attrs=None, namespace=None, protocol=False):
1100        """
1101        Return the Node instance for the tag.
1102        If protocol is True convert to a new Protocol/Message instance.
1103        """
1104        tag = Node.getTag(self, name, attrs, namespace)
1105        if protocol and tag:
1106            if name == 'message':
1107                return Message(node=tag)
1108            return Protocol(node=tag)
1109        return tag
1110
1111    def __setitem__(self, item, val):
1112        """
1113        Set the item 'item' to the value 'val'
1114        """
1115        if item in ['to', 'from']:
1116            if not isinstance(val, JID):
1117                val = JID.from_string(val)
1118        return self.setAttr(item, val)
1119
1120
1121class Message(Protocol):
1122    """
1123    XMPP Message stanza - "push" mechanism
1124    """
1125
1126    def __init__(self,
1127                 to=None,
1128                 body=None,
1129                 xhtml=None,
1130                 typ=None,
1131                 subject=None,
1132                 attrs=None,
1133                 frm=None,
1134                 payload=None,
1135                 timestamp=None,
1136                 xmlns=Namespace.CLIENT,
1137                 node=None):
1138        """
1139        You can specify recipient, text of message, type of message any
1140        additional attributes, sender of the message, any additional payload
1141        (f.e. jabber:x:delay element) and namespace in one go.
1142
1143        Alternatively you can pass in the other XML object as the 'node'
1144        parameted to replicate it as message
1145        """
1146        Protocol.__init__(self,
1147                          'message',
1148                          to=to,
1149                          typ=typ,
1150                          attrs=attrs,
1151                          frm=frm,
1152                          payload=payload,
1153                          timestamp=timestamp,
1154                          xmlns=xmlns,
1155                          node=node)
1156        if body:
1157            self.setBody(body)
1158        if xhtml is not None:
1159            self.setXHTML(xhtml)
1160        if subject is not None:
1161            self.setSubject(subject)
1162
1163    def getBody(self):
1164        """
1165        Return text of the message
1166        """
1167        return self.getTagData('body')
1168
1169    def getXHTML(self):
1170        return self.getTag('html', namespace=Namespace.XHTML_IM)
1171
1172    def getSubject(self):
1173        """
1174        Return subject of the message
1175        """
1176        return self.getTagData('subject')
1177
1178    def getThread(self):
1179        """
1180        Return thread of the message
1181        """
1182        return self.getTagData('thread')
1183
1184    def getOriginID(self):
1185        """
1186        Return origin-id of the message
1187        """
1188        return self.getTagAttr('origin-id', namespace=Namespace.SID, attr='id')
1189
1190    def getStanzaIDAttrs(self):
1191        """
1192        Return the stanza-id attributes of the message
1193        """
1194        try:
1195            attrs = self.getTag('stanza-id', namespace=Namespace.SID).getAttrs()
1196        except Exception:
1197            return None, None
1198        return attrs['id'], attrs['by']
1199
1200    def setBody(self, val):
1201        """
1202        Set the text of the message"""
1203        self.setTagData('body', val)
1204
1205    def setXHTML(self, body, add=False):
1206        if isinstance(body, str):
1207            body = Node(node=body)
1208        if add:
1209            xhtml = self.getTag('html', namespace=Namespace.XHTML_IM)
1210            if xhtml is not None:
1211                xhtml.addChild(node=body)
1212            else:
1213                self.addChild('html',
1214                              namespace=Namespace.XHTML_IM,
1215                              payload=body)
1216        else:
1217            xhtml_nodes = self.getTags('html', namespace=Namespace.XHTML_IM)
1218            for xhtml in xhtml_nodes:
1219                self.delChild(xhtml)
1220            self.addChild('html', namespace=Namespace.XHTML_IM, payload=body)
1221
1222    def setSubject(self, val):
1223        """
1224        Set the subject of the message
1225        """
1226        self.setTagData('subject', val)
1227
1228    def setThread(self, val):
1229        """
1230        Set the thread of the message
1231        """
1232        self.setTagData('thread', val)
1233
1234    def setOriginID(self, val):
1235        """
1236        Sets the origin-id of the message
1237        """
1238        self.setTag('origin-id', namespace=Namespace.SID, attrs={'id': val})
1239
1240    def buildReply(self, text=None):
1241        """
1242        Builds and returns another message object with specified text. The to,
1243        from, thread and type properties of new message are pre-set as reply to
1244        this message
1245        """
1246        m = Message(to=self.getFrom(),
1247                    frm=self.getTo(),
1248                    body=text,
1249                    typ=self.getType())
1250        th = self.getThread()
1251        if th:
1252            m.setThread(th)
1253        return m
1254
1255    def getStatusCode(self):
1256        """
1257        Return the status code of the message (for groupchat config change)
1258        """
1259        attrs = []
1260        for xtag in self.getTags('x'):
1261            for child in xtag.getTags('status'):
1262                attrs.append(child.getAttr('code'))
1263        return attrs
1264
1265    def setMarker(self, type_, id_):
1266        self.setTag(type_, namespace=Namespace.CHATMARKERS, attrs={'id': id_})
1267
1268    def setMarkable(self):
1269        self.setTag('markable', namespace=Namespace.CHATMARKERS)
1270
1271    def setReceiptRequest(self):
1272        self.setTag('request', namespace=Namespace.RECEIPTS)
1273
1274    def setReceiptReceived(self, id_):
1275        self.setTag('received', namespace=Namespace.RECEIPTS, attrs={'id': id_})
1276
1277    def setOOB(self, url, desc=None):
1278        oob = self.setTag('x', namespace=Namespace.X_OOB)
1279        oob.setTagData('url', url)
1280        if desc is not None:
1281            oob.setTagData('desc', desc)
1282
1283    def setCorrection(self, id_):
1284        self.setTag('replace', namespace=Namespace.CORRECT, attrs={'id': id_})
1285
1286    def setAttention(self):
1287        self.setTag('attention', namespace=Namespace.ATTENTION)
1288
1289    def setHint(self, hint):
1290        self.setTag(hint, namespace=Namespace.HINTS)
1291
1292
1293class Presence(Protocol):
1294
1295    def __init__(self,
1296                 to=None,
1297                 typ=None,
1298                 priority=None,
1299                 show=None,
1300                 status=None,
1301                 attrs=None,
1302                 frm=None,
1303                 timestamp=None,
1304                 payload=None,
1305                 xmlns=Namespace.CLIENT,
1306                 node=None):
1307        """
1308        You can specify recipient, type of message, priority, show and status
1309        values any additional attributes, sender of the presence, timestamp, any
1310        additional payload (f.e. jabber:x:delay element) and namespace in one
1311        go. Alternatively you can pass in the other XML object as the 'node'
1312        parameted to replicate it as presence
1313        """
1314        Protocol.__init__(self,
1315                          'presence',
1316                          to=to,
1317                          typ=typ,
1318                          attrs=attrs,
1319                          frm=frm,
1320                          payload=payload,
1321                          timestamp=timestamp,
1322                          xmlns=xmlns,
1323                          node=node)
1324        if priority:
1325            self.setPriority(priority)
1326        if show:
1327            self.setShow(show)
1328        if status:
1329            self.setStatus(status)
1330
1331    def getPriority(self):
1332        """
1333        Return the priority of the message
1334        """
1335        return self.getTagData('priority')
1336
1337    def getShow(self):
1338        """
1339        Return the show value of the message
1340        """
1341        return self.getTagData('show')
1342
1343    def getStatus(self):
1344        """
1345        Return the status string of the message
1346        """
1347        return self.getTagData('status') or ''
1348
1349    def setPriority(self, val):
1350        """
1351        Set the priority of the message
1352        """
1353        self.setTagData('priority', val)
1354
1355    def setShow(self, val):
1356        """
1357        Set the show value of the message
1358        """
1359        if val not in ['away', 'chat', 'dnd', 'xa']:
1360            raise ValueError('Invalid show value: %s' % val)
1361        self.setTagData('show', val)
1362
1363    def setStatus(self, val):
1364        """
1365        Set the status string of the message
1366        """
1367        self.setTagData('status', val)
1368
1369    def _muc_getItemAttr(self, tag, attr):
1370        for xtag in self.getTags('x'):
1371            if xtag.getNamespace() not in (Namespace.MUC_USER,
1372                                           Namespace.MUC_ADMIN):
1373                continue
1374            for child in xtag.getTags(tag):
1375                return child.getAttr(attr)
1376
1377    def _muc_getSubTagDataAttr(self, tag, attr):
1378        for xtag in self.getTags('x'):
1379            if xtag.getNamespace() not in (Namespace.MUC_USER,
1380                                           Namespace.MUC_ADMIN):
1381                continue
1382            for child in xtag.getTags('item'):
1383                for cchild in child.getTags(tag):
1384                    return cchild.getData(), cchild.getAttr(attr)
1385        return None, None
1386
1387    def getRole(self):
1388        """
1389        Return the presence role (for groupchat)
1390        """
1391        return self._muc_getItemAttr('item', 'role')
1392
1393    def getAffiliation(self):
1394        """
1395        Return the presence affiliation (for groupchat)
1396        """
1397        return self._muc_getItemAttr('item', 'affiliation')
1398
1399    def getNewNick(self):
1400        """
1401        Return the status code of the presence (for groupchat)
1402        """
1403        return self._muc_getItemAttr('item', 'nick')
1404
1405    def getJid(self):
1406        """
1407        Return the presence jid (for groupchat)
1408        """
1409        return self._muc_getItemAttr('item', 'jid')
1410
1411    def getReason(self):
1412        """
1413        Returns the reason of the presence (for groupchat)
1414        """
1415        return self._muc_getSubTagDataAttr('reason', '')[0]
1416
1417    def getActor(self):
1418        """
1419        Return the reason of the presence (for groupchat)
1420        """
1421        return self._muc_getSubTagDataAttr('actor', 'jid')[1]
1422
1423    def getStatusCode(self):
1424        """
1425        Return the status code of the presence (for groupchat)
1426        """
1427        attrs = []
1428        for xtag in self.getTags('x'):
1429            for child in xtag.getTags('status'):
1430                attrs.append(child.getAttr('code'))
1431        return attrs
1432
1433class Iq(Protocol):
1434    """
1435    XMPP Iq object - get/set dialog mechanism
1436    """
1437
1438    def __init__(self,
1439                 typ=None,
1440                 queryNS=None,
1441                 attrs=None,
1442                 to=None,
1443                 frm=None,
1444                 payload=None,
1445                 xmlns=Namespace.CLIENT,
1446                 node=None):
1447        """
1448        You can specify type, query namespace any additional attributes,
1449        recipient of the iq, sender of the iq, any additional payload (f.e.
1450        jabber:x:data node) and namespace in one go.
1451
1452        Alternatively you can pass in the other XML object as the 'node'
1453        parameted to replicate it as an iq
1454        """
1455        Protocol.__init__(self,
1456                          'iq',
1457                          to=to,
1458                          typ=typ,
1459                          attrs=attrs,
1460                          frm=frm,
1461                          xmlns=xmlns,
1462                          node=node)
1463        if payload:
1464            self.setQueryPayload(payload)
1465        if queryNS:
1466            self.setQueryNS(queryNS)
1467
1468    def getQuery(self):
1469        """
1470        Return the IQ's child element if it exists, None otherwise.
1471        """
1472        children = self.getChildren()
1473        if children and self.getType() != 'error' and \
1474        children[0].getName() != 'error':
1475            return children[0]
1476        return None
1477
1478    def getQueryNS(self):
1479        """
1480        Return the namespace of the 'query' child element
1481        """
1482        tag = self.getQuery()
1483        if tag:
1484            return tag.getNamespace()
1485        return None
1486
1487    def getQuerynode(self):
1488        """
1489        Return the 'node' attribute value of the 'query' child element
1490        """
1491        tag = self.getQuery()
1492        if tag:
1493            return tag.getAttr('node')
1494        return None
1495
1496    def getQueryPayload(self):
1497        """
1498        Return the 'query' child element payload
1499        """
1500        tag = self.getQuery()
1501        if tag:
1502            return tag.getPayload()
1503        return None
1504
1505    def getQueryChildren(self):
1506        """
1507        Return the 'query' child element child nodes
1508        """
1509        tag = self.getQuery()
1510        if tag:
1511            return tag.getChildren()
1512        return None
1513
1514    def getQueryChild(self, name=None):
1515        """
1516        Return the 'query' child element with name, or the first element
1517        which is not an error element
1518        """
1519        query = self.getQuery()
1520        if not query:
1521            return None
1522        for node in query.getChildren():
1523            if name is not None:
1524                if node.getName() == name:
1525                    return node
1526            else:
1527                if node.getName() != 'error':
1528                    return node
1529        return None
1530
1531    def setQuery(self, name=None):
1532        """
1533        Change the name of the query node, creating it if needed. Keep the
1534        existing name if none is given (use 'query' if it's a creation).
1535        Return the query node.
1536        """
1537        query = self.getQuery()
1538        if query is None:
1539            query = self.addChild('query')
1540        if name is not None:
1541            query.setName(name)
1542        return query
1543
1544    def setQueryNS(self, namespace):
1545        """
1546        Set the namespace of the 'query' child element
1547        """
1548        self.setQuery().setNamespace(namespace)
1549
1550    def setQueryPayload(self, payload):
1551        """
1552        Set the 'query' child element payload
1553        """
1554        self.setQuery().setPayload(payload)
1555
1556    def setQuerynode(self, node):
1557        """
1558        Set the 'node' attribute value of the 'query' child element
1559        """
1560        self.setQuery().setAttr('node', node)
1561
1562    def buildReply(self, typ):
1563        """
1564        Build and return another Iq object of specified type. The to, from and
1565        query child node of new Iq are pre-set as reply to this Iq.
1566        """
1567        iq = Iq(typ,
1568                to=self.getFrom(),
1569                frm=self.getTo(),
1570                attrs={'id': self.getID()})
1571        iq.setQuery(self.getQuery().getName()).setNamespace(self.getQueryNS())
1572        return iq
1573
1574    def buildSimpleReply(self, typ):
1575        return Iq(typ,
1576                  to=self.getFrom(),
1577                  attrs={'id': self.getID()})
1578
1579
1580class Hashes(Node):
1581    """
1582    Hash elements for various XEPs as defined in XEP-300
1583
1584    RECOMENDED HASH USE:
1585    Algorithm     Support
1586    MD2           MUST NOT
1587    MD4           MUST NOT
1588    MD5           MAY
1589    SHA-1         MUST
1590    SHA-256       MUST
1591    SHA-512       SHOULD
1592    """
1593
1594    supported = ('md5', 'sha-1', 'sha-256', 'sha-512')
1595
1596    def __init__(self, nsp=Namespace.HASHES):
1597        Node.__init__(self, None, {}, [], None, None, False, None)
1598        self.setNamespace(nsp)
1599        self.setName('hash')
1600
1601    def calculateHash(self, algo, file_string):
1602        """
1603        Calculate the hash and add it. It is preferable doing it here
1604        instead of doing it all over the place in Gajim.
1605        """
1606        hl = None
1607        hash_ = None
1608        # file_string can be a string or a file
1609        if isinstance(file_string, str):
1610            if algo == 'sha-1':
1611                hl = hashlib.sha1()
1612            elif algo == 'md5':
1613                hl = hashlib.md5()
1614            elif algo == 'sha-256':
1615                hl = hashlib.sha256()
1616            elif algo == 'sha-512':
1617                hl = hashlib.sha512()
1618            if hl:
1619                hl.update(file_string)
1620                hash_ = hl.hexdigest()
1621        else: # if it is a file
1622            if algo == 'sha-1':
1623                hl = hashlib.sha1()
1624            elif algo == 'md5':
1625                hl = hashlib.md5()
1626            elif algo == 'sha-256':
1627                hl = hashlib.sha256()
1628            elif algo == 'sha-512':
1629                hl = hashlib.sha512()
1630            if hl:
1631                for line in file_string:
1632                    hl.update(line)
1633                hash_ = hl.hexdigest()
1634        return hash_
1635
1636    def addHash(self, hash_, algo):
1637        self.setAttr('algo', algo)
1638        self.setData(hash_)
1639
1640class Hashes2(Node):
1641    """
1642    Hash elements for various XEPs as defined in XEP-300
1643
1644    RECOMENDED HASH USE:
1645    Algorithm     Support
1646    MD2           MUST NOT
1647    MD4           MUST NOT
1648    MD5           MUST NOT
1649    SHA-1         SHOULD NOT
1650    SHA-256       MUST
1651    SHA-512       SHOULD
1652    SHA3-256      MUST
1653    SHA3-512      SHOULD
1654    BLAKE2b256    MUST
1655    BLAKE2b512    SHOULD
1656    """
1657
1658    supported = ('sha-256', 'sha-512', 'sha3-256',
1659                 'sha3-512', 'blake2b-256', 'blake2b-512')
1660
1661    def __init__(self, nsp=Namespace.HASHES_2):
1662        Node.__init__(self, None, {}, [], None, None, False, None)
1663        self.setNamespace(nsp)
1664        self.setName('hash')
1665
1666    def calculateHash(self, algo, file_string):
1667        """
1668        Calculate the hash and add it. It is preferable doing it here
1669        instead of doing it all over the place in Gajim.
1670        """
1671        hl = None
1672        hash_ = None
1673        if algo == 'sha-256':
1674            hl = hashlib.sha256()
1675        elif algo == 'sha-512':
1676            hl = hashlib.sha512()
1677        elif algo == 'sha3-256':
1678            hl = hashlib.sha3_256()
1679        elif algo == 'sha3-512':
1680            hl = hashlib.sha3_512()
1681        elif algo == 'blake2b-256':
1682            hl = hashlib.blake2b(digest_size=32)
1683        elif algo == 'blake2b-512':
1684            hl = hashlib.blake2b(digest_size=64)
1685        # file_string can be a string or a file
1686        if hl is not None:
1687            if isinstance(file_string, bytes):
1688                hl.update(file_string)
1689            else: # if it is a file
1690                for line in file_string:
1691                    hl.update(line)
1692            hash_ = b64encode(hl.digest()).decode('ascii')
1693        return hash_
1694
1695    def addHash(self, hash_, algo):
1696        self.setAttr('algo', algo)
1697        self.setData(hash_)
1698
1699
1700class BindRequest(Iq):
1701    def __init__(self, resource):
1702        if resource is not None:
1703            resource = Node('resource', payload=resource)
1704        Iq.__init__(self, typ='set')
1705        self.addChild(node=Node('bind',
1706                                {'xmlns': Namespace.BIND},
1707                                payload=resource))
1708
1709
1710class TLSRequest(Node):
1711    def __init__(self):
1712        Node.__init__(self, tag='starttls', attrs={'xmlns': Namespace.TLS})
1713
1714
1715class SessionRequest(Iq):
1716    def __init__(self):
1717        Iq.__init__(self, typ='set')
1718        self.addChild(node=Node('session', attrs={'xmlns': Namespace.SESSION}))
1719
1720
1721class StreamHeader(Node):
1722    def __init__(self, domain, lang=None):
1723        if lang is None:
1724            lang = 'en'
1725        Node.__init__(self,
1726                      tag='stream:stream',
1727                      attrs={'xmlns': Namespace.CLIENT,
1728                             'version': '1.0',
1729                             'xmlns:stream': Namespace.STREAMS,
1730                             'to': domain,
1731                             'xml:lang': lang})
1732
1733
1734class WebsocketOpenHeader(Node):
1735    def __init__(self, domain, lang=None):
1736        if lang is None:
1737            lang = 'en'
1738        Node.__init__(self,
1739                      tag='open',
1740                      attrs={'xmlns': Namespace.FRAMING,
1741                             'version': '1.0',
1742                             'to': domain,
1743                             'xml:lang': lang})
1744
1745class WebsocketCloseHeader(Node):
1746    def __init__(self):
1747        Node.__init__(self, tag='close', attrs={'xmlns': Namespace.FRAMING})
1748
1749
1750class Features(Node):
1751    def __init__(self, node):
1752        Node.__init__(self, node=node)
1753
1754    def has_starttls(self):
1755        tls = self.getTag('starttls', namespace=Namespace.TLS)
1756        if tls is not None:
1757            required = tls.getTag('required') is not None
1758            return True, required
1759        return False, False
1760
1761    def has_sasl(self):
1762        return self.getTag('mechanisms', namespace=Namespace.SASL) is not None
1763
1764    def get_mechs(self):
1765        mechanisms = self.getTag('mechanisms', namespace=Namespace.SASL)
1766        if mechanisms is None:
1767            return set()
1768        mechanisms = mechanisms.getTags('mechanism')
1769        return set(mech.getData() for mech in mechanisms)
1770
1771    def get_domain_based_name(self):
1772        hostname = self.getTag('hostname',
1773                               namespace=Namespace.DOMAIN_BASED_NAME)
1774        if hostname is not None:
1775            return hostname.getData()
1776        return None
1777
1778    def has_bind(self):
1779        return self.getTag('bind', namespace=Namespace.BIND) is not None
1780
1781    def session_required(self):
1782        session = self.getTag('session', namespace=Namespace.SESSION)
1783        if session is not None:
1784            optional = session.getTag('optional') is not None
1785            return not optional
1786        return False
1787
1788    def has_sm(self):
1789        return self.getTag('sm', namespace=Namespace.STREAM_MGMT) is not None
1790
1791    def has_roster_version(self):
1792        return self.getTag('ver', namespace=Namespace.ROSTER_VER) is not None
1793
1794    def has_register(self):
1795        return self.getTag(
1796            'register', namespace=Namespace.REGISTER_FEATURE) is not None
1797
1798    def has_anonymous(self):
1799        return 'ANONYMOUS' in self.get_mechs()
1800
1801
1802class ErrorNode(Node):
1803    """
1804    XMPP-style error element
1805
1806    In the case of stanza error should be attached to XMPP stanza.
1807    In the case of stream-level errors should be used separately.
1808    """
1809
1810    def __init__(self, name, code=None, typ=None, text=None):
1811        """
1812        Mandatory parameter: name - name of error condition.
1813        Optional parameters: code, typ, text.
1814        Used for backwards compartibility with older jabber protocol.
1815        """
1816        if name in ERRORS:
1817            cod, type_, txt = ERRORS[name]
1818            ns = name.split()[0]
1819        else:
1820            cod, ns, type_, txt = '500', Namespace.STANZAS, 'cancel', ''
1821        if typ:
1822            type_ = typ
1823        if code:
1824            cod = code
1825        if text:
1826            txt = text
1827        Node.__init__(self, 'error', {}, [Node(name)])
1828        if type_:
1829            self.setAttr('type', type_)
1830        if not cod:
1831            self.setName('stream:error')
1832        if txt:
1833            self.addChild(node=Node(ns + ' text', {}, [txt]))
1834        if cod:
1835            self.setAttr('code', cod)
1836
1837class Error(Protocol):
1838    """
1839    Used to quickly transform received stanza into error reply
1840    """
1841
1842    def __init__(self, node, error, reply=1):
1843        """
1844        Create error reply basing on the received 'node' stanza and the 'error'
1845        error condition
1846
1847        If the 'node' is not the received stanza but locally created ('to' and
1848        'from' fields needs not swapping) specify the 'reply' argument as false.
1849        """
1850        if reply:
1851            Protocol.__init__(self,
1852                              to=node.getFrom(),
1853                              frm=node.getTo(),
1854                              node=node)
1855        else:
1856            Protocol.__init__(self, node=node)
1857        self.setError(error)
1858        if node.getType() == 'error':
1859            self.__str__ = self.__dupstr__
1860
1861    def __dupstr__(self, _dup1=None, _dup2=None):
1862        """
1863        Dummy function used as preventor of creating error node in reply to
1864        error node. I.e. you will not be able to serialise "double" error
1865        into string.
1866        """
1867        return ''
1868
1869class DataField(Node):
1870    """
1871    This class is used in the DataForm class to describe the single data item
1872
1873    If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) then
1874    you will need to work with instances of this class.
1875    """
1876
1877    def __init__(self,
1878                 name=None,
1879                 value=None,
1880                 typ=None,
1881                 required=0,
1882                 desc=None,
1883                 options=None,
1884                 node=None):
1885        """
1886        Create new data field of specified name,value and type
1887
1888        Also 'required','desc' and 'options' fields can be set. Alternatively
1889        other XML object can be passed in as the 'node' parameted
1890        to replicate it as a new datafiled.
1891        """
1892        Node.__init__(self, 'field', node=node)
1893        if name:
1894            self.setVar(name)
1895        if isinstance(value, (list, tuple)):
1896            self.setValues(value)
1897        elif value:
1898            self.setValue(value)
1899        if typ:
1900            self.setType(typ)
1901        elif not typ and not node:
1902            self.setType('text-single')
1903        if required:
1904            self.setRequired(required)
1905        if desc:
1906            self.setDesc(desc)
1907        if options:
1908            self.setOptions(options)
1909
1910    def setRequired(self, req=1):
1911        """
1912        Change the state of the 'required' flag
1913        """
1914        if req:
1915            self.setTag('required')
1916        else:
1917            try:
1918                self.delChild('required')
1919            except ValueError:
1920                return
1921
1922    def isRequired(self):
1923        """
1924        Return in this field a required one
1925        """
1926        return self.getTag('required')
1927
1928    def setDesc(self, desc):
1929        """
1930        Set the description of this field
1931        """
1932        self.setTagData('desc', desc)
1933
1934    def getDesc(self):
1935        """
1936        Return the description of this field
1937        """
1938        return self.getTagData('desc')
1939
1940    def setValue(self, val):
1941        """
1942        Set the value of this field
1943        """
1944        self.setTagData('value', val)
1945
1946    def getValue(self):
1947        return self.getTagData('value')
1948
1949    def setValues(self, lst):
1950        """
1951        Set the values of this field as values-list. Replaces all previous filed
1952        values! If you need to just add a value - use addValue method
1953        """
1954        while self.getTag('value'):
1955            self.delChild('value')
1956        for val in lst:
1957            self.addValue(val)
1958
1959    def addValue(self, val):
1960        """
1961        Add one more value to this field. Used in 'get' iq's or such
1962        """
1963        self.addChild('value', {}, [val])
1964
1965    def getValues(self):
1966        """
1967        Return the list of values associated with this field
1968        """
1969        ret = []
1970        for tag in self.getTags('value'):
1971            ret.append(tag.getData())
1972        return ret
1973
1974    def getOptions(self):
1975        """
1976        Return label-option pairs list associated with this field
1977        """
1978        ret = []
1979        for tag in self.getTags('option'):
1980            ret.append([tag.getAttr('label'), tag.getTagData('value')])
1981        return ret
1982
1983    def setOptions(self, lst):
1984        """
1985        Set label-option pairs list associated with this field
1986        """
1987        while self.getTag('option'):
1988            self.delChild('option')
1989        for opt in lst:
1990            self.addOption(opt)
1991
1992    def addOption(self, opt):
1993        """
1994        Add one more label-option pair to this field
1995        """
1996        if isinstance(opt, list):
1997            self.addChild('option',
1998                          {'label': opt[0]}).setTagData('value', opt[1])
1999        else:
2000            self.addChild('option').setTagData('value', opt)
2001
2002    def getType(self):
2003        """
2004        Get type of this field
2005        """
2006        return self.getAttr('type')
2007
2008    def setType(self, val):
2009        """
2010        Set type of this field
2011        """
2012        return self.setAttr('type', val)
2013
2014    def getVar(self):
2015        """
2016        Get 'var' attribute value of this field
2017        """
2018        return self.getAttr('var')
2019
2020    def setVar(self, val):
2021        """
2022        Set 'var' attribute value of this field
2023        """
2024        return self.setAttr('var', val)
2025
2026class DataForm(Node):
2027    """
2028    Used for manipulating dataforms in XMPP
2029
2030    Relevant XEPs: 0004, 0068, 0122. Can be used in disco, pub-sub and many
2031    other applications.
2032    """
2033    def __init__(self, typ=None, data=None, title=None, node=None):
2034        """
2035        Create new dataform of type 'typ'. 'data' is the list of DataField
2036        instances that this dataform contains, 'title' - the title string.  You
2037        can specify the 'node' argument as the other node to be used as base for
2038        constructing this dataform
2039
2040        title and instructions is optional and SHOULD NOT contain newlines.
2041        Several instructions MAY be present.
2042        'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' )
2043        'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' )
2044            respectively.
2045        'cancel' form can not contain any fields. All other forms contains
2046            AT LEAST one field.
2047        'title' MAY be included in forms of type "form" and "result"
2048        """
2049        Node.__init__(self, 'x', node=node)
2050        if node:
2051            newkids = []
2052            for n in self.getChildren():
2053                if n.getName() == 'field':
2054                    newkids.append(DataField(node=n))
2055                else:
2056                    newkids.append(n)
2057            self.kids = newkids
2058        if typ:
2059            self.setType(typ)
2060        self.setNamespace(Namespace.DATA)
2061        if title:
2062            self.setTitle(title)
2063        if data is not None:
2064            if isinstance(data, dict):
2065                newdata = []
2066                for name in data.keys():
2067                    newdata.append(DataField(name, data[name]))
2068                data = newdata
2069            for child in data:
2070                if child.__class__.__name__ == 'DataField':
2071                    self.kids.append(child)
2072                elif isinstance(child, Node):
2073                    self.kids.append(DataField(node=child))
2074                else:  # Must be a string
2075                    self.addInstructions(child)
2076
2077    def getType(self):
2078        """
2079        Return the type of dataform
2080        """
2081        return self.getAttr('type')
2082
2083    def setType(self, typ):
2084        """
2085        Set the type of dataform
2086        """
2087        self.setAttr('type', typ)
2088
2089    def getTitle(self):
2090        """
2091        Return the title of dataform
2092        """
2093        return self.getTagData('title')
2094
2095    def setTitle(self, text):
2096        """
2097        Set the title of dataform
2098        """
2099        self.setTagData('title', text)
2100
2101    def getInstructions(self):
2102        """
2103        Return the instructions of dataform
2104        """
2105        return self.getTagData('instructions')
2106
2107    def setInstructions(self, text):
2108        """
2109        Set the instructions of dataform
2110        """
2111        self.setTagData('instructions', text)
2112
2113    def addInstructions(self, text):
2114        """
2115        Add one more instruction to the dataform
2116        """
2117        self.addChild('instructions', {}, [text])
2118
2119    def getField(self, name):
2120        """
2121        Return the datafield object with name 'name' (if exists)
2122        """
2123        return self.getTag('field', attrs={'var': name})
2124
2125    def setField(self, name):
2126        """
2127        Create if nessessary or get the existing datafield object with name
2128        'name' and return it
2129        """
2130        f = self.getField(name)
2131        if f:
2132            return f
2133        return self.addChild(node=DataField(name))
2134
2135    def asDict(self):
2136        """
2137        Represent dataform as simple dictionary mapping of datafield names to
2138        their values
2139        """
2140        ret = {}
2141        for field in self.getTags('field'):
2142            name = field.getAttr('var')
2143            typ = field.getType()
2144            if typ and typ.endswith('-multi'):
2145                val = []
2146                for i in field.getTags('value'):
2147                    val.append(i.getData())
2148            else:
2149                val = field.getTagData('value')
2150            ret[name] = val
2151        if self.getTag('instructions'):
2152            ret['instructions'] = self.getInstructions()
2153        return ret
2154
2155    def __getitem__(self, name):
2156        """
2157        Simple dictionary interface for getting datafields values by their names
2158        """
2159        item = self.getField(name)
2160        if item:
2161            return item.getValue()
2162        raise IndexError('No such field')
2163
2164    def __setitem__(self, name, val):
2165        """
2166        Simple dictionary interface for setting datafields values by their names
2167        """
2168        return self.setField(name).setValue(val)
2169