koa-classic-server 3.0.0-alpha.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +101 -0
- package/README.md +550 -635
- package/__tests__/benchmark-results-v3.0.0.txt +372 -0
- package/__tests__/benchmark.js +1 -1
- package/__tests__/compression.test.js +17 -3
- package/__tests__/customTest/serversToLoad.util.js +4 -4
- package/__tests__/demo-regex-index.js +4 -4
- package/__tests__/directory-sorting-links.test.js +1 -1
- package/__tests__/dt-unknown.test.js +19 -19
- package/__tests__/ejs.test.js +1 -1
- package/__tests__/hidden-option.test.js +48 -63
- package/__tests__/hideExtension.test.js +70 -13
- package/__tests__/index.test.js +6 -6
- package/__tests__/listing.test.js +437 -0
- package/__tests__/logger.test.js +232 -0
- package/__tests__/range.test.js +2 -2
- package/__tests__/security-headers.test.js +20 -8
- package/__tests__/security.test.js +5 -5
- package/__tests__/server-cache.test.js +178 -7
- package/__tests__/symlink.test.js +10 -10
- package/__tests__/template-timeout.test.js +321 -0
- package/docs/CHANGELOG.md +209 -4
- package/docs/CODE_REVIEW.md +2 -0
- package/docs/DOCUMENTATION.md +259 -32
- package/docs/EXAMPLES_INDEX_OPTION.md +1 -1
- package/docs/FLOW_DIAGRAM.md +2 -0
- package/docs/INDEX_OPTION_PRIORITY.md +2 -2
- package/docs/OPTIMIZATION_HTTP_CACHING.md +2 -0
- 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/index.cjs +551 -178
- package/package.json +6 -1
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# Security Improvement for V3
|
|
2
|
+
|
|
3
|
+
Analisi di sicurezza del progetto `koa-classic-server` v3.0.0-alpha.0, con roadmap degli interventi da effettuare.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Indice
|
|
8
|
+
|
|
9
|
+
### Punti di Forza (da mantenere e documentare)
|
|
10
|
+
- [ ] [PS-1] Path Traversal — protezione multi-layer
|
|
11
|
+
- [ ] [PS-2] Hidden Files/Directories — controllo esplicito
|
|
12
|
+
- [ ] [PS-3] XSS Prevention — escaping nel directory listing
|
|
13
|
+
- [ ] [PS-4] Security Headers — CSP, X-Frame-Options, Referrer-Policy
|
|
14
|
+
- [ ] [PS-5] Dipendenze — superficie d'attacco minima
|
|
15
|
+
|
|
16
|
+
### Miglioramenti Prioritari
|
|
17
|
+
- [x] [M-1] Timeout configurabile sul template rendering *(Medio)*
|
|
18
|
+
- [x] [M-2] Cache staleness su filesystem NFS/distribuiti *(Medio)*
|
|
19
|
+
- [x] [M-3] Documentare il rischio DNS Rebinding *(Basso)*
|
|
20
|
+
- [x] [M-4] Documentare i limiti dei security headers sui file statici *(Basso)*
|
|
21
|
+
|
|
22
|
+
### Nice-to-Have
|
|
23
|
+
- [x] [N-1] Logger iniettabile dall'esterno
|
|
24
|
+
- [x] [N-2] Protezione contro directory listing con molti file (DoS)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Punti di Forza
|
|
29
|
+
|
|
30
|
+
### [PS-1] Path Traversal
|
|
31
|
+
|
|
32
|
+
La protezione è implementata a più livelli in `index.cjs` (righe 884–910):
|
|
33
|
+
|
|
34
|
+
1. **Null byte guard** — rifiuta con `400 Bad Request` qualsiasi path contenente `\0`
|
|
35
|
+
2. **Normalizzazione** — `path.normalize()` applicata prima di qualsiasi `path.join()`
|
|
36
|
+
3. **Boundary check** — il path risolto deve iniziare con `rootDir`; altrimenti risponde `403 Forbidden`
|
|
37
|
+
4. **URL-encoded variants** — gestite automaticamente dal layer di parsing di Koa (es. `%2e%2e%2f`)
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
if (requestedPath.includes('\0')) {
|
|
41
|
+
ctx.status = 400;
|
|
42
|
+
ctx.body = 'Bad Request';
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const normalizedPath = path.normalize(requestedPath);
|
|
46
|
+
const fullPath = path.join(normalizedRootDir, normalizedPath);
|
|
47
|
+
if (!fullPath.startsWith(normalizedRootDir)) {
|
|
48
|
+
ctx.status = 403;
|
|
49
|
+
ctx.body = 'Forbidden';
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Test di copertura: `__tests__/security.test.js` verifica `/../package.json`, `/%2e%2e%2f`, `/../../../etc/hosts`.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### [PS-2] Hidden Files/Directories
|
|
59
|
+
|
|
60
|
+
Introdotta in v3.0.0, l'opzione `hidden` permette un controllo granulare sulla visibilità di file e directory (righe ~537–550, ~760–795 in `index.cjs`):
|
|
61
|
+
|
|
62
|
+
- **Default `'visible'`** per dot-files e dot-directory (allineato alla *design philosophy* — vedi `CLAUDE.md`)
|
|
63
|
+
- **Blacklist assoluta** per pattern come `.git`, `.svn` (prevale su whitelist e default)
|
|
64
|
+
- **Whitelist** per `.well-known` (sempre visibile; utile per ACME / Let's Encrypt)
|
|
65
|
+
- **Pattern `alwaysHide`** — supporta glob e `RegExp` per match path-aware
|
|
66
|
+
|
|
67
|
+
Priority logic: `blacklist > whitelist > alwaysHide > default`.
|
|
68
|
+
|
|
69
|
+
> **Cambio rispetto al ciclo v3-alpha:** una prima implementazione di PS-2 aveva impostato `dotFiles.default: 'hidden'` come hardening-by-default + un warning runtime se omesso. Quella scelta è stata revertita alla finale v3.0.0 perché violava il principio "HTTP file server first" (`GET /.env` ritornava 404 anche se il file esisteva — sorpresa per l'operatore). PS-2 ora fornisce *meccanismi* di hardening; la *policy* (cosa nascondere) è scelta esplicita dell'operatore, documentata nella **Security Checklist** di `README.md` e `DOCUMENTATION.md`.
|
|
70
|
+
|
|
71
|
+
Test di copertura: `__tests__/hidden-option.test.js` — copre sia il default `'visible'` (system behavior) sia il path opt-in `default: 'hidden'`.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### [PS-3] XSS Prevention
|
|
76
|
+
|
|
77
|
+
Tutti i nomi file nel directory listing passano per `escapeHtml()` (righe 119–125):
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const _HTML_ESCAPE_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
81
|
+
function escapeHtml(unsafe) {
|
|
82
|
+
if (typeof unsafe !== 'string') return unsafe;
|
|
83
|
+
return unsafe.replace(_HTML_ESCAPE_RE, c => _HTML_ESCAPE_MAP[c]);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
L'header `Content-Disposition` usa encoding RFC 5987 (percent-encoding UTF-8) con fallback ASCII quoted-string.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### [PS-4] Security Headers
|
|
92
|
+
|
|
93
|
+
Applicati alle pagine generate dal middleware (directory listing, pagine di errore):
|
|
94
|
+
|
|
95
|
+
| Header | Valore |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `Content-Security-Policy` | `default-src 'none'; style-src '<hash>'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'` |
|
|
98
|
+
| `X-Content-Type-Options` | `nosniff` |
|
|
99
|
+
| `X-Frame-Options` | `DENY` |
|
|
100
|
+
| `Referrer-Policy` | `no-referrer` |
|
|
101
|
+
| `Permissions-Policy` | `camera=(), microphone=(), geolocation=(), payment=()` |
|
|
102
|
+
|
|
103
|
+
Il CSP usa un hash SHA-256 del CSS inline invece di `'unsafe-inline'`.
|
|
104
|
+
|
|
105
|
+
Test di copertura: `__tests__/security-headers.test.js`.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### [PS-5] Dipendenze — Superficie d'Attacco Minima
|
|
110
|
+
|
|
111
|
+
| Pacchetto | Tipo | Note |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `mime-types ^3.0.2` | Runtime | Unica dipendenza runtime |
|
|
114
|
+
| `koa ^2.16.4 \|\| >=3.1.2` | Peer | Sicurezza dipende dalla versione scelta dall'utente |
|
|
115
|
+
| `jest`, `supertest`, `eslint`, `ejs`, `autocannon`, `inquirer` | Dev only | Non impattano il bundle di produzione |
|
|
116
|
+
|
|
117
|
+
Superficie d'attacco della supply chain: **molto ridotta**.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Miglioramenti Prioritari
|
|
122
|
+
|
|
123
|
+
### [M-1] Timeout configurabile sul template rendering *(Priorità: Media)*
|
|
124
|
+
|
|
125
|
+
**Problema**
|
|
126
|
+
|
|
127
|
+
Il callback `template.render` viene eseguito senza alcun timeout. Se la funzione esegue operazioni async lente (query DB, fetch remoti), la connessione rimane aperta indefinitamente, esponendo il server a un potenziale DoS per esaurimento di connessioni.
|
|
128
|
+
|
|
129
|
+
**Soluzione proposta**
|
|
130
|
+
|
|
131
|
+
Aggiungere un'opzione `template.renderTimeout` (default: `5000` ms) e wrappare la chiamata:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
const TIMEOUT_MS = options.template?.renderTimeout ?? 5000;
|
|
135
|
+
|
|
136
|
+
const renderWithTimeout = Promise.race([
|
|
137
|
+
options.template.render(ctx, next, filePath, rawBuffer),
|
|
138
|
+
new Promise((_, reject) =>
|
|
139
|
+
setTimeout(() => reject(new Error('Template render timeout')), TIMEOUT_MS)
|
|
140
|
+
)
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await renderWithTimeout;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
ctx.status = 500;
|
|
147
|
+
ctx.body = 'Internal Server Error';
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Impatto:** basso (aggiunta opzionale, backward-compatible).
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### [M-2] Cache staleness su filesystem NFS/distribuiti *(Priorità: Media)*
|
|
156
|
+
|
|
157
|
+
**Problema**
|
|
158
|
+
|
|
159
|
+
La validazione della cache server-side si basa esclusivamente su `mtime + size` (righe 1055–1058). Su filesystem NFS o distribuiti (es. container con volumi montati), `mtime` può non aggiornarsi immediatamente dopo una modifica, portando il middleware a servire contenuti obsoleti.
|
|
160
|
+
|
|
161
|
+
**Soluzione proposta**
|
|
162
|
+
|
|
163
|
+
1. Aggiungere un'opzione `serverCache.maxAge` (in ms) per l'invalidazione time-based come secondo layer di controllo.
|
|
164
|
+
2. Documentare chiaramente la limitazione nella documentazione e nei JSDoc.
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
serverCache: {
|
|
168
|
+
rawFile: {
|
|
169
|
+
enabled: false,
|
|
170
|
+
maxSize: 52428800,
|
|
171
|
+
maxFileSize: 1048576,
|
|
172
|
+
maxAge: 0 // 0 = disabilitato, altrimenti ms
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Impatto:** medio (richiede modifica alla logica LFU cache).
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### [M-3] Documentare il rischio DNS Rebinding *(Priorità: Bassa)*
|
|
182
|
+
|
|
183
|
+
**Problema**
|
|
184
|
+
|
|
185
|
+
Il middleware non valida l'header `Host`. In scenari intranet/localhost senza reverse proxy davanti, un attaccante può eseguire attacchi di DNS rebinding per accedere al server come se fosse un'origine fidata.
|
|
186
|
+
|
|
187
|
+
**Soluzione proposta**
|
|
188
|
+
|
|
189
|
+
Non è necessario implementarlo nel middleware (responsabilità del reverse proxy o di Koa stesso), ma va documentato come prerequisito di deployment:
|
|
190
|
+
|
|
191
|
+
> **Nota di sicurezza:** questo middleware non valida l'header `Host`. In produzione, è necessario configurare un reverse proxy (nginx, Caddy) che accetti solo gli hostname attesi, oppure usare [`koa-host-header-safe`](https://github.com/search?q=koa+host+header) o simili.
|
|
192
|
+
|
|
193
|
+
**Stato V3:** documentato in `docs/DOCUMENTATION.md` → *Best Practices → Sicurezza → DNS Rebinding*, con esempio nginx e middleware Koa di allowlist su `ctx.host`.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### [M-4] Documentare i limiti dei security headers sui file statici *(Priorità: Bassa)*
|
|
198
|
+
|
|
199
|
+
**Problema**
|
|
200
|
+
|
|
201
|
+
I security headers (CSP, X-Frame-Options, ecc.) vengono aggiunti **solo** alle pagine generate dal middleware (directory listing, errori 404/500). I file statici serviti direttamente (HTML, JS, CSS dell'utente) non ricevono alcun header di sicurezza aggiuntivo.
|
|
202
|
+
|
|
203
|
+
Questo comportamento è by-design ma può generare false aspettative negli utenti che si aspettano una protezione automatica su tutti i file.
|
|
204
|
+
|
|
205
|
+
**Soluzione proposta**
|
|
206
|
+
|
|
207
|
+
Aggiungere nella documentazione una sezione dedicata che spieghi:
|
|
208
|
+
- Quali pagine ricevono i security headers
|
|
209
|
+
- Come aggiungere headers custom sui file statici tramite Koa middleware separato
|
|
210
|
+
- Esempio con `ctx.set()` a monte di `koa-classic-server`
|
|
211
|
+
|
|
212
|
+
**Stato V3:** documentato in `docs/DOCUMENTATION.md` → *Best Practices → Sicurezza → Limiti dei Security Headers sui file statici*, con tabella degli header impostati automaticamente, esempio di middleware Koa upstream con CSP/HSTS/Referrer-Policy, e note operative su CSP report-only e COOP/COEP.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Nice-to-Have
|
|
217
|
+
|
|
218
|
+
### [N-1] Logger iniettabile dall'esterno
|
|
219
|
+
|
|
220
|
+
**Problema**
|
|
221
|
+
|
|
222
|
+
Gli errori interni (stream error, file access error) vengono scritti direttamente su `console.error`. In produzione, con sistemi di log aggregati, questo può esporre stack trace o percorsi file in output non controllati.
|
|
223
|
+
|
|
224
|
+
**Soluzione proposta**
|
|
225
|
+
|
|
226
|
+
Aggiungere un'opzione `logger` che accetti un oggetto compatibile con l'interfaccia standard (`{ error, warn, info }`):
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
koaClassicServer(rootDir, {
|
|
230
|
+
logger: pinoInstance // o winston, console, ecc.
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Default: `console` (backward-compatible).
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### [N-2] Protezione contro directory listing con molti file
|
|
239
|
+
|
|
240
|
+
**Problema**
|
|
241
|
+
|
|
242
|
+
Il directory listing processa le entry in batch da 64 elementi con `Promise.all()`. Con directory contenenti decine di migliaia di file, la generazione della risposta può occupare molta memoria e CPU, contribuendo a un DoS indiretto.
|
|
243
|
+
|
|
244
|
+
**Soluzione proposta**
|
|
245
|
+
|
|
246
|
+
1. Aggiungere un'opzione `dirListing.maxEntries` (default: es. `10000`) che tronca il listing e mostra un avviso.
|
|
247
|
+
2. Aggiungere paginazione opzionale al directory listing.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Riepilogo
|
|
252
|
+
|
|
253
|
+
| ID | Descrizione | Priorità | Stato |
|
|
254
|
+
|---|---|---|---|
|
|
255
|
+
| PS-1 | Path Traversal multi-layer | — | Implementato |
|
|
256
|
+
| PS-2 | Hidden Files/Directories | — | Implementato |
|
|
257
|
+
| PS-3 | XSS Prevention nel listing | — | Implementato |
|
|
258
|
+
| PS-4 | Security Headers CSP/HSTS | — | Implementato |
|
|
259
|
+
| PS-5 | Dipendenze minimali | — | Implementato |
|
|
260
|
+
| M-1 | Timeout template rendering | Media | Implementato |
|
|
261
|
+
| M-2 | Cache staleness NFS | Media | Implementato |
|
|
262
|
+
| M-3 | Documentare DNS Rebinding | Bassa | Documentato |
|
|
263
|
+
| M-4 | Documentare limiti security headers | Bassa | Documentato |
|
|
264
|
+
| N-1 | Logger iniettabile | Nice-to-have | Implementato |
|
|
265
|
+
| N-2 | Protezione DoS directory listing | Nice-to-have | Implementato (con caveat — vedi sotto) |
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Future Work — v3.1
|
|
270
|
+
|
|
271
|
+
### [F-1] Opt-in streaming read per directory adversarial *(rimandato a v3.1)*
|
|
272
|
+
|
|
273
|
+
**Contesto**
|
|
274
|
+
|
|
275
|
+
La prima implementazione di N-2 (v3.0.0-alpha.0) usava `fs.promises.opendir()` con async iterator per limitare la lettura a `dirListing.maxEntries` entry e bounded la RAM indipendentemente dalla dimensione su disco. I benchmark hanno mostrato una regressione di latenza di 3-4× sui listing rispetto a v2 (es. dir 10k file: 90 ms → 405 ms), dovuta all'overhead di una `Promise` per ogni entry nell'async iterator.
|
|
276
|
+
|
|
277
|
+
Prima del rilascio è stato applicato un fix (vedi commit di v3.0.0-alpha.0): la lettura è tornata a usare `fs.promises.readdir({ withFileTypes: true })` seguita da `slice(0, dirListing.maxEntries)`. Recupera le performance v2, ma **rinuncia alla garanzia "RAM bounded regardless of disk size"**: una directory con milioni di file alloca milioni di Dirent prima dello slicing.
|
|
278
|
+
|
|
279
|
+
**Decisione v3.0**
|
|
280
|
+
|
|
281
|
+
Per il caso d'uso primario (servire asset statici controllati dall'operatore) la nuova implementazione è ottimale. Il caso edge — directory scrivibile da utenti non fidati, attaccante che crea milioni di file — non è la modalità d'uso dichiarata del middleware e va affrontata a livello applicativo / OS.
|
|
282
|
+
|
|
283
|
+
**Proposta per v3.1**
|
|
284
|
+
|
|
285
|
+
Aggiungere un'opzione `dirListing.readMode` (`'fast' | 'bounded'`, default `'fast'`):
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
app.use(koaClassicServer(rootDir, {
|
|
289
|
+
dirListing: {
|
|
290
|
+
maxEntries: 1000,
|
|
291
|
+
readMode: 'bounded', // opendir() streaming, RAM bounded a O(maxEntries)
|
|
292
|
+
}
|
|
293
|
+
}));
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
- `'fast'` (default) — comportamento v3.0: readdir + slice, performance v2-class
|
|
297
|
+
- `'bounded'` — opendir async iterator: lettura interrotta a `dirListing.maxEntries`, RAM bounded indipendentemente dalla dimensione su disco, latenza più alta sui listing
|
|
298
|
+
|
|
299
|
+
Trade-off documentato chiaramente nella user-facing doc. Da valutare prima del freeze 3.1:
|
|
300
|
+
- Test simmetrici nelle due modalità
|
|
301
|
+
- Validazione factory (`dirListing.readMode` ∈ `{'fast', 'bounded'}`)
|
|
302
|
+
- Aggiornamento README + DOCUMENTATION.md con esempio di scelta
|
|
303
|
+
|
|
304
|
+
Il caso d'uso target di `'bounded'`: hosting multi-tenant con directory scrivibili da utenti (es. `/uploads`), backup server, log shipper con cartelle spool.
|
|
305
|
+
|
|
306
|
+
**Rinviato perché**
|
|
307
|
+
|
|
308
|
+
- Non è regressione rispetto a v2 (v2 aveva esattamente questo profilo memoria)
|
|
309
|
+
- Caso d'uso adversarial-directory minoritario nel target del middleware
|
|
310
|
+
- Aggiungere un'opzione API non banale richiede design review e test approfonditi, meglio non aggiungerla in fretta nel rush release di 3.0
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
### [F-1bis] Brainstorm — hybrid automatico fast/bounded *(da decidere in v3.1)*
|
|
315
|
+
|
|
316
|
+
In sede di discussione dei default v3.0 è emersa la richiesta di valutare un **terzo modo**: una scelta automatica fra `fast` e `bounded` basata sulla dimensione effettiva della directory, in modo che 99% degli operatori non debbano configurare nulla. Documento qui i risultati del brainstorm così la decisione finale per v3.1 ha già contesto.
|
|
317
|
+
|
|
318
|
+
#### Vincolo tecnico: chicken-and-egg
|
|
319
|
+
|
|
320
|
+
Per scegliere il path PRIMA di leggere serve un modo *economico* di stimare il numero di entry. `readdir()` (fast) lo scopri solo *dopo* aver pagato l'allocazione completa — quindi il danno è già fatto. `opendir()` (bounded) lo scopri streamando — ma se stai streamando hai già pagato l'overhead per-entry, non c'è più nulla da salvare.
|
|
321
|
+
|
|
322
|
+
L'hybrid puramente automatico richiede una di queste premesse:
|
|
323
|
+
|
|
324
|
+
1. **Size hint dal FS** (`fs.stat().size` su directory)
|
|
325
|
+
2. **Probe-and-commit** (leggi le prime N entry, poi decidi)
|
|
326
|
+
3. **Cache di metadati da richieste precedenti**
|
|
327
|
+
|
|
328
|
+
Tutte e tre hanno problemi.
|
|
329
|
+
|
|
330
|
+
#### Approccio A — `fs.stat()` size hint
|
|
331
|
+
|
|
332
|
+
Su ext4/xfs/tmpfs la `size` di una directory è grossolanamente correlata col numero di entry (~24-64 byte per entry). Una stat() extra costa ~0.1 ms.
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
const dirStat = await fs.promises.stat(toOpen);
|
|
336
|
+
if (dirStat.size > THRESHOLD_BYTES) {
|
|
337
|
+
return streamingRead(toOpen, maxEntries); // bounded
|
|
338
|
+
} else {
|
|
339
|
+
return fastRead(toOpen, maxEntries); // readdir + slice
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Pro:** decisione *prima* dell'allocazione, costo ridotto, semplice.
|
|
344
|
+
**Contro decisivo:**
|
|
345
|
+
- **FS-dependent**: NFS / remote FS / FUSE / overlayfs spesso ritornano `size: 0` o valori non significativi per le directory.
|
|
346
|
+
- **False negative**: dir con `size` piccola ma molte entry (nomi corti, hash table compatta) → bypassa il safe path → OOM possibile.
|
|
347
|
+
- **False positive**: dir con `size` grande ma poche entry (nomi molto lunghi) → safe path inutile → 3-4× più lento del necessario.
|
|
348
|
+
- Soglia magica filesystem-dependent.
|
|
349
|
+
|
|
350
|
+
Funziona ~85% dei casi su FS POSIX locali, ma "85%" su un comportamento di sicurezza non è abbastanza.
|
|
351
|
+
|
|
352
|
+
#### Approccio B — Probe-and-commit
|
|
353
|
+
|
|
354
|
+
Apri sempre con `opendir()`. Leggi le prime N entry (es. 5000) via streaming, poi decidi:
|
|
355
|
+
|
|
356
|
+
```javascript
|
|
357
|
+
const handle = await fs.opendir(toOpen);
|
|
358
|
+
const buffer = [];
|
|
359
|
+
for await (const entry of handle) {
|
|
360
|
+
buffer.push(entry);
|
|
361
|
+
if (buffer.length >= PROBE_LIMIT) break;
|
|
362
|
+
}
|
|
363
|
+
// dir piccola → buffer è già il risultato finale
|
|
364
|
+
// dir grande → continua streaming (slow) o butta e ricomincia con readdir (work duplicato)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Pro:** niente heuristica FS-dependent, decisione basata su dato reale.
|
|
368
|
+
**Contro decisivo:**
|
|
369
|
+
- Paghi sempre l'overhead opendir per le prime N entry (N=5000 → ~155 ms anche su dir di 100 file).
|
|
370
|
+
- Per dir piccole è una regressione netta.
|
|
371
|
+
- Se sopra threshold devi scegliere fra continuare streaming (lento) o re-leggere via readdir (lavoro duplicato).
|
|
372
|
+
|
|
373
|
+
Non hybridizza davvero: regressione per i casi piccoli.
|
|
374
|
+
|
|
375
|
+
#### Approccio C — Cache di metadati
|
|
376
|
+
|
|
377
|
+
Ricorda il numero di entry dalla richiesta precedente alla stessa directory.
|
|
378
|
+
|
|
379
|
+
**Contro:**
|
|
380
|
+
- Prima richiesta sempre slow.
|
|
381
|
+
- Stale cache su FS che cambiano.
|
|
382
|
+
- Multi-process unsafe.
|
|
383
|
+
- Memory overhead.
|
|
384
|
+
|
|
385
|
+
Scartato in fase di brainstorm.
|
|
386
|
+
|
|
387
|
+
#### Cosa fanno nginx e Apache
|
|
388
|
+
|
|
389
|
+
Ricerca rapida sul comportamento di altri server statici:
|
|
390
|
+
|
|
391
|
+
| Server | Modulo | Strategia | Note |
|
|
392
|
+
|---|---|---|---|
|
|
393
|
+
| **nginx** | `autoindex` | `opendir()` + iterazione sempre | Performance "decente, non eccellente". Documentazione raccomanda di disabilitarlo in produzione. |
|
|
394
|
+
| **Apache HTTPd** | `mod_autoindex` | `opendir()` + iterazione sempre | Idem. Considerato feature di dev/admin, non hot path. |
|
|
395
|
+
| **lighttpd** | `mod_dirlisting` | `opendir()` + iterazione | Stesso pattern. |
|
|
396
|
+
| **caddy** | listing module | `os.ReadDir()` (Go), che è l'equivalente di readdir+slice | Niente hybrid. |
|
|
397
|
+
|
|
398
|
+
**Pattern industria**: nessuno fa hybrid automatico. La scelta universale è **`opendir()` sempre** (accetta la lentezza come prezzo della sicurezza) oppure **`readdir()` sempre** (caddy, accetta la potenziale RAM exhaustion).
|
|
399
|
+
|
|
400
|
+
La motivazione consensuale: *autoindex non è un hot path. Chi serve milioni di file con autoindex acceso o ha una specifica esigenza (lo configurerà) o sta abusando della feature (servirà disabilitarlo).*
|
|
401
|
+
|
|
402
|
+
#### Conclusione del brainstorm
|
|
403
|
+
|
|
404
|
+
L'hybrid automatico ha tre realizzazioni teoriche, **nessuna soddisfacente**:
|
|
405
|
+
|
|
406
|
+
| Approccio | Pro | Contro decisivo |
|
|
407
|
+
|---|---|---|
|
|
408
|
+
| A — stat() heuristic | trasparente | FS-dependent, soglia magica, false-negative pericolosi |
|
|
409
|
+
| B — probe-and-commit | non heuristica | overhead sempre, lavoro duplicato |
|
|
410
|
+
| C — cache | trasparente per richieste successive | prima sempre slow, stale, unsafe |
|
|
411
|
+
|
|
412
|
+
Le opzioni *robuste* restano due:
|
|
413
|
+
|
|
414
|
+
1. **`readMode: 'fast' | 'bounded'` esplicito** (la proposta originale [F-1]) — operatore sceglie consapevolmente
|
|
415
|
+
2. **`readMode: 'auto' | 'fast' | 'bounded'`** con `auto` = approccio A — friendly per i casi POSIX comuni, escape hatch per workload critici
|
|
416
|
+
|
|
417
|
+
**Raccomandazione per v3.1:** preferire (1) — è quello che fanno tutti gli altri server. La variante (2) con `auto` come default amichevole è tecnicamente possibile ma il caveat "best-effort, fragile su NFS/FUSE" rende l'opzione `auto` più rumorosa da documentare di quanto valga.
|
|
418
|
+
|
|
419
|
+
**Decisione finale rimandata al freeze 3.1.**
|
|
420
|
+
|
|
421
|
+
---
|
|
@@ -127,7 +127,7 @@ const path = require('path');
|
|
|
127
127
|
const app = new Koa();
|
|
128
128
|
|
|
129
129
|
app.use(koaClassicServer(path.join(__dirname, 'public'), {
|
|
130
|
-
|
|
130
|
+
dirListing: { enabled: true },
|
|
131
131
|
template: {
|
|
132
132
|
ext: ['ejs'],
|
|
133
133
|
render: async (ctx, next, filePath) => {
|
|
@@ -427,7 +427,7 @@ app.use(
|
|
|
427
427
|
koaClassicServer(
|
|
428
428
|
__dirname + '/public',
|
|
429
429
|
{
|
|
430
|
-
|
|
430
|
+
dirListing: { enabled: true },
|
|
431
431
|
template: {
|
|
432
432
|
render: async (ctx, next, filePath) => {
|
|
433
433
|
try {
|
|
@@ -467,7 +467,7 @@ app.use(
|
|
|
467
467
|
koaClassicServer(
|
|
468
468
|
__dirname + '/public',
|
|
469
469
|
{
|
|
470
|
-
|
|
470
|
+
dirListing: { enabled: true },
|
|
471
471
|
template: {
|
|
472
472
|
render: async (ctx, next, filePath) => {
|
|
473
473
|
try {
|
|
@@ -540,7 +540,7 @@ app.use(
|
|
|
540
540
|
koaClassicServer(
|
|
541
541
|
__dirname + '/public',
|
|
542
542
|
{
|
|
543
|
-
|
|
543
|
+
dirListing: { enabled: true },
|
|
544
544
|
template: {
|
|
545
545
|
render: async (ctx, next, filePath) => {
|
|
546
546
|
try {
|
|
@@ -680,7 +680,7 @@ app.use(
|
|
|
680
680
|
koaClassicServer(
|
|
681
681
|
__dirname + `${ital8Conf.wwwPath}`,
|
|
682
682
|
{
|
|
683
|
-
|
|
683
|
+
dirListing: { enabled: true },
|
|
684
684
|
urlsReserved: ['/' + ital8Conf.adminPrefix, '/' + ital8Conf.apiPrefix, '/' + ital8Conf.viewsPrefix],
|
|
685
685
|
template: {
|
|
686
686
|
render: async (ctx, next, filePath) => {
|