comparison lightstring.js @ 63:20da4fb67977

Auth PLAIN as plugin. Several fixes.
author Sonny Piers <sonny.piers@gmail.com>
date Wed, 01 Feb 2012 19:24:41 +0100
parents b1e75cdbb0ad
children d9f5ae0b6d98
comparison
equal deleted inserted replaced
62:b1e75cdbb0ad 63:20da4fb67977
124 Lightstring.Connection = function(aService) { 124 Lightstring.Connection = function(aService) {
125 if (aService) 125 if (aService)
126 this.service = aService; 126 this.service = aService;
127 this.handlers = {}; 127 this.handlers = {};
128 this.callbacks = {}; 128 this.callbacks = {};
129 this.on('stream:features', function(stanza) {
130 var nodes = stanza.DOM.querySelectorAll('mechanism');
131 //SASL/Auth features
132 if (nodes.length > 0) {
133 this.emit('mechanisms', stanza);
134 var mechanisms = {};
135 for (var i = 0; i < nodes.length; i++)
136 mechanisms[nodes[i].textContent] = true;
137
138
139 //FIXME support SCRAM-SHA1 && allow specify method preferences
140 if ('DIGEST-MD5' in mechanisms)
141 this.send(
142 "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'" +
143 " mechanism='DIGEST-MD5'/>"
144 );
145 else if ('PLAIN' in mechanisms) {
146 var token = btoa(
147 this.jid +
148 '\u0000' +
149 this.jid.node +
150 '\u0000' +
151 this.password
152 );
153 this.send(
154 "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'" +
155 " mechanism='PLAIN'>" + token + "</auth>"
156 );
157 }
158 }
159 //XMPP features
160 else {
161 this.emit('features', stanza);
162 var that = this;
163 //Bind http://xmpp.org/rfcs/rfc3920.html#bind
164 var bind =
165 "<iq type='set' id='"+Lightstring.newId('sendiq:')+"' xmlns='jabber:client'>" +
166 "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>" +
167 (this.jid.resource? "<resource>" + this.jid.resource + "</resource>": "") +
168 "</bind>" +
169 "</iq>";
170 this.send(
171 bind,
172 //Success
173 function(stanza) {
174 //Session http://xmpp.org/rfcs/rfc3921.html#session
175 this.jid = new Lightstring.JID(stanza.DOM.textContent);
176 that.send(
177 "<iq type='set' id='"+Lightstring.newId('sendiq:')+"' xmlns='jabber:client'>" +
178 "<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>" +
179 "</iq>",
180 function() {
181 that.emit('connected');
182 }
183 );
184 },
185 //Error
186 function(stanza) {
187 //TODO: Error?
188 }
189 );
190 }
191 });
192 this.on('success', function(stanza) {
193 this.send(
194 "<stream:stream to='" + this.jid.domain + "'" +
195 " xmlns='jabber:client'" +
196 " xmlns:stream='http://etherx.jabber.org/streams'" +
197 " version='1.0'/>"
198 );
199 });
200 this.on('failure', function(stanza) {
201 this.emit('conn-error', stanza.DOM.firstChild.tagName);
202 });
203 this.on('challenge', function(stanza) {
204 //FIXME this is mostly Strophe code
205
206 function _quote(str) {
207 return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
208 };
209
210 var challenge = atob(stanza.DOM.textContent);
211
212 var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/;
213
214 var cnonce = MD5.hexdigest(Math.random() * 1234567890);
215 var realm = '';
216 var host = null;
217 var nonce = '';
218 var qop = '';
219 var matches;
220
221 while (challenge.match(attribMatch)) {
222 matches = challenge.match(attribMatch);
223 challenge = challenge.replace(matches[0], '');
224 matches[2] = matches[2].replace(/^"(.+)"$/, '$1');
225 switch (matches[1]) {
226 case 'realm':
227 realm = matches[2];
228 break;
229 case 'nonce':
230 nonce = matches[2];
231 break;
232 case 'qop':
233 qop = matches[2];
234 break;
235 case 'host':
236 host = matches[2];
237 break;
238 }
239 }
240
241 var digest_uri = 'xmpp/' + this.jid.domain;
242 if (host !== null)
243 digest_uri = digest_uri + '/' + host;
244 var A1 = MD5.hash(this.jid.node +
245 ':' + realm + ':' + this.password) +
246 ':' + nonce + ':' + cnonce;
247 var A2 = 'AUTHENTICATE:' + digest_uri;
248
249 var responseText = '';
250 responseText += 'username=' + _quote(this.jid.node) + ',';
251 responseText += 'realm=' + _quote(realm) + ',';
252 responseText += 'nonce=' + _quote(nonce) + ',';
253 responseText += 'cnonce=' + _quote(cnonce) + ',';
254 responseText += 'nc="00000001",';
255 responseText += 'qop="auth",';
256 responseText += 'digest-uri=' + _quote(digest_uri) + ',';
257 responseText += 'response=' + _quote(
258 MD5.hexdigest(MD5.hexdigest(A1) + ':' +
259 nonce + ':00000001:' +
260 cnonce + ':auth:' +
261 MD5.hexdigest(A2))) + ',';
262 responseText += 'charset="utf-8"';
263 this.send(
264 "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" +
265 btoa(responseText) +
266 "</response>");
267 });
268 }; 129 };
269 Lightstring.Connection.prototype = { 130 Lightstring.Connection.prototype = {
270 /** 131 /**
271 * @function Create and open a websocket then go though the XMPP authentification process. 132 * @function Create and open a websocket then go though the XMPP authentification process.
272 * @param {String} [aJid] The JID (Jabber id) to use. 133 * @param {String} [aJid] The JID (Jabber id) to use.
317 this.socket.addEventListener('message', function(e) { 178 this.socket.addEventListener('message', function(e) {
318 var stanza = new Lightstring.Stanza(e.data); 179 var stanza = new Lightstring.Stanza(e.data);
319 180
320 //TODO node-xmpp-bosh sends a self-closing stream:stream tag; it is wrong! 181 //TODO node-xmpp-bosh sends a self-closing stream:stream tag; it is wrong!
321 that.emit('input', stanza); 182 that.emit('input', stanza);
322 183
323 if(!stanza.DOM) 184 if(!stanza.DOM)
324 return; 185 return;
325 186
326 that.emit(stanza.DOM.tagName, stanza); 187
327 188 var name = stanza.DOM.localName;
328 if (stanza.DOM.tagName === 'iq') { 189 if (name === 'features') {
190 //SASL mechanisms
191 if (stanza.DOM.firstChild.localName === 'mechanisms') {
192 stanza.mechanisms = [];
193 var nodes = stanza.DOM.querySelectorAll('mechanism');
194 for (var i = 0; i < nodes.length; i++)
195 stanza.mechanisms.push(nodes[i].textContent);
196 that.emit('mechanisms', stanza);
197 }
198 //XMPP features
199 else if (stanza.DOM.firstChild.localName === 'c') {
200 //TODO: stanza.features
201 that.emit('features', stanza);
202 }
203 }
204 else if (name === 'challenge') {
205
206
207 }
208 else if (name === 'response') {
209
210
211 }
212 else if (name === 'success') {
213 that.emit('success', stanza);
214 }
215 else if(name === 'stream') {
216
217
218 }
219
220
221
222 //Iq callbacks
223 else if (name === 'iq') {
329 var payload = stanza.DOM.firstChild; 224 var payload = stanza.DOM.firstChild;
330 if (payload) 225 if (payload)
331 that.emit('iq/' + payload.namespaceURI + ':' + payload.localName, stanza); 226 that.emit('iq/' + payload.namespaceURI + ':' + payload.localName, stanza);
332 227
333 var id = stanza.DOM.getAttributeNS(null, 'id'); 228 var id = stanza.DOM.getAttributeNS(null, 'id');
343 callback.success(stanza); 238 callback.success(stanza);
344 else if (type === 'error' && callback.error) 239 else if (type === 'error' && callback.error)
345 callback.error(stanza); 240 callback.error(stanza);
346 241
347 delete that.callbacks[id]; 242 delete that.callbacks[id];
348
349 //TODO: really needed?
350 } else if (stanza.DOM.tagName === 'message') {
351 var payloads = stanza.DOM.children;
352 for (var i = 0; i < payloads.length; i++)
353 that.emit('message/' + payloads[i].namespaceURI + ':' + payloads[i].localName, stanza);
354 } 243 }
355 }); 244 });
356 }, 245 },
357 /** 246 /**
358 * @function Send a message. 247 * @function Send a message.
422 for (var handler in plugin.handlers) 311 for (var handler in plugin.handlers)
423 this.on(handler, plugin.handlers[handler]); 312 this.on(handler, plugin.handlers[handler]);
424 313
425 //Methods 314 //Methods
426 this[name] = {}; 315 this[name] = {};
427 for (var method in plugins.methods) 316 for (var method in plugin.methods)
428 this[name][method].bind(this); 317 this[name][method].bind(this);
429 318
430 if (plugin.init) 319 if (plugin.init)
431 plugin.init(); 320 plugin.init();
432 } 321 }
439 emit: function(aName, aData) { 328 emit: function(aName, aData) {
440 var handlers = this.handlers[aName]; 329 var handlers = this.handlers[aName];
441 if (!handlers) 330 if (!handlers)
442 return; 331 return;
443 332
444 if (aData.localName !== 'iq') { 333 if (aData && aData.DOM && aData.DOM.localName !== 'iq') {
445 for (var i = 0; i < handlers.length; i++) 334 for (var i = 0; i < handlers.length; i++)
446 handlers[i].call(this, aData); 335 handlers[i].call(this, aData);
447 336
448 return; 337 return;
449 } 338 }