import * as plugins from '../typedserver.plugins.js'; import * as interfaces from '../interfaces/index.js'; import { Handler } from './classes.handler.js'; import { Compressor, type TCompressionMethod, type ICompressionResult } from './classes.compressor.js'; export class HandlerStatic extends Handler { public compressor = new Compressor(); constructor( pathArg: string, optionsArg?: { requestModifier?: interfaces.TRequestModifier; responseModifier?: interfaces.TResponseModifier; headers?: { [key: string]: string }; serveIndexHtmlDefault?: boolean; enableCompression?: boolean; preferredCompressionMethod?: TCompressionMethod; } ) { super('GET', async (req, res) => { let requestPath = req.path; let requestHeaders = req.headers; let requestBody = req.body; let travelData: unknown; if (optionsArg && optionsArg.requestModifier) { const modifiedRequest = await optionsArg.requestModifier({ headers: requestHeaders, path: requestPath, body: requestBody, }); requestHeaders = modifiedRequest.headers; requestPath = modifiedRequest.path; requestBody = modifiedRequest.body; travelData = modifiedRequest.travelData; } // lets compute some paths let filePath: string = requestPath.slice(req.route.path.length - 1); // lets slice of the root if (requestPath === '') { console.log('replaced root with index.html'); filePath = 'index.html'; } console.log(filePath); const joinedPath = plugins.path.join(pathArg, filePath); const defaultPath = plugins.path.join(pathArg, 'index.html'); let parsedPath = plugins.path.parse(joinedPath); let usedPath: string; // important security checks if ( requestPath.includes('..') || // don't allow going up the filePath requestPath.includes('~') || // don't allow referencing of home directory !joinedPath.startsWith(pathArg) // make sure the joined path is within the directory ) { res.writeHead(500); res.end(); return; } // set additional headers if (optionsArg && optionsArg.headers) { for (const key of Object.keys(optionsArg.headers)) { res.set(key, optionsArg.headers[key]); } } // lets actually care about serving, if security checks pass let fileBuffer: Buffer; try { fileBuffer = plugins.smartfile.fs.toBufferSync(joinedPath); usedPath = joinedPath; } catch (err) { // try serving index.html instead console.log(`could not resolve ${joinedPath}`); if (optionsArg && optionsArg.serveIndexHtmlDefault) { console.log(`serving default path ${defaultPath} instead of ${joinedPath}`); try { parsedPath = plugins.path.parse(defaultPath); fileBuffer = plugins.smartfile.fs.toBufferSync(defaultPath); usedPath = defaultPath; } catch (err) { res.writeHead(500); res.end('File not found!'); return; } } else { res.writeHead(500); res.end('File not found!'); return; } } res.type(parsedPath.ext); const headers = res.getHeaders(); // lets modify the response at last if (optionsArg && optionsArg.responseModifier) { const modifiedResponse = await optionsArg.responseModifier({ headers: res.getHeaders(), path: usedPath, responseContent: fileBuffer, travelData, }); // headers for (const key of Object.keys(res.getHeaders())) { if (!modifiedResponse.headers[key]) { res.removeHeader(key); } } for (const key of Object.keys(modifiedResponse.headers)) { res.setHeader(key, modifiedResponse.headers[key]); } // responseContent fileBuffer = modifiedResponse.responseContent; } // lets finally deal with compression let compressionResult: ICompressionResult; if (optionsArg && optionsArg.enableCompression) { compressionResult = await this.compressor.maybeCompress(requestHeaders, fileBuffer, [optionsArg.preferredCompressionMethod]); } else { compressionResult = { compressionMethod: 'none', result: fileBuffer, }; } res.status(200); if (compressionResult?.compressionMethod) { res.header('Content-Encoding', compressionResult.compressionMethod); } res.write(compressionResult.result); res.end(); }); } }