Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
at main 2.8 kB view raw
1/** 2 * Request utilities for validation and helper functions 3 */ 4 5import type { Record as WispSettings } from '@wisp/lexicons/types/place/wisp/settings'; 6import { access } from 'fs/promises'; 7 8/** 9 * Default index file names to check for directory requests 10 * Will be checked in order until one is found 11 */ 12export const DEFAULT_INDEX_FILES = ['index.html', 'index.htm']; 13 14/** 15 * Get index files list from settings or use defaults 16 */ 17export function getIndexFiles(settings: WispSettings | null): string[] { 18 if (settings?.indexFiles && settings.indexFiles.length > 0) { 19 return settings.indexFiles; 20 } 21 return DEFAULT_INDEX_FILES; 22} 23 24/** 25 * Match a file path against a glob pattern 26 * Supports * wildcard and basic path matching 27 */ 28export function matchGlob(path: string, pattern: string): boolean { 29 // Normalize paths 30 const normalizedPath = path.startsWith('/') ? path : '/' + path; 31 const normalizedPattern = pattern.startsWith('/') ? pattern : '/' + pattern; 32 33 // Convert glob pattern to regex 34 const regexPattern = normalizedPattern 35 .replace(/\./g, '\\.') 36 .replace(/\*/g, '.*') 37 .replace(/\?/g, '.'); 38 39 const regex = new RegExp('^' + regexPattern + '$'); 40 return regex.test(normalizedPath); 41} 42 43/** 44 * Apply custom headers from settings to response headers 45 */ 46export function applyCustomHeaders(headers: Record<string, string>, filePath: string, settings: WispSettings | null) { 47 if (!settings?.headers || settings.headers.length === 0) return; 48 49 for (const customHeader of settings.headers) { 50 // If path glob is specified, check if it matches 51 if (customHeader.path) { 52 if (!matchGlob(filePath, customHeader.path)) { 53 continue; 54 } 55 } 56 // Apply the header 57 headers[customHeader.name] = customHeader.value; 58 } 59} 60 61/** 62 * Validate site name (rkey) to prevent injection attacks 63 * Must match AT Protocol rkey format 64 */ 65export function isValidRkey(rkey: string): boolean { 66 if (!rkey || typeof rkey !== 'string') return false; 67 if (rkey.length < 1 || rkey.length > 512) return false; 68 if (rkey === '.' || rkey === '..') return false; 69 if (rkey.includes('/') || rkey.includes('\\') || rkey.includes('\0')) return false; 70 const validRkeyPattern = /^[a-zA-Z0-9._~:-]+$/; 71 return validRkeyPattern.test(rkey); 72} 73 74/** 75 * Async file existence check 76 */ 77export async function fileExists(path: string): Promise<boolean> { 78 try { 79 await access(path); 80 return true; 81 } catch { 82 return false; 83 } 84} 85 86/** 87 * Extract and normalize headers from request 88 */ 89export function extractHeaders(rawHeaders: Headers): Record<string, string> { 90 const headers: Record<string, string> = {}; 91 rawHeaders.forEach((value, key) => { 92 headers[key.toLowerCase()] = value; 93 }); 94 return headers; 95} 96