kaven-utils
Version:
Utils for Node.js.
404 lines (403 loc) • 12.8 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: 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 };
}