OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.words.test.test_jabberclient -*- | |
2 # | |
3 # Copyright (c) 2001-2005 Twisted Matrix Laboratories. | |
4 # See LICENSE for details. | |
5 | |
6 from twisted.internet import defer | |
7 from twisted.words.xish import domish, xpath, utility | |
8 from twisted.words.protocols.jabber import xmlstream, sasl, error | |
9 from twisted.words.protocols.jabber.jid import JID | |
10 | |
11 NS_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams' | |
12 NS_XMPP_BIND = 'urn:ietf:params:xml:ns:xmpp-bind' | |
13 NS_XMPP_SESSION = 'urn:ietf:params:xml:ns:xmpp-session' | |
14 NS_IQ_AUTH_FEATURE = 'http://jabber.org/features/iq-auth' | |
15 | |
16 DigestAuthQry = xpath.internQuery("/iq/query/digest") | |
17 PlaintextAuthQry = xpath.internQuery("/iq/query/password") | |
18 | |
19 def basicClientFactory(jid, secret): | |
20 a = BasicAuthenticator(jid, secret) | |
21 return xmlstream.XmlStreamFactory(a) | |
22 | |
23 class IQ(domish.Element): | |
24 """ | |
25 Wrapper for a Info/Query packet. | |
26 | |
27 This provides the necessary functionality to send IQs and get notified when | |
28 a result comes back. It's a subclass from L{domish.Element}, so you can use | |
29 the standard DOM manipulation calls to add data to the outbound request. | |
30 | |
31 @type callbacks: L{utility.CallbackList} | |
32 @cvar callbacks: Callback list to be notified when response comes back | |
33 | |
34 """ | |
35 def __init__(self, xmlstream, type = "set"): | |
36 """ | |
37 @type xmlstream: L{xmlstream.XmlStream} | |
38 @param xmlstream: XmlStream to use for transmission of this IQ | |
39 | |
40 @type type: L{str} | |
41 @param type: IQ type identifier ('get' or 'set') | |
42 """ | |
43 | |
44 domish.Element.__init__(self, ("jabber:client", "iq")) | |
45 self.addUniqueId() | |
46 self["type"] = type | |
47 self._xmlstream = xmlstream | |
48 self.callbacks = utility.CallbackList() | |
49 | |
50 def addCallback(self, fn, *args, **kwargs): | |
51 """ | |
52 Register a callback for notification when the IQ result is available. | |
53 """ | |
54 | |
55 self.callbacks.addCallback(True, fn, *args, **kwargs) | |
56 | |
57 def send(self, to = None): | |
58 """ | |
59 Call this method to send this IQ request via the associated XmlStream. | |
60 | |
61 @param to: Jabber ID of the entity to send the request to | |
62 @type to: L{str} | |
63 | |
64 @returns: Callback list for this IQ. Any callbacks added to this list | |
65 will be fired when the result comes back. | |
66 """ | |
67 if to != None: | |
68 self["to"] = to | |
69 self._xmlstream.addOnetimeObserver("/iq[@id='%s']" % self["id"], \ | |
70 self._resultEvent) | |
71 self._xmlstream.send(self) | |
72 | |
73 def _resultEvent(self, iq): | |
74 self.callbacks.callback(iq) | |
75 self.callbacks = None | |
76 | |
77 | |
78 | |
79 class IQAuthInitializer(object): | |
80 """ | |
81 Non-SASL Authentication initializer for the initiating entity. | |
82 | |
83 This protocol is defined in | |
84 U{JEP-0078<http://www.jabber.org/jeps/jep-0078.html>} and mainly serves for | |
85 compatibility with pre-XMPP-1.0 server implementations. | |
86 """ | |
87 | |
88 INVALID_USER_EVENT = "//event/client/basicauth/invaliduser" | |
89 AUTH_FAILED_EVENT = "//event/client/basicauth/authfailed" | |
90 | |
91 def __init__(self, xs): | |
92 self.xmlstream = xs | |
93 | |
94 | |
95 def initialize(self): | |
96 # Send request for auth fields | |
97 iq = xmlstream.IQ(self.xmlstream, "get") | |
98 iq.addElement(("jabber:iq:auth", "query")) | |
99 jid = self.xmlstream.authenticator.jid | |
100 iq.query.addElement("username", content = jid.user) | |
101 | |
102 d = iq.send() | |
103 d.addCallbacks(self._cbAuthQuery, self._ebAuthQuery) | |
104 return d | |
105 | |
106 | |
107 def _cbAuthQuery(self, iq): | |
108 jid = self.xmlstream.authenticator.jid | |
109 password = self.xmlstream.authenticator.password | |
110 | |
111 # Construct auth request | |
112 reply = xmlstream.IQ(self.xmlstream, "set") | |
113 reply.addElement(("jabber:iq:auth", "query")) | |
114 reply.query.addElement("username", content = jid.user) | |
115 reply.query.addElement("resource", content = jid.resource) | |
116 | |
117 # Prefer digest over plaintext | |
118 if DigestAuthQry.matches(iq): | |
119 digest = xmlstream.hashPassword(self.xmlstream.sid, password) | |
120 reply.query.addElement("digest", content = digest) | |
121 else: | |
122 reply.query.addElement("password", content = password) | |
123 | |
124 d = reply.send() | |
125 d.addCallbacks(self._cbAuth, self._ebAuth) | |
126 return d | |
127 | |
128 | |
129 def _ebAuthQuery(self, failure): | |
130 failure.trap(error.StanzaError) | |
131 e = failure.value | |
132 if e.condition == 'not-authorized': | |
133 self.xmlstream.dispatch(e.stanza, self.INVALID_USER_EVENT) | |
134 else: | |
135 self.xmlstream.dispatch(e.stanza, self.AUTH_FAILED_EVENT) | |
136 | |
137 return failure | |
138 | |
139 | |
140 def _cbAuth(self, iq): | |
141 pass | |
142 | |
143 | |
144 def _ebAuth(self, failure): | |
145 failure.trap(error.StanzaError) | |
146 self.xmlstream.dispatch(failure.value.stanza, self.AUTH_FAILED_EVENT) | |
147 return failure | |
148 | |
149 | |
150 | |
151 class BasicAuthenticator(xmlstream.ConnectAuthenticator): | |
152 """ | |
153 Authenticates an XmlStream against a Jabber server as a Client. | |
154 | |
155 This only implements non-SASL authentication, per | |
156 U{JEP-0078<http://www.jabber.org/jeps/jep-0078.html>}. Additionally, this | |
157 authenticator provides the ability to perform inline registration, per | |
158 U{JEP-0077<http://www.jabber.org/jeps/jep-0077.html>}. | |
159 | |
160 Under normal circumstances, the BasicAuthenticator generates the | |
161 L{xmlstream.STREAM_AUTHD_EVENT} once the stream has authenticated. However, | |
162 it can also generate other events, such as: | |
163 - L{INVALID_USER_EVENT} : Authentication failed, due to invalid username | |
164 - L{AUTH_FAILED_EVENT} : Authentication failed, due to invalid password | |
165 - L{REGISTER_FAILED_EVENT} : Registration failed | |
166 | |
167 If authentication fails for any reason, you can attempt to register by | |
168 calling the L{registerAccount} method. If the registration succeeds, a | |
169 L{xmlstream.STREAM_AUTHD_EVENT} will be fired. Otherwise, one of the above | |
170 errors will be generated (again). | |
171 """ | |
172 | |
173 namespace = "jabber:client" | |
174 | |
175 INVALID_USER_EVENT = IQAuthInitializer.INVALID_USER_EVENT | |
176 AUTH_FAILED_EVENT = IQAuthInitializer.AUTH_FAILED_EVENT | |
177 REGISTER_FAILED_EVENT = "//event/client/basicauth/registerfailed" | |
178 | |
179 def __init__(self, jid, password): | |
180 xmlstream.ConnectAuthenticator.__init__(self, jid.host) | |
181 self.jid = jid | |
182 self.password = password | |
183 | |
184 def associateWithStream(self, xs): | |
185 xs.version = (0, 0) | |
186 xmlstream.ConnectAuthenticator.associateWithStream(self, xs) | |
187 | |
188 inits = [ (xmlstream.TLSInitiatingInitializer, False), | |
189 (IQAuthInitializer, True), | |
190 ] | |
191 | |
192 for initClass, required in inits: | |
193 init = initClass(xs) | |
194 init.required = required | |
195 xs.initializers.append(init) | |
196 | |
197 # TODO: move registration into an Initializer? | |
198 | |
199 def registerAccount(self, username = None, password = None): | |
200 if username: | |
201 self.jid.user = username | |
202 if password: | |
203 self.password = password | |
204 | |
205 iq = IQ(self.xmlstream, "set") | |
206 iq.addElement(("jabber:iq:register", "query")) | |
207 iq.query.addElement("username", content = self.jid.user) | |
208 iq.query.addElement("password", content = self.password) | |
209 | |
210 iq.addCallback(self._registerResultEvent) | |
211 | |
212 iq.send() | |
213 | |
214 def _registerResultEvent(self, iq): | |
215 if iq["type"] == "result": | |
216 # Registration succeeded -- go ahead and auth | |
217 self.streamStarted() | |
218 else: | |
219 # Registration failed | |
220 self.xmlstream.dispatch(iq, self.REGISTER_FAILED_EVENT) | |
221 | |
222 | |
223 | |
224 class CheckVersionInitializer(object): | |
225 """ | |
226 Initializer that checks if the minimum common stream version number is 1.0. | |
227 """ | |
228 | |
229 def __init__(self, xs): | |
230 self.xmlstream = xs | |
231 | |
232 | |
233 def initialize(self): | |
234 if self.xmlstream.version < (1, 0): | |
235 raise error.StreamError('unsupported-version') | |
236 | |
237 | |
238 | |
239 class BindInitializer(xmlstream.BaseFeatureInitiatingInitializer): | |
240 """ | |
241 Initializer that implements Resource Binding for the initiating entity. | |
242 | |
243 This protocol is documented in U{RFC 3920, section | |
244 7<http://www.xmpp.org/specs/rfc3920.html#bind>}. | |
245 """ | |
246 | |
247 feature = (NS_XMPP_BIND, 'bind') | |
248 | |
249 def start(self): | |
250 iq = xmlstream.IQ(self.xmlstream, 'set') | |
251 bind = iq.addElement((NS_XMPP_BIND, 'bind')) | |
252 resource = self.xmlstream.authenticator.jid.resource | |
253 if resource: | |
254 bind.addElement('resource', content=resource) | |
255 d = iq.send() | |
256 d.addCallback(self.onBind) | |
257 return d | |
258 | |
259 | |
260 def onBind(self, iq): | |
261 if iq.bind: | |
262 self.xmlstream.authenticator.jid = JID(unicode(iq.bind.jid)) | |
263 | |
264 | |
265 | |
266 class SessionInitializer(xmlstream.BaseFeatureInitiatingInitializer): | |
267 """ | |
268 Initializer that implements session establishment for the initiating | |
269 entity. | |
270 | |
271 This protocol is defined in U{RFC 3921, section | |
272 3<http://www.xmpp.org/specs/rfc3921.html#session>}. | |
273 """ | |
274 | |
275 feature = (NS_XMPP_SESSION, 'session') | |
276 | |
277 def start(self): | |
278 iq = xmlstream.IQ(self.xmlstream, 'set') | |
279 session = iq.addElement((NS_XMPP_SESSION, 'session')) | |
280 return iq.send() | |
281 | |
282 | |
283 | |
284 def XMPPClientFactory(jid, password): | |
285 """ | |
286 Client factory for XMPP 1.0 (only). | |
287 | |
288 This returns a L{xmlstream.XmlStreamFactory} with an L{XMPPAuthenticator} | |
289 object to perform the stream initialization steps (such as authentication). | |
290 | |
291 @see: The notes at L{XMPPAuthenticator} describe how the L{jid} and | |
292 L{password} parameters are to be used. | |
293 | |
294 @param jid: Jabber ID to connect with. | |
295 @type jid: L{jid.JID} | |
296 @param password: password to authenticate with. | |
297 @type password: L{unicode} | |
298 @return: XML stream factory. | |
299 @rtype: L{xmlstream.XmlStreamFactory} | |
300 """ | |
301 a = XMPPAuthenticator(jid, password) | |
302 return xmlstream.XmlStreamFactory(a) | |
303 | |
304 | |
305 | |
306 class XMPPAuthenticator(xmlstream.ConnectAuthenticator): | |
307 """ | |
308 Initializes an XmlStream connecting to an XMPP server as a Client. | |
309 | |
310 This authenticator performs the initialization steps needed to start | |
311 exchanging XML stanzas with an XMPP server as an XMPP client. It checks if | |
312 the server advertises XML stream version 1.0, negotiates TLS (when | |
313 available), performs SASL authentication, binds a resource and establishes | |
314 a session. | |
315 | |
316 Upon successful stream initialization, the L{xmlstream.STREAM_AUTHD_EVENT} | |
317 event will be dispatched through the XML stream object. Otherwise, the | |
318 L{xmlstream.INIT_FAILED_EVENT} event will be dispatched with a failure | |
319 object. | |
320 | |
321 After inspection of the failure, initialization can then be restarted by | |
322 calling L{initializeStream}. For example, in case of authentication | |
323 failure, a user may be given the opportunity to input the correct password. | |
324 By setting the L{password} instance variable and restarting initialization, | |
325 the stream authentication step is then retried, and subsequent steps are | |
326 performed if succesful. | |
327 | |
328 @ivar jid: Jabber ID to authenticate with. This may contain a resource | |
329 part, as a suggestion to the server for resource binding. A | |
330 server may override this, though. If the resource part is left | |
331 off, the server will generate a unique resource identifier. | |
332 The server will always return the full Jabber ID in the | |
333 resource binding step, and this is stored in this instance | |
334 variable. | |
335 @type jid: L{jid.JID} | |
336 @ivar password: password to be used during SASL authentication. | |
337 @type password: L{unicode} | |
338 """ | |
339 | |
340 namespace = 'jabber:client' | |
341 | |
342 def __init__(self, jid, password): | |
343 xmlstream.ConnectAuthenticator.__init__(self, jid.host) | |
344 self.jid = jid | |
345 self.password = password | |
346 | |
347 | |
348 def associateWithStream(self, xs): | |
349 """ | |
350 Register with the XML stream. | |
351 | |
352 Populates stream's list of initializers, along with their | |
353 requiredness. This list is used by | |
354 L{ConnectAuthenticator.initializeStream} to perform the initalization | |
355 steps. | |
356 """ | |
357 xmlstream.ConnectAuthenticator.associateWithStream(self, xs) | |
358 | |
359 xs.initializers = [CheckVersionInitializer(xs)] | |
360 inits = [ (xmlstream.TLSInitiatingInitializer, False), | |
361 (sasl.SASLInitiatingInitializer, True), | |
362 (BindInitializer, False), | |
363 (SessionInitializer, False), | |
364 ] | |
365 | |
366 for initClass, required in inits: | |
367 init = initClass(xs) | |
368 init.required = required | |
369 xs.initializers.append(init) | |
OLD | NEW |