koa-classic-server 2.6.1 → 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/CLAUDE.md +101 -0
- package/README.md +564 -591
- package/__tests__/benchmark-results-v3.0.0.txt +372 -0
- package/__tests__/benchmark.js +1 -1
- package/__tests__/caching-headers.test.js +30 -30
- package/__tests__/compression-fixtures/data.json +1 -0
- package/__tests__/compression-fixtures/large.txt +1 -0
- package/__tests__/compression-fixtures/small.txt +1 -0
- package/__tests__/compression.test.js +284 -0
- package/__tests__/customTest/serversToLoad.util.js +5 -5
- package/__tests__/demo-regex-index.js +4 -4
- package/__tests__/deprecation-warnings.test.js +71 -183
- package/__tests__/directory-sorting-links.test.js +1 -1
- package/__tests__/dt-unknown.test.js +39 -28
- package/__tests__/ejs.test.js +1 -1
- package/__tests__/hidden-fixtures/.dot-dir/inside.txt +1 -0
- package/__tests__/hidden-fixtures/.env +2 -0
- package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +1 -0
- package/__tests__/hidden-fixtures/config/secrets/password.txt +1 -0
- package/__tests__/hidden-fixtures/data.key +1 -0
- package/__tests__/hidden-fixtures/file.secret +1 -0
- package/__tests__/hidden-fixtures/index.html +1 -0
- package/__tests__/hidden-fixtures/normal.txt +1 -0
- package/__tests__/hidden-fixtures/subdir/.env +1 -0
- package/__tests__/hidden-fixtures/subdir/regular.txt +1 -0
- package/__tests__/hidden-option.test.js +407 -0
- package/__tests__/hideExtension.test.js +70 -13
- package/__tests__/index-option.test.js +18 -16
- package/__tests__/index.test.js +14 -10
- package/__tests__/listing.test.js +437 -0
- package/__tests__/logger.test.js +232 -0
- package/__tests__/range-fixtures/sample.txt +1 -0
- package/__tests__/range.test.js +223 -0
- package/__tests__/security-headers.test.js +165 -0
- package/__tests__/security.test.js +148 -162
- package/__tests__/server-cache-fixtures/large.txt +1 -0
- package/__tests__/server-cache-fixtures/small.txt +1 -0
- package/__tests__/server-cache.test.js +594 -0
- package/__tests__/symlink.test.js +18 -15
- package/__tests__/template-timeout.test.js +321 -0
- package/docs/ACTION_PLAN.md +293 -0
- package/docs/CHANGELOG.md +289 -0
- package/docs/CODE_REVIEW.md +2 -0
- package/docs/DOCUMENTATION.md +259 -32
- package/docs/EXAMPLES_INDEX_OPTION.md +3 -3
- package/docs/FLOW_DIAGRAM.md +15 -13
- package/docs/INDEX_OPTION_PRIORITY.md +2 -2
- package/docs/OPTIMIZATION_HTTP_CACHING.md +2 -0
- package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
- package/docs/PERFORMANCE_COMPARISON.md +7 -7
- package/docs/security_improvement_for_V3.md +421 -0
- package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +5 -5
- package/docs/template-engine/esempi-incrementali.js +1 -1
- package/eslint.config.mjs +17 -0
- package/index.cjs +1507 -429
- package/index.mjs +1 -5
- package/package.json +9 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Piano d'Azione — koa-classic-server
|
|
2
|
+
|
|
3
|
+
> Documento di lavoro per il miglioramento progressivo del progetto.
|
|
4
|
+
> Ogni fase segue il ciclo: **Descrizione → Soluzioni proposte → Domande → Implementazione → Test → Avanzamento**
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Indice delle fasi
|
|
9
|
+
|
|
10
|
+
| # | Fase | Priorità | Stato |
|
|
11
|
+
|---|------|----------|-------|
|
|
12
|
+
| 1 | [File nascosti nel directory listing](#fase-1--file-nascosti-nel-directory-listing) | Alta | ⬜ Da fare |
|
|
13
|
+
| 2 | [Opzione glob per escludere file sensibili](#fase-2--opzione-glob-per-escludere-file-sensibili) | Alta | ⬜ Da fare |
|
|
14
|
+
| 3 | [Content-Security-Policy nell'HTML generato](#fase-3--content-security-policy-nellhtml-generato) | Media | ⬜ Da fare |
|
|
15
|
+
| 4 | [Aggiunta ESLint e standardizzazione `===`](#fase-4--aggiunta-eslint-e-standardizzazione-) | Media | ⬜ Da fare |
|
|
16
|
+
| 5 | [Range Requests HTTP 206](#fase-5--range-requests-http-206) | Media | ⬜ Da fare |
|
|
17
|
+
| 6 | [Gzip / Brotli compression](#fase-6--gzip--brotli-compression) | Media | ⬜ Da fare |
|
|
18
|
+
| 7 | [Traduzione commenti in inglese nei test](#fase-7--traduzione-commenti-in-inglese-nei-test) | Bassa | ⬜ Da fare |
|
|
19
|
+
| 8 | [Roadmap rimozione opzioni deprecate (v3.0.0)](#fase-8--roadmap-rimozione-opzioni-deprecate-v300) | Bassa | ⬜ Da fare |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
> **Legenda stato:** ⬜ Da fare · 🔄 In corso · ✅ Completato · ⏸️ In pausa
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Processo standard per ogni fase
|
|
28
|
+
|
|
29
|
+
Per ogni fase si segue sempre questo ciclo:
|
|
30
|
+
|
|
31
|
+
1. **Descrizione** — Spiegazione del problema e del suo impatto
|
|
32
|
+
2. **Soluzioni proposte** — Almeno 2 alternative valutate con pro/contro
|
|
33
|
+
3. **Domande** — Se le informazioni disponibili sono < 90%, si chiedono chiarimenti prima di procedere
|
|
34
|
+
4. **Implementazione** — Modifica del codice sul branch `claude/project-review-EN6kt`
|
|
35
|
+
5. **Test** — Esecuzione della suite di test e verifica regressioni
|
|
36
|
+
6. **Avanzamento** — Aggiornamento dello stato in questo file e passaggio alla fase successiva
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Fase 1 — File nascosti nel directory listing
|
|
41
|
+
|
|
42
|
+
**Stato:** ⬜ Da fare
|
|
43
|
+
**Priorità:** Alta
|
|
44
|
+
**File coinvolti:** `index.cjs` (funzione `show_dir`)
|
|
45
|
+
|
|
46
|
+
### Descrizione del problema
|
|
47
|
+
|
|
48
|
+
Il directory listing mostra **tutti** i file e le cartelle presenti nella webroot, inclusi quelli che iniziano con `.` (dot files), come:
|
|
49
|
+
|
|
50
|
+
- `.env` — variabili d'ambiente con credenziali
|
|
51
|
+
- `.gitignore`, `.gitattributes` — metadati Git
|
|
52
|
+
- `.htpasswd` — password Apache
|
|
53
|
+
- `.DS_Store` — metadati macOS
|
|
54
|
+
- `.npmrc` — configurazioni npm con token
|
|
55
|
+
|
|
56
|
+
Server come Apache e Nginx nascondono questi file per default. Un file dimenticato nella webroot potrebbe esporre informazioni sensibili agli utenti del browser.
|
|
57
|
+
|
|
58
|
+
### Soluzioni proposte
|
|
59
|
+
|
|
60
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
61
|
+
|
|
62
|
+
### Domande
|
|
63
|
+
|
|
64
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
65
|
+
|
|
66
|
+
### Implementazione
|
|
67
|
+
|
|
68
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
69
|
+
|
|
70
|
+
### Test
|
|
71
|
+
|
|
72
|
+
*Da verificare dopo l'implementazione.*
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Fase 2 — Opzione glob per escludere file sensibili
|
|
77
|
+
|
|
78
|
+
**Stato:** ⬜ Da fare
|
|
79
|
+
**Priorità:** Alta
|
|
80
|
+
**File coinvolti:** `index.cjs` (opzioni di configurazione + `show_dir`)
|
|
81
|
+
|
|
82
|
+
### Descrizione del problema
|
|
83
|
+
|
|
84
|
+
Non esiste un'opzione per escludere file o cartelle specifici dal serving e dal directory listing tramite pattern (glob o RegExp). Se un file come `config.json`, `secrets.yaml` o qualsiasi altro file sensibile si trova nella webroot, viene servito senza restrizioni.
|
|
85
|
+
|
|
86
|
+
Esempi di pattern utili:
|
|
87
|
+
- `*.env` — tutti i file .env
|
|
88
|
+
- `**/*.secret` — file con estensione .secret
|
|
89
|
+
- `private/**` — intera cartella private
|
|
90
|
+
|
|
91
|
+
### Soluzioni proposte
|
|
92
|
+
|
|
93
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
94
|
+
|
|
95
|
+
### Domande
|
|
96
|
+
|
|
97
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
98
|
+
|
|
99
|
+
### Implementazione
|
|
100
|
+
|
|
101
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
102
|
+
|
|
103
|
+
### Test
|
|
104
|
+
|
|
105
|
+
*Da verificare dopo l'implementazione.*
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Fase 3 — Content-Security-Policy nell'HTML generato
|
|
110
|
+
|
|
111
|
+
**Stato:** ⬜ Da fare
|
|
112
|
+
**Priorità:** Media
|
|
113
|
+
**File coinvolti:** `index.cjs` (funzione `show_dir`)
|
|
114
|
+
|
|
115
|
+
### Descrizione del problema
|
|
116
|
+
|
|
117
|
+
Le pagine HTML generate dal directory listing non includono un header `Content-Security-Policy`. Senza CSP, il browser non ha indicazioni su quali risorse sono autorizzate, aumentando il rischio di attacchi XSS in caso di eventuali vulnerabilità future nel codice di generazione HTML.
|
|
118
|
+
|
|
119
|
+
Il progetto già implementa l'escape HTML (funzione `escapeHtml`), ma aggiungere CSP è un ulteriore livello di difesa in profondità.
|
|
120
|
+
|
|
121
|
+
### Soluzioni proposte
|
|
122
|
+
|
|
123
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
124
|
+
|
|
125
|
+
### Domande
|
|
126
|
+
|
|
127
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
128
|
+
|
|
129
|
+
### Implementazione
|
|
130
|
+
|
|
131
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
132
|
+
|
|
133
|
+
### Test
|
|
134
|
+
|
|
135
|
+
*Da verificare dopo l'implementazione.*
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Fase 4 — Aggiunta ESLint e standardizzazione `===`
|
|
140
|
+
|
|
141
|
+
**Stato:** ⬜ Da fare
|
|
142
|
+
**Priorità:** Media
|
|
143
|
+
**File coinvolti:** `index.cjs`, `package.json`, nuovi file di config
|
|
144
|
+
|
|
145
|
+
### Descrizione del problema
|
|
146
|
+
|
|
147
|
+
Il codice usa `==` (uguaglianza debole con coercizione di tipo) in circa 25 punti, documentati in `docs/CODE_REVIEW.md`. Anche se nel contesto attuale non causano bug, l'uso di `===` è la best practice JavaScript e previene errori sottili in futuro.
|
|
148
|
+
|
|
149
|
+
Inoltre il progetto non ha alcun linter o formatter configurato, il che rende difficile mantenere la coerenza stilistica nel tempo e nelle contribuzioni esterne.
|
|
150
|
+
|
|
151
|
+
### Soluzioni proposte
|
|
152
|
+
|
|
153
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
154
|
+
|
|
155
|
+
### Domande
|
|
156
|
+
|
|
157
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
158
|
+
|
|
159
|
+
### Implementazione
|
|
160
|
+
|
|
161
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
162
|
+
|
|
163
|
+
### Test
|
|
164
|
+
|
|
165
|
+
*Da verificare dopo l'implementazione.*
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Fase 5 — Range Requests HTTP 206
|
|
170
|
+
|
|
171
|
+
**Stato:** ⬜ Da fare
|
|
172
|
+
**Priorità:** Media
|
|
173
|
+
**File coinvolti:** `index.cjs` (logica di file serving)
|
|
174
|
+
|
|
175
|
+
### Descrizione del problema
|
|
176
|
+
|
|
177
|
+
Il middleware non supporta l'header `Range` delle richieste HTTP. Questo significa che:
|
|
178
|
+
|
|
179
|
+
- File audio e video non possono essere riprodotti in streaming dai browser
|
|
180
|
+
- Download di file grandi non possono essere ripresi dopo interruzione
|
|
181
|
+
- Strumenti come `curl --range` non funzionano
|
|
182
|
+
|
|
183
|
+
Il supporto HTTP 206 (Partial Content) è lo standard per la distribuzione di contenuti multimediali e file di grandi dimensioni.
|
|
184
|
+
|
|
185
|
+
### Soluzioni proposte
|
|
186
|
+
|
|
187
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
188
|
+
|
|
189
|
+
### Domande
|
|
190
|
+
|
|
191
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
192
|
+
|
|
193
|
+
### Implementazione
|
|
194
|
+
|
|
195
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
196
|
+
|
|
197
|
+
### Test
|
|
198
|
+
|
|
199
|
+
*Da verificare dopo l'implementazione.*
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Fase 6 — Gzip / Brotli compression
|
|
204
|
+
|
|
205
|
+
**Stato:** ⬜ Da fare
|
|
206
|
+
**Priorità:** Media
|
|
207
|
+
**File coinvolti:** `index.cjs`, `package.json`
|
|
208
|
+
|
|
209
|
+
### Descrizione del problema
|
|
210
|
+
|
|
211
|
+
Il middleware serve tutti i file senza compressione. Per file di testo (HTML, CSS, JS, JSON, SVG), la compressione può ridurre le dimensioni del 60-80%, con impatto diretto su:
|
|
212
|
+
|
|
213
|
+
- Velocità di caricamento per gli utenti
|
|
214
|
+
- Consumo di banda del server
|
|
215
|
+
- Score Lighthouse / Web Vitals
|
|
216
|
+
|
|
217
|
+
### Soluzioni proposte
|
|
218
|
+
|
|
219
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
220
|
+
|
|
221
|
+
### Domande
|
|
222
|
+
|
|
223
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
224
|
+
|
|
225
|
+
### Implementazione
|
|
226
|
+
|
|
227
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
228
|
+
|
|
229
|
+
### Test
|
|
230
|
+
|
|
231
|
+
*Da verificare dopo l'implementazione.*
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Fase 7 — Traduzione commenti in inglese nei test
|
|
236
|
+
|
|
237
|
+
**Stato:** ⬜ Da fare
|
|
238
|
+
**Priorità:** Bassa
|
|
239
|
+
**File coinvolti:** `__tests__/dt-unknown.test.js` e altri
|
|
240
|
+
|
|
241
|
+
### Descrizione del problema
|
|
242
|
+
|
|
243
|
+
Alcuni file di test contengono commenti in italiano, il che riduce l'accessibilità del codice per i contribuenti internazionali. Per un progetto open source pubblicato su npm, la lingua standard della codebase dovrebbe essere l'inglese.
|
|
244
|
+
|
|
245
|
+
### Soluzioni proposte
|
|
246
|
+
|
|
247
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
248
|
+
|
|
249
|
+
### Domande
|
|
250
|
+
|
|
251
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
252
|
+
|
|
253
|
+
### Implementazione
|
|
254
|
+
|
|
255
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
256
|
+
|
|
257
|
+
### Test
|
|
258
|
+
|
|
259
|
+
*Da verificare dopo l'implementazione.*
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Fase 8 — Roadmap rimozione opzioni deprecate (v3.0.0)
|
|
264
|
+
|
|
265
|
+
**Stato:** ⬜ Da fare
|
|
266
|
+
**Priorità:** Bassa
|
|
267
|
+
**File coinvolti:** `index.cjs`, `docs/CHANGELOG.md`, `README.md`
|
|
268
|
+
|
|
269
|
+
### Descrizione del problema
|
|
270
|
+
|
|
271
|
+
Le opzioni legacy `cacheMaxAge` ed `enableCaching` sono ancora supportate con deprecation warning ma non hanno una data di rimozione pianificata. Mantenerle a tempo indeterminato appesantisce il codice e può confondere i nuovi utenti che leggono la documentazione.
|
|
272
|
+
|
|
273
|
+
Una roadmap chiara verso `v3.0.0` con breaking changes definiti aiuta gli utenti a pianificare la migrazione.
|
|
274
|
+
|
|
275
|
+
### Soluzioni proposte
|
|
276
|
+
|
|
277
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
278
|
+
|
|
279
|
+
### Domande
|
|
280
|
+
|
|
281
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
282
|
+
|
|
283
|
+
### Implementazione
|
|
284
|
+
|
|
285
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
286
|
+
|
|
287
|
+
### Test
|
|
288
|
+
|
|
289
|
+
*Da verificare dopo l'implementazione.*
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
*Documento aggiornato: 2026-03-21*
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,295 @@ All notable changes to koa-classic-server will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.0.0] - 2026-05-13
|
|
9
|
+
|
|
10
|
+
### 🆕 New Features
|
|
11
|
+
|
|
12
|
+
#### `hidden` option — protect dot-files, dot-dirs and custom patterns (Fase 1 + Fase 2)
|
|
13
|
+
|
|
14
|
+
A new `hidden` option controls which files and directories are blocked from both directory listing and direct HTTP access (returning **404**). Applies recursively to the entire directory tree.
|
|
15
|
+
|
|
16
|
+
**Default behavior (secure out of the box):**
|
|
17
|
+
- Dot-files (names starting with `.`) → **hidden by default** (`dotFiles.default: 'hidden'`)
|
|
18
|
+
- Dot-directories → **visible by default** (`dotDirs.default: 'visible'`)
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
app.use(koaClassicServer('/public', {
|
|
22
|
+
hidden: {
|
|
23
|
+
dotFiles: {
|
|
24
|
+
default: 'hidden', // 'hidden' | 'visible'
|
|
25
|
+
whitelist: ['.well-known'], // Always visible (string exact/glob or RegExp)
|
|
26
|
+
blacklist: [], // Always hidden — overrides whitelist
|
|
27
|
+
},
|
|
28
|
+
dotDirs: {
|
|
29
|
+
default: 'visible',
|
|
30
|
+
whitelist: [],
|
|
31
|
+
blacklist: ['.git', /^\.svn/],
|
|
32
|
+
},
|
|
33
|
+
alwaysHide: ['*.secret', 'config/secrets/**', /\.key$/], // Path-aware patterns
|
|
34
|
+
}
|
|
35
|
+
}));
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Priority (highest to lowest):**
|
|
39
|
+
1. `blacklist` — always hidden, beats everything
|
|
40
|
+
2. `whitelist` — always visible, overrides `alwaysHide` and `default`
|
|
41
|
+
3. `alwaysHide` — path-aware patterns, beats `default`
|
|
42
|
+
4. `default` — fallback behavior for unmatched dot-entries
|
|
43
|
+
|
|
44
|
+
**`alwaysHide` pattern rules:**
|
|
45
|
+
- String without `/`: matches basename at any depth (e.g. `*.secret` hides `a/b/file.secret`)
|
|
46
|
+
- String with `/`: path-anchored from root (e.g. `config/secrets/**`)
|
|
47
|
+
- RegExp: tested against the full relative path
|
|
48
|
+
|
|
49
|
+
**Blocked dot-dirs block sub-paths too:**
|
|
50
|
+
`GET /.git/config` returns 404 if `.git` is in `dotDirs.blacklist`.
|
|
51
|
+
|
|
52
|
+
#### `template.renderTimeout` — bounded template execution (Security M-1)
|
|
53
|
+
|
|
54
|
+
The template `render` callback now runs under a configurable timeout (default **30 000 ms**, `0` = disabled). When a render exceeds the timeout the middleware responds **`504 Gateway Timeout`** with the usual security headers, instead of leaving the client connection blocked on a slow/hung render. Protects against DoS via connection exhaustion when a render performs unbounded I/O (DB queries, remote fetches, etc.).
|
|
55
|
+
|
|
56
|
+
The `render` function now receives an **`AbortSignal` as 5th argument**. The signal aborts on timeout *and* when the client disconnects (even when `renderTimeout: 0`). Cooperative renders that propagate the signal to `fetch` / DB clients / `fs.promises.*` also free backend resources on timeout.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
app.use(koaClassicServer('/public', {
|
|
60
|
+
template: {
|
|
61
|
+
ext: ['ejs'],
|
|
62
|
+
renderTimeout: 5000, // ms; 0 disables
|
|
63
|
+
render: async (ctx, next, filePath, rawBuffer, signal) => {
|
|
64
|
+
const data = await db.query('SELECT ...', { signal }); // honour signal
|
|
65
|
+
const ext = await fetch('https://api/...', { signal });
|
|
66
|
+
signal.throwIfAborted();
|
|
67
|
+
ctx.type = 'text/html';
|
|
68
|
+
ctx.body = ejs.render(rawBuffer.toString(), { data, ext });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Backward compatible:** existing 4-argument render functions keep working — the 5th argument is simply ignored.
|
|
75
|
+
|
|
76
|
+
#### `serverCache.*.maxAge` — time-based cache invalidation (Security M-2)
|
|
77
|
+
|
|
78
|
+
Both server-side caches (`serverCache.rawFile` and `serverCache.compressedFile`) accept a new `maxAge` option (ms, default `0` = disabled). When `> 0`, an entry is considered stale after `maxAge` ms regardless of `mtime + size`, forcing a fresh disk read on the next request.
|
|
79
|
+
|
|
80
|
+
Designed for **NFS / SMB / Docker bind mounts** where the OS attribute cache can keep `stat()` returning a stale `mtime` for several seconds after a remote modification — making the mtime+size invariant insufficient to detect changes. `maxAge` bounds the worst-case staleness window to a known value.
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
app.use(koaClassicServer('/public', {
|
|
84
|
+
serverCache: {
|
|
85
|
+
rawFile: { enabled: true, maxAge: 30000 }, // refresh every 30 s
|
|
86
|
+
compressedFile: { enabled: true, maxAge: 30000 }
|
|
87
|
+
}
|
|
88
|
+
}));
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> **Limitation:** `maxAge` limits but does not eliminate NFS staleness. For strict freshness combine with a low `actimeo=` on the mount.
|
|
92
|
+
|
|
93
|
+
Internally a new `LFUCache.refresh(key, fields)` method updates the entry in place while preserving its LFU frequency, so popular files refreshed by `maxAge` don't fall to the bottom of the eviction bucket.
|
|
94
|
+
|
|
95
|
+
#### `logger` option — pluggable structured logging (Security N-1)
|
|
96
|
+
|
|
97
|
+
All internal `console.error` / `console.warn` calls now route through an injectable logger. Pass any object that exposes `error(...)` and `warn(...)` methods — `console` (default), `pino`, `winston`, `bunyan`, or a custom adapter — to integrate with aggregated logging pipelines in production.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
const pino = require('pino')();
|
|
101
|
+
|
|
102
|
+
app.use(koaClassicServer('/public', {
|
|
103
|
+
logger: pino
|
|
104
|
+
}));
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Contract:**
|
|
108
|
+
- Must be an object with `typeof logger.error === 'function'` and `typeof logger.warn === 'function'`
|
|
109
|
+
- Invalid loggers (missing methods, non-objects, `null`, `false`, arrays) throw at factory time
|
|
110
|
+
- Extra methods (`info`, `debug`, `fatal`, ...) are ignored — pass any superset freely
|
|
111
|
+
|
|
112
|
+
**ANSI escape codes** in warning messages are only emitted when the logger is the global `console` (detected by reference). Structured loggers receive the plain text, keeping log aggregators clean.
|
|
113
|
+
|
|
114
|
+
**Backward compatible:** when `logger` is omitted, the default is `console` — existing code and tests that spy on `console.error` / `console.warn` continue to work unchanged.
|
|
115
|
+
|
|
116
|
+
#### `dirListing` namespace — bounded and paginated directory listings (Security N-2)
|
|
117
|
+
|
|
118
|
+
A new namespaced option groups all directory-listing config together, replacing the v2-era flat `showDirContents` knob with a structured object. Hardens the listing against indirect DoS via very large directories and improves usability on big folders.
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
app.use(koaClassicServer('/public', {
|
|
122
|
+
dirListing: {
|
|
123
|
+
enabled: true, // render listing HTML when no index file matches (default: true)
|
|
124
|
+
maxEntries: 10000, // hard cap on visible / sorted / stat'd entries (default; 0 = disabled)
|
|
125
|
+
entriesPerPage: 100, // entries per page in the listing UI (default; 0 = disabled)
|
|
126
|
+
}
|
|
127
|
+
}));
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**dirListing.enabled**
|
|
131
|
+
- Replaces the v2 top-level `showDirContents`. Accepts `true` / `false`. When `false`, requests for a directory without a matching index file return 404 instead of an HTML listing.
|
|
132
|
+
|
|
133
|
+
**dirListing.maxEntries**
|
|
134
|
+
- Caps how many entries are sorted, stat'd, and rendered per directory listing. Excess entries trigger a yellow banner at the top of the page and an `X-Dir-Truncated: <N>` response header so monitoring can distinguish capped listings.
|
|
135
|
+
- Implementation: the middleware calls `fs.promises.readdir()` once and then slices the result. This bounds rendering and CPU cost but **not** the size of the initial `readdir()` allocation. For typical static-file servers (where the directory contents are controlled by the operator) this is the right trade-off — it recovers v2-class listing performance.
|
|
136
|
+
- Default `10000` is permissive enough for normal use while bounding rendering cost on accidentally-large folders.
|
|
137
|
+
- **Caveat for adversarial workloads:** if you serve a directory writable by untrusted parties, an attacker creating millions of files could still force a large `readdir()` allocation. Tracked for v3.1 as opt-in streaming reads — see `docs/security_improvement_for_V3.md` → *Future Work* → *[F-1]*.
|
|
138
|
+
|
|
139
|
+
**dirListing.entriesPerPage**
|
|
140
|
+
- Pagination kicks in only when the visible entries exceed `entriesPerPage`; small directories render in a single page exactly like before.
|
|
141
|
+
- The current page is selected by `?page=N` (0-based). Invalid or out-of-range values clamp silently to the nearest valid page.
|
|
142
|
+
- A numbered paginator (`« First | ‹ Prev | 0 1 … N-1 | Next › | Last »`) is rendered below the table, preserving any active `sort`/`order`. An `X-Dir-Pagination: <current>/<last>` header is also emitted.
|
|
143
|
+
|
|
144
|
+
**Migration from v2**
|
|
145
|
+
|
|
146
|
+
`showDirContents` (a v2-stable option) keeps working as a **backward-compatibility alias** for `dirListing.enabled`. v2 code that passes it continues to function unchanged. A one-time deprecation warning is emitted via the configured `logger.warn(...)` to encourage migration:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
[koa-classic-server] DEPRECATION: options.showDirContents was renamed to dirListing.enabled in v3.0.0.
|
|
150
|
+
The old name is currently accepted as an alias and may be removed in a future major version.
|
|
151
|
+
Replace with: dirListing: { enabled: true }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Passing both `showDirContents` and `dirListing.enabled` at the same time throws — pick one.
|
|
155
|
+
|
|
156
|
+
**Migration from v3.0.0-alpha.0**
|
|
157
|
+
|
|
158
|
+
The two V3-alpha-only legacy names throw helpful errors at startup (no v2 user can have these in production):
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
options.maxDirEntries was relocated in v3.0.0.
|
|
162
|
+
Replace with: dirListing: { maxEntries: 10000 }
|
|
163
|
+
|
|
164
|
+
options.pageSize was relocated and renamed in v3.0.0.
|
|
165
|
+
Replace with: dirListing: { entriesPerPage: 100 }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**CSP impact:** the listing CSS now includes rules for `.kcs-banner` and `.kcs-pagination`. The page's CSP hash is auto-recomputed at module load (no manual config change needed).
|
|
169
|
+
|
|
170
|
+
### 📝 Documentation
|
|
171
|
+
|
|
172
|
+
#### DNS Rebinding deployment guidance (Security M-3)
|
|
173
|
+
|
|
174
|
+
The `Host` header is intentionally not validated by the middleware — host validation belongs to the reverse proxy or to a dedicated application-level guard. The new *Best Practices → Sicurezza → DNS Rebinding* section in `docs/DOCUMENTATION.md` explains:
|
|
175
|
+
|
|
176
|
+
- When the risk applies (LAN/loopback exposure without a fronting proxy).
|
|
177
|
+
- When it doesn't (reverse proxy with `server_name` allowlist, public IP behind CDN/WAF).
|
|
178
|
+
- A drop-in nginx allowlist snippet.
|
|
179
|
+
- A Koa middleware that checks `ctx.host` against an allowlist and returns `421 Misdirected Request`, plus a note on `app.proxy = true` + `X-Forwarded-Host` when terminating TLS upstream.
|
|
180
|
+
|
|
181
|
+
No code change in `index.cjs` — documentation only.
|
|
182
|
+
|
|
183
|
+
#### Security headers scope and limits (Security M-4)
|
|
184
|
+
|
|
185
|
+
Clarify that the security headers emitted by the middleware (`Content-Security-Policy`, `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`) are applied **only** to middleware-generated responses (directory listing + error pages). User-served static files are returned without these headers — by design, because the right policy is application-specific.
|
|
186
|
+
|
|
187
|
+
The new *Best Practices → Sicurezza → Limiti dei Security Headers sui file statici* section in `docs/DOCUMENTATION.md` covers:
|
|
188
|
+
|
|
189
|
+
- A table listing which headers are emitted automatically and on which responses.
|
|
190
|
+
- An upstream Koa middleware example that adds `X-Content-Type-Options`, `Referrer-Policy`, `Strict-Transport-Security` to every response and a strict CSP only to HTML responses.
|
|
191
|
+
- Notes on rolling out CSP via `Content-Security-Policy-Report-Only`, and on `COOP`/`COEP` for projects using `SharedArrayBuffer`.
|
|
192
|
+
|
|
193
|
+
No code change in `index.cjs` — documentation only.
|
|
194
|
+
|
|
195
|
+
### 🎯 Design Philosophy
|
|
196
|
+
|
|
197
|
+
v3.0.0 codifies the project's design intent in a new top-level `CLAUDE.md`: **koa-classic-server is an HTTP file server first**. The contract with the operator is: *"if a file is in `rootDir`, `GET` on its path returns it"*. Defaults serve files without applying surprise restrictions — the operator's directory is the source of truth.
|
|
198
|
+
|
|
199
|
+
This drove a revision of two v3-alpha defaults late in the cycle (see *Breaking Changes* below) and shapes how new features will be designed going forward. Operators harden via explicit configuration; the README and `docs/DOCUMENTATION.md` now ship a **Security Checklist** and a **Suggested Production Security Configuration** to help with that.
|
|
200
|
+
|
|
201
|
+
### ⚠️ Breaking Changes
|
|
202
|
+
|
|
203
|
+
#### Dot-files visible by default (philosophy alignment)
|
|
204
|
+
|
|
205
|
+
Earlier in the v3.0.0 alpha cycle, `hidden.dotFiles.default` was flipped to `'hidden'` as a security-by-default choice. This created surprise behavior — `GET /.env` returning 404 even when the file exists — which violates the "file server first" design philosophy.
|
|
206
|
+
|
|
207
|
+
**Final v3.0.0 behavior:** `hidden.dotFiles.default` is `'visible'`, restoring v2 behavior. The implicit-default warning that fired in alpha when the option was omitted is also removed.
|
|
208
|
+
|
|
209
|
+
| Default | v2 | v3.0.0-alpha early | **v3.0.0 final** |
|
|
210
|
+
|---|---|---|---|
|
|
211
|
+
| `hidden.dotFiles.default` | `'visible'` | `'hidden'` | **`'visible'`** |
|
|
212
|
+
| Implicit-default runtime warning | — | emitted | **removed** |
|
|
213
|
+
|
|
214
|
+
**Operators upgrading from v2:** no change in behavior — your existing dot-files keep being served. **Migration to harden** (recommended for production): set `hidden.dotFiles.default: 'hidden'` explicitly and whitelist `.well-known` for ACME. See the *Security Checklist* in `README.md`.
|
|
215
|
+
|
|
216
|
+
#### `dirListing.maxEntries` default raised from 10,000 → 100,000
|
|
217
|
+
|
|
218
|
+
The earlier v3-alpha default of `10,000` was tight enough that operators with normal-sized media catalogs, releases archives, or asset directories would hit truncation silently (the listing banner would appear). This violated the "no surprise restrictions" rule — the cap was acting as a policy restriction rather than a safety net.
|
|
219
|
+
|
|
220
|
+
**Final v3.0.0 behavior:** `dirListing.maxEntries` defaults to `100,000` — high enough that 99% of legitimate deployments never hit it, low enough to bound rendering cost on accidentally-huge directories (log rotation broken, mistakenly mounted FS).
|
|
221
|
+
|
|
222
|
+
| Default | v3.0.0-alpha early | **v3.0.0 final** |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| `dirListing.maxEntries` | `10000` | **`100000`** |
|
|
225
|
+
| `dirListing.entriesPerPage` | `100` | `100` (unchanged) |
|
|
226
|
+
|
|
227
|
+
**Caveat:** even with `maxEntries: 100000`, the initial `fs.promises.readdir()` allocation is not bounded. For adversarial-directory workloads (multi-tenant uploads, untrusted writes), this gap will be closed by the v3.1 `dirListing.readMode: 'bounded'` option — tracked under `[F-1]` in `docs/security_improvement_for_V3.md`.
|
|
228
|
+
|
|
229
|
+
#### Dot-files hardening is now opt-in (was implicit "secure by default")
|
|
230
|
+
|
|
231
|
+
Operators who *want* the v3-alpha behavior (dot-files hidden by default, including `.env`, `.git/config`, etc.) must now opt in explicitly:
|
|
232
|
+
|
|
233
|
+
```javascript
|
|
234
|
+
app.use(koaClassicServer('/public', {
|
|
235
|
+
hidden: {
|
|
236
|
+
dotFiles: { default: 'hidden', whitelist: ['.well-known'] },
|
|
237
|
+
dotDirs: { default: 'hidden', whitelist: ['.well-known'] },
|
|
238
|
+
},
|
|
239
|
+
}));
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
This snippet now appears in the *Suggested Production Security Configuration* in both `README.md` and `docs/DOCUMENTATION.md`.
|
|
243
|
+
|
|
244
|
+
#### Removed string format for `index` option
|
|
245
|
+
- **Removed**: `index: 'index.html'` — passing a non-empty string now throws an `Error` at startup
|
|
246
|
+
- **Empty string** `index: ''` is still silently treated as `[]` (no index file, show directory listing)
|
|
247
|
+
- **Migration**:
|
|
248
|
+
```js
|
|
249
|
+
// Before (v2.x — now throws)
|
|
250
|
+
app.use(koaClassicServer('/public', { index: 'index.html' }));
|
|
251
|
+
|
|
252
|
+
// After (v3.0.0)
|
|
253
|
+
app.use(koaClassicServer('/public', { index: ['index.html'] }));
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### Removed deprecated option names `cacheMaxAge` and `enableCaching`
|
|
257
|
+
- **Removed**: `cacheMaxAge` — use `browserCacheMaxAge` instead
|
|
258
|
+
- **Removed**: `enableCaching` — use `browserCacheEnabled` instead
|
|
259
|
+
- **Behaviour**: Passing either removed option now throws an `Error` at startup with a clear migration message pointing to the new name and the current value.
|
|
260
|
+
- **Migration**:
|
|
261
|
+
```js
|
|
262
|
+
// Before (v2.x — now throws)
|
|
263
|
+
app.use(koaClassicServer('/public', {
|
|
264
|
+
enableCaching: true,
|
|
265
|
+
cacheMaxAge: 3600
|
|
266
|
+
}));
|
|
267
|
+
|
|
268
|
+
// After (v3.0.0)
|
|
269
|
+
app.use(koaClassicServer('/public', {
|
|
270
|
+
browserCacheEnabled: true,
|
|
271
|
+
browserCacheMaxAge: 3600
|
|
272
|
+
}));
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### Renamed `compression.minSize` → `compression.minFileSize`
|
|
276
|
+
|
|
277
|
+
The threshold below which files are served uncompressed has a clearer name. Brings naming into line with `serverCache.rawFile.maxFileSize`, where "file size" is the explicit unit. Affects only alpha-tester code (the `compression` namespace was introduced in v3 and is not present in v2).
|
|
278
|
+
|
|
279
|
+
- **Removed**: `compression.minSize` — passing it now throws an `Error` at startup
|
|
280
|
+
- **Migration**:
|
|
281
|
+
```js
|
|
282
|
+
// Before (v3.0.0-alpha.0 — now throws)
|
|
283
|
+
app.use(koaClassicServer('/public', {
|
|
284
|
+
compression: { minSize: 2048 }
|
|
285
|
+
}));
|
|
286
|
+
|
|
287
|
+
// After (v3.0.0)
|
|
288
|
+
app.use(koaClassicServer('/public', {
|
|
289
|
+
compression: { minFileSize: 2048 }
|
|
290
|
+
}));
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The `false` shorthand (disable the threshold entirely) is preserved on the new name: `compression: { minFileSize: false }`.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
8
297
|
## [2.6.1] - 2026-03-04
|
|
9
298
|
|
|
10
299
|
### 🐛 Bug Fix
|
package/docs/CODE_REVIEW.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Code Review - koa-classic-server
|
|
2
2
|
|
|
3
|
+
> **Nota storica:** questo documento è uno snapshot di code review precedente al refactor V3. Riferimenti a `showDirContents` corrispondono a `dirListing.enabled` nell'API V3 corrente. Vedi [README.md → Migration Guide](../README.md#from-v2x-to-v3x).
|
|
4
|
+
|
|
3
5
|
## Analisi Generale del Codice
|
|
4
6
|
|
|
5
7
|
Data: 2025-11-18
|