summaryrefslogtreecommitdiff
path: root/sleekxmpp/jid.py
blob: e6da5746b7c94d75418a3fd6beb63df1df7eb5bc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# -*- coding: utf-8 -*-
"""
    sleekxmpp.jid
    ~~~~~~~~~~~~~~~~~~~~~~~

    This module allows for working with Jabber IDs (JIDs) by
    providing accessors for the various components of a JID.

    Part of SleekXMPP: The Sleek XMPP Library

    :copyright: (c) 2011 Nathanael C. Fritz
    :license: MIT, see LICENSE for more details
"""

from __future__ import unicode_literals

import re
import socket
import stringprep
import encodings.idna

from sleekxmpp.util import stringprep_profiles


ILLEGAL_CHARS = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r' + \
                '\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19' + \
                '\x1a\x1b\x1c\x1d\x1e\x1f' + \
                ' !"#$%&\'()*+,./:;<=>?@[\\]^_`{|}~\x7f'

JID_PATTERN = "^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$"


nodeprep = stringprep_profiles.create(
    nfkc=True,
    bidi=True,
    mappings=[
        stringprep_profiles.b1_mapping,
        stringprep_profiles.c12_mapping],
    prohibited=[
        stringprep.in_table_c11,
        stringprep.in_table_c12,
        stringprep.in_table_c21,
        stringprep.in_table_c22,
        stringprep.in_table_c3,
        stringprep.in_table_c4,
        stringprep.in_table_c5,
        stringprep.in_table_c6,
        stringprep.in_table_c7,
        stringprep.in_table_c8,
        stringprep.in_table_c9,
        lambda c: c in '\'"&/:<>@'],
    unassigned=[stringprep.in_table_a1])


resourceprep = stringprep_profiles.create(
    nfkc=True,
    bidi=True,
    mappings=[stringprep_profiles.b1_mapping],
    prohibited=[
        stringprep.in_table_c12,
        stringprep.in_table_c21,
        stringprep.in_table_c22,
        stringprep.in_table_c3,
        stringprep.in_table_c4,
        stringprep.in_table_c5,
        stringprep.in_table_c6,
        stringprep.in_table_c7,
        stringprep.in_table_c8,
        stringprep.in_table_c9],
    unassigned=[stringprep.in_table_a1])


class InvalidJID(ValueError):
    pass


def parse_jid(data):
    """
    Parse string data into the node, domain, and resource
    components of a JID.
    """
    match = re.match(JID_PATTERN, data)
    if not match:
        raise InvalidJID

    (node, domain, resource) = match.groups()

    ip_addr = False

    try:
        socket.inet_aton(domain)
        ip_addr = True
    except socket.error:
        pass

    if not ip_addr and hasattr(socket, 'inet_pton'):
        try:
            socket.inet_pton(socket.AF_INET6, domain.strip('[]'))
            ip_addr = True
        except socket.error:
            pass

    if not ip_addr:
        domain_parts = []
        for label in domain.split('.'):
            try:
                label = encodings.idna.nameprep(label)
                encodings.idna.ToASCII(label)
            except UnicodeError:
                raise InvalidJID

            for char in label:
                if char in ILLEGAL_CHARS:
                    raise InvalidJID

            if '-' in (label[0], label[-1]):
                raise InvalidJID

            domain_parts.append(label)
        domain = '.'.join(domain_parts)

    try:
        if node is not None:
            node = nodeprep(node)
        if resource is not None:
            resource = resourceprep(resource)
    except stringprep_profiles.StringPrepError:
        raise InvalidJID

    return node, domain, resource


class JID(object):

    """
    A representation of a Jabber ID, or JID.

    Each JID may have three components: a user, a domain, and an optional
    resource. For example: user@domain/resource

    When a resource is not used, the JID is called a bare JID.
    The JID is a full JID otherwise.

    **JID Properties:**
        :jid: Alias for ``full``.
        :full: The string value of the full JID.
        :bare: The string value of the bare JID.
        :user: The username portion of the JID.
        :username: Alias for ``user``.
        :local: Alias for ``user``.
        :node: Alias for ``user``.
        :domain: The domain name portion of the JID.
        :server: Alias for ``domain``.
        :host: Alias for ``domain``.
        :resource: The resource portion of the JID.

    :param string jid: A string of the form ``'[user@]domain[/resource]'``.
    """

    def __init__(self, jid=None, local=None, domain=None, resource=None):
        """Initialize a new JID"""
        self._jid = (None, None, None)

        if jid is None or jid == '':
            jid = (None, None, None)
        elif not isinstance(jid, JID):
            jid = parse_jid(jid)
        else:
            jid = jid._jid

        orig_local, orig_domain, orig_resource = jid
        self._jid = (local or orig_local or None,
                     domain or orig_domain or None,
                     resource or orig_resource or None)

    def regenerate(self):
        """Deprecated"""
        pass

    def reset(self, data):
        """Start fresh from a new JID string.

        :param string data: A string of the form ``'[user@]domain[/resource]'``.
        """
        self._jid = JID(data)._jid

    def __getattr__(self, name):
        """handle getting the jid values, using cache if available.

        :param name: one of: user, server, domain, resource,
                     full, or bare.
        """
        if name == 'resource':
            return self._jid[2] or ''
        elif name in ('user', 'username', 'local', 'node'):
            return self._jid[0] or ''
        elif name in ('server', 'domain', 'host'):
            return self._jid[1] or ''
        elif name in ('full', 'jid'):
            return str(self)
        elif name == 'bare':
            return str(JID(local=self._jid[0],
                           domain=self._jid[1]))
        else:
            object.__getattr__(self, name)

    def __setattr__(self, name, value):
        """handle getting the jid values, using cache if available.

        :param name: one of: ``user``, ``username``, ``local``,
                             ``node``, ``server``, ``domain``, ``host``,
                             ``resource``, ``full``, ``jid``, or ``bare``.
        :param value: The new string value of the JID component.
        """
        if name == 'resource':
            self._jid = JID(self, resource=value)._jid
        elif name in ('user', 'username', 'local', 'node'):
            self._jid = JID(self, local=value)._jid
        elif name in ('server', 'domain', 'host'):
            self._jid = JID(self, domain=value)._jid
        elif name in ('full', 'jid'):
            self._jid = JID(value)._jid
        elif name == 'bare':
            parsed = JID(value)._jid
            self._jid = (parsed[0], parsed[1], self._jid[2])
        else:
            object.__setattr__(self, name, value)

    def __str__(self):
        """Use the full JID as the string value."""
        result = []
        if self._jid[0]:
            result.append(self._jid[0])
            result.append('@')
        if self._jid[1]:
            result.append(self._jid[1])
        if self._jid[2]:
            result.append('/')
            result.append(self._jid[2])
        return ''.join(result)

    def __repr__(self):
        return self.__str__()

    def __eq__(self, other):
        """
        Two JIDs are considered equal if they have the same full JID value.
        """
        other = JID(other)
        return self._jid == other._jid

    def __ne__(self, other):
        """Two JIDs are considered unequal if they are not equal."""
        return not self._jid == other._jid

    def __hash__(self):
        """Hash a JID based on the string version of its full JID."""
        return hash(self.__str__())

    def __copy__(self):
        """Generate a duplicate JID."""
        return JID(self)