koa-classic-server 3.0.0-alpha.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,35 +1,34 @@
1
1
  # koa-classic-server
2
2
 
3
- 🚀 **Production-ready Koa middleware** for serving static files with Apache2-like directory listing, sortable columns, HTTP caching, template engine support, and enterprise-grade security.
3
+ 🚀 **Production-ready Koa middleware** for serving static files with Apache2-like directory listing, sortable columns, pagination, hash-based CSP, template-engine timeouts, injectable logging, and enterprise-grade security.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/koa-classic-server.svg)](https://www.npmjs.com/package/koa-classic-server)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
- [![Tests](https://img.shields.io/badge/tests-309%20passing-brightgreen.svg)]()
7
+ [![Tests](https://img.shields.io/badge/tests-532%20passing-brightgreen.svg)]()
8
+ [![Node](https://img.shields.io/badge/node-%3E%3D18-blue.svg)]()
8
9
 
9
10
  ---
10
11
 
11
- ## 🎉 Version 2.X - Production-Ready Release
12
-
13
- The 2.X series brings major performance improvements, enhanced security, and powerful new features while maintaining full backward compatibility.
14
-
15
- ### Key Features in Version 2.X
16
-
17
- ✅ **URL Rewriting Support** - Compatible with i18n and routing middleware via `useOriginalUrl` option
18
- ✅ **Improved Caching Controls** - Clear `browserCacheEnabled` and `browserCacheMaxAge` options
19
- ✅ **Development-Friendly Defaults** - Caching disabled by default for easier development
20
- ✅ **Production Optimized** - Enable caching in production for 80-95% bandwidth reduction
21
- ✅ **Sortable Directory Columns** - Click Name/Type/Size to sort (Apache2-like)
22
- ✅ **File Size Display** - Human-readable file sizes (B, KB, MB, GB, TB)
23
- ✅ **HTTP Caching** - ETag and Last-Modified headers with 304 responses
24
- ✅ **Async/Await** - Non-blocking I/O for high performance
25
- ✅ **Performance Optimized** - 50-70% faster directory listings
26
- ✅ **Enhanced Index Option** - Array format with RegExp support
27
- ✅ **Template Engine Support** - EJS, Pug, Handlebars, Nunjucks, and more
28
- ✅ **Enterprise Security** - Path traversal, XSS, race condition protection
29
- ✅ **Clean URLs** - Hide file extensions with `hideExtension` (mod_rewrite-like behavior)
30
- ✅ **Symlink Support** - Full symbolic link support (NixOS, Docker, npm link, Capistrano)
31
- ✅ **Comprehensive Testing** - 309 tests passing with extensive coverage
32
- ✅ **Complete Documentation** - Detailed guides and examples
12
+ ## 🎉 Version 3.0 File Server First, Observable, Bounded
13
+
14
+ The 3.0 series builds on 2.x with new observability hooks, bounded resource usage on accidentally-large directories, and a more focused **design philosophy**: koa-classic-server is an **HTTP file server first** — defaults serve files without applying surprise restrictions, and hardening is opt-in via explicit configuration plus a documented Security Checklist.
15
+
16
+ ### Key Features in Version 3.x
17
+
18
+ ✅ **Design philosophy made explicit** *"if a file is in `rootDir`, `GET` returns it"* — codified in [`CLAUDE.md`](./CLAUDE.md), with a **Security Checklist** + **Suggested Production Security Configuration** in this README and `docs/DOCUMENTATION.md`
19
+ **`dirListing` namespace** listing options grouped under one structured object (`enabled`, `maxEntries`, `entriesPerPage`); the v2 `showDirContents` flag is kept as a deprecated alias with a one-time warning
20
+ ✅ **Soft cap on listing rendering** — `dirListing.maxEntries` defaults to `100000` as a *safety net* against accidentally-huge directories (broken log rotation, mistakenly mounted FS), NOT as a policy restriction; banner + `X-Dir-Truncated` header on the rare hit. Opt-in RAM-bounded streaming reads planned for v3.1.
21
+ ✅ **Paginated listings** — `dirListing.entriesPerPage` adds 0-based `?page=N` navigation with First/Prev/Next/Last + `X-Dir-Pagination` header
22
+ ✅ **Template render timeout + AbortSignal** — `template.renderTimeout` (default 30s) + a per-request `template.signal` so slow renders never wedge the server
23
+ ✅ **Injectable logger** pass any `{ error, warn, info, debug }`-shaped logger (Pino, Bunyan, Winston, console) for full observability
24
+ ✅ **Hash-based CSP on listing page** — automatic SHA-256 of inline CSS, recomputed at module load
25
+ ✅ **Security headers on generated pages** — `CSP`, `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy` on listing + error pages
26
+ ✅ **Sortable Directory Columns** Click Name/Type/Size to sort (Apache2-like) with sort/order preserved across paginator links
27
+ ✅ **HTTP Caching** — ETag, Last-Modified, conditional 304 responses (opt-in via `browserCacheEnabled`)
28
+ ✅ **Template Engine Support** EJS, Pug, Handlebars, Nunjucks, and more — with full async/await, AbortSignal forwarding, and timeout enforcement
29
+ ✅ **Clean URLs** Hide file extensions via `hideExtension` (mod_rewrite-like)
30
+ ✅ **Symlink Support** Transparent resolution + clear indicators in the listing
31
+ ✅ **532 tests passing** comprehensive coverage including security, listing pagination, logger injection, template timeouts, and edge cases
33
32
 
34
33
  [See full changelog →](./docs/CHANGELOG.md)
35
34
 
@@ -37,23 +36,26 @@ The 2.X series brings major performance improvements, enhanced security, and pow
37
36
 
38
37
  ## Features
39
38
 
40
- **koa-classic-server** is a high-performance middleware for serving static files with Apache2-like behavior, making file browsing intuitive and powerful.
39
+ **koa-classic-server** is a high-performance middleware for serving static files with Apache2-like behavior, making file browsing intuitive, observable, and safe.
41
40
 
42
41
  ### Core Features
43
42
 
44
- - 🗂️ **Apache2-like Directory Listing** - Sortable columns (Name, Type, Size)
45
- - 📄 **Static File Serving** - Automatic MIME type detection with streaming
46
- - 📊 **Sortable Columns** - Click headers to sort ascending/descending
47
- - 📏 **File Sizes** - Human-readable display (B, KB, MB, GB, TB)
48
- - **HTTP Caching** - ETag, Last-Modified, 304 responses
49
- - 🎨 **Template Engine Support** - EJS, Pug, Handlebars, Nunjucks, etc.
50
- - 🔒 **Enterprise Security** - Path traversal, XSS, race condition protection
51
- - ⚙️ **Highly Configurable** - URL prefixes, reserved paths, index files
52
- - 🚀 **High Performance** - Async/await, non-blocking I/O, optimized algorithms
53
- - 🔗 **Symlink Support** - Transparent symlink resolution with directory listing indicators
54
- - 🌐 **Clean URLs** - Hide file extensions for SEO-friendly URLs via `hideExtension`
55
- - 🧪 **Well-Tested** - 309 passing tests with comprehensive coverage
56
- - 📦 **Dual Module Support** - CommonJS and ES Modules
43
+ - 🗂️ **Apache2-like Directory Listing** Sortable columns (Name, Type, Size)
44
+ - 📄 **Static File Serving** Automatic MIME type detection with streaming
45
+ - 📊 **Sortable Columns** Click headers to sort ascending/descending
46
+ - 📏 **File Sizes** Human-readable display (B, KB, MB, GB, TB)
47
+ - 📃 **Bounded + Paginated Listings** `dirListing.maxEntries` cap + `dirListing.entriesPerPage` navigation
48
+ - ⏱️ **Template Render Timeout** Configurable timeout with AbortSignal propagation
49
+ - 📝 **Injectable Logger** Plug Pino/Bunyan/Winston/console at construction time
50
+ - **HTTP Caching** ETag, Last-Modified, 304 responses (opt-in)
51
+ - 🎨 **Template Engine Support** EJS, Pug, Handlebars, Nunjucks, etc.
52
+ - 🔒 **Enterprise Security** Path traversal, XSS, race condition protection, CSP, dot-file hiding
53
+ - ⚙️ **Highly Configurable** URL prefixes, reserved paths, index files, hidden patterns
54
+ - 🚀 **High Performance** Async/await, non-blocking I/O, single-syscall directory reads
55
+ - 🔗 **Symlink Support** Transparent resolution with directory listing indicators
56
+ - 🌐 **Clean URLs** — Hide file extensions for SEO-friendly URLs via `hideExtension`
57
+ - 🧪 **Well-Tested** — 532 passing tests with comprehensive coverage
58
+ - 📦 **Dual Module Support** — CommonJS and ES Modules
57
59
 
58
60
  ---
59
61
 
@@ -95,11 +97,15 @@ const koaClassicServer = require('koa-classic-server');
95
97
  const app = new Koa();
96
98
 
97
99
  app.use(koaClassicServer(__dirname + '/public', {
98
- showDirContents: true,
99
100
  index: ['index.html', 'index.htm'],
100
101
  urlPrefix: '/static',
101
- browserCacheMaxAge: 3600,
102
- browserCacheEnabled: true
102
+ dirListing: {
103
+ enabled: true,
104
+ maxEntries: 5000, // cap huge directories
105
+ entriesPerPage: 50, // 50 entries per listing page
106
+ },
107
+ browserCacheEnabled: true,
108
+ browserCacheMaxAge: 3600,
103
109
  }));
104
110
 
105
111
  app.listen(3000);
@@ -121,365 +127,222 @@ import koaClassicServer from 'koa-classic-server';
121
127
 
122
128
  ### 2. Basic File Server
123
129
 
124
- Serve static files from a directory:
125
-
126
130
  ```javascript
127
131
  const Koa = require('koa');
132
+ const path = require('path');
128
133
  const koaClassicServer = require('koa-classic-server');
129
134
 
130
135
  const app = new Koa();
131
136
 
132
- app.use(koaClassicServer(__dirname + '/public', {
133
- showDirContents: true,
134
- index: ['index.html']
137
+ app.use(koaClassicServer(path.join(__dirname, 'public'), {
138
+ dirListing: { enabled: true },
139
+ index: ['index.html'],
135
140
  }));
136
141
 
137
142
  app.listen(3000);
138
143
  ```
139
144
 
140
- **What it does:**
141
- - Serves files from `/public` directory
142
- - Shows directory listing when accessing folders
143
- - Looks for `index.html` in directories
144
- - Sortable columns (Name, Type, Size)
145
- - File sizes displayed in human-readable format
146
-
147
145
  ### 3. With URL Prefix
148
146
 
149
- Serve files under a specific URL path:
150
-
151
147
  ```javascript
152
- app.use(koaClassicServer(__dirname + '/assets', {
148
+ app.use(koaClassicServer(__dirname + '/public', {
153
149
  urlPrefix: '/static',
154
- showDirContents: true
155
150
  }));
151
+ // http://localhost:3000/static/image.png → public/image.png
156
152
  ```
157
153
 
158
- **Result:**
159
- - `http://localhost:3000/static/image.png` → serves `/assets/image.png`
160
- - `http://localhost:3000/static/` → shows `/assets` directory listing
161
-
162
154
  ### 4. With Reserved Paths
163
155
 
164
- Protect specific directories from being accessed:
165
-
166
156
  ```javascript
167
- app.use(koaClassicServer(__dirname + '/www', {
168
- urlsReserved: ['/admin', '/config', '/.git', '/node_modules']
157
+ app.use(koaClassicServer(__dirname, {
158
+ urlsReserved: ['/api', '/admin', '/.git', '/node_modules'],
169
159
  }));
160
+ // /api/* is passed through to the next middleware untouched.
170
161
  ```
171
162
 
172
- **Result:**
173
- - `/admin/*` → passed to next middleware (not served)
174
- - `/config/*` → protected
175
- - Other paths → served normally
163
+ ### 5. Bounded + Paginated Directory Listings (V3)
176
164
 
177
- ### 5. With Template Engine (EJS)
178
-
179
- Dynamically render templates with data:
165
+ For directories that may grow without bound (uploads, archives, logs), cap the maximum number of entries the middleware will enumerate and paginate what's visible:
180
166
 
181
167
  ```javascript
182
- const ejs = require('ejs');
183
-
184
- app.use(koaClassicServer(__dirname + '/views', {
185
- template: {
186
- ext: ['ejs', 'html.ejs'],
187
- render: async (ctx, next, filePath) => {
188
- const data = {
189
- title: 'My App',
190
- user: ctx.state.user || { name: 'Guest' },
191
- items: ['Item 1', 'Item 2', 'Item 3'],
192
- timestamp: new Date().toISOString()
193
- };
194
-
195
- ctx.body = await ejs.renderFile(filePath, data);
196
- ctx.type = 'text/html';
197
- }
198
- }
168
+ app.use(koaClassicServer(__dirname + '/uploads', {
169
+ dirListing: {
170
+ enabled: true,
171
+ maxEntries: 10000, // cap visible / sorted / stat'd entries (default; 0 = disabled)
172
+ entriesPerPage: 100, // entries per page in the listing UI (default; 0 = disabled)
173
+ },
199
174
  }));
200
175
  ```
201
176
 
202
- **Template example (`views/dashboard.ejs`):**
203
- ```html
204
- <!DOCTYPE html>
205
- <html>
206
- <head>
207
- <title><%= title %></title>
208
- </head>
209
- <body>
210
- <h1>Welcome, <%= user.name %>!</h1>
211
- <ul>
212
- <% items.forEach(item => { %>
213
- <li><%= item %></li>
214
- <% }); %>
215
- </ul>
216
- <p>Generated at: <%= timestamp %></p>
217
- </body>
218
- </html>
219
- ```
177
+ **What happens on a directory with 1,000,000 files**
220
178
 
221
- **See complete guide:** [Template Engine Documentation →](./docs/template-engine/TEMPLATE_ENGINE_GUIDE.md)
179
+ - The middleware calls `fs.promises.readdir()` once and slices the result to `dirListing.maxEntries` — sorting, stat'ing, and rendering are CPU-bounded by `dirListing.maxEntries`. The initial `readdir()` itself is **not** bounded (see v3.1 roadmap for an opt-in streaming mode targeting adversarial-directory workloads).
180
+ - A yellow banner appears at the top of the listing: *"Showing first 10000 entries (cap reached)…"*
181
+ - The response carries `X-Dir-Truncated: 10000` so monitoring can flag capped pages.
182
+ - Pagination is rendered below the table with `« First · ‹ Prev · 0 1 … N · Next › · Last »`, and an `X-Dir-Pagination: <current>/<last>` response header is set.
183
+ - Navigate via `?page=N` (0-based). Out-of-range values clamp silently to the nearest valid page. Active `sort` / `order` query params are preserved across paginator links.
222
184
 
223
- ### 6. Clean URLs with hideExtension
185
+ ### 6. Template Engine with Timeout + AbortSignal (V3)
224
186
 
225
- Hide file extensions from URLs, similar to Apache's `mod_rewrite`:
187
+ V3 hardens template rendering against runaway or hung renders: the middleware enforces a configurable timeout and forwards a `template.signal` (AbortSignal) you can use inside your renderer to abort I/O and long-running work.
226
188
 
227
189
  ```javascript
228
190
  const ejs = require('ejs');
191
+ const koaClassicServer = require('koa-classic-server');
229
192
 
230
- app.use(koaClassicServer(__dirname + '/public', {
231
- showDirContents: true,
232
- index: ['index.ejs'],
233
- hideExtension: {
234
- ext: '.ejs', // Extension to hide (required)
235
- redirect: 301 // HTTP redirect code (optional, default: 301)
236
- },
193
+ app.use(koaClassicServer(__dirname + '/views', {
237
194
  template: {
238
195
  ext: ['ejs'],
239
- render: async (ctx, next, filePath) => {
196
+ renderTimeout: 5000, // 5s hard cap (default 30000ms; 0 disables the cap)
197
+ render: async (ctx, next, filePath, { signal }) => {
198
+ // Forward the signal to your I/O — fetch, DB queries, async work.
199
+ const data = await fetchData({ signal });
200
+ if (signal.aborted) return;
240
201
  ctx.body = await ejs.renderFile(filePath, data);
241
- ctx.type = 'text/html';
242
- }
243
- }
202
+ ctx.type = 'text/html';
203
+ },
204
+ },
244
205
  }));
245
206
  ```
246
207
 
247
- **URL Behavior:**
248
-
249
- | Request URL | Action | Result |
250
- |-------------|--------|--------|
251
- | `/about` | Resolves `about.ejs` | Serves file (200) |
252
- | `/blog/article` | Resolves `blog/article.ejs` | Serves file (200) |
253
- | `/about.ejs` | Redirect | 301 → `/about` |
254
- | `/about.ejs?lang=it` | Redirect | 301 → `/about?lang=it` |
255
- | `/index.ejs` | Redirect | 301 → `/` |
256
- | `/section/index.ejs` | Redirect | 301 → `/section/` |
257
- | `/style.css` | No interference | Normal flow |
258
- | `/about/` | No interference | Shows directory listing |
259
-
260
- **Conflict Resolution:**
208
+ If the renderer exceeds `renderTimeout`, the request fails closed with a 500 and a single warning is emitted via the configured logger — the response stream is never left half-written.
261
209
 
262
- - **Directory vs file**: When both `about/` directory and `about.ejs` file exist, `/about` serves the file. Use `/about/` to access the directory.
263
- - **Extensionless vs extension**: When both `about` (no ext) and `about.ejs` exist, `/about` always serves `about.ejs`. The extensionless file becomes unreachable.
210
+ ### 7. Injectable Logger (V3)
264
211
 
265
- > **Note**: This conflict resolution behavior differs from Apache/Nginx, where directories typically take priority over files with the same base name.
266
-
267
- ### 7. URL Rewriting Support (useOriginalUrl)
268
-
269
- When using URL rewriting middleware (i18n, routing), set `useOriginalUrl: false` so koa-classic-server resolves files from the rewritten URL instead of the original one:
212
+ By default the middleware logs to `console`. Pass any object exposing `error`, `warn`, `info`, `debug` to integrate with your production logging stack:
270
213
 
271
214
  ```javascript
272
- const Koa = require('koa');
273
- const koaClassicServer = require('koa-classic-server');
215
+ const pino = require('pino')();
274
216
 
275
- const app = new Koa();
276
-
277
- // i18n middleware: strips language prefix and rewrites ctx.url
278
- app.use(async (ctx, next) => {
279
- const langMatch = ctx.path.match(/^\/(it|en|fr|de|es)\//);
280
- if (langMatch) {
281
- ctx.state.lang = langMatch[1]; // Save language for templates
282
- ctx.url = ctx.path.replace(/^\/(it|en|fr|de|es)/, '') + ctx.search;
283
- }
284
- await next();
285
- });
286
-
287
- // Serve files using the rewritten URL
288
217
  app.use(koaClassicServer(__dirname + '/public', {
289
- useOriginalUrl: false // Use ctx.url (rewritten) instead of ctx.originalUrl
218
+ logger: pino, // any { error, warn, info, debug }-shaped object works
290
219
  }));
291
-
292
- app.listen(3000);
293
220
  ```
294
221
 
295
- **How it works:**
296
-
297
- | Request | `ctx.originalUrl` | `ctx.url` (rewritten) | File resolved |
298
- |---------|-------------------|----------------------|---------------|
299
- | `/it/page.html` | `/it/page.html` | `/page.html` | `public/page.html` |
300
- | `/en/page.html` | `/en/page.html` | `/page.html` | `public/page.html` |
301
- | `/page.html` | `/page.html` | `/page.html` | `public/page.html` |
222
+ - Backward compatible: when `logger` is omitted, behavior is unchanged (uses `console`).
223
+ - All internal warnings and errors flow through the same logger — useful for routing them to Sentry, Datadog, or stdout JSON.
302
224
 
303
- - With `useOriginalUrl: true` (default): the server would look for `public/it/page.html` (which doesn't exist)
304
- - With `useOriginalUrl: false`: the server looks for `public/page.html` (correct)
225
+ ### 8. Hidden Files & Dot-File Protection (V3 default: hidden)
305
226
 
306
- ### 8. Advanced hideExtension Scenarios
227
+ Dot-files and dot-directories are **visible by default in v3** — aligned with the "file server first" philosophy (see [`CLAUDE.md`](./CLAUDE.md)). For production deployments where `.env`, `.git/config`, etc. could be served accidentally, **opt into hardening** explicitly via `hidden.dotFiles.default: 'hidden'`. This is the first item on the [Security Checklist](#design-philosophy--security-checklist).
307
228
 
308
- #### Recommended file structure
309
-
310
- ```
311
- views/
312
- ├── index.ejs ← / (home page)
313
- ├── about.ejs ← /about
314
- ├── contact.ejs ← /contact
315
- ├── blog/
316
- │ ├── index.ejs ← /blog/
317
- ├── first-post.ejs ← /blog/first-post
318
- │ └── second-post.ejs ← /blog/second-post
319
- ├── docs/
320
- │ ├── index.ejs ← /docs/
321
- │ ├── getting-started.ejs /docs/getting-started
322
- │ └── api-reference.ejs ← /docs/api-reference
323
- └── assets/
324
- ├── style.css ← /assets/style.css (served normally)
325
- └── script.js ← /assets/script.js (served normally)
229
+ ```javascript
230
+ app.use(koaClassicServer(__dirname + '/www', {
231
+ hidden: {
232
+ dotFiles: {
233
+ default: 'hidden', // 'hidden' | 'visible'
234
+ whitelist: ['.well-known', '.htaccess'],// exact name, glob, or RegExp
235
+ blacklist: [], // overrides whitelist
236
+ },
237
+ dotDirs: {
238
+ default: 'visible',
239
+ whitelist: [],
240
+ blacklist: ['.git'],
241
+ },
242
+ alwaysHide: ['*.key', /secret/i, '/private/**'], // path-aware patterns
243
+ },
244
+ }));
326
245
  ```
327
246
 
328
- #### hideExtension with i18n middleware
247
+ ### 9. Clean URLs with `hideExtension`
329
248
 
330
- Combine `hideExtension` with `useOriginalUrl: false` for multilingual sites with clean URLs:
249
+ Serve `.ejs` (or any extension) as extensionless URLs and 301-redirect the canonical form:
331
250
 
332
251
  ```javascript
333
- const Koa = require('koa');
334
- const koaClassicServer = require('koa-classic-server');
335
- const ejs = require('ejs');
336
-
337
- const app = new Koa();
338
-
339
- // i18n middleware: /it/about → ctx.url = /about, ctx.state.lang = 'it'
340
- app.use(async (ctx, next) => {
341
- const langMatch = ctx.path.match(/^\/(it|en|fr)\//);
342
- if (langMatch) {
343
- ctx.state.lang = langMatch[1];
344
- ctx.url = ctx.path.replace(/^\/(it|en|fr)/, '') + ctx.search;
345
- } else {
346
- ctx.state.lang = 'en'; // Default language
347
- }
348
- await next();
349
- });
350
-
351
252
  app.use(koaClassicServer(__dirname + '/views', {
352
- index: ['index.ejs'],
353
- useOriginalUrl: false, // Resolve files from rewritten URL
354
253
  hideExtension: {
355
- ext: '.ejs',
356
- redirect: 301
254
+ ext: '.ejs', // required, must start with '.'
255
+ redirect: 301, // optional, 301 (default) or 302
357
256
  },
358
257
  template: {
359
258
  ext: ['ejs'],
360
259
  render: async (ctx, next, filePath) => {
361
- ctx.body = await ejs.renderFile(filePath, { lang: ctx.state.lang });
362
- ctx.type = 'text/html';
363
- }
364
- }
260
+ ctx.body = await ejs.renderFile(filePath);
261
+ ctx.type = 'text/html';
262
+ },
263
+ },
365
264
  }));
366
-
367
- app.listen(3000);
265
+ // GET /about → serves views/about.ejs
266
+ // GET /about.ejs → 301 redirect to /about
368
267
  ```
369
268
 
370
- **Result:**
371
-
372
- | Request | Rewritten URL | File resolved | Redirect target |
373
- |---------|---------------|---------------|-----------------|
374
- | `/it/about` | `/about` | `views/about.ejs` | — |
375
- | `/en/blog/first-post` | `/blog/first-post` | `views/blog/first-post.ejs` | — |
376
- | `/it/about.ejs` | `/about.ejs` | — | 301 → `/it/about` (preserves `/it/` prefix) |
377
-
378
- > **Note**: Redirects always use `ctx.originalUrl` to preserve the language prefix, regardless of the `useOriginalUrl` setting.
269
+ ### 10. URL Rewriting Support (`useOriginalUrl`)
379
270
 
380
- #### Temporary redirect (302)
381
-
382
- Use `redirect: 302` instead of 301 when the URL mapping may change (staging, A/B testing, or during migration):
271
+ Set `useOriginalUrl: false` when running behind i18n routers or path-rewriters that mutate `ctx.url`:
383
272
 
384
273
  ```javascript
385
- hideExtension: {
386
- ext: '.ejs',
387
- redirect: 302 // Temporary redirect browsers won't cache it
388
- }
389
- ```
390
-
391
- > **When to use 302**: A 301 (permanent) tells browsers and search engines to cache the redirect. Use 302 (temporary) during development, staging, or when you're not yet sure the clean URL structure is final.
392
-
393
- ### 9. With HTTP Caching
394
-
395
- Enable aggressive caching for static files:
274
+ app.use(async (ctx, next) => {
275
+ if (ctx.path.startsWith('/it/')) {
276
+ ctx.url = ctx.path.replace(/^\/it/, ''); // /it/page.html /page.html
277
+ }
278
+ await next();
279
+ });
396
280
 
397
- ```javascript
398
- app.use(koaClassicServer(__dirname + '/public', {
399
- browserCacheEnabled: true, // Enable ETag and Last-Modified
400
- browserCacheMaxAge: 86400, // Cache for 24 hours (in seconds)
281
+ app.use(koaClassicServer(__dirname + '/www', {
282
+ useOriginalUrl: false, // use ctx.url (rewritten) instead of ctx.originalUrl
401
283
  }));
402
284
  ```
403
285
 
404
- **⚠️ Important: Production Recommendation**
405
-
406
- The default value for `browserCacheEnabled` is `false` to facilitate development (where you want changes to be immediately visible). **For production environments, it is strongly recommended to set `browserCacheEnabled: true`** to benefit from:
407
-
408
- - 80-95% bandwidth reduction
409
- - 304 Not Modified responses for unchanged files
410
- - Faster page loads for returning visitors
411
- - Reduced server load
412
-
413
- **See details:** [HTTP Caching Optimization →](./docs/OPTIMIZATION_HTTP_CACHING.md)
414
-
415
- ### 10. Multiple Index Files with Priority
416
-
417
- Search for multiple index files with custom order:
286
+ ### 11. HTTP Caching (opt-in)
418
287
 
419
288
  ```javascript
420
289
  app.use(koaClassicServer(__dirname + '/public', {
421
- index: [
422
- 'index.html', // First priority
423
- 'index.htm', // Second priority
424
- /index\.[eE][jJ][sS]/, // Third: index.ejs (case-insensitive)
425
- 'default.html' // Last priority
426
- ]
290
+ browserCacheEnabled: true, // emit ETag + Last-Modified, honor If-None-Match / If-Modified-Since
291
+ browserCacheMaxAge: 86400, // Cache-Control: max-age=86400 (24h)
427
292
  }));
428
293
  ```
429
294
 
430
- **See details:** [Index Option Priority →](./docs/INDEX_OPTION_PRIORITY.md)
431
-
432
- ### 11. Complete Production Example
295
+ Defaults: `browserCacheEnabled: false` (development-friendly). Enable in production for an 80–95% bandwidth reduction on cache hits.
433
296
 
434
- Real-world configuration for production:
297
+ ### 12. Complete Production Example
435
298
 
436
299
  ```javascript
437
- const Koa = require('koa');
438
- const koaClassicServer = require('koa-classic-server');
439
- const ejs = require('ejs');
300
+ const Koa = require('koa');
440
301
  const path = require('path');
302
+ const pino = require('pino')({ level: 'info' });
303
+ const ejs = require('ejs');
304
+ const koaClassicServer = require('koa-classic-server');
441
305
 
442
306
  const app = new Koa();
443
307
 
444
- // Serve static assets with caching
308
+ // Allowlist Host headers to mitigate DNS rebinding (see docs/DOCUMENTATION.md → Sicurezza).
309
+ const ALLOWED_HOSTS = new Set(['app.example.com', 'localhost:3000']);
310
+ app.use(async (ctx, next) => {
311
+ if (!ALLOWED_HOSTS.has(ctx.host)) { ctx.status = 421; ctx.body = 'Host not allowed'; return; }
312
+ await next();
313
+ });
314
+
315
+ // Static-file security headers (see docs/DOCUMENTATION.md → Limiti dei Security Headers).
316
+ app.use(async (ctx, next) => {
317
+ ctx.set('X-Content-Type-Options', 'nosniff');
318
+ ctx.set('Referrer-Policy', 'strict-origin-when-cross-origin');
319
+ ctx.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains');
320
+ await next();
321
+ });
322
+
445
323
  app.use(koaClassicServer(path.join(__dirname, 'public'), {
446
- method: ['GET', 'HEAD'],
447
- showDirContents: false, // Disable directory listing in production
448
- index: ['index.html', 'index.htm'],
449
- urlPrefix: '/assets',
450
- urlsReserved: ['/admin', '/api', '/.git'],
324
+ index: ['index.html'],
325
+ dirListing: {
326
+ enabled: process.env.NODE_ENV !== 'production',
327
+ maxEntries: 10000,
328
+ entriesPerPage: 100,
329
+ },
451
330
  browserCacheEnabled: true,
452
- browserCacheMaxAge: 86400, // 24 hours
453
- }));
454
-
455
- // Serve dynamic templates with clean URLs
456
- app.use(koaClassicServer(path.join(__dirname, 'views'), {
457
- showDirContents: false,
458
- index: ['index.ejs'],
459
- useOriginalUrl: false, // Use ctx.url (for i18n or routing middleware)
460
- hideExtension: {
461
- ext: '.ejs', // /about → serves about.ejs
462
- redirect: 301 // /about.ejs → 301 redirect to /about
331
+ browserCacheMaxAge: 86400,
332
+ logger: pino,
333
+ hidden: {
334
+ dotFiles: { default: 'hidden', whitelist: ['.well-known'] },
335
+ dotDirs: { default: 'hidden', whitelist: ['.well-known'] },
336
+ alwaysHide: ['*.key', /^backup-/],
463
337
  },
464
338
  template: {
465
- ext: ['ejs'],
466
- render: async (ctx, next, filePath) => {
467
- const data = {
468
- env: process.env.NODE_ENV,
469
- user: ctx.state.user,
470
- config: ctx.state.config
471
- };
472
-
473
- try {
474
- ctx.body = await ejs.renderFile(filePath, data);
475
- ctx.type = 'text/html';
476
- } catch (error) {
477
- console.error('Template error:', error);
478
- ctx.status = 500;
479
- ctx.body = 'Internal Server Error';
480
- }
481
- }
482
- }
339
+ ext: ['ejs'],
340
+ renderTimeout: 5000,
341
+ render: async (ctx, next, filePath, { signal }) => {
342
+ ctx.body = await ejs.renderFile(filePath, { user: ctx.state.user }, { signal });
343
+ ctx.type = 'text/html';
344
+ },
345
+ },
483
346
  }));
484
347
 
485
348
  app.listen(3000);
@@ -489,262 +352,324 @@ app.listen(3000);
489
352
 
490
353
  ## API Reference
491
354
 
492
- ### koaClassicServer(rootDir, options)
355
+ ### `koaClassicServer(rootDir, options)`
493
356
 
494
357
  Creates a Koa middleware for serving static files.
495
358
 
496
359
  **Parameters:**
497
-
498
- - **`rootDir`** (String, required): Absolute path to the directory containing files
499
- - **`options`** (Object, optional): Configuration options
360
+ - **`rootDir`** *(String, required)* — Absolute path to the directory containing files
361
+ - **`options`** *(Object, optional)* Configuration options
500
362
 
501
363
  **Returns:** Koa middleware function
502
364
 
503
- ### Options
365
+ ### Options Summary
504
366
 
505
367
  ```javascript
506
368
  {
507
369
  // HTTP methods allowed (default: ['GET'])
508
370
  method: ['GET', 'HEAD'],
509
371
 
510
- // Show directory contents (default: true)
511
- showDirContents: true,
372
+ // Directory listing (V3 namespace)
373
+ dirListing: {
374
+ enabled: true,
375
+ maxEntries: 10000, // cap visible entries (0 = disabled)
376
+ entriesPerPage: 100, // entries per page (0 = disabled)
377
+ },
512
378
 
513
- // Index file configuration
514
- // Array format (recommended):
515
- // - Strings: exact matches ['index.html', 'default.html']
516
- // - RegExp: pattern matches [/index\.html/i]
517
- // - Mixed: ['index.html', /INDEX\.HTM/i]
518
- // Priority determined by array order (first match wins)
519
- // See docs/INDEX_OPTION_PRIORITY.md for details
379
+ // Index file resolution (Array of strings and/or RegExp)
520
380
  index: ['index.html', 'index.htm'],
521
381
 
522
- // URL path prefix (default: '')
523
- // Files served under this prefix
524
- urlPrefix: '/static',
525
-
526
- // Reserved paths (default: [])
527
- // First-level directories passed to next middleware
528
- urlsReserved: ['/admin', '/api', '/.git'],
529
-
530
- // Template engine configuration
531
- template: {
532
- // Template rendering function
533
- render: async (ctx, next, filePath) => {
534
- // Your rendering logic
535
- ctx.body = await yourEngine.render(filePath, data);
536
- },
382
+ // URL routing
383
+ urlPrefix: '/static',
384
+ urlsReserved: ['/api', '/admin'],
385
+ useOriginalUrl: true,
537
386
 
538
- // File extensions to process
539
- ext: ['ejs', 'pug', 'hbs']
387
+ // Hidden files / dirs
388
+ hidden: {
389
+ dotFiles: { default: 'visible', whitelist: [], blacklist: [] },
390
+ dotDirs: { default: 'visible', whitelist: [], blacklist: [] },
391
+ alwaysHide: [],
540
392
  },
541
393
 
542
- // Browser HTTP caching configuration
543
- // NOTE: Default is false for development. Set to true in production for better performance!
544
- browserCacheEnabled: false, // Enable ETag & Last-Modified (default: false)
545
- browserCacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 3600 = 1 hour)
394
+ // Clean URLs
395
+ hideExtension: { ext: '.ejs', redirect: 301 },
546
396
 
547
- // URL resolution
548
- useOriginalUrl: true, // Use ctx.originalUrl (default) or ctx.url
549
- // Set false for URL rewriting middleware (i18n, routing)
397
+ // Browser HTTP caching
398
+ browserCacheEnabled: false,
399
+ browserCacheMaxAge: 3600,
550
400
 
551
- // Clean URLs - hide file extension from URLs (mod_rewrite-like)
552
- hideExtension: {
553
- ext: '.ejs', // Extension to hide (required, must start with '.')
554
- redirect: 301 // HTTP redirect code (optional, default: 301)
555
- },
556
-
557
- // Block files/dirs from listing and serving (HTTP 404)
558
- // Dot-files (names starting with '.') are hidden by default.
559
- // Dot-directories are visible by default.
560
- hidden: {
561
- dotFiles: {
562
- default: 'hidden', // 'hidden' | 'visible' — system default: 'hidden'
563
- whitelist: ['.well-known'], // Always visible — string (exact/glob) or RegExp
564
- blacklist: [], // Always hidden — overrides whitelist
565
- },
566
- dotDirs: {
567
- default: 'visible', // 'hidden' | 'visible' — system default: 'visible'
568
- whitelist: [],
569
- blacklist: ['.git'],
570
- },
571
- alwaysHide: ['*.key', /secret/i], // Path-aware patterns (string glob or RegExp)
401
+ // Template engine
402
+ template: {
403
+ ext: ['ejs'],
404
+ renderTimeout: 30000, // ms; 0 disables the cap
405
+ render: async (ctx, next, filePath, { signal }) => { /* ... */ },
572
406
  },
573
407
 
408
+ // Observability
409
+ logger: console, // any { error, warn, info, debug } shape
574
410
  }
575
411
  ```
576
412
 
577
413
  ### Options Details
578
414
 
579
415
  | Option | Type | Default | Description |
580
- |--------|------|---------|-------------|
581
- | `method` | Array | `['GET']` | Allowed HTTP methods |
582
- | `showDirContents` | Boolean | `true` | Show directory listing |
583
- | `index` | Array | `[]` | Index file patterns (strings, RegExp, or mixed) |
584
- | `urlPrefix` | String | `''` | URL path prefix |
585
- | `urlsReserved` | Array | `[]` | Reserved directory paths (first-level only) |
586
- | `template.render` | Function | `undefined` | Template rendering function |
587
- | `template.ext` | Array | `[]` | Extensions for template rendering |
588
- | `browserCacheEnabled` | Boolean | `false` | Enable browser HTTP caching headers (recommended: `true` in production) |
589
- | `browserCacheMaxAge` | Number | `3600` | Browser cache duration in seconds |
590
- | `useOriginalUrl` | Boolean | `true` | Use `ctx.originalUrl` (default) or `ctx.url` for URL resolution |
591
- | `hideExtension.ext` | String | - | Extension to hide (e.g. `'.ejs'`). Enables clean URL feature |
592
- | `hideExtension.redirect` | Number | `301` | HTTP redirect code for URLs with extension |
593
- | `hidden.dotFiles.default` | String | `'hidden'` | Default visibility for dot-files: `'hidden'` or `'visible'` |
594
- | `hidden.dotFiles.whitelist` | Array | `[]` | Dot-file names always visible (string exact/glob or RegExp) |
595
- | `hidden.dotFiles.blacklist` | Array | `[]` | Dot-file names always hidden — overrides whitelist |
596
- | `hidden.dotDirs.default` | String | `'visible'` | Default visibility for dot-dirs: `'hidden'` or `'visible'` |
597
- | `hidden.dotDirs.whitelist` | Array | `[]` | Dot-dir names always visible |
598
- | `hidden.dotDirs.blacklist` | Array | `[]` | Dot-dir names always hidden overrides whitelist |
599
- | `hidden.alwaysHide` | Array | `[]` | Path-aware patterns (string glob or RegExp) for any file/dir. Secondary to whitelist/blacklist. |
600
-
601
- #### useOriginalUrl (Boolean, default: true)
602
-
603
- Controls which URL property is used for file resolution:
604
- - **`true` (default)**: Uses `ctx.originalUrl` (immutable, reflects the original request)
605
- - **`false`**: Uses `ctx.url` (mutable, can be modified by middleware)
606
-
607
- **When to use `false`:**
608
-
609
- Set `useOriginalUrl: false` when using URL rewriting middleware such as i18n routers or path rewriters that modify `ctx.url`. This allows koa-classic-server to serve files based on the rewritten URL instead of the original request URL.
610
-
611
- **Example with i18n middleware:**
612
-
613
- ```javascript
614
- const Koa = require('koa');
615
- const koaClassicServer = require('koa-classic-server');
416
+ |---|---|---|---|
417
+ | `method` | `String[]` | `['GET']` | Allowed HTTP methods |
418
+ | `dirListing.enabled` | `Boolean` | `true` | **V3** Render directory listing HTML when no index file matches |
419
+ | `dirListing.maxEntries` | `Number` | `10000` | **V3** Cap entries shown / sorted / stat'd (0 = disabled) |
420
+ | `dirListing.entriesPerPage` | `Number` | `100` | **V3** Entries per listing page (0 = disabled) |
421
+ | `index` | `Array` | `[]` | Index file patterns (strings, RegExp, or mixed) |
422
+ | `urlPrefix` | `String` | `''` | URL path prefix |
423
+ | `urlsReserved` | `String[]` | `[]` | First-level paths passed through to next middleware |
424
+ | `useOriginalUrl` | `Boolean` | `true` | Use `ctx.originalUrl` (`true`) or `ctx.url` (`false`) |
425
+ | `hideExtension.ext` | `String` | | Extension to hide (`.ejs`, must start with `.`) |
426
+ | `hideExtension.redirect` | `Number` | `301` | HTTP redirect code |
427
+ | `hidden.dotFiles.default` | `String` | `'visible'` | Default visibility for `.foo` files (`'hidden'` to harden) |
428
+ | `hidden.dotFiles.whitelist` | `Array` | `[]` | Names always visible (string/glob/RegExp) |
429
+ | `hidden.dotFiles.blacklist` | `Array` | `[]` | Names always hidden (overrides whitelist) |
430
+ | `hidden.dotDirs.default` | `String` | `'visible'` | Default visibility for `.foo` directories |
431
+ | `hidden.dotDirs.whitelist` | `Array` | `[]` | Names always visible |
432
+ | `hidden.dotDirs.blacklist` | `Array` | `[]` | Names always hidden |
433
+ | `hidden.alwaysHide` | `Array` | `[]` | Path-aware patterns (string glob or RegExp) |
434
+ | `browserCacheEnabled` | `Boolean` | `false` | Emit ETag + Last-Modified (recommended `true` in production) |
435
+ | `browserCacheMaxAge` | `Number` | `3600` | `Cache-Control: max-age` in seconds |
436
+ | `template.render` | `Function` | – | `async (ctx, next, filePath, { signal }) => void` |
437
+ | `template.ext` | `String[]` | `[]` | Extensions handled by the template engine |
438
+ | `template.renderTimeout` | `Number` | `30000` | **V3** Max render time in ms (0 = disabled) |
439
+ | `logger` | `Object` | `console` | **V3** Logger with `{ error, warn, info, debug }` |
440
+
441
+ For deep dives, see [DOCUMENTATION.md](./docs/DOCUMENTATION.md) and the per-option guides in [`docs/`](./docs).
616
442
 
617
- const app = new Koa();
443
+ ---
618
444
 
619
- // i18n middleware that rewrites URLs
620
- app.use(async (ctx, next) => {
621
- if (ctx.path.match(/^\/it\//)) {
622
- ctx.url = ctx.path.replace(/^\/it/, ''); // /it/page.html → /page.html
623
- }
624
- await next();
625
- });
445
+ ## Directory Listing Features
626
446
 
627
- // Serve files using rewritten URL
628
- app.use(koaClassicServer(__dirname + '/www', {
629
- useOriginalUrl: false // Use ctx.url (rewritten) instead of ctx.originalUrl
630
- }));
447
+ ### Sortable Columns
631
448
 
632
- app.listen(3000);
633
- ```
449
+ Click any column header to sort:
450
+ - **Name** — Alphabetical (A→Z / Z→A)
451
+ - **Type** — By MIME type (directories first)
452
+ - **Size** — By byte size (directories first)
634
453
 
635
- **How it works:**
636
- - Request: `GET /it/page.html`
637
- - `ctx.originalUrl`: `/it/page.html` (unchanged)
638
- - `ctx.url`: `/page.html` (rewritten by middleware)
639
- - With `useOriginalUrl: false`: Server looks for `/www/page.html` ✅
640
- - With `useOriginalUrl: true` (default): Server looks for `/www/it/page.html` ❌ 404
454
+ Visual indicators: `↑` ascending, `↓` descending. Sort + order are preserved across pagination links.
641
455
 
642
- ---
456
+ ### Pagination (V3)
643
457
 
644
- ## Directory Listing Features
458
+ When the number of visible entries exceeds `dirListing.entriesPerPage`, a numbered paginator is rendered below the table:
645
459
 
646
- ### Sortable Columns
460
+ ```
461
+ « First · ‹ Prev · 0 · 1 · … · 7 · 8 · 9 · Next › · Last »
462
+ ```
647
463
 
648
- Click on column headers to sort:
464
+ - Page index is 0-based (`?page=N`).
465
+ - Invalid or out-of-range values clamp silently.
466
+ - Response header `X-Dir-Pagination: <current>/<last>` is emitted only when pagination is meaningful.
649
467
 
650
- - **Name** - Alphabetical sorting (A-Z or Z-A)
651
- - **Type** - Sort by MIME type (directories always first)
652
- - **Size** - Sort by file size (directories always first)
468
+ ### Truncation Banner (V3)
653
469
 
654
- Visual indicators:
655
- - **↑** - Ascending order
656
- - **↓** - Descending order
470
+ When `dirListing.maxEntries` is hit, a banner is rendered above the table and `X-Dir-Truncated: <N>` is set, so capped listings are visible both to users and to monitoring.
657
471
 
658
472
  ### File Size Display
659
473
 
660
- Human-readable format:
661
- - `1.5 KB` - Kilobytes
662
- - `2.3 MB` - Megabytes
663
- - `1.2 GB` - Gigabytes
664
- - `-` - Directories (no size)
474
+ Human-readable: `1.5 KB`, `2.3 MB`, `1.2 GB`. Directories show `-`.
665
475
 
666
476
  ### Navigation
667
477
 
668
- - **Click folder name** - Enter directory
669
- - **Click file name** - Download/view file
670
- - **Parent Directory** - Go up one level
478
+ - Click folder enter directory
479
+ - Click file serve / download
480
+ - **Parent Directory** link go up one level
671
481
 
672
482
  ### Symlink Support
673
483
 
674
- The middleware fully supports symbolic links, which is essential for environments where served files are symlinks rather than regular files:
484
+ The middleware follows symbolic links transparently via `fs.promises.stat()` useful in NixOS, Docker bind mounts, `npm link`, and Capistrano-style deploys.
675
485
 
676
- - **NixOS buildFHSEnv** - Files in www/ appear as symlinks to the Nix store
677
- - **Docker bind mounts** - Mounted files may appear as symlinks
678
- - **npm link** - Linked packages are symlinks
679
- - **Capistrano-style deploys** - The `current` directory is a symlink to the active release
486
+ | Entry type | Indicator | Clickable | Type column |
487
+ |---|---|---|---|
488
+ | Symlink to file | `( Symlink )` | yes | target MIME |
489
+ | Symlink to directory | `( Symlink )` | yes | `DIR` |
490
+ | Broken symlink | `( Broken Symlink )` | no | original MIME guess |
680
491
 
681
- **How it works:**
492
+ Regular files incur zero additional `stat()` overhead.
682
493
 
683
- Symlinks are followed transparently via `fs.promises.stat()`, but only when `dirent.isSymbolicLink()` is true. Regular files incur zero additional overhead.
494
+ ---
684
495
 
685
- **Directory listing indicators:**
496
+ ## Security
686
497
 
687
- | Entry type | Indicator | Clickable | Type shown |
688
- |------------|-----------|-----------|------------|
689
- | Symlink to file | `( Symlink )` | Yes | Target MIME type |
690
- | Symlink to directory | `( Symlink )` | Yes | `DIR` |
691
- | Broken/circular symlink | `( Broken Symlink )` | No | `unknown` |
692
- | Regular file/directory | none | Yes | Real type |
498
+ ### Built-in Protection
693
499
 
694
- **Edge cases handled:**
695
- - Broken symlinks (missing target) return 404 on direct access
696
- - Circular symlinks (A → B → A) are treated as broken, no infinite loops
697
- - Symlinks to directories are fully navigable
500
+ #### 1. Path Traversal
698
501
 
699
- ---
502
+ ```text
503
+ GET /../../etc/passwd → 403 Forbidden
504
+ GET /%2e%2e%2fpackage.json → 403 Forbidden
505
+ GET /file\0.txt → 400 Bad Request (null-byte guard)
506
+ ```
700
507
 
701
- ## Security
508
+ Defense in depth: null-byte rejection → `path.normalize()` → resolved-path boundary check against `rootDir`.
702
509
 
703
- ### Built-in Protection
510
+ #### 2. XSS in Directory Listing
704
511
 
705
- koa-classic-server includes enterprise-grade security:
512
+ All file and directory names are HTML-escaped. CSS is inlined under a hash-based `Content-Security-Policy` recomputed at module load — script execution from inline `<style>`/`<script>` is rejected by the browser.
706
513
 
707
- #### 1. Path Traversal Protection
514
+ #### 3. Dot-Files Hidden by Default (V3)
708
515
 
709
- Prevents access to files outside `rootDir`:
516
+ `.env`, `.git/config`, SSH keys, etc. return 404 unless explicitly whitelisted via `hidden.dotFiles.whitelist`. The `.well-known` whitelist pattern stays friendly to ACME / Let's Encrypt.
710
517
 
711
- ```javascript
712
- // ❌ Blocked requests
713
- GET /../../../etc/passwd → 403 Forbidden
714
- GET /../config/database.yml → 403 Forbidden
715
- GET /%2e%2e%2fpackage.json → 403 Forbidden
716
- ```
518
+ #### 4. Security Headers on Generated Pages
717
519
 
718
- #### 2. XSS Protection
520
+ The middleware emits the following on directory listings and error pages (404/405/500/etc.):
719
521
 
720
- All filenames and paths are HTML-escaped:
522
+ | Header | Value |
523
+ |---|---|
524
+ | `Content-Security-Policy` | hash-based on listing, fully restrictive on errors |
525
+ | `X-Content-Type-Options` | `nosniff` |
526
+ | `X-Frame-Options` | `DENY` |
527
+ | `Referrer-Policy` | `no-referrer` |
528
+ | `Permissions-Policy` | `camera=(), microphone=(), geolocation=(), payment=()` |
721
529
 
722
- ```javascript
723
- // Malicious filename: <script>alert('xss')</script>.txt
724
- // Displayed as: &lt;script&gt;alert('xss')&lt;/script&gt;.txt
725
- // ✅ Safe - script doesn't execute
726
- ```
530
+ > ⚠️ User-served static files (HTML/JS/CSS on disk) are returned **without** these headers — by design. See [docs/DOCUMENTATION.md → Limiti dei Security Headers](./docs/DOCUMENTATION.md#limiti-dei-security-headers-sui-file-statici) for an upstream-middleware example that applies your own CSP/HSTS to static files.
531
+
532
+ #### 5. DNS Rebinding
727
533
 
728
- #### 3. Reserved URLs
534
+ The middleware does not validate the `Host` header — that belongs to the reverse proxy or an application-level allowlist. See [docs/DOCUMENTATION.md → DNS Rebinding](./docs/DOCUMENTATION.md#dns-rebinding--valida-lheader-host-a-monte) for nginx + Koa allowlist examples.
729
535
 
730
- Protect sensitive directories:
536
+ #### 6. Reserved URLs
731
537
 
732
538
  ```javascript
733
539
  app.use(koaClassicServer(__dirname, {
734
- urlsReserved: ['/admin', '/config', '/.git', '/node_modules']
540
+ urlsReserved: ['/admin', '/api', '/.git', '/node_modules'],
735
541
  }));
736
542
  ```
737
543
 
738
- #### 4. Race Condition Protection
544
+ #### 7. Race-Condition Protection
545
+
546
+ File metadata is verified before streaming. A file deleted between check and access returns `404`, never a crash or partial response.
547
+
548
+ #### 8. Bounded Listings (V3)
739
549
 
740
- File access is verified before streaming:
550
+ `dirListing.maxEntries` caps the number of entries that are sorted, stat'd, and rendered per listing — bounds CPU and HTML size against accidentally-large folders. The initial `readdir()` is not bounded by this option; an opt-in streaming mode for adversarial-directory workloads is planned for v3.1.
551
+
552
+ #### 9. Template Render Timeout (V3)
553
+
554
+ `template.renderTimeout` (default 30 s) prevents a hung or runaway template render from blocking the request indefinitely; the AbortSignal forwarded to the renderer lets you abort downstream I/O cleanly.
555
+
556
+ **See:**
557
+ - [Security improvement roadmap →](./docs/security_improvement_for_V3.md)
558
+ - [Security tests →](./__tests__/security.test.js)
559
+
560
+ ### Design philosophy & Security Checklist
561
+
562
+ koa-classic-server follows the principle: **"if a file is in `rootDir`, `GET` on its path returns it"**. The defaults serve files without applying surprise restrictions — the operator is the source of truth. See [`CLAUDE.md`](./CLAUDE.md) for the full design philosophy.
563
+
564
+ This means hardening is **opt-in via explicit configuration**. The checklist below covers the most common production concerns. Each item is one or two lines of configuration; not all of them apply to every deployment.
565
+
566
+ #### ✅ Static site / public asset serving
567
+
568
+ - [ ] **Hide dot-files** that may contain secrets:
569
+ `hidden: { dotFiles: { default: 'hidden', whitelist: ['.well-known'] } }`
570
+ - [ ] **Block dot-directories** like `.git`:
571
+ `hidden: { dotDirs: { default: 'hidden', whitelist: ['.well-known'] } }`
572
+ - [ ] **Disable directory listing** in production:
573
+ `dirListing: { enabled: false }` (combine with an `index` file)
574
+ - [ ] **Enable browser HTTP caching**:
575
+ `browserCacheEnabled: true, browserCacheMaxAge: 86400`
576
+ - [ ] **Restrict methods** to read-only (default already `['GET']`):
577
+ `method: ['GET', 'HEAD']`
578
+ - [ ] **Reserve sensitive paths** for app routes:
579
+ `urlsReserved: ['/api', '/admin']`
580
+ - [ ] **Add upstream security headers** for user-served HTML (not auto-added by this middleware — see *DNS Rebinding / Headers* in `docs/DOCUMENTATION.md`).
581
+
582
+ #### ✅ User uploads, multi-tenant, untrusted-write directories
583
+
584
+ - [ ] **Lower the entry cap** for accidentally-large dirs:
585
+ `dirListing: { maxEntries: 1000 }` (default 100000 is a safety net, not a security feature)
586
+ - [ ] **Hide dot-files at every depth**:
587
+ `hidden: { dotFiles: { default: 'hidden' }, dotDirs: { default: 'hidden' } }`
588
+ - [ ] **Add path-aware blocklists** for known secret patterns:
589
+ `hidden: { alwaysHide: ['*.key', '*.pem', /\.secret$/, 'config/secrets/**'] }`
590
+ - [ ] **Monitor directory growth externally** (cron + alert) — the v3.0 cap bounds rendering CPU but not the initial `readdir()` allocation. See `[F-1]` in `docs/security_improvement_for_V3.md` for the v3.1 streaming-read opt-in tracking this gap.
591
+
592
+ #### ✅ Production hygiene (any deployment)
593
+
594
+ - [ ] **Validate `Host` header upstream** (nginx `server_name` allowlist or app-level middleware) — this middleware does NOT validate `Host`. See *DNS Rebinding* in `docs/DOCUMENTATION.md`.
595
+ - [ ] **Disable template-engine in production** if you don't use SSR — minimizes attack surface:
596
+ omit the `template` option entirely
597
+ - [ ] **Tune `template.renderTimeout`** if you do use SSR — default 30 s is conservative; tighten for tight-SLA services
598
+ - [ ] **Inject a real logger** instead of `console`:
599
+ `logger: pino()` so security-relevant warnings reach your aggregation
600
+ - [ ] **Pin the latest patch version** in `package.json` and run `npm audit` in CI
601
+
602
+ ### Suggested production security configuration
603
+
604
+ A single configuration block that covers most production deployments. Start here and tune for your workload (static site vs uploads vs internal admin):
741
605
 
742
606
  ```javascript
743
- // File deleted between check and access?
744
- // Returns 404 instead of crashing
607
+ const Koa = require('koa');
608
+ const pino = require('pino')({ level: 'info' });
609
+ const path = require('path');
610
+ const koaClassicServer = require('koa-classic-server');
611
+
612
+ const app = new Koa();
613
+
614
+ // 1) Validate Host header — mitigates DNS rebinding on LAN / loopback exposure.
615
+ const ALLOWED_HOSTS = new Set([
616
+ 'app.example.com',
617
+ 'localhost:3000',
618
+ ]);
619
+ app.use(async (ctx, next) => {
620
+ if (!ALLOWED_HOSTS.has(ctx.host)) {
621
+ ctx.status = 421;
622
+ ctx.body = 'Misdirected Request';
623
+ return;
624
+ }
625
+ await next();
626
+ });
627
+
628
+ // 2) Apply security headers to user-served HTML/JS/CSS. The middleware
629
+ // sets these only on its own generated pages (listing + errors).
630
+ app.use(async (ctx, next) => {
631
+ ctx.set('X-Content-Type-Options', 'nosniff');
632
+ ctx.set('Referrer-Policy', 'strict-origin-when-cross-origin');
633
+ ctx.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains');
634
+ await next();
635
+ });
636
+
637
+ // 3) The file server with hardened defaults.
638
+ app.use(koaClassicServer(path.join(__dirname, 'public'), {
639
+ method: ['GET', 'HEAD'], // read-only
640
+
641
+ index: ['index.html'], // serve index when present
642
+
643
+ dirListing: {
644
+ enabled: process.env.NODE_ENV !== 'production',
645
+ maxEntries: 10000, // tighten the soft cap below the 100k default
646
+ entriesPerPage: 100,
647
+ },
648
+
649
+ hidden: {
650
+ dotFiles: {
651
+ default: 'hidden', // hide .env / .htaccess / etc by default
652
+ whitelist: ['.well-known'], // expose ACME / Let's Encrypt
653
+ },
654
+ dotDirs: {
655
+ default: 'hidden',
656
+ whitelist: ['.well-known'],
657
+ },
658
+ alwaysHide: ['*.key', '*.pem', /^backup-/, /\.secret$/],
659
+ },
660
+
661
+ browserCacheEnabled: true,
662
+ browserCacheMaxAge: 86400, // 24 h — bandwidth savings on cache hits
663
+
664
+ logger: pino, // pipe internal warnings to structured logs
665
+
666
+ urlsReserved: ['/api', '/admin'], // routes handled by other middleware
667
+ }));
668
+
669
+ app.listen(3000);
745
670
  ```
746
671
 
747
- **See full security audit:** [Security Tests →](./__tests__/security.test.js)
672
+ For multi-tenant or user-upload scenarios, also drop `dirListing.maxEntries` to `1000` and monitor the served directory's size externally.
748
673
 
749
674
  ---
750
675
 
@@ -752,32 +677,21 @@ File access is verified before streaming:
752
677
 
753
678
  ### Optimizations
754
679
 
755
- Version 2.x includes major performance improvements:
756
-
757
- - **Async/Await** - Non-blocking I/O, event loop never blocked
758
- - **Array Join** - 30-40% less memory vs string concatenation
759
- - **HTTP Caching** - 80-95% bandwidth reduction
760
- - **Single stat() Call** - No double file system access
761
- - **Streaming** - Large files streamed efficiently
680
+ - **Single-syscall `readdir()`** — directory entries fetched in one batched syscall, then sliced to `dirListing.maxEntries` to cap rendering work
681
+ - **Single `stat()`** per item — no double filesystem traversal
682
+ - **Array `.join()`** for listing HTML significantly less GC pressure than `+=`
683
+ - **HTTP conditional responses** 304s with `If-None-Match` / `If-Modified-Since` (when caching enabled)
684
+ - **File streaming** large files streamed via `fs.createReadStream`, never buffered in full
685
+ - **Pre-computed CSP hash** — SHA-256 of inline CSS hashed once at module load, not per request
762
686
 
763
687
  ### Benchmarks
764
688
 
765
- Performance results on directory with 1,000 files:
766
-
767
- ```
768
- Before (v1.x): ~350ms per request
769
- After (v2.x): ~190ms per request
770
- Improvement: 46% faster
771
- ```
772
-
773
- **See detailed benchmarks:** [Performance Analysis →](./docs/PERFORMANCE_ANALYSIS.md)
689
+ See [`docs/BENCHMARKS.md`](./docs/BENCHMARKS.md) and [`docs/PERFORMANCE_COMPARISON.md`](./docs/PERFORMANCE_COMPARISON.md) for full benchmarks and methodology.
774
690
 
775
691
  ---
776
692
 
777
693
  ## Testing
778
694
 
779
- Run the comprehensive test suite:
780
-
781
695
  ```bash
782
696
  # Run all tests
783
697
  npm test
@@ -789,75 +703,45 @@ npm run test:security
789
703
  npm run test:performance
790
704
  ```
791
705
 
792
- **Test Coverage:**
793
- - ✅ 309 tests passing
794
- - ✅ Security tests (path traversal, XSS, race conditions)
795
- - ✅ EJS template integration tests
796
- - ✅ Index option tests (arrays, RegExp)
797
- - ✅ hideExtension tests (clean URLs, redirects, conflicts, validation)
798
- - ✅ Symlink tests (file, directory, broken, circular, indicators)
706
+ **Coverage:**
707
+ - ✅ 532 tests passing across 20 suites
708
+ - ✅ Security (path traversal, XSS, race conditions, CSP, hidden-files)
709
+ - ✅ Directory listing (sorting, pagination, truncation cap, symlinks)
710
+ - ✅ Template engine (timeout, abort signal, error propagation, EJS integration)
711
+ - ✅ Logger injection (validation, custom logger, console default)
712
+ - ✅ Index option (arrays, RegExp, priority)
713
+ - ✅ `hideExtension` (clean URLs, redirects, conflicts, validation)
714
+ - ✅ HTTP caching (ETag, Last-Modified, 304)
799
715
  - ✅ Performance benchmarks
800
- - ✅ Directory sorting tests
801
716
 
802
717
  ---
803
718
 
804
719
  ## Complete Documentation
805
720
 
806
- ### Core Documentation
807
-
808
- - **[DOCUMENTATION.md](./docs/DOCUMENTATION.md)** - Complete API reference and usage guide
809
- - **[FLOW_DIAGRAM.md](./docs/FLOW_DIAGRAM.md)** - Visual flow diagrams and code execution paths
810
- - **[CHANGELOG.md](./docs/CHANGELOG.md)** - Version history and release notes
721
+ ### Core
722
+ - **[DOCUMENTATION.md](./docs/DOCUMENTATION.md)** — Full API reference and usage guide
723
+ - **[FLOW_DIAGRAM.md](./docs/FLOW_DIAGRAM.md)** Visual flow diagrams and execution paths
724
+ - **[CHANGELOG.md](./docs/CHANGELOG.md)** Version history and release notes
811
725
 
812
726
  ### Template Engine
813
-
814
- - **[TEMPLATE_ENGINE_GUIDE.md](./docs/template-engine/TEMPLATE_ENGINE_GUIDE.md)** - Complete guide to template engine integration
815
- - Progressive examples (simple to enterprise)
816
- - EJS, Pug, Handlebars, Nunjucks support
817
- - Best practices and troubleshooting
727
+ - **[TEMPLATE_ENGINE_GUIDE.md](./docs/template-engine/TEMPLATE_ENGINE_GUIDE.md)** — EJS, Pug, Handlebars, Nunjucks; AbortSignal + timeout patterns
818
728
 
819
729
  ### Configuration
730
+ - **[INDEX_OPTION_PRIORITY.md](./docs/INDEX_OPTION_PRIORITY.md)** — Priority rules for `index`
731
+ - **[EXAMPLES_INDEX_OPTION.md](./docs/EXAMPLES_INDEX_OPTION.md)** — 10 practical examples
820
732
 
821
- - **[INDEX_OPTION_PRIORITY.md](./docs/INDEX_OPTION_PRIORITY.md)** - Detailed priority behavior for `index` option
822
- - String vs Array vs RegExp formats
823
- - Priority order examples
824
- - Migration guide from v1.x
825
-
826
- - **[EXAMPLES_INDEX_OPTION.md](./docs/EXAMPLES_INDEX_OPTION.md)** - 10 practical examples of `index` option with RegExp
827
- - Case-insensitive matching
828
- - Multiple extensions
829
- - Complex patterns
733
+ ### Security
734
+ - **[security_improvement_for_V3.md](./docs/security_improvement_for_V3.md)** Audit roadmap and status
830
735
 
831
736
  ### Performance
832
-
833
- - **[PERFORMANCE_ANALYSIS.md](./docs/PERFORMANCE_ANALYSIS.md)** - Performance optimization analysis
834
- - Before/after comparisons
835
- - Memory usage analysis
836
- - Bottleneck identification
837
-
838
- - **[PERFORMANCE_COMPARISON.md](./docs/PERFORMANCE_COMPARISON.md)** - Detailed performance benchmarks
839
- - Request latency
840
- - Throughput metrics
841
- - Concurrent request handling
842
-
843
- - **[OPTIMIZATION_HTTP_CACHING.md](./docs/OPTIMIZATION_HTTP_CACHING.md)** - HTTP caching implementation details
844
- - ETag generation
845
- - Last-Modified headers
846
- - 304 Not Modified responses
847
-
848
- - **[BENCHMARKS.md](./docs/BENCHMARKS.md)** - Benchmark results and methodology
737
+ - **[PERFORMANCE_ANALYSIS.md](./docs/PERFORMANCE_ANALYSIS.md)** — Optimization analysis
738
+ - **[PERFORMANCE_COMPARISON.md](./docs/PERFORMANCE_COMPARISON.md)** Latency, throughput, concurrency
739
+ - **[OPTIMIZATION_HTTP_CACHING.md](./docs/OPTIMIZATION_HTTP_CACHING.md)** — Caching internals
740
+ - **[BENCHMARKS.md](./docs/BENCHMARKS.md)** Methodology and results
849
741
 
850
742
  ### Code Quality
851
-
852
- - **[CODE_REVIEW.md](./docs/CODE_REVIEW.md)** - Code quality analysis and review
853
- - Security audit
854
- - Best practices
855
- - Standardization improvements
856
-
857
- - **[DEBUG_REPORT.md](./docs/DEBUG_REPORT.md)** - Known limitations and debugging info
858
- - Reserved URLs behavior
859
- - Edge cases
860
- - Troubleshooting tips
743
+ - **[CODE_REVIEW.md](./docs/CODE_REVIEW.md)** — Code review and standards
744
+ - **[DEBUG_REPORT.md](./docs/DEBUG_REPORT.md)** Known limitations and debugging
861
745
 
862
746
  ---
863
747
 
@@ -865,69 +749,81 @@ npm run test:performance
865
749
 
866
750
  ### From v2.x to v3.x
867
751
 
868
- **Breaking Changes:**
869
- - `index` option: String format removed — passing a non-empty string now throws an Error
870
- - `cacheMaxAge` option: removed use `browserCacheMaxAge`
871
- - `enableCaching` option: removed — use `browserCacheEnabled`
872
- - **Dot-files hidden by default** — `hidden.dotFiles.default` is `'hidden'` out of the box.
873
- In v2.x, dot-files like `.env` were served normally. In v3.x they return 404 unless explicitly allowed.
752
+ **Breaking changes**
753
+
754
+ | What | v2.x | v3.x |
755
+ |---|---|---|
756
+ | `index: 'index.html'` | accepted | **throws**must be an array |
757
+ | `cacheMaxAge` | accepted | **removed** use `browserCacheMaxAge` |
758
+ | `enableCaching` | accepted | **removed** — use `browserCacheEnabled` |
759
+ | `showDirContents` | accepted | accepted as **deprecated alias** — emits a one-time warning, prefer `dirListing: { enabled: true }` |
760
+ | Dot-files | served | **served** (unchanged — opt into hiding via `hidden.dotFiles.default: 'hidden'`; see Security Checklist) |
761
+ | Logger | `console` only | `logger` option injects any logger; default still `console` |
762
+ | Template `render` signature | `(ctx, next, filePath)` | `(ctx, next, filePath, { signal })` — old signature still works, `signal` is opt-in |
874
763
 
875
- **Migration:**
764
+ **Quick migration**
876
765
 
877
766
  ```javascript
878
- // v2.x (now throws in v3)
879
- { index: 'index.html' }
767
+ // v2.x
768
+ app.use(koaClassicServer(root, {
769
+ index: 'index.html',
770
+ enableCaching: true,
771
+ cacheMaxAge: 3600,
772
+ showDirContents: true,
773
+ }));
880
774
 
881
775
  // v3.x
882
- { index: ['index.html'] }
776
+ app.use(koaClassicServer(root, {
777
+ index: ['index.html'],
778
+ browserCacheEnabled: true,
779
+ browserCacheMaxAge: 3600,
780
+ dirListing: { enabled: true },
781
+ }));
883
782
  ```
884
783
 
885
- ```javascript
886
- // v2.x — dot-files were served (no protection)
887
- // v3.x — dot-files hidden by default (recommended)
784
+ **Dot-files in v3**
888
785
 
889
- // To restore v2.x behavior for dot-files:
890
- {
891
- hidden: { dotFiles: { default: 'visible' } }
892
- }
786
+ ```javascript
787
+ // To restore v2.x behavior (serve dot-files):
788
+ { hidden: { dotFiles: { default: 'visible' } } }
893
789
 
894
- // Recommended v3.x — hide dot-files but expose .well-known for ACME/Let's Encrypt:
790
+ // Recommended v3 — hide dot-files but expose .well-known for ACME / Let's Encrypt:
895
791
  {
896
792
  hidden: {
897
793
  dotFiles: { default: 'hidden', whitelist: ['.well-known'] },
898
- dotDirs: { default: 'hidden', whitelist: ['.well-known'] }
899
- }
794
+ dotDirs: { default: 'hidden', whitelist: ['.well-known'] },
795
+ },
900
796
  }
901
797
  ```
902
798
 
903
- ---
904
-
905
- ### From v1.x to v2.x
799
+ **Template render in v3**
906
800
 
907
- **Breaking Changes:**
908
- - `index` option: String format deprecated (use array format)
801
+ ```javascript
802
+ // v2.x still works:
803
+ template: { render: async (ctx, next, filePath) => { /* ... */ } }
804
+
805
+ // v3.x — opt into the AbortSignal:
806
+ template: {
807
+ renderTimeout: 5000,
808
+ render: async (ctx, next, filePath, { signal }) => {
809
+ const data = await fetchData({ signal });
810
+ ctx.body = await ejs.renderFile(filePath, data, { signal });
811
+ ctx.type = 'text/html';
812
+ },
813
+ }
814
+ ```
909
815
 
910
- **Migration:**
816
+ ### From v1.x to v2.x
911
817
 
912
818
  ```javascript
913
819
  // v1.x
914
- {
915
- index: 'index.html'
916
- }
820
+ { index: 'index.html' }
917
821
 
918
822
  // v2.x+
919
- {
920
- index: ['index.html']
921
- }
823
+ { index: ['index.html'] }
922
824
  ```
923
825
 
924
- **New Features:**
925
- - HTTP caching (enabled by default)
926
- - Sortable directory columns
927
- - File size display
928
- - Enhanced index option with RegExp
929
-
930
- **See full migration guide:** [CHANGELOG.md](./docs/CHANGELOG.md)
826
+ See the full [CHANGELOG.md](./docs/CHANGELOG.md) for every change.
931
827
 
932
828
  ---
933
829
 
@@ -952,22 +848,26 @@ const koaClassicServer = require('koa-classic-server');
952
848
 
953
849
  const app = new Koa();
954
850
 
955
- // Serve static assets
851
+ // Static assets — no listing in production
956
852
  app.use(koaClassicServer(__dirname + '/public', {
957
853
  urlPrefix: '/static',
958
- showDirContents: false
854
+ dirListing: { enabled: false },
959
855
  }));
960
856
 
961
- // Serve user uploads
857
+ // User uploads — paginated browsable index
962
858
  app.use(koaClassicServer(__dirname + '/uploads', {
963
859
  urlPrefix: '/files',
964
- showDirContents: true
860
+ dirListing: {
861
+ enabled: true,
862
+ maxEntries: 5000,
863
+ entriesPerPage: 50,
864
+ },
965
865
  }));
966
866
 
967
867
  app.listen(3000);
968
868
  ```
969
869
 
970
- ### Example 3: Development Server with Templates
870
+ ### Example 3: Development Server with Templates + Timeout
971
871
 
972
872
  ```javascript
973
873
  const Koa = require('koa');
@@ -976,63 +876,77 @@ const ejs = require('ejs');
976
876
 
977
877
  const app = new Koa();
978
878
 
979
- // Development mode - show directories
980
879
  app.use(koaClassicServer(__dirname + '/src', {
981
- showDirContents: true,
880
+ dirListing: { enabled: true },
982
881
  template: {
983
882
  ext: ['ejs'],
984
- render: async (ctx, next, filePath) => {
883
+ renderTimeout: 3000,
884
+ render: async (ctx, next, filePath, { signal }) => {
985
885
  ctx.body = await ejs.renderFile(filePath, {
986
886
  dev: true,
987
- timestamp: Date.now()
988
- });
887
+ timestamp: Date.now(),
888
+ }, { signal });
989
889
  ctx.type = 'text/html';
990
- }
991
- }
890
+ },
891
+ },
992
892
  }));
993
893
 
994
894
  app.listen(3000);
995
895
  ```
996
896
 
997
- ---
897
+ ### Example 4: Production with Pino Logger + Caching
998
898
 
999
- ## Troubleshooting
899
+ ```javascript
900
+ const Koa = require('koa');
901
+ const pino = require('pino')({ level: 'info' });
902
+ const koaClassicServer = require('koa-classic-server');
1000
903
 
1001
- ### Common Issues
904
+ const app = new Koa();
1002
905
 
1003
- **Issue: 404 errors for all files**
906
+ app.use(koaClassicServer(__dirname + '/public', {
907
+ index: ['index.html'],
908
+ dirListing: { enabled: false },
909
+ browserCacheEnabled: true,
910
+ browserCacheMaxAge: 86400,
911
+ logger: pino,
912
+ }));
1004
913
 
1005
- Check that `rootDir` is an absolute path:
914
+ app.listen(3000);
915
+ ```
1006
916
 
1007
- ```javascript
1008
- // ❌ Wrong (relative path)
1009
- koaClassicServer('./public')
917
+ ---
1010
918
 
1011
- // ✅ Correct (absolute path)
1012
- koaClassicServer(__dirname + '/public')
1013
- koaClassicServer(path.join(__dirname, 'public'))
1014
- ```
919
+ ## Troubleshooting
1015
920
 
1016
- **Issue: Reserved URLs not working**
921
+ **404 for all files**
1017
922
 
1018
- Reserved URLs only work for first-level directories:
923
+ Use an absolute path for `rootDir`:
1019
924
 
1020
925
  ```javascript
1021
- urlsReserved: ['/admin'] // Blocks /admin/*
1022
- urlsReserved: ['/admin/users'] // Doesn't work (nested)
926
+ koaClassicServer('./public') // relative
927
+ koaClassicServer(__dirname + '/public') // absolute
928
+ koaClassicServer(path.join(__dirname, 'pub')) // ✅ absolute
1023
929
  ```
1024
930
 
1025
- **Issue: Directory sorting not working**
931
+ **Reserved URLs not matching nested paths**
932
+
933
+ `urlsReserved` only matches first-level path segments — use it for top-level routes (`/api`), not nested ones (`/api/users`).
934
+
935
+ **Directory listing shows fewer files than expected**
936
+
937
+ Check the response headers: `X-Dir-Truncated` indicates the `dirListing.maxEntries` cap was reached. Increase the cap or paginate via `?page=N`.
938
+
939
+ **Templates time out under load**
1026
940
 
1027
- Make sure you're accessing directories without query params initially. The sorting is applied when you click headers.
941
+ Lower `template.renderTimeout` to fail fast, forward the `signal` to your I/O, and check the logger output for `Template render timeout after Xms` warnings.
1028
942
 
1029
- **See full troubleshooting:** [DEBUG_REPORT.md](./docs/DEBUG_REPORT.md)
943
+ See full troubleshooting: [DEBUG_REPORT.md](./docs/DEBUG_REPORT.md).
1030
944
 
1031
945
  ---
1032
946
 
1033
947
  ## Contributing
1034
948
 
1035
- Contributions are welcome! Please:
949
+ Contributions are welcome:
1036
950
 
1037
951
  1. Fork the repository
1038
952
  2. Create a feature branch
@@ -1044,8 +958,9 @@ Contributions are welcome! Please:
1044
958
 
1045
959
  ## Known Limitations
1046
960
 
1047
- - Reserved URLs only work for first-level directories
1048
- - Template rendering is synchronous per request
961
+ - `urlsReserved` only matches first-level path segments
962
+ - The middleware does not validate the `Host` header — configure a reverse proxy or an upstream allowlist (see [DOCUMENTATION.md → DNS Rebinding](./docs/DOCUMENTATION.md#dns-rebinding--valida-lheader-host-a-monte))
963
+ - Static files are returned without security headers — apply your own upstream middleware (see [DOCUMENTATION.md → Limiti dei Security Headers](./docs/DOCUMENTATION.md#limiti-dei-security-headers-sui-file-statici))
1049
964
 
1050
965
  See [DEBUG_REPORT.md](./docs/DEBUG_REPORT.md) for technical details.
1051
966
 
@@ -1053,7 +968,7 @@ See [DEBUG_REPORT.md](./docs/DEBUG_REPORT.md) for technical details.
1053
968
 
1054
969
  ## License
1055
970
 
1056
- MIT License - see LICENSE file for details
971
+ MIT License see LICENSE file for details.
1057
972
 
1058
973
  ---
1059
974
 
@@ -1065,10 +980,10 @@ Italo Paesano
1065
980
 
1066
981
  ## Links
1067
982
 
1068
- - **[npm Package](https://www.npmjs.com/package/koa-classic-server)** - Official npm package
1069
- - **[GitHub Repository](https://github.com/italopaesano/koa-classic-server)** - Source code
1070
- - **[Issue Tracker](https://github.com/italopaesano/koa-classic-server/issues)** - Report bugs
1071
- - **[Full Documentation](./docs/DOCUMENTATION.md)** - Complete reference
983
+ - **[npm Package](https://www.npmjs.com/package/koa-classic-server)** Official npm package
984
+ - **[GitHub Repository](https://github.com/italopaesano/koa-classic-server)** Source code
985
+ - **[Issue Tracker](https://github.com/italopaesano/koa-classic-server/issues)** Report bugs
986
+ - **[Full Documentation](./docs/DOCUMENTATION.md)** Complete reference
1072
987
 
1073
988
  ---
1074
989