UNPKG

okanjo

Version:

Integrate your application with the Okanjo API.

358 lines (305 loc) 11.3 kB
/* * Date: 1/27/16 3:49 PM * * ---- * * (c) Okanjo Partners Inc * https://okanjo.com * support@okanjo.com * * https://github.com/okanjo/okanjo-nodejs * * ---- * * TL;DR? see: http://www.tldrlegal.com/license/mit-license * * The MIT License (MIT) * Copyright (c) 2013 Okanjo Partners Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ var util = require('util'), Provider = require('../provider'), modInfo = require('../../package.json'); /** * Transmits requests over HTTP or HTTPS directly to the Okanjo API * @param {Client} client * @constructor */ function HttpProvider(client) { Provider.call(this, client); const defaults = { api: { protocol: 'https', host: 'api2.okanjo.com', port: 443, }, farm: { protocol: 'https', host: 'farm.okanjo.com', port: 443, }, shortcodes: { protocol: 'https', host: 'shortcodes.okanjo.com', port: 443, }, sso: { protocol: 'https', host: 'okanjo.com', port: 443, } }; // Set defaults and apply config overrides this.apis = {}; Object.keys(defaults).forEach(key => { this.apis[key] = Object.assign({}, defaults[key]); Object.assign(this.apis[key], client.config[key] || {}); this.apis[key].protocolName = this.apis[key].protocol; this.apis[key].protocol = require(this.apis[key].protocol); }); /** * How long to wait for a request to complete before giving up * @type {number} */ this.timeout = client.config.timeout || 30000; /** * Which user agent to identify as * @type {string} */ this.userAgent = client.config.userAgent || ("okanjo-nodejs/" + modInfo.version); /** * When connecting over SSL, limit the ciphers to accept * @type {string} */ this.ciphers = 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:!MD5'; } util.inherits(HttpProvider, Provider); /** * Returns the value for the Authorization header * @param {string} key - API key * @param {string} token - Session Token * @return {string} - Encoded authorization header * @private */ HttpProvider.prototype._getAuthorization = function(key, token) { return 'Basic ' + Buffer.from(key + ":" + (token ? token : "")).toString('base64'); }; /** * Handles when a request fails to complete within the expected time frame * @param {object} req * @param {function} callback * @private */ HttpProvider.prototype._handleRequestTimeout = function(req, callback) { callback({ statusCode: 504, error: "ETIMEDOUT", message: "API request took too long to complete", attributes: { source: new Error('ETIMEDOUT') } }, null, req); }; /** * Handles processing the response received from the server * @param {object} req * @param {function} callback * @param {Query} query * @param {object} res * @private */ HttpProvider.prototype._handleRequestResponse = function(req, callback, query, res) { // Bucket to throw all the chunks into, and friends var chunks = [], payload, headers; // We expect UTF-8 from the server. res.setEncoding('utf8'); // Push chunks to the stack res.on('data', function(chunk) { chunks.push(chunk+""); }); // Process the request res.on('end', function() { // Extract / default the headers if none received for some awful reason /* istanbul ignore next: what web server would spit back no headers? I mean really... */ headers = res.headers || {}; // Glue the payload together payload = chunks.join(''); var err, data; // Verify the response was JSON if (headers['content-type'] && headers['content-type'].indexOf('application/json') === 0) { try { err = null; data = JSON.parse(payload); var isResponseError = res.statusCode >= 400, // Server status range isPayloadError = data && data.statusCode && data.statusCode >= 400; // Payload status range // Swap data with error if the response failed if (isResponseError || isPayloadError) { err = data; // Copy the payload status if different if (data.statusCode !== res.statusCode) { if (isPayloadError) { err.responseStatusCode = res.statusCode; } else { err.payloadStatusCode = data.statusCode; err.statusCode = res.statusCode; } } // Strip the data, since we moved it to the error slot data = null; // Check for unauthorized response if (err.statusCode === 401) this._unauthorizedHook(err, query); } } catch (error) { err = { statusCode: 500, error: "Response parsing error", message: "Failed to parse response as JSON", data: payload, attributes: { source: error } }; } } else if (headers['content-type'] && headers['content-type'].indexOf('text/csv') === 0) { // API will not return CSV content with non-200 status code // Wrap the output for consistency with all other routes data = { statusCode: res.statusCode, error: null, data: payload, }; } else { // No idea what to do with this so wrap it up err = { statusCode: res.statusCode, error: res.statusMessage || /* istanbul ignore next: super edge case */ "Invalid Response Received", message: "Response content type was expected to be `application/json` or `text/csv` but was actually `" + headers['content-type'] + "`", data: payload, attributes: { source: new Error('Invalid response received') } }; } // Send the response callback(err, data, req); }.bind(this)); }; /** * Handles when an error occurs when making a request * @param {object} req * @param {function} callback * @param {Error} error - The error that occurred * @private */ HttpProvider.prototype._handleRequestError = function(req, callback, error) { callback({ statusCode: 503, error: error.message, message: "Something went wrong", attributes: { source: error } }, null, req); }; /** * Executes the query over HTTP * @param {Query} query - The query to execute * @param {requestCallback} [callback] – Callback to fire when request is completed */ HttpProvider.prototype.execute = function(query, callback) { return new Promise((resolve, reject) => { // Prevent duplicate callbacks if multiple failures occur let replied = false; const done = function(err, res) { if (!replied) { replied = true; if (err) { if (callback) { return callback(err, null); } else { return reject(err); } } else { if (callback) { return callback(null, res); } else { return resolve(res); } } } }; // Initialize headers var headers = Object.assign({}, query.headers); headers['User-Agent'] = this.userAgent; // Set cookies var cookies = Object.keys(query.cookies).map(key => { return key + '=' + query.cookies[key]; }).join(', '); if (cookies.length > 0) { headers.Cookie = cookies; } // Build authorization var key = query.key || this.client.config.key || "", sessionToken = query.sessionToken || this.client.config.sessionToken || ""; // Attach authorization (if any) to the request headers if (key || sessionToken) headers.Authorization = this._getAuthorization(key, sessionToken); // Just in case we throw before req is set var req = {}; var api = this.apis[query.api]; if (!api) { return this._handleRequestError(query, done, new Error('Unknown API: '+query.api )); } var path = query.getFullPath(); if (path instanceof Error) { return this._handleRequestError(query, done, path); } // Add host header headers.Host = api.host; const ciphers = this.ciphers; try { // Fire off the request! req = api.protocol.request({ host: api.host, port: api.port, ciphers: ciphers, method: query.method, path: path, headers: headers }); req.setTimeout(this.timeout, this._handleRequestTimeout.bind(this, req, done)); req.on('response', this._handleRequestResponse.bind(this, req, done, query)); req.on('error', this._handleRequestError.bind(this, req, done)); req.on('socket', function(socket) { socket.on(api.protocolName === 'https' ? 'secureConnect' : 'connect', function() { if (query.payload) { req.write(JSON.stringify(query.payload)); } req.end(); }); }.bind(this)); } catch (err) { /* istanbul ignore next: out of scope */ // Request blew up before it even got off the ground this._handleRequestError(req, done, err); } }); }; module.exports = HttpProvider;