UNPKG

kaven-utils

Version:

Utils for Node.js.

384 lines (383 loc) 12.3 kB
/******************************************************************** * @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 }; }