UNPKG

tiinyrpc

Version:

An small rpc server base on tinyrpc protocol

355 lines (354 loc) 13.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Server = void 0; const http_1 = __importDefault(require("http")); const events_1 = __importDefault(require("events")); const beson_1 = __importDefault(require("beson")); const consts_js_1 = require("./consts.js"); ; ; ; ; ; ; ; ; const _ServerInst = new WeakMap(); class Server extends events_1.default.EventEmitter { static init(options) { const server = new Server(); const session = { callmap: {}, max_body: options?.max_body || 0, server: http_1.default.createServer() }; _ServerInst.set(server, Object.assign({ session: null, max_body: 0, audit: null }, options, { session })); BindMessageProcessor.call(server); return server; } get max_body() { return _ServerInst.get(this).session.max_body; } set max_body(size) { if (typeof size !== "number" || !Number.isFinite(size) || Number.isNaN(size)) { throw new Error("Property max_body must be a finite number!"); } _ServerInst.get(this).session.max_body = size; } get is_listening() { return _ServerInst.get(this).session.server.listening; } scope(options) { const instance = new Server(); _ServerInst.set(instance, Object.assign({ session: null, audit: null }, options, { session: _ServerInst.get(this).session })); return instance; } handle(callmap, handler) { if (typeof callmap === "string") { callmap = { [callmap]: handler }; } const _inst = _ServerInst.get(this); const regmap = _inst.session.callmap; for (const func in callmap) { if (regmap[func]) { throw Object.assign(new Error(`Call '${func} has been registered already!'`), { code: consts_js_1.ErrorCode.CALL_EXISTS, detail: { func } }); } const handler = callmap[func]; const handler_type = typeof handler; if (handler_type !== "function") { throw Object.assign(new Error(`Given handler expects function, got ${handler_type}!`), { code: consts_js_1.ErrorCode.INVALID_HANDLER_TYPE, detail: { func } }); } regmap[func] = { handler, auditor: _inst.audit }; } return this; } unhandle(func) { const _inst = _ServerInst.get(this); const regmap = _inst.session.callmap; delete regmap[func]; return this; } listen(options) { return new Promise((res, rej) => { const server = _ServerInst.get(this).session.server; server.once('error', rej); // Unix socket if ('path' in options) { server.listen(options.path, success); return; } if (options.host) { server.listen(options.port, options.host, success); } else { server.listen(options.port, success); } function success() { server.off('error', rej); return res(server.address()); } }); } release() { return new Promise((res, rej) => { _ServerInst.get(this).session.server.close((err) => err ? rej(err) : res()); }); } } exports.Server = Server; function BindMessageProcessor() { const __Server = _ServerInst.get(this); const Session = __Server.session; __Server.session.server.on('request', (req, res) => { Promise.resolve().then(async () => { const result = await ReadAll(req, Session.max_body); // Check method if (req.method !== "POST") { WriteResponse(res, 405, "application/json", { rpc: "1.0", error: { stage: consts_js_1.Stage.PAYLOAD_PARSER, code: consts_js_1.ErrorCode.UNSUPPORTED_METHOD, message: "This server accepts only POST method!", detail: { method: req.method } } }); return; } // Check size if (!Buffer.isBuffer(result)) { WriteResponse(res, 400, "application/json", { rpc: "1.0", error: { stage: consts_js_1.Stage.PAYLOAD_PARSER, code: consts_js_1.ErrorCode.PAYLOAD_IS_TOO_LARGE, message: "Your request payload is too large!", detail: { payload: result.total_size, limit: Session.max_body } } }); return; } // Parse content according to Content-Type header let payload, mime; { const raw_content_type = req.headers['content-type'] || ''; const content_type = parseContentTypeHeader(Array.isArray(raw_content_type) ? raw_content_type[0] : raw_content_type); if (content_type.mime === "application/json") { mime = content_type.mime; try { payload = JSON.parse(result.toString('utf8')); } catch (e) { WriteResponse(res, 400, "application/json", { rpc: "1.0", error: { stage: consts_js_1.Stage.PAYLOAD_PARSER, code: consts_js_1.ErrorCode.INVALID_PAYLOAD_FORMAT, message: "Provided body content is not a valid JSON!" } }); return; } } else if (content_type.mime === "application/beson") { mime = content_type.mime; payload = beson_1.default.Deserialize(result); if (payload === undefined) { WriteResponse(res, 400, "application/json", { rpc: "1.0", error: { stage: consts_js_1.Stage.PAYLOAD_PARSER, code: consts_js_1.ErrorCode.INVALID_PAYLOAD_FORMAT, message: "Provided body content is not a valid BESON!" } }); return; } } else { WriteResponse(res, 400, "application/json", { rpc: "1.0", error: { stage: consts_js_1.Stage.PAYLOAD_PARSER, code: consts_js_1.ErrorCode.INVALID_PAYLOAD_FORMAT, message: "Unspported payload mime type!" } }); return; } } // Check if body is an object if (Object(payload) !== payload) { WriteResponse(res, 400, "application/json", { rpc: "1.0", error: { stage: consts_js_1.Stage.PAYLOAD_PARSER, code: consts_js_1.ErrorCode.INVALID_PAYLOAD_FORMAT, message: "Unspported payload mime type!", detail: { type: typeof payload } } }); return; } // Check request structure { const errors = []; if (payload.rpc !== "1.0") { errors.push('Invalid rpc protocol'); } if (typeof payload.id !== "string" && typeof payload.id !== "number") { errors.push('Invalid request id'); } if (!payload.call || typeof payload.call !== "string") { errors.push('Invald call name'); } if (!Array.isArray(payload.args)) { errors.push('Invalid argument list'); } if (errors.length > 0) { WriteResponse(res, 400, "application/json", { rpc: "1.0", error: { stage: consts_js_1.Stage.PAYLOAD_PARSER, code: consts_js_1.ErrorCode.INVALID_PAYLOAD_FORMAT, message: "Your payload content is invalid!", detail: errors } }); return; } } const call_info = Session.callmap[payload.call]; if (!call_info) { WriteResponse(res, 404, mime, { rpc: "1.0", error: { stage: consts_js_1.Stage.CALL_EXEC, code: consts_js_1.ErrorCode.CALL_NOT_FOUND, message: "Target call is not found!", detail: { call: payload.call } } }); return; } const auditor = call_info.auditor; if (typeof auditor === "function") { const is_go = await auditor(req, payload); if (is_go !== true) { WriteResponse(res, 403, mime, { rpc: "1.0", error: { stage: consts_js_1.Stage.CALL_AUDIT, code: consts_js_1.ErrorCode.INVALID_PAYLOAD_FORMAT, message: "You're not allowed to perform this operation!", detail: { info: is_go } } }); return; } } try { const result = await call_info.handler.call(null, ...payload.args); WriteResponse(res, 200, mime, { rpc: "1.0", id: payload.id, ret: result }); } catch (e) { console.error(`Received error when executing \`${payload.call}\`!`, e); const err = e; if (err instanceof Error) { WriteResponse(res, 500, mime, { rpc: "1.0", id: payload.id, error: { stage: consts_js_1.Stage.CALL_EXEC, code: err.code || consts_js_1.ErrorCode.CALL_EXEC_ERROR, message: err.message, detail: err.detail } }); return; } else { WriteResponse(res, 500, mime, { rpc: "1.0", id: payload.id, error: { stage: consts_js_1.Stage.CALL_EXEC, code: consts_js_1.ErrorCode.CALL_EXEC_ERROR, message: 'Unknown exception has be caught!', detail: err } }); return; } } }) .catch((e) => { console.error("Received error in outter most scope!", e); WriteResponse(res, 500, "application/json", { rpc: "1.0", error: { stage: consts_js_1.Stage.UNKNOWN, code: e.code || consts_js_1.ErrorCode.UNEXPECTED_ERROR, message: e.message } }); }); }); } function ReadAll(req, max_body = 0) { return new Promise((res, rej) => { const chunks = []; let data_length = 0; req .on('data', (chunk) => { data_length += chunk.length; if (max_body > 0 && data_length >= max_body) return; chunks.push(chunk); }) .on('end', () => { const remained = Buffer.concat(chunks); if (data_length !== remained.length) { return res({ remained, total_size: data_length }); } else { return res(remained); } }) .on('error', rej); }); } function parseContentTypeHeader(contentTypeHeader) { const parts = contentTypeHeader.split(";"); const mime = (parts[0] || '').trim(); const attributes = {}; for (let i = 1; i < parts.length; i++) { const attribute = parts[i].trim(); const [name, value] = attribute.split("="); attributes[name.trim()] = value ? value.trim() : true; } return { mime, attributes }; } function WriteResponse(res, status, mime, body) { res.writeHead(status, { "Content-Type": mime }); if (mime === "application/json") { res.end(JSON.stringify(body)); } else { res.end(beson_1.default.Serialize(body)); } }