'use strict'; const compact = require('lodash/compact'); const extend = require('lodash/extend'); const isFunction = require('lodash/isFunction'); const once = require('lodash/once'); const partial = require('lodash/partial'); const JSONStream = require('JSONStream'); const JSONstringify = require('json-stringify-safe'); const uuid = require('uuid/v4'); const generateRequest = require('./generateRequest'); /** * @namespace */ const Utils = module.exports; // same reference as other files use, for tidyness const utils = Utils; Utils.request = generateRequest; /** * Generates a JSON-RPC 1.0 or 2.0 response * @param {Object} error Error member * @param {Object} result Result member * @param {String|Number|null} id Id of request * @param {Number} version JSON-RPC version to use * @return {Object} A JSON-RPC 1.0 or 2.0 response */ Utils.response = function(error, result, id, version) { id = typeof(id) === 'undefined' || id === null ? null : id; error = typeof(error) === 'undefined' || error === null ? null : error; version = typeof(version) === 'undefined' || version === null ? 2 : version; result = typeof(result) === 'undefined' || result === null ? null : result; const response = (version === 2) ? { jsonrpc: "2.0", id: id } : { id: id }; // errors are always included in version 1 if(version === 1) { response.error = error; } // one or the other with precedence for errors if(error) { response.error = error; } else { response.result = result; } return response; }; /** * Generates a random UUID * @return {String} */ Utils.generateId = function() { return uuid(); }; /** * Merges properties of object b into object a * @param {...Object} Objects to be merged * @return {Object} * @private */ Utils.merge = function() { return extend.apply(null, arguments); }; /** * Parses an incoming stream for requests using JSONStream * @param {Stream} stream * @param {Object} options * @param {Function} onRequest - Called once for stream errors and an unlimited amount of times for valid requests */ Utils.parseStream = function(stream, options, onRequest) { const onError = once(onRequest); const onSuccess = partial(onRequest, null); const result = JSONStream.parse(); result.on('data', function(data) { // apply reviver walk function to prevent stringify/parse again if(isFunction(options.reviver)) { data = Utils.walk({'': data}, '', options.reviver); } onSuccess(data); }); result.on('error', onError); stream.on('error', onError); stream.pipe(result); }; /** * Helper to parse a stream and interpret it as JSON * @param {Stream} stream Stream instance * @param {Function} [options] Optional options for JSON.parse * @param {Function} callback */ Utils.parseBody = function(stream, options, callback) { callback = once(callback); let data = ''; stream.setEncoding('utf8'); stream.on('data', function(str) { data += str; }); stream.on('error', function(err) { callback(err); }); stream.on('end', function() { utils.JSON.parse(data, options, function(err, request) { if(err) { return callback(err); } callback(null, request); }); }); }; /** * Returns a HTTP request listener bound to the server in the argument. * @param {http.Server} self Instance of a HTTP server * @param {JaysonServer} server Instance of JaysonServer (typically jayson.Server) * @return {Function} * @private */ Utils.getHttpListener = function(self, server) { return function(req, res) { const options = self.options || {}; server.emit('http request', req); // 405 method not allowed if not POST if(!Utils.isMethod(req, 'POST')) { return respond('Method Not Allowed', 405, {'allow': 'POST'}); } // 415 unsupported media type if Content-Type is not correct if(!Utils.isContentType(req, 'application/json')) { return respond('Unsupported Media Type', 415); } Utils.parseBody(req, options, function(err, request) { if(err) { return respond(err, 400); } server.call(request, function(error, success) { const response = error || success; if(!response) { // no response received at all, must be a notification return respond('', 204); } utils.JSON.stringify(response, options, function(err, body) { if(err) { return respond(err, 500); } const headers = { 'content-length': Buffer.byteLength(body, options.encoding), 'content-type': 'application/json; charset=utf-8' }; respond(body, 200, headers); }); }); }); function respond(response, code, headers) { const body = response instanceof Error ? response.toString() : response; server.emit('http response', res, req); res.writeHead(code, headers || {}); res.end(body); } }; }; /** * Determines if a HTTP Request comes with a specific Content-Type * @param {ServerRequest} request * @param {String} type * @return {Boolean} * @private */ Utils.isContentType = function(request, type) { request = request || {headers: {}}; const contentType = request.headers['content-type'] || ''; return RegExp(type, 'i').test(contentType); }; /** * Determines if a HTTP Request is of a specific method * @param {ServerRequest} request * @param {String} method * @return {Boolean} * @private */ Utils.isMethod = function(request, method) { method = (method || '').toUpperCase(); return (request.method || '') === method; }; /** * Recursively walk an object and apply a function on its members * @param {Object} holder The object to walk * @param {String} key The key to look at * @param {Function} fn The function to apply to members * @return {Object} */ Utils.walk = function(holder, key, fn) { let k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = Utils.walk(value, k, fn); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return fn.call(holder, key, value); }; /** * @namespace */ Utils.JSON = {}; /** * Parses a JSON string and then invokes the given callback * @param {String} str The string to parse * @param {Object} options Object with options, possibly holding a "reviver" function * @param {Function} callback */ Utils.JSON.parse = function(str, options, callback) { let reviver = null; let obj = null; options = options || {}; if(isFunction(options.reviver)) { reviver = options.reviver; } try { obj = JSON.parse.apply(JSON, compact([str, reviver])); } catch(err) { return callback(err); } callback(null, obj); }; /** * Stringifies JSON and then invokes the given callback * @param {Object} obj The object to stringify * @param {Object} options Object with options, possibly holding a "replacer" function * @param {Function} callback */ Utils.JSON.stringify = function(obj, options, callback) { let replacer = null; let str = null; options = options || {}; if(isFunction(options.replacer)) { replacer = options.replacer; } try { str = JSONstringify.apply(JSON, compact([obj, replacer])); } catch(err) { return callback(err); } callback(null, str); }; /** * @namespace */ Utils.Request = {}; /** * Determines if the passed request is a batch request * @param {Object} request The request * @return {Boolean} */ Utils.Request.isBatch = function(request) { return Array.isArray(request); }; /** * Determines if the passed request is a notification request * @param {Object} request The request * @return {Boolean} */ Utils.Request.isNotification = function(request) { return Boolean( request && !Utils.Request.isBatch(request) && (typeof(request.id) === 'undefined' || request.id === null) ); }; /** * Determines if the passed request is a valid JSON-RPC 2.0 Request * @param {Object} request The request * @return {Boolean} */ Utils.Request.isValidVersionTwoRequest = function(request) { return Boolean( request && typeof(request) === 'object' && request.jsonrpc === '2.0' && typeof(request.method) === 'string' && ( typeof(request.params) === 'undefined' || Array.isArray(request.params) || (request.params && typeof(request.params) === 'object') ) && ( typeof(request.id) === 'undefined' || typeof(request.id) === 'string' || typeof(request.id) === 'number' || request.id === null ) ); }; /** * Determines if the passed request is a valid JSON-RPC 1.0 Request * @param {Object} request The request * @return {Boolean} */ Utils.Request.isValidVersionOneRequest = function(request) { return Boolean( request && typeof(request) === 'object' && typeof(request.method) === 'string' && Array.isArray(request.params) && typeof(request.id) !== 'undefined' ); }; /** * Determines if the passed request is a valid JSON-RPC Request * @param {Object} request The request * @param {Number} [version=2] JSON-RPC version 1 or 2 * @return {Boolean} */ Utils.Request.isValidRequest = function(request, version) { version = version === 1 ? 1 : 2; return Boolean( request && ( (version === 1 && Utils.Request.isValidVersionOneRequest(request)) || (version === 2 && Utils.Request.isValidVersionTwoRequest(request)) ) ); }; /** * @namespace */ Utils.Response = {}; /** * Determines if the passed error is a valid JSON-RPC error response * @param {Object} error The error * @param {Number} [version=2] JSON-RPC version 1 or 2 * @return {Boolean} */ Utils.Response.isValidError = function(error, version) { version = version === 1 ? 1 : 2; return Boolean( version === 1 && ( typeof(error) !== 'undefined' && error !== null ) || version === 2 && ( error && typeof(error.code) === 'number' && parseInt(error.code, 10) === error.code && typeof(error.message) === 'string' ) ); }; /** * Determines if the passed object is a valid JSON-RPC response * @param {Object} response The response * @param {Number} [version=2] JSON-RPC version 1 or 2 * @return {Boolean} */ Utils.Response.isValidResponse = function(response, version) { version = version === 1 ? 1 : 2; return Boolean( response !== null && typeof response === 'object' && (version === 2 && ( // check version response.jsonrpc === '2.0' && ( // check id response.id === null || typeof response.id === 'string' || typeof response.id === 'number' ) && ( // result and error do not exist at the same time (typeof response.result === 'undefined' && typeof response.error !== 'undefined') || (typeof response.result !== 'undefined' && typeof response.error === 'undefined') ) && ( // check result (typeof response.result !== 'undefined') // check error object || ( response.error !== null && typeof response.error === 'object' && typeof response.error.code === 'number' // check error.code is integer && ((response.error.code | 0) === response.error.code) && typeof response.error.message === 'string' ) ) ) || version === 1 && ( typeof response.id !== 'undefined' && ( // result and error relation (the other null if one is not) (typeof response.result !== 'undefined' && response.error === null) || (typeof response.error !== 'undefined' && response.result === null) ) )) ); };