1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """XMPP stream support with fallback to legacy non-SASL Jabber authentication.
18
19 Normative reference:
20 - `JEP 78 <http://www.jabber.org/jeps/jep-0078.html>`__
21 """
22
23 __docformat__="restructuredtext en"
24
25 import hashlib
26 import logging
27
28 from pyxmpp.iq import Iq
29 from pyxmpp.utils import to_utf8,from_utf8
30 from pyxmpp.jid import JID
31 from pyxmpp.clientstream import ClientStream
32 from pyxmpp.jabber.register import Register
33
34 from pyxmpp.exceptions import ClientStreamError, LegacyAuthenticationError, RegistrationError
35
37 """Handles Jabber (both XMPP and legacy protocol) client connection stream.
38
39 Both client and server side of the connection is supported. This class handles
40 client SASL and legacy authentication, authorisation and XMPP resource binding.
41 """
42 - def __init__(self, jid, password = None, server = None, port = 5222,
43 auth_methods = ("sasl:DIGEST-MD5", "digest"),
44 tls_settings = None, keepalive = 0, owner = None):
45 """Initialize a LegacyClientStream object.
46
47 :Parameters:
48 - `jid`: local JID.
49 - `password`: user's password.
50 - `server`: server to use. If not given then address will be derived form the JID.
51 - `port`: port number to use. If not given then address will be derived form the JID.
52 - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms
53 in the list should be prefixed with "sasl:" string.
54 - `tls_settings`: settings for StartTLS -- `TLSSettings` instance.
55 - `keepalive`: keepalive output interval. 0 to disable.
56 - `owner`: `Client`, `Component` or similar object "owning" this stream.
57 :Types:
58 - `jid`: `pyxmpp.JID`
59 - `password`: `unicode`
60 - `server`: `unicode`
61 - `port`: `int`
62 - `auth_methods`: sequence of `str`
63 - `tls_settings`: `pyxmpp.TLSSettings`
64 - `keepalive`: `int`
65 """
66 (self.authenticated, self.available_auth_methods, self.auth_stanza,
67 self.peer_authenticated, self.auth_method_used,
68 self.registration_callback, self.registration_form, self.__register) = (None,) * 8
69 ClientStream.__init__(self, jid, password, server, port,
70 auth_methods, tls_settings, keepalive, owner)
71 self.__logger=logging.getLogger("pyxmpp.jabber.LegacyClientStream")
72
74 """Reset the `LegacyClientStream` object state, making the object ready
75 to handle new connections."""
76 ClientStream._reset(self)
77 self.available_auth_methods = None
78 self.auth_stanza = None
79 self.registration_callback = None
80
81 - def _post_connect(self):
82 """Initialize authentication when the connection is established
83 and we are the initiator."""
84 if not self.initiator:
85 if "plain" in self.auth_methods or "digest" in self.auth_methods:
86 self.set_iq_get_handler("query","jabber:iq:auth",
87 self.auth_in_stage1)
88 self.set_iq_set_handler("query","jabber:iq:auth",
89 self.auth_in_stage2)
90 elif self.registration_callback:
91 iq = Iq(stanza_type = "get")
92 iq.set_content(Register())
93 self.set_response_handlers(iq, self.registration_form_received, self.registration_error)
94 self.send(iq)
95 return
96 ClientStream._post_connect(self)
97
98 - def _post_auth(self):
99 """Unregister legacy authentication handlers after successfull
100 authentication."""
101 ClientStream._post_auth(self)
102 if not self.initiator:
103 self.unset_iq_get_handler("query","jabber:iq:auth")
104 self.unset_iq_set_handler("query","jabber:iq:auth")
105
107 """Try to authenticate using the first one of allowed authentication
108 methods left.
109
110 [client only]"""
111 if self.authenticated:
112 self.__logger.debug("try_auth: already authenticated")
113 return
114 self.__logger.debug("trying auth: %r" % (self._auth_methods_left,))
115 if not self._auth_methods_left:
116 raise LegacyAuthenticationError,"No allowed authentication methods available"
117 method=self._auth_methods_left[0]
118 if method.startswith("sasl:"):
119 return ClientStream._try_auth(self)
120 elif method not in ("plain","digest"):
121 self._auth_methods_left.pop(0)
122 self.__logger.debug("Skipping unknown auth method: %s" % method)
123 return self._try_auth()
124 elif self.available_auth_methods is not None:
125 if method in self.available_auth_methods:
126 self._auth_methods_left.pop(0)
127 self.auth_method_used=method
128 if method=="digest":
129 self._digest_auth_stage2(self.auth_stanza)
130 else:
131 self._plain_auth_stage2(self.auth_stanza)
132 self.auth_stanza=None
133 return
134 else:
135 self.__logger.debug("Skipping unavailable auth method: %s" % method)
136 else:
137 self._auth_stage1()
138
140 """Handle the first stage (<iq type='get'/>) of legacy ("plain" or
141 "digest") authentication.
142
143 [server only]"""
144 self.lock.acquire()
145 try:
146 if "plain" not in self.auth_methods and "digest" not in self.auth_methods:
147 iq=stanza.make_error_response("not-allowed")
148 self.send(iq)
149 return
150
151 iq=stanza.make_result_response()
152 q=iq.new_query("jabber:iq:auth")
153 q.newChild(None,"username",None)
154 q.newChild(None,"resource",None)
155 if "plain" in self.auth_methods:
156 q.newChild(None,"password",None)
157 if "digest" in self.auth_methods:
158 q.newChild(None,"digest",None)
159 self.send(iq)
160 iq.free()
161 finally:
162 self.lock.release()
163
204
218
220 """Handle legacy authentication timeout.
221
222 [client only]"""
223 self.lock.acquire()
224 try:
225 self.__logger.debug("Timeout while waiting for jabber:iq:auth result")
226 if self._auth_methods_left:
227 self._auth_methods_left.pop(0)
228 finally:
229 self.lock.release()
230
232 """Handle legacy authentication error.
233
234 [client only]"""
235 self.lock.acquire()
236 try:
237 err=stanza.get_error()
238 ae=err.xpath_eval("e:*",{"e":"jabber:iq:auth:error"})
239 if ae:
240 ae=ae[0].name
241 else:
242 ae=err.get_condition().name
243 raise LegacyAuthenticationError,("Authentication error condition: %s"
244 % (ae,))
245 finally:
246 self.lock.release()
247
249 """Handle the first stage authentication response (result of the <iq
250 type="get"/>).
251
252 [client only]"""
253 self.lock.acquire()
254 try:
255 self.__logger.debug("Procesing auth response...")
256 self.available_auth_methods=[]
257 if (stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"}) and self.stream_id):
258 self.available_auth_methods.append("digest")
259 if (stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"})):
260 self.available_auth_methods.append("plain")
261 self.auth_stanza=stanza.copy()
262 self._try_auth()
263 finally:
264 self.lock.release()
265
279
307
324
362
364 """Handle success of the legacy authentication."""
365 self.lock.acquire()
366 try:
367 self.__logger.debug("Authenticated")
368 self.authenticated=True
369 self.state_change("authorized",self.my_jid)
370 self._post_auth()
371 finally:
372 self.lock.release()
373
375 """Handle in-band registration error.
376
377 [client only]
378
379 :Parameters:
380 - `stanza`: the error stanza received or `None` on timeout.
381 :Types:
382 - `stanza`: `pyxmpp.stanza.Stanza`"""
383 self.lock.acquire()
384 try:
385 err=stanza.get_error()
386 ae=err.xpath_eval("e:*",{"e":"jabber:iq:auth:error"})
387 if ae:
388 ae=ae[0].name
389 else:
390 ae=err.get_condition().name
391 raise RegistrationError,("Authentication error condition: %s" % (ae,))
392 finally:
393 self.lock.release()
394
414
438
440 """Handle registration success.
441
442 [client only]
443
444 Clean up registration stuff, change state to "registered" and initialize
445 authentication.
446
447 :Parameters:
448 - `stanza`: the stanza received.
449 :Types:
450 - `stanza`: `pyxmpp.iq.Iq`"""
451 _unused = stanza
452 self.lock.acquire()
453 try:
454 self.state_change("registered", self.registration_form)
455 if ('FORM_TYPE' in self.registration_form
456 and self.registration_form['FORM_TYPE'].value == 'jabber:iq:register'):
457 if 'username' in self.registration_form:
458 self.my_jid = JID(self.registration_form['username'].value,
459 self.my_jid.domain, self.my_jid.resource)
460 if 'password' in self.registration_form:
461 self.password = self.registration_form['password'].value
462 self.registration_callback = None
463 self._post_connect()
464 finally:
465 self.lock.release()
466
467
468