UNPKG

kaven-utils

Version:

Utils for Node.js.

404 lines (403 loc) 12.8 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: 2025-05-21 17:56:31.412 * @version: 5.4.5 * @times: 88 * @lines: 480 * @copyright: Copyright © 2018-2025 Kaven. All Rights Reserved. * @description: [description] * @license: [license] ********************************************************************/ import { AnsiTextBrightBlue, AnsiTextBrightRed, AnsiTextCyan, AnsiTextGreen, AnsiTextRed, FileSize, FormatCurrentDate, HttpStatusCode, KavenLog, LogLevel, Strings_Development, Strings_Empty } 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 { KavenLogger } from "./KavenLogger.js"; import { LoadJsonFile, LoadJsonFileSync, SaveStringToFile, SaveStringToFileSync } from "./KavenUtility.FileSystem.js"; import { InternalLogger } from "./KavenUtility.Internal.js"; /** * * @param dir * @param names * @returns * @since 5.0.0 * @version 2023-11-18 */ export async function LoadJsonConfig(dir, ...names) { if (names.length === 0) { names = [ ".config.json", ".configuration.json", "config.json", "configuration.json", ".config.example.json", ".configuration.example.json", "config.example.json", "configuration.example.json", ]; } const load = async (file) => { if (existsSync(file)) { const r = await LoadJsonFile(file); KavenLogger.Default.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; } /** * * @param dir * @param names * @returns * @since 5.0.1 * @version 2023-11-18 */ export function LoadJsonConfigSync(dir, ...names) { if (names.length === 0) { names = [ ".config.json", ".configuration.json", "config.json", "configuration.json", ".config.example.json", ".configuration.example.json", "config.example.json", "configuration.example.json", ]; } const load = (file) => { if (existsSync(file)) { const r = LoadJsonFileSync(file); KavenLogger.Default.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-05-21 */ export function CreateExpressLogger(options, logger) { 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`); let level = LogLevel.Info; const statusCode = res.statusCode; // client/server errors if (statusCode >= 400 && statusCode < 600) { level = LogLevel.Warn; } 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 ${FileSize(req.socket.bytesRead)}`); list.push(`write ${FileSize(req.socket.bytesWritten)}`); list.push(`${responseTimeInMilliseconds} ms`); const log = new KavenLog(list.join(", "), level).SetMessageContainsAnsiColor(); (logger ?? KavenLogger.Default)?.Log(log); } catch (ex) { (logger ?? KavenLogger.Default)?.Error(ex); } }; // Attach the calculateResponseTime function to the response object // res.on("finish", calculateResponseTime); res.on("close", calculateResponseTime); } catch (ex) { (logger ?? InternalLogger())?.Error(ex); } finally { // Continue to the next middleware or route handler next(); } }; } /** * * @param log default: `true` * @param redirectTo default: `/` * @returns ErrorRequestHandler * @since 2.0.14 * @version 2021-03-19 */ export function CreateExpressErrorHandler(log = true, redirectTo = "/") { const h = (err, _, res, next) => { if (log) { KavenLogger.Default.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; } /** * * @param domainNames * @param allowEmpty default: `false` * @param redirectTo * @param status default: `403` * @returns * @since 2.0.14 * @version 2021-03-19 */ export function CreateExpressCheckReferer(domainNames, allowEmpty = false, redirectTo, status) { const h = (req, res, next) => { const referer = req.get("Referer"); const abort = () => { if (redirectTo !== undefined) { if (status !== undefined) { res.redirect(status, redirectTo); } else { res.redirect(redirectTo); } return; } if (status === undefined) { status = HttpStatusCode.Forbidden; } KavenLogger.Default.Warn("Invalid referer:", referer); res.sendStatus(status); }; if (!referer) { if (allowEmpty) { return next(); } return abort(); } const url = new URL(referer); const hostname = url.hostname.toLowerCase(); if (hostname === "localhost") { 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 2022-06-29 */ export function HandleSignalsForServer(server, disposeBeforeShutdown) { // 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) => { KavenLogger.Default.Warn("shutdown!"); server.close(async () => { if (disposeBeforeShutdown) { await disposeBeforeShutdown(); } KavenLogger.Default.Warn(`server stopped by ${signal} with value ${value}`); await KavenLogger.Default.Stop(); 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, () => { KavenLogger.Default.Warn(`process received a ${signal} signal`); shutdown(signal, signals[signal]); }); }); } /** * * @param app * @param port * @param options * @since 2.0.14 * @version 2023-11-18 */ 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, () => { KavenLogger.Default.Info(`App is running at ${AnsiTextBrightBlue(url)} in ${AnsiTextBrightRed(options?.mode ?? "unspecific")} mode`); if (options?.mode === Strings_Development) { KavenLogger.Default.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 }; }