Skip to content

Instantly share code, notes, and snippets.

@SplittyDev
Last active August 13, 2017 02:28
Show Gist options
  • Save SplittyDev/0d10a762ca97beaf6366427918b6f815 to your computer and use it in GitHub Desktop.
Save SplittyDev/0d10a762ca97beaf6366427918b6f815 to your computer and use it in GitHub Desktop.
/*
* JSON-RPC 2.0 implementation as per http://www.jsonrpc.org/specification.
* License: MIT (https://opensource.org/licenses/MIT)
* Author: Marco Quinten <splittydev@gmail.com>
*/
const debug = require('debug')('json-rpc');
// List of public API endpoints
var exposed = [];
// JSON-RPC 2.0 error codes
const JsonRpcErrors = {
// Official response codes
ParseError: -32700,
InternalError: -32603,
InvalidParams: -32602,
MethodNotFound: -32601,
InvalidRequest: -32600,
// Custom response codes
RuntimeError: -32001,
ServerError: -32000,
};
/**
* Test if a JSON-RPC 2.0 object is a notification.
* Notifications do not allow for replies.
*
* @param {any} json_req The JSON-RPC 2.0 object.
* @returns Whether the JSON-RPC 2.0 object is a notification.
*/
function json_rpc_is_notification(json_req) {
return json_req.id === void 0;
}
/**
* Send a JSON-RPC 2.0 response.
* No additional checks are done.
*
* @param {any} json_resp The JSON-RPC 2.0 response.
* @param {any} send_callback The send function.
*/
function json_rpc_send_raw(json_resp, send_callback) {
try {
// Try sending the response
send_callback(json_resp);
} catch (e) {
// Log errors to the console
debug(`Error: ${e.message}`);
}
}
/**
* Create and send a JSON-RPC 2.0 result.
* Checks for notification requests are done.
*
* @param {any} json_req The JSON-RPC 2.0 request.
* @param {any} result The result to be sent.
* @param {any} send_callback The send function.
*/
function json_rpc_send_result(json_req, result, send_callback) {
// Bail out if the JSON-RPC 2.0 object is a notification
if (json_rpc_is_notification(json_req)) {
return;
}
// Craft the response
const json_resp = JSON.stringify({
"jsonrpc": "2.0",
"result": result,
"id": json_req.id,
});
// Send the response
json_rpc_send_raw(json_resp, send_callback);
}
/**
* Create and send a JSON-RPC 2.0 error.
* Checks for notification requests are done.
*
* @param {any} json_req The JSON-RPC 2.0 request.
* @param {any} code The error code.
* @param {any} message The error message.
* @param {any=} error The error object.
*/
function json_rpc_send_error(json_req, code, message, error) {
// Bail out if the JSON-RPC 2.0 object is a notification
if (json_rpc_is_notification(json_req)) {
return;
}
// Craft the response
const json_resp = JSON.stringify({
"jsonrpc": "2.0",
"error": {
"code": code,
"message": message,
"data": error || [],
},
"id": json_req.id,
});
// Send the response
json_rpc_send_raw(json_resp, send_callback);
}
/**
* Turn a function into a valid API endpoint.
*
* @param {any} func The function.
* @returns The wrapped function.
*/
function json_rpc_wrap(func) {
return (json_req, send_callback) => {
// Construct RPC object
const rpc = {
// Send result
send: (result) => {
json_rpc_send_result(json_req, result, send_callback);
},
};
try {
// Try invoking the function
func(rpc, json_req.params);
} catch (e) {
// Handle errors by sending back an error response
json_rpc_send_error(json_req, JsonRpcErrors.RuntimeError, e.message, e);
}
};
}
/**
* Test a JSON object for JSON-RPC 2.0 compliance.
*
* @param {any} json The JSON object to be validated.
* @returns Whether the specified JSON object is a valid JSON-RPC 2.0 request.
*/
function json_rpc_validate(json) {
// Check header
if (json.jsonrpc !== '2.0') {
debug('Field `jsonrpc` MUST be exactly "2.0"!');
return false;
}
// Check method
if (json.method.constructor !== String) {
debug('Field `method` MUST be a String!');
return false;
}
// Check id
if (json.id !== void 0 // id can be omitted
&& json.id.constructor !== String
&& json.id.constructor !== Number
&& json.id.constructor !== null) {
debug('Field `id` MUST be of type String, Number, or null!');
return false;
} else if (json.id === null) {
debug('Field `id` SHOULD NOT be null!');
} else if (json.id.constructor === Number && (Math.trunc(json.id) !== json.id)) {
debug('Field `id` SHOULD NOT contain a fractional part!');
}
// All good!
return true;
}
/**
* Call a local API endpoint.
*
* @param {any} plain_json The plain-text request.
* @param {any} send_callback The send function.
* @returns Whether the JSON-RPC 2.0 call succeeded.
*/
function json_rpc_call(plain_json, send_callback) {
// Parse plain-text request
const json_req = (() => {
try {
return JSON.parse(plain_json);
} catch (e) {
debug('Malformed JSON!');
}
})();
// Validate request
if (!json_rpc_validate(json_req)) {
debug('Malformed JSON-RPC 2.0 request!');
json_rpc_send_error(json_req, JsonRpcErrors.InvalidRequest, 'Invalid format!');
return false;
}
// Find local endpoint
const endpoint = exposed.find(func => {
return func.name === json_req.method;
});
// Check if the desired endpoint was found
if (endpoint === void 0) {
debug(`Method not found: ${json_req.method}`);
json_rpc_send_error(json_req, JsonRpcErrors.MethodNotFound);
return false;
}
debug(`<- ${json_req.method}`);
// Invoke local endpoint
endpoint.call(json_req, send_callback);
// All good!
return true;
}
/**
* Expose a function as a local API endpoint.
*
* @param {any} name The name of the function on the remote end.
* @param {any} func The function on the local end.
*/
function json_rpc_expose(name, func) {
// Whitelist the endpoint
exposed.push({
name: name,
call: json_rpc_wrap(func),
});
}
module.exports = {
call: json_rpc_call,
expose: json_rpc_expose,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment