relay/src/rpc.ts

240 lines
5.4 KiB
TypeScript
Raw Normal View History

//const require = createRequire(import.meta.url);
//import { createRequire } from "module";
2022-06-27 17:53:00 +00:00
import crypto from "crypto";
import jayson from "jayson/promise/index.js";
2022-07-19 22:31:15 +00:00
import { pack, unpack } from "msgpackr";
import { Mutex } from "async-mutex";
2022-06-27 17:53:00 +00:00
import NodeCache from "node-cache";
2022-07-19 22:31:15 +00:00
import { get as getDHT } from "./dht.js";
import { rpcMethods } from "./rpc/index.js";
import { start as startDns } from "./dns.js";
2022-07-04 23:17:58 +00:00
import {
JSONRPCError,
2022-07-19 22:31:15 +00:00
JSONRPCRequest,
JSONRPCResponseWithError,
JSONRPCResponseWithResult,
2022-07-04 23:17:58 +00:00
} from "jayson";
import config from "./config.js";
import { ERR_NOT_READY, errorExit } from "./error.js";
2022-07-25 06:45:16 +00:00
import log from "loglevel";
// @ts-ignore
import stringify from "json-stable-stringify";
import type { StreamFileResponse } from "./streams.js";
import { getStream } from "./streams.js";
2022-07-04 23:17:58 +00:00
2022-06-27 17:53:00 +00:00
const pendingRequests = new NodeCache();
const processedRequests = new NodeCache({
2022-07-19 22:31:15 +00:00
stdTTL: 60 * 60 * 12,
2022-06-27 17:53:00 +00:00
});
2022-07-04 23:17:58 +00:00
let jsonServer: jayson.Server;
2022-06-27 17:53:00 +00:00
interface RPCRequest {
2022-08-18 23:29:06 +00:00
bypassCache: boolean;
2022-07-19 22:31:15 +00:00
chain: string;
query: string;
data: string;
2022-06-27 17:53:00 +00:00
}
interface RPCResponse {
2022-07-19 22:31:15 +00:00
updated: number;
data: any;
error?: string;
2022-06-27 17:53:00 +00:00
}
function hash(data: string): string {
2022-07-19 22:31:15 +00:00
return crypto.createHash("sha256").update(data).digest("hex");
2022-06-27 17:53:00 +00:00
}
function getRequestId(request: RPCRequest) {
2022-07-19 22:31:15 +00:00
const clonedRequest = Object.assign({}, request);
2022-06-27 17:53:00 +00:00
2022-07-19 22:31:15 +00:00
// @ts-ignore
2022-08-18 23:29:06 +00:00
delete clonedRequest.bypassCache;
2022-06-27 17:53:00 +00:00
2022-07-19 22:31:15 +00:00
return hash(stringify(clonedRequest));
2022-06-27 17:53:00 +00:00
}
function maybeProcessRequest(request: RPCRequest) {
2022-07-19 22:31:15 +00:00
if (!request.chain) {
throw new Error("RPC chain missing");
}
2022-06-27 17:53:00 +00:00
2022-07-19 22:31:15 +00:00
if (!request.data) {
throw new Error("RPC data missing");
}
2022-06-27 17:53:00 +00:00
2022-07-19 22:31:15 +00:00
return processRequest(request);
2022-06-27 17:53:00 +00:00
}
async function processRequest(request: RPCRequest): Promise<RPCResponse> {
2022-07-19 22:31:15 +00:00
const reqId = getRequestId(request);
let lock: Mutex = pendingRequests.get(reqId) as Mutex;
const lockExists = !!lock;
if (!lockExists) {
lock = new Mutex();
pendingRequests.set(reqId, lock);
}
if (lock.isLocked()) {
await lock.waitForUnlock();
return processedRequests.get(reqId) as RPCResponse;
}
await lock.acquire();
2022-08-18 23:29:06 +00:00
if (!request.bypassCache && processedRequests.get(reqId)) {
2022-07-19 22:31:15 +00:00
return processedRequests.get(reqId) as RPCResponse;
}
let rpcResp;
let error;
try {
rpcResp = await processRpcRequest(
{
method: request.query,
jsonrpc: "2.0",
params: request.data,
id: 1,
} as unknown as JSONRPCRequest,
request.chain
);
} catch (e) {
error = (e as Error).message;
}
let dbData: RPCResponse = {
updated: Date.now(),
data: "",
};
if (rpcResp) {
rpcResp = rpcResp as JSONRPCResponseWithResult;
if (false === rpcResp.result) {
error = true;
2022-06-27 17:53:00 +00:00
}
2022-07-04 23:17:58 +00:00
2022-07-19 22:31:15 +00:00
rpcResp = rpcResp as unknown as JSONRPCResponseWithError;
2022-07-23 00:57:53 +00:00
if (rpcResp.error && typeof rpcResp.error === "object") {
error = (rpcResp.error as JSONRPCError).message;
2022-06-27 17:53:00 +00:00
}
2022-07-19 22:31:15 +00:00
}
2022-06-27 17:53:00 +00:00
if (error) {
dbData.error = error as string;
} else {
dbData.data = (rpcResp as unknown as JSONRPCResponseWithResult).result;
}
2022-06-27 17:53:00 +00:00
if (
2022-08-18 23:29:06 +00:00
(!processedRequests.get(reqId) || request.bypassCache) &&
dbData.data?.error !== ERR_NOT_READY
) {
2022-07-19 22:31:15 +00:00
processedRequests.set(reqId, dbData);
}
2022-06-27 17:53:00 +00:00
2022-07-19 22:31:15 +00:00
await lock.release();
2022-06-27 17:53:00 +00:00
2022-07-19 22:31:15 +00:00
return dbData;
2022-06-27 17:53:00 +00:00
}
2022-07-04 23:17:58 +00:00
export async function processRpcRequest(
2022-07-19 22:31:15 +00:00
request: JSONRPCRequest,
chain: string
2022-07-04 23:17:58 +00:00
): Promise<JSONRPCResponseWithResult | JSONRPCResponseWithError | undefined> {
2022-07-19 22:31:15 +00:00
return new Promise((resolve) => {
jsonServer.call(
request,
{ chain },
(
err?: JSONRPCResponseWithError | null,
result?: JSONRPCResponseWithResult
): void => {
if (err) {
return resolve(err);
}
resolve(result);
}
);
});
2022-07-04 23:17:58 +00:00
}
2022-06-27 17:53:00 +00:00
export async function start() {
2022-07-19 22:31:15 +00:00
if (!config.str("pocket-app-id") || !config.str("pocket-app-key")) {
errorExit("Please set pocket-app-id and pocket-app-key config options.");
2022-07-19 22:31:15 +00:00
}
jsonServer = new jayson.Server(rpcMethods, { useContext: true });
(await getDHT("server")).on("connection", RPCConnection.handleRequest);
await startDns();
}
class RPCConnection {
private _socket: any;
constructor(socket: any) {
this._socket = socket;
2022-07-19 22:31:15 +00:00
socket.rawStream._ondestroy = () => false;
socket.once("data", this.checkRpc.bind(this));
}
private async checkRpc(data: Buffer) {
if (data.toString() === "rpc") {
this._socket.once("data", this.processRequest);
}
}
2022-07-19 22:31:15 +00:00
private async processRequest(data: Buffer) {
let request: RPCRequest;
try {
request = unpack(data) as RPCRequest;
} catch (e) {
return;
}
const that = this as any;
let response;
try {
response = await maybeProcessRequest(request);
} catch (error) {
2022-07-25 06:45:16 +00:00
log.trace(error);
that.write(pack({ error }));
that.end();
return;
}
if (response.data?.streamId) {
const stream = getStream(
response.data?.streamId
) as AsyncIterable<Uint8Array>;
const emptyData = Uint8Array.from([]);
const streamResp = {
2022-08-05 13:55:24 +00:00
data: {
data: emptyData,
done: false,
} as StreamFileResponse,
};
for await (const chunk of stream) {
2022-08-05 13:55:24 +00:00
streamResp.data.data = chunk as unknown as Uint8Array;
that.write(pack(streamResp));
}
2022-08-05 13:55:24 +00:00
streamResp.data.data = emptyData;
streamResp.data.done = true;
response = streamResp;
}
that.write(pack(response));
that.end();
}
public static handleRequest(socket: any) {
new RPCConnection(socket);
}
2022-06-27 17:53:00 +00:00
}