···
const BASE_HOST = process.env.BASE_HOST || 'wisp.place';
16
+
* Configurable index file names to check for directory requests
17
+
* Will be checked in order until one is found
19
+
const INDEX_FILES = ['index.html', 'index.htm'];
* Validate site name (rkey) to prevent injection attacks
* Must match AT Protocol rkey format
···
// If not forced, check if the requested file exists before redirecting
// Build the expected file path
88
-
let checkPath = filePath || 'index.html';
94
+
let checkPath = filePath || INDEX_FILES[0];
if (checkPath.endsWith('/')) {
90
-
checkPath += 'index.html';
96
+
checkPath += INDEX_FILES[0];
const cachedFile = getCachedFilePath(did, rkey, checkPath);
···
// Internal function to serve a file (used by both normal serving and rewrites)
async function serveFileInternal(did: string, rkey: string, filePath: string) {
136
-
// Default to index.html if path is empty or ends with /
137
-
let requestPath = filePath || 'index.html';
142
+
// Default to first index file if path is empty
143
+
let requestPath = filePath || INDEX_FILES[0];
145
+
// If path ends with /, append first index file
if (requestPath.endsWith('/')) {
139
-
requestPath += 'index.html';
147
+
requestPath += INDEX_FILES[0];
const cacheKey = getCacheKey(did, rkey, requestPath);
const cachedFile = getCachedFilePath(did, rkey, requestPath);
153
+
// Check if the cached file path is a directory
154
+
if (await fileExists(cachedFile)) {
155
+
const { stat } = await import('fs/promises');
157
+
const stats = await stat(cachedFile);
158
+
if (stats.isDirectory()) {
159
+
// It's a directory, try each index file in order
160
+
for (const indexFile of INDEX_FILES) {
161
+
const indexPath = `${requestPath}/${indexFile}`;
162
+
const indexFilePath = getCachedFilePath(did, rkey, indexPath);
163
+
if (await fileExists(indexFilePath)) {
164
+
return serveFileInternal(did, rkey, indexPath);
167
+
// No index file found, fall through to 404
170
+
// If stat fails, continue with normal flow
// Check in-memory cache first
let content = fileCache.get(cacheKey);
let meta = metadataCache.get(cacheKey);
···
return new Response(content, { headers });
204
-
// Try index.html for directory-like paths
233
+
// Try index files for directory-like paths
if (!requestPath.includes('.')) {
206
-
const indexPath = `${requestPath}/index.html`;
207
-
const indexCacheKey = getCacheKey(did, rkey, indexPath);
208
-
const indexFile = getCachedFilePath(did, rkey, indexPath);
235
+
for (const indexFileName of INDEX_FILES) {
236
+
const indexPath = `${requestPath}/${indexFileName}`;
237
+
const indexCacheKey = getCacheKey(did, rkey, indexPath);
238
+
const indexFile = getCachedFilePath(did, rkey, indexPath);
210
-
let indexContent = fileCache.get(indexCacheKey);
211
-
let indexMeta = metadataCache.get(indexCacheKey);
240
+
let indexContent = fileCache.get(indexCacheKey);
241
+
let indexMeta = metadataCache.get(indexCacheKey);
213
-
if (!indexContent && await fileExists(indexFile)) {
214
-
indexContent = await readFile(indexFile);
215
-
fileCache.set(indexCacheKey, indexContent, indexContent.length);
243
+
if (!indexContent && await fileExists(indexFile)) {
244
+
indexContent = await readFile(indexFile);
245
+
fileCache.set(indexCacheKey, indexContent, indexContent.length);
217
-
const indexMetaFile = `${indexFile}.meta`;
218
-
if (await fileExists(indexMetaFile)) {
219
-
const metaJson = await readFile(indexMetaFile, 'utf-8');
220
-
indexMeta = JSON.parse(metaJson);
221
-
metadataCache.set(indexCacheKey, indexMeta!, JSON.stringify(indexMeta).length);
247
+
const indexMetaFile = `${indexFile}.meta`;
248
+
if (await fileExists(indexMetaFile)) {
249
+
const metaJson = await readFile(indexMetaFile, 'utf-8');
250
+
indexMeta = JSON.parse(metaJson);
251
+
metadataCache.set(indexCacheKey, indexMeta!, JSON.stringify(indexMeta).length);
225
-
if (indexContent) {
226
-
const headers: Record<string, string> = {
227
-
'Content-Type': 'text/html; charset=utf-8',
228
-
'Cache-Control': 'public, max-age=300',
255
+
if (indexContent) {
256
+
const headers: Record<string, string> = {
257
+
'Content-Type': 'text/html; charset=utf-8',
258
+
'Cache-Control': 'public, max-age=300',
231
-
if (indexMeta && indexMeta.encoding === 'gzip') {
232
-
headers['Content-Encoding'] = 'gzip';
261
+
if (indexMeta && indexMeta.encoding === 'gzip') {
262
+
headers['Content-Encoding'] = 'gzip';
235
-
return new Response(indexContent, { headers });
265
+
return new Response(indexContent, { headers });
···
// If not forced, check if the requested file exists before redirecting
// Build the expected file path
279
-
let checkPath = filePath || 'index.html';
310
+
let checkPath = filePath || INDEX_FILES[0];
if (checkPath.endsWith('/')) {
281
-
checkPath += 'index.html';
312
+
checkPath += INDEX_FILES[0];
const cachedFile = getCachedFilePath(did, rkey, checkPath);
···
// Internal function to serve a file with rewriting
async function serveFileInternalWithRewrite(did: string, rkey: string, filePath: string, basePath: string) {
332
-
// Default to index.html if path is empty or ends with /
333
-
let requestPath = filePath || 'index.html';
363
+
// Default to first index file if path is empty
364
+
let requestPath = filePath || INDEX_FILES[0];
366
+
// If path ends with /, append first index file
if (requestPath.endsWith('/')) {
335
-
requestPath += 'index.html';
368
+
requestPath += INDEX_FILES[0];
const cacheKey = getCacheKey(did, rkey, requestPath);
const cachedFile = getCachedFilePath(did, rkey, requestPath);
374
+
// Check if the cached file path is a directory
375
+
if (await fileExists(cachedFile)) {
376
+
const { stat } = await import('fs/promises');
378
+
const stats = await stat(cachedFile);
379
+
if (stats.isDirectory()) {
380
+
// It's a directory, try each index file in order
381
+
for (const indexFile of INDEX_FILES) {
382
+
const indexPath = `${requestPath}/${indexFile}`;
383
+
const indexFilePath = getCachedFilePath(did, rkey, indexPath);
384
+
if (await fileExists(indexFilePath)) {
385
+
return serveFileInternalWithRewrite(did, rkey, indexPath, basePath);
388
+
// No index file found, fall through to 404
391
+
// If stat fails, continue with normal flow
// Check for rewritten HTML in cache first (if it's HTML)
const mimeTypeGuess = lookup(requestPath) || 'application/octet-stream';
if (isHtmlContent(requestPath, mimeTypeGuess)) {
···
return new Response(content, { headers });
438
-
// Try index.html for directory-like paths
492
+
// Try index files for directory-like paths
if (!requestPath.includes('.')) {
440
-
const indexPath = `${requestPath}/index.html`;
441
-
const indexCacheKey = getCacheKey(did, rkey, indexPath);
442
-
const indexFile = getCachedFilePath(did, rkey, indexPath);
494
+
for (const indexFileName of INDEX_FILES) {
495
+
const indexPath = `${requestPath}/${indexFileName}`;
496
+
const indexCacheKey = getCacheKey(did, rkey, indexPath);
497
+
const indexFile = getCachedFilePath(did, rkey, indexPath);
444
-
// Check for rewritten index.html in cache
445
-
const rewrittenKey = getCacheKey(did, rkey, indexPath, `rewritten:${basePath}`);
446
-
const rewrittenContent = rewrittenHtmlCache.get(rewrittenKey);
447
-
if (rewrittenContent) {
448
-
return new Response(rewrittenContent, {
450
-
'Content-Type': 'text/html; charset=utf-8',
451
-
'Content-Encoding': 'gzip',
452
-
'Cache-Control': 'public, max-age=300',
499
+
// Check for rewritten index file in cache
500
+
const rewrittenKey = getCacheKey(did, rkey, indexPath, `rewritten:${basePath}`);
501
+
const rewrittenContent = rewrittenHtmlCache.get(rewrittenKey);
502
+
if (rewrittenContent) {
503
+
return new Response(rewrittenContent, {
505
+
'Content-Type': 'text/html; charset=utf-8',
506
+
'Content-Encoding': 'gzip',
507
+
'Cache-Control': 'public, max-age=300',
457
-
let indexContent = fileCache.get(indexCacheKey);
458
-
let indexMeta = metadataCache.get(indexCacheKey);
512
+
let indexContent = fileCache.get(indexCacheKey);
513
+
let indexMeta = metadataCache.get(indexCacheKey);
460
-
if (!indexContent && await fileExists(indexFile)) {
461
-
indexContent = await readFile(indexFile);
462
-
fileCache.set(indexCacheKey, indexContent, indexContent.length);
515
+
if (!indexContent && await fileExists(indexFile)) {
516
+
indexContent = await readFile(indexFile);
517
+
fileCache.set(indexCacheKey, indexContent, indexContent.length);
464
-
const indexMetaFile = `${indexFile}.meta`;
465
-
if (await fileExists(indexMetaFile)) {
466
-
const metaJson = await readFile(indexMetaFile, 'utf-8');
467
-
indexMeta = JSON.parse(metaJson);
468
-
metadataCache.set(indexCacheKey, indexMeta!, JSON.stringify(indexMeta).length);
519
+
const indexMetaFile = `${indexFile}.meta`;
520
+
if (await fileExists(indexMetaFile)) {
521
+
const metaJson = await readFile(indexMetaFile, 'utf-8');
522
+
indexMeta = JSON.parse(metaJson);
523
+
metadataCache.set(indexCacheKey, indexMeta!, JSON.stringify(indexMeta).length);
472
-
if (indexContent) {
473
-
const isGzipped = indexMeta?.encoding === 'gzip';
527
+
if (indexContent) {
528
+
const isGzipped = indexMeta?.encoding === 'gzip';
475
-
let htmlContent: string;
477
-
// Verify content is actually gzipped
478
-
const hasGzipMagic = indexContent.length >= 2 && indexContent[0] === 0x1f && indexContent[1] === 0x8b;
479
-
if (hasGzipMagic) {
480
-
const { gunzipSync } = await import('zlib');
481
-
htmlContent = gunzipSync(indexContent).toString('utf-8');
530
+
let htmlContent: string;
532
+
// Verify content is actually gzipped
533
+
const hasGzipMagic = indexContent.length >= 2 && indexContent[0] === 0x1f && indexContent[1] === 0x8b;
534
+
if (hasGzipMagic) {
535
+
const { gunzipSync } = await import('zlib');
536
+
htmlContent = gunzipSync(indexContent).toString('utf-8');
538
+
console.warn(`Index file marked as gzipped but lacks magic bytes, serving as-is`);
539
+
htmlContent = indexContent.toString('utf-8');
483
-
console.warn(`Index file marked as gzipped but lacks magic bytes, serving as-is`);
htmlContent = indexContent.toString('utf-8');
487
-
htmlContent = indexContent.toString('utf-8');
489
-
const rewritten = rewriteHtmlPaths(htmlContent, basePath, indexPath);
544
+
const rewritten = rewriteHtmlPaths(htmlContent, basePath, indexPath);
491
-
const { gzipSync } = await import('zlib');
492
-
const recompressed = gzipSync(Buffer.from(rewritten, 'utf-8'));
546
+
const { gzipSync } = await import('zlib');
547
+
const recompressed = gzipSync(Buffer.from(rewritten, 'utf-8'));
494
-
rewrittenHtmlCache.set(rewrittenKey, recompressed, recompressed.length);
549
+
rewrittenHtmlCache.set(rewrittenKey, recompressed, recompressed.length);
496
-
return new Response(recompressed, {
498
-
'Content-Type': 'text/html; charset=utf-8',
499
-
'Content-Encoding': 'gzip',
500
-
'Cache-Control': 'public, max-age=300',
551
+
return new Response(recompressed, {
553
+
'Content-Type': 'text/html; charset=utf-8',
554
+
'Content-Encoding': 'gzip',
555
+
'Cache-Control': 'public, max-age=300',