UNPKG

11.8 kBJavaScriptView Raw
1'use strict';
2
3const compact = require('lodash/compact');
4const extend = require('lodash/extend');
5const isFunction = require('lodash/isFunction');
6const once = require('lodash/once');
7const partial = require('lodash/partial');
8const JSONStream = require('JSONStream');
9const JSONstringify = require('json-stringify-safe');
10const uuid = require('uuid/v4');
11
12const generateRequest = require('./generateRequest');
13
14/** * @namespace */
15const Utils = module.exports;
16
17// same reference as other files use, for tidyness
18const utils = Utils;
19
20Utils.request = generateRequest;
21
22/**
23 * Generates a JSON-RPC 1.0 or 2.0 response
24 * @param {Object} error Error member
25 * @param {Object} result Result member
26 * @param {String|Number|null} id Id of request
27 * @param {Number} version JSON-RPC version to use
28 * @return {Object} A JSON-RPC 1.0 or 2.0 response
29 */
30Utils.response = function(error, result, id, version) {
31 id = typeof(id) === 'undefined' || id === null ? null : id;
32 error = typeof(error) === 'undefined' || error === null ? null : error;
33 version = typeof(version) === 'undefined' || version === null ? 2 : version;
34 const response = (version === 2) ? { jsonrpc: "2.0", id: id } : { id: id };
35
36 // errors are always included in version 1
37 if(version === 1) {
38 response.error = error;
39 }
40
41 // one or the other with precedence for errors
42 if(error) {
43 response.error = error;
44 } else {
45 response.result = result;
46 }
47 return response;
48};
49
50/**
51 * Generates a random UUID
52 * @return {String}
53 */
54Utils.generateId = function() {
55 return uuid();
56};
57
58/**
59 * Merges properties of object b into object a
60 * @param {...Object} Objects to be merged
61 * @return {Object}
62 * @private
63 */
64Utils.merge = function() {
65 return extend.apply(null, arguments);
66};
67
68/**
69 * Parses an incoming stream for requests using JSONStream
70 * @param {Stream} stream
71 * @param {Object} options
72 * @param {Function} onRequest - Called once for stream errors and an unlimited amount of times for valid requests
73 */
74Utils.parseStream = function(stream, options, onRequest) {
75
76 const onError = once(onRequest);
77 const onSuccess = partial(onRequest, null);
78
79 const result = JSONStream.parse();
80
81 result.on('data', function(data) {
82
83 // apply reviver walk function to prevent stringify/parse again
84 if(isFunction(options.reviver)) {
85 data = Utils.walk({'': data}, '', options.reviver);
86 }
87
88 onSuccess(data);
89 });
90
91 result.on('error', onError);
92 stream.on('error', onError);
93
94 stream.pipe(result);
95
96};
97
98/**
99 * Helper to parse a stream and interpret it as JSON
100 * @param {Stream} stream Stream instance
101 * @param {Function} [options] Optional options for JSON.parse
102 * @param {Function} callback
103 */
104Utils.parseBody = function(stream, options, callback) {
105
106 callback = once(callback);
107 let data = '';
108
109 stream.setEncoding('utf8');
110
111 stream.on('data', function(str) {
112 data += str;
113 });
114
115 stream.on('error', function(err) {
116 callback(err);
117 });
118
119 stream.on('end', function() {
120 utils.JSON.parse(data, options, function(err, request) {
121 if(err) {
122 return callback(err);
123 }
124 callback(null, request);
125 });
126 });
127
128};
129
130/**
131 * Returns a HTTP request listener bound to the server in the argument.
132 * @param {http.Server} self Instance of a HTTP server
133 * @param {JaysonServer} server Instance of JaysonServer (typically jayson.Server)
134 * @return {Function}
135 * @private
136 */
137Utils.getHttpListener = function(self, server) {
138 return function(req, res) {
139 const options = self.options || {};
140
141 server.emit('http request', req);
142
143 // 405 method not allowed if not POST
144 if(!Utils.isMethod(req, 'POST')) {
145 return respond('Method Not Allowed', 405, {'allow': 'POST'});
146 }
147
148 // 415 unsupported media type if Content-Type is not correct
149 if(!Utils.isContentType(req, 'application/json')) {
150 return respond('Unsupported Media Type', 415);
151 }
152
153 Utils.parseBody(req, options, function(err, request) {
154 if(err) {
155 return respond(err, 400);
156 }
157
158 server.call(request, function(error, success) {
159 const response = error || success;
160 if(!response) {
161 // no response received at all, must be a notification
162 return respond('', 204);
163 }
164
165 utils.JSON.stringify(response, options, function(err, body) {
166 if(err) {
167 return respond(err, 500);
168 }
169
170 const headers = {
171 'content-length': Buffer.byteLength(body, options.encoding),
172 'content-type': 'application/json; charset=utf-8'
173 };
174
175 respond(body, 200, headers);
176 });
177
178 });
179
180 });
181
182 function respond(response, code, headers) {
183 const body = response instanceof Error ? response.toString() : response;
184 server.emit('http response', res, req);
185 res.writeHead(code, headers || {});
186 res.end(body);
187 }
188
189 };
190};
191
192/**
193 * Determines if a HTTP Request comes with a specific Content-Type
194 * @param {ServerRequest} request
195 * @param {String} type
196 * @return {Boolean}
197 * @private
198 */
199Utils.isContentType = function(request, type) {
200 request = request || {headers: {}};
201 const contentType = request.headers['content-type'] || '';
202 return RegExp(type, 'i').test(contentType);
203};
204
205/**
206 * Determines if a HTTP Request is of a specific method
207 * @param {ServerRequest} request
208 * @param {String} method
209 * @return {Boolean}
210 * @private
211 */
212Utils.isMethod = function(request, method) {
213 method = (method || '').toUpperCase();
214 return (request.method || '') === method;
215};
216
217/**
218 * Recursively walk an object and apply a function on its members
219 * @param {Object} holder The object to walk
220 * @param {String} key The key to look at
221 * @param {Function} fn The function to apply to members
222 * @return {Object}
223 */
224Utils.walk = function(holder, key, fn) {
225 let k, v, value = holder[key];
226 if (value && typeof value === 'object') {
227 for (k in value) {
228 if (Object.prototype.hasOwnProperty.call(value, k)) {
229 v = Utils.walk(value, k, fn);
230 if (v !== undefined) {
231 value[k] = v;
232 } else {
233 delete value[k];
234 }
235 }
236 }
237 }
238 return fn.call(holder, key, value);
239};
240
241/** * @namespace */
242Utils.JSON = {};
243
244/**
245 * Parses a JSON string and then invokes the given callback
246 * @param {String} str The string to parse
247 * @param {Object} options Object with options, possibly holding a "reviver" function
248 * @param {Function} callback
249 */
250Utils.JSON.parse = function(str, options, callback) {
251 let reviver = null;
252 let obj = null;
253 options = options || {};
254
255 if(isFunction(options.reviver)) {
256 reviver = options.reviver;
257 }
258
259 try {
260 obj = JSON.parse.apply(JSON, compact([str, reviver]));
261 } catch(err) {
262 return callback(err);
263 }
264
265 callback(null, obj);
266};
267
268/**
269 * Stringifies JSON and then invokes the given callback
270 * @param {Object} obj The object to stringify
271 * @param {Object} options Object with options, possibly holding a "replacer" function
272 * @param {Function} callback
273 */
274Utils.JSON.stringify = function(obj, options, callback) {
275 let replacer = null;
276 let str = null;
277 options = options || {};
278
279 if(isFunction(options.replacer)) {
280 replacer = options.replacer;
281 }
282
283 try {
284 str = JSONstringify.apply(JSON, compact([obj, replacer]));
285 } catch(err) {
286 return callback(err);
287 }
288
289 callback(null, str);
290};
291
292/** * @namespace */
293Utils.Request = {};
294
295/**
296 * Determines if the passed request is a batch request
297 * @param {Object} request The request
298 * @return {Boolean}
299 */
300Utils.Request.isBatch = function(request) {
301 return Array.isArray(request);
302};
303
304/**
305 * Determines if the passed request is a notification request
306 * @param {Object} request The request
307 * @return {Boolean}
308 */
309Utils.Request.isNotification = function(request) {
310 return Boolean(
311 request
312 && !Utils.Request.isBatch(request)
313 && (typeof(request.id) === 'undefined'
314 || request.id === null)
315 );
316};
317
318/**
319 * Determines if the passed request is a valid JSON-RPC 2.0 Request
320 * @param {Object} request The request
321 * @return {Boolean}
322 */
323Utils.Request.isValidVersionTwoRequest = function(request) {
324 return Boolean(
325 request
326 && typeof(request) === 'object'
327 && request.jsonrpc === '2.0'
328 && typeof(request.method) === 'string'
329 && (
330 typeof(request.params) === 'undefined'
331 || Array.isArray(request.params)
332 || (request.params && typeof(request.params) === 'object')
333 )
334 && (
335 typeof(request.id) === 'undefined'
336 || typeof(request.id) === 'string'
337 || typeof(request.id) === 'number'
338 || request.id === null
339 )
340 );
341};
342
343/**
344 * Determines if the passed request is a valid JSON-RPC 1.0 Request
345 * @param {Object} request The request
346 * @return {Boolean}
347 */
348Utils.Request.isValidVersionOneRequest = function(request) {
349 return Boolean(
350 request
351 && typeof(request) === 'object'
352 && typeof(request.method) === 'string'
353 && Array.isArray(request.params)
354 && typeof(request.id) !== 'undefined'
355 );
356};
357
358/**
359 * Determines if the passed request is a valid JSON-RPC Request
360 * @param {Object} request The request
361 * @param {Number} [version=2] JSON-RPC version 1 or 2
362 * @return {Boolean}
363 */
364Utils.Request.isValidRequest = function(request, version) {
365 version = version === 1 ? 1 : 2;
366 return Boolean(
367 request
368 && (
369 (version === 1 && Utils.Request.isValidVersionOneRequest(request)) ||
370 (version === 2 && Utils.Request.isValidVersionTwoRequest(request))
371 )
372 );
373};
374
375/** * @namespace */
376Utils.Response = {};
377
378/**
379 * Determines if the passed error is a valid JSON-RPC error response
380 * @param {Object} error The error
381 * @param {Number} [version=2] JSON-RPC version 1 or 2
382 * @return {Boolean}
383 */
384Utils.Response.isValidError = function(error, version) {
385 version = version === 1 ? 1 : 2;
386 return Boolean(
387 version === 1 && (
388 typeof(error) !== 'undefined'
389 && error !== null
390 )
391 || version === 2 && (
392 error
393 && typeof(error.code) === 'number'
394 && parseInt(error.code, 10) === error.code
395 && typeof(error.message) === 'string'
396 )
397 );
398};
399
400/**
401 * Determines if the passed object is a valid JSON-RPC response
402 * @param {Object} response The response
403 * @param {Number} [version=2] JSON-RPC version 1 or 2
404 * @return {Boolean}
405 */
406Utils.Response.isValidResponse = function(response, version) {
407 version = version === 1 ? 1 : 2;
408 return Boolean(
409 response !== null
410 && typeof response === 'object'
411 && (version === 2 && (
412 // check version
413 response.jsonrpc === '2.0'
414 && (
415 // check id
416 response.id === null
417 || typeof response.id === 'string'
418 || typeof response.id === 'number'
419 )
420 && (
421 // result and error do not exist at the same time
422 (typeof response.result === 'undefined' && typeof response.error !== 'undefined')
423 || (typeof response.result !== 'undefined' && typeof response.error === 'undefined')
424 )
425 && (
426 // check result
427 (typeof response.result !== 'undefined')
428 // check error object
429 || (
430 response.error !== null
431 && typeof response.error === 'object'
432 && typeof response.error.code === 'number'
433 // check error.code is integer
434 && ((response.error.code | 0) === response.error.code)
435 && typeof response.error.message === 'string'
436 )
437 )
438 )
439 || version === 1 && (
440 typeof response.id !== 'undefined'
441 && (
442 // result and error relation (the other null if one is not)
443 (typeof response.result !== 'undefined' && response.error === null)
444 || (typeof response.error !== 'undefined' && response.result === null)
445 )
446 ))
447 );
448};