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