UNPKG

wexen

Version:
564 lines (563 loc) 20.9 kB
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