1/* 2This library is free software; you can redistribute it and/or modify it 3 under the terms of the GNU Lesser General Public License as published 4 by the Free Software Foundation; either version 2.1 of the License, or 5 (at your option) any later version. 6 . 7 This library is distributed in the hope that it will be useful, but 8 WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 10 General Public License for more details. 11 12 Copyright (c) dodo <dodo@blacksec.org>, 2011 13 14*/ 15 16/** 17* File: strophe.register.js 18* A Strophe plugin for XMPP In-Band Registration. 19*/ 20Strophe.addConnectionPlugin('register', { 21 _connection: null, 22 23 //The plugin must have the init function. 24 init: function(conn) { 25 this._connection = conn; 26 27 // compute free emun index number 28 var i = 0; 29 Object.keys(Strophe.Status).forEach(function (key) { 30 i = Math.max(i, Strophe.Status[key]); 31 }); 32 33 /* extend name space 34 * NS.REGISTER - In-Band Registration 35 * from XEP 77. 36 */ 37 Strophe.addNamespace('REGISTER', 'jabber:iq:register'); 38 Strophe.Status.REGISTERING = i + 1; 39 Strophe.Status.REGIFAIL = i + 2; 40 Strophe.Status.REGISTER = i + 3; 41 Strophe.Status.SUBMITTING = i + 4; 42 Strophe.Status.SBMTFAIL = i + 5; 43 Strophe.Status.REGISTERED = i + 6; 44 45 if (conn.disco) 46 conn.disco.addFeature(Strophe.NS.REGISTER); 47 48 // hooking strophe's connection.reset 49 var self = this, reset = conn.reset; 50 conn.reset = function () { 51 reset(); 52 self.instructions = ""; 53 self.fields = {}; 54 self.authentication = {}; 55 self.registered = false; 56 self.enabled = null; 57 }; 58 }, 59 60 /** Function: connect 61 * Starts the registration process. 62 * 63 * As the registration process proceeds, the user supplied callback will 64 * be triggered multiple times with status updates. The callback 65 * should take two arguments - the status code and the error condition. 66 * 67 * The status code will be one of the values in the Strophe.Status 68 * constants. The error condition will be one of the conditions 69 * defined in RFC 3920 or the condition 'strophe-parsererror'. 70 * 71 * Please see XEP 77 for a more detailed explanation of the optional 72 * parameters below. 73 * 74 * Parameters: 75 * (String) domain - The xmpp server's Domain. This will be the server, 76 * which will be contacted to register a new JID. 77 * The server has to provide and allow In-Band Registration (XEP-0077). 78 * (Function) callback The connect callback function. 79 * (Integer) wait - The optional HTTPBIND wait value. This is the 80 * time the server will wait before returning an empty result for 81 * a request. The default setting of 60 seconds is recommended. 82 * Other settings will require tweaks to the Strophe.TIMEOUT value. 83 * (Integer) hold - The optional HTTPBIND hold value. This is the 84 * number of connections the server will hold at one time. This 85 * should almost always be set to 1 (the default). 86 */ 87 connect: function (domain, callback, wait, hold) { 88 var that = this._connection; 89 this.instructions = ""; 90 this.fields = {}; 91 this.authentication = {}; 92 this.registered = false; 93 this.enabled = false; 94 95 that.connect_callback = callback; 96 that.connected = false; 97 that.authenticated = false; 98 that.disconnecting = false; 99 that.errors = 0; 100 101 that.domain = domain || that.domain; 102 that.wait = wait || that.wait; 103 that.hold = hold || that.hold; 104 105 // build the body tag 106 var body = that._buildBody().attrs({ 107 to: that.domain, 108 "xml:lang": "en", 109 wait: that.wait, 110 hold: that.hold, 111 content: "text/xml; charset=utf-8", 112 ver: "1.6", 113 "xmpp:version": "1.0", 114 "xmlns:xmpp": Strophe.NS.BOSH 115 }); 116 117 that._changeConnectStatus(Strophe.Status.CONNECTING, null); 118 119 that._requests.push( 120 new Strophe.Request(body.tree(), 121 that._onRequestStateChange.bind( 122 that, this._register_cb.bind(this)), 123 body.tree().getAttribute("rid"))); 124 that._throttledRequestHandler(); 125 }, 126 127 /** PrivateFunction: _register_cb 128 * _Private_ handler for initial registration request. 129 * 130 * This handler is used to process the initial registration request 131 * response from the BOSH server. It is used to set up a bosh session 132 * and requesting registration fields from host. 133 * 134 * Parameters: 135 * (Strophe.Request) req - The current request. 136 */ 137 _register_cb: function (req) { 138 var that = this._connection; 139 140 Strophe.info("_register_cb was called"); 141 that.connected = true; 142 143 var bodyWrap = req.getResponse(); 144 if (!bodyWrap) { return; } 145 146 if (that.xmlInput !== Strophe.Connection.prototype.xmlInput) { 147 that.xmlInput(bodyWrap); 148 } 149 if (that.rawInput !== Strophe.Connection.prototype.rawInput) { 150 that.rawInput(Strophe.serialize(bodyWrap)); 151 } 152 153 var typ = bodyWrap.getAttribute("type"); 154 var cond, conflict; 155 if (typ !== null && typ == "terminate") { 156 // an error occurred 157 cond = bodyWrap.getAttribute("condition"); 158 conflict = bodyWrap.getElementsByTagName("conflict"); 159 if (cond !== null) { 160 if (cond == "remote-stream-error" && conflict.length > 0) { 161 cond = "conflict"; 162 } 163 that._changeConnectStatus(Strophe.Status.CONNFAIL, cond); 164 } else { 165 that._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); 166 } 167 return; 168 } 169 170 // check to make sure we don't overwrite these if _connect_cb is 171 // called multiple times in the case of missing stream:features 172 if (!that.sid) { 173 that.sid = bodyWrap.getAttribute("sid"); 174 } 175 if (!that.stream_id) { 176 that.stream_id = bodyWrap.getAttribute("authid"); 177 } 178 179 var wind = bodyWrap.getAttribute('requests'); 180 if (wind) { that.window = parseInt(wind, 10); } 181 var hold = bodyWrap.getAttribute('hold'); 182 if (hold) { that.hold = parseInt(hold, 10); } 183 var wait = bodyWrap.getAttribute('wait'); 184 if (wait) { that.wait = parseInt(wait, 10); } 185 186 187 var register, mechanisms; 188 register = bodyWrap.getElementsByTagName("register"); 189 mechanisms = bodyWrap.getElementsByTagName("mechanism"); 190 if (register.length === 0 && mechanisms.length === 0) { 191 // we didn't get stream:features yet, so we need wait for it 192 // by sending a blank poll request 193 var body = that._buildBody(); 194 that._requests.push( 195 new Strophe.Request(body.tree(), 196 that._onRequestStateChange.bind( 197 that, this._register_cb.bind(this)), 198 body.tree().getAttribute("rid"))); 199 that._throttledRequestHandler(); 200 return; 201 } 202 203 var i, mech; 204 for (i = 0; i < mechanisms.length; i++) { 205 mech = Strophe.getText(mechanisms[i]); 206 if (mech == 'DIGEST-MD5') { 207 this.authentication.sasl_digest_md5 = true; 208 } else if (mech == 'PLAIN') { 209 this.authentication.sasl_plain = true; 210 } else if (mech == 'ANONYMOUS') { 211 this.authentication.sasl_anonymous = true; 212 } 213 } 214 215 if (register.length === 0) { 216 that._changeConnectStatus(Strophe.Status.REGIFAIL, null); 217 return; 218 } else this.enabled = true; 219 220 // send a get request for registration, to get all required data fields 221 that._changeConnectStatus(Strophe.Status.REGISTERING, null); 222 that._addSysHandler(this._get_register_cb.bind(this), 223 null, "iq", null, null); 224 that.send($iq({type: "get"}).c("query", 225 {xmlns: Strophe.NS.REGISTER}).tree()); 226 }, 227 228 /** PrivateFunction: _get_register_cb 229 * _Private_ handler for Registration Fields Request. 230 * 231 * Parameters: 232 * (XMLElement) elem - The query stanza. 233 * 234 * Returns: 235 * false to remove the handler. 236 */ 237 _get_register_cb: function (stanza) { 238 var i, query, field, that = this._connection; 239 query = stanza.getElementsByTagName("query"); 240 241 if (query.length !== 1) { 242 that._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown"); 243 return false; 244 } 245 query = query[0]; 246 // get required fields 247 for (i = 0; i < query.childNodes.length; i++) { 248 field = query.childNodes[i]; 249 if (field.tagName.toLowerCase() === 'instructions') { 250 // this is a special element 251 // it provides info about given data fields in a textual way. 252 this.instructions = Strophe.getText(field); 253 continue; 254 } else if (field.tagName.toLowerCase() === 'x') { 255 // ignore x for now 256 continue; 257 } 258 this.fields[field.tagName.toLowerCase()] = Strophe.getText(field); 259 } 260 that._changeConnectStatus(Strophe.Status.REGISTER, null); 261 return false; 262 }, 263 264 /** Function: submit 265 * Submits Registration data. 266 * 267 * As the registration process proceeds, the user supplied callback will 268 * be triggered with status code Strophe.Status.REGISTER. At this point 269 * the user should fill all required fields in connection.register.fields 270 * and invoke this function to procceed in the registration process. 271 */ 272 submit: function () { 273 var i, name, query, fields, that = this._connection; 274 query = $iq({type: "set"}).c("query", {xmlns:Strophe.NS.REGISTER}); 275 276 // set required fields 277 fields = Object.keys(this.fields); 278 for (i = 0; i < fields.length; i++) { 279 name = fields[i]; 280 query.c(name).t(this.fields[name]).up(); 281 } 282 283 // providing required information 284 that._changeConnectStatus(Strophe.Status.SUBMITTING, null); 285 that._addSysHandler(this._submit_cb.bind(this), 286 null, "iq", null, null); 287 that.send(query); 288 }, 289 290 /** PrivateFunction: _submit_cb 291 * _Private_ handler for submitted registration information. 292 * 293 * Parameters: 294 * (XMLElement) elem - The query stanza. 295 * 296 * Returns: 297 * false to remove the handler. 298 */ 299 _submit_cb: function (stanza) { 300 var i, query, field, error = null, that = this._connection; 301 302 query = stanza.getElementsByTagName("query"); 303 if (query.length > 0) { 304 query = query[0]; 305 // update fields 306 for (i = 0; i < query.childNodes.length; i++) { 307 field = query.childNodes[i]; 308 if (field.tagName.toLowerCase() === 'instructions') { 309 // this is a special element 310 // it provides info about given data fields in a textual way 311 this.instructions = Strophe.getText(field); 312 continue; 313 } 314 this.fields[field.tagName.toLowerCase()]=Strophe.getText(field); 315 } 316 } 317 318 if (stanza.getAttribute("type") === "error") { 319 error = stanza.getElementsByTagName("error"); 320 if (error.length !== 1) { 321 that._changeConnectStatus(Strophe.Status.SBMTFAIL, "unknown"); 322 return false; 323 } 324 // this is either 'conflict' or 'not-acceptable' 325 error = error[0].firstChild.tagName.toLowerCase(); 326 if (error === 'conflict') 327 // already registered 328 this.registered = true; 329 } else 330 this.registered = true; 331 332 if (this.registered) { 333 that.jid = this.fields.username + "@" + that.domain; 334 that.pass = this.fields.password; 335 } 336 337 if (error === null) { 338 Strophe.info("Registered successful."); 339 that._changeConnectStatus(Strophe.Status.REGISTERED, null); 340 } else { 341 Strophe.info("Registration failed."); 342 that._changeConnectStatus(Strophe.Status.SBMTFAIL, error); 343 } 344 return false; 345 }, 346 347 /** Function: authenticate 348 * Login with newly registered account. 349 * 350 * This is just a helper function to authenticate with the new account of 351 * the successful registration. This is recommended to do in the 352 * user supplied callback when receiving Strophe.Status.REGISTERED. 353 * It is also possible to do it on Strophe.Status.SBMTFAIL when 354 * connection.register.registered is true under the circumstances that an 355 * already existing account with the appendant password was supplied. 356 */ 357 authenticate: function () { 358 var auth_str, hashed_auth_str, that = this._connection; 359 360 if (Strophe.getNodeFromJid(that.jid) === null && 361 this.authentication.sasl_anonymous) { 362 that._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); 363 that._sasl_success_handler = that._addSysHandler( 364 that._sasl_success_cb.bind(that), null, 365 "success", null, null); 366 that._sasl_failure_handler = that._addSysHandler( 367 that._sasl_failure_cb.bind(that), null, 368 "failure", null, null); 369 370 that.send($build("auth", { 371 xmlns: Strophe.NS.SASL, 372 mechanism: "ANONYMOUS" 373 }).tree()); 374 } else if (Strophe.getNodeFromJid(that.jid) === null) { 375 // we don't have a node, which is required for non-anonymous 376 // client connections 377 that._changeConnectStatus(Strophe.Status.CONNFAIL, 378 'x-strophe-bad-non-anon-jid'); 379 that.disconnect(); 380 } else if (this.authentication.sasl_digest_md5) { 381 that._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); 382 that._sasl_challenge_handler = that._addSysHandler( 383 that._sasl_challenge1_cb.bind(that), null, 384 "challenge", null, null); 385 that._sasl_failure_handler = that._addSysHandler( 386 that._sasl_failure_cb.bind(that), null, 387 "failure", null, null); 388 389 that.send($build("auth", { 390 xmlns: Strophe.NS.SASL, 391 mechanism: "DIGEST-MD5" 392 }).tree()); 393 } else if (this.authentication.sasl_plain) { 394 // Build the plain auth string (barejid null 395 // username null password) and base 64 encoded. 396 auth_str = Strophe.getBareJidFromJid(that.jid); 397 auth_str = auth_str + "\u0000"; 398 auth_str = auth_str + Strophe.getNodeFromJid(that.jid); 399 auth_str = auth_str + "\u0000"; 400 auth_str = auth_str + that.pass; 401 402 that._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); 403 that._sasl_success_handler = that._addSysHandler( 404 that._sasl_success_cb.bind(that), null, 405 "success", null, null); 406 that._sasl_failure_handler = that._addSysHandler( 407 that._sasl_failure_cb.bind(that), null, 408 "failure", null, null); 409 410 hashed_auth_str = Base64.encode(auth_str); 411 that.send($build("auth", { 412 xmlns: Strophe.NS.SASL, 413 mechanism: "PLAIN" 414 }).t(hashed_auth_str).tree()); 415 } else { 416 that._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); 417 that._addSysHandler(that._auth1_cb.bind(that), null, null, 418 null, "_auth_1"); 419 420 that.send($iq({ 421 type: "get", 422 to: that.domain, 423 id: "_auth_1" 424 }).c("query", { 425 xmlns: Strophe.NS.AUTH 426 }).c("username", {}).t(Strophe.getNodeFromJid(that.jid)).tree()); 427 } 428 }, 429}); 430