/** * Handles requests for route beneath a directory with a .spa file dropped in it. * @module */ import * as path from '@std/path'; import { PRECHECK, SERVER } from '../server.ts'; import { getCookies } from '@std/http/cookie'; import { load_html_with_ssi } from './html.ts'; async function find_spa_file_root(request_path: string): Promise { let current_path = Deno.cwd(); const relative_path = path.relative(current_path, request_path); const path_elements = relative_path.split('/').slice(0, -1); let element; while ((element = path_elements.shift())) { current_path = path.join(current_path, element); try { const spa_file_stat = await Deno.stat(path.join(current_path, '.spa')); if (spa_file_stat.isFile) { return current_path; } } catch (error) { if (error instanceof Deno.errors.NotFound) { continue; } throw error; } } return undefined; } export type HTTP_METHOD = 'GET' | 'PUT' | 'POST' | 'DELETE' | 'HEAD' | 'OPTIONS'; export type HANDLER_METHOD = ( request: Request, normalized_path: string, server: SERVER ) => Promise | Response | undefined; export const PRECHECKS: Partial> = {}; export const HANDLERS: Partial> = { HEAD: async (_request: Request, normalized_path: string, _server: SERVER): Promise => { const spa_root = await find_spa_file_root(normalized_path); if (!spa_root) { return; } for await (const index_filename of ['index.html', 'index.htm']) { try { const index_file_path = path.join(spa_root, index_filename); const index_file_stat = await Deno.stat(index_file_path); if (index_file_stat.isFile) { return new Response('', { headers: { 'Content-Type': 'text/html', 'Content-Length': `${index_file_stat.size}`, 'Last-Modified': `${index_file_stat.mtime ?? index_file_stat.ctime}` } }); } } catch (error) { if (error instanceof Deno.errors.NotFound) { continue; } throw error; } } }, GET: async (request: Request, normalized_path: string, _server: SERVER): Promise => { const spa_root = await find_spa_file_root(normalized_path); if (!spa_root) { return; } for await (const index_filename of ['index.html', 'index.htm']) { try { const index_file_path = path.join(spa_root, index_filename); const index_file_stat = await Deno.stat(index_file_path); if (index_file_stat.isFile) { const processed: string = await load_html_with_ssi(index_file_path); const accepts = request.headers.get('accept') ?? 'text/html'; if (!['*/*', 'text/html', 'text/plain'].includes(accepts)) { return new Response('unsupported accepts header for SPA: ' + accepts, { status: 400 }); } return new Response(processed, { headers: { 'Content-Type': accepts === '*/*' ? 'text/html' : accepts } }); } } catch (error) { if (error instanceof Deno.errors.NotFound) { continue; } throw error; } } }, OPTIONS: async (request: Request, normalized_path: string): Promise => { const spa_root = await find_spa_file_root(normalized_path); if (!spa_root) { return; } for await (const index_filename of ['index.html', 'index.htm']) { try { const index_file_path = path.join(spa_root, index_filename); const index_file_stat = await Deno.stat(index_file_path); if (index_file_stat.isFile) { const accepts = request.headers.get('accept') ?? 'text/html'; if (!['*/*', 'text/html', 'text/plain'].includes(accepts)) { return new Response('unsupported accepts header for SPA: ' + accepts, { status: 400 }); } const allowed = ['GET', 'HEAD', 'OPTIONS']; return new Response('', { headers: { 'Allow': allowed.sort().join(','), 'Access-Control-Allow-Origin': Deno.env.get('SERVERUS_ACCESS_CONTROL_ALLOW_ORIGIN') ?? '*' } }); } } catch (error) { if (error instanceof Deno.errors.NotFound) { continue; } throw error; } } } }; /** * Handles requests for static files. * * @param request The incoming HTTP request * @returns Either a response (a static file was requested and returned properly) or undefined if unhandled. */ export default async function handle_spa_files_in_path(request: Request, server: SERVER): Promise { const method: HTTP_METHOD = request.method.toUpperCase() as HTTP_METHOD; const handler: HANDLER_METHOD | undefined = HANDLERS[method]; if (!handler) { return; } const url = new URL(request.url); const normalized_path = path.resolve(path.normalize(decodeURIComponent(url.pathname)).replace(/^\/+/, '')); // if they're requesting something outside the working dir, just bail if (!normalized_path.startsWith(Deno.cwd())) { return; } const prechecks: PRECHECK[] = PRECHECKS[method] ?? []; const cookies: Record = getCookies(request.headers); const query = Object.fromEntries(new URL(request.url).searchParams.entries()); const metadata = { cookies, query }; for await (const precheck of prechecks) { const error_response: Response | undefined = await precheck(request, metadata); if (error_response) { return error_response; } } return await handler(request, normalized_path, server); }