1 # -*- coding: utf-8 -*-
4 ~~~~~~~~~~~~~~~~~~~~~~~
6 This module allows for working with Jabber IDs (JIDs).
8 Part of SleekXMPP: The Sleek XMPP Library
10 :copyright: (c) 2011 Nathanael C. Fritz
11 :license: MIT, see LICENSE for more details
14 from __future__
import unicode_literals
22 from copy
import deepcopy
24 from sleekxmpp
.util
import stringprep_profiles
25 from sleekxmpp
.thirdparty
import OrderedDict
27 #: These characters are not allowed to appear in a JID.
28 ILLEGAL_CHARS
= '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r' + \
29 '\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19' + \
30 '\x1a\x1b\x1c\x1d\x1e\x1f' + \
31 ' !"#$%&\'()*+,./:;<=>?@[\\]^_`{|}~\x7f'
33 #: The basic regex pattern that a JID must match in order to determine
34 #: the local, domain, and resource parts. This regex does NOT do any
35 #: validation, which requires application of nodeprep, resourceprep, etc.
36 JID_PATTERN
= re
.compile(
37 "^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$"
40 #: The set of escape sequences for the characters not allowed by nodeprep.
41 JID_ESCAPE_SEQUENCES
= set(['\\20', '\\22', '\\26', '\\27', '\\2f',
42 '\\3a', '\\3c', '\\3e', '\\40', '\\5c'])
44 #: A mapping of unallowed characters to their escape sequences. An escape
45 #: sequence for '\' is also included since it must also be escaped in
46 #: certain situations.
47 JID_ESCAPE_TRANSFORMATIONS
= {' ': '\\20',
58 #: The reverse mapping of escape sequences to their original forms.
59 JID_UNESCAPE_TRANSFORMATIONS
= {'\\20': ' ',
70 JID_CACHE
= OrderedDict()
71 JID_CACHE_LOCK
= threading
.Lock()
72 JID_CACHE_MAX_SIZE
= 1024
74 def _cache(key
, parts
, locked
):
75 JID_CACHE
[key
] = (parts
, locked
)
76 if len(JID_CACHE
) > JID_CACHE_MAX_SIZE
:
78 while len(JID_CACHE
) > JID_CACHE_MAX_SIZE
:
80 for key
, item
in JID_CACHE
.items():
81 if not item
[1]: # if not locked
84 if not found
: # more than MAX_SIZE locked
89 # pylint: disable=c0103
90 #: The nodeprep profile of stringprep used to validate the local,
91 #: or username, portion of a JID.
92 nodeprep
= stringprep_profiles
.create(
96 stringprep_profiles
.b1_mapping
,
97 stringprep
.map_table_b2
],
99 stringprep
.in_table_c11
,
100 stringprep
.in_table_c12
,
101 stringprep
.in_table_c21
,
102 stringprep
.in_table_c22
,
103 stringprep
.in_table_c3
,
104 stringprep
.in_table_c4
,
105 stringprep
.in_table_c5
,
106 stringprep
.in_table_c6
,
107 stringprep
.in_table_c7
,
108 stringprep
.in_table_c8
,
109 stringprep
.in_table_c9
,
110 lambda c
: c
in ' \'"&/:<>@'],
111 unassigned
=[stringprep
.in_table_a1
])
113 # pylint: disable=c0103
114 #: The resourceprep profile of stringprep, which is used to validate
115 #: the resource portion of a JID.
116 resourceprep
= stringprep_profiles
.create(
119 mappings
=[stringprep_profiles
.b1_mapping
],
121 stringprep
.in_table_c12
,
122 stringprep
.in_table_c21
,
123 stringprep
.in_table_c22
,
124 stringprep
.in_table_c3
,
125 stringprep
.in_table_c4
,
126 stringprep
.in_table_c5
,
127 stringprep
.in_table_c6
,
128 stringprep
.in_table_c7
,
129 stringprep
.in_table_c8
,
130 stringprep
.in_table_c9
],
131 unassigned
=[stringprep
.in_table_a1
])
134 def _parse_jid(data
):
136 Parse string data into the node, domain, and resource
137 components of a JID, if possible.
139 :param string data: A string that is potentially a JID.
143 :returns: tuple of the validated local, domain, and resource strings
145 match
= JID_PATTERN
.match(data
)
147 raise InvalidJID('JID could not be parsed')
149 (node
, domain
, resource
) = match
.groups()
151 node
= _validate_node(node
)
152 domain
= _validate_domain(domain
)
153 resource
= _validate_resource(resource
)
155 return node
, domain
, resource
158 def _validate_node(node
):
159 """Validate the local, or username, portion of a JID.
163 :returns: The local portion of a JID, as validated by nodeprep.
167 node
= nodeprep(node
)
170 raise InvalidJID('Localpart must not be 0 bytes')
172 raise InvalidJID('Localpart must be less than 1024 bytes')
174 except stringprep_profiles
.StringPrepError
:
175 raise InvalidJID('Invalid local part')
178 def _validate_domain(domain
):
179 """Validate the domain portion of a JID.
181 IP literal addresses are left as-is, if valid. Domain names
182 are stripped of any trailing label separators (`.`), and are
183 checked with the nameprep profile of stringprep. If the given
184 domain is actually a punyencoded version of a domain name, it
185 is converted back into its original Unicode form. Domains must
186 also not start or end with a dash (`-`).
190 :returns: The validated domain name
194 # First, check if this is an IPv4 address
196 socket
.inet_aton(domain
)
201 # Check if this is an IPv6 address
202 if not ip_addr
and hasattr(socket
, 'inet_pton'):
204 socket
.inet_pton(socket
.AF_INET6
, domain
.strip('[]'))
205 domain
= '[%s]' % domain
.strip('[]')
207 except (socket
.error
, ValueError):
211 # This is a domain name, which must be checked further
213 if domain
and domain
[-1] == '.':
217 for label
in domain
.split('.'):
219 label
= encodings
.idna
.nameprep(label
)
220 encodings
.idna
.ToASCII(label
)
223 pass_nameprep
= False
225 if not pass_nameprep
:
226 raise InvalidJID('Could not encode domain as ASCII')
228 if label
.startswith('xn--'):
229 label
= encodings
.idna
.ToUnicode(label
)
232 if char
in ILLEGAL_CHARS
:
233 raise InvalidJID('Domain contains illegal characters')
235 if '-' in (label
[0], label
[-1]):
236 raise InvalidJID('Domain started or ended with -')
238 domain_parts
.append(label
)
239 domain
= '.'.join(domain_parts
)
242 raise InvalidJID('Domain must not be 0 bytes')
243 if len(domain
) > 1023:
244 raise InvalidJID('Domain must be less than 1024 bytes')
249 def _validate_resource(resource
):
250 """Validate the resource portion of a JID.
254 :returns: The local portion of a JID, as validated by resourceprep.
257 if resource
is not None:
258 resource
= resourceprep(resource
)
261 raise InvalidJID('Resource must not be 0 bytes')
262 if len(resource
) > 1023:
263 raise InvalidJID('Resource must be less than 1024 bytes')
265 except stringprep_profiles
.StringPrepError
:
266 raise InvalidJID('Invalid resource')
269 def _escape_node(node
):
270 """Escape the local portion of a JID."""
273 for i
, char
in enumerate(node
):
275 if ''.join((node
[i
:i
+3])) in JID_ESCAPE_SEQUENCES
:
276 result
.append('\\5c')
280 for i
, char
in enumerate(result
):
282 result
[i
] = JID_ESCAPE_TRANSFORMATIONS
.get(char
, char
)
284 escaped
= ''.join(result
)
286 if escaped
.startswith('\\20') or escaped
.endswith('\\20'):
287 raise InvalidJID('Escaped local part starts or ends with "\\20"')
289 _validate_node(escaped
)
294 def _unescape_node(node
):
295 """Unescape a local portion of a JID.
298 The unescaped local portion is meant ONLY for presentation,
299 and should not be used for other purposes.
303 for i
, char
in enumerate(node
):
306 if seq
not in JID_ESCAPE_SEQUENCES
:
310 unescaped
.append(JID_UNESCAPE_TRANSFORMATIONS
.get(seq
, char
))
312 # Pop character off the escape sequence, and ignore it
315 unescaped
.append(char
)
316 unescaped
= ''.join(unescaped
)
321 def _format_jid(local
=None, domain
=None, resource
=None):
322 """Format the given JID components into a full or bare JID.
324 :param string local: Optional. The local portion of the JID.
325 :param string domain: Required. The domain name portion of the JID.
326 :param strin resource: Optional. The resource portion of the JID.
328 :return: A full or bare JID string.
335 result
.append(domain
)
338 result
.append(resource
)
339 return ''.join(result
)
342 class InvalidJID(ValueError):
344 Raised when attempting to create a JID that does not pass validation.
346 It can also be raised if modifying an existing JID in such a way as
347 to make it invalid, such trying to remove the domain from an existing
348 full JID while the local and resource portions still exist.
351 # pylint: disable=R0903
352 class UnescapedJID(object):
355 .. versionadded:: 1.1.10
358 def __init__(self
, local
, domain
, resource
):
359 self
._jid
= (local
, domain
, resource
)
361 # pylint: disable=R0911
362 def __getattr__(self
, name
):
363 """Retrieve the given JID component.
365 :param name: one of: user, server, domain, resource,
368 if name
== 'resource':
369 return self
._jid
[2] or ''
370 elif name
in ('user', 'username', 'local', 'node'):
371 return self
._jid
[0] or ''
372 elif name
in ('server', 'domain', 'host'):
373 return self
._jid
[1] or ''
374 elif name
in ('full', 'jid'):
375 return _format_jid(*self
._jid
)
377 return _format_jid(self
._jid
[0], self
._jid
[1])
379 return getattr(super(JID
, self
), '_jid')
384 """Use the full JID as the string value."""
385 return _format_jid(*self
._jid
)
388 """Use the full JID as the representation."""
389 return self
.__str
__()
395 A representation of a Jabber ID, or JID.
397 Each JID may have three components: a user, a domain, and an optional
398 resource. For example: user@domain/resource
400 When a resource is not used, the JID is called a bare JID.
401 The JID is a full JID otherwise.
404 :jid: Alias for ``full``.
405 :full: The string value of the full JID.
406 :bare: The string value of the bare JID.
407 :user: The username portion of the JID.
408 :username: Alias for ``user``.
409 :local: Alias for ``user``.
410 :node: Alias for ``user``.
411 :domain: The domain name portion of the JID.
412 :server: Alias for ``domain``.
413 :host: Alias for ``domain``.
414 :resource: The resource portion of the JID.
417 A string of the form ``'[user@]domain[/resource]'``.
419 Optional. Specify the local, or username, portion
420 of the JID. If provided, it will override the local
421 value provided by the `jid` parameter. The given
422 local value will also be escaped if necessary.
423 :param string domain:
424 Optional. Specify the domain of the JID. If
425 provided, it will override the domain given by
427 :param string resource:
428 Optional. Specify the resource value of the JID.
429 If provided, it will override the domain given
430 by the `jid` parameter.
435 # pylint: disable=W0212
436 def __init__(self
, jid
=None, **kwargs
):
437 locked
= kwargs
.get('cache_lock', False)
438 in_local
= kwargs
.get('local', None)
439 in_domain
= kwargs
.get('domain', None)
440 in_resource
= kwargs
.get('resource', None)
442 if in_local
or in_domain
or in_resource
:
443 parts
= (in_local
, in_domain
, in_resource
)
445 # only check cache if there is a jid string, or parts, not if there
449 if (jid
is not None) and (parts
is None):
450 if isinstance(jid
, JID
):
451 # it's already good to go, and there are no additions
455 self
._jid
, locked
= JID_CACHE
.get(jid
, (None, locked
))
456 elif jid
is None and parts
is not None:
458 self
._jid
, locked
= JID_CACHE
.get(parts
, (None, locked
))
461 parsed_jid
= (None, None, None)
462 elif not isinstance(jid
, JID
):
463 parsed_jid
= _parse_jid(jid
)
465 parsed_jid
= jid
._jid
467 local
, domain
, resource
= parsed_jid
469 if 'local' in kwargs
:
470 local
= _escape_node(in_local
)
471 if 'domain' in kwargs
:
472 domain
= _validate_domain(in_domain
)
473 if 'resource' in kwargs
:
474 resource
= _validate_resource(in_resource
)
476 self
._jid
= (local
, domain
, resource
)
478 _cache(key
, self
._jid
, locked
)
481 """Return an unescaped JID object.
483 Using an unescaped JID is preferred for displaying JIDs
484 to humans, and they should NOT be used for any other
485 purposes than for presentation.
487 :return: :class:`UnescapedJID`
489 .. versionadded:: 1.1.10
491 return UnescapedJID(_unescape_node(self
._jid
[0]),
495 def regenerate(self
):
498 .. deprecated:: 1.1.10
502 def reset(self
, data
):
503 """Start fresh from a new JID string.
505 :param string data: A string of the form ``'[user@]domain[/resource]'``.
507 .. deprecated:: 1.1.10
509 self
._jid
= JID(data
)._jid
513 return self
._jid
[2] or ''
517 return self
._jid
[0] or ''
521 return self
._jid
[0] or ''
525 return self
._jid
[0] or ''
529 return self
._jid
[0] or ''
533 return _format_jid(self
._jid
[0], self
._jid
[1])
537 return self
._jid
[1] or ''
541 return self
._jid
[1] or ''
545 return self
._jid
[1] or ''
549 return _format_jid(*self
._jid
)
553 return _format_jid(*self
._jid
)
557 return _format_jid(self
._jid
[0], self
._jid
[1])
561 def resource(self
, value
):
562 self
._jid
= JID(self
, resource
=value
)._jid
565 def user(self
, value
):
566 self
._jid
= JID(self
, local
=value
)._jid
569 def username(self
, value
):
570 self
._jid
= JID(self
, local
=value
)._jid
573 def local(self
, value
):
574 self
._jid
= JID(self
, local
=value
)._jid
577 def node(self
, value
):
578 self
._jid
= JID(self
, local
=value
)._jid
581 def server(self
, value
):
582 self
._jid
= JID(self
, domain
=value
)._jid
585 def domain(self
, value
):
586 self
._jid
= JID(self
, domain
=value
)._jid
589 def host(self
, value
):
590 self
._jid
= JID(self
, domain
=value
)._jid
593 def full(self
, value
):
594 self
._jid
= JID(value
)._jid
597 def jid(self
, value
):
598 self
._jid
= JID(value
)._jid
601 def bare(self
, value
):
602 parsed
= JID(value
)._jid
603 self
._jid
= (parsed
[0], parsed
[1], self
._jid
[2])
607 """Use the full JID as the string value."""
608 return _format_jid(*self
._jid
)
611 """Use the full JID as the representation."""
612 return self
.__str
__()
614 # pylint: disable=W0212
615 def __eq__(self
, other
):
616 """Two JIDs are equal if they have the same full JID value."""
617 if isinstance(other
, UnescapedJID
):
621 return self
._jid
== other
._jid
623 # pylint: disable=W0212
624 def __ne__(self
, other
):
625 """Two JIDs are considered unequal if they are not equal."""
626 return not self
== other
629 """Hash a JID based on the string version of its full JID."""
630 return hash(self
.__str
__())
633 """Generate a duplicate JID."""
636 def __deepcopy__(self
, memo
):
637 """Generate a duplicate JID."""
638 return JID(deepcopy(str(self
), memo
))