'use strict';
var util = require('util');
var http = require('http');
var parameters = require('./parameters');
var ApiHttpError = require('./errors').ApiHttpError;
var OAuthError = require('./errors').OAuthError;
var qs = require('querystring');
var _ = require('lodash');
var helpers = require('./helpers');
var oauthHelper = require('./oauth');
var USER_AGENT = 'Node.js HTTP Client';
Formats request parameters as expected by the API.
function prepare(data, consumerkey) {
var prop;
data = data || {};
for (prop in data) {
if (data.hasOwnProperty(prop)) {
if (_.isDate(data[prop])) {
data[prop] = helpers.toYYYYMMDD(data[prop]);
}
}
}
data.oauth_consumer_key = consumerkey;
return data;
}
Generates the default request headers
function createHeaders(host, headers) {
return _.extend({}, headers, {
'host': host,
'User-Agent' : USER_AGENT
});
}
Logs out a headers hash
function logHeaders(logger, headers) {
return _.each(_.keys(headers), function (key) {
logger.info(key + ': ' + headers[key]);
});
}
Makes a GET request to the API.
function get(endpointInfo, requestData, headers, credentials, logger,
callback) {
var normalisedData = prepare(requestData, credentials.consumerkey);
var fullUrl = endpointInfo.url + '?' + qs.stringify(normalisedData);
var hostInfo = {
host: endpointInfo.host,
port: endpointInfo.port
};
Decide whether to make an oauth signed request or not
if (endpointInfo.authtype) {
hostInfo.host = endpointInfo.sslHost;
dispatchSecure(endpointInfo.url, 'GET', requestData, headers,
endpointInfo.authtype, hostInfo, credentials, logger,
callback);
} else {
dispatch(endpointInfo.url, 'GET', requestData, headers, hostInfo,
credentials, logger, callback);
}
}
Makes a POST/PUT request to the API.
function postOrPut(httpMethod, endpointInfo, requestData, headers, credentials,
logger, callback) {
var hostInfo = {
host: endpointInfo.host,
port: endpointInfo.port
};
if (endpointInfo.authtype) {
hostInfo.host = endpointInfo.sslHost;
dispatchSecure(endpointInfo.url, httpMethod, requestData, headers,
endpointInfo.authtype, hostInfo, credentials, logger, callback);
} else {
dispatch(endpointInfo.url, httpMethod, requestData, headers, hostInfo,
credentials, logger, callback);
}
}
function buildSecureUrl(httpMethod, hostInfo, path, requestData) {
var querystring = httpMethod === 'GET'
? '?' + qs.stringify(requestData)
: '';
path = parameters.template(path, requestData);
return 'https://' + hostInfo.host + ':' + hostInfo.port + path + querystring;
}
Dispatches an oauth signed request to the API
function dispatchSecure(path, httpMethod, requestData, headers, authtype,
hostInfo, credentials, logger, callback) {
var url;
var is2Legged = authtype === '2-legged';
var token = is2Legged ? null : requestData.accesstoken;
var secret = is2Legged ? null : requestData.accesssecret;
var mergedHeaders = createHeaders(hostInfo.host, headers);
var oauthClient = oauthHelper.createOAuthWrapper(credentials.consumerkey,
credentials.consumersecret, mergedHeaders);
var methodLookup = {
'POST' : oauthClient.post.bind(oauthClient),
'PUT' : oauthClient.put.bind(oauthClient)
};
var oauthMethod = methodLookup[httpMethod];
hostInfo.port = hostInfo.port || 443;
requestData = prepare(requestData, credentials.consumerkey);
if (!is2Legged) {
delete requestData.accesstoken;
delete requestData.accesssecret;
}
url = buildSecureUrl(httpMethod, hostInfo, path, requestData);
logger.info('token: ' + token + ' secret: ' + secret);
logger.info(httpMethod + ': ' + url + ' (' + authtype + ' oauth)');
logHeaders(logger, mergedHeaders);
function cbWithDataAndResponse(err, data, response) {
var apiError;
if (err) {
API server error
if (err.statusCode && err.statusCode >= 400) {
non 200 status and string for response body is usually an oauth error from one of the endpoints
if (typeof err.data === 'string' && /oauth/i.test(err.data)) {
apiError = new OAuthError(err.data, err.data + ': ' +
path);
} else {
apiError = new ApiHttpError(
response.statusCode, err.data, path);
}
return callback(apiError);
}
Something unknown went wrong
logger.error(err);
return callback(err);
}
return callback(null, data, response);
}
if (httpMethod === 'GET') {
return oauthClient.get(url, token, secret, cbWithDataAndResponse);
}
if ( oauthMethod ) {
logger.info('DATA: ' + qs.stringify(requestData));
return oauthMethod(url, token, secret, requestData,
'application/x-www-form-urlencoded', cbWithDataAndResponse);
}
return callback(new Error('Unsupported HTTP verb: ' + httpMethod));
}
Dispatches requests to the API. Serializes the data in keeping with the API specification and applies approriate HTTP headers.
function dispatch(url, httpMethod, data, headers, hostInfo, credentials,
logger, callback) {
hostInfo.port = hostInfo.port || 80;
var apiRequest, prop, hasErrored;
var mergedHeaders = createHeaders(hostInfo.host, headers);
var apiPath = url;
data = prepare(data, credentials.consumerkey);
Special case for track previews: we explicitly request to be given the XML response back instead of a redirect to the track download.
if (url.indexOf('track/preview') >= 0) {
data.redirect = 'false';
}
if (httpMethod === 'GET') {
url = url + '?' + qs.stringify(data);
}
logger.info(util.format('%s: http://%s:%s%s', httpMethod,
hostInfo.host, hostInfo.port, url));
logHeaders(logger, mergedHeaders);
Make the request
apiRequest = http.request({
method: httpMethod,
disable connection pooling to get round node’s unnecessarily low 5 max connections
agent: false,
hostname: hostInfo.host,
Force scheme to http for browserify otherwise it will pick up the scheme from window.location.protocol which is app:// in firefoxos
scheme: 'http',
Set this so browserify doesn’t set it to true on the xhr, which causes an http status of 0 and empty response text as it forces the XHR to do a pre-flight access-control check and the API currently does not set CORS headers.
withCredentials: false,
path: url,
port: hostInfo.port,
headers: mergedHeaders
}, function handleResponse(response) {
var responseBuffer = '';
if (typeof response.setEncoding === 'function') {
response.setEncoding('utf8');
}
response.on('data', function bufferData(chunk) {
responseBuffer += chunk;
});
response.on('end', function endResponse() {
if (+response.statusCode >= 400) {
return callback(new ApiHttpError(
response.statusCode, responseBuffer, apiPath));
}
if (!hasErrored) {
return callback(null, responseBuffer, response);
}
});
});
apiRequest.on('error', function logErrorAndCallback(data) {
Flag that we’ve errored so we don’t call the callback twice if we get an end event on the response.
hasErrored = true;
logger.info('Error fetching [' + url + ']. Body:\n' + data);
return callback(new Error('Error connecting to ' + url));
});
if (httpMethod === 'GET') {
apiRequest.end();
} else {
apiRequest.end(data);
}
}
module.exports.get = get;
module.exports.postOrPut = postOrPut;
module.exports.createHeaders = createHeaders;
module.exports.prepare = prepare;
module.exports.dispatch = dispatch;
module.exports.dispatchSecure = dispatchSecure;