|
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 { |
|
47 var attr = {xmlns:uri}; |
|
48 for(var i=0;i<attr_arr.length;i++) |
|
49 attr[attr_arr[i][0]] = attr_arr[i][1]; |
|
50 for(var i=0;i<namespaces.length;i++) |
|
51 if(namespaces[i][0].length > 0) |
|
52 attr["xmlns:"+namespaces[i][0]] = namespaces[i][1]; |
|
53 if(!stanza) |
|
54 { |
|
55 if(stream.opened) |
|
56 stanza = xmpp.stanza(tagname, attr); |
|
57 else if(tagname == "stream" && uri == xmpp.xmlns.streams) |
|
58 { |
|
59 stream.opened = true; |
|
60 callbacks.opened(attr); |
|
61 } |
|
62 else |
|
63 { |
|
64 callbacks.error("no-stream"); |
|
65 } |
|
66 } |
|
67 else |
|
68 { |
|
69 stanza.c(tagname, attr); |
|
70 } |
|
71 |
|
72 }); |
|
73 |
|
74 cb.onEndElementNS(function(tagname) { |
|
75 if(stanza) |
|
76 if(stanza.last_node.length == 1) |
|
77 { |
|
78 callbacks.stanza(stanza); |
|
79 stanza = null; |
|
80 } |
|
81 else |
|
82 stanza.up(); |
|
83 else |
|
84 { |
|
85 stream.opened = false; |
|
86 callbacks.closed(); |
|
87 } |
|
88 }); |
|
89 |
|
90 cb.onCharacters(function(chars) { |
|
91 if(stanza) |
|
92 stanza.t(chars); |
|
93 }); |
|
94 }); |
|
95 |
|
96 this.data = function (data) |
|
97 { |
|
98 return this.parser.parseString(data); |
|
99 } |
|
100 |
|
101 return this; |
|
102 }; |
|
103 |
|
104 |
|
105 /** Connection: Takes host/port, manages stream **/ |
|
106 xmpp.Connection = function (host, port) |
|
107 { |
|
108 this.host = host || "localhost"; |
|
109 this.port = port || 5347; |
|
110 |
|
111 this.socket = tcp.createConnection(); |
|
112 |
|
113 this.stream = new xmpp.Stream({ |
|
114 opened: recontext(this, this._stream_opened), |
|
115 stanza: recontext(this, this._handle_stanza), |
|
116 closed: recontext(this, this._stream_closed) |
|
117 }); |
|
118 |
|
119 return this; |
|
120 }; |
|
121 |
|
122 exports.Connection.prototype = { |
|
123 connect: function (jid, pass, callback) |
|
124 { |
|
125 this.jid = jid; |
|
126 this.password = pass; |
|
127 this.connect_callback = callback; |
|
128 |
|
129 var conn = this; |
|
130 this.socket.addListener("connect", recontext(this, conn._socket_connected)); |
|
131 this.socket.addListener("disconnect", recontext(this, conn._socket_disconnected)); |
|
132 this.socket.addListener("receive", recontext(this, conn._socket_received)); |
|
133 |
|
134 // Connect TCP socket |
|
135 this.socket.connect(this.port, this.host); |
|
136 |
|
137 this._setStatus(xmpp.Status.CONNECTING); |
|
138 }, |
|
139 |
|
140 send: function (data) |
|
141 { |
|
142 this.debug("SND: "+data); |
|
143 this.socket.send(data.toString()); |
|
144 }, |
|
145 |
|
146 // Update the status of the connection, call connect_callback |
|
147 _setStatus: function (status, condition) |
|
148 { |
|
149 this.status = status; |
|
150 this.connect_callback(status, condition); |
|
151 }, |
|
152 |
|
153 // Socket listeners, called on TCP-level events |
|
154 _socket_connected: function () |
|
155 { |
|
156 this.info("CONNECTED."); |
|
157 this.send("<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' to='"+this.jid+"'>"); |
|
158 }, |
|
159 |
|
160 _socket_disconnected: function (had_error) |
|
161 { |
|
162 if(this.status == xmpp.Status.CONNECTING) |
|
163 this._setStatus(xmpp.Status.CONNFAIL); |
|
164 elseif(this.status == xmpp.Status.CONNECTED) |
|
165 this._setStatus(xmpp.Status.DISCONNECTED); |
|
166 this.info("DISCONNECTED."); |
|
167 }, |
|
168 |
|
169 _socket_received: function (data) |
|
170 { |
|
171 this.debug("RCV: "+data); |
|
172 // Push to parser |
|
173 this.stream.data(data); |
|
174 }, |
|
175 |
|
176 // Stream listeners, called on XMPP-level events |
|
177 _stream_opened: function (attr) |
|
178 { |
|
179 this.debug("STREAM: opened."); |
|
180 this._setStatus(xmpp.Status.AUTHENTICATING); |
|
181 var handshake = sha1.hex(attr.id + this.password); |
|
182 this.debug("Sending authentication token..."); |
|
183 this.send("<handshake>"+handshake+"</handshake>"); |
|
184 }, |
|
185 |
|
186 _handle_stanza: function (stanza) |
|
187 { |
|
188 if(stanza.attr.xmlns == xmpp.xmlns.component_accept) |
|
189 { |
|
190 if(stanza.name == "handshake") |
|
191 { |
|
192 this._setStatus(xmpp.Status.CONNECTED); |
|
193 } |
|
194 } |
|
195 this.debug("STANZA: "+stanza.toString()); |
|
196 }, |
|
197 |
|
198 _stream_closed: function () |
|
199 { |
|
200 this.debug("STREAM: closed."); |
|
201 this.socket.close(); |
|
202 if(this.status == xmpp.Status.CONNECTING) |
|
203 this._setStatus(xmpp.status.CONNFAIL); |
|
204 else |
|
205 this._setStatus(xmpp.Status.DISCONNECTED); |
|
206 }, |
|
207 |
|
208 _stream_error: function (condition) |
|
209 { |
|
210 this._setStatus(xmpp.Status.ERROR, condition); |
|
211 }, |
|
212 |
|
213 // Logging |
|
214 log: function (level, message) {}, |
|
215 debug: function (message) { return this.log(xmpp.LogLevel.DEBUG, message); }, |
|
216 info: function (message) { return this.log(xmpp.LogLevel.INFO , message); }, |
|
217 warn: function (message) { return this.log(xmpp.LogLevel.WARN , message); }, |
|
218 error: function (message) { return this.log(xmpp.LogLevel.ERROR, message); }, |
|
219 fatal: function (message) { return this.log(xmpp.LogLevel.FATAL, message); } |
|
220 |
|
221 }; |
|
222 |
|
223 function xmlescape(s) |
|
224 { |
|
225 return s.replace(/&/g, "&") |
|
226 .replace(/</g, "<") |
|
227 .replace(/>/g, ">") |
|
228 .replace(/\"/g, """); |
|
229 } |
|
230 |
|
231 /** StanzaBuilder: Helps create and manipulate XML snippets **/ |
|
232 xmpp.StanzaBuilder = function (name, attr) |
|
233 { |
|
234 this.name = name; |
|
235 this.attr = attr || {}; |
|
236 this.tags = []; |
|
237 this.children = []; |
|
238 this.last_node = [this]; |
|
239 return this; |
|
240 }; |
|
241 |
|
242 xmpp.StanzaBuilder.prototype = { |
|
243 c: function (name, attr) |
|
244 { |
|
245 var s = new xmpp.StanzaBuilder(name, attr); |
|
246 var parent = this.last_node[this.last_node.length-1]; |
|
247 parent.tags.push(s); |
|
248 parent.children.push(s); |
|
249 this.last_node.push(s); |
|
250 return this; |
|
251 }, |
|
252 |
|
253 t: function (text) |
|
254 { |
|
255 var parent = this.last_node[this.last_node.length-1]; |
|
256 parent.children.push(text); |
|
257 return this; |
|
258 }, |
|
259 |
|
260 up: function () |
|
261 { |
|
262 this.last_node.pop(); |
|
263 return this; |
|
264 }, |
|
265 |
|
266 toString: function (top_tag_only) |
|
267 { |
|
268 var buf = []; |
|
269 buf.push("<" + this.name); |
|
270 for(var attr in this.attr) |
|
271 { |
|
272 buf.push(" " + attr + "='" + xmlescape(this.attr[attr]) + "'"); |
|
273 } |
|
274 |
|
275 // Now add children if wanted |
|
276 if(top_tag_only) |
|
277 { |
|
278 buf.push(">"); |
|
279 } |
|
280 else if(this.children.length == 0) |
|
281 { |
|
282 buf.push("/>"); |
|
283 } |
|
284 else |
|
285 { |
|
286 buf.push(">"); |
|
287 for(var i = 0; i<this.children.length; i++) |
|
288 { |
|
289 var child = this.children[i]; |
|
290 if(typeof(child) == "string") |
|
291 buf.push(xmlescape(child)); |
|
292 else |
|
293 buf.push(child.toString()); |
|
294 } |
|
295 buf.push("</" + this.name + ">"); |
|
296 } |
|
297 return buf.join(""); |
|
298 } |
|
299 } |
|
300 |
|
301 xmpp.stanza = function (name, attr) |
|
302 { |
|
303 return new xmpp.StanzaBuilder(name, attr); |
|
304 } |
|
305 |
|
306 xmpp.message = function (attr) |
|
307 { |
|
308 return xmpp.stanza("message", attr); |
|
309 } |
|
310 |
|
311 xmpp.presence = function (attr) |
|
312 { |
|
313 return xmpp.stanza("presence", attr); |
|
314 } |
|
315 |
|
316 xmpp.iq = function (attr) |
|
317 { |
|
318 return xmpp.stanza("iq", attr); |
|
319 } |