UNPKG

@jsonjoy.com/reactive-rpc

Version:

Reactive-RPC is a library for building reactive APIs over WebSocket, HTTP, and other RPCs.

248 lines 9.94 kB
"use strict"; var _a; Object.defineProperty(exports, "__esModule", { value: true }); exports.Http1Server = exports.Http1EndpointMatch = void 0; const tslib_1 = require("tslib"); const http = tslib_1.__importStar(require("http")); const https = tslib_1.__importStar(require("https")); const Writer_1 = require("@jsonjoy.com/util/lib/buffers/Writer"); const Codecs_1 = require("@jsonjoy.com/json-pack/lib/codecs/Codecs"); const WsServerConnection_1 = require("../ws/server/WsServerConnection"); const WsFrameEncoder_1 = require("../ws/codec/WsFrameEncoder"); const jit_router_1 = require("@jsonjoy.com/jit-router"); const printTree_1 = require("sonic-forest/lib/print/printTree"); const errors_1 = require("../errors"); const util_1 = require("./util"); const util_2 = require("../util"); const context_1 = require("./context"); const RpcCodecs_1 = require("../../common/codec/RpcCodecs"); const RpcMessageCodecs_1 = require("../../common/codec/RpcMessageCodecs"); const NullObject_1 = require("@jsonjoy.com/util/lib/NullObject"); class Http1EndpointMatch { constructor(handler, msgCodec) { this.handler = handler; this.msgCodec = msgCodec; } } exports.Http1EndpointMatch = Http1EndpointMatch; class Http1Server { constructor(opts) { this.opts = opts; this.onnotfound = (res) => { res.writeHead(404, 'Not Found'); res.end(); }; this.oninternalerror = (error, res) => { if (error instanceof errors_1.PayloadTooLarge) { res.statusCode = 413; res.statusMessage = 'Payload Too Large'; res.end(); return; } res.statusCode = 500; res.statusMessage = 'Internal Server Error'; res.end(); }; this.httpRouter = new jit_router_1.Router(); this.httpMatcher = () => undefined; this.onRequest = async (req, res) => { try { res.sendDate = false; const url = req.url ?? ''; const queryStartIndex = url.indexOf('?'); let path = url; let query = ''; if (queryStartIndex >= 0) { path = url.slice(0, queryStartIndex); query = url.slice(queryStartIndex + 1); } const route = (req.method || '') + path; const match = this.httpMatcher(route); if (!match) { this.onnotfound(res, req); return; } const codecs = this.codecs; const ip = this.findIp(req); const token = this.findToken(req); const matchData = match.data; const ctx = new context_1.Http1ConnectionContext(req, res, path, query, ip, token, match.params, new NullObject_1.NullObject(), codecs.value.json, codecs.value.json, matchData.msgCodec); const headers = req.headers; const contentType = headers['content-type']; if (typeof contentType === 'string') (0, util_1.setCodecs)(ctx, contentType, codecs); const handler = matchData.handler; await handler(ctx); } catch (error) { this.oninternalerror(error, res, req); } }; this.wsRouter = new jit_router_1.Router(); this.wsMatcher = () => undefined; this.onUpgrade = (req, socket) => { if (req.headers.upgrade === 'websocket') { this.onWsUpgrade(req, socket); } else { } }; this.onWsUpgrade = (req, socket) => { const url = req.url ?? ''; const queryStartIndex = url.indexOf('?'); let path = url; let query = ''; if (queryStartIndex >= 0) { path = url.slice(0, queryStartIndex); query = url.slice(queryStartIndex + 1); } const match = this.wsMatcher(path); if (!match) { socket.end(); return; } const def = match.data; const headers = req.headers; const connection = new WsServerConnection_1.WsServerConnection(this.wsEncoder, socket); connection.maxIncomingMessage = def.maxIncomingMessage ?? 2 * 1024 * 1024; connection.maxBackpressure = def.maxOutgoingBackpressure ?? 2 * 1024 * 1024; if (def.onWsUpgrade) def.onWsUpgrade(req, connection); else { const secWebSocketKey = headers['sec-websocket-key'] ?? ''; const secWebSocketProtocol = headers['sec-websocket-protocol'] ?? ''; const secWebSocketExtensions = headers['sec-websocket-extensions'] ?? ''; connection.upgrade(secWebSocketKey, secWebSocketProtocol, secWebSocketExtensions); } const codecs = this.codecs; const ip = this.findIp(req); const token = this.findToken(req); const ctx = new context_1.WsConnectionContext(connection, path, query, ip, token, match.params, new NullObject_1.NullObject(), codecs.value.json, codecs.value.json, codecs.messages.compact); const contentType = headers['content-type']; if (typeof contentType === 'string') (0, util_1.setCodecs)(ctx, contentType, codecs); else { const secWebSocketProtocol = headers['sec-websocket-protocol'] ?? ''; if (typeof secWebSocketProtocol === 'string') (0, util_1.setCodecs)(ctx, secWebSocketProtocol, codecs); } def.handler(ctx, req); }; this.server = opts.server; const writer = opts.writer ?? new Writer_1.Writer(); this.codecs = opts.codecs ?? new RpcCodecs_1.RpcCodecs(opts.codecs ?? new Codecs_1.Codecs(writer), new RpcMessageCodecs_1.RpcMessageCodecs()); this.wsEncoder = new WsFrameEncoder_1.WsFrameEncoder(writer); } async start() { const server = this.server; this.httpMatcher = this.httpRouter.compile(); this.wsMatcher = this.wsRouter.compile(); server.on('request', this.onRequest); server.on('upgrade', this.onUpgrade); server.on('clientError', (err, socket) => { socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); }); } async stop() { const server = this.server; server.removeAllListeners(); } route(def) { let path = def.path; if (path[0] !== '/') path = '/' + path; const method = def.method ? def.method.toUpperCase() : 'GET'; const route = method + path; Number(route); const match = new Http1EndpointMatch(def.handler, def.msgCodec ?? this.codecs.messages.jsonRpc2); this.httpRouter.add(route, match); } ws(def) { this.wsRouter.add(def.path, def); } findIp(req) { const headers = req.headers; const ip = headers['x-forwarded-for'] || headers['x-real-ip'] || req.socket.remoteAddress || ''; return ip instanceof Array ? ip[0] : ip; } findToken(req) { let token = ''; const headers = req.headers; let header; header = headers.authorization; if (typeof header === 'string') token = (0, util_2.findTokenInText)(header); if (token) return token; const url = req.url; if (typeof url === 'string') token = (0, util_2.findTokenInText)(url); if (token) return token; header = headers.cookie; if (typeof header === 'string') token = (0, util_2.findTokenInText)(header); if (token) return token; header = headers['sec-websocket-protocol']; if (typeof header === 'string') token = (0, util_2.findTokenInText)(header); return token; } enableHttpPing(path = '/ping') { this.route({ path, handler: (ctx) => { ctx.res.end('"pong"'); }, }); } enableKamalPing(path = '/up', response = 'yes') { this.route({ path, handler: (ctx) => { ctx.res.end(response); }, }); } toString(tab = '') { return (`${this.constructor.name}` + (0, printTree_1.printTree)(tab, [ (tab) => `HTTP ${this.httpRouter.toString(tab)}`, (tab) => `WebSocket ${this.wsRouter.toString(tab)}`, ])); } } exports.Http1Server = Http1Server; _a = Http1Server; Http1Server.create = async (opts = {}) => { if (opts.tls) { const { secureContext, secureContextRefreshInterval } = opts; const server = https.createServer({ ...(secureContext ? await secureContext() : {}), ...opts.conf, }); if (secureContext && secureContextRefreshInterval) { const timer = setInterval(() => { try { secureContext() .then((context) => { server.setSecureContext(context); }) .catch((error) => { console.error('Failed to update secure context:', error); }); } catch (error) { console.error('Failed to update secure context:', error); } }, secureContextRefreshInterval); server.once('close', () => { clearInterval(timer); }); } return server; } return http.createServer(opts.conf || {}); }; //# sourceMappingURL=Http1Server.js.map