kaven-utils
Version:
Utils for Node.js.
384 lines (383 loc) • 12.3 kB
JavaScript
/********************************************************************
* @author: Kaven
* @email: kaven@wuwenkai.com
* @website: http://blog.kaven.xyz
* @file: [Kaven-Utils] /src/KavenUtility.Server.ts
* @create: 2018-08-30 13:24:44.049
* @modify: 2026-03-12 14:22:09.448
* @version: 6.1.3
* @times: 104
* @lines: 473
* @copyright: Copyright © 2018-2026 Kaven. All Rights Reserved.
* @description: [description]
* @license: [license]
********************************************************************/
import { AnsiTextBrightBlue, AnsiTextBrightRed, AnsiTextCyan, AnsiTextGreen, AnsiTextRed, FormatCurrentDate, HttpStatusCode, IsPrivateIP, LoggingAgent, Strings_Development, Strings_Empty, ToFileSize } from "kaven-basic";
import { existsSync, readFileSync } from "node:fs";
import * as http from "node:http";
import * as https from "node:https";
import { isAbsolute, join } from "node:path";
import { URL } from "node:url";
import { LoadJsonFile, LoadJsonFileSync, SaveStringToFile, SaveStringToFileSync } from "./KavenUtility.FileSystem.js";
import { DefaultConfigFileNames } from "./base/Constants.js";
export async function LoadJsonConfig(p1, p2, ...names) {
let logger = undefined;
let dir;
if (typeof p1 === "string") {
dir = p1;
if (p2 !== undefined) {
names = [p2, ...names];
}
}
else {
logger = p1;
dir = p2;
}
if (names.length === 0) {
names = DefaultConfigFileNames;
}
const load = async (file) => {
if (existsSync(file)) {
const r = await LoadJsonFile(file);
logger?.Info(`Load config file: ${file}`);
return r;
}
return false;
};
for (const name of names) {
if (isAbsolute(name)) {
const r = await load(name);
if (r !== false) {
return r;
}
}
const path = join(dir, name);
const r = await load(path);
if (r !== false) {
return r;
}
}
return undefined;
}
export function LoadJsonConfigSync(p1, p2, ...names) {
let logger = undefined;
let dir;
if (typeof p1 === "string") {
dir = p1;
if (p2 !== undefined) {
names = [p2, ...names];
}
}
else {
logger = p1;
dir = p2;
}
if (names.length === 0) {
names = DefaultConfigFileNames;
}
const load = (file) => {
if (existsSync(file)) {
const r = LoadJsonFileSync(file);
logger?.Info(`Load config file: ${file}`);
return r;
}
return false;
};
for (const name of names) {
if (isAbsolute(name)) {
const r = load(name);
if (r !== false) {
return r;
}
}
const path = join(dir, name);
const r = load(path);
if (r !== false) {
return r;
}
}
return undefined;
}
/**
* @since 5.0.0
* @version 2023-11-18
*/
export async function SaveJsonConfig(config, path) {
return await SaveStringToFile(JSON.stringify(config, undefined, 4), path);
}
/**
* @since 5.0.1
* @version 2023-11-18
*/
export function SaveJsonConfigSync(config, path) {
return SaveStringToFileSync(JSON.stringify(config, undefined, 4), path);
}
/**
* @since 1.0.5
* @version 2025-10-14
*/
export function CreateExpressLogger(logger, options) {
return (req, res, next) => {
try {
const startDate = FormatCurrentDate();
const start = process.hrtime(); // Record the high-resolution start time
// Create a function to calculate the response time
const calculateResponseTime = () => {
try {
const end = process.hrtime(start); // Record the high-resolution end time
const responseTimeInMilliseconds = end[0] * 1000 + end[1] / 1e6;
// console.log(`Response time: ${responseTimeInMilliseconds.toFixed(2)}ms`);
const statusCode = res.statusCode;
const color = (str) => {
if (str === undefined) {
return str;
}
if (!isNaN(statusCode)) {
if (statusCode >= 200 && statusCode < 300) {
return AnsiTextGreen(str);
}
else if (statusCode >= 300 && statusCode < 400) {
return AnsiTextCyan(str);
}
else {
return AnsiTextRed(str);
}
}
return str;
};
const list = [];
// request status
{
const part = [];
if (options?.dateTime) {
part.push(`[${startDate}]`);
}
part.push(...[
`[${color(req.method)}]`,
` ${req.originalUrl} `,
`[${color(`${statusCode}`)}]`,
]);
list.push(part.join(Strings_Empty));
}
if (options?.useRemoteAddr) {
list.push(`${req.socket.remoteAddress}`);
}
else {
list.push(`${req.ip}`);
}
list.push(`read ${ToFileSize(req.socket.bytesRead)}`);
list.push(`write ${ToFileSize(req.socket.bytesWritten)}`);
list.push(`${responseTimeInMilliseconds} ms`);
const log = list.join(", ");
// client/server errors
if (statusCode >= 400 && statusCode < 600) {
logger.Warn(log);
}
else {
logger.Info(log);
}
}
catch (ex) {
logger.Error(ex);
}
};
// Attach the calculateResponseTime function to the response object
// res.on("finish", calculateResponseTime);
res.on("close", calculateResponseTime);
}
catch (ex) {
logger.Error(ex);
}
finally {
// Continue to the next middleware or route handler
next();
}
};
}
/**
* @since 2.0.14
* @version 2021-03-19
*/
export function CreateExpressErrorHandler(logger, redirectTo = "/") {
const h = (err, _, res, next) => {
logger?.Error(err.stack);
if (redirectTo) {
res.redirect(redirectTo);
}
else {
next();
}
};
return h;
}
/**
*
* @param redirectTo default: `/`
* @returns RequestHandler
* @since 2.0.14
* @version 2021-03-19
*/
export function CreateExpress404Handler(redirectTo = "/") {
const h = (_, res) => {
res.redirect(redirectTo);
};
return h;
}
/**
* @since 2.0.14
* @version 2025-10-14
*/
export function CreateExpressCheckReferer(domainNames, options) {
options ??= {};
const h = (req, res, next) => {
const referer = req.get("Referer");
const abort = () => {
if (options.redirectTo !== undefined) {
if (options.status !== undefined) {
res.redirect(options.status, options.redirectTo);
}
else {
res.redirect(options.redirectTo);
}
return;
}
if (options.status === undefined) {
options.status = HttpStatusCode.Forbidden;
}
options.logger?.Warn(`Invalid referer: ${referer}`);
res.sendStatus(options.status);
};
if (!referer) {
if (options.allowEmpty) {
return next();
}
return abort();
}
const url = new URL(referer);
const hostname = url.hostname.toLowerCase();
if (hostname === "localhost" || hostname == "127.0.0.1") {
return next();
}
if (options.allowPrivateIP && IsPrivateIP(hostname)) {
return next();
}
for (const domain of domainNames) {
if (hostname === domain) {
return next();
}
if (hostname.endsWith(`.${domain}`)) {
return next();
}
}
return abort();
};
return h;
}
/**
*
* @since 4.3.1
* @version 2025-06-19
*/
export function HandleSignalsForServer(server, disposeBeforeShutdown, logger) {
// The signals we want to handle
// NOTE: although it is tempting, the SIGKILL signal (9) cannot be intercepted and handled
const signals = {
SIGHUP: 1,
SIGINT: 2,
SIGTERM: 15,
};
// Do any necessary shutdown logic for our application here
const shutdown = (signal, value) => {
logger?.Warn("shutdown!");
server.close(async () => {
logger?.Warn(`server stopped by ${signal} with value ${value}`);
if (disposeBeforeShutdown) {
await disposeBeforeShutdown();
}
if (logger instanceof LoggingAgent) {
await Promise.all(Array.from(logger.Handlers).map(p => p.Dispose()));
}
process.exit(128 + value);
});
};
// Create a listener for each of the signals that we want to handle
Object.keys(signals).forEach((signal) => {
process.on(signal, () => {
logger?.Warn(`process received a ${signal} signal`);
shutdown(signal, signals[signal]);
});
});
}
/**
* @since 2.0.14
* @version 2025-10-14
*/
export async function StartServer(app, port, options) {
if (options === undefined) {
options = {};
}
if (!options.hostname) {
options.hostname = "0.0.0.0";
}
let server;
if (options.enableHttps) {
if (options.sslCertFile && options.sslKeyFile) {
const serverOptions = {
cert: readFileSync(options.sslCertFile, "utf8"),
key: readFileSync(options.sslKeyFile, "utf8"),
};
server = https.createServer(serverOptions, app);
}
else {
throw new Error("Please provide ssl files");
}
}
else {
server = http.createServer(app);
}
if (options?.beforeListen) {
await options.beforeListen(server);
}
const url = `http${options.enableHttps ? "s" : ""}://127.0.0.1:${port}`;
server.listen(port, options.hostname, () => {
options.logger?.Info(`App is running at ${AnsiTextBrightBlue(url)} in ${AnsiTextBrightRed(options?.mode ?? "unspecific")} mode`);
if (options?.mode === Strings_Development) {
options.logger?.Info("Press CTRL-C to stop\n");
}
});
if (options.handleSignals || options.disposeBeforeShutdown) {
HandleSignalsForServer(server, options.disposeBeforeShutdown);
}
// 2022-06-29
return server;
}
/**
*
* @param authentication
* @since 4.1.0
* @version 2022-09-20
* @returns
*/
export function CreateExpressAuthentication(authentication) {
const auth = async (req, res) => {
const ok = await authentication.Authenticate({
authorization: req.headers.authorization,
method: req.method,
ip: req.ip,
});
if (res) {
if (!ok) {
const auth = authentication.GetResponse();
res.writeHead(auth.StatusLine.StatusCode, auth.GetHeaders());
res.end(auth.Body?.Data);
}
}
return ok;
};
const handler = async (req, res, next) => {
if (await auth(req, res)) {
next();
}
};
return { auth, handler };
}