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
package/docs/DOCUMENTATION.md
CHANGED
|
@@ -157,7 +157,7 @@ const koaClassicServer = require('koa-classic-server');
|
|
|
157
157
|
const app = new Koa();
|
|
158
158
|
|
|
159
159
|
app.use(koaClassicServer(__dirname + '/public', {
|
|
160
|
-
|
|
160
|
+
dirListing: { enabled: true },
|
|
161
161
|
index: 'index.html',
|
|
162
162
|
urlPrefix: '/static'
|
|
163
163
|
}));
|
|
@@ -193,7 +193,7 @@ const options = {
|
|
|
193
193
|
|
|
194
194
|
// Mostra il contenuto delle directory
|
|
195
195
|
// Default: true
|
|
196
|
-
|
|
196
|
+
dirListing: { enabled: true },
|
|
197
197
|
|
|
198
198
|
// Nome del file index da caricare automaticamente nelle directory
|
|
199
199
|
// Se presente, viene caricato invece del directory listing
|
|
@@ -245,16 +245,16 @@ method: ['GET', 'HEAD']
|
|
|
245
245
|
method: ['GET', 'HEAD', 'POST']
|
|
246
246
|
```
|
|
247
247
|
|
|
248
|
-
#### `
|
|
248
|
+
#### `dirListing.enabled` (Boolean)
|
|
249
249
|
|
|
250
250
|
Controlla se mostrare il contenuto delle directory.
|
|
251
251
|
|
|
252
252
|
```javascript
|
|
253
253
|
// Mostra directory listing
|
|
254
|
-
|
|
254
|
+
dirListing: { enabled: true }
|
|
255
255
|
|
|
256
256
|
// Non mostra directory (restituisce "Not Found")
|
|
257
|
-
|
|
257
|
+
dirListing: { enabled: false }
|
|
258
258
|
```
|
|
259
259
|
|
|
260
260
|
#### `index` (String)
|
|
@@ -275,7 +275,7 @@ index: ''
|
|
|
275
275
|
**Comportamento:**
|
|
276
276
|
1. Utente accede a `/cartella/`
|
|
277
277
|
2. Se esiste `/cartella/index.html` → viene servito
|
|
278
|
-
3. Altrimenti → mostra directory listing (se `
|
|
278
|
+
3. Altrimenti → mostra directory listing (se `dirListing: { enabled: true }`)
|
|
279
279
|
|
|
280
280
|
#### `urlPrefix` (String)
|
|
281
281
|
|
|
@@ -373,7 +373,7 @@ const koaClassicServer = require('koa-classic-server');
|
|
|
373
373
|
const app = new Koa();
|
|
374
374
|
|
|
375
375
|
app.use(koaClassicServer(__dirname + '/public', {
|
|
376
|
-
|
|
376
|
+
dirListing: { enabled: true },
|
|
377
377
|
index: 'index.html'
|
|
378
378
|
}));
|
|
379
379
|
|
|
@@ -400,7 +400,7 @@ const app = new Koa();
|
|
|
400
400
|
app.use(koaClassicServer(__dirname + '/public', {
|
|
401
401
|
urlPrefix: '/static',
|
|
402
402
|
method: ['GET', 'HEAD'],
|
|
403
|
-
|
|
403
|
+
dirListing: { enabled: true }
|
|
404
404
|
}));
|
|
405
405
|
|
|
406
406
|
// Altri middleware per route diverse
|
|
@@ -427,7 +427,7 @@ const koaClassicServer = require('koa-classic-server');
|
|
|
427
427
|
const app = new Koa();
|
|
428
428
|
|
|
429
429
|
app.use(koaClassicServer(__dirname + '/www', {
|
|
430
|
-
|
|
430
|
+
dirListing: { enabled: true },
|
|
431
431
|
// Protegge directory sensibili
|
|
432
432
|
urlsReserved: ['/config', '/private', '/.git', '/node_modules']
|
|
433
433
|
}));
|
|
@@ -452,7 +452,7 @@ const ejs = require('ejs');
|
|
|
452
452
|
const app = new Koa();
|
|
453
453
|
|
|
454
454
|
app.use(koaClassicServer(__dirname + '/views', {
|
|
455
|
-
|
|
455
|
+
dirListing: { enabled: false },
|
|
456
456
|
template: {
|
|
457
457
|
render: async (ctx, next, filePath) => {
|
|
458
458
|
// Rendering EJS con dati
|
|
@@ -485,19 +485,19 @@ const app = new Koa();
|
|
|
485
485
|
// File statici pubblici
|
|
486
486
|
app.use(koaClassicServer(__dirname + '/public', {
|
|
487
487
|
urlPrefix: '/public',
|
|
488
|
-
|
|
488
|
+
dirListing: { enabled: true }
|
|
489
489
|
}));
|
|
490
490
|
|
|
491
491
|
// Asset (CSS, JS, images)
|
|
492
492
|
app.use(koaClassicServer(__dirname + '/assets', {
|
|
493
493
|
urlPrefix: '/assets',
|
|
494
|
-
|
|
494
|
+
dirListing: { enabled: false }
|
|
495
495
|
}));
|
|
496
496
|
|
|
497
497
|
// Download area
|
|
498
498
|
app.use(koaClassicServer(__dirname + '/downloads', {
|
|
499
499
|
urlPrefix: '/downloads',
|
|
500
|
-
|
|
500
|
+
dirListing: { enabled: true },
|
|
501
501
|
index: 'README.txt'
|
|
502
502
|
}));
|
|
503
503
|
|
|
@@ -549,7 +549,7 @@ const templateRender = async (ctx, next, filePath) => {
|
|
|
549
549
|
// File server
|
|
550
550
|
app.use(koaClassicServer(path.join(__dirname, 'public'), {
|
|
551
551
|
method: ['GET', 'HEAD'],
|
|
552
|
-
|
|
552
|
+
dirListing: { enabled: process.env.NODE_ENV !== 'production' },
|
|
553
553
|
index: 'index.html',
|
|
554
554
|
urlPrefix: '/files',
|
|
555
555
|
urlsReserved: ['/admin', '/private', '/config', '/.env'],
|
|
@@ -599,7 +599,7 @@ app.use(router.allowedMethods());
|
|
|
599
599
|
|
|
600
600
|
// File statici (dopo le route API)
|
|
601
601
|
app.use(koaClassicServer(__dirname + '/public', {
|
|
602
|
-
|
|
602
|
+
dirListing: { enabled: true }
|
|
603
603
|
}));
|
|
604
604
|
|
|
605
605
|
app.listen(3000);
|
|
@@ -631,7 +631,7 @@ Crea e restituisce un middleware Koa per servire file statici.
|
|
|
631
631
|
|
|
632
632
|
```javascript
|
|
633
633
|
const middleware = koaClassicServer('/path/to/files', {
|
|
634
|
-
|
|
634
|
+
dirListing: { enabled: true }
|
|
635
635
|
});
|
|
636
636
|
|
|
637
637
|
app.use(middleware);
|
|
@@ -682,7 +682,7 @@ template: {
|
|
|
682
682
|
|
|
683
683
|
### Gestione delle Directory
|
|
684
684
|
|
|
685
|
-
#### Caso 1:
|
|
685
|
+
#### Caso 1: dirListing.enabled = true, index presente
|
|
686
686
|
|
|
687
687
|
```
|
|
688
688
|
Richiesta: GET /cartella/
|
|
@@ -693,7 +693,7 @@ Filesystem:
|
|
|
693
693
|
Risultato: Serve /cartella/index.html
|
|
694
694
|
```
|
|
695
695
|
|
|
696
|
-
#### Caso 2:
|
|
696
|
+
#### Caso 2: dirListing.enabled = true, index assente
|
|
697
697
|
|
|
698
698
|
```
|
|
699
699
|
Richiesta: GET /cartella/
|
|
@@ -704,7 +704,7 @@ Filesystem:
|
|
|
704
704
|
Risultato: Mostra directory listing HTML
|
|
705
705
|
```
|
|
706
706
|
|
|
707
|
-
#### Caso 3:
|
|
707
|
+
#### Caso 3: dirListing.enabled = false
|
|
708
708
|
|
|
709
709
|
```
|
|
710
710
|
Richiesta: GET /cartella/
|
|
@@ -925,7 +925,7 @@ describe('My custom test', () => {
|
|
|
925
925
|
beforeAll(() => {
|
|
926
926
|
app = new Koa();
|
|
927
927
|
app.use(koaClassicServer(__dirname + '/test-files', {
|
|
928
|
-
|
|
928
|
+
dirListing: { enabled: true }
|
|
929
929
|
}));
|
|
930
930
|
server = app.listen();
|
|
931
931
|
});
|
|
@@ -1042,11 +1042,11 @@ npm install ejs
|
|
|
1042
1042
|
**Soluzione:**
|
|
1043
1043
|
|
|
1044
1044
|
```javascript
|
|
1045
|
-
// ❌
|
|
1046
|
-
|
|
1045
|
+
// ❌ dirListing.enabled disabilitato
|
|
1046
|
+
dirListing: { enabled: false }
|
|
1047
1047
|
|
|
1048
|
-
// ✅
|
|
1049
|
-
|
|
1048
|
+
// ✅ dirListing.enabled abilitato
|
|
1049
|
+
dirListing: { enabled: true }
|
|
1050
1050
|
```
|
|
1051
1051
|
|
|
1052
1052
|
---
|
|
@@ -1128,12 +1128,12 @@ const url = '/' + encodeURIComponent(filename);
|
|
|
1128
1128
|
|
|
1129
1129
|
1. Disabilita directory listing:
|
|
1130
1130
|
```javascript
|
|
1131
|
-
|
|
1131
|
+
dirListing: { enabled: false }
|
|
1132
1132
|
```
|
|
1133
1133
|
|
|
1134
1134
|
2. Usa file index:
|
|
1135
1135
|
```javascript
|
|
1136
|
-
|
|
1136
|
+
dirListing: { enabled: true },
|
|
1137
1137
|
index: 'index.html' // Carica index invece di listing
|
|
1138
1138
|
```
|
|
1139
1139
|
|
|
@@ -1278,11 +1278,238 @@ method: ['GET', 'HEAD']
|
|
|
1278
1278
|
#### Disabilita Directory Listing in Produzione
|
|
1279
1279
|
```javascript
|
|
1280
1280
|
app.use(koaClassicServer(rootDir, {
|
|
1281
|
-
|
|
1281
|
+
dirListing: { enabled: process.env.NODE_ENV !== 'production' },
|
|
1282
1282
|
index: 'index.html'
|
|
1283
1283
|
}));
|
|
1284
1284
|
```
|
|
1285
1285
|
|
|
1286
|
+
#### DNS Rebinding — valida l'header `Host` a monte
|
|
1287
|
+
|
|
1288
|
+
`koa-classic-server` **non valida l'header `Host`** delle richieste in entrata. Questo è intenzionale: la validazione del Virtual Host appartiene allo strato di rete (reverse proxy o middleware Koa applicativo), non a un file server.
|
|
1289
|
+
|
|
1290
|
+
In assenza di tale validazione, un host LAN/loopback (es. `127.0.0.1`, `192.168.x.x`) è vulnerabile a **DNS rebinding**: un attaccante remoto può far risolvere il proprio dominio all'IP della vittima ed eseguire richieste cross-origin verso il server locale come se provenissero da un'origine legittima del browser.
|
|
1291
|
+
|
|
1292
|
+
**Quando è un problema concreto**
|
|
1293
|
+
- Server raggiunto da `localhost` / IP privati senza reverse proxy davanti.
|
|
1294
|
+
- Browser dell'utente che visita pagine non fidate mentre il server è attivo.
|
|
1295
|
+
|
|
1296
|
+
**Quando NON è un problema**
|
|
1297
|
+
- Deploy dietro reverse proxy (nginx, Caddy, Apache, Traefik) configurato con allowlist di `server_name` / hostname.
|
|
1298
|
+
- Server raggiungibile solo da IP pubblico dietro CDN/WAF.
|
|
1299
|
+
- Binding esplicito a interfaccia non instradabile (`app.listen(port, '127.0.0.1')`) e nessun browser locale espone l'utente a pagine ostili.
|
|
1300
|
+
|
|
1301
|
+
**Mitigazione 1 — reverse proxy (raccomandato in produzione)**
|
|
1302
|
+
|
|
1303
|
+
Esempio nginx:
|
|
1304
|
+
```nginx
|
|
1305
|
+
server {
|
|
1306
|
+
listen 80;
|
|
1307
|
+
server_name app.example.com; # ← allowlist
|
|
1308
|
+
location / { proxy_pass http://127.0.0.1:3000; }
|
|
1309
|
+
}
|
|
1310
|
+
# Tutte le richieste con Host diverso da app.example.com vengono rifiutate qui.
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
**Mitigazione 2 — middleware Koa applicativo (utile in dev/LAN)**
|
|
1314
|
+
|
|
1315
|
+
Antepone una guardia su `ctx.host` PRIMA di `koa-classic-server`:
|
|
1316
|
+
|
|
1317
|
+
```javascript
|
|
1318
|
+
const ALLOWED_HOSTS = new Set([
|
|
1319
|
+
'app.example.com',
|
|
1320
|
+
'localhost:3000',
|
|
1321
|
+
'127.0.0.1:3000',
|
|
1322
|
+
]);
|
|
1323
|
+
|
|
1324
|
+
app.use(async (ctx, next) => {
|
|
1325
|
+
// ctx.host include la porta (es. "localhost:3000")
|
|
1326
|
+
if (!ALLOWED_HOSTS.has(ctx.host)) {
|
|
1327
|
+
ctx.status = 421; // Misdirected Request
|
|
1328
|
+
ctx.body = 'Host not allowed';
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
await next();
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
app.use(koaClassicServer(rootDir));
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
> ⚠️ Se il proxy a monte termina TLS e inoltra all'app, configura `app.proxy = true` e usa `ctx.hostname` con `X-Forwarded-Host` solo se il proxy è fidato.
|
|
1338
|
+
|
|
1339
|
+
#### Limiti dei Security Headers sui file statici
|
|
1340
|
+
|
|
1341
|
+
`koa-classic-server` imposta automaticamente i seguenti header SOLO sulle **risposte generate** dal middleware (directory listing HTML, pagine di errore 400/403/404/405/500):
|
|
1342
|
+
|
|
1343
|
+
| Header | Valore |
|
|
1344
|
+
|---|---|
|
|
1345
|
+
| `Content-Security-Policy` | `default-src 'none'; ...` (hash-based per il listing, fully restrictive per gli errori) |
|
|
1346
|
+
| `X-Content-Type-Options` | `nosniff` |
|
|
1347
|
+
| `X-Frame-Options` | `DENY` |
|
|
1348
|
+
| `Referrer-Policy` | `no-referrer` |
|
|
1349
|
+
| `Permissions-Policy` | `camera=(), microphone=(), geolocation=(), payment=()` |
|
|
1350
|
+
|
|
1351
|
+
**Cosa NON riceve questi header**
|
|
1352
|
+
|
|
1353
|
+
I **file statici** serviti dal disco (HTML, JS, CSS, immagini, font, video, qualsiasi altra estensione) sono restituiti **senza** alcun header di sicurezza aggiunto. Questo è **by-design**:
|
|
1354
|
+
|
|
1355
|
+
- Le policy di sicurezza appropriate sono diverse caso per caso (CSP per app SPA, sandbox per documenti, COEP/COOP per pagine con SharedArrayBuffer, ecc.).
|
|
1356
|
+
- Imporre una CSP rigida di default romperebbe la maggior parte dei siti reali (script inline, CDN, Google Fonts, ecc.).
|
|
1357
|
+
- L'utente possiede la propria policy e deve dichiararla esplicitamente.
|
|
1358
|
+
|
|
1359
|
+
> ⚠️ Non assumere che il middleware "metta in sicurezza" i tuoi file: gli header sopra elencati vengono inviati **solo** quando la risposta è generata dal middleware stesso.
|
|
1360
|
+
|
|
1361
|
+
**Come aggiungere security headers ai file statici**
|
|
1362
|
+
|
|
1363
|
+
Inserisci un middleware Koa **prima** di `koa-classic-server`. Il middleware si applica a ogni risposta uscente, statica o generata che sia, e i `ctx.set()` rimangono attivi anche dopo che il file server scrive il body:
|
|
1364
|
+
|
|
1365
|
+
```javascript
|
|
1366
|
+
const Koa = require('koa');
|
|
1367
|
+
const koaClassicServer = require('koa-classic-server');
|
|
1368
|
+
|
|
1369
|
+
const app = new Koa();
|
|
1370
|
+
|
|
1371
|
+
// 1) Header globali su TUTTE le risposte (statiche + generate).
|
|
1372
|
+
app.use(async (ctx, next) => {
|
|
1373
|
+
// Header sempre validi per un file server pubblico:
|
|
1374
|
+
ctx.set('X-Content-Type-Options', 'nosniff');
|
|
1375
|
+
ctx.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
1376
|
+
ctx.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains');
|
|
1377
|
+
|
|
1378
|
+
// CSP per file HTML serviti dall'utente (mantieni il file server per gli altri MIME).
|
|
1379
|
+
// Esempio strict-by-default per un sito statico moderno:
|
|
1380
|
+
if (ctx.path.endsWith('.html') || ctx.path === '/' || ctx.path.endsWith('/')) {
|
|
1381
|
+
ctx.set('Content-Security-Policy',
|
|
1382
|
+
"default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; " +
|
|
1383
|
+
"script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
await next();
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
// 2) Quindi il file server.
|
|
1391
|
+
app.use(koaClassicServer(__dirname + '/public'));
|
|
1392
|
+
|
|
1393
|
+
app.listen(3000);
|
|
1394
|
+
```
|
|
1395
|
+
|
|
1396
|
+
**Note operative**
|
|
1397
|
+
|
|
1398
|
+
- Il middleware sopra **non sovrascrive** gli header che `koa-classic-server` imposta sulle proprie pagine (listing/errori): Koa preserva il primo `set()`, ma se vuoi essere esplicito puoi applicare le tue policy solo a `ctx.status < 400` e a `Content-Type` HTML.
|
|
1399
|
+
- Una CSP rigida con `default-src 'self'` rompe pagine che usano script inline o CDN: parti **report-only** (`Content-Security-Policy-Report-Only`) per rilevare le violazioni prima di enforce-arla.
|
|
1400
|
+
- Per progetti SPA/PWA che richiedono `SharedArrayBuffer`, considera anche `Cross-Origin-Opener-Policy: same-origin` e `Cross-Origin-Embedder-Policy: require-corp`.
|
|
1401
|
+
|
|
1402
|
+
#### Security Checklist & Suggested Configuration
|
|
1403
|
+
|
|
1404
|
+
`koa-classic-server` parte dal principio **"file server first"** (vedi [`CLAUDE.md`](../CLAUDE.md)): i default servono i file senza restrizioni di policy nascoste. L'operatore irrobustisce il deploy tramite **configurazione esplicita**.
|
|
1405
|
+
|
|
1406
|
+
##### Checklist per categoria
|
|
1407
|
+
|
|
1408
|
+
**Static site / public asset serving**
|
|
1409
|
+
|
|
1410
|
+
| ✓ | Cosa | Snippet |
|
|
1411
|
+
|---|---|---|
|
|
1412
|
+
| ☐ | Nascondi dot-files con potenziali segreti | `hidden: { dotFiles: { default: 'hidden', whitelist: ['.well-known'] } }` |
|
|
1413
|
+
| ☐ | Blocca dot-directories tipo `.git` | `hidden: { dotDirs: { default: 'hidden', whitelist: ['.well-known'] } }` |
|
|
1414
|
+
| ☐ | Disabilita listing in produzione | `dirListing: { enabled: false }` + `index` |
|
|
1415
|
+
| ☐ | Abilita HTTP caching | `browserCacheEnabled: true, browserCacheMaxAge: 86400` |
|
|
1416
|
+
| ☐ | Restringi i metodi | `method: ['GET', 'HEAD']` |
|
|
1417
|
+
| ☐ | Riserva path applicativi | `urlsReserved: ['/api', '/admin']` |
|
|
1418
|
+
| ☐ | Aggiungi security headers upstream sui file utente | middleware Koa upstream (vedi *Limiti dei Security Headers* sopra) |
|
|
1419
|
+
|
|
1420
|
+
**User uploads / multi-tenant / directory scrivibili da terzi**
|
|
1421
|
+
|
|
1422
|
+
| ✓ | Cosa | Snippet |
|
|
1423
|
+
|---|---|---|
|
|
1424
|
+
| ☐ | Cap entries più stretto contro dir gonfiate | `dirListing: { maxEntries: 1000 }` |
|
|
1425
|
+
| ☐ | Hide dot-files a ogni profondità | `hidden: { dotFiles: { default: 'hidden' }, dotDirs: { default: 'hidden' } }` |
|
|
1426
|
+
| ☐ | Blocklist path-aware per pattern noti | `hidden: { alwaysHide: ['*.key', '*.pem', /\.secret$/, 'config/secrets/**'] }` |
|
|
1427
|
+
| ☐ | Monitora la crescita dir esternamente (cron + alert) | — |
|
|
1428
|
+
|
|
1429
|
+
> **Caveat v3.0:** `dirListing.maxEntries` bounda rendering/CPU ma NON la lettura iniziale `readdir()`. Per workload adversarial (utenti non fidati che possono creare milioni di file) la protezione RAM completa arriverà con il `readMode: 'bounded'` di v3.1 (vedi `[F-1]` in `docs/security_improvement_for_V3.md`).
|
|
1430
|
+
|
|
1431
|
+
**Production hygiene (qualsiasi deploy)**
|
|
1432
|
+
|
|
1433
|
+
| ✓ | Cosa | Snippet |
|
|
1434
|
+
|---|---|---|
|
|
1435
|
+
| ☐ | Allowlist Host upstream | nginx `server_name` o middleware Koa allowlist su `ctx.host` |
|
|
1436
|
+
| ☐ | Disabilita template engine se non SSR | ometti `template` |
|
|
1437
|
+
| ☐ | Tune `template.renderTimeout` | abbassa da 30 s per SLA stretti |
|
|
1438
|
+
| ☐ | Logger strutturato (Pino/Winston) | `logger: pino()` |
|
|
1439
|
+
| ☐ | Pin patch version + `npm audit` in CI | `package.json` + workflow CI |
|
|
1440
|
+
|
|
1441
|
+
##### Suggested production security configuration
|
|
1442
|
+
|
|
1443
|
+
Una sola configurazione di partenza che copre l'80% dei deploy. Tweak per workload specifici.
|
|
1444
|
+
|
|
1445
|
+
```javascript
|
|
1446
|
+
const Koa = require('koa');
|
|
1447
|
+
const pino = require('pino')({ level: 'info' });
|
|
1448
|
+
const path = require('path');
|
|
1449
|
+
const koaClassicServer = require('koa-classic-server');
|
|
1450
|
+
|
|
1451
|
+
const app = new Koa();
|
|
1452
|
+
|
|
1453
|
+
// 1) Host allowlist — mitiga DNS rebinding su esposizione LAN/loopback.
|
|
1454
|
+
const ALLOWED_HOSTS = new Set([
|
|
1455
|
+
'app.example.com',
|
|
1456
|
+
'localhost:3000',
|
|
1457
|
+
]);
|
|
1458
|
+
app.use(async (ctx, next) => {
|
|
1459
|
+
if (!ALLOWED_HOSTS.has(ctx.host)) {
|
|
1460
|
+
ctx.status = 421;
|
|
1461
|
+
ctx.body = 'Misdirected Request';
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
await next();
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
// 2) Security headers sui file utente (il middleware li imposta solo su
|
|
1468
|
+
// listing/errori, NON sui file statici — by design).
|
|
1469
|
+
app.use(async (ctx, next) => {
|
|
1470
|
+
ctx.set('X-Content-Type-Options', 'nosniff');
|
|
1471
|
+
ctx.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
1472
|
+
ctx.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains');
|
|
1473
|
+
await next();
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
// 3) Il file server con default irrobustiti per produzione.
|
|
1477
|
+
app.use(koaClassicServer(path.join(__dirname, 'public'), {
|
|
1478
|
+
method: ['GET', 'HEAD'],
|
|
1479
|
+
|
|
1480
|
+
index: ['index.html'],
|
|
1481
|
+
|
|
1482
|
+
dirListing: {
|
|
1483
|
+
enabled: process.env.NODE_ENV !== 'production',
|
|
1484
|
+
maxEntries: 10000, // più stretto del default 100000 anti-OOM
|
|
1485
|
+
entriesPerPage: 100,
|
|
1486
|
+
},
|
|
1487
|
+
|
|
1488
|
+
hidden: {
|
|
1489
|
+
dotFiles: {
|
|
1490
|
+
default: 'hidden', // hardening esplicito vs default 'visible' filosofico
|
|
1491
|
+
whitelist: ['.well-known'], // ACME / Let's Encrypt
|
|
1492
|
+
},
|
|
1493
|
+
dotDirs: {
|
|
1494
|
+
default: 'hidden',
|
|
1495
|
+
whitelist: ['.well-known'],
|
|
1496
|
+
},
|
|
1497
|
+
alwaysHide: ['*.key', '*.pem', /^backup-/, /\.secret$/],
|
|
1498
|
+
},
|
|
1499
|
+
|
|
1500
|
+
browserCacheEnabled: true,
|
|
1501
|
+
browserCacheMaxAge: 86400, // 24 h cache
|
|
1502
|
+
|
|
1503
|
+
logger: pino, // structured logging
|
|
1504
|
+
|
|
1505
|
+
urlsReserved: ['/api', '/admin'], // route applicative gestite altrove
|
|
1506
|
+
}));
|
|
1507
|
+
|
|
1508
|
+
app.listen(3000);
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
Per **user-uploads / multi-tenant**: oltre al blocco sopra, riduci `dirListing.maxEntries` a `1000` e monitora la dimensione della directory dall'esterno fino a quando v3.1 non aggiunge il `readMode: 'bounded'`.
|
|
1512
|
+
|
|
1286
1513
|
---
|
|
1287
1514
|
|
|
1288
1515
|
### 2. Performance
|
|
@@ -1292,7 +1519,7 @@ app.use(koaClassicServer(rootDir, {
|
|
|
1292
1519
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
1293
1520
|
|
|
1294
1521
|
app.use(koaClassicServer(rootDir, {
|
|
1295
|
-
|
|
1522
|
+
dirListing: { enabled: !isProduction },
|
|
1296
1523
|
method: ['GET', 'HEAD']
|
|
1297
1524
|
}));
|
|
1298
1525
|
```
|
|
@@ -1334,7 +1561,7 @@ module.exports = {
|
|
|
1334
1561
|
rootDir: path.join(__dirname, '../public'),
|
|
1335
1562
|
options: {
|
|
1336
1563
|
method: ['GET', 'HEAD'],
|
|
1337
|
-
|
|
1564
|
+
dirListing: { enabled: true },
|
|
1338
1565
|
index: 'index.html',
|
|
1339
1566
|
urlPrefix: '/static',
|
|
1340
1567
|
urlsReserved: ['/admin', '/config']
|
|
@@ -1459,7 +1686,7 @@ if (isDev) {
|
|
|
1459
1686
|
|
|
1460
1687
|
// File server con configurazione ambiente-specifica
|
|
1461
1688
|
app.use(koaClassicServer(path.join(__dirname, 'public'), {
|
|
1462
|
-
|
|
1689
|
+
dirListing: { enabled: isDev }, // Solo in dev
|
|
1463
1690
|
index: 'index.html',
|
|
1464
1691
|
urlPrefix: isDev ? '' : '/static', // Prefix solo in prod
|
|
1465
1692
|
urlsReserved: ['/admin', '/config', '/.env'],
|
|
@@ -1491,7 +1718,7 @@ app.listen(process.env.PORT || 3000);
|
|
|
1491
1718
|
const configs = [
|
|
1492
1719
|
{ name: 'base', options: {} },
|
|
1493
1720
|
{ name: 'with-prefix', options: { urlPrefix: '/public' } },
|
|
1494
|
-
{ name: 'no-listing', options: {
|
|
1721
|
+
{ name: 'no-listing', options: { dirListing: { enabled: false } } }
|
|
1495
1722
|
];
|
|
1496
1723
|
|
|
1497
1724
|
configs.forEach(({ name, options }) => {
|
|
@@ -1521,7 +1748,7 @@ configs.forEach(({ name, options }) => {
|
|
|
1521
1748
|
```javascript
|
|
1522
1749
|
app.use(koaClassicServer(__dirname + '/public', {
|
|
1523
1750
|
// Mostra directory solo in development per sicurezza
|
|
1524
|
-
|
|
1751
|
+
dirListing: { enabled: process.env.NODE_ENV !== 'production' },
|
|
1525
1752
|
|
|
1526
1753
|
// Protegge configurazioni sensibili
|
|
1527
1754
|
// Nota: funziona solo per directory di primo livello
|
|
@@ -339,7 +339,7 @@ const app = new Koa();
|
|
|
339
339
|
// Configurazione production-ready
|
|
340
340
|
app.use(koaClassicServer('./public', {
|
|
341
341
|
method: ['GET', 'HEAD'],
|
|
342
|
-
|
|
342
|
+
dirListing: { enabled: true },
|
|
343
343
|
|
|
344
344
|
// Index configuration con fallback multipli
|
|
345
345
|
index: [
|
package/docs/FLOW_DIAGRAM.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Flow Diagram - koa-classic-server
|
|
2
2
|
|
|
3
|
+
> **Nota storica:** diagrammi e snippet di questo documento sono pre-V3. Riferimenti a `options.showDirContents` corrispondono a `options.dirListing.enabled` nell'API V3 corrente. Vedi [README.md → Migration Guide](../README.md#from-v2x-to-v3x).
|
|
4
|
+
|
|
3
5
|
## Table of Contents
|
|
4
6
|
- [Overview](#overview)
|
|
5
7
|
- [Main Flow Diagram](#main-flow-diagram)
|
|
@@ -61,7 +61,7 @@ index: ['index1.html', 'index2.html', 'index3.html']
|
|
|
61
61
|
- ✅ Se trovato → **SERVE index3.html** (STOP)
|
|
62
62
|
- ❌ Se non trovato → Passa al passo 4
|
|
63
63
|
|
|
64
|
-
4. Nessun file trovato → **Mostra directory listing** (se `
|
|
64
|
+
4. Nessun file trovato → **Mostra directory listing** (se `dirListing: { enabled: true }`)
|
|
65
65
|
|
|
66
66
|
---
|
|
67
67
|
|
|
@@ -502,7 +502,7 @@ app.use(koaClassicServer('./public', {
|
|
|
502
502
|
**A:** Le stringhe sono **molto più veloci** (O(1) vs O(n)). Metti sempre le stringhe prima delle RegExp.
|
|
503
503
|
|
|
504
504
|
### Q: Cosa succede se nessun pattern matcha?
|
|
505
|
-
**A:** Se `
|
|
505
|
+
**A:** Se `dirListing: { enabled: true }`, mostra directory listing. Altrimenti restituisce 404.
|
|
506
506
|
|
|
507
507
|
---
|
|
508
508
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Ottimizzazione #3: HTTP Caching Headers
|
|
2
2
|
|
|
3
|
+
> **Nota storica:** documento di analisi pre-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
|
## Panoramica
|
|
4
6
|
|
|
5
7
|
**Problema:** I file statici vengono riscaricati dal browser ad ogni richiesta, anche se non sono cambiati.
|