@madisonreed/amazon-payments
Version:
API wrapper for Amazon Payments
376 lines (307 loc) • 8.98 kB
JavaScript
var request = require('request');
var crypto = require('crypto');
var URL = require('url');
var xml2js = require('xml2js');
var error = require('./error');
var offAmazonPayments = require('./offAmazonPayments').offAmazonPayments;
var reports = require('./reports').reports;
var api = require('./api').api;
exports.amazon = Amazon;
// exposing these methods so they can be tested
exports.composeParams = composeParams;
exports.attachSignature = attachSignature;
exports.parseSNSResponse = parseSNSResponse;
function Amazon(config) {
this.config = config;
this.debug = config.debug;
this.callApiMethod = callApiMethod;
this.callMwsMethod = callMwsMethod;
this.offAmazonPayments = new offAmazonPayments(this);
this.reports = new reports(this);
this.parseSNSResponse = parseSNSResponse;
this.api = new api(this);
}
function callApiMethod(action, params, bearer, callback) {
if (arguments.length == 3) {
callback = bearer;
bearer = false;
}
var self = this;
var opts = {
url: self.config.environment.apiEndpoint + '/' + action,
method: 'get'
};
if (params) {
opts.qs = params;
}
if (bearer) {
opts.headers = {
'Authorization': 'bearer ' + bearer
};
}
request(opts, function(err, res, body) {
if (err) {
return callback(err);
}
var response = parseApiResponse(body);
if (response instanceof Error) {
return callback(response);
} else {
return callback(null, response);
}
});
}
function callMwsMethod(method, version, params, callback) {
var self = this;
var url = self.config.environment.mwsEndpoint;
var required = {
AWSAccessKeyId: self.config.mwsAccessKey,
Action: method,
SellerId: self.config.sellerId,
Timestamp: getFormattedTimestamp(),
Version: version
};
params = composeParams(params);
for (var k in required) {
if (!params.hasOwnProperty(k)) {
params[k] = required[k];
}
}
params = attachSignature(url, self.config.mwsSecretKey, params);
var opts = {
url: url,
method: 'post',
form: params
};
request(opts, function(err, res, body) {
if (err) {
return callback(err);
}
parseMwsResponse(method, res.headers, body, callback);
});
}
function composeParams(params, label, composed) {
composed = safeObjectCast(composed);
params = safeObjectCast(params);
Object.keys(params).forEach(function(key) {
var value = params[key];
var newLabel = label ? label + "." + key : key;
if (isObject(value)) {
composeParams(value, newLabel, composed);
} else {
composed[newLabel] = value;
}
});
return composed;
}
function attachSignature(url, secret, params) {
var method = 'HmacSHA256';
params.SignatureMethod = 'HmacSHA256';
params.SignatureVersion = '2';
var sortedParams = Object.keys(params).sort(function(a, b) {
return a == b ? 0 : a < b ? -1 : 1;
}).map(function(key) {
return RFC3986Encode(key) + '=' + RFC3986Encode(params[key]);
}).join('&');
var parsedUrl = URL.parse(url);
var hmac = crypto.createHmac('SHA256', secret);
var stringToSign = [
'POST',
parsedUrl.hostname,
(parsedUrl.pathname || '/'),
sortedParams,
].join("\n");
hmac.update(stringToSign);
params.Signature = hmac.digest('base64');
return params;
}
function getFormattedTimestamp(date) {
date = date || new Date();
var year = date.getUTCFullYear();
var month = padNum(date.getUTCMonth() + 1);
var day = padNum(date.getUTCDate());
var hour = padNum(date.getUTCHours());
var minute = padNum(date.getUTCMinutes());
var second = padNum(date.getUTCSeconds());
return year + '-' + month + '-' + day + 'T' + hour + ':' + minute + ':' + second + 'z';
}
function RFC3986Encode(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, function(c) {
return '%' + c.charCodeAt(0).toString(16);
});
}
function padNum(num, size, str) {
switch (arguments.length) {
case 1:
size = 2;
case 2:
str = '0';
}
num = '' + num;
while (num.length < size) {
num = str + num;
}
return num;
}
/**
* Parse the MWS response.
*
* @param {string} method
* @param {Object[]} headers
* @param {string} response
* @param {function} callback
*/
function parseMwsResponse(method, headers, response, callback) {
// if it's XML, then we an parse correctly
if (headers && headers['content-type'] == 'text/xml') {
xml2js.parseString(response, {explicitArray: false}, function(err, result) {
if (err) {
return callback(err);
}
if (result.ErrorResponse) {
err = {
Code: 'Unknown',
Message: 'Unknown MWS error'
};
if (result.ErrorResponse.Error) {
err = result.ErrorResponse.Error;
}
return callback(error.apiError(err.Code, err.Message, result));
} else {
callback(null, new Response(method, result));
}
});
} else {
callback(null, new Response(method, { "Response": response }));
}
}
function parseApiResponse(response) {
var parsed;
try {
parsed = JSON.parse(response);
} catch(e) {
return error.parseError("Could not parse Amazon response.", response);
}
if (parsed.error) {
return error.apiError(parsed.error, parsed.error_description, parsed);
} else {
return parsed;
}
}
function parseSNSResponse(response, callback) {
var defaultHostPattern = /^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$/;
var required = [
'Message',
'MessageId',
'SignatureVersion',
'Signature',
'SigningCertURL',
'Timestamp',
'TopicArn',
'Type'
];
var signable = [
'Message',
'MessageId',
'Subject',
'SubscribeURL',
'Timestamp',
'Token',
'TopicArn',
'Type',
];
for (var i = 0; i < required.length; i ++) {
if (!response.hasOwnProperty(required[i])) {
return callback(error.missingParameter('Missing parameter on SNS response: ' + required[i]));
}
}
if (response.SignatureVersion != 1) {
return callback(error.invalidSignatureVersion('Unknown SNS Signature version: ' + response.SignatureVersion));
}
var verifier = crypto.createVerify('SHA1');
signable.forEach(function(key) {
if (response.hasOwnProperty(key)) {
verifier.update(key + '\n' + response[key] + '\n');
}
});
var parsed = URL.parse(response.SigningCertURL);
if (parsed.protocol !== 'https:' || parsed.path.substr(-4) !== '.pem' || !defaultHostPattern.test(parsed.host)) {
return callback(error.invalidCertificateDomain('The certificate is located on an invalid domain.'));
}
request(response.SigningCertURL, function(err, res, cert) {
if (err) {
return callback(err);
}
var isValid = verifier.verify(cert, response.Signature, 'base64');
if (!isValid) {
return callback(error.signatureMismatch('Signature mismatch, unverified response'));
}
if (response.Type != 'Notification') {
return callback(null, response);
}
parseIPNMessage(response.Message, function (err, message) {
if (err) {
return callback(err);
}
callback(null, message);
});
});
}
function parseIPNMessage(message, callback) {
message = safeJSONParse(message);
if (!isObject(message) || !message.NotificationData) {
return callback(null, message);
}
var type = message.NotificationType;
var xmlKeys = {
PaymentRefund: ['RefundNotification', 'RefundDetails'],
PaymentCapture: ['CaptureNotification', 'CaptureDetails'],
PaymentAuthorize: ['AuthorizationNotification', 'AuthorizationDetails'],
OrderReferenceNotification: ['OrderReferenceNotification', 'OrderReference'],
BillingAgreementNotification: ['BillingAgreementNotification', 'BillingAgreement']
};
xml2js.parseString(message.NotificationData, {explicitArray: false}, function(err, result) {
if (err) {
return callback(err);
}
var keys = xmlKeys[type] || [];
message.NotificationData = new Response(type, result, keys[0], keys[1]);
callback(null, message);
});
}
function Response(method, rawResponse, primaryKey, subKey) {
primaryKey = primaryKey || method + 'Response';
subKey = subKey || method + 'Result';
if (!rawResponse.hasOwnProperty(primaryKey)) {
return rawResponse;
}
var _response = rawResponse[primaryKey];
var _result = _response[subKey];
if (_response.ResponseMetadata) {
Object.defineProperty(this, 'requestId', {
enumerable: false,
get: function () {
return _response.ResponseMetadata.RequestId;
}
});
}
return _result;
}
function safeJSONParse(data) {
var parsed;
try {
parsed = JSON.parse(data);
} catch(e) {
parsed = data;
}
return parsed;
}
function isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
function safeObjectCast(obj) {
if (!isObject(obj)) {
return {};
}
return obj;
}