Mercurial > eldonilo > lightstring
comparison lightstring.js @ 13:9aeb0750b9d1
fix an error with the stream builder
author | Sonny Piers <sonny.piers@gmail.com> |
---|---|
date | Sun, 15 Jan 2012 15:23:51 +0100 |
parents | 9fbd0e3678b5 |
children | 6707f450549e |
comparison
equal
deleted
inserted
replaced
12:9fbd0e3678b5 | 13:9aeb0750b9d1 |
---|---|
21 * @namespace No code from lightstring should be callable outside this namespace/scope. | 21 * @namespace No code from lightstring should be callable outside this namespace/scope. |
22 */ | 22 */ |
23 var Lightstring = { | 23 var Lightstring = { |
24 /** | 24 /** |
25 * @namespace Holds XMPP namespaces. | 25 * @namespace Holds XMPP namespaces. |
26 * | 26 */ |
27 */ | 27 NS: { |
28 NS: {}, | 28 stream: 'http://etherx.jabber.org/streams', |
29 jabberClient: 'jabber:client' | |
30 }, | |
29 /** | 31 /** |
30 * @namespace Holds XMPP stanza builders. | 32 * @namespace Holds XMPP stanza builders. |
31 * | 33 */ |
32 */ | 34 stanza: { |
33 stanza: {}, | 35 stream: { |
36 open: function(aService) { | |
37 //FIXME no ending "/" - node-xmpp-bosh bug | |
38 return "<stream:stream to='" + aService + "'\ | |
39 xmlns='" + Lightstring.NS.jabberClient + "'\ | |
40 xmlns:stream='" + Lightstring.NS.stream + "'\ | |
41 version='1.0'/>"; | |
42 }, | |
43 close: function() { | |
44 return '</stream:stream>'; | |
45 } | |
46 } | |
47 }, | |
34 /** | 48 /** |
35 * @private | 49 * @private |
36 */ | 50 */ |
37 parser: new DOMParser(), | 51 parser: new DOMParser(), |
38 /** | 52 /** |
39 * @private | 53 * @private |
40 */ | 54 */ |
41 serializer: new XMLSerializer(), | 55 serializer: new XMLSerializer(), |
42 /** | 56 /** |
43 * @function Transforms a XML string to a DOM object. | 57 * @function Transforms a XML string to a DOM object. |
44 * @param {String} aString XML string | 58 * @param {String} aString XML string. |
45 * @returns {Object} Domified XML. | 59 * @return {Object} Domified XML. |
46 */ | 60 */ |
47 xml2dom: function(aString) { | 61 xml2dom: function(aString) { |
48 return this.parser.parseFromString(aString, 'text/xml').documentElement; | 62 return this.parser.parseFromString(aString, 'text/xml').documentElement; |
49 }, | 63 }, |
50 /** | 64 /** |
51 * @function Transforms a DOM object to a XML string. | 65 * @function Transforms a DOM object to a XML string. |
52 * @param {Object} aString DOM object | 66 * @param {Object} aString DOM object. |
53 * @returns {String} Stringified DOM. | 67 * @return {String} Stringified DOM. |
54 */ | 68 */ |
55 dom2xml: function(aElement) { | 69 dom2xml: function(aElement) { |
56 return this.serializer.serializeToString(aElement); | 70 return this.serializer.serializeToString(aElement); |
57 }, | 71 } |
58 }; | 72 }; |
59 | 73 |
60 /** | 74 /** |
61 * @constructor Creates a new Lightstring connection | 75 * @constructor Creates a new Lightstring connection |
62 * @param {String} [aService] The Websocket service URL. | 76 * @param {String} [aService] The Websocket service URL. |
63 * @memberOf Lightstring | 77 * @memberOf Lightstring |
64 */ | 78 */ |
65 Lightstring.Connection = function (aService) { | 79 Lightstring.Connection = function(aService) { |
66 if(aService) | 80 if (aService) |
67 this.service = aService; | 81 this.service = aService; |
68 this.handlers = {}; | 82 this.handlers = {}; |
69 this.iqid = 1024; | 83 this.iqid = 1024; |
70 this.getNewId = function() { | 84 this.getNewId = function() { |
71 this.iqid++; | 85 this.iqid++; |
72 return 'sendiq:'+this.iqid; | 86 return 'sendiq:' + this.iqid; |
73 }; | 87 }; |
74 /** | 88 /** |
75 * @function Create and open a websocket then go though the XMPP authentification process. | 89 * @function Create and open a websocket then go though the XMPP authentification process. |
76 * @param {String} [aJid] The JID (Jabber id) to use. | 90 * @param {String} [aJid] The JID (Jabber id) to use. |
77 * @param {String} [aPassword] The associated password. | 91 * @param {String} [aPassword] The associated password. |
78 */ | 92 */ |
79 this.connect = function(aJid, aPassword) { | 93 this.connect = function(aJid, aPassword) { |
80 this.emit('connecting'); | 94 this.emit('connecting'); |
81 if(aJid) | 95 if (aJid) |
82 this.jid = aJid; | 96 this.jid = aJid; |
83 if(this.jid) { | 97 if (this.jid) { |
84 this.domain = this.jid.split('@')[1]; | 98 this.host = this.jid.split('@')[1]; |
85 this.node = this.jid.split('@')[0]; | 99 this.node = this.jid.split('@')[0]; |
86 this.resource = this.jid.split('/')[1]; | 100 this.resource = this.jid.split('/')[1]; |
87 } | 101 } |
88 if(aPassword) | 102 if (aPassword) |
89 this.password = aPassword; | 103 this.password = aPassword; |
90 | 104 |
91 if(!this.jid) | 105 if (!this.jid) |
92 throw "Lightstring: Connection.jid is undefined."; | 106 throw 'Lightstring: Connection.jid is undefined.'; |
93 if(!this.password) | 107 if (!this.password) |
94 throw "Lightstring: Connection.password is undefined."; | 108 throw 'Lightstring: Connection.password is undefined.'; |
95 if(!this.service) | 109 if (!this.service) |
96 throw "Lightstring: Connection.service is undefined."; | 110 throw 'Lightstring: Connection.service is undefined.'; |
97 | 111 |
98 //"Bug 695635 - tracking bug: unprefix WebSockets" https://bugzil.la/695635 | 112 //"Bug 695635 - tracking bug: unprefix WebSockets" https://bugzil.la/695635 |
99 try { | 113 try { |
100 this.socket = new WebSocket(this.service, 'xmpp'); | 114 this.socket = new WebSocket(this.service, 'xmpp'); |
101 } | 115 } |
102 catch(error) { | 116 catch (error) { |
103 this.socket = new MozWebSocket(this.service, 'xmpp'); | 117 this.socket = new MozWebSocket(this.service, 'xmpp'); |
104 } | 118 } |
105 | 119 |
106 var that = this; | 120 var that = this; |
107 this.socket.addEventListener('open', function() { | 121 this.socket.addEventListener('open', function() { |
108 if(this.protocol !== 'xmpp') | 122 if (this.protocol !== 'xmpp') |
109 throw "Lightstring: The server located at "+that.service+" is not XMPP aware."; | 123 throw 'Lightstring: The server located at '+ that.service + ' is not XMPP aware.'; |
110 //FIXME no ending "/" - node-xmpp-bosh bug | 124 |
111 var stream = | 125 var stream = Lightstring.stanza.stream.open(that.host); |
112 "<stream:stream to='"+that.domain+"'\ | 126 |
113 xmlns='jabber:client'\ | 127 that.socket.send(stream); |
114 xmlns:stream='http://etherx.jabber.org/streams'\ | |
115 version='1.0'/>"; | |
116 | |
117 that.socket.send(stream) | |
118 that.emit('XMLOutput', stream); | 128 that.emit('XMLOutput', stream); |
119 }); | 129 }); |
120 this.socket.addEventListener('error', function(e) { | 130 this.socket.addEventListener('error', function(e) { |
121 that.emit('error', e.data); | 131 that.emit('error', e.data); |
132 console.log(e.data); | |
122 }); | 133 }); |
123 this.socket.addEventListener('close', function(e) { | 134 this.socket.addEventListener('close', function(e) { |
124 that.emit('disconnected', e.data); | 135 that.emit('disconnected', e.data); |
125 }); | 136 }); |
126 this.socket.addEventListener('message', function(e) { | 137 this.socket.addEventListener('message', function(e) { |
127 that.emit('XMLInput', e.data); | 138 that.emit('XMLInput', e.data); |
128 var elm = Lightstring.xml2dom(e.data); | 139 var elm = Lightstring.xml2dom(e.data); |
129 that.emit('DOMInput', elm); | 140 that.emit('DOMInput', elm); |
130 that.emit(elm.tagName, elm); | 141 that.emit(elm.tagName, elm); |
131 | 142 |
132 if(elm.tagName === 'iq') | 143 if (elm.tagName === 'iq') |
133 that.emit(elm.getAttribute('id'), elm); | 144 that.emit(elm.getAttribute('id'), elm); |
134 }); | 145 }); |
135 }; | 146 }; |
136 /** | 147 /** |
137 * @function Send a message. | 148 * @function Send a message. |
138 * @param {String|Object} aStanza The message to send. | 149 * @param {String|Object} aStanza The message to send. |
139 * @param {Function} [aCallback] A callback that will be called when the answer will answer. | 150 * @param {Function} [aCallback] Executed on answer. (stanza must be iq) |
140 */ | 151 */ |
141 this.send = function(aStanza, aCallback) { | 152 this.send = function(aStanza, aCallback) { |
142 if(typeof aStanza === 'string') { | 153 if (typeof aStanza === 'string') { |
143 var str = aStanza; | 154 var str = aStanza; |
144 var elm = Lightstring.xml2dom(str); | 155 var elm = Lightstring.xml2dom(str); |
145 } | 156 } |
146 else if(aStanza instanceof Element) { | 157 else if (aStanza instanceof Element) { |
147 var elm = aStanza; | 158 var elm = aStanza; |
148 var str = this.dom2xml(elm); | 159 var str = this.dom2xml(elm); |
149 } | 160 } |
150 else { | 161 else { |
151 that.emit('error', 'Unsupported data type.'); | 162 that.emit('error', 'Unsupported data type.'); |
152 } | 163 } |
153 | 164 |
154 | 165 |
155 if(elm.tagName === 'iq') { | 166 if (elm.tagName === 'iq') { |
156 var id = elm.getAttribute('id'); | 167 var id = elm.getAttribute('id'); |
157 if(!id) { | 168 if (!id) { |
158 elm.setAttribute('id', this.getNewId()) | 169 elm.setAttribute('id', this.getNewId()); |
159 str = Lightstring.dom2xml(elm) | 170 str = Lightstring.dom2xml(elm); |
160 } | 171 } |
161 if(aCallback) | 172 if (aCallback) |
162 this.on(elm.getAttribute('id'), aCallback); | 173 this.on(elm.getAttribute('id'), aCallback); |
163 } | 174 } |
164 else if(aCallback) { | 175 else if (aCallback) { |
165 that.emit('warning', 'Callback can\'t be called with non-iq stanza.'); | 176 that.emit('warning', 'Callback can\'t be called with non-iq stanza.'); |
166 } | 177 } |
167 | 178 |
168 | 179 |
169 this.socket.send(str); | 180 this.socket.send(str); |
170 this.emit('XMLOutput', str); | 181 this.emit('XMLOutput', str); |
171 this.emit('DOMOutput', elm); | 182 this.emit('DOMOutput', elm); |
172 }; | 183 }; |
173 /** | 184 /** |
174 * @function Close the XMPP stream and the socket. | 185 * @function Closes the XMPP stream and the socket. |
175 */ | 186 */ |
176 this.disconnect = function() { | 187 this.disconnect = function() { |
177 this.emit('disconnecting'); | 188 this.emit('disconnecting'); |
178 this.send('</stream:stream>'); | 189 var stream = Lighstring.stanza.stream.close(); |
190 this.send(stream); | |
191 that.emit('XMLOutput', stream); | |
179 this.socket.close(); | 192 this.socket.close(); |
180 }; | 193 }; |
181 /** | 194 /** |
182 * @function Emit an event. | 195 * @function Emits an event. |
183 * @param {String} aName The event name. | 196 * @param {String} aName The event name. |
184 * @param {Function|Array|Object} [aData] Data about the event. | 197 * @param {Function|Array|Object} [aData] Data about the event. |
185 */ | 198 */ |
186 this.emit = function(aName, aData) { | 199 this.emit = function(aName, aData) { |
187 var handlers = this.handlers[aName]; | 200 var handlers = this.handlers[aName]; |
188 if(!handlers) | 201 if (!handlers) |
189 return; | 202 return; |
190 | 203 |
191 //FIXME Better idea than passing the context as argument? | 204 //FIXME Better idea than passing the context as argument? |
192 for(var i=0; i<handlers.length; i++) | 205 for (var i = 0; i < handlers.length; i++) |
193 handlers[i](aData, this); | 206 handlers[i](aData, this); |
194 | 207 |
195 if(aName.match('sendiq:')) | 208 if (aName.match('sendiq:')) |
196 delete this.handlers[aName]; | 209 delete this.handlers[aName]; |
197 }; | 210 }; |
198 /** | 211 /** |
199 * @function Register an event handler. | 212 * @function Register an event handler. |
200 * @param {String} aName The event name. | 213 * @param {String} aName The event name. |
201 * @param {Function} aCallback The callback to call when the event is emitted. | 214 * @param {Function} aCallback The callback to call when the event is emitted. |
202 */ | 215 */ |
203 this.on = function(aName, callback) { | 216 this.on = function(aName, callback) { |
204 if(!this.handlers[aName]) | 217 if (!this.handlers[aName]) |
205 this.handlers[aName] = []; | 218 this.handlers[aName] = []; |
206 this.handlers[aName].push(callback); | 219 this.handlers[aName].push(callback); |
207 }; | 220 }; |
208 //FIXME do this! | 221 //FIXME do this! |
209 //~ this.once = function(name, callback) { | 222 //~ this.once = function(name, callback) { |
212 //~ this.handlers[name].push(callback); | 225 //~ this.handlers[name].push(callback); |
213 //~ }; | 226 //~ }; |
214 this.on('stream:features', function(stanza, that) { | 227 this.on('stream:features', function(stanza, that) { |
215 var nodes = stanza.querySelectorAll('mechanism'); | 228 var nodes = stanza.querySelectorAll('mechanism'); |
216 //SASL/Auth features | 229 //SASL/Auth features |
217 if(nodes.length > 0) { | 230 if (nodes.length > 0) { |
218 that.emit('mechanisms', stanza); | 231 that.emit('mechanisms', stanza); |
219 var mechanisms = {}; | 232 var mechanisms = {}; |
220 for(var i=0; i<nodes.length; i++) | 233 for (var i = 0; i < nodes.length; i++) |
221 mechanisms[nodes[i].textContent] = true; | 234 mechanisms[nodes[i].textContent] = true; |
222 | 235 |
223 | 236 |
224 //FIXME support SCRAM-SHA1 && allow specify method preferences | 237 //FIXME support SCRAM-SHA1 && allow specify method preferences |
225 if('DIGEST-MD5' in mechanisms) | 238 if ('DIGEST-MD5' in mechanisms) |
226 that.send( | 239 that.send( |
227 "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'\ | 240 "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'\ |
228 mechanism='DIGEST-MD5'/>" | 241 mechanism='DIGEST-MD5'/>" |
229 ); | 242 ); |
230 else if('PLAIN' in mechanisms) { | 243 else if ('PLAIN' in mechanisms) { |
231 var token = btoa( | 244 var token = btoa( |
232 that.jid + | 245 that.jid + |
233 "\u0000" + | 246 '\u0000' + |
234 that.jid.node + | 247 that.jid.node + |
235 "\u0000" + | 248 '\u0000' + |
236 that.password | 249 that.password |
237 ); | 250 ); |
238 that.send( | 251 that.send( |
239 "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'\ | 252 "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'\ |
240 mechanism='PLAIN'>"+token+"</auth>" | 253 mechanism='PLAIN'>" + token + '</auth>' |
241 ); | 254 ); |
242 } | 255 } |
243 } | 256 } |
244 //XMPP features | 257 //XMPP features |
245 else { | 258 else { |
262 }); | 275 }); |
263 } | 276 } |
264 }); | 277 }); |
265 this.on('success', function(stanza, that) { | 278 this.on('success', function(stanza, that) { |
266 that.send( | 279 that.send( |
267 "<stream:stream to='"+that.domain+"'\ | 280 "<stream:stream to='" + that.domain + "'\ |
268 xmlns='jabber:client'\ | 281 xmlns='jabber:client'\ |
269 xmlns:stream='http://etherx.jabber.org/streams'\ | 282 xmlns:stream='http://etherx.jabber.org/streams'\ |
270 version='1.0' />" | 283 version='1.0' />" |
271 ); | 284 ); |
272 }); | 285 }); |
273 this.on('failure', function(stanza, that) { | 286 this.on('failure', function(stanza, that) { |
274 that.emit('conn-error', stanza.firstChild.tagName); | 287 that.emit('conn-error', stanza.firstChild.tagName); |
275 }); | 288 }); |
276 this.on('challenge', function(stanza, that) { | 289 this.on('challenge', function(stanza, that) { |
277 //FIXME this is mostly Strophe code | 290 //FIXME this is mostly Strophe code |
278 | 291 |
279 function _quote(str) { | 292 function _quote(str) { |
280 return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; | 293 return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; |
281 }; | 294 }; |
282 | 295 |
283 var challenge = atob(stanza.textContent); | 296 var challenge = atob(stanza.textContent); |
284 | 297 |
285 var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; | 298 var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; |
286 | 299 |
287 var cnonce = MD5.hexdigest(Math.random() * 1234567890); | 300 var cnonce = MD5.hexdigest(Math.random() * 1234567890); |
288 var realm = ""; | 301 var realm = ''; |
289 var host = null; | 302 var host = null; |
290 var nonce = ""; | 303 var nonce = ''; |
291 var qop = ""; | 304 var qop = ''; |
292 var matches; | 305 var matches; |
293 | 306 |
294 while (challenge.match(attribMatch)) { | 307 while (challenge.match(attribMatch)) { |
295 matches = challenge.match(attribMatch); | 308 matches = challenge.match(attribMatch); |
296 challenge = challenge.replace(matches[0], ""); | 309 challenge = challenge.replace(matches[0], ''); |
297 matches[2] = matches[2].replace(/^"(.+)"$/, "$1"); | 310 matches[2] = matches[2].replace(/^"(.+)"$/, '$1'); |
298 switch (matches[1]) { | 311 switch (matches[1]) { |
299 case "realm": | 312 case 'realm': |
300 realm = matches[2]; | 313 realm = matches[2]; |
301 break; | 314 break; |
302 case "nonce": | 315 case 'nonce': |
303 nonce = matches[2]; | 316 nonce = matches[2]; |
304 break; | 317 break; |
305 case "qop": | 318 case 'qop': |
306 qop = matches[2]; | 319 qop = matches[2]; |
307 break; | 320 break; |
308 case "host": | 321 case 'host': |
309 host = matches[2]; | 322 host = matches[2]; |
310 break; | 323 break; |
311 } | 324 } |
312 } | 325 } |
313 | 326 |
314 var digest_uri = "xmpp/" + that.domain; | 327 var digest_uri = 'xmpp/' + that.domain; |
315 if (host !== null) { | 328 if (host !== null) { |
316 digest_uri = digest_uri + "/" + host; | 329 digest_uri = digest_uri + '/' + host; |
317 } | 330 } |
318 var A1 = MD5.hash(that.node + | 331 var A1 = MD5.hash(that.node + |
319 ":" + realm + ":" + that.password) + | 332 ':' + realm + ':' + that.password) + |
320 ":" + nonce + ":" + cnonce; | 333 ':' + nonce + ':' + cnonce; |
321 var A2 = 'AUTHENTICATE:' + digest_uri; | 334 var A2 = 'AUTHENTICATE:' + digest_uri; |
322 | 335 |
323 var responseText = ""; | 336 var responseText = ''; |
324 responseText += 'username=' + _quote(that.node) + ','; | 337 responseText += 'username=' + _quote(that.node) + ','; |
325 responseText += 'realm=' + _quote(realm) + ','; | 338 responseText += 'realm=' + _quote(realm) + ','; |
326 responseText += 'nonce=' + _quote(nonce) + ','; | 339 responseText += 'nonce=' + _quote(nonce) + ','; |
327 responseText += 'cnonce=' + _quote(cnonce) + ','; | 340 responseText += 'cnonce=' + _quote(cnonce) + ','; |
328 responseText += 'nc="00000001",'; | 341 responseText += 'nc="00000001",'; |
329 responseText += 'qop="auth",'; | 342 responseText += 'qop="auth",'; |
330 responseText += 'digest-uri=' + _quote(digest_uri) + ','; | 343 responseText += 'digest-uri=' + _quote(digest_uri) + ','; |
331 responseText += 'response=' + _quote( | 344 responseText += 'response=' + _quote( |
332 MD5.hexdigest(MD5.hexdigest(A1) + ":" + | 345 MD5.hexdigest(MD5.hexdigest(A1) + ':' + |
333 nonce + ":00000001:" + | 346 nonce + ':00000001:' + |
334 cnonce + ":auth:" + | 347 cnonce + ':auth:' + |
335 MD5.hexdigest(A2))) + ','; | 348 MD5.hexdigest(A2))) + ','; |
336 responseText += 'charset="utf-8"'; | 349 responseText += 'charset="utf-8"'; |
337 that.send( | 350 that.send( |
338 "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" | 351 "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" |
339 +btoa(responseText)+ | 352 + btoa(responseText) + |
340 "</response>"); | 353 '</response>'); |
341 }); | 354 }); |
342 }; | 355 }; |