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 };