ember-introjs
Version:
An Ember Component for intro.js
316 lines (284 loc) • 11.6 kB
JavaScript
var parseUrl = require("url").parse;
var querystring = require("./util/querystring");
var whenPromise = require("./promise").whenPromise;
var makeRequest = require("./http-client").request;
var crypto = require("crypto");
function encodeRfc3986(str){
return !str ? "" : encodeURIComponent(str)
.replace(/\!/g, "%21")
.replace(/\'/g, "%27")
.replace(/\(/g, "%28")
.replace(/\)/g, "%29")
.replace(/\*/g, "%2A");
}
function parseResponse(response){
return response.body.join("").then(function(body){
if(response.status == 200){
return querystring.parse(body);
}else{
var err = new Error(response.status + ": " + body);
err.status = response.status;
err.headers = response.headers;
err.body = body;
throw err;
}
});
}
exports.Client = Client;
function Client(identifier, secret, tempRequestUrl, tokenRequestUrl, callback, version, signatureMethod, nonceGenerator, headers){
this.identifier = identifier;
this.tempRequestUrl = tempRequestUrl;
this.tokenRequestUrl = tokenRequestUrl;
this.callback = callback;
this.version = version || false;
// _createSignature actually uses the variable, not the instance property
this.signatureMethod = signatureMethod = signatureMethod || "HMAC-SHA1";
this.generateNonce = nonceGenerator || Client.makeNonceGenerator(32);
this.headers = headers || Client.Headers;
if(this.signatureMethod != "PLAINTEXT" && this.signatureMethod != "HMAC-SHA1"){
throw new Error("Unsupported signature method: " + this.signatureMethod);
}
// We don't store the secrets on the instance itself, that way it can
// be passed to other actors without leaking
secret = encodeRfc3986(secret);
this._createSignature = function(tokenSecret, baseString){
if(baseString === undefined){
baseString = tokenSecret;
tokenSecret = "";
}
var key = secret + "&" + tokenSecret;
if(signatureMethod == "PLAINTEXT"){
return key;
}else{
return crypto.createHmac("SHA1", key).update(baseString).digest("base64");
}
};
}
Client.Headers = {
Accept: "*/*",
Connection: "close",
"User-Agent": "promised-io/oauth"
};
// The default headers shouldn't change after clients have been created,
// but you're free to replace the object or pass headers to the Client
// constructor.
Object.freeze(Client.Headers);
Client.NonceChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Client.makeNonceGenerator = function(nonceSize){
var nonce = Array(nonceSize + 1).join("-").split("");
var chars = Client.NonceChars.split("");
return function nonceGenerator(){
return nonce.map(getRandomChar).join("");
};
function getRandomChar(){
return chars[Math.floor(Math.random() * chars.length)];
}
};
Client.getTimestamp = function(){
return Math.floor(Date.now() / 1000).toString();
};
// Binds the client against a set of token credentials.
// The resulting object can be used to make signed requests.
// The secret won't be exposed on the object itself.
Client.prototype.bind = function(tokenIdentifier, tokenSecret){
var bound = {
identifier: this.identifier,
tokenIdentifier: tokenIdentifier,
signatureMethod: this.signatureMethod,
version: this.version,
headers: this.headers,
generateNonce: this.generateNonce
};
bound._createSignature = this._createSignature.bind(this, encodeRfc3986(tokenSecret));
bound._createSignatureBase = this._createSignatureBase;
bound._normalizeUrl = this._normalizeUrl;
bound._collectOAuthParams = this._collectOAuthParams;
bound._normalizeParams = this._normalizeParams;
bound._signRequest = this._signRequest;
bound.request = this.request;
return bound;
};
// Wrapper for `http-client.request` which signs the request
// with the client credentials, and optionally token credentials if bound.
Client.prototype.request = function(originalRequest){
var request = {};
for(var key in originalRequest){
if(originalRequest.hasOwnProperty(key)){
request[key] = originalRequest[key];
}
}
// Normalize the request. `engines/node/http-client.request` is
// quite flexible, but this should do it.
if(request.url){
var parsed = parseUrl(request.url);
parsed.pathInfo = parsed.pathname;
parsed.queryString = parsed.query;
for(var i in parsed){
request[i] = parsed[i];
}
}
request.pathname = request.pathname || request.pathInfo || "/";
request.queryString = request.queryString || request.query || "";
request.method = (request.method || "GET").toUpperCase();
request.protocol = request.protocol.toLowerCase();
request.hostname = (request.host || request.hostname).toLowerCase();
request.headers = {};
for(var h in this.headers){
request.headers[h] = this.headers[h];
}
for(var h in originalRequest.headers){
request.headers[h] = originalRequest.headers[h];
}
// We'll be setting the Authorization header; due to how `engines/node/http-client.request`
// is implemented we need to set the Host header as well.
request.headers.host = request.headers.host || request.hostname + (request.port ? ":" + request.port : "");
// Parse all request parameters into a flattened array of parameter pairs.
// Note that this array contains munged parameter names.
var requestParams = [], uncombinedRequestParams = [];
// Start with parameters that were defined in the query string
if(request.queryString){
querystring.parseToArray(requestParams, request.queryString);
}
// Allow parameters to be defined in object notation, this is *not* part of `http-client.request`!
// It saves an extra stringify+parse though.
if(request.requestParams){
for(var i in request.requestParams){
if(request.requestParams.hasOwnProperty(i)){
querystring.addToArray(uncombinedRequestParams, i, request.requestParams[i]);
}
}
}
// Send the parameters from `request.requestParams` in the query string
// for GET and DELETE requests. We immediately concat to the `requestParams` array,
// which is then built into the query string.
if(request.method == "GET" || request.method == "DELETE"){
requestParams = requestParams.concat(uncombinedRequestParams);
}
// Rebuild the query string
request.queryString = requestParams.reduce(function(qs, v, i){
return qs + (i % 2 ? "=" + querystring.escape(v) : (qs.length ? "&" : "") + querystring.escape(v));
}, "");
// Depending on the request content type, look for request parameters in the body
var waitForBody = false;
if(request.headers && request.headers["Content-Type"] == "application/x-www-form-urlencoded"){
waitForBody = whenPromise(request.body.join(""), function(body){
querystring.parseToArray(requestParams, body);
return body;
});
}
// If we're a POST or PUT and are not sending any content, or are sending urlencoded content,
// add the `request.request` to the request body. If we are sending non-urlencoded content through
// a POST or PUT, the `request.requestParams` are ignored.
if(request.requestParams && (request.method == "POST" || request.method == "PUT") && (!request.headers || !request.headers["Content-Type"] || request.headers["Content-Type"] == "application/x-www-form-urlencoded")){
waitForBody = whenPromise(waitForBody, function(body){
requestParams = requestParams.concat(uncombinedRequestParams);
body = (body ? body + "&" : "") + querystring.stringify(request.requestParams);
request.body = [body];
request.headers["Content-Type"] = "application/x-www-form-urlencoded";
});
}
// Sign the request and then actually make it.
return whenPromise(waitForBody, function(){
this._signRequest(request, requestParams);
return makeRequest(request);
}.bind(this));
};
Client.prototype._normalizeUrl = function(request){
var normalized = request.protocol + "//" + request.hostname;
if(request.protocol == "http:" && request.port && (request.port + "") != "80"){
normalized += ":" + request.port;
}
if(request.protocol == "https:" && request.port && (request.port + "") != "443"){
normalized += ":" + request.port;
}
return normalized + request.pathname;
};
Client.prototype._collectOAuthParams = function(request, requestParams){
var oauthParams = {};
if(request.oauthParams){
for(var p in request.oauthParams){
// Don't allow `request.oauthParams` to override standard values.
// `oauth_token` and `oauth_version` are conditionally added,
// the other parameters are always set. Hence we just test for
// the first two.
if(p != "oauth_token" && p != "oauth_version"){
oauthParams[p] = request.oauthParams[p];
}
}
}
oauthParams.oauth_consumer_key = this.identifier;
oauthParams.oauth_signature_method = this.signatureMethod;
oauthParams.oauth_timestamp = Client.getTimestamp();
oauthParams.oauth_nonce = this.generateNonce();
if(this.tokenIdentifier){
oauthParams.oauth_token = this.tokenIdentifier;
}
if(this.version){
oauthParams.oauth_version = this.version;
}
for(var i in oauthParams){
requestParams.push(i, oauthParams[i]);
}
return oauthParams;
};
Client.prototype._normalizeParams = function(requestParams){
// Encode requestParams
requestParams = requestParams.map(encodeRfc3986);
// Unflatten the requestParams for sorting
requestParams = requestParams.reduce(function(result, _, i, arr){
if(i % 2 == 0){
result.push(arr.slice(i, i + 2));
}
return result;
}, []);
// Sort the unflattened requestParams
requestParams.sort(function(a, b){
if(a[0] == b[0]){
return a[1] < b[1] ? -1 : 1;
}else{
return a[0] < b[0] ? -1 : 1;
}
});
return requestParams.map(function(pair){ return pair.join("="); }).join("&");
};
Client.prototype._createSignatureBase = function(requestMethod, baseUri, params){
return [requestMethod, baseUri, params].map(encodeRfc3986).join("&");
};
Client.prototype._signRequest = function(request, requestParams){
// Calculate base URI string
var baseUri = this._normalizeUrl(request);
// Register OAuth parameters and add to the request parameters
// Additional parameters can be specified via the `request.oauthParams` object
var oauthParams = this._collectOAuthParams(request, requestParams);
// Generate parameter string
var params = this._normalizeParams(requestParams);
// Sign the base string
var baseString = this._createSignatureBase(request.method, baseUri, params);
oauthParams.oauth_signature = this._createSignature(baseString);
// Add Authorization header
request.headers.authorization = "OAuth " + Object.keys(oauthParams).map(function(name){
return encodeRfc3986(name) + "=\"" + encodeRfc3986(oauthParams[name]) + "\"";
}).join(",");
// Now the request object can be used to make a signed request
return request;
};
Client.prototype.obtainTempCredentials = function(oauthParams, extraParams){
oauthParams = oauthParams || {};
if(this.callback && !oauthParams.oauth_callback){
oauthParams.oauth_callback = this.callback;
}
return this.request({
method: "POST",
url: this.tempRequestUrl,
oauthParams: oauthParams,
requestParams: extraParams || {}
}).then(parseResponse);
};
Client.prototype.obtainTokenCredentials = function(tokenIdentifier, tokenSecret, verifierToken, extraParams){
return this.bind(tokenIdentifier, tokenSecret).request({
method: "POST",
url: this.tokenRequestUrl,
oauthParams: { oauth_verifier: verifierToken },
requestParams: extraParams
}).then(parseResponse);
};