Last active
August 13, 2017 02:28
-
-
Save SplittyDev/0d10a762ca97beaf6366427918b6f815 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* JSON-RPC 2.0 implementation as per http://www.jsonrpc.org/specification. | |
* License: MIT (https://opensource.org/licenses/MIT) | |
* Author: Marco Quinten <[email protected]> | |
*/ | |
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