Wed, 03 Feb 2010 21:02:21 +0000
Match and fire handler callbacks for incoming stanzas
0 | 1 | // Node libs |
2 | var tcp = require("tcp"); | |
3 | ||
4 | // External libs | |
5 | var xml = require("./node-xml"); | |
6 | var sha1 = require("./sha1"); | |
7 | ||
8 | // This lib | |
9 | var xmpp = exports; | |
10 | ||
11 | // Wraps a function so that its 'this' is always 'context' when called | |
12 | var recontext = function (context, f) { return function () { return f.apply(context, arguments); }; }; | |
13 | ||
14 | xmpp.xmlns = { | |
15 | streams: "http://etherx.jabber.org/streams", | |
16 | component_accept: "jabber:component:accept" | |
17 | }; | |
18 | ||
19 | xmpp.Status = { | |
20 | ERROR: 0, | |
21 | CONNECTING: 1, | |
22 | CONNFAIL: 2, | |
23 | AUTHENTICATING: 3, | |
24 | AUTHFAIL: 4, | |
25 | CONNECTED: 5, | |
26 | DISCONNECTED: 6, | |
27 | DISCONNECTING: 7, | |
28 | }; | |
29 | ||
30 | xmpp.LogLevel = { | |
31 | DEBUG: 0, | |
32 | INFO: 1, | |
33 | WARN: 2, | |
34 | ERROR: 3, | |
35 | FATAL: 4 | |
36 | }; | |
37 | /** XMPPStream: Takes a parser, eats bytes, fires callbacks on stream events **/ | |
38 | xmpp.Stream = function (callbacks) | |
39 | { | |
40 | this.callbacks = callbacks; | |
41 | var stream = this; | |
42 | var stanza; | |
43 | this.parser = new xml.SaxParser(function (cb) | |
44 | { | |
45 | cb.onStartElementNS(function (tagname, attr_arr, prefix, uri, namespaces) | |
46 | { | |
4
67b1d93509d3
Strip default stream namespace from stanza objects
Matthew Wild <mwild1@gmail.com>
parents:
3
diff
changeset
|
47 | var attr = {}; |
67b1d93509d3
Strip default stream namespace from stanza objects
Matthew Wild <mwild1@gmail.com>
parents:
3
diff
changeset
|
48 | if(uri != xmpp.xmlns.component_accept) |
67b1d93509d3
Strip default stream namespace from stanza objects
Matthew Wild <mwild1@gmail.com>
parents:
3
diff
changeset
|
49 | attr.xmlns = uri; |
0 | 50 | for(var i=0;i<attr_arr.length;i++) |
51 | attr[attr_arr[i][0]] = attr_arr[i][1]; | |
52 | for(var i=0;i<namespaces.length;i++) | |
53 | if(namespaces[i][0].length > 0) | |
54 | attr["xmlns:"+namespaces[i][0]] = namespaces[i][1]; | |
55 | if(!stanza) | |
56 | { | |
57 | if(stream.opened) | |
58 | stanza = xmpp.stanza(tagname, attr); | |
59 | else if(tagname == "stream" && uri == xmpp.xmlns.streams) | |
60 | { | |
61 | stream.opened = true; | |
62 | callbacks.opened(attr); | |
63 | } | |
64 | else | |
65 | { | |
66 | callbacks.error("no-stream"); | |
67 | } | |
68 | } | |
69 | else | |
70 | { | |
71 | stanza.c(tagname, attr); | |
72 | } | |
73 | ||
74 | }); | |
75 | ||
76 | cb.onEndElementNS(function(tagname) { | |
77 | if(stanza) | |
78 | if(stanza.last_node.length == 1) | |
79 | { | |
80 | callbacks.stanza(stanza); | |
81 | stanza = null; | |
82 | } | |
83 | else | |
84 | stanza.up(); | |
85 | else | |
86 | { | |
87 | stream.opened = false; | |
88 | callbacks.closed(); | |
89 | } | |
90 | }); | |
91 | ||
92 | cb.onCharacters(function(chars) { | |
93 | if(stanza) | |
94 | stanza.t(chars); | |
95 | }); | |
96 | }); | |
97 | ||
98 | this.data = function (data) | |
99 | { | |
100 | return this.parser.parseString(data); | |
101 | } | |
102 | ||
103 | return this; | |
104 | }; | |
105 | ||
106 | ||
107 | /** Connection: Takes host/port, manages stream **/ | |
108 | xmpp.Connection = function (host, port) | |
109 | { | |
110 | this.host = host || "localhost"; | |
111 | this.port = port || 5347; | |
112 | ||
113 | this.socket = tcp.createConnection(); | |
114 | ||
115 | this.stream = new xmpp.Stream({ | |
116 | opened: recontext(this, this._stream_opened), | |
117 | stanza: recontext(this, this._handle_stanza), | |
118 | closed: recontext(this, this._stream_closed) | |
119 | }); | |
120 | ||
5 | 121 | this._uniqueId = 0; |
122 | ||
0 | 123 | return this; |
124 | }; | |
125 | ||
126 | exports.Connection.prototype = { | |
127 | connect: function (jid, pass, callback) | |
128 | { | |
129 | this.jid = jid; | |
130 | this.password = pass; | |
131 | this.connect_callback = callback; | |
132 | ||
133 | var conn = this; | |
134 | this.socket.addListener("connect", recontext(this, conn._socket_connected)); | |
135 | this.socket.addListener("disconnect", recontext(this, conn._socket_disconnected)); | |
136 | this.socket.addListener("receive", recontext(this, conn._socket_received)); | |
137 | ||
6 | 138 | this.handlers = []; |
139 | ||
0 | 140 | // Connect TCP socket |
141 | this.socket.connect(this.port, this.host); | |
142 | ||
143 | this._setStatus(xmpp.Status.CONNECTING); | |
144 | }, | |
145 | ||
146 | send: function (data) | |
147 | { | |
148 | this.debug("SND: "+data); | |
149 | this.socket.send(data.toString()); | |
150 | }, | |
151 | ||
5 | 152 | |
6 | 153 | addHandler: function (handler, ns, name, type, id, from, options) |
154 | { | |
155 | return this.handlers.push({ | |
156 | callback: handler, | |
157 | xmlns: ns, | |
158 | name: name, | |
159 | type: type, | |
160 | id: id, | |
161 | from: from, | |
162 | matchBare: options && options.matchBare}); | |
163 | }, | |
164 | ||
5 | 165 | getUniqueId: function (suffix) |
166 | { | |
167 | return ++this._uniqueId + (suffix?(":"+suffix):""); | |
168 | }, | |
169 | ||
0 | 170 | // Update the status of the connection, call connect_callback |
171 | _setStatus: function (status, condition) | |
172 | { | |
173 | this.status = status; | |
174 | this.connect_callback(status, condition); | |
175 | }, | |
176 | ||
177 | // Socket listeners, called on TCP-level events | |
178 | _socket_connected: function () | |
179 | { | |
180 | this.info("CONNECTED."); | |
181 | this.send("<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' to='"+this.jid+"'>"); | |
182 | }, | |
183 | ||
184 | _socket_disconnected: function (had_error) | |
185 | { | |
186 | if(this.status == xmpp.Status.CONNECTING) | |
187 | this._setStatus(xmpp.Status.CONNFAIL); | |
188 | elseif(this.status == xmpp.Status.CONNECTED) | |
189 | this._setStatus(xmpp.Status.DISCONNECTED); | |
190 | this.info("DISCONNECTED."); | |
191 | }, | |
192 | ||
193 | _socket_received: function (data) | |
194 | { | |
195 | this.debug("RCV: "+data); | |
196 | // Push to parser | |
197 | this.stream.data(data); | |
198 | }, | |
199 | ||
200 | // Stream listeners, called on XMPP-level events | |
201 | _stream_opened: function (attr) | |
202 | { | |
203 | this.debug("STREAM: opened."); | |
204 | this._setStatus(xmpp.Status.AUTHENTICATING); | |
205 | var handshake = sha1.hex(attr.id + this.password); | |
206 | this.debug("Sending authentication token..."); | |
207 | this.send("<handshake>"+handshake+"</handshake>"); | |
208 | }, | |
209 | ||
210 | _handle_stanza: function (stanza) | |
211 | { | |
4
67b1d93509d3
Strip default stream namespace from stanza objects
Matthew Wild <mwild1@gmail.com>
parents:
3
diff
changeset
|
212 | if(!stanza.attr.xmlns) // Default namespace |
0 | 213 | { |
214 | if(stanza.name == "handshake") | |
215 | { | |
216 | this._setStatus(xmpp.Status.CONNECTED); | |
217 | } | |
218 | } | |
219 | this.debug("STANZA: "+stanza.toString()); | |
7
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
220 | |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
221 | // Match and call handlers |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
222 | var removeHandlers = []; |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
223 | for(var i=0;i<this.handlers.length;i++) |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
224 | { |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
225 | var handler = this.handlers[i]; |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
226 | if( |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
227 | (!handler.name || handler.name == stanza.name) && |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
228 | (!handler.xmlns || (handler.xmlns == stanza.attr.xmlns |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
229 | || (stanza.tags[0] && handler.xmlns == stanza.tags[0].attr.xmlns))) && |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
230 | (!handler.type || handler.type == stanza.attr.type) && |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
231 | (!handler.id || handler.id == stanza.attr.id) && |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
232 | (!handler.from || (handler.from == (handler.matchBare?xmpp.getBareJID(stanza.attr.from):stanza.attr.from))) && |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
233 | (!handler.to || (handler.to == (handler.matchBare?xmpp.getBareJID(stanza.attr.to):stanza.attr.to))) |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
234 | ) |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
235 | { |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
236 | var ret = handler.callback(stanza); |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
237 | if(ret == false) |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
238 | removeHandlers.push(i); |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
239 | } |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
240 | } |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
241 | |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
242 | var adjust = 0; |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
243 | for(var i=0;i<removeHandlers.length;i++) |
394b0c8cad04
Match and fire handler callbacks for incoming stanzas
Matthew Wild <mwild1@gmail.com>
parents:
6
diff
changeset
|
244 | this.handlers.splice(removeHandlers[i]-(adjust++), 1); |
0 | 245 | }, |
246 | ||
247 | _stream_closed: function () | |
248 | { | |
249 | this.debug("STREAM: closed."); | |
250 | this.socket.close(); | |
251 | if(this.status == xmpp.Status.CONNECTING) | |
252 | this._setStatus(xmpp.status.CONNFAIL); | |
253 | else | |
254 | this._setStatus(xmpp.Status.DISCONNECTED); | |
255 | }, | |
256 | ||
257 | _stream_error: function (condition) | |
258 | { | |
259 | this._setStatus(xmpp.Status.ERROR, condition); | |
260 | }, | |
261 | ||
262 | // Logging | |
263 | log: function (level, message) {}, | |
264 | debug: function (message) { return this.log(xmpp.LogLevel.DEBUG, message); }, | |
265 | info: function (message) { return this.log(xmpp.LogLevel.INFO , message); }, | |
266 | warn: function (message) { return this.log(xmpp.LogLevel.WARN , message); }, | |
267 | error: function (message) { return this.log(xmpp.LogLevel.ERROR, message); }, | |
268 | fatal: function (message) { return this.log(xmpp.LogLevel.FATAL, message); } | |
269 | ||
270 | }; | |
271 | ||
272 | function xmlescape(s) | |
273 | { | |
274 | return s.replace(/&/g, "&") | |
275 | .replace(/</g, "<") | |
276 | .replace(/>/g, ">") | |
1 | 277 | .replace(/\"/g, """) |
278 | .replace(/\'/g, "'"); | |
0 | 279 | } |
280 | ||
281 | /** StanzaBuilder: Helps create and manipulate XML snippets **/ | |
282 | xmpp.StanzaBuilder = function (name, attr) | |
283 | { | |
284 | this.name = name; | |
285 | this.attr = attr || {}; | |
286 | this.tags = []; | |
287 | this.children = []; | |
288 | this.last_node = [this]; | |
289 | return this; | |
290 | }; | |
291 | ||
292 | xmpp.StanzaBuilder.prototype = { | |
293 | c: function (name, attr) | |
294 | { | |
295 | var s = new xmpp.StanzaBuilder(name, attr); | |
296 | var parent = this.last_node[this.last_node.length-1]; | |
297 | parent.tags.push(s); | |
298 | parent.children.push(s); | |
299 | this.last_node.push(s); | |
300 | return this; | |
301 | }, | |
302 | ||
303 | t: function (text) | |
304 | { | |
305 | var parent = this.last_node[this.last_node.length-1]; | |
306 | parent.children.push(text); | |
307 | return this; | |
308 | }, | |
309 | ||
310 | up: function () | |
311 | { | |
312 | this.last_node.pop(); | |
313 | return this; | |
314 | }, | |
315 | ||
316 | toString: function (top_tag_only) | |
317 | { | |
318 | var buf = []; | |
319 | buf.push("<" + this.name); | |
320 | for(var attr in this.attr) | |
321 | { | |
322 | buf.push(" " + attr + "='" + xmlescape(this.attr[attr]) + "'"); | |
323 | } | |
324 | ||
325 | // Now add children if wanted | |
326 | if(top_tag_only) | |
327 | { | |
328 | buf.push(">"); | |
329 | } | |
330 | else if(this.children.length == 0) | |
331 | { | |
332 | buf.push("/>"); | |
333 | } | |
334 | else | |
335 | { | |
336 | buf.push(">"); | |
337 | for(var i = 0; i<this.children.length; i++) | |
338 | { | |
339 | var child = this.children[i]; | |
340 | if(typeof(child) == "string") | |
341 | buf.push(xmlescape(child)); | |
342 | else | |
343 | buf.push(child.toString()); | |
344 | } | |
345 | buf.push("</" + this.name + ">"); | |
346 | } | |
347 | return buf.join(""); | |
2
b88bcbbe08e1
Add stanza.getChild(name, xmlns) method to mirror Prosody's API
Matthew Wild <mwild1@gmail.com>
parents:
1
diff
changeset
|
348 | }, |
b88bcbbe08e1
Add stanza.getChild(name, xmlns) method to mirror Prosody's API
Matthew Wild <mwild1@gmail.com>
parents:
1
diff
changeset
|
349 | |
b88bcbbe08e1
Add stanza.getChild(name, xmlns) method to mirror Prosody's API
Matthew Wild <mwild1@gmail.com>
parents:
1
diff
changeset
|
350 | getChild: function (name, xmlns) { |
b88bcbbe08e1
Add stanza.getChild(name, xmlns) method to mirror Prosody's API
Matthew Wild <mwild1@gmail.com>
parents:
1
diff
changeset
|
351 | for(var i=0;i<this.tags.length;i++) |
b88bcbbe08e1
Add stanza.getChild(name, xmlns) method to mirror Prosody's API
Matthew Wild <mwild1@gmail.com>
parents:
1
diff
changeset
|
352 | { |
b88bcbbe08e1
Add stanza.getChild(name, xmlns) method to mirror Prosody's API
Matthew Wild <mwild1@gmail.com>
parents:
1
diff
changeset
|
353 | var child = this.tags[i]; |
b88bcbbe08e1
Add stanza.getChild(name, xmlns) method to mirror Prosody's API
Matthew Wild <mwild1@gmail.com>
parents:
1
diff
changeset
|
354 | if((!name || child.name == name) && (!xmlns || child.attr.xmlns == xmlns)) |
b88bcbbe08e1
Add stanza.getChild(name, xmlns) method to mirror Prosody's API
Matthew Wild <mwild1@gmail.com>
parents:
1
diff
changeset
|
355 | return child; |
b88bcbbe08e1
Add stanza.getChild(name, xmlns) method to mirror Prosody's API
Matthew Wild <mwild1@gmail.com>
parents:
1
diff
changeset
|
356 | } |
b88bcbbe08e1
Add stanza.getChild(name, xmlns) method to mirror Prosody's API
Matthew Wild <mwild1@gmail.com>
parents:
1
diff
changeset
|
357 | return null; |
b88bcbbe08e1
Add stanza.getChild(name, xmlns) method to mirror Prosody's API
Matthew Wild <mwild1@gmail.com>
parents:
1
diff
changeset
|
358 | }, |
3
2d83fe899f5f
Add stanza.getText() to retrieve all text nodes joined as a string
Matthew Wild <mwild1@gmail.com>
parents:
2
diff
changeset
|
359 | |
2d83fe899f5f
Add stanza.getText() to retrieve all text nodes joined as a string
Matthew Wild <mwild1@gmail.com>
parents:
2
diff
changeset
|
360 | getText: function () { |
2d83fe899f5f
Add stanza.getText() to retrieve all text nodes joined as a string
Matthew Wild <mwild1@gmail.com>
parents:
2
diff
changeset
|
361 | var buf = []; |
2d83fe899f5f
Add stanza.getText() to retrieve all text nodes joined as a string
Matthew Wild <mwild1@gmail.com>
parents:
2
diff
changeset
|
362 | for(var i=0;i<this.children.length;i++) |
2d83fe899f5f
Add stanza.getText() to retrieve all text nodes joined as a string
Matthew Wild <mwild1@gmail.com>
parents:
2
diff
changeset
|
363 | if(typeof(this.children[i]) == "string") |
2d83fe899f5f
Add stanza.getText() to retrieve all text nodes joined as a string
Matthew Wild <mwild1@gmail.com>
parents:
2
diff
changeset
|
364 | buf.push(this.children[i]); |
2d83fe899f5f
Add stanza.getText() to retrieve all text nodes joined as a string
Matthew Wild <mwild1@gmail.com>
parents:
2
diff
changeset
|
365 | return buf.join(""); |
0 | 366 | } |
367 | } | |
368 | ||
369 | xmpp.stanza = function (name, attr) | |
370 | { | |
371 | return new xmpp.StanzaBuilder(name, attr); | |
372 | } | |
373 | ||
374 | xmpp.message = function (attr) | |
375 | { | |
376 | return xmpp.stanza("message", attr); | |
377 | } | |
378 | ||
379 | xmpp.presence = function (attr) | |
380 | { | |
381 | return xmpp.stanza("presence", attr); | |
382 | } | |
383 | ||
384 | xmpp.iq = function (attr) | |
385 | { | |
386 | return xmpp.stanza("iq", attr); | |
387 | } |