123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- 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);
- };
|