experimental/traffic-portal/middleware.ts (101 lines of code) (raw):

/** * @license Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { opendir } from "fs/promises"; import { join } from "path"; import type { NextFunction, Request, Response } from "express"; import { LogLevel, Logger } from "src/app/utils"; import { environment } from "src/environments/environment"; import type { ServerConfig } from "./server.config"; /** * StaticFile defines what compression files are available. */ interface StaticFile { compressions: Array<CompressionType>; } /** * CompressionType defines the different compression algorithms. */ interface CompressionType { fileExt: string; headerEncoding: string; name: string; } /** * TPResponseLocals are the express.Response.locals properties specific to a * response writer for the TP server. */ interface TPResponseLocals { config: ServerConfig; foundFiles: Map<string, StaticFile>; logger: Logger; /** The time at which the request was received. */ startTime: Date; /** * The time at which the response was finished being written (or * `undefined` if not done yet). */ endTime?: Date | undefined; } /** * AuthenticatedResponse is a response writer for endpoints that require * authentication. */ export type TPResponseWriter = Response<unknown, TPResponseLocals>; /** * An HTTP request handler for the TP server. */ export type TPHandler = (req: Request, resp: TPResponseWriter, next: NextFunction) => void | PromiseLike<void>; const gzip = { fileExt: "gz", headerEncoding: "gzip", name: "gzip" }; const br = { fileExt: "br", headerEncoding: "br", name: "brotli" }; /** * getFiles recursively gets all the files in a directory. * * @param path The path to get files from. * @returns Files found in the directory. */ async function getFiles(path: string): Promise<string[]> { const dir = await opendir(path); let dirEnt = await dir.read(); let files = new Array<string>(); while (dirEnt !== null) { const name = join(path, dirEnt.name); if (dirEnt.isDirectory()) { files = files.concat(await getFiles(name)); } else { files.push(name); } dirEnt = await dir.read(); } await dir.close(); return files; } /** * loggingMiddleWare is a middleware factory for express.js that provides a * logger. * It does also provide a link to server configuration that can be used in * handlers, and a couple other niceties. * * @param config The server configuration. * @returns A middleware that adds a property `logger` to `resp.locals` for * logging purposes. */ export async function loggingMiddleWare(config: ServerConfig): Promise<TPHandler> { const allFiles = await getFiles(config.browserFolder); const compressedFiles = new Map( allFiles.filter( file => file.match(/\.(br|gz)$/) ).map( file => [file, undefined] ) ); const foundFiles = new Map<string, StaticFile>( allFiles.filter( file => file.match(/\.(js|css|tff|svg)$/) ).map( file => { const staticFile: StaticFile = { compressions: [] }; if (compressedFiles.has(`${file}.${br.fileExt}`)) { staticFile.compressions.push(br); } if (compressedFiles.has(`${file}.${gzip.fileExt}`)) { staticFile.compressions.push(gzip); } return [file, staticFile]; } ) ); return async (req: Request, resp: TPResponseWriter, next: NextFunction): Promise<void> => { resp.locals.config = config; const prefix = `${req.ip} HTTP/${req.httpVersion} ${req.method} ${req.url} ${req.hostname}`; resp.locals.logger = new Logger(console, environment.production ? LogLevel.INFO : LogLevel.DEBUG, prefix); resp.locals.logger.debug("handling"); resp.locals.startTime = new Date(); resp.locals.foundFiles = foundFiles; next(); }; } /** * errorMiddleWare is a middleware for express.js that provides automatic * handling of errors that aren't caught in the endpoint handlers. * * @param err Any error passed along by other handlers. * @param _ The client request - unused. * @param resp The server's response-writer * @param next A function provided by Express which will call the next handler. */ export function errorMiddleWare(err: unknown, _: Request, resp: TPResponseWriter, next: NextFunction): void { if (err !== null && err !== undefined) { resp.locals.logger.error("unhandled error bubbled to routing:", String(err)); if (!environment.production) { console.trace(err); } if (!resp.locals.endTime) { resp.status(502); // "Bad Gateway" resp.write('{"alerts":[{"level":"error","text":"Unknown Traffic Portal server error occurred"}]}\n'); resp.end("\n"); resp.locals.endTime = new Date(); next(err); } } }