Mercurial > eldonilo > lightstring
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 } |
