@litert/televoke
Version:
A simple RPC service framework.
205 lines • 8.42 kB
JavaScript
"use strict";
/**
* Copyright 2025 Angus.Fenying <fenying@litert.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.createLegacyHttpGateway = createLegacyHttpGateway;
exports.createCustomLegacyHttpGateway = createCustomLegacyHttpGateway;
const Shared = require("../../shared");
const node_events_1 = require("node:events");
const LegacyHttp_Transporter_1 = require("./LegacyHttp.Transporter");
const v1 = require("../../shared/Encodings/v1");
const encoder = new v1.TvEncoderV1();
const INVALID_REQUEST_RESPONSE = Buffer.from(encoder.encodeApiErrorResponse('null', Shared.Encodings.v1.EResponseCode.MALFORMED_ARGUMENTS, '"INVALID REQUEST"', 0));
function refuseBadRequest(resp) {
try {
resp.writeHead(400, {
'content-length': INVALID_REQUEST_RESPONSE.byteLength,
});
const socket = resp.socket;
resp.end(INVALID_REQUEST_RESPONSE);
socket.destroy();
}
catch {
// do nothing.
}
}
class LegacyHttpGateway extends node_events_1.EventEmitter {
constructor(registerListener, _server) {
super();
this._server = _server;
this._onRequest = (req, resp) => {
if (req.method !== 'POST' || !req.headers['content-length']) {
refuseBadRequest(resp);
this.emit('error', new Shared.errors.invalid_packet({
reason: 'invalid_request',
data: {
'method': req.method,
'headers': req.headers,
'url': req.url,
},
}));
return;
}
const length = parseInt(req.headers['content-length']);
if (!Number.isSafeInteger(length) || length > v1.MAX_PACKET_SIZE) { // Maximum request packet is 64MB
refuseBadRequest(resp);
this.emit('error', new Shared.errors.invalid_packet({
reason: 'invalid_packet_length',
data: {
'length': length,
'max': v1.MAX_PACKET_SIZE,
},
}));
return;
}
const buf = Buffer.allocUnsafe(length);
let offset = 0;
const recvAt = Date.now();
resp.on('error', (e) => this.emit('error', e));
req.on('error', (e) => this.emit('error', e))
.on('data', (chunk) => {
const index = offset;
offset += chunk.byteLength;
if (offset > length) {
refuseBadRequest(resp);
this.emit('error', new Shared.errors.invalid_packet({
reason: 'length_exceeded',
recv: offset,
expected: length,
}));
return;
}
chunk.copy(buf, index);
})
.on('end', () => {
let input;
if (offset !== length) {
refuseBadRequest(resp);
return;
}
try {
input = JSON.parse(buf);
}
catch (e) {
refuseBadRequest(resp);
this.emit('error', new Shared.errors.invalid_packet({
reason: 'invalid_json',
}, e));
return;
}
if (typeof input?.api !== 'string') {
refuseBadRequest(resp);
this.emit('error', new Shared.errors.invalid_packet({
reason: 'malformed_json',
}));
return;
}
this._server.processLegacyApi((result) => {
if (!resp.writable) {
return;
}
if (result instanceof Shared.TelevokeError) {
if (result instanceof Shared.errors.app_error) {
this._sendResponse(resp, encoder.encodeApiErrorResponse(input.rid, v1.EResponseCode.FAILURE, result.message, recvAt));
}
else if (result instanceof Shared.errors.api_not_found) {
this._sendResponse(resp, encoder.encodeApiErrorResponse(input.rid, v1.EResponseCode.API_NOT_FOUND, 'null', recvAt));
}
else if (result instanceof Shared.ProtocolError) {
this._sendResponse(resp, encoder.encodeApiErrorResponse(input.rid, v1.EResponseCode.SYSTEM_ERROR, JSON.stringify({
name: result.name,
message: result.message,
data: result.data,
}), recvAt));
}
else {
this._sendResponse(resp, encoder.encodeApiErrorResponse(input.rid, v1.EResponseCode.SYSTEM_ERROR, 'null', recvAt));
}
return;
}
let data;
try {
data = encoder.encodeApiOkResponse(input.rid, result ?? null, recvAt);
}
catch (e) {
data = encoder.encodeApiErrorResponse(input.rid, v1.EResponseCode.SYSTEM_ERROR, 'null', recvAt);
this.emit('error', new Shared.errors.unprocessable_error({ api: input.api }, e));
}
this._sendResponse(resp, data);
}, input.api, input.args, new LegacyHttp_Transporter_1.LegacyHttpTransporter(req));
});
};
if (this._server.router.encoding !== 'json') {
throw new TypeError('Legacy HTTP gateway only supports JSON encoding');
}
this._listener = registerListener({
onErrorCallback: (e) => this.emit('error', e),
onRequestCallback: this._onRequest,
});
}
_sendResponse(resp, data) {
try {
resp.setHeader('content-length', Buffer.byteLength(data));
resp.end(data);
}
catch (e) {
this.emit('error', e);
}
}
get running() {
return this._listener.running;
}
async start() {
if (this.running) {
return;
}
await this._listener.start?.();
}
async stop() {
if (!this.running) {
return Promise.resolve();
}
await this._listener.stop?.();
}
}
/**
* Create a legacy HTTP gateway, binding to a built-in simple HTTP server.
*
* > When using built-in HTTP server, the api will ignore headers, path and query string in the URL.
*
* @param listener The built-in HTTP listener to bind to.
* @param server The server to process the requests.
*/
function createLegacyHttpGateway(listener, server) {
return new LegacyHttpGateway((o) => {
listener.on('error', o.onErrorCallback);
listener.setLegacyApiProcessor(o.onRequestCallback);
return listener;
}, server);
}
/**
* Create a legacy HTTP gateway, binding to a custom HTTP server.
*
* > When using a custom HTTP server, it's able to preprocess the request before passing to the server, like
* > authentication, rate limiting, etc.
*
* @param registerListener The function to register the listener to the custom HTTP server.
* @param server The server to process the requests.
*/
function createCustomLegacyHttpGateway(registerListener, server) {
return new LegacyHttpGateway(registerListener, server);
}
//# sourceMappingURL=LegacyHttp.Server.js.map