koa-classic-server 3.0.1 → 4.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 +186 -818
- package/index.cjs +837 -252
- package/package.json +12 -6
- package/.github/workflows/npm-publish.yml +0 -98
- package/CLAUDE.md +0 -101
- package/__tests__/benchmark-results-baseline-v1.2.0.txt +0 -354
- package/__tests__/benchmark-results-optimized-v2.0.0.txt +0 -354
- package/__tests__/benchmark-results-v3.0.0.txt +0 -372
- package/__tests__/benchmark.js +0 -239
- package/__tests__/caching-headers.test.js +0 -556
- package/__tests__/compression-fixtures/data.json +0 -1
- package/__tests__/compression-fixtures/large.txt +0 -1
- package/__tests__/compression-fixtures/small.txt +0 -1
- package/__tests__/compression.test.js +0 -284
- package/__tests__/customTest/README.md +0 -6
- package/__tests__/customTest/loadConfig.util.js +0 -41
- package/__tests__/customTest/serversToLoad.util.js +0 -93
- package/__tests__/demo-regex-index.js +0 -140
- package/__tests__/deprecation-warnings.test.js +0 -105
- package/__tests__/directory-sorting-links.test.js +0 -135
- package/__tests__/dt-unknown.test.js +0 -635
- package/__tests__/ejs.test.js +0 -299
- package/__tests__/head-method.test.js +0 -160
- package/__tests__/hidden-fixtures/.dot-dir/inside.txt +0 -1
- package/__tests__/hidden-fixtures/.env +0 -2
- package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +0 -1
- package/__tests__/hidden-fixtures/config/secrets/password.txt +0 -1
- package/__tests__/hidden-fixtures/data.key +0 -1
- package/__tests__/hidden-fixtures/file.secret +0 -1
- package/__tests__/hidden-fixtures/index.html +0 -1
- package/__tests__/hidden-fixtures/normal.txt +0 -1
- package/__tests__/hidden-fixtures/subdir/.env +0 -1
- package/__tests__/hidden-fixtures/subdir/regular.txt +0 -1
- package/__tests__/hidden-option.test.js +0 -407
- package/__tests__/hideExtension.test.js +0 -607
- package/__tests__/index-option.test.js +0 -449
- package/__tests__/index.test.js +0 -345
- package/__tests__/listing.test.js +0 -437
- package/__tests__/logger.test.js +0 -232
- package/__tests__/performance.test.js +0 -370
- package/__tests__/publicWwwTest/cartella/sottocartella/ciao.html +0 -11
- package/__tests__/publicWwwTest/cartella/sottocartella/provaEjs/testEjs.ejs +0 -11
- package/__tests__/publicWwwTest/cartella vuota con spazi nel nome/file con spazio nel nome .txt +0 -1
- package/__tests__/publicWwwTest/cartella vuota con spazi nel nome /file con spazio nel nome .txt +0 -1
- package/__tests__/publicWwwTest/ejs-templates/complex.ejs +0 -56
- package/__tests__/publicWwwTest/ejs-templates/index.ejs +0 -30
- package/__tests__/publicWwwTest/ejs-templates/simple.ejs +0 -13
- package/__tests__/publicWwwTest/ejs-templates/with-conditional.ejs +0 -28
- package/__tests__/publicWwwTest/ejs-templates/with-escaping.ejs +0 -26
- package/__tests__/publicWwwTest/ejs-templates/with-loop.ejs +0 -16
- package/__tests__/publicWwwTest/hideext-test/about/index.html +0 -1
- package/__tests__/publicWwwTest/hideext-test/about.EJS +0 -1
- package/__tests__/publicWwwTest/hideext-test/about.ejs +0 -2
- package/__tests__/publicWwwTest/hideext-test/blog/articolo.ejs +0 -2
- package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina +0 -1
- package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina.ejs +0 -1
- package/__tests__/publicWwwTest/hideext-test/file.ejs.bak +0 -1
- package/__tests__/publicWwwTest/hideext-test/index.ejs +0 -2
- package/__tests__/publicWwwTest/hideext-test/photo.txt +0 -1
- package/__tests__/publicWwwTest/hideext-test/sezione/index.ejs +0 -1
- package/__tests__/publicWwwTest/hideext-test/style.css +0 -1
- package/__tests__/publicWwwTest/ile_vuoto.txt +0 -0
- package/__tests__/publicWwwTest/index.html +0 -11
- package/__tests__/publicWwwTest/prova file .txt +0 -2
- package/__tests__/publicWwwTest/semplicetxt.txt +0 -1
- package/__tests__/publicWwwTest/test-page.html +0 -1
- package/__tests__/publicWwwTest/test.txt +0 -1
- package/__tests__/range-fixtures/sample.txt +0 -1
- package/__tests__/range.test.js +0 -223
- package/__tests__/security-headers.test.js +0 -165
- package/__tests__/security.test.js +0 -322
- package/__tests__/server-cache-fixtures/large.txt +0 -1
- package/__tests__/server-cache-fixtures/small.txt +0 -1
- package/__tests__/server-cache.test.js +0 -594
- package/__tests__/setup-benchmark.js +0 -178
- package/__tests__/symlink.test.js +0 -441
- package/__tests__/template-timeout.test.js +0 -321
- package/__tests__/test-regex-quick.js +0 -158
- package/__tests__/useOriginalUrl.test.js +0 -213
- package/docs/ACTION_PLAN.md +0 -293
- package/docs/BENCHMARKS.md +0 -317
- package/docs/CHANGELOG.md +0 -906
- package/docs/CODE_REVIEW.md +0 -300
- package/docs/DEBUG_REPORT.md +0 -593
- package/docs/DOCUMENTATION.md +0 -1864
- package/docs/EXAMPLES_INDEX_OPTION.md +0 -395
- package/docs/FLOW_DIAGRAM.md +0 -954
- package/docs/INDEX_OPTION_PRIORITY.md +0 -527
- package/docs/OPTIMIZATION_HTTP_CACHING.md +0 -689
- package/docs/OPTIMIZATION_ROADMAP_for_V3.md +0 -864
- package/docs/PERFORMANCE_ANALYSIS.md +0 -839
- package/docs/PERFORMANCE_COMPARISON.md +0 -388
- package/docs/noteExports.md +0 -148
- package/docs/security_improvement_for_V3.md +0 -421
- package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +0 -1734
- package/docs/template-engine/esempi-incrementali.js +0 -192
- package/docs/template-engine/examples/esempio1-nessun-dato.ejs +0 -12
- package/docs/template-engine/examples/esempio2-una-variabile.ejs +0 -11
- package/docs/template-engine/examples/esempio3-piu-variabili.ejs +0 -15
- package/docs/template-engine/examples/esempio4-condizionale.ejs +0 -18
- package/docs/template-engine/examples/esempio5-loop.ejs +0 -18
- package/docs/template-engine/examples/index-esempi.html +0 -181
- package/docs/template-engine/examples/index.html +0 -40
- package/docs/template-engine/examples/test.ejs +0 -64
- package/eslint.config.mjs +0 -17
- package/jest.config.js +0 -18
package/README.md
CHANGED
|
@@ -1,131 +1,49 @@
|
|
|
1
1
|
# koa-classic-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Serve a directory of files over HTTP from Koa** — with classic, sortable directory
|
|
4
|
+
listings, clean URLs, automatic compression, and HTTP caching. In the spirit of a
|
|
5
|
+
traditional web server, but intentionally its own thing.
|
|
4
6
|
|
|
5
7
|
[](https://www.npmjs.com/package/koa-classic-server)
|
|
6
8
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[]()
|
|
8
10
|
[]()
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
[See full changelog →](./docs/CHANGELOG.md)
|
|
12
|
+
One rule drives every default:
|
|
34
13
|
|
|
35
|
-
|
|
14
|
+
> **If a file exists under `rootDir`, `GET` on its path returns it. A directory without an
|
|
15
|
+
> index file shows a listing of every visible entry.**
|
|
36
16
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
**koa-classic-server** is a high-performance middleware for serving static files with Apache2-like behavior, making file browsing intuitive, observable, and safe.
|
|
40
|
-
|
|
41
|
-
### Core Features
|
|
42
|
-
|
|
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
|
|
17
|
+
Defaults are transparent — the middleware never hides or restricts your files unless you ask.
|
|
18
|
+
Hardening is opt-in.
|
|
59
19
|
|
|
60
20
|
---
|
|
61
21
|
|
|
62
|
-
##
|
|
22
|
+
## Install
|
|
63
23
|
|
|
64
24
|
```bash
|
|
65
25
|
npm install koa-classic-server
|
|
66
26
|
```
|
|
67
27
|
|
|
68
|
-
**
|
|
69
|
-
- Node.js >= 18.0.0
|
|
70
|
-
- Koa >= 2.0.0 (Koa 3 requires >= 3.1.2)
|
|
71
|
-
|
|
72
|
-
---
|
|
28
|
+
Requires **Node ≥ 18** and **Koa ≥ 2** (Koa 3 needs ≥ 3.1.2).
|
|
73
29
|
|
|
74
|
-
|
|
30
|
+
### Import
|
|
75
31
|
|
|
76
|
-
|
|
32
|
+
Ships as both CommonJS and ES modules via conditional exports — use whichever your project prefers:
|
|
77
33
|
|
|
78
34
|
```javascript
|
|
79
|
-
|
|
35
|
+
// CommonJS
|
|
80
36
|
const koaClassicServer = require('koa-classic-server');
|
|
81
37
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Serve files from "public" directory
|
|
85
|
-
app.use(koaClassicServer(__dirname + '/public'));
|
|
86
|
-
|
|
87
|
-
app.listen(3000);
|
|
88
|
-
console.log('Server running on http://localhost:3000');
|
|
38
|
+
// ES modules
|
|
39
|
+
import koaClassicServer from 'koa-classic-server';
|
|
89
40
|
```
|
|
90
41
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
```javascript
|
|
94
|
-
const Koa = require('koa');
|
|
95
|
-
const koaClassicServer = require('koa-classic-server');
|
|
96
|
-
|
|
97
|
-
const app = new Koa();
|
|
98
|
-
|
|
99
|
-
app.use(koaClassicServer(__dirname + '/public', {
|
|
100
|
-
index: ['index.html', 'index.htm'],
|
|
101
|
-
urlPrefix: '/static',
|
|
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,
|
|
109
|
-
}));
|
|
110
|
-
|
|
111
|
-
app.listen(3000);
|
|
112
|
-
```
|
|
42
|
+
The examples below use CommonJS; they work identically with the ESM import.
|
|
113
43
|
|
|
114
44
|
---
|
|
115
45
|
|
|
116
|
-
##
|
|
117
|
-
|
|
118
|
-
### 1. Import
|
|
119
|
-
|
|
120
|
-
```javascript
|
|
121
|
-
// CommonJS
|
|
122
|
-
const koaClassicServer = require('koa-classic-server');
|
|
123
|
-
|
|
124
|
-
// ES Modules
|
|
125
|
-
import koaClassicServer from 'koa-classic-server';
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### 2. Basic File Server
|
|
46
|
+
## Quick start
|
|
129
47
|
|
|
130
48
|
```javascript
|
|
131
49
|
const Koa = require('koa');
|
|
@@ -133,864 +51,314 @@ const path = require('path');
|
|
|
133
51
|
const koaClassicServer = require('koa-classic-server');
|
|
134
52
|
|
|
135
53
|
const app = new Koa();
|
|
136
|
-
|
|
137
|
-
app.use(koaClassicServer(path.join(__dirname, 'public'), {
|
|
138
|
-
dirListing: { enabled: true },
|
|
139
|
-
index: ['index.html'],
|
|
140
|
-
}));
|
|
141
|
-
|
|
54
|
+
app.use(koaClassicServer(path.join(__dirname, 'public')));
|
|
142
55
|
app.listen(3000);
|
|
56
|
+
// → serves ./public, with a browsable listing when there's no index file
|
|
143
57
|
```
|
|
144
58
|
|
|
145
|
-
|
|
59
|
+
> `rootDir` **must be an absolute path** — use `path.join(__dirname, ...)`.
|
|
146
60
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
```
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Examples
|
|
64
|
+
|
|
65
|
+
Each example is self-contained. Copy, adjust the path, run.
|
|
153
66
|
|
|
154
|
-
###
|
|
67
|
+
### Serve a site with an index file
|
|
155
68
|
|
|
156
69
|
```javascript
|
|
157
|
-
app.use(koaClassicServer(__dirname, {
|
|
158
|
-
|
|
70
|
+
app.use(koaClassicServer(path.join(__dirname, 'public'), {
|
|
71
|
+
index: ['index.html'], // GET / → public/index.html
|
|
159
72
|
}));
|
|
160
|
-
// /api/* is passed through to the next middleware untouched.
|
|
161
73
|
```
|
|
162
74
|
|
|
163
|
-
###
|
|
164
|
-
|
|
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:
|
|
75
|
+
### A browsable, sortable, paginated listing
|
|
166
76
|
|
|
167
77
|
```javascript
|
|
168
|
-
app.use(koaClassicServer(__dirname
|
|
78
|
+
app.use(koaClassicServer(path.join(__dirname, 'uploads'), {
|
|
169
79
|
dirListing: {
|
|
170
80
|
enabled: true,
|
|
171
|
-
|
|
172
|
-
entriesPerPage: 100, // entries per page in the listing UI (default; 0 = disabled)
|
|
81
|
+
entriesPerPage: 100, // paginate with ?page=N once a folder exceeds this
|
|
173
82
|
},
|
|
174
83
|
}));
|
|
84
|
+
// Click Name / Type / Size to sort; sort order is kept across pages.
|
|
175
85
|
```
|
|
176
86
|
|
|
177
|
-
|
|
178
|
-
|
|
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.
|
|
184
|
-
|
|
185
|
-
### 6. Template Engine with Timeout + AbortSignal (V3)
|
|
186
|
-
|
|
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.
|
|
87
|
+
### Clean URLs with a template engine (EJS)
|
|
188
88
|
|
|
189
89
|
```javascript
|
|
190
90
|
const ejs = require('ejs');
|
|
191
|
-
const koaClassicServer = require('koa-classic-server');
|
|
192
91
|
|
|
193
|
-
app.use(koaClassicServer(__dirname
|
|
92
|
+
app.use(koaClassicServer(path.join(__dirname, 'views'), {
|
|
93
|
+
hideExtension: { ext: '.ejs' }, // GET /about → views/about.ejs ; GET /about.ejs → 301 /about
|
|
194
94
|
template: {
|
|
195
95
|
ext: ['ejs'],
|
|
196
|
-
|
|
197
|
-
render: async (ctx, next, filePath,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (signal.aborted) return;
|
|
201
|
-
ctx.body = await ejs.renderFile(filePath, data);
|
|
202
|
-
ctx.type = 'text/html';
|
|
96
|
+
// signature: (ctx, next, filePath, rawBuffer, signal)
|
|
97
|
+
render: async (ctx, next, filePath, rawBuffer, signal) => {
|
|
98
|
+
ctx.type = 'html';
|
|
99
|
+
ctx.body = await ejs.renderFile(filePath, { user: ctx.state.user }, { signal });
|
|
203
100
|
},
|
|
204
101
|
},
|
|
205
102
|
}));
|
|
206
103
|
```
|
|
207
104
|
|
|
208
|
-
|
|
105
|
+
`rawBuffer` (4th arg) is the file's bytes if already in cache (may be `null`) and is **read-only**.
|
|
106
|
+
`signal` (5th arg) aborts on timeout **and** on client disconnect — forward it to your I/O.
|
|
209
107
|
|
|
210
|
-
###
|
|
211
|
-
|
|
212
|
-
By default the middleware logs to `console`. Pass any object exposing `error`, `warn`, `info`, `debug` to integrate with your production logging stack:
|
|
108
|
+
### HTTP caching (production)
|
|
213
109
|
|
|
214
110
|
```javascript
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
logger: pino, // any { error, warn, info, debug }-shaped object works
|
|
111
|
+
app.use(koaClassicServer(path.join(__dirname, 'public'), {
|
|
112
|
+
browserCacheEnabled: true, // ETag + Last-Modified, and 304 on revalidation
|
|
113
|
+
browserCacheMaxAge: 86400, // Cache-Control: max-age=86400 (24h)
|
|
219
114
|
}));
|
|
220
115
|
```
|
|
221
116
|
|
|
222
|
-
|
|
223
|
-
|
|
117
|
+
Off by default (development-friendly). Conditional requests are fully handled: `If-None-Match`
|
|
118
|
+
(lists, `*`, weak tags), `If-Modified-Since`, and `Range` (206) all behave per the HTTP spec.
|
|
224
119
|
|
|
225
|
-
###
|
|
120
|
+
### Compression — automatic
|
|
226
121
|
|
|
227
|
-
|
|
122
|
+
Brotli/gzip are **on by default** for compressible types, negotiated from `Accept-Encoding`
|
|
123
|
+
and cached server-side. Nothing to configure. To tune or disable:
|
|
228
124
|
|
|
229
125
|
```javascript
|
|
230
|
-
app.use(koaClassicServer(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
126
|
+
app.use(koaClassicServer(root, {
|
|
127
|
+
compression: {
|
|
128
|
+
encodings: ['br', 'gzip'], // server preference order; [] disables
|
|
129
|
+
minFileSize: 1024, // don't compress tiny files
|
|
243
130
|
},
|
|
244
131
|
}));
|
|
245
|
-
```
|
|
246
132
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
Serve `.ejs` (or any extension) as extensionless URLs and 301-redirect the canonical form:
|
|
250
|
-
|
|
251
|
-
```javascript
|
|
252
|
-
app.use(koaClassicServer(__dirname + '/views', {
|
|
253
|
-
hideExtension: {
|
|
254
|
-
ext: '.ejs', // required, must start with '.'
|
|
255
|
-
redirect: 301, // optional, 301 (default) or 302
|
|
256
|
-
},
|
|
257
|
-
template: {
|
|
258
|
-
ext: ['ejs'],
|
|
259
|
-
render: async (ctx, next, filePath) => {
|
|
260
|
-
ctx.body = await ejs.renderFile(filePath);
|
|
261
|
-
ctx.type = 'text/html';
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
}));
|
|
265
|
-
// GET /about → serves views/about.ejs
|
|
266
|
-
// GET /about.ejs → 301 redirect to /about
|
|
133
|
+
// or turn it off entirely:
|
|
134
|
+
app.use(koaClassicServer(root, { compression: false }));
|
|
267
135
|
```
|
|
268
136
|
|
|
269
|
-
###
|
|
137
|
+
### Hide sensitive files
|
|
270
138
|
|
|
271
|
-
|
|
139
|
+
Dot-files are **served by default** (the operator's directory is the source of truth). For a
|
|
140
|
+
public deployment, hide them explicitly:
|
|
272
141
|
|
|
273
142
|
```javascript
|
|
274
|
-
app.use(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
app.use(koaClassicServer(__dirname + '/www', {
|
|
282
|
-
useOriginalUrl: false, // use ctx.url (rewritten) instead of ctx.originalUrl
|
|
143
|
+
app.use(koaClassicServer(path.join(__dirname, 'www'), {
|
|
144
|
+
hidden: {
|
|
145
|
+
dotFiles: { default: 'hidden', whitelist: ['.well-known'] }, // keep ACME/Let's Encrypt working
|
|
146
|
+
dotDirs: { default: 'hidden', whitelist: ['.well-known'] },
|
|
147
|
+
alwaysHide: ['*.key', /secret/i], // path-aware patterns
|
|
148
|
+
},
|
|
283
149
|
}));
|
|
150
|
+
// .env, .git/config, *.key → 404
|
|
284
151
|
```
|
|
285
152
|
|
|
286
|
-
###
|
|
153
|
+
### Mount under a prefix, pass through some routes
|
|
287
154
|
|
|
288
155
|
```javascript
|
|
289
|
-
app.use(koaClassicServer(__dirname
|
|
290
|
-
|
|
291
|
-
|
|
156
|
+
app.use(koaClassicServer(path.join(__dirname, 'public'), {
|
|
157
|
+
urlPrefix: '/static', // GET /static/app.js → public/app.js
|
|
158
|
+
urlsReserved: ['/api', '/admin'], // first-level paths handed to the next middleware
|
|
292
159
|
}));
|
|
293
160
|
```
|
|
294
161
|
|
|
295
|
-
|
|
162
|
+
### Serve several directories
|
|
296
163
|
|
|
297
|
-
|
|
164
|
+
Mount it once per directory, each under its own `urlPrefix` — a request outside a mount's prefix falls through to the next:
|
|
298
165
|
|
|
299
166
|
```javascript
|
|
300
|
-
|
|
301
|
-
const path = require('path');
|
|
302
|
-
const pino = require('pino')({ level: 'info' });
|
|
303
|
-
const ejs = require('ejs');
|
|
304
|
-
const koaClassicServer = require('koa-classic-server');
|
|
305
|
-
|
|
306
|
-
const app = new Koa();
|
|
307
|
-
|
|
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
|
-
|
|
167
|
+
// Assets under /static, no listing
|
|
323
168
|
app.use(koaClassicServer(path.join(__dirname, 'public'), {
|
|
324
|
-
|
|
325
|
-
dirListing: {
|
|
326
|
-
enabled: process.env.NODE_ENV !== 'production',
|
|
327
|
-
maxEntries: 10000,
|
|
328
|
-
entriesPerPage: 100,
|
|
329
|
-
},
|
|
330
|
-
browserCacheEnabled: true,
|
|
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-/],
|
|
337
|
-
},
|
|
338
|
-
template: {
|
|
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
|
-
},
|
|
169
|
+
urlPrefix: '/static',
|
|
170
|
+
dirListing: { enabled: false },
|
|
346
171
|
}));
|
|
347
172
|
|
|
348
|
-
|
|
173
|
+
// Uploads under /files, browsable
|
|
174
|
+
app.use(koaClassicServer(path.join(__dirname, 'uploads'), {
|
|
175
|
+
urlPrefix: '/files',
|
|
176
|
+
dirListing: { enabled: true },
|
|
177
|
+
}));
|
|
349
178
|
```
|
|
350
179
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
## API Reference
|
|
180
|
+
### Allow HEAD (health checks, preflight)
|
|
354
181
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
Creates a Koa middleware for serving static files.
|
|
358
|
-
|
|
359
|
-
**Parameters:**
|
|
360
|
-
- **`rootDir`** *(String, required)* — Absolute path to the directory containing files
|
|
361
|
-
- **`options`** *(Object, optional)* — Configuration options
|
|
362
|
-
|
|
363
|
-
**Returns:** Koa middleware function
|
|
364
|
-
|
|
365
|
-
### Options Summary
|
|
182
|
+
Only `GET` is accepted by default; other methods fall through to the next middleware:
|
|
366
183
|
|
|
367
184
|
```javascript
|
|
368
|
-
{
|
|
369
|
-
|
|
370
|
-
method: ['GET', 'HEAD'],
|
|
371
|
-
|
|
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
|
-
},
|
|
378
|
-
|
|
379
|
-
// Index file resolution (Array of strings and/or RegExp)
|
|
380
|
-
index: ['index.html', 'index.htm'],
|
|
381
|
-
|
|
382
|
-
// URL routing
|
|
383
|
-
urlPrefix: '/static',
|
|
384
|
-
urlsReserved: ['/api', '/admin'],
|
|
385
|
-
useOriginalUrl: true,
|
|
386
|
-
|
|
387
|
-
// Hidden files / dirs
|
|
388
|
-
hidden: {
|
|
389
|
-
dotFiles: { default: 'visible', whitelist: [], blacklist: [] },
|
|
390
|
-
dotDirs: { default: 'visible', whitelist: [], blacklist: [] },
|
|
391
|
-
alwaysHide: [],
|
|
392
|
-
},
|
|
393
|
-
|
|
394
|
-
// Clean URLs
|
|
395
|
-
hideExtension: { ext: '.ejs', redirect: 301 },
|
|
396
|
-
|
|
397
|
-
// Browser HTTP caching
|
|
398
|
-
browserCacheEnabled: false,
|
|
399
|
-
browserCacheMaxAge: 3600,
|
|
400
|
-
|
|
401
|
-
// Template engine
|
|
402
|
-
template: {
|
|
403
|
-
ext: ['ejs'],
|
|
404
|
-
renderTimeout: 30000, // ms; 0 disables the cap
|
|
405
|
-
render: async (ctx, next, filePath, { signal }) => { /* ... */ },
|
|
406
|
-
},
|
|
407
|
-
|
|
408
|
-
// Observability
|
|
409
|
-
logger: console, // any { error, warn, info, debug } shape
|
|
410
|
-
}
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
### Options Details
|
|
414
|
-
|
|
415
|
-
| Option | Type | Default | Description |
|
|
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).
|
|
442
|
-
|
|
443
|
-
---
|
|
444
|
-
|
|
445
|
-
## Directory Listing Features
|
|
446
|
-
|
|
447
|
-
### Sortable Columns
|
|
448
|
-
|
|
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)
|
|
453
|
-
|
|
454
|
-
Visual indicators: `↑` ascending, `↓` descending. Sort + order are preserved across pagination links.
|
|
455
|
-
|
|
456
|
-
### Pagination (V3)
|
|
457
|
-
|
|
458
|
-
When the number of visible entries exceeds `dirListing.entriesPerPage`, a numbered paginator is rendered below the table:
|
|
459
|
-
|
|
460
|
-
```
|
|
461
|
-
« First · ‹ Prev · 0 · 1 · … · 7 · 8 · 9 · Next › · Last »
|
|
462
|
-
```
|
|
463
|
-
|
|
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.
|
|
467
|
-
|
|
468
|
-
### Truncation Banner (V3)
|
|
469
|
-
|
|
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.
|
|
471
|
-
|
|
472
|
-
### File Size Display
|
|
473
|
-
|
|
474
|
-
Human-readable: `1.5 KB`, `2.3 MB`, `1.2 GB`. Directories show `-`.
|
|
475
|
-
|
|
476
|
-
### Navigation
|
|
477
|
-
|
|
478
|
-
- Click folder → enter directory
|
|
479
|
-
- Click file → serve / download
|
|
480
|
-
- **Parent Directory** link → go up one level
|
|
481
|
-
|
|
482
|
-
### Symlink Support
|
|
483
|
-
|
|
484
|
-
The middleware follows symbolic links transparently via `fs.promises.stat()` — useful in NixOS, Docker bind mounts, `npm link`, and Capistrano-style deploys.
|
|
485
|
-
|
|
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 |
|
|
491
|
-
|
|
492
|
-
Regular files incur zero additional `stat()` overhead.
|
|
493
|
-
|
|
494
|
-
---
|
|
495
|
-
|
|
496
|
-
## Security
|
|
497
|
-
|
|
498
|
-
### Built-in Protection
|
|
499
|
-
|
|
500
|
-
#### 1. Path Traversal
|
|
501
|
-
|
|
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
|
-
```
|
|
507
|
-
|
|
508
|
-
Defense in depth: null-byte rejection → `path.normalize()` → resolved-path boundary check against `rootDir`.
|
|
509
|
-
|
|
510
|
-
#### 2. XSS in Directory Listing
|
|
511
|
-
|
|
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.
|
|
513
|
-
|
|
514
|
-
#### 3. Dot-Files Hidden by Default (V3)
|
|
515
|
-
|
|
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.
|
|
517
|
-
|
|
518
|
-
#### 4. Security Headers on Generated Pages
|
|
519
|
-
|
|
520
|
-
The middleware emits the following on directory listings and error pages (404/405/500/etc.):
|
|
521
|
-
|
|
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=()` |
|
|
529
|
-
|
|
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
|
|
533
|
-
|
|
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.
|
|
535
|
-
|
|
536
|
-
#### 6. Reserved URLs
|
|
537
|
-
|
|
538
|
-
```javascript
|
|
539
|
-
app.use(koaClassicServer(__dirname, {
|
|
540
|
-
urlsReserved: ['/admin', '/api', '/.git', '/node_modules'],
|
|
185
|
+
app.use(koaClassicServer(root, {
|
|
186
|
+
method: ['GET', 'HEAD'], // HEAD mirrors GET: same status + headers, no body
|
|
541
187
|
}));
|
|
542
188
|
```
|
|
543
189
|
|
|
544
|
-
|
|
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)
|
|
549
|
-
|
|
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
|
|
190
|
+
### Behind a URL rewriter (i18n, routing)
|
|
603
191
|
|
|
604
|
-
|
|
192
|
+
When an upstream middleware mutates `ctx.url`, tell the server to resolve against the rewritten URL:
|
|
605
193
|
|
|
606
194
|
```javascript
|
|
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
195
|
app.use(async (ctx, next) => {
|
|
620
|
-
if (
|
|
621
|
-
ctx.status = 421;
|
|
622
|
-
ctx.body = 'Misdirected Request';
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
196
|
+
if (ctx.path.startsWith('/it/')) ctx.url = ctx.url.replace(/^\/it/, ''); // /it/page → /page
|
|
625
197
|
await next();
|
|
626
198
|
});
|
|
627
199
|
|
|
628
|
-
|
|
629
|
-
//
|
|
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
|
|
200
|
+
app.use(koaClassicServer(path.join(__dirname, 'www'), {
|
|
201
|
+
useOriginalUrl: false, // resolve ctx.url (rewritten) instead of ctx.originalUrl
|
|
667
202
|
}));
|
|
668
|
-
|
|
669
|
-
app.listen(3000);
|
|
670
203
|
```
|
|
671
204
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
---
|
|
675
|
-
|
|
676
|
-
## Performance
|
|
677
|
-
|
|
678
|
-
### Optimizations
|
|
679
|
-
|
|
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
|
|
686
|
-
|
|
687
|
-
### Benchmarks
|
|
688
|
-
|
|
689
|
-
See [`docs/BENCHMARKS.md`](./docs/BENCHMARKS.md) and [`docs/PERFORMANCE_COMPARISON.md`](./docs/PERFORMANCE_COMPARISON.md) for full benchmarks and methodology.
|
|
690
|
-
|
|
691
|
-
---
|
|
692
|
-
|
|
693
|
-
## Testing
|
|
694
|
-
|
|
695
|
-
```bash
|
|
696
|
-
# Run all tests
|
|
697
|
-
npm test
|
|
698
|
-
|
|
699
|
-
# Run security tests only
|
|
700
|
-
npm run test:security
|
|
701
|
-
|
|
702
|
-
# Run performance benchmarks
|
|
703
|
-
npm run test:performance
|
|
704
|
-
```
|
|
705
|
-
|
|
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)
|
|
715
|
-
- ✅ Performance benchmarks
|
|
716
|
-
|
|
717
|
-
---
|
|
205
|
+
### Keep symlinks inside `rootDir`
|
|
718
206
|
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
725
|
-
|
|
726
|
-
### Template Engine
|
|
727
|
-
- **[TEMPLATE_ENGINE_GUIDE.md](./docs/template-engine/TEMPLATE_ENGINE_GUIDE.md)** — EJS, Pug, Handlebars, Nunjucks; AbortSignal + timeout patterns
|
|
728
|
-
|
|
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
|
|
732
|
-
|
|
733
|
-
### Security
|
|
734
|
-
- **[security_improvement_for_V3.md](./docs/security_improvement_for_V3.md)** — Audit roadmap and status
|
|
735
|
-
|
|
736
|
-
### Performance
|
|
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
|
|
741
|
-
|
|
742
|
-
### Code Quality
|
|
743
|
-
- **[CODE_REVIEW.md](./docs/CODE_REVIEW.md)** — Code review and standards
|
|
744
|
-
- **[DEBUG_REPORT.md](./docs/DEBUG_REPORT.md)** — Known limitations and debugging
|
|
745
|
-
|
|
746
|
-
---
|
|
747
|
-
|
|
748
|
-
## Migration Guide
|
|
749
|
-
|
|
750
|
-
### From v2.x to v3.x
|
|
751
|
-
|
|
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 |
|
|
763
|
-
|
|
764
|
-
**Quick migration**
|
|
207
|
+
If `rootDir` holds files writable by untrusted parties (uploads, multi-tenant), stop a planted
|
|
208
|
+
symlink from escaping the served tree:
|
|
765
209
|
|
|
766
210
|
```javascript
|
|
767
|
-
// v2.x
|
|
768
211
|
app.use(koaClassicServer(root, {
|
|
769
|
-
|
|
770
|
-
enableCaching: true,
|
|
771
|
-
cacheMaxAge: 3600,
|
|
772
|
-
showDirContents: true,
|
|
773
|
-
}));
|
|
774
|
-
|
|
775
|
-
// v3.x
|
|
776
|
-
app.use(koaClassicServer(root, {
|
|
777
|
-
index: ['index.html'],
|
|
778
|
-
browserCacheEnabled: true,
|
|
779
|
-
browserCacheMaxAge: 3600,
|
|
780
|
-
dirListing: { enabled: true },
|
|
212
|
+
symlinks: 'follow-within-root', // a link resolving outside rootDir → 404
|
|
781
213
|
}));
|
|
782
214
|
```
|
|
783
215
|
|
|
784
|
-
|
|
216
|
+
### A production-shaped setup
|
|
785
217
|
|
|
786
218
|
```javascript
|
|
787
|
-
|
|
788
|
-
{ hidden: { dotFiles: { default: 'visible' } } }
|
|
219
|
+
const pino = require('pino')({ level: 'info' });
|
|
789
220
|
|
|
790
|
-
|
|
791
|
-
|
|
221
|
+
app.use(koaClassicServer(path.join(__dirname, 'public'), {
|
|
222
|
+
index: ['index.html'],
|
|
223
|
+
dirListing: { enabled: process.env.NODE_ENV !== 'production' },
|
|
224
|
+
browserCacheEnabled: true,
|
|
225
|
+
browserCacheMaxAge: 86400,
|
|
226
|
+
logger: pino, // any { error, warn, info, debug } object (Pino/Winston/Bunyan/console)
|
|
792
227
|
hidden: {
|
|
793
228
|
dotFiles: { default: 'hidden', whitelist: ['.well-known'] },
|
|
794
229
|
dotDirs: { default: 'hidden', whitelist: ['.well-known'] },
|
|
795
230
|
},
|
|
796
|
-
}
|
|
231
|
+
}));
|
|
797
232
|
```
|
|
798
233
|
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
```
|
|
234
|
+
---
|
|
815
235
|
|
|
816
|
-
|
|
236
|
+
## What's new in v4
|
|
817
237
|
|
|
818
|
-
|
|
819
|
-
// v1.x
|
|
820
|
-
{ index: 'index.html' }
|
|
238
|
+
**v4.0.0** is the robustness release. One behavior change made it a major:
|
|
821
239
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
240
|
+
- **Canonical trailing slash** (default on): `GET /dir` → `301 /dir/`, and `GET /file/` → `404`,
|
|
241
|
+
so relative links in an index page resolve correctly. Restore the old behavior with
|
|
242
|
+
`dirListing: { trailingSlash: false }`.
|
|
243
|
+
- **Bounded compression** — `compression.maxFileSize` (10 MB) caps the buffered high-quality path;
|
|
244
|
+
larger files still compress, via bounded-RAM streaming. Opt out with `maxFileSize: false`.
|
|
245
|
+
- **Stricter option validation** — a non-object `options` argument now throws instead of coercing.
|
|
825
246
|
|
|
826
|
-
|
|
247
|
+
Plus HTTP-conformance fixes across content negotiation (`Accept-Encoding` q-values, `Vary`),
|
|
248
|
+
conditional requests (`If-None-Match` lists/`*`/weak, precedence over `Range`), and safer
|
|
249
|
+
config validation (malformed `urlPrefix` / `urlsReserved` / `browserCacheMaxAge` now warn, and
|
|
250
|
+
will throw in the next major). Full details in the **[changelog](./docs/CHANGELOG.md)**.
|
|
827
251
|
|
|
828
252
|
---
|
|
829
253
|
|
|
830
|
-
##
|
|
254
|
+
## Options
|
|
831
255
|
|
|
832
|
-
|
|
256
|
+
Defaults shown; every option is optional.
|
|
833
257
|
|
|
834
258
|
```javascript
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const app = new Koa();
|
|
839
|
-
app.use(koaClassicServer(__dirname + '/public'));
|
|
840
|
-
app.listen(3000);
|
|
841
|
-
```
|
|
842
|
-
|
|
843
|
-
### Example 2: Multi-Directory Server
|
|
259
|
+
koaClassicServer(rootDir, {
|
|
260
|
+
method: ['GET'], // allowed HTTP methods
|
|
844
261
|
|
|
845
|
-
```javascript
|
|
846
|
-
const Koa = require('koa');
|
|
847
|
-
const koaClassicServer = require('koa-classic-server');
|
|
848
|
-
|
|
849
|
-
const app = new Koa();
|
|
850
|
-
|
|
851
|
-
// Static assets — no listing in production
|
|
852
|
-
app.use(koaClassicServer(__dirname + '/public', {
|
|
853
|
-
urlPrefix: '/static',
|
|
854
|
-
dirListing: { enabled: false },
|
|
855
|
-
}));
|
|
856
|
-
|
|
857
|
-
// User uploads — paginated browsable index
|
|
858
|
-
app.use(koaClassicServer(__dirname + '/uploads', {
|
|
859
|
-
urlPrefix: '/files',
|
|
860
262
|
dirListing: {
|
|
861
|
-
enabled: true,
|
|
862
|
-
|
|
863
|
-
|
|
263
|
+
enabled: true, // render a listing when no index matches (false → 404)
|
|
264
|
+
entriesPerPage: 100, // paginate above this (0 = off)
|
|
265
|
+
maxEntries: 10000, // safety-net cap on entries rendered (0 = off)
|
|
266
|
+
trailingSlash: true, // v4: /dir → 301 /dir/, /file/ → 404
|
|
864
267
|
},
|
|
865
|
-
}));
|
|
866
|
-
|
|
867
|
-
app.listen(3000);
|
|
868
|
-
```
|
|
869
268
|
|
|
870
|
-
|
|
269
|
+
index: [], // e.g. ['index.html']; strings and/or RegExp
|
|
871
270
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const ejs = require('ejs');
|
|
271
|
+
urlPrefix: '', // e.g. '/static' (leading slash, no trailing)
|
|
272
|
+
urlsReserved: [], // e.g. ['/api'] — first-level, passed to next()
|
|
273
|
+
useOriginalUrl: true, // false when an upstream middleware rewrites ctx.url
|
|
876
274
|
|
|
877
|
-
|
|
275
|
+
hideExtension: { ext: '.ejs', redirect: 301 }, // clean URLs
|
|
878
276
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
renderTimeout: 3000,
|
|
884
|
-
render: async (ctx, next, filePath, { signal }) => {
|
|
885
|
-
ctx.body = await ejs.renderFile(filePath, {
|
|
886
|
-
dev: true,
|
|
887
|
-
timestamp: Date.now(),
|
|
888
|
-
}, { signal });
|
|
889
|
-
ctx.type = 'text/html';
|
|
890
|
-
},
|
|
277
|
+
hidden: { // everything visible by default
|
|
278
|
+
dotFiles: { default: 'visible', whitelist: [], blacklist: [] },
|
|
279
|
+
dotDirs: { default: 'visible', whitelist: [], blacklist: [] },
|
|
280
|
+
alwaysHide: [], // glob strings or RegExp, path-aware
|
|
891
281
|
},
|
|
892
|
-
}));
|
|
893
|
-
|
|
894
|
-
app.listen(3000);
|
|
895
|
-
```
|
|
896
|
-
|
|
897
|
-
### Example 4: Production with Pino Logger + Caching
|
|
898
|
-
|
|
899
|
-
```javascript
|
|
900
|
-
const Koa = require('koa');
|
|
901
|
-
const pino = require('pino')({ level: 'info' });
|
|
902
|
-
const koaClassicServer = require('koa-classic-server');
|
|
903
282
|
|
|
904
|
-
|
|
283
|
+
browserCacheEnabled: false, // ETag/Last-Modified/304 (recommended true in prod)
|
|
284
|
+
browserCacheMaxAge: 3600, // Cache-Control max-age (seconds)
|
|
905
285
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
app.listen(3000);
|
|
915
|
-
```
|
|
916
|
-
|
|
917
|
-
---
|
|
286
|
+
compression: { // or `compression: false`
|
|
287
|
+
enabled: true,
|
|
288
|
+
encodings: ['br', 'gzip'],
|
|
289
|
+
minFileSize: 1024,
|
|
290
|
+
maxFileSize: 10485760, // 10 MB buffered-path cap (false = no cap)
|
|
291
|
+
mimeTypes: [], // override the compressible-type list
|
|
292
|
+
},
|
|
918
293
|
|
|
919
|
-
|
|
294
|
+
serverCache: { // in-memory server-side caches
|
|
295
|
+
rawFile: { enabled: false }, // cache raw file buffers
|
|
296
|
+
compressedFile: { enabled: true }, // cache br/gzip responses
|
|
297
|
+
},
|
|
920
298
|
|
|
921
|
-
|
|
299
|
+
symlinks: 'follow', // 'follow' | 'follow-within-root' | 'deny'
|
|
300
|
+
staticSecurityHeaders: { nosniff: false }, // set nosniff on static responses
|
|
922
301
|
|
|
923
|
-
|
|
302
|
+
template: {
|
|
303
|
+
ext: [], // e.g. ['ejs']
|
|
304
|
+
renderTimeout: 30000, // ms; on timeout the request fails closed (0 = off)
|
|
305
|
+
render: async (ctx, next, filePath, rawBuffer, signal) => { /* ... */ },
|
|
306
|
+
},
|
|
924
307
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
koaClassicServer(__dirname + '/public') // ✅ absolute
|
|
928
|
-
koaClassicServer(path.join(__dirname, 'pub')) // ✅ absolute
|
|
308
|
+
logger: console, // { error, warn, info, debug }
|
|
309
|
+
})
|
|
929
310
|
```
|
|
930
311
|
|
|
931
|
-
|
|
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**
|
|
940
|
-
|
|
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.
|
|
942
|
-
|
|
943
|
-
See full troubleshooting: [DEBUG_REPORT.md](./docs/DEBUG_REPORT.md).
|
|
312
|
+
Full per-option reference: **[docs/DOCUMENTATION.md](./docs/DOCUMENTATION.md)**.
|
|
944
313
|
|
|
945
314
|
---
|
|
946
315
|
|
|
947
|
-
##
|
|
316
|
+
## Security
|
|
948
317
|
|
|
949
|
-
|
|
318
|
+
Defaults are transparent, so **hardening is a deliberate opt-in**. The single source of truth is
|
|
319
|
+
the **[Security Hardening Guide](./docs/SECURITY_HARDENING.md)** — threat models, per-topic
|
|
320
|
+
recommendations, per-profile checklists, and a copy-paste maximally-hardened config.
|
|
950
321
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
5. Submit a pull request
|
|
322
|
+
Built in and always on: path-traversal defense (traversal / encoded / null-byte / boundary →
|
|
323
|
+
404 or 400, never a leak), HTML-escaping + hash-based CSP on the listing, security headers on
|
|
324
|
+
generated pages, and last-resort error containment. Host validation and static-file CSP/HSTS
|
|
325
|
+
are intentionally left to a reverse proxy or an upstream middleware.
|
|
956
326
|
|
|
957
327
|
---
|
|
958
328
|
|
|
959
|
-
##
|
|
960
|
-
|
|
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))
|
|
964
|
-
|
|
965
|
-
See [DEBUG_REPORT.md](./docs/DEBUG_REPORT.md) for technical details.
|
|
329
|
+
## Migrating from v3
|
|
966
330
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
331
|
+
| What | v3 | v4 |
|
|
332
|
+
|---|---|---|
|
|
333
|
+
| `GET /dir` (directory) | serves the index/listing | **301 → `/dir/`** (set `dirListing.trailingSlash: false` to keep v3) |
|
|
334
|
+
| `GET /file/` (file + slash) | serves the file | **404** |
|
|
335
|
+
| Huge compressible file | buffered whole into RAM | streamed above `compression.maxFileSize` (10 MB) |
|
|
336
|
+
| `options` not an object | coerced / crashed | **throws** at construction |
|
|
337
|
+
| Malformed `urlPrefix` / `urlsReserved` / negative `browserCacheMaxAge` | silent | **deprecation warning** (throws next major) |
|
|
970
338
|
|
|
971
|
-
|
|
339
|
+
The v2 → v3 notes remain in the **[changelog](./docs/CHANGELOG.md)**.
|
|
972
340
|
|
|
973
341
|
---
|
|
974
342
|
|
|
975
|
-
##
|
|
343
|
+
## Testing
|
|
976
344
|
|
|
977
|
-
|
|
345
|
+
```bash
|
|
346
|
+
npm test # full suite (lints first) — 717 tests
|
|
347
|
+
npm run test:security # security tests only
|
|
348
|
+
npm run test:performance # benchmarks
|
|
349
|
+
```
|
|
978
350
|
|
|
979
351
|
---
|
|
980
352
|
|
|
981
|
-
##
|
|
353
|
+
## Docs
|
|
982
354
|
|
|
983
|
-
- **[
|
|
984
|
-
- **[
|
|
985
|
-
- **[
|
|
986
|
-
- **[
|
|
355
|
+
- **[DOCUMENTATION.md](./docs/DOCUMENTATION.md)** — full API reference
|
|
356
|
+
- **[SECURITY_HARDENING.md](./docs/SECURITY_HARDENING.md)** — hardening guide (canonical)
|
|
357
|
+
- **[CHANGELOG.md](./docs/CHANGELOG.md)** — version history
|
|
358
|
+
- **[TEMPLATE_ENGINE_GUIDE.md](./docs/template-engine/TEMPLATE_ENGINE_GUIDE.md)** — EJS/Pug/Handlebars/Nunjucks
|
|
987
359
|
|
|
988
360
|
---
|
|
989
361
|
|
|
990
|
-
##
|
|
991
|
-
|
|
992
|
-
See [CHANGELOG.md](./docs/CHANGELOG.md) for version history.
|
|
993
|
-
|
|
994
|
-
---
|
|
362
|
+
## License
|
|
995
363
|
|
|
996
|
-
|
|
364
|
+
MIT © Italo Paesano · [npm](https://www.npmjs.com/package/koa-classic-server) · [GitHub](https://github.com/italopaesano/koa-classic-server) · [Issues](https://github.com/italopaesano/koa-classic-server/issues)
|