UNPKG

9.56 kBJavaScriptView Raw
1'use strict';
2
3var util = require('util');
4var http = require('http');
5var parameters = require('./parameters');
6var ApiHttpError = require('./errors').ApiHttpError;
7var OAuthError = require('./errors').OAuthError;
8var qs = require('querystring');
9var _ = require('lodash');
10var helpers = require('./helpers');
11var oauthHelper = require('./oauth');
12var USER_AGENT = 'Node.js HTTP Client';
13
14// Formats request parameters as expected by the API.
15//
16// - @param {Object} data - hash of pararameters
17// - @param {String} consumerkey - consumer key
18// - @return {String} - Encoded parameter string
19function prepare(data, consumerkey) {
20 var prop;
21 data = data || {};
22
23 for (prop in data) {
24 if (data.hasOwnProperty(prop)) {
25 if (_.isDate(data[prop])) {
26 data[prop] = helpers.toYYYYMMDD(data[prop]);
27 }
28 }
29 }
30
31 data.oauth_consumer_key = consumerkey;
32
33 return data;
34}
35
36// Generates the default request headers
37//
38// - @return {Object}
39function createHeaders(host, headers) {
40 return _.extend({}, headers, {
41 'host': host,
42 'User-Agent' : USER_AGENT
43 });
44}
45
46// Logs out a headers hash
47//
48// - @param {Object} logger - The logger to use
49// - @param {Object} headers - The hash of headers to log
50function logHeaders(logger, headers) {
51 return _.each(_.keys(headers), function (key) {
52 logger.info(key + ': ' + headers[key]);
53 });
54}
55
56// Makes a GET request to the API.
57//
58// - @param {Object} endpointInfo - Generic metadata about the endpoint to hit.
59// - @param {Object} requestData - Parameters for this specific request.
60// - @param {Object} headers - Custom headers for this request.
61// - @param {Object} credentials - OAuth consumerkey and consumersecret.
62// - @param {Object} logger - An object implementing the npm log levels.
63// - @param {Function} callback - The callback to call with the response.
64function get(endpointInfo, requestData, headers, credentials, logger,
65 callback) {
66
67 var normalisedData = prepare(requestData, credentials.consumerkey);
68 var fullUrl = endpointInfo.url + '?' + qs.stringify(normalisedData);
69 var hostInfo = {
70 host: endpointInfo.host,
71 port: endpointInfo.port
72 };
73
74 // Decide whether to make an oauth signed request or not
75 if (endpointInfo.authtype) {
76 hostInfo.host = endpointInfo.sslHost;
77 dispatchSecure(endpointInfo.url, 'GET', requestData, headers,
78 endpointInfo.authtype, hostInfo, credentials, logger,
79 callback);
80 } else {
81 dispatch(endpointInfo.url, 'GET', requestData, headers, hostInfo,
82 credentials, logger, callback);
83 }
84}
85
86// Makes a POST/PUT request to the API.
87//
88// - @param {String} httpMethod - POST or PUT.
89// - @param {Object} endpointInfo - Generic metadata about the endpoint to hit.
90// - @param {Object} requestData - Parameters for this specific request.
91// - @param {Object} headers - Headers for this request.
92// - @param {Object} credentials - OAuth consumerkey and consumersecret.
93// - @param {Object} logger - An object implementing the npm log levels.
94// - @param {Function} callback - The callback to call with the response.
95function postOrPut(httpMethod, endpointInfo, requestData, headers, credentials,
96 logger, callback) {
97
98 var hostInfo = {
99 host: endpointInfo.host,
100 port: endpointInfo.port
101 };
102
103 if (endpointInfo.authtype) {
104 hostInfo.host = endpointInfo.sslHost;
105 dispatchSecure(endpointInfo.url, httpMethod, requestData, headers,
106 endpointInfo.authtype, hostInfo, credentials, logger, callback);
107 } else {
108 dispatch(endpointInfo.url, httpMethod, requestData, headers, hostInfo,
109 credentials, logger, callback);
110 }
111}
112
113function buildSecureUrl(httpMethod, hostInfo, path, requestData) {
114 var querystring = httpMethod === 'GET'
115 ? '?' + qs.stringify(requestData)
116 : '';
117 path = parameters.template(path, requestData);
118 return 'https://' + hostInfo.host + ':' + hostInfo.port + path + querystring;
119}
120
121// Dispatches an oauth signed request to the API
122//
123// - @param {String} url - the path of the API url to request.
124// - @param {String} httpMethod
125// - @param {Object} requestData - hash of the parameters for the request.
126// - @param {Object} headers - Headers for this request.
127// - @param {String} authType - OAuth request type: '2-legged' or '3-legged'
128// - @param {Object} hostInfo - API host information
129// - @param {Function} callback - The callback to call with the response.
130function dispatchSecure(path, httpMethod, requestData, headers, authtype,
131 hostInfo, credentials, logger, callback) {
132 var url;
133 var is2Legged = authtype === '2-legged';
134 var token = is2Legged ? null : requestData.accesstoken;
135 var secret = is2Legged ? null : requestData.accesssecret;
136 var mergedHeaders = createHeaders(hostInfo.host, headers);
137 var oauthClient = oauthHelper.createOAuthWrapper(credentials.consumerkey,
138 credentials.consumersecret, mergedHeaders);
139 var methodLookup = {
140 'POST' : oauthClient.post.bind(oauthClient),
141 'PUT' : oauthClient.put.bind(oauthClient)
142 };
143 var oauthMethod = methodLookup[httpMethod];
144
145 hostInfo.port = hostInfo.port || 443;
146
147 requestData = prepare(requestData, credentials.consumerkey);
148
149 if (!is2Legged) {
150 delete requestData.accesstoken;
151 delete requestData.accesssecret;
152 }
153
154 url = buildSecureUrl(httpMethod, hostInfo, path, requestData);
155
156 logger.info('token: ' + token + ' secret: ' + secret);
157 logger.info(httpMethod + ': ' + url + ' (' + authtype + ' oauth)');
158 logHeaders(logger, mergedHeaders);
159
160 function cbWithDataAndResponse(err, data, response) {
161 var apiError;
162
163 if (err) {
164 // API server error
165 if (err.statusCode && err.statusCode >= 400) {
166 // non 200 status and string for response body is usually an
167 // oauth error from one of the endpoints
168 if (typeof err.data === 'string' && /oauth/i.test(err.data)) {
169 apiError = new OAuthError(err.data, err.data + ': ' +
170 path);
171 } else {
172 apiError = new ApiHttpError(
173 response.statusCode, err.data, path);
174 }
175
176 return callback(apiError);
177 }
178
179 // Something unknown went wrong
180 logger.error(err);
181 return callback(err);
182 }
183
184 return callback(null, data, response);
185 }
186
187 if (httpMethod === 'GET') {
188 return oauthClient.get(url, token, secret, cbWithDataAndResponse);
189 }
190
191 if ( oauthMethod ) {
192 logger.info('DATA: ' + qs.stringify(requestData));
193 return oauthMethod(url, token, secret, requestData,
194 'application/x-www-form-urlencoded', cbWithDataAndResponse);
195 }
196
197 return callback(new Error('Unsupported HTTP verb: ' + httpMethod));
198}
199
200// Dispatches requests to the API. Serializes the data in keeping with the API
201// specification and applies approriate HTTP headers.
202//
203// - @param {String} url - the URL on the API to make the request to.
204// - @param {String} httpMethod
205// - @param {Object} data - hash of the parameters for the request.
206// - @param {Object} headers - Headers for this request.
207// - @param {Object} hostInfo - hash of host, port and prefix
208// - @param {Object} credentials - hash of oauth consumer key and secret
209// - @param {Object} logger - an object implementing the npm log levels
210// - @param {Function} callback
211function dispatch(url, httpMethod, data, headers, hostInfo, credentials,
212 logger, callback) {
213
214 hostInfo.port = hostInfo.port || 80;
215
216 var apiRequest, prop, hasErrored;
217 var mergedHeaders = createHeaders(hostInfo.host, headers);
218 var apiPath = url;
219
220 data = prepare(data, credentials.consumerkey);
221
222 // Special case for track previews: we explicitly request to be given
223 // the XML response back instead of a redirect to the track download.
224 if (url.indexOf('track/preview') >= 0) {
225 data.redirect = 'false';
226 }
227
228 if (httpMethod === 'GET') {
229 url = url + '?' + qs.stringify(data);
230 }
231
232 logger.info(util.format('%s: http://%s:%s%s', httpMethod,
233 hostInfo.host, hostInfo.port, url));
234 logHeaders(logger, mergedHeaders);
235
236 // Make the request
237 apiRequest = http.request({
238 method: httpMethod,
239 // disable connection pooling to get round node's unnecessarily low
240 // 5 max connections
241 agent: false,
242 hostname: hostInfo.host,
243 // Force scheme to http for browserify otherwise it will pick up the
244 // scheme from window.location.protocol which is app:// in firefoxos
245 scheme: 'http',
246 // Set this so browserify doesn't set it to true on the xhr, which
247 // causes an http status of 0 and empty response text as it forces
248 // the XHR to do a pre-flight access-control check and the API
249 // currently does not set CORS headers.
250 withCredentials: false,
251 path: url,
252 port: hostInfo.port,
253 headers: mergedHeaders
254 }, function handleResponse(response) {
255 var responseBuffer = '';
256
257 if (typeof response.setEncoding === 'function') {
258 response.setEncoding('utf8');
259 }
260
261 response.on('data', function bufferData(chunk) {
262 responseBuffer += chunk;
263 });
264
265 response.on('end', function endResponse() {
266 if (+response.statusCode >= 400) {
267 return callback(new ApiHttpError(
268 response.statusCode, responseBuffer, apiPath));
269 }
270
271 if (!hasErrored) {
272 return callback(null, responseBuffer, response);
273 }
274 });
275 });
276
277 apiRequest.on('error', function logErrorAndCallback(data) {
278 // Flag that we've errored so we don't call the callback twice
279 // if we get an end event on the response.
280 hasErrored = true;
281 logger.info('Error fetching [' + url + ']. Body:\n' + data);
282
283 return callback(new Error('Error connecting to ' + url));
284 });
285
286 if (httpMethod === 'GET') {
287 apiRequest.end();
288 } else {
289 apiRequest.end(data);
290 }
291}
292
293module.exports.get = get;
294module.exports.postOrPut = postOrPut;
295module.exports.createHeaders = createHeaders;
296module.exports.prepare = prepare;
297module.exports.dispatch = dispatch;
298module.exports.dispatchSecure = dispatchSecure;