@mojojs/core
Version:
Real-time web framework
230 lines • 8.64 kB
JavaScript
import cluster from 'node:cluster';
import http from 'node:http';
import https from 'node:https';
import os from 'node:os';
import { Stream } from 'node:stream';
import { URL } from 'node:url';
import { ServerRequest } from './server/request.js';
import { ServerResponse } from './server/response.js';
import { WebSocket } from './websocket.js';
import Path from '@mojojs/path';
import { termEscape } from '@mojojs/util';
import { WebSocketServer } from 'ws';
/*
To regenerate the certificate run this command:
openssl req -x509 -newkey rsa:4096 -nodes -sha256 -out development.crt -keyout development.key -days 7300
-subj '/CN=localhost'
*/
const certs = Path.currentFile().dirname().sibling('vendor', 'certs');
const devCert = certs.child('development.crt').toString();
const devKey = certs.child('development.key').toString();
/**
* HTTP and WebSocket server class.
*/
export class Server {
constructor(app, options = {}) {
/**
* Server URLs.
*/
this.urls = [];
this._servers = [];
this.app = app;
this.headersTimeout = options.headersTimeout;
this.keepAliveTimeout = options.keepAliveTimeout;
this.maxRequestsPerSocket = options.maxRequestsPerSocket;
this.requestTimeout = options.requestTimeout;
this.reverseProxy = options.reverseProxy ?? false;
this._cluster = options.cluster ?? false;
this._listen = options.listen ?? ['http://*:3000'];
this._servers = [];
this._quiet = options.quiet ?? false;
this._workers = options.workers ?? os.cpus().length;
}
/**
* Turn URL into listen arguments.
*/
static listenArgsForURL(url) {
const listen = [];
const { hostname, port, protocol, searchParams } = url;
if (protocol === 'http+unix:') {
listen.push({ path: urlToSocketPath(url) });
}
else if (port !== '' && hostname !== '') {
listen.push(parseInt(port));
listen.push(hostname === '*' ? '0.0.0.0' : hostname.replace(/^\[/, '').replace(/]$/, ''));
}
else if (searchParams.has('fd') === true) {
listen.push({ fd: parseInt(searchParams.get('fd') ?? '') });
}
else {
listen.push(undefined, '0.0.0.0');
}
return listen;
}
/**
* Start server.
*/
async start() {
const { app } = this;
await app.hooks.serverStart(app);
if (this._cluster === true && cluster.isPrimary === true) {
for (let i = 0; i < this._workers; i++)
cluster.fork();
}
else {
for (const location of this._listen) {
await this._createServer(location);
}
}
}
/**
* Stop server.
*/
async stop() {
await Promise.all(this._servers.map(async (server) => await new Promise(resolve => server.close(resolve))));
// Clean up UNIX domain sockets
for (const url of this.urls) {
if (url.protocol === 'http+unix')
await new Path(urlToSocketPath(url)).rm();
}
const { app } = this;
await app.hooks.serverStop(app);
}
async _createServer(location) {
const url = new URL(location);
let isHttps = false;
const options = {};
if (url.protocol === 'https:') {
const { searchParams } = url;
options.cert = await new Path(searchParams.get('cert') ?? devCert).readFile();
options.key = await new Path(searchParams.get('key') ?? devKey).readFile();
isHttps = true;
}
await this.app.warmup();
const wss = new WebSocketServer({ noServer: true });
const server = (isHttps ? https : http).createServer(options, this._handleRequest.bind(this));
this._servers.push(server);
if (this.maxRequestsPerSocket !== undefined)
server.maxRequestsPerSocket = this.maxRequestsPerSocket;
if (this.headersTimeout !== undefined)
server.headersTimeout = this.headersTimeout;
if (this.keepAliveTimeout !== undefined)
server.keepAliveTimeout = this.keepAliveTimeout;
if (this.requestTimeout !== undefined)
server.requestTimeout = this.requestTimeout;
server.on('upgrade', this._handleUpgrade.bind(this, wss));
if (process.env.MOJO_SERVER_DEBUG === '1') {
server.on('connection', (socket) => {
const stderr = process.stderr;
socket.on('data', (chunk) => stderr.write(termEscape(`-- Server <<< Client\n${chunk}`)));
const write = socket.write;
socket.write = (chunk, cb) => {
stderr.write(termEscape(`-- Server >>> Client\n${chunk}`));
return write.apply(socket, [chunk, cb]);
};
});
}
return new Promise(resolve => {
server.listen(...Server.listenArgsForURL(url), () => {
const address = server.address();
// UNIX domain socket
let realLocation;
if (typeof address === 'string') {
realLocation = new URL(`http+unix://${address}`);
}
// TCP socket
else if (address !== null && typeof address === 'object') {
const host = address.family === 'IPv6' ? `[${address.address}]` : address.address;
realLocation = new URL(`${url.protocol}//${host}:${address.port}`);
}
else {
throw new Error('Unknown server address');
}
this.urls.push(realLocation);
if (this._quiet === false) {
if (this._cluster === true) {
console.log(`[${process.pid}] Web application available at ${realLocation}`);
}
else {
console.log(`Web application available at ${realLocation}`);
}
}
resolve();
});
});
}
_handleRequest(req, raw) {
const { app } = this;
const { socket } = req;
const ctx = app.newContext(this._prepareRequest(req, socket, false), new ServerResponse((res, body) => {
sendResponse(res, body, raw);
}), { name: 'server', req, res: raw });
raw.on('finish', () => ctx.emit('finish'));
app.handleRequest(ctx).catch(error => ctx.exception(error));
}
_handleUpgrade(wss, req, socket, head) {
const { app } = this;
const ctx = app.newContext(this._prepareRequest(req, socket, true), new ServerResponse((res, body) => {
sendResponse(res, body, new http.ServerResponse(req));
}), { name: 'server' });
app
.handleRequest(ctx)
.then(() => {
if (ctx.isAccepted === true) {
wss.handleUpgrade(req, socket, head, ws => {
ctx.handleUpgrade(new WebSocket(ws, null, { jsonMode: ctx.jsonMode }));
});
}
else {
socket.destroy();
}
})
.catch(error => {
if (ctx.isAccepted === false)
socket.destroy();
return ctx.exception(error);
});
}
_prepareRequest(req, socket, isWebSocket) {
return new ServerRequest({
body: req,
headers: req.rawHeaders,
isSecure: socket.encrypted ?? false,
isWebSocket: isWebSocket,
method: req.method,
remoteAddress: socket.remoteAddress,
reverseProxy: this.reverseProxy,
url: req.url
});
}
}
function sendHeaders(res, raw) {
const statusCode = res.statusCode;
const statusMessage = res.statusMessage;
const headers = res.headers.toArray();
if (statusMessage === null) {
raw.writeHead(statusCode, headers);
}
else {
raw.writeHead(statusCode, statusMessage, headers);
}
}
function sendResponse(res, body, raw) {
if (typeof body === 'string' || Buffer.isBuffer(body)) {
res.length(Buffer.byteLength(body));
sendHeaders(res, raw);
raw.end(body);
}
else if (body instanceof Stream) {
sendHeaders(res, raw);
body.pipe(raw);
}
else {
sendHeaders(res, raw);
raw.end();
}
}
function urlToSocketPath(url) {
return url.host + url.pathname;
}
//# sourceMappingURL=server.js.map