src/server/protocol/base.js
const MessageBuffer = require("../../util/buffer");
const { formatResponse, formatError } = require("../../util/format");
const { ERR_CODES, ERR_MSGS } = require("../../util/constants");
/**
* Creates an instance of JsonRpcServerProtocol. This is the
* base protocol from which all others inherit.
*
*/
class JsonRpcServerProtocol {
/**
* @param {class} factory Instance of [JsonRpcServerFactory]{@link JsonRpcServerFactory}
* @param {class} client Instance of `net.Socket`
* @param {(1|2)} version JSON-RPC version to use
* @param {string} delimiter Delimiter to use for `messageBuffer`
* @property {class} messageBuffer Instance of [MessageBuffer]{@link MessageBuffer}
* @property {string} event="data" The event name to listen for incoming data
*/
constructor(factory, client, version, delimiter) {
this.client = client;
this.factory = factory;
this.delimiter = delimiter;
this.version = version;
this.messageBuffer = new MessageBuffer(delimiter);
this.event = "data";
}
/**
* Registers the `event` data listener when client connects.
*
* Pushes received data into `messageBuffer` and calls
* [_waitForData]{@link JsonRpcServerProtocol#_waitForData}.
*
*/
clientConnected() {
this.client.on(this.event, (data) => {
this.messageBuffer.push(data);
this._waitForData();
});
}
/**
* Accumulate data while [MessageBuffer.isFinished]{@link MessageBuffer#isFinished} is returning false.
*
* If the buffer returns a message it will be passed to [validateRequest]{@link JsonRpcServerProtocol#validateRequest}.
* If [validateRequest]{@link JsonRpcServerProtocol#validateRequest} returns a parsed result, then the result
* is passed to [_maybeHandleRequest]{@link JsonRpcServerProtocol#_maybeHandleRequest}.
* If [_maybeHandleRequest]{@link JsonRpcServerProtocol#_maybeHandleRequest} returns true, then
* [gotRequest]{@link JsonRpcServerProtocol#gotRequest} is called.<br/><br/>
*
* If any of the above throws an error, [gotError]{@link JsonRpcServerProtocol#gotError} is called.
*
* @private
*
*/
_waitForData() {
while (!this.messageBuffer.isFinished()) {
const chunk = this.messageBuffer.handleData();
this._validateData(chunk);
}
}
/**
* Validates data returned from `messageBuffer`.
*
* Will call [gotError]{@link JsonRpcClientProtocol#gotError} if error thrown
* during validation.
*
* @param {string} chunk Data to validate
* @private
*
*/
_validateData(chunk) {
try {
const result = this.validateRequest(chunk);
const isMessage = this._maybeHandleRequest(result);
if (isMessage) {
this.gotRequest(result);
}
} catch (e) {
this.gotError(e);
}
}
/**
* Validate the request message
*
* @param {string} chunk
* @returns {JSON}
* @throws Will throw an error with a JSON-RPC error object if chunk cannot be parsed
*/
validateRequest(chunk) {
try {
return JSON.parse(chunk);
} catch (e) {
throw new Error(
formatError({
jsonrpc: this.version,
id: null,
code: ERR_CODES.parseError,
message: ERR_MSGS.parseError,
delimiter: this.delimiter
})
);
}
}
/**
* Determines the type of request being made (batch, notification, request) and
* calls the corresponding function.
*
* @param {JSON} result Valid JSON-RPC request object
* @private
*/
_maybeHandleRequest(result) {
if (Array.isArray(result)) {
if (result.length === 0) {
return this.writeToClient(
formatError({
code: ERR_CODES.invalidRequest,
message: ERR_MSGS.invalidRequest,
delimiter: this.delimiter,
jsonrpc: this.version,
id: null
})
);
}
// possible batch request
this.gotBatchRequest(result).then((res) => {
if (res.length !== 0) {
// if all the messages in the batch were notifications,
// then we wouldnt want to return anything
this.writeToClient(JSON.stringify(res) + this.delimiter);
}
});
} else if (result === Object(result) && !("id" in result)) {
// no id, so assume notification
this.gotNotification(result);
return false;
} else {
this.validateMessage(result);
return true;
}
}
/**
* Validates if there are any issues with the incoming request<br/>
*
*
* @param {JSON} message Valid JSON-RPC request object
* @throws Will throw an error for any of the below reasons
*
* Reason|Type
* ---|---
* message is not an object| Invalid Request
* the server does not have the required method| Method Not Found
* the params are not an array or object| Invalid Params
* the "jsonrpc" property was passed for a v1 server| Invalid Request
*/
validateMessage(message) {
if (!(message === Object(message))) {
this._raiseError(
ERR_MSGS.invalidRequest,
ERR_CODES.invalidRequest,
null,
this.version
);
} else if (!(typeof message.method === "string")) {
this._raiseError(
ERR_MSGS.invalidRequest,
ERR_CODES.invalidRequest,
message.id,
message.jsonrpc
);
} else if (!(message.method in this.factory.methods)) {
this._raiseError(
ERR_MSGS.methodNotFound,
ERR_CODES.methodNotFound,
message.id,
message.jsonrpc
);
} else if (
message.params
&& !Array.isArray(message.params)
&& !(message.params === Object(message.params))
) {
this._raiseError(
ERR_MSGS.invalidParams,
ERR_CODES.invalidParams,
message.id,
message.jsonrpc
);
} else if (message.jsonrpc && this.version !== 2) {
this._raiseError(
ERR_MSGS.invalidRequest,
ERR_CODES.invalidRequest,
message.id,
this.version
);
}
}
/**
* Send message to the client
*
* @param {string} message Stringified JSON-RPC message object
*/
writeToClient(message) {
this.client.write(message);
}
/**
* Calls `emit` on factory with the event name being `message.method` and
* the date being `message`.
*
* @param {string} message JSON-RPC message object
*/
gotNotification(message) {
this.factory.emit(message.method, message);
}
/**
* Attempts to get the result for the request object. Will
* send result to client if successful and will send an error
* otherwise.
*
* @param {JSON} message JSON-RPC message object
* @returns {Promise}
*/
gotRequest(message) {
return this.getResult(message)
.then((result) => {
this.writeToClient(result);
})
.catch((error) => {
this.gotError(Error(error));
});
}
/**
* Attempts to get the result for all requests in the batch.
* Will send result to client if successful and error otherwise.
*
* @param {JSON[]} requests Valid JSON-RPC batch request
* @returns {Promise[]}
*/
gotBatchRequest(requests) {
const batchResponses = requests
.map((request) => {
try {
const isMessage = this._maybeHandleRequest(request);
if (isMessage) {
// if its a notification we dont want to return anything
return this.getResult(request)
.then(result => JSON.parse(result))
.catch(error => JSON.parse(error));
}
return null;
} catch (e) {
// basically reject the whole batch if any one thing fails
return JSON.parse(e.message);
}
})
.filter(el => el != null);
return Promise.all(batchResponses);
}
/**
* Get the result for the request. Calls the function associated
* with the method and returns the result.
*
* @param {JSON} message Valid JSON-RPC message object
* @returns {Promise}
*/
getResult(message) {
// function needs to be async since the method can be a promise
return new Promise((resolve, reject) => {
const { params } = message;
const response = {
jsonrpc: message.jsonrpc,
id: message.id,
delimiter: this.delimiter
};
const error = {
jsonrpc: message.jsonrpc,
id: message.id,
delimiter: this.delimiter
};
try {
const methodResult = params
? this.factory.methods[message.method](params)
: this.factory.methods[message.method]();
if (
methodResult instanceof Promise
|| typeof methodResult.then === "function"
) {
methodResult
.then((results) => {
response.result = results;
resolve(formatResponse(response));
})
.catch((resError) => {
error.code = ERR_CODES.internal;
error.message = `${JSON.stringify(resError.message || resError)}`;
error.data = resError.data;
reject(formatError(error));
});
} else {
response.result = methodResult;
resolve(formatResponse(response));
}
} catch (e) {
if (e instanceof TypeError) {
error.code = ERR_CODES.invalidParams;
error.message = ERR_MSGS.invalidParams;
// error.data = e.message;
} else {
error.code = ERR_CODES.unknown;
error.message = ERR_MSGS.unknown;
// error.data = e.message;
}
error.data = e.data;
reject(formatError(error));
}
});
}
/**
*
* @param {string} message Error message
* @param {number} code Error code
* @param {string|number} id Error message ID
* @throws Throws a JSON-RPC error object
* @private
*/
_raiseError(message, code, id, version) {
const error = formatError({
jsonrpc: version,
delimiter: this.delimiter,
id,
code,
message
});
throw new Error(error);
}
/**
* Writes error to the client. Will send a JSON-RPC error object if the
* passed error cannot be parsed.
*
* @param {Error} error `Error` object instance where the message should be a JSON-RPC message object
*/
gotError(error) {
let err;
try {
err = JSON.stringify(JSON.parse(error.message)) + this.delimiter;
} catch (e) {
err = formatError({
jsonrpc: this.version,
delimiter: this.delimiter,
id: null,
code: ERR_CODES.unknown,
message: JSON.stringify(error, Object.getOwnPropertyNames(error))
});
}
this.writeToClient(err);
}
}
module.exports = JsonRpcServerProtocol;