1/* OAuthSimple
2  * A simpler version of OAuth
3  *
4  * author:     jr conlin
5  * mail:       src@anticipatr.com
6  * copyright:  unitedHeroes.net
7  * version:    1.0
8  * url:        http://unitedHeroes.net/OAuthSimple
9  *
10  * Copyright (c) 2009, unitedHeroes.net
11  * All rights reserved.
12  *
13  * Redistribution and use in source and binary forms, with or without
14  * modification, are permitted provided that the following conditions are met:
15  *     * Redistributions of source code must retain the above copyright
16  *       notice, this list of conditions and the following disclaimer.
17  *     * Redistributions in binary form must reproduce the above copyright
18  *       notice, this list of conditions and the following disclaimer in the
19  *       documentation and/or other materials provided with the distribution.
20  *     * Neither the name of the unitedHeroes.net nor the
21  *       names of its contributors may be used to endorse or promote products
22  *       derived from this software without specific prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY UNITEDHEROES.NET ''AS IS'' AND ANY
25  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27  * DISCLAIMED. IN NO EVENT SHALL UNITEDHEROES.NET BE LIABLE FOR ANY
28  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
31  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 */
35var OAuthSimple;
36
37if (OAuthSimple === undefined)
38{
39    /* Simple OAuth
40     *
41     * This class only builds the OAuth elements, it does not do the actual
42     * transmission or reception of the tokens. It does not validate elements
43     * of the token. It is for client use only.
44     *
45     * api_key is the API key, also known as the OAuth consumer key
46     * shared_secret is the shared secret (duh).
47     *
48     * Both the api_key and shared_secret are generally provided by the site
49     * offering OAuth services. You need to specify them at object creation
50     * because nobody <explative>ing uses OAuth without that minimal set of
51     * signatures.
52     *
53     * If you want to use the higher order security that comes from the
54     * OAuth token (sorry, I don't provide the functions to fetch that because
55     * sites aren't horribly consistent about how they offer that), you need to
56     * pass those in either with .setTokensAndSecrets() or as an argument to the
57     * .sign() or .getHeaderString() functions.
58     *
59     * Example:
60       <code>
61        var oauthObject = OAuthSimple().sign({path:'http://example.com/rest/',
62                                              parameters: 'foo=bar&gorp=banana',
63                                              signatures:{
64                                                api_key:'12345abcd',
65                                                shared_secret:'xyz-5309'
66                                             }});
67        document.getElementById('someLink').href=oauthObject.signed_url;
68       </code>
69     *
70     * that will sign as a "GET" using "SHA1-MAC" the url. If you need more than
71     * that, read on, McDuff.
72     */
73
74    /** OAuthSimple creator
75     *
76     * Create an instance of OAuthSimple
77     *
78     * @param api_key {string}       The API Key (sometimes referred to as the consumer key) This value is usually supplied by the site you wish to use.
79     * @param shared_secret (string) The shared secret. This value is also usually provided by the site you wish to use.
80     */
81    OAuthSimple = function (consumer_key,shared_secret)
82    {
83/*        if (api_key == undefined)
84            throw("Missing argument: api_key (oauth_consumer_key) for OAuthSimple. This is usually provided by the hosting site.");
85        if (shared_secret == undefined)
86            throw("Missing argument: shared_secret (shared secret) for OAuthSimple. This is usually provided by the hosting site.");
87*/      this._secrets={};
88        this._parameters={};
89
90        // General configuration options.
91        if (consumer_key !== undefined) {
92            this._secrets['consumer_key'] = consumer_key;
93            }
94        if (shared_secret !== undefined) {
95            this._secrets['shared_secret'] = shared_secret;
96            }
97        this._default_signature_method= "HMAC-SHA1";
98        this._action = "GET";
99        this._nonce_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
100
101
102        this.reset = function() {
103            this._parameters={};
104            this._path=undefined;
105            return this;
106        };
107
108        /** set the parameters either from a hash or a string
109         *
110         * @param {string,object} List of parameters for the call, this can either be a URI string (e.g. "foo=bar&gorp=banana" or an object/hash)
111         */
112        this.setParameters = function (parameters) {
113            if (parameters === undefined) {
114                parameters = {};
115                }
116            if (typeof(parameters) == 'string') {
117                parameters=this._parseParameterString(parameters);
118                }
119            this._parameters = parameters;
120            if (this._parameters['oauth_nonce'] === undefined) {
121                this._getNonce();
122                }
123            if (this._parameters['oauth_timestamp'] === undefined) {
124                this._getTimestamp();
125                }
126            if (this._parameters['oauth_method'] === undefined) {
127                this.setSignatureMethod();
128                }
129            if (this._parameters['oauth_consumer_key'] === undefined) {
130                this._getApiKey();
131                }
132            if(this._parameters['oauth_token'] === undefined) {
133                this._getAccessToken();
134                }
135
136            return this;
137        };
138
139        /** convienence method for setParameters
140         *
141         * @param parameters {string,object} See .setParameters
142         */
143        this.setQueryString = function (parameters) {
144            return this.setParameters(parameters);
145        };
146
147        /** Set the target URL (does not include the parameters)
148         *
149         * @param path {string} the fully qualified URI (excluding query arguments) (e.g "http://example.org/foo")
150         */
151        this.setURL = function (path) {
152            if (path == '') {
153                throw ('No path specified for OAuthSimple.setURL');
154                }
155            this._path = path;
156            return this;
157        };
158
159        /** convienence method for setURL
160         *
161         * @param path {string} see .setURL
162         */
163        this.setPath = function(path){
164            return this.setURL(path);
165        };
166
167        /** set the "action" for the url, (e.g. GET,POST, DELETE, etc.)
168         *
169         * @param action {string} HTTP Action word.
170         */
171        this.setAction = function(action) {
172            if (action === undefined) {
173                action="GET";
174                }
175            action = action.toUpperCase();
176            if (action.match('[^A-Z]')) {
177                throw ('Invalid action specified for OAuthSimple.setAction');
178                }
179            this._action = action;
180            return this;
181        };
182
183        /** set the signatures (as well as validate the ones you have)
184         *
185         * @param signatures {object} object/hash of the token/signature pairs {api_key:, shared_secret:, oauth_token: oauth_secret:}
186         */
187        this.setTokensAndSecrets = function(signatures) {
188            if (signatures)
189            {
190                for (var i in signatures) {
191                    this._secrets[i] = signatures[i];
192                    }
193            }
194            // Aliases
195            if (this._secrets['api_key']) {
196                this._secrets.consumer_key = this._secrets.api_key;
197                }
198            if (this._secrets['access_token']) {
199                this._secrets.oauth_token = this._secrets.access_token;
200                }
201            if (this._secrets['access_secret']) {
202                this._secrets.oauth_secret = this._secrets.access_secret;
203                }
204            // Gauntlet
205            if (this._secrets.consumer_key === undefined) {
206                throw('Missing required consumer_key in OAuthSimple.setTokensAndSecrets');
207                }
208            if (this._secrets.shared_secret === undefined) {
209                throw('Missing required shared_secret in OAuthSimple.setTokensAndSecrets');
210                }
211            if ((this._secrets.oauth_token !== undefined) && (this._secrets.oauth_secret === undefined)) {
212                throw('Missing oauth_secret for supplied oauth_token in OAuthSimple.setTokensAndSecrets');
213                }
214            return this;
215        };
216
217        /** set the signature method (currently only Plaintext or SHA-MAC1)
218         *
219         * @param method {string} Method of signing the transaction (only PLAINTEXT and SHA-MAC1 allowed for now)
220         */
221        this.setSignatureMethod = function(method) {
222            if (method === undefined) {
223                method = this._default_signature_method;
224                }
225            //TODO: accept things other than PlainText or SHA-MAC1
226            if (method.toUpperCase().match(/(PLAINTEXT|HMAC-SHA1)/) === undefined) {
227                throw ('Unknown signing method specified for OAuthSimple.setSignatureMethod');
228                }
229            this._parameters['oauth_signature_method']= method.toUpperCase();
230            return this;
231        };
232
233        /** sign the request
234         *
235         * note: all arguments are optional, provided you've set them using the
236         * other helper functions.
237         *
238         * @param args {object} hash of arguments for the call
239         *                   {action:, path:, parameters:, method:, signatures:}
240         *                   all arguments are optional.
241         */
242        this.sign = function (args) {
243            if (args === undefined) {
244                args = {};
245                }
246            // Set any given parameters
247            if(args['action'] !== undefined) {
248                this.setAction(args['action']);
249                }
250            if (args['path'] !== undefined) {
251                this.setPath(args['path']);
252                }
253            if (args['method'] !== undefined) {
254                this.setSignatureMethod(args['method']);
255                }
256            this.setTokensAndSecrets(args['signatures']);
257            if (args['parameters'] !== undefined){
258            this.setParameters(args['parameters']);
259            }
260            // check the parameters
261            var normParams = this._normalizedParameters();
262            this._parameters['oauth_signature']=this._generateSignature(normParams);
263            return {
264                parameters: this._parameters,
265                signature: this._oauthEscape(this._parameters['oauth_signature']),
266                signed_url: this._path + '?' + this._normalizedParameters(),
267                header: this.getHeaderString()
268            };
269        };
270
271        /** Return a formatted "header" string
272         *
273         * NOTE: This doesn't set the "Authorization: " prefix, which is required.
274         * I don't set it because various set header functions prefer different
275         * ways to do that.
276         *
277         * @param args {object} see .sign
278         */
279        this.getHeaderString = function(args) {
280            if (this._parameters['oauth_signature'] === undefined) {
281                this.sign(args);
282                }
283
284            var result = 'OAuth ';
285            for (var pName in this._parameters)
286            {
287                if (!pName.match(/^oauth/)) {
288                    continue;
289                    }
290                if ((this._parameters[pName]) instanceof Array)
291                {
292                    var pLength = this._parameters[pName].length;
293                    for (var j=0;j<pLength;j++)
294                    {
295                        result += pName +'="'+this._oauthEscape(this._parameters[pName][j])+'" ';
296                    }
297                }
298                else
299                {
300                    result += pName + '="'+this._oauthEscape(this._parameters[pName])+'" ';
301                }
302            }
303            return result;
304        };
305
306        // Start Private Methods.
307
308        /** convert the parameter string into a hash of objects.
309         *
310         */
311        this._parseParameterString = function(paramString){
312            var elements = paramString.split('&');
313            var result={};
314            for(var element=elements.shift();element;element=elements.shift())
315            {
316                var keyToken=element.split('=');
317                var value='';
318                if (keyToken[1]) {
319                    value=decodeURIComponent(keyToken[1]);
320                    }
321                if(result[keyToken[0]]){
322                    if (!(result[keyToken[0]] instanceof Array))
323                    {
324                        result[keyToken[0]] = Array(result[keyToken[0]],value);
325                    }
326                    else
327                    {
328                        result[keyToken[0]].push(value);
329                    }
330                }
331                else
332                {
333                    result[keyToken[0]]=value;
334                }
335            }
336            return result;
337        };
338
339        this._oauthEscape = function(string) {
340            if (string === undefined) {
341                return "";
342                }
343            if (string instanceof Array)
344            {
345                throw('Array passed to _oauthEscape');
346            }
347            return encodeURIComponent(string).replace(/\!/g, "%21").
348            replace(/\*/g, "%2A").
349            replace(/'/g, "%27").
350            replace(/\(/g, "%28").
351            replace(/\)/g, "%29");
352        };
353
354        this._getNonce = function (length) {
355            if (length === undefined) {
356                length=5;
357                }
358            var result = "";
359            var cLength = this._nonce_chars.length;
360            for (var i = 0; i < length;i++) {
361                var rnum = Math.floor(Math.random() *cLength);
362                result += this._nonce_chars.substring(rnum,rnum+1);
363            }
364            this._parameters['oauth_nonce']=result;
365            return result;
366        };
367
368        this._getApiKey = function() {
369            if (this._secrets.consumer_key === undefined) {
370                throw('No consumer_key set for OAuthSimple.');
371                }
372            this._parameters['oauth_consumer_key']=this._secrets.consumer_key;
373            return this._parameters.oauth_consumer_key;
374        };
375
376        this._getAccessToken = function() {
377            if (this._secrets['oauth_secret'] === undefined) {
378                return '';
379                }
380            if (this._secrets['oauth_token'] === undefined) {
381                throw('No oauth_token (access_token) set for OAuthSimple.');
382                }
383            this._parameters['oauth_token'] = this._secrets.oauth_token;
384            return this._parameters.oauth_token;
385        };
386
387        this._getTimestamp = function() {
388            var d = new Date();
389            var ts = Math.floor(d.getTime()/1000);
390            this._parameters['oauth_timestamp'] = ts;
391            return ts;
392        };
393
394        this.b64_hmac_sha1 = function(k,d,_p,_z){
395        // heavily optimized and compressed version of http://pajhome.org.uk/crypt/md5/sha1.js
396        // _p = b64pad, _z = character size; not used here but I left them available just in case
397        if(!_p){_p='=';}if(!_z){_z=8;}function _f(t,b,c,d){if(t<20){return(b&c)|((~b)&d);}if(t<40){return b^c^d;}if(t<60){return(b&c)|(b&d)|(c&d);}return b^c^d;}function _k(t){return(t<20)?1518500249:(t<40)?1859775393:(t<60)?-1894007588:-899497514;}function _s(x,y){var l=(x&0xFFFF)+(y&0xFFFF),m=(x>>16)+(y>>16)+(l>>16);return(m<<16)|(l&0xFFFF);}function _r(n,c){return(n<<c)|(n>>>(32-c));}function _c(x,l){x[l>>5]|=0x80<<(24-l%32);x[((l+64>>9)<<4)+15]=l;var w=[80],a=1732584193,b=-271733879,c=-1732584194,d=271733878,e=-1009589776;for(var i=0;i<x.length;i+=16){var o=a,p=b,q=c,r=d,s=e;for(var j=0;j<80;j++){if(j<16){w[j]=x[i+j];}else{w[j]=_r(w[j-3]^w[j-8]^w[j-14]^w[j-16],1);}var t=_s(_s(_r(a,5),_f(j,b,c,d)),_s(_s(e,w[j]),_k(j)));e=d;d=c;c=_r(b,30);b=a;a=t;}a=_s(a,o);b=_s(b,p);c=_s(c,q);d=_s(d,r);e=_s(e,s);}return[a,b,c,d,e];}function _b(s){var b=[],m=(1<<_z)-1;for(var i=0;i<s.length*_z;i+=_z){b[i>>5]|=(s.charCodeAt(i/8)&m)<<(32-_z-i%32);}return b;}function _h(k,d){var b=_b(k);if(b.length>16){b=_c(b,k.length*_z);}var p=[16],o=[16];for(var i=0;i<16;i++){p[i]=b[i]^0x36363636;o[i]=b[i]^0x5C5C5C5C;}var h=_c(p.concat(_b(d)),512+d.length*_z);return _c(o.concat(h),512+160);}function _n(b){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s='';for(var i=0;i<b.length*4;i+=3){var r=(((b[i>>2]>>8*(3-i%4))&0xFF)<<16)|(((b[i+1>>2]>>8*(3-(i+1)%4))&0xFF)<<8)|((b[i+2>>2]>>8*(3-(i+2)%4))&0xFF);for(var j=0;j<4;j++){if(i*8+j*6>b.length*32){s+=_p;}else{s+=t.charAt((r>>6*(3-j))&0x3F);}}}return s;}function _x(k,d){return _n(_h(k,d));}return _x(k,d);
398        }
399
400
401        this._normalizedParameters = function() {
402            var elements = new Array();
403            var paramNames = [];
404            var ra =0;
405            for (var paramName in this._parameters)
406            {
407                if (ra++ > 1000) {
408                    throw('runaway 1');
409                    }
410                paramNames.unshift(paramName);
411            }
412            paramNames = paramNames.sort();
413            pLen = paramNames.length;
414            for (var i=0;i<pLen; i++)
415            {
416                paramName=paramNames[i];
417                //skip secrets.
418                if (paramName.match(/\w+_secret/)) {
419                    continue;
420                    }
421                if (this._parameters[paramName] instanceof Array)
422                {
423                    var sorted = this._parameters[paramName].sort();
424                    var spLen = sorted.length;
425                    for (var j = 0;j<spLen;j++){
426                        if (ra++ > 1000) {
427                            throw('runaway 1');
428                            }
429                        elements.push(this._oauthEscape(paramName) + '=' +
430                                  this._oauthEscape(sorted[j]));
431                    }
432                    continue;
433                }
434                elements.push(this._oauthEscape(paramName) + '=' +
435                              this._oauthEscape(this._parameters[paramName]));
436            }
437            return elements.join('&');
438        };
439
440        this._generateSignature = function() {
441
442            var secretKey = this._oauthEscape(this._secrets.shared_secret)+'&'+
443                this._oauthEscape(this._secrets.oauth_secret);
444            if (this._parameters['oauth_signature_method'] == 'PLAINTEXT')
445            {
446                return secretKey;
447            }
448            if (this._parameters['oauth_signature_method'] == 'HMAC-SHA1')
449            {
450                var sigString = this._oauthEscape(this._action)+'&'+this._oauthEscape(this._path)+'&'+this._oauthEscape(this._normalizedParameters());
451                return this.b64_hmac_sha1(secretKey,sigString);
452            }
453            return null;
454        };
455
456    return this;
457    };
458}
459