wexen
Version:
564 lines (563 loc) • 20.9 kB
JavaScript
import { parse } from "node:url";
import { readFileSync, existsSync, statSync } from "node:fs";
import { gzip } from "zlib";
import util from "node:util";
import { createSecureServer } from "node:http2";
import { networkInterfaces } from "node:os";
import { WebSocketServer, WebSocket } from "ws";
import { resolve } from "node:path";
class HttpError extends Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
}
}
function newHttpHeaders(items) {
return {
items,
get(key) {
const value = this.items[key];
if (typeof value === "string") {
return value;
}
return void 0;
},
set(key, value) {
if (value == null) {
delete this.items[key];
} else {
this.items[key] = value;
}
}
};
}
var HttpMethod = /* @__PURE__ */ ((HttpMethod2) => {
HttpMethod2["Delete"] = "DELETE";
HttpMethod2["Get"] = "GET";
HttpMethod2["Head"] = "HEAD";
HttpMethod2["Options"] = "OPTIONS";
HttpMethod2["Patch"] = "PATCH";
HttpMethod2["Post"] = "POST";
HttpMethod2["Put"] = "PUT";
return HttpMethod2;
})(HttpMethod || {});
var HttpStatusCode = /* @__PURE__ */ ((HttpStatusCode2) => {
HttpStatusCode2[HttpStatusCode2["Continue"] = 100] = "Continue";
HttpStatusCode2[HttpStatusCode2["SwitchingProtocols"] = 101] = "SwitchingProtocols";
HttpStatusCode2[HttpStatusCode2["Processing"] = 102] = "Processing";
HttpStatusCode2[HttpStatusCode2["EarlyHints"] = 103] = "EarlyHints";
HttpStatusCode2[HttpStatusCode2["Ok"] = 200] = "Ok";
HttpStatusCode2[HttpStatusCode2["Created"] = 201] = "Created";
HttpStatusCode2[HttpStatusCode2["Accepted"] = 202] = "Accepted";
HttpStatusCode2[HttpStatusCode2["NonAuthoritativeInformation"] = 203] = "NonAuthoritativeInformation";
HttpStatusCode2[HttpStatusCode2["NoContent"] = 204] = "NoContent";
HttpStatusCode2[HttpStatusCode2["ResetContent"] = 205] = "ResetContent";
HttpStatusCode2[HttpStatusCode2["PartialContent"] = 206] = "PartialContent";
HttpStatusCode2[HttpStatusCode2["MultiStatus"] = 207] = "MultiStatus";
HttpStatusCode2[HttpStatusCode2["MultipleChoices"] = 300] = "MultipleChoices";
HttpStatusCode2[HttpStatusCode2["MovedPermanently"] = 301] = "MovedPermanently";
HttpStatusCode2[HttpStatusCode2["MovedTemporarily"] = 302] = "MovedTemporarily";
HttpStatusCode2[HttpStatusCode2["SeeOther"] = 303] = "SeeOther";
HttpStatusCode2[HttpStatusCode2["NotModified"] = 304] = "NotModified";
HttpStatusCode2[HttpStatusCode2["UseProxy"] = 305] = "UseProxy";
HttpStatusCode2[HttpStatusCode2["TemporaryRedirect"] = 307] = "TemporaryRedirect";
HttpStatusCode2[HttpStatusCode2["PermanentRedirect"] = 308] = "PermanentRedirect";
HttpStatusCode2[HttpStatusCode2["BadRequest"] = 400] = "BadRequest";
HttpStatusCode2[HttpStatusCode2["Unauthorized"] = 401] = "Unauthorized";
HttpStatusCode2[HttpStatusCode2["PaymentRequired"] = 402] = "PaymentRequired";
HttpStatusCode2[HttpStatusCode2["Forbidden"] = 403] = "Forbidden";
HttpStatusCode2[HttpStatusCode2["NotFound"] = 404] = "NotFound";
HttpStatusCode2[HttpStatusCode2["MethodNotAllowed"] = 405] = "MethodNotAllowed";
HttpStatusCode2[HttpStatusCode2["NotAcceptable"] = 406] = "NotAcceptable";
HttpStatusCode2[HttpStatusCode2["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired";
HttpStatusCode2[HttpStatusCode2["RequestTimeout"] = 408] = "RequestTimeout";
HttpStatusCode2[HttpStatusCode2["Conflict"] = 409] = "Conflict";
HttpStatusCode2[HttpStatusCode2["Gone"] = 410] = "Gone";
HttpStatusCode2[HttpStatusCode2["LengthRequired"] = 411] = "LengthRequired";
HttpStatusCode2[HttpStatusCode2["PreconditionFailed"] = 412] = "PreconditionFailed";
HttpStatusCode2[HttpStatusCode2["RequestTooLong"] = 413] = "RequestTooLong";
HttpStatusCode2[HttpStatusCode2["RequestUriTooLong"] = 414] = "RequestUriTooLong";
HttpStatusCode2[HttpStatusCode2["UnsupportedMediaType"] = 415] = "UnsupportedMediaType";
HttpStatusCode2[HttpStatusCode2["RequestedRangeNotSatisfiable"] = 416] = "RequestedRangeNotSatisfiable";
HttpStatusCode2[HttpStatusCode2["ExpectationFailed"] = 417] = "ExpectationFailed";
HttpStatusCode2[HttpStatusCode2["ImATeapot"] = 418] = "ImATeapot";
HttpStatusCode2[HttpStatusCode2["InsufficientSpaceOnResource"] = 419] = "InsufficientSpaceOnResource";
HttpStatusCode2[HttpStatusCode2["MethodFailure"] = 420] = "MethodFailure";
HttpStatusCode2[HttpStatusCode2["MisdirectedRequest"] = 421] = "MisdirectedRequest";
HttpStatusCode2[HttpStatusCode2["UnprocessableEntity"] = 422] = "UnprocessableEntity";
HttpStatusCode2[HttpStatusCode2["Locked"] = 423] = "Locked";
HttpStatusCode2[HttpStatusCode2["FailedDependency"] = 424] = "FailedDependency";
HttpStatusCode2[HttpStatusCode2["UpgradeRequired"] = 426] = "UpgradeRequired";
HttpStatusCode2[HttpStatusCode2["PreconditionRequired"] = 428] = "PreconditionRequired";
HttpStatusCode2[HttpStatusCode2["TooManyRequests"] = 429] = "TooManyRequests";
HttpStatusCode2[HttpStatusCode2["RequestHeaderFieldsTooLarge"] = 431] = "RequestHeaderFieldsTooLarge";
HttpStatusCode2[HttpStatusCode2["UnavailableForLegalReasons"] = 451] = "UnavailableForLegalReasons";
HttpStatusCode2[HttpStatusCode2["InternalServerError"] = 500] = "InternalServerError";
HttpStatusCode2[HttpStatusCode2["NotImplemented"] = 501] = "NotImplemented";
HttpStatusCode2[HttpStatusCode2["BadGateway"] = 502] = "BadGateway";
HttpStatusCode2[HttpStatusCode2["ServiceUnavailable"] = 503] = "ServiceUnavailable";
HttpStatusCode2[HttpStatusCode2["GatewayTimeout"] = 504] = "GatewayTimeout";
HttpStatusCode2[HttpStatusCode2["HttpVersionNotSupported"] = 505] = "HttpVersionNotSupported";
HttpStatusCode2[HttpStatusCode2["InsufficientStorage"] = 507] = "InsufficientStorage";
HttpStatusCode2[HttpStatusCode2["NetworkAuthenticationRequired"] = 511] = "NetworkAuthenticationRequired";
return HttpStatusCode2;
})(HttpStatusCode || {});
function newHttpRequest(request) {
let _data = null;
return {
remoteAddress: request.socket.remoteAddress,
// todo checked before used
method: request.method,
url: parse(request.url ?? "", true),
headers: newHttpHeaders(request.headers),
query() {
return this.url.query;
},
async text() {
if (_data) {
return _data;
}
return new Promise((resolve2, reject) => {
let data = "";
request.on("data", (chunk) => data += chunk);
request.on("end", () => {
_data = data;
resolve2(data);
});
request.on("error", (err) => reject(err));
});
},
async json() {
return JSON.parse(await this.text());
}
};
}
function newFileResponse(filePath, statusCode = 200) {
const pathExtension = filePath.split(".").pop()?.toLowerCase() || "";
return {
statusCode,
headers: newHttpHeaders({ "content-type": contentTypeFromExtension(pathExtension) }),
body: readFileSync(filePath),
filePath,
async send(request, response) {
const acceptEncoding = request.headers["accept-encoding"];
const supportsGzip = acceptEncoding?.includes("gzip");
if (supportsGzip) {
gzip(this.body, (err, compressedData) => {
if (err) {
response.writeHead(500);
response.end("Compression Error");
return;
}
response.setHeader("Content-Encoding", "gzip");
response.writeHead(response.statusCode, this.headers.items);
response.end(compressedData);
});
} else {
response.writeHead(response.statusCode, this.headers.items);
response.end(this.body);
}
}
};
}
function newHtmlResponse(html, statusCode = 200) {
return {
statusCode,
headers: newHttpHeaders({ "content-type": "text/html" }),
body: html,
async send(request, response) {
const acceptEncoding = request.headers["accept-encoding"];
const supportsGzip = acceptEncoding?.includes("gzip");
if (supportsGzip) {
gzip(this.body, (err, compressedData) => {
if (err) {
response.writeHead(500);
response.end("Compression Error");
return;
}
response.setHeader("Content-Encoding", "gzip");
response.setHeader("Content-Type", "text/html");
response.writeHead(response.statusCode, this.headers.items);
response.end(compressedData);
});
} else {
response.writeHead(response.statusCode, this.headers.items);
response.end(this.body);
}
}
};
}
function newJsonResponse(value, statusCode = HttpStatusCode.Ok) {
return {
statusCode,
headers: newHttpHeaders({ "content-type": "application/json" }),
body: JSON.stringify(value),
async send(_request, response) {
response.writeHead(this.statusCode, this.headers.items);
response.end(this.body);
}
};
}
const notFoundMessage = JSON.stringify({ error: "Not Found" });
function newNotFoundResponse(value) {
return {
statusCode: HttpStatusCode.NotFound,
headers: newHttpHeaders({ "content-type": "application/json" }),
body: value ? JSON.stringify(value) : notFoundMessage,
async send(_request, response) {
response.writeHead(this.statusCode, this.headers.items);
response.end(this.body);
}
};
}
function runWebServer(config) {
logger.info(`Web server is running:`, TerminalColor.FG_GREEN);
logger.info(`-`.repeat(24), TerminalColor.FG_GREEN);
return createSecureServer(config, serverListener(config)).listen(config.port, () => logListening(config));
}
function serverListener(config) {
const middlewares = config.middlewares ?? [];
return async (req, res) => {
const performanceTime = process.hrtime.bigint();
try {
if (!req.url) {
throw new Error("URL Not Found");
}
if (!req.method) {
throw new Error("HTTP Method Not Found");
}
const request = newHttpRequest(req);
const response = await middlewareResponse(middlewares, request);
const origin = req.headers.origin;
if (origin && config.origins?.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Allow-Credentials", "true");
if (req.method === HttpMethod.Options) {
res.writeHead(HttpStatusCode.NoContent);
return res.end();
}
await response.send(req, res);
const humanizedTime = humanizeTime(process.hrtime.bigint() - performanceTime);
const logLevel = isSuccessfulStatusCode(response.statusCode) ? LogLevel.Info : LogLevel.Error;
logRequest(logLevel, req, response.statusCode, humanizedTime);
} catch (err) {
const error = err instanceof HttpError ? err : new HttpError(HttpStatusCode.InternalServerError, "Internal Error");
res.writeHead(error.statusCode, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: error.message }));
const humanizedTime = humanizeTime(process.hrtime.bigint() - performanceTime);
logRequest(LogLevel.Error, req, error.statusCode, `${humanizedTime}
${String(err["stack"] ?? err)}`);
}
};
}
async function middlewareResponse(middlewares, request) {
for (const middleware of middlewares) {
const response = await middleware(request);
if (response) {
return response;
}
}
return newNotFoundResponse();
}
function logListening(config) {
logger.info(`https://localhost:${config.port}`);
const interfaces = networkInterfaces();
Object.keys(interfaces).map((x) => interfaces[x] ?? []).flat().filter((x) => x.family === "IPv4" && !x.internal).forEach((x) => logger.info(`https://${x.address}:${config.port}`));
logger.info(`-`.repeat(24));
}
function isSuccessfulStatusCode(statusCode) {
return statusCode >= 200 && statusCode < 300;
}
function createWebSocketServer(server) {
const wss = new WebSocketServer({ server });
wss.on("connection", (ws) => {
logger.info(`New client connected`);
ws.on("message", (message) => {
wss.clients.forEach((peer) => {
if (peer !== ws && peer.readyState === WebSocket.OPEN) {
peer.send(message.toString());
}
});
});
ws.on("error", (error) => logger.error(String(error)));
ws.on("close", (code, reason) => {
if (reason.toString()) {
logger.info(`Client disconnected. Code ${code}. Reason ${reason.toString()}`);
} else {
logger.info(`Client disconnected`);
}
});
});
return wss;
}
function logLevelToTerminalColor(level) {
switch (level) {
case LogLevel.Error:
return TerminalColor.FG_RED;
case LogLevel.Warn:
return TerminalColor.FG_YELLOW;
case LogLevel.Info:
return TerminalColor.FG_BLUE;
case LogLevel.Verbose:
return TerminalColor.FG_GRAY;
case LogLevel.Debug:
return TerminalColor.FG_WHITE;
case LogLevel.Silly:
return TerminalColor.FG_MAGENTA;
default:
return TerminalColor.FG_WHITE;
}
}
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
LogLevel2["Error"] = "error";
LogLevel2["Warn"] = "warn";
LogLevel2["Info"] = "info";
LogLevel2["Verbose"] = "verbose";
LogLevel2["Debug"] = "debug";
LogLevel2["Silly"] = "silly";
return LogLevel2;
})(LogLevel || {});
function logRequest(level, request, statusCode, message, color) {
const statusCodeText = String(statusCode).padStart(3, "0");
const methodText = (request.method ?? "<none>").padEnd(6, " ");
const url = request.url ?? "<none>";
const messageText = message ? " " + message : "";
const remoteAddress = request.socket.remoteAddress ?? "<none>";
const ip = remoteAddress.replace("::ffff:", "").padEnd(16, " ");
logger[level](`${ip} ${statusCodeText} ${methodText} ${url}${messageText}`, color);
}
const logger = {
error: (message, color) => {
console.log(logFormat(LogLevel.Error, message, color).join(" "));
},
warn: (message, color) => {
console.log(logFormat(LogLevel.Warn, message, color).join(" "));
},
info: (message, color) => {
console.log(logFormat(LogLevel.Info, message, color).join(" "));
},
verbose: (message, color) => {
console.log(logFormat(LogLevel.Verbose, message, color).join(" "));
},
debug: (message, color) => {
console.log(logFormat(LogLevel.Debug, message, color).join(" "));
},
silly: (message, color) => {
console.log(logFormat(LogLevel.Silly, message, color).join(" "));
}
};
process.on("uncaughtException", (error) => {
logger.error("Uncaught Exception:\n" + util.format(error));
});
process.on("unhandledRejection", (reason) => {
logger.error("Unhandled Rejection:\n" + util.format(reason));
});
function logFormat(level, message, color) {
const dateTime = /* @__PURE__ */ new Date();
const time = dateTime.toLocaleString("ru-RU", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
}) + "." + String(dateTime.getMilliseconds()).padStart(3, "0");
const levelColor = color ?? logLevelToTerminalColor(level);
return [
`[${terminalColor(`${time}`, TerminalColor.FG_GRAY)}]`,
// `[${terminalColor(levelCode, TerminalColor.FG_GRAY)}]`,
terminalColor(message, levelColor)
];
}
function controllerMiddleware(controllers) {
const controllerDictionary = prepareControllers(controllers);
return async (request) => {
const requestPath = request.url.pathname?.toLowerCase() ?? "";
const middlewares = controllerDictionary[requestPath];
for (const middleware of middlewares) {
const result = await middleware(request);
if (result != null) {
return result;
}
}
return null;
};
}
function prepareControllers(controllers, parentPath = "", result = {}) {
if (!controllers?.length) {
return result;
}
for (const controller of controllers) {
const path = "/" + joinUrl(parentPath, controller.path).toLowerCase();
if (controller.middlewares?.length) {
result[path] ??= [];
result[path].push(...controller.middlewares);
}
if (controller.children) {
prepareControllers(controller.children, path, result);
}
}
return result;
}
function routeMiddleware(method, middleware) {
return async (request) => {
if (request.method !== method) {
return null;
}
return middleware(request);
};
}
function staticFilesMiddleware(staticFileDirectories) {
return async (request) => {
if (request.method !== HttpMethod.Get) {
const notAllowedResponse = newJsonResponse(
{ error: "Method Not Allowed" },
HttpStatusCode.MethodNotAllowed
);
notAllowedResponse.headers.set("Allow", "GET");
return notAllowedResponse;
}
const requestPath = request.url?.path?.slice(1) || "index.html";
for (const directory of staticFileDirectories) {
const path = resolve(directory, requestPath);
if (path && existsSync(path) && statSync(path).isFile()) {
return newFileResponse(path);
}
}
};
}
function findMap(items, predicateMap) {
for (const item of items) {
const result = predicateMap(item);
if (result != null) {
return result;
}
}
return null;
}
function filterMap(items, predicateMap) {
const newArray = [];
for (const item of items) {
const result = predicateMap(item);
if (result != null) {
newArray.push(result);
}
}
return newArray;
}
const contentTypes = {
html: "text/html",
css: "text/css",
js: "text/javascript",
json: "application/json",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
svg: "image/svg+xml",
pdf: "application/pdf",
ico: "image/x-icon",
xml: "application/xml",
txt: "text/plain",
mp4: "video/mp4",
mp3: "audio/mpeg",
wav: "audio/wav",
webp: "image/webp",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
zip: "application/zip",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
};
function contentTypeFromExtension(extension) {
return contentTypes[extension] ?? "application/octet-stream";
}
function humanizeTime(ns) {
const totalMilliseconds = ns / 1000000n;
const totalSeconds = totalMilliseconds / 1000n;
const totalMinutes = totalSeconds / 60n;
const nanoseconds = ns % 1000000n;
const milliseconds = totalMilliseconds % 1000n;
const seconds = totalSeconds % 60n;
const minutes = totalMinutes % 60n;
const hours = totalMinutes / 60n;
const result = [];
if (hours > 0) {
result.push(`${hours}h`);
}
if (minutes > 0) {
result.push(`${minutes}m`);
}
if (seconds > 0) {
result.push(`${seconds}s`);
}
if (milliseconds > 0) {
result.push(`${milliseconds}ms`);
} else if (nanoseconds > 0) {
result.push(`0.${ns / 1000n}ms`);
}
return result.join("");
}
function joinUrl(...parts) {
return parts.flatMap((x) => x?.split("/")).filter((x) => !!x).join("/");
}
var TerminalColor = /* @__PURE__ */ ((TerminalColor2) => {
TerminalColor2["RESET"] = "\x1B[0m";
TerminalColor2["BRIGHT"] = "\x1B[1m";
TerminalColor2["DIM"] = "\x1B[2m";
TerminalColor2["UNDERSCORE"] = "\x1B[4m";
TerminalColor2["BLINK"] = "\x1B[5m";
TerminalColor2["REVERSE"] = "\x1B[7m";
TerminalColor2["HIDDEN"] = "\x1B[8m";
TerminalColor2["FG_BLACK"] = "\x1B[30m";
TerminalColor2["FG_RED"] = "\x1B[31m";
TerminalColor2["FG_GREEN"] = "\x1B[32m";
TerminalColor2["FG_YELLOW"] = "\x1B[33m";
TerminalColor2["FG_BLUE"] = "\x1B[34m";
TerminalColor2["FG_MAGENTA"] = "\x1B[35m";
TerminalColor2["FG_CYAN"] = "\x1B[36m";
TerminalColor2["FG_WHITE"] = "\x1B[37m";
TerminalColor2["FG_GRAY"] = "\x1B[90m";
TerminalColor2["BG_BLACK"] = "\x1B[40m";
TerminalColor2["BG_RED"] = "\x1B[41m";
TerminalColor2["BG_GREEN"] = "\x1B[42m";
TerminalColor2["BG_YELLOW"] = "\x1B[43m";
TerminalColor2["BG_BLUE"] = "\x1B[44m";
TerminalColor2["BG_MAGENTA"] = "\x1B[45m";
TerminalColor2["BG_CYAN"] = "\x1B[46m";
TerminalColor2["BG_WHITE"] = "\x1B[47m";
TerminalColor2["BG_GRAY"] = "\x1B[100m";
return TerminalColor2;
})(TerminalColor || {});
function terminalColor(text, color) {
return `${color}${text}${"\x1B[0m"}`;
}
export {
HttpError,
HttpMethod,
HttpStatusCode,
LogLevel,
TerminalColor,
contentTypeFromExtension,
controllerMiddleware,
createWebSocketServer,
filterMap,
findMap,
humanizeTime,
joinUrl,
logLevelToTerminalColor,
logRequest,
logger,
newFileResponse,
newHtmlResponse,
newHttpHeaders,
newHttpRequest,
newJsonResponse,
newNotFoundResponse,
prepareControllers,
routeMiddleware,
runWebServer,
staticFilesMiddleware,
terminalColor
};
//# sourceMappingURL=index.js.map