/* | |
* Copyright 2008 Netflix, Inc. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
/* Here's some JavaScript software for implementing OAuth. | |
This isn't as useful as you might hope. OAuth is based around | |
allowing tools and websites to talk to each other. However, | |
JavaScript running in web browsers is hampered by security | |
restrictions that prevent code running on one website from | |
accessing data stored or served on another. | |
Before you start hacking, make sure you understand the limitations | |
posed by cross-domain XMLHttpRequest. | |
On the bright side, some platforms use JavaScript as their | |
language, but enable the programmer to access other web sites. | |
Examples include Google Gadgets, and Microsoft Vista Sidebar. | |
For those platforms, this library should come in handy. | |
*/ | |
// The HMAC-SHA1 signature method calls b64_hmac_sha1, defined by | |
// http://pajhome.org.uk/crypt/md5/sha1.js | |
/* An OAuth message is represented as an object like this: | |
{method: "GET", action: "http://server.com/path", parameters: ...} | |
The parameters may be either a map {name: value, name2: value2} | |
or an Array of name-value pairs [[name, value], [name2, value2]]. | |
The latter representation is more powerful: it supports parameters | |
in a specific sequence, or several parameters with the same name; | |
for example [["a", 1], ["b", 2], ["a", 3]]. | |
Parameter names and values are NOT percent-encoded in an object. | |
They must be encoded before transmission and decoded after reception. | |
For example, this message object: | |
{method: "GET", action: "http://server/path", parameters: {p: "x y"}} | |
... can be transmitted as an HTTP request that begins: | |
GET /path?p=x%20y HTTP/1.0 | |
(This isn't a valid OAuth request, since it lacks a signature etc.) | |
Note that the object "x y" is transmitted as x%20y. To encode | |
parameters, you can call OAuth.addToURL, OAuth.formEncode or | |
OAuth.getAuthorization. | |
This message object model harmonizes with the browser object model for | |
input elements of an form, whose value property isn't percent encoded. | |
The browser encodes each value before transmitting it. For example, | |
see consumer.setInputs in example/consumer.js. | |
*/ | |
/* This script needs to know what time it is. By default, it uses the local | |
clock (new Date), which is apt to be inaccurate in browsers. To do | |
better, you can load this script from a URL whose query string contains | |
an oauth_timestamp parameter, whose value is a current Unix timestamp. | |
For example, when generating the enclosing document using PHP: | |
<script src="oauth.js?oauth_timestamp=<?=time()?>" ... | |
Another option is to call OAuth.correctTimestamp with a Unix timestamp. | |
*/ | |
var OAuth; if (OAuth == null) OAuth = {}; | |
OAuth.setProperties = function setProperties(into, from) { | |
if (into != null && from != null) { | |
for (var key in from) { | |
into[key] = from[key]; | |
} | |
} | |
return into; | |
} | |
OAuth.setProperties(OAuth, // utility functions | |
{ | |
percentEncode: function percentEncode(s) { | |
if (s == null) { | |
return ""; | |
} | |
if (s instanceof Array) { | |
var e = ""; | |
for (var i = 0; i < s.length; ++s) { | |
if (e != "") e += '&'; | |
e += OAuth.percentEncode(s[i]); | |
} | |
return e; | |
} | |
s = encodeURIComponent(s); | |
// Now replace the values which encodeURIComponent doesn't do | |
// encodeURIComponent ignores: - _ . ! ~ * ' ( ) | |
// OAuth dictates the only ones you can ignore are: - _ . ~ | |
// Source: http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Functions:encodeURIComponent | |
s = s.replace(/\!/g, "%21"); | |
s = s.replace(/\*/g, "%2A"); | |
s = s.replace(/\'/g, "%27"); | |
s = s.replace(/\(/g, "%28"); | |
s = s.replace(/\)/g, "%29"); | |
return s; | |
} | |
, | |
decodePercent: function decodePercent(s) { | |
if (s != null) { | |
// Handle application/x-www-form-urlencoded, which is defined by | |
// http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 | |
s = s.replace(/\+/g, " "); | |
} | |
return decodeURIComponent(s); | |
} | |
, | |
/** Convert the given parameters to an Array of name-value pairs. */ | |
getParameterList: function getParameterList(parameters) { | |
if (parameters == null) { | |
return []; | |
} | |
if (typeof parameters != "object") { | |
return OAuth.decodeForm(parameters + ""); | |
} | |
if (parameters instanceof Array) { | |
return parameters; | |
} | |
var list = []; | |
for (var p in parameters) { | |
list.push([p, parameters[p]]); | |
} | |
return list; | |
} | |
, | |
/** Convert the given parameters to a map from name to value. */ | |
getParameterMap: function getParameterMap(parameters) { | |
if (parameters == null) { | |
return {}; | |
} | |
if (typeof parameters != "object") { | |
return OAuth.getParameterMap(OAuth.decodeForm(parameters + "")); | |
} | |
if (parameters instanceof Array) { | |
var map = {}; | |
for (var p = 0; p < parameters.length; ++p) { | |
var key = parameters[p][0]; | |
if (map[key] === undefined) { // first value wins | |
map[key] = parameters[p][1]; | |
} | |
} | |
return map; | |
} | |
return parameters; | |
} | |
, | |
getParameter: function getParameter(parameters, name) { | |
if (parameters instanceof Array) { | |
for (var p = 0; p < parameters.length; ++p) { | |
if (parameters[p][0] == name) { | |
return parameters[p][1]; // first value wins | |
} | |
} | |
} else { | |
return OAuth.getParameterMap(parameters)[name]; | |
} | |
return null; | |
} | |
, | |
formEncode: function formEncode(parameters) { | |
var form = ""; | |
var list = OAuth.getParameterList(parameters); | |
for (var p = 0; p < list.length; ++p) { | |
var value = list[p][1]; | |
if (value == null) value = ""; | |
if (form != "") form += '&'; | |
form += OAuth.percentEncode(list[p][0]) | |
+'='+ OAuth.percentEncode(value); | |
} | |
return form; | |
} | |
, | |
decodeForm: function decodeForm(form) { | |
var list = []; | |
var nvps = form.split('&'); | |
for (var n = 0; n < nvps.length; ++n) { | |
var nvp = nvps[n]; | |
if (nvp == "") { | |
continue; | |
} | |
var equals = nvp.indexOf('='); | |
var name; | |
var value; | |
if (equals < 0) { | |
name = OAuth.decodePercent(nvp); | |
value = null; | |
} else { | |
name = OAuth.decodePercent(nvp.substring(0, equals)); | |
value = OAuth.decodePercent(nvp.substring(equals + 1)); | |
} | |
list.push([name, value]); | |
} | |
return list; | |
} | |
, | |
setParameter: function setParameter(message, name, value) { | |
var parameters = message.parameters; | |
if (parameters instanceof Array) { | |
for (var p = 0; p < parameters.length; ++p) { | |
if (parameters[p][0] == name) { | |
if (value === undefined) { | |
parameters.splice(p, 1); | |
} else { | |
parameters[p][1] = value; | |
value = undefined; | |
} | |
} | |
} | |
if (value !== undefined) { | |
parameters.push([name, value]); | |
} | |
} else { | |
parameters = OAuth.getParameterMap(parameters); | |
parameters[name] = value; | |
message.parameters = parameters; | |
} | |
} | |
, | |
setParameters: function setParameters(message, parameters) { | |
var list = OAuth.getParameterList(parameters); | |
for (var i = 0; i < list.length; ++i) { | |
OAuth.setParameter(message, list[i][0], list[i][1]); | |
} | |
} | |
, | |
/** Fill in parameters to help construct a request message. | |
This function doesn't fill in every parameter. | |
The accessor object should be like: | |
{consumerKey:'foo', consumerSecret:'bar', accessorSecret:'nurn', token:'krelm', tokenSecret:'blah'} | |
The accessorSecret property is optional. | |
*/ | |
completeRequest: function completeRequest(message, accessor) { | |
if (message.method == null) { | |
message.method = "GET"; | |
} | |
var map = OAuth.getParameterMap(message.parameters); | |
if (map.oauth_consumer_key == null) { | |
OAuth.setParameter(message, "oauth_consumer_key", accessor.consumerKey || ""); | |
} | |
if (map.oauth_token == null && accessor.token != null) { | |
OAuth.setParameter(message, "oauth_token", accessor.token); | |
} | |
if (map.oauth_version == null) { | |
OAuth.setParameter(message, "oauth_version", "1.0"); | |
} | |
if (map.oauth_timestamp == null) { | |
OAuth.setParameter(message, "oauth_timestamp", OAuth.timestamp()); | |
} | |
if (map.oauth_nonce == null) { | |
OAuth.setParameter(message, "oauth_nonce", OAuth.nonce(6)); | |
} | |
OAuth.SignatureMethod.sign(message, accessor); | |
} | |
, | |
setTimestampAndNonce: function setTimestampAndNonce(message) { | |
OAuth.setParameter(message, "oauth_timestamp", OAuth.timestamp()); | |
OAuth.setParameter(message, "oauth_nonce", OAuth.nonce(6)); | |
} | |
, | |
addToURL: function addToURL(url, parameters) { | |
newURL = url; | |
if (parameters != null) { | |
var toAdd = OAuth.formEncode(parameters); | |
if (toAdd.length > 0) { | |
var q = url.indexOf('?'); | |
if (q < 0) newURL += '?'; | |
else newURL += '&'; | |
newURL += toAdd; | |
} | |
} | |
return newURL; | |
} | |
, | |
/** Construct the value of the Authorization header for an HTTP request. */ | |
getAuthorizationHeader: function getAuthorizationHeader(realm, parameters) { | |
var header = 'OAuth realm="' + OAuth.percentEncode(realm) + '"'; | |
var list = OAuth.getParameterList(parameters); | |
for (var p = 0; p < list.length; ++p) { | |
var parameter = list[p]; | |
var name = parameter[0]; | |
if (name.indexOf("oauth_") == 0) { | |
header += ',' + OAuth.percentEncode(name) + '="' + OAuth.percentEncode(parameter[1]) + '"'; | |
} | |
} | |
return header; | |
} | |
, | |
/** Correct the time using a parameter from the URL from which the last script was loaded. */ | |
correctTimestampFromSrc: function correctTimestampFromSrc(parameterName) { | |
parameterName = parameterName || "oauth_timestamp"; | |
var scripts = document.getElementsByTagName('script'); | |
if (scripts == null || !scripts.length) return; | |
var src = scripts[scripts.length-1].src; | |
if (!src) return; | |
var q = src.indexOf("?"); | |
if (q < 0) return; | |
parameters = OAuth.getParameterMap(OAuth.decodeForm(src.substring(q+1))); | |
var t = parameters[parameterName]; | |
if (t == null) return; | |
OAuth.correctTimestamp(t); | |
} | |
, | |
/** Generate timestamps starting with the given value. */ | |
correctTimestamp: function correctTimestamp(timestamp) { | |
OAuth.timeCorrectionMsec = (timestamp * 1000) - (new Date()).getTime(); | |
} | |
, | |
/** The difference between the correct time and my clock. */ | |
timeCorrectionMsec: 0 | |
, | |
timestamp: function timestamp() { | |
var t = (new Date()).getTime() + OAuth.timeCorrectionMsec; | |
return Math.floor(t / 1000); | |
} | |
, | |
nonce: function nonce(length) { | |
var chars = OAuth.nonce.CHARS; | |
var result = ""; | |
for (var i = 0; i < length; ++i) { | |
var rnum = Math.floor(Math.random() * chars.length); | |
result += chars.substring(rnum, rnum+1); | |
} | |
return result; | |
} | |
}); | |
OAuth.nonce.CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; | |
/** Define a constructor function, | |
without causing trouble to anyone who was using it as a namespace. | |
That is, if parent[name] already existed and had properties, | |
copy those properties into the new constructor. | |
*/ | |
OAuth.declareClass = function declareClass(parent, name, newConstructor) { | |
var previous = parent[name]; | |
parent[name] = newConstructor; | |
if (newConstructor != null && previous != null) { | |
for (var key in previous) { | |
if (key != "prototype") { | |
newConstructor[key] = previous[key]; | |
} | |
} | |
} | |
return newConstructor; | |
} | |
/** An abstract algorithm for signing messages. */ | |
OAuth.declareClass(OAuth, "SignatureMethod", function OAuthSignatureMethod(){}); | |
OAuth.setProperties(OAuth.SignatureMethod.prototype, // instance members | |
{ | |
/** Add a signature to the message. */ | |
sign: function sign(message) { | |
var baseString = OAuth.SignatureMethod.getBaseString(message); | |
var signature = this.getSignature(baseString); | |
OAuth.setParameter(message, "oauth_signature", signature); | |
return signature; // just in case someone's interested | |
} | |
, | |
/** Set the key string for signing. */ | |
initialize: function initialize(name, accessor) { | |
var consumerSecret; | |
if (accessor.accessorSecret != null | |
&& name.length > 9 | |
&& name.substring(name.length-9) == "-Accessor") | |
{ | |
consumerSecret = accessor.accessorSecret; | |
} else { | |
consumerSecret = accessor.consumerSecret; | |
} | |
this.key = OAuth.percentEncode(consumerSecret) | |
+"&"+ OAuth.percentEncode(accessor.tokenSecret); | |
} | |
}); | |
/* SignatureMethod expects an accessor object to be like this: | |
{tokenSecret: "lakjsdflkj...", consumerSecret: "QOUEWRI..", accessorSecret: "xcmvzc..."} | |
The accessorSecret property is optional. | |
*/ | |
// Class members: | |
OAuth.setProperties(OAuth.SignatureMethod, // class members | |
{ | |
sign: function sign(message, accessor) { | |
var name = OAuth.getParameterMap(message.parameters).oauth_signature_method; | |
if (name == null || name == "") { | |
name = "HMAC-SHA1"; | |
OAuth.setParameter(message, "oauth_signature_method", name); | |
} | |
OAuth.SignatureMethod.newMethod(name, accessor).sign(message); | |
} | |
, | |
/** Instantiate a SignatureMethod for the given method name. */ | |
newMethod: function newMethod(name, accessor) { | |
var impl = OAuth.SignatureMethod.REGISTERED[name]; | |
if (impl != null) { | |
var method = new impl(); | |
method.initialize(name, accessor); | |
return method; | |
} | |
var err = new Error("signature_method_rejected"); | |
var acceptable = ""; | |
for (var r in OAuth.SignatureMethod.REGISTERED) { | |
if (acceptable != "") acceptable += '&'; | |
acceptable += OAuth.percentEncode(r); | |
} | |
err.oauth_acceptable_signature_methods = acceptable; | |
throw err; | |
} | |
, | |
/** A map from signature method name to constructor. */ | |
REGISTERED : {} | |
, | |
/** Subsequently, the given constructor will be used for the named methods. | |
The constructor will be called with no parameters. | |
The resulting object should usually implement getSignature(baseString). | |
You can easily define such a constructor by calling makeSubclass, below. | |
*/ | |
registerMethodClass: function registerMethodClass(names, classConstructor) { | |
for (var n = 0; n < names.length; ++n) { | |
OAuth.SignatureMethod.REGISTERED[names[n]] = classConstructor; | |
} | |
} | |
, | |
/** Create a subclass of OAuth.SignatureMethod, with the given getSignature function. */ | |
makeSubclass: function makeSubclass(getSignatureFunction) { | |
var superClass = OAuth.SignatureMethod; | |
var subClass = function() { | |
superClass.call(this); | |
}; | |
subClass.prototype = new superClass(); | |
// Delete instance variables from prototype: | |
// delete subclass.prototype... There aren't any. | |
subClass.prototype.getSignature = getSignatureFunction; | |
subClass.prototype.constructor = subClass; | |
return subClass; | |
} | |
, | |
getBaseString: function getBaseString(message) { | |
var URL = message.action; | |
var q = URL.indexOf('?'); | |
var parameters; | |
if (q < 0) { | |
parameters = message.parameters; | |
} else { | |
// Combine the URL query string with the other parameters: | |
parameters = OAuth.decodeForm(URL.substring(q + 1)); | |
var toAdd = OAuth.getParameterList(message.parameters); | |
for (var a = 0; a < toAdd.length; ++a) { | |
parameters.push(toAdd[a]); | |
} | |
} | |
return OAuth.percentEncode(message.method.toUpperCase()) | |
+'&'+ OAuth.percentEncode(OAuth.SignatureMethod.normalizeUrl(URL)) | |
+'&'+ OAuth.percentEncode(OAuth.SignatureMethod.normalizeParameters(parameters)); | |
} | |
, | |
normalizeUrl: function normalizeUrl(url) { | |
var uri = OAuth.SignatureMethod.parseUri(url); | |
var scheme = uri.protocol.toLowerCase(); | |
var authority = uri.authority.toLowerCase(); | |
var dropPort = (scheme == "http" && uri.port == 80) | |
|| (scheme == "https" && uri.port == 443); | |
if (dropPort) { | |
// find the last : in the authority | |
var index = authority.lastIndexOf(":"); | |
if (index >= 0) { | |
authority = authority.substring(0, index); | |
} | |
} | |
var path = uri.path; | |
if (!path) { | |
path = "/"; // conforms to RFC 2616 section 3.2.2 | |
} | |
// we know that there is no query and no fragment here. | |
return scheme + "://" + authority + path; | |
} | |
, | |
parseUri: function parseUri (str) { | |
/* This function was adapted from parseUri 1.2.1 | |
http://stevenlevithan.com/demo/parseuri/js/assets/parseuri.js | |
*/ | |
var o = {key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], | |
parser: {strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ }}; | |
var m = o.parser.strict.exec(str); | |
var uri = {}; | |
var i = 14; | |
while (i--) uri[o.key[i]] = m[i] || ""; | |
return uri; | |
} | |
, | |
normalizeParameters: function normalizeParameters(parameters) { | |
if (parameters == null) { | |
return ""; | |
} | |
var list = OAuth.getParameterList(parameters); | |
var sortable = []; | |
for (var p = 0; p < list.length; ++p) { | |
var nvp = list[p]; | |
if (nvp[0] != "oauth_signature") { | |
sortable.push([ OAuth.percentEncode(nvp[0]) | |
+ " " // because it comes before any character that can appear in a percentEncoded string. | |
+ OAuth.percentEncode(nvp[1]) | |
, nvp]); | |
} | |
} | |
sortable.sort(function(a,b) { | |
if (a[0] < b[0]) return -1; | |
if (a[0] > b[0]) return 1; | |
return 0; | |
}); | |
var sorted = []; | |
for (var s = 0; s < sortable.length; ++s) { | |
sorted.push(sortable[s][1]); | |
} | |
return OAuth.formEncode(sorted); | |
} | |
}); | |
OAuth.SignatureMethod.registerMethodClass(["PLAINTEXT", "PLAINTEXT-Accessor"], | |
OAuth.SignatureMethod.makeSubclass( | |
function getSignature(baseString) { | |
return this.key; | |
} | |
)); | |
OAuth.SignatureMethod.registerMethodClass(["HMAC-SHA1", "HMAC-SHA1-Accessor"], | |
OAuth.SignatureMethod.makeSubclass( | |
function getSignature(baseString) { | |
b64pad = '='; | |
var signature = b64_hmac_sha1(this.key, baseString); | |
return signature; | |
} | |
)); | |
try { | |
OAuth.correctTimestampFromSrc(); | |
} catch(e) { | |
} |