koa-classic-server 3.0.0 → 3.1.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.
Files changed (105) hide show
  1. package/README.md +44 -120
  2. package/index.cjs +236 -21
  3. package/package.json +10 -5
  4. package/.github/workflows/npm-publish.yml +0 -98
  5. package/CLAUDE.md +0 -101
  6. package/__tests__/benchmark-results-baseline-v1.2.0.txt +0 -354
  7. package/__tests__/benchmark-results-optimized-v2.0.0.txt +0 -354
  8. package/__tests__/benchmark-results-v3.0.0.txt +0 -372
  9. package/__tests__/benchmark.js +0 -239
  10. package/__tests__/caching-headers.test.js +0 -556
  11. package/__tests__/compression-fixtures/data.json +0 -1
  12. package/__tests__/compression-fixtures/large.txt +0 -1
  13. package/__tests__/compression-fixtures/small.txt +0 -1
  14. package/__tests__/compression.test.js +0 -284
  15. package/__tests__/customTest/README.md +0 -6
  16. package/__tests__/customTest/loadConfig.util.js +0 -41
  17. package/__tests__/customTest/serversToLoad.util.js +0 -93
  18. package/__tests__/demo-regex-index.js +0 -140
  19. package/__tests__/deprecation-warnings.test.js +0 -105
  20. package/__tests__/directory-sorting-links.test.js +0 -135
  21. package/__tests__/dt-unknown.test.js +0 -635
  22. package/__tests__/ejs.test.js +0 -299
  23. package/__tests__/hidden-fixtures/.dot-dir/inside.txt +0 -1
  24. package/__tests__/hidden-fixtures/.env +0 -2
  25. package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +0 -1
  26. package/__tests__/hidden-fixtures/config/secrets/password.txt +0 -1
  27. package/__tests__/hidden-fixtures/data.key +0 -1
  28. package/__tests__/hidden-fixtures/file.secret +0 -1
  29. package/__tests__/hidden-fixtures/index.html +0 -1
  30. package/__tests__/hidden-fixtures/normal.txt +0 -1
  31. package/__tests__/hidden-fixtures/subdir/.env +0 -1
  32. package/__tests__/hidden-fixtures/subdir/regular.txt +0 -1
  33. package/__tests__/hidden-option.test.js +0 -407
  34. package/__tests__/hideExtension.test.js +0 -607
  35. package/__tests__/index-option.test.js +0 -449
  36. package/__tests__/index.test.js +0 -345
  37. package/__tests__/listing.test.js +0 -437
  38. package/__tests__/logger.test.js +0 -232
  39. package/__tests__/performance.test.js +0 -370
  40. package/__tests__/publicWwwTest/cartella/sottocartella/ciao.html +0 -11
  41. package/__tests__/publicWwwTest/cartella/sottocartella/provaEjs/testEjs.ejs +0 -11
  42. package/__tests__/publicWwwTest/cartella vuota con spazi nel nome/file con spazio nel nome .txt +0 -1
  43. package/__tests__/publicWwwTest/cartella vuota con spazi nel nome /file con spazio nel nome .txt +0 -1
  44. package/__tests__/publicWwwTest/ejs-templates/complex.ejs +0 -56
  45. package/__tests__/publicWwwTest/ejs-templates/index.ejs +0 -30
  46. package/__tests__/publicWwwTest/ejs-templates/simple.ejs +0 -13
  47. package/__tests__/publicWwwTest/ejs-templates/with-conditional.ejs +0 -28
  48. package/__tests__/publicWwwTest/ejs-templates/with-escaping.ejs +0 -26
  49. package/__tests__/publicWwwTest/ejs-templates/with-loop.ejs +0 -16
  50. package/__tests__/publicWwwTest/hideext-test/about/index.html +0 -1
  51. package/__tests__/publicWwwTest/hideext-test/about.EJS +0 -1
  52. package/__tests__/publicWwwTest/hideext-test/about.ejs +0 -2
  53. package/__tests__/publicWwwTest/hideext-test/blog/articolo.ejs +0 -2
  54. package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina +0 -1
  55. package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina.ejs +0 -1
  56. package/__tests__/publicWwwTest/hideext-test/file.ejs.bak +0 -1
  57. package/__tests__/publicWwwTest/hideext-test/index.ejs +0 -2
  58. package/__tests__/publicWwwTest/hideext-test/photo.txt +0 -1
  59. package/__tests__/publicWwwTest/hideext-test/sezione/index.ejs +0 -1
  60. package/__tests__/publicWwwTest/hideext-test/style.css +0 -1
  61. package/__tests__/publicWwwTest/ile_vuoto.txt +0 -0
  62. package/__tests__/publicWwwTest/index.html +0 -11
  63. package/__tests__/publicWwwTest/prova file .txt +0 -2
  64. package/__tests__/publicWwwTest/semplicetxt.txt +0 -1
  65. package/__tests__/publicWwwTest/test-page.html +0 -1
  66. package/__tests__/publicWwwTest/test.txt +0 -1
  67. package/__tests__/range-fixtures/sample.txt +0 -1
  68. package/__tests__/range.test.js +0 -223
  69. package/__tests__/security-headers.test.js +0 -165
  70. package/__tests__/security.test.js +0 -322
  71. package/__tests__/server-cache-fixtures/large.txt +0 -1
  72. package/__tests__/server-cache-fixtures/small.txt +0 -1
  73. package/__tests__/server-cache.test.js +0 -594
  74. package/__tests__/setup-benchmark.js +0 -178
  75. package/__tests__/symlink.test.js +0 -441
  76. package/__tests__/template-timeout.test.js +0 -321
  77. package/__tests__/test-regex-quick.js +0 -158
  78. package/__tests__/useOriginalUrl.test.js +0 -213
  79. package/docs/ACTION_PLAN.md +0 -293
  80. package/docs/BENCHMARKS.md +0 -317
  81. package/docs/CHANGELOG.md +0 -880
  82. package/docs/CODE_REVIEW.md +0 -300
  83. package/docs/DEBUG_REPORT.md +0 -593
  84. package/docs/DOCUMENTATION.md +0 -1864
  85. package/docs/EXAMPLES_INDEX_OPTION.md +0 -395
  86. package/docs/FLOW_DIAGRAM.md +0 -954
  87. package/docs/INDEX_OPTION_PRIORITY.md +0 -527
  88. package/docs/OPTIMIZATION_HTTP_CACHING.md +0 -689
  89. package/docs/OPTIMIZATION_ROADMAP_for_V3.md +0 -864
  90. package/docs/PERFORMANCE_ANALYSIS.md +0 -839
  91. package/docs/PERFORMANCE_COMPARISON.md +0 -388
  92. package/docs/noteExports.md +0 -148
  93. package/docs/security_improvement_for_V3.md +0 -421
  94. package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +0 -1734
  95. package/docs/template-engine/esempi-incrementali.js +0 -192
  96. package/docs/template-engine/examples/esempio1-nessun-dato.ejs +0 -12
  97. package/docs/template-engine/examples/esempio2-una-variabile.ejs +0 -11
  98. package/docs/template-engine/examples/esempio3-piu-variabili.ejs +0 -15
  99. package/docs/template-engine/examples/esempio4-condizionale.ejs +0 -18
  100. package/docs/template-engine/examples/esempio5-loop.ejs +0 -18
  101. package/docs/template-engine/examples/index-esempi.html +0 -181
  102. package/docs/template-engine/examples/index.html +0 -40
  103. package/docs/template-engine/examples/test.ejs +0 -64
  104. package/eslint.config.mjs +0 -17
  105. package/jest.config.js +0 -18
@@ -1,1864 +0,0 @@
1
- # KOA-CLASSIC-SERVER - Documentazione Completa
2
-
3
- ## Indice
4
-
5
- - [Descrizione del Modulo](#descrizione-del-modulo)
6
- - [Installazione](#installazione)
7
- - [Avvio Rapido](#avvio-rapido)
8
- - [Configurazione](#configurazione)
9
- - [Esempi d'Uso](#esempi-duso)
10
- - [API Reference](#api-reference)
11
- - [Comportamento del Middleware](#comportamento-del-middleware)
12
- - [Testing](#testing)
13
- - [Troubleshooting](#troubleshooting)
14
- - [Problemi Noti](#problemi-noti)
15
- - [Best Practices](#best-practices)
16
-
17
- ---
18
-
19
- ## Descrizione del Modulo
20
-
21
- ### Panoramica
22
-
23
- **koa-classic-server** è un middleware per Koa.js che emula il comportamento di Apache 2 per la gestione di file statici. Il modulo permette di servire file e directory con funzionalità avanzate di directory listing, supporto per template engine, gestione di URL riservati e configurazione flessibile.
24
-
25
- ### Caratteristiche Principali
26
-
27
- #### 1. Servizio File Statici
28
- - Serve file statici da una directory specificata
29
- - Riconoscimento automatico dei MIME types
30
- - Gestione corretta di encoding e content-disposition
31
- - Supporto per caratteri speciali e Unicode nei nomi file
32
-
33
- #### 2. Directory Listing
34
- - Visualizzazione del contenuto delle directory in formato HTML tabellare
35
- - Navigazione parent directory con link ".. Parent Directory"
36
- - Indicazione chiara del tipo di risorsa (DIR, MIME type)
37
- - Gestione e visualizzazione cartelle riservate
38
- - Supporto completo link simbolici (symlink a file, directory, broken e circolari)
39
-
40
- #### 3. Supporto Template Engine
41
- - Integrazione flessibile con motori di template (es. EJS, Pug, Handlebars)
42
- - Rendering personalizzato per estensioni specifiche
43
- - Callback configurabile per il rendering
44
- - Accesso completo al contesto Koa (ctx, next)
45
-
46
- #### 4. Gestione URL Avanzata
47
- - Supporto per URL prefix (es. `/public`, `/static`, `/files`)
48
- - URL riservati non accessibili da remoto
49
- - Normalizzazione automatica degli URL (rimozione trailing slash)
50
- - Decodifica URI corretta per spazi e caratteri speciali
51
- - Gestione case-sensitive dei path
52
-
53
- #### 5. Compatibilità Moduli
54
- - Supporto CommonJS (`require`)
55
- - Supporto ES Modules (`import`)
56
- - Conditional exports in package.json per massima compatibilità
57
-
58
- ### Architettura
59
-
60
- ```
61
- koa-classic-server/
62
- ├── index.cjs # Implementazione principale (CommonJS)
63
- ├── index.mjs # Wrapper ES Modules
64
- ├── package.json # Configurazione con conditional exports
65
- ├── __tests__/ # Suite di test Jest
66
- │ ├── index.test.js # Test completi
67
- │ └── publicWwwTest/ # Cartella di test
68
- ├── customTest/ # Utility per test manuali
69
- │ ├── loadConfig.util.js # Script interattivo
70
- │ └── serversToLoad.util.js # Configurazioni test
71
- ├── LICENSE # Licenza MIT
72
- ├── README.md # Documentazione base
73
- └── DOCUMENTATION.md # Questa documentazione
74
- ```
75
-
76
- ### Flusso di Esecuzione
77
-
78
- Il middleware segue questo flusso per ogni richiesta HTTP:
79
-
80
- 1. **Validazione Metodo HTTP**: Verifica che il metodo sia tra quelli ammessi
81
- 2. **Normalizzazione URL**: Rimuove trailing slash e normalizza il path
82
- 3. **Verifica URL Prefix**: Controlla che la richiesta cada sotto il prefix configurato
83
- 4. **Controllo URL Riservati**: Verifica che non sia una risorsa protetta
84
- 5. **Risoluzione Path**: Costruisce il path completo file/directory
85
- 6. **Esistenza Risorsa**: Verifica che file o directory esistano
86
- 7. **Gestione Risorsa**:
87
- - **File**: Template rendering o servizio statico
88
- - **Directory**: Index file o directory listing
89
-
90
- ### Dipendenze
91
-
92
- #### Production
93
- - **mime-types** (^2.1.35): Riconoscimento automatico MIME types
94
-
95
- #### Peer Dependencies
96
- - **koa** (^2.0.0 || >=3.1.2): Framework web minimale per Node.js
97
-
98
- #### Development
99
- - **jest** (^30.2.0): Framework di testing
100
- - **supertest** (^7.2.2): Testing richieste HTTP
101
- - **inquirer** (^13.3.0): CLI interattiva per testing manuale
102
- - **autocannon** (^8.0.0): HTTP benchmarking
103
- - **ejs** (^3.1.10): Template engine per i test
104
-
105
- ---
106
-
107
- ## Installazione
108
-
109
- ### Installazione via npm
110
-
111
- ```bash
112
- npm install koa-classic-server
113
- ```
114
-
115
- ### Installazione via yarn
116
-
117
- ```bash
118
- yarn add koa-classic-server
119
- ```
120
-
121
- ### Requisiti
122
-
123
- - **Node.js**: 12.20+ (raccomandato 14+)
124
- - **Koa**: 2.x
125
-
126
- ---
127
-
128
- ## Avvio Rapido
129
-
130
- ### Esempio Minimale
131
-
132
- ```javascript
133
- const Koa = require('koa');
134
- const koaClassicServer = require('koa-classic-server');
135
-
136
- const app = new Koa();
137
-
138
- // Serve tutti i file dalla cartella "public"
139
- app.use(koaClassicServer(__dirname + '/public'));
140
-
141
- app.listen(3000);
142
- console.log('Server avviato su http://localhost:3000');
143
- ```
144
-
145
- Questa configurazione base:
146
- - Serve tutti i file dalla cartella `public`
147
- - Mostra il contenuto delle directory
148
- - Accetta solo richieste GET
149
- - Nessun URL prefix o percorso riservato
150
-
151
- ### Esempio con Opzioni
152
-
153
- ```javascript
154
- const Koa = require('koa');
155
- const koaClassicServer = require('koa-classic-server');
156
-
157
- const app = new Koa();
158
-
159
- app.use(koaClassicServer(__dirname + '/public', {
160
- dirListing: { enabled: true },
161
- index: 'index.html',
162
- urlPrefix: '/static'
163
- }));
164
-
165
- app.listen(3000);
166
- console.log('Server avviato su http://localhost:3000/static');
167
- ```
168
-
169
- ---
170
-
171
- ## Configurazione
172
-
173
- ### Sintassi
174
-
175
- ```javascript
176
- koaClassicServer(rootDir, options)
177
- ```
178
-
179
- ### Parametri
180
-
181
- | Parametro | Tipo | Obbligatorio | Descrizione |
182
- |-----------|------|--------------|-------------|
183
- | `rootDir` | String | Sì | Path assoluto della directory da servire |
184
- | `options` | Object | No | Oggetto di configurazione |
185
-
186
- ### Opzioni Disponibili
187
-
188
- ```javascript
189
- const options = {
190
- // Metodi HTTP ammessi
191
- // Default: ['GET']
192
- method: ['GET', 'HEAD'],
193
-
194
- // Mostra il contenuto delle directory
195
- // Default: true
196
- dirListing: { enabled: true },
197
-
198
- // Nome del file index da caricare automaticamente nelle directory
199
- // Se presente, viene caricato invece del directory listing
200
- // Default: ''
201
- index: 'index.html',
202
-
203
- // Prefisso URL da rimuovere dal path
204
- // Es: '/public' significa che i file sono accessibili sotto /public/
205
- // Default: ''
206
- urlPrefix: '/public',
207
-
208
- // Array di percorsi riservati (non accessibili da remoto)
209
- // Funziona solo per directory di primo livello
210
- // Default: []
211
- urlsReserved: ['/api', '/admin', '/config'],
212
-
213
- // Configurazione template engine
214
- template: {
215
- // Funzione di rendering personalizzata
216
- // Riceve: ctx (contesto Koa), next (middleware successivo), filePath (path file)
217
- // Default: undefined
218
- render: async (ctx, next, filePath) => {
219
- // Implementazione custom
220
- // Es: ctx.body = await ejs.renderFile(filePath, data);
221
- },
222
-
223
- // Array di estensioni file da processare con template.render
224
- // Se un file ha una di queste estensioni, viene chiamato render()
225
- // Default: []
226
- ext: ['ejs', 'EJS', 'pug', 'html']
227
- }
228
- };
229
- ```
230
-
231
- ### Dettaglio Opzioni
232
-
233
- #### `method` (Array)
234
-
235
- Specifica i metodi HTTP accettati dal middleware. Se una richiesta usa un metodo non presente nell'array, viene passata al middleware successivo.
236
-
237
- ```javascript
238
- // Solo GET
239
- method: ['GET']
240
-
241
- // GET e HEAD (utile per check esistenza file)
242
- method: ['GET', 'HEAD']
243
-
244
- // Multipli metodi (uso avanzato)
245
- method: ['GET', 'HEAD', 'POST']
246
- ```
247
-
248
- #### `dirListing.enabled` (Boolean)
249
-
250
- Controlla se mostrare il contenuto delle directory.
251
-
252
- ```javascript
253
- // Mostra directory listing
254
- dirListing: { enabled: true }
255
-
256
- // Non mostra directory (restituisce "Not Found")
257
- dirListing: { enabled: false }
258
- ```
259
-
260
- #### `index` (String)
261
-
262
- Nome del file da caricare automaticamente quando si accede a una directory.
263
-
264
- ```javascript
265
- // Carica index.html se presente
266
- index: 'index.html'
267
-
268
- // Carica default.htm se presente
269
- index: 'default.htm'
270
-
271
- // Nessun index (mostra sempre directory listing)
272
- index: ''
273
- ```
274
-
275
- **Comportamento:**
276
- 1. Utente accede a `/cartella/`
277
- 2. Se esiste `/cartella/index.html` → viene servito
278
- 3. Altrimenti → mostra directory listing (se `dirListing: { enabled: true }`)
279
-
280
- #### `urlPrefix` (String)
281
-
282
- Prefisso URL che il middleware deve intercettare. Utile per montare il file server sotto un percorso specifico.
283
-
284
- ```javascript
285
- // File accessibili sotto /static
286
- // Es: /static/image.png, /static/css/style.css
287
- urlPrefix: '/static'
288
-
289
- // File accessibili sotto /public
290
- urlPrefix: '/public'
291
-
292
- // Nessun prefix (root del server)
293
- urlPrefix: ''
294
- ```
295
-
296
- **Importante:**
297
- - Il prefix viene rimosso prima di cercare il file nel filesystem
298
- - Richiesta: `/static/image.png` → cerca `rootDir/image.png`
299
-
300
- #### `urlsReserved` (Array)
301
-
302
- Array di percorsi protetti, non accessibili da remoto. Utile per proteggere directory sensibili.
303
-
304
- ```javascript
305
- urlsReserved: ['/config', '/private', '/.env']
306
- ```
307
-
308
- **Limitazioni:**
309
- - Funziona solo per directory di primo livello
310
- - Non supporta percorsi annidati
311
- - Non supporta wildcard
312
-
313
- **Esempio:**
314
- ```javascript
315
- // ✅ Funziona
316
- urlsReserved: ['/admin']
317
- // Blocca: /admin, /admin/file.txt, /admin/sub/file.txt
318
-
319
- // ❌ Non funziona come ci si aspetta
320
- urlsReserved: ['/folder/subfolder']
321
- // Non blocca percorsi annidati
322
- ```
323
-
324
- #### `template.render` (Function)
325
-
326
- Funzione personalizzata per il rendering di template.
327
-
328
- **Firma:**
329
- ```javascript
330
- async function render(ctx, next, filePath) {
331
- // ctx: Contesto Koa
332
- // next: Middleware successivo
333
- // filePath: Path completo del file da renderizzare
334
-
335
- // Esempio EJS
336
- ctx.body = await ejs.renderFile(filePath, {
337
- // Dati passati al template
338
- title: 'My Page',
339
- user: ctx.state.user
340
- });
341
- }
342
- ```
343
-
344
- #### `template.ext` (Array)
345
-
346
- Array di estensioni file che devono essere processate con `template.render`.
347
-
348
- ```javascript
349
- // Solo file .ejs
350
- ext: ['ejs']
351
-
352
- // File .ejs e .EJS (case-sensitive)
353
- ext: ['ejs', 'EJS']
354
-
355
- // Multipli template engine
356
- ext: ['ejs', 'pug', 'html', 'hbs']
357
- ```
358
-
359
- **Comportamento:**
360
- - Se file ha estensione in `ext` E `render` è definito → esegue rendering
361
- - Altrimenti → serve file normalmente
362
-
363
- ---
364
-
365
- ## Esempi d'Uso
366
-
367
- ### 1. Server Base con Directory Listing
368
-
369
- ```javascript
370
- const Koa = require('koa');
371
- const koaClassicServer = require('koa-classic-server');
372
-
373
- const app = new Koa();
374
-
375
- app.use(koaClassicServer(__dirname + '/public', {
376
- dirListing: { enabled: true },
377
- index: 'index.html'
378
- }));
379
-
380
- app.listen(3000);
381
- console.log('Server su http://localhost:3000');
382
- ```
383
-
384
- **Funzionalità:**
385
- - Serve file da `public/`
386
- - Mostra directory listing
387
- - Carica `index.html` automaticamente se presente
388
-
389
- ---
390
-
391
- ### 2. Server con URL Prefix
392
-
393
- ```javascript
394
- const Koa = require('koa');
395
- const koaClassicServer = require('koa-classic-server');
396
-
397
- const app = new Koa();
398
-
399
- // File accessibili sotto /static
400
- app.use(koaClassicServer(__dirname + '/public', {
401
- urlPrefix: '/static',
402
- method: ['GET', 'HEAD'],
403
- dirListing: { enabled: true }
404
- }));
405
-
406
- // Altri middleware per route diverse
407
- app.use(async (ctx) => {
408
- ctx.body = 'Homepage del sito';
409
- });
410
-
411
- app.listen(3000);
412
- ```
413
-
414
- **URL esempi:**
415
- - `http://localhost:3000/` → Homepage
416
- - `http://localhost:3000/static/` → Directory listing di public/
417
- - `http://localhost:3000/static/image.png` → public/image.png
418
-
419
- ---
420
-
421
- ### 3. Server con Cartelle Protette
422
-
423
- ```javascript
424
- const Koa = require('koa');
425
- const koaClassicServer = require('koa-classic-server');
426
-
427
- const app = new Koa();
428
-
429
- app.use(koaClassicServer(__dirname + '/www', {
430
- dirListing: { enabled: true },
431
- // Protegge directory sensibili
432
- urlsReserved: ['/config', '/private', '/.git', '/node_modules']
433
- }));
434
-
435
- app.listen(3000);
436
- ```
437
-
438
- **Comportamento:**
439
- - `/config/` → Not Found (protetto)
440
- - `/private/secret.txt` → Not Found (protetto)
441
- - `/public/file.txt` → Accessibile
442
-
443
- ---
444
-
445
- ### 4. Server con Template Engine (EJS)
446
-
447
- ```javascript
448
- const Koa = require('koa');
449
- const koaClassicServer = require('koa-classic-server');
450
- const ejs = require('ejs');
451
-
452
- const app = new Koa();
453
-
454
- app.use(koaClassicServer(__dirname + '/views', {
455
- dirListing: { enabled: false },
456
- template: {
457
- render: async (ctx, next, filePath) => {
458
- // Rendering EJS con dati
459
- ctx.body = await ejs.renderFile(filePath, {
460
- title: 'My App',
461
- filePath: filePath,
462
- href: ctx.href,
463
- query: ctx.query,
464
- user: ctx.state.user || 'Guest'
465
- });
466
- },
467
- ext: ['ejs', 'EJS']
468
- }
469
- }));
470
-
471
- app.listen(3000);
472
- console.log('Server con EJS su http://localhost:3000');
473
- ```
474
-
475
- ---
476
-
477
- ### 5. Server Multi-Directory
478
-
479
- ```javascript
480
- const Koa = require('koa');
481
- const koaClassicServer = require('koa-classic-server');
482
-
483
- const app = new Koa();
484
-
485
- // File statici pubblici
486
- app.use(koaClassicServer(__dirname + '/public', {
487
- urlPrefix: '/public',
488
- dirListing: { enabled: true }
489
- }));
490
-
491
- // Asset (CSS, JS, images)
492
- app.use(koaClassicServer(__dirname + '/assets', {
493
- urlPrefix: '/assets',
494
- dirListing: { enabled: false }
495
- }));
496
-
497
- // Download area
498
- app.use(koaClassicServer(__dirname + '/downloads', {
499
- urlPrefix: '/downloads',
500
- dirListing: { enabled: true },
501
- index: 'README.txt'
502
- }));
503
-
504
- app.listen(3000);
505
- ```
506
-
507
- ---
508
-
509
- ### 6. Configurazione Completa Produzione
510
-
511
- ```javascript
512
- const Koa = require('koa');
513
- const koaClassicServer = require('koa-classic-server');
514
- const ejs = require('ejs');
515
- const path = require('path');
516
-
517
- const app = new Koa();
518
-
519
- // Logging middleware
520
- app.use(async (ctx, next) => {
521
- const start = Date.now();
522
- await next();
523
- const ms = Date.now() - start;
524
- console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
525
- });
526
-
527
- // Error handling
528
- app.on('error', (err, ctx) => {
529
- console.error('Server error:', err);
530
- });
531
-
532
- // Template render function
533
- const templateRender = async (ctx, next, filePath) => {
534
- try {
535
- ctx.body = await ejs.renderFile(filePath, {
536
- filePath: filePath,
537
- href: ctx.href,
538
- query: ctx.query,
539
- method: ctx.method,
540
- env: process.env.NODE_ENV
541
- });
542
- } catch (err) {
543
- console.error('Template render error:', err);
544
- ctx.status = 500;
545
- ctx.body = 'Template Error';
546
- }
547
- };
548
-
549
- // File server
550
- app.use(koaClassicServer(path.join(__dirname, 'public'), {
551
- method: ['GET', 'HEAD'],
552
- dirListing: { enabled: process.env.NODE_ENV !== 'production' },
553
- index: 'index.html',
554
- urlPrefix: '/files',
555
- urlsReserved: ['/admin', '/private', '/config', '/.env'],
556
- template: {
557
- render: templateRender,
558
- ext: ['ejs', 'html']
559
- }
560
- }));
561
-
562
- // 404 fallback
563
- app.use(async (ctx) => {
564
- ctx.status = 404;
565
- ctx.body = 'Pagina non trovata';
566
- });
567
-
568
- const port = process.env.PORT || 3000;
569
- app.listen(port, () => {
570
- console.log(`Server avviato su http://localhost:${port}`);
571
- console.log(`Ambiente: ${process.env.NODE_ENV || 'development'}`);
572
- });
573
- ```
574
-
575
- ---
576
-
577
- ### 7. Integrazione con Router Koa
578
-
579
- ```javascript
580
- const Koa = require('koa');
581
- const Router = require('@koa/router');
582
- const koaClassicServer = require('koa-classic-server');
583
-
584
- const app = new Koa();
585
- const router = new Router();
586
-
587
- // API routes
588
- router.get('/api/users', (ctx) => {
589
- ctx.body = { users: ['Alice', 'Bob'] };
590
- });
591
-
592
- router.post('/api/login', (ctx) => {
593
- ctx.body = { token: 'xyz123' };
594
- });
595
-
596
- // Monta il router
597
- app.use(router.routes());
598
- app.use(router.allowedMethods());
599
-
600
- // File statici (dopo le route API)
601
- app.use(koaClassicServer(__dirname + '/public', {
602
- dirListing: { enabled: true }
603
- }));
604
-
605
- app.listen(3000);
606
- ```
607
-
608
- **Ordine importante:**
609
- 1. Router con API dinamiche
610
- 2. File server statico
611
- 3. Questo permette alle API di avere precedenza sui file
612
-
613
- ---
614
-
615
- ## API Reference
616
-
617
- ### koaClassicServer(rootDir, options)
618
-
619
- Crea e restituisce un middleware Koa per servire file statici.
620
-
621
- **Parametri:**
622
-
623
- - `rootDir` (String, required): Path assoluto directory root
624
- - `options` (Object, optional): Oggetto configurazione
625
-
626
- **Restituisce:**
627
-
628
- - Function: Middleware Koa `async (ctx, next) => {...}`
629
-
630
- **Esempio:**
631
-
632
- ```javascript
633
- const middleware = koaClassicServer('/path/to/files', {
634
- dirListing: { enabled: true }
635
- });
636
-
637
- app.use(middleware);
638
- ```
639
-
640
- ### Oggetto Options
641
-
642
- Vedi sezione [Configurazione](#configurazione) per dettagli completi.
643
-
644
- ### Context (ctx) nel Template Render
645
-
646
- Quando usi `template.render`, hai accesso completo al contesto Koa:
647
-
648
- ```javascript
649
- template: {
650
- render: async (ctx, next, filePath) => {
651
- // ctx.request - Oggetto richiesta
652
- console.log(ctx.request.method); // 'GET', 'POST', etc.
653
- console.log(ctx.request.url); // '/path/to/file'
654
- console.log(ctx.request.header); // Headers HTTP
655
-
656
- // ctx.response - Oggetto risposta
657
- ctx.response.type = 'text/html';
658
- ctx.response.body = '<html>...</html>';
659
-
660
- // Shortcuts
661
- console.log(ctx.method); // 'GET'
662
- console.log(ctx.url); // '/path/to/file'
663
- console.log(ctx.href); // 'http://localhost:3000/path/to/file'
664
- console.log(ctx.query); // { key: 'value' }
665
- console.log(ctx.path); // '/path/to/file'
666
-
667
- // State (dati condivisi tra middleware)
668
- console.log(ctx.state.user);
669
-
670
- // Cookies
671
- console.log(ctx.cookies.get('session'));
672
-
673
- // filePath - Path completo del file
674
- console.log(filePath); // '/var/www/public/page.ejs'
675
- }
676
- }
677
- ```
678
-
679
- ---
680
-
681
- ## Comportamento del Middleware
682
-
683
- ### Gestione delle Directory
684
-
685
- #### Caso 1: dirListing.enabled = true, index presente
686
-
687
- ```
688
- Richiesta: GET /cartella/
689
- Filesystem:
690
- /cartella/index.html ✓ (esiste)
691
- /cartella/file1.txt
692
-
693
- Risultato: Serve /cartella/index.html
694
- ```
695
-
696
- #### Caso 2: dirListing.enabled = true, index assente
697
-
698
- ```
699
- Richiesta: GET /cartella/
700
- Filesystem:
701
- /cartella/file1.txt
702
- /cartella/file2.jpg
703
-
704
- Risultato: Mostra directory listing HTML
705
- ```
706
-
707
- #### Caso 3: dirListing.enabled = false
708
-
709
- ```
710
- Richiesta: GET /cartella/
711
-
712
- Risultato: "Not Found" (indipendentemente da index)
713
- ```
714
-
715
- ### Gestione dei File
716
-
717
- #### File Normale
718
-
719
- ```
720
- Richiesta: GET /document.pdf
721
- Filesystem: /document.pdf (esiste)
722
-
723
- Risultato:
724
- Status: 200
725
- Content-Type: application/pdf
726
- Content-Disposition: inline; filename=document.pdf
727
- Body: [file stream]
728
- ```
729
-
730
- #### File Template
731
-
732
- ```
733
- Richiesta: GET /page.ejs
734
- Filesystem: /page.ejs (esiste)
735
- Config: template.ext = ['ejs'], template.render definito
736
-
737
- Risultato:
738
- Status: 200
739
- Content-Type: [settato da template.render]
740
- Body: [output renderizzato]
741
- ```
742
-
743
- ### URL Riservati
744
-
745
- ```
746
- Config: urlsReserved = ['/admin', '/private']
747
-
748
- Richiesta: GET /admin/config.json
749
- Risultato: Passa a middleware successivo (salta koa-classic-server)
750
-
751
- Richiesta: GET /admin/
752
- Risultato: Directory listing mostra "DIR BUT RESERVED"
753
-
754
- Richiesta: GET /public/file.txt
755
- Risultato: File servito normalmente
756
- ```
757
-
758
- ### Parent Directory Navigation
759
-
760
- Nel directory listing, viene sempre mostrato link alla parent directory, tranne nella root:
761
-
762
- ```html
763
- <!-- Root: http://localhost:3000/ -->
764
- <!-- Nessun parent directory link -->
765
-
766
- <!-- Sub-directory: http://localhost:3000/folder/ -->
767
- <table>
768
- <tr><td><a href="http://localhost:3000"><b>.. Parent Directory</b></a></td><td>DIR</td></tr>
769
- <!-- altri file... -->
770
- </table>
771
- ```
772
-
773
- ### Normalizzazione URL
774
-
775
- Il middleware normalizza automaticamente gli URL:
776
-
777
- ```
778
- http://localhost:3000/folder/ → http://localhost:3000/folder
779
- http://localhost:3000/file.txt/ → http://localhost:3000/file.txt
780
- ```
781
-
782
- Questo assicura comportamento coerente indipendentemente dal trailing slash.
783
-
784
- ### Gestione dei Link Simbolici (Symlink)
785
-
786
- Il middleware supporta completamente i link simbolici (symlink). Questo è fondamentale in ambienti dove i file serviti sono symlink anziché file regolari, come ad esempio:
787
-
788
- - **NixOS con buildFHSEnv** (chroot-like): i file nella directory www/ appaiono come symlink al Nix store
789
- - **Docker bind mounts**: i file montati possono risultare come symlink
790
- - **npm link**: i pacchetti linkati sono symlink
791
- - **Deploy Capistrano-style**: la directory `current` è un symlink alla release attiva
792
-
793
- #### Comportamento
794
-
795
- Il middleware segue i symlink in modo trasparente tramite `fs.promises.stat()` (che risolve il symlink al target reale), ma solo quando `dirent.isSymbolicLink()` è `true`. Per i file regolari non viene effettuata alcuna chiamata aggiuntiva (zero overhead).
796
-
797
- #### Risoluzione Index File
798
-
799
- Quando il middleware cerca un file index in una directory (opzione `index`), i symlink che puntano a file regolari vengono inclusi nella ricerca, sia con pattern stringa che RegExp:
800
-
801
- ```
802
- Directory:
803
- index.ejs → /nix/store/.../index.ejs (symlink a file)
804
- style.css (file regolare)
805
-
806
- Config: index: ['index.ejs']
807
- Risultato: Serve index.ejs attraverso il symlink ✓
808
- ```
809
-
810
- #### Directory Listing
811
-
812
- Nel directory listing, i symlink sono identificati visivamente:
813
-
814
- | Caso | Indicatore | Cliccabile | Tipo mostrato |
815
- |------|-----------|------------|---------------|
816
- | Symlink a file | `( Symlink )` | Sì | MIME type del target |
817
- | Symlink a directory | `( Symlink )` | Sì | `DIR` |
818
- | Symlink rotto/circolare | `( Broken Symlink )` | No | `unknown` |
819
- | File/directory regolare | nessuno | Sì | tipo reale |
820
-
821
- #### Casi Limite
822
-
823
- - **Broken symlink** (target inesistente): il GET diretto restituisce 404; nel listing il nome appare senza link
824
- - **Symlink circolare** (A → B → A): trattato come broken symlink, nessun loop infinito
825
- - **Symlink a directory**: navigabile come una directory regolare, i file al suo interno sono accessibili
826
-
827
- ### MIME Types
828
-
829
- MIME types riconosciuti automaticamente tramite estensione file:
830
-
831
- ```javascript
832
- .html → text/html
833
- .css → text/css
834
- .js → application/javascript
835
- .json → application/json
836
- .png → image/png
837
- .jpg → image/jpeg
838
- .pdf → application/pdf
839
- .txt → text/plain
840
- // ... e molti altri
841
- ```
842
-
843
- Se MIME type non riconosciuto:
844
- ```
845
- Estensione sconosciuta → 'unknow' (nel directory listing)
846
- ```
847
-
848
- ---
849
-
850
- ## Testing
851
-
852
- ### Test Automatici (Jest)
853
-
854
- Il progetto include una suite completa di test.
855
-
856
- ```bash
857
- # Esegui tutti i test
858
- npm test
859
-
860
- # Test con coverage
861
- npm test -- --coverage
862
-
863
- # Test in watch mode
864
- npm test -- --watch
865
- ```
866
-
867
- ### Test Coverage
868
-
869
- I test coprono:
870
-
871
- - ✅ Servizio file statici
872
- - ✅ Directory listing
873
- - ✅ URL prefix
874
- - ✅ URL riservati
875
- - ✅ File index
876
- - ✅ Percorsi non esistenti
877
- - ✅ Metodi HTTP
878
- - ✅ Caratteri speciali nei nomi file
879
- - ⚠️ Template rendering (parziale)
880
- - ⚠️ Directory listing completo (parziale)
881
-
882
- ### Test Manuali Interattivi
883
-
884
- Per testare manualmente diverse configurazioni:
885
-
886
- ```bash
887
- npm run loadConfig
888
- ```
889
-
890
- Questo comando:
891
- 1. Mostra menu interattivo con configurazioni predefinite
892
- 2. Avvia server con configurazione scelta
893
- 3. Permette test manuale via browser
894
-
895
- **Configurazioni disponibili:**
896
- - Test generico base
897
- - Test con URL prefix `/public`
898
- - Test con file index
899
- - Test con percorsi riservati
900
-
901
- ### Struttura Test
902
-
903
- ```
904
- __tests__/
905
- ├── index.test.js # Suite principale
906
- └── publicWwwTest/ # Cartella test con file/directory campione
907
- ├── file.txt
908
- ├── image.png
909
- ├── subfolder/
910
- └── ...
911
- ```
912
-
913
- ### Scrivere Nuovi Test
914
-
915
- Esempio test personalizzato:
916
-
917
- ```javascript
918
- const supertest = require('supertest');
919
- const koaClassicServer = require('../index.cjs');
920
- const Koa = require('koa');
921
-
922
- describe('My custom test', () => {
923
- let app, server;
924
-
925
- beforeAll(() => {
926
- app = new Koa();
927
- app.use(koaClassicServer(__dirname + '/test-files', {
928
- dirListing: { enabled: true }
929
- }));
930
- server = app.listen();
931
- });
932
-
933
- test('should serve HTML file', async () => {
934
- const res = await supertest(server).get('/index.html');
935
- expect(res.status).toBe(200);
936
- expect(res.type).toMatch(/text\/html/);
937
- });
938
-
939
- afterAll(() => {
940
- server.close();
941
- });
942
- });
943
- ```
944
-
945
- ---
946
-
947
- ## Troubleshooting
948
-
949
- ### File non viene servito
950
-
951
- **Problema:** Richiesta a file esistente restituisce 404
952
-
953
- **Soluzioni:**
954
-
955
- 1. Verifica path assoluto:
956
- ```javascript
957
- // ❌ Path relativo
958
- koaClassicServer('./public')
959
-
960
- // ✅ Path assoluto
961
- koaClassicServer(__dirname + '/public')
962
- koaClassicServer(path.join(__dirname, 'public'))
963
- ```
964
-
965
- 2. Controlla URL prefix:
966
- ```javascript
967
- // Config
968
- urlPrefix: '/static'
969
-
970
- // ❌ URL sbagliato
971
- http://localhost:3000/file.txt
972
-
973
- // ✅ URL corretto
974
- http://localhost:3000/static/file.txt
975
- ```
976
-
977
- 3. Verifica URL riservati:
978
- ```javascript
979
- // Config
980
- urlsReserved: ['/protected']
981
-
982
- // ❌ File in cartella protetta
983
- GET /protected/file.txt → 404
984
-
985
- // ✅ File fuori da cartella protetta
986
- GET /public/file.txt → 200
987
- ```
988
-
989
- ---
990
-
991
- ### Template non viene renderizzato
992
-
993
- **Problema:** File template viene servito come testo invece di essere renderizzato
994
-
995
- **Soluzioni:**
996
-
997
- 1. Verifica estensione in `template.ext`:
998
- ```javascript
999
- // File: page.ejs
1000
-
1001
- // ❌ Estensione mancante
1002
- template: {
1003
- render: renderFunction,
1004
- ext: ['html'] // manca 'ejs'
1005
- }
1006
-
1007
- // ✅ Estensione presente
1008
- template: {
1009
- render: renderFunction,
1010
- ext: ['ejs', 'EJS']
1011
- }
1012
- ```
1013
-
1014
- 2. Verifica `template.render` sia funzione:
1015
- ```javascript
1016
- // ❌ render non definito
1017
- template: {
1018
- ext: ['ejs']
1019
- // render mancante!
1020
- }
1021
-
1022
- // ✅ render definito
1023
- template: {
1024
- render: async (ctx, next, filePath) => {
1025
- ctx.body = await ejs.renderFile(filePath, {});
1026
- },
1027
- ext: ['ejs']
1028
- }
1029
- ```
1030
-
1031
- 3. Installa template engine:
1032
- ```bash
1033
- npm install ejs
1034
- ```
1035
-
1036
- ---
1037
-
1038
- ### Directory listing non funziona
1039
-
1040
- **Problema:** Accesso a directory restituisce "Not Found"
1041
-
1042
- **Soluzione:**
1043
-
1044
- ```javascript
1045
- // ❌ dirListing.enabled disabilitato
1046
- dirListing: { enabled: false }
1047
-
1048
- // ✅ dirListing.enabled abilitato
1049
- dirListing: { enabled: true }
1050
- ```
1051
-
1052
- ---
1053
-
1054
- ### URL riservati non proteggono
1055
-
1056
- **Problema:** File in cartelle riservate sono accessibili
1057
-
1058
- **Cause comuni:**
1059
-
1060
- 1. **Percorsi annidati non supportati:**
1061
- ```javascript
1062
- // ❌ Non funziona
1063
- urlsReserved: ['/path/to/protected']
1064
-
1065
- // ✅ Solo primo livello
1066
- urlsReserved: ['/protected']
1067
- ```
1068
-
1069
- 2. **Spazi nei nomi (bug noto):**
1070
- ```javascript
1071
- // ⚠️ Problematico
1072
- urlsReserved: ['/percorso riservato']
1073
-
1074
- // ✅ Usa underscore o trattini
1075
- urlsReserved: ['/percorso_riservato', '/percorso-riservato']
1076
- ```
1077
-
1078
- ---
1079
-
1080
- ### Metodo HTTP non accettato
1081
-
1082
- **Problema:** Richieste POST/PUT/DELETE non funzionano
1083
-
1084
- **Soluzione:**
1085
-
1086
- ```javascript
1087
- // ❌ Solo GET (default)
1088
- method: ['GET']
1089
-
1090
- // ✅ Aggiungi metodi necessari
1091
- method: ['GET', 'POST', 'PUT', 'DELETE']
1092
- ```
1093
-
1094
- **Nota:** Di solito file server necessita solo GET e HEAD
1095
-
1096
- ---
1097
-
1098
- ### Caratteri speciali in nomi file
1099
-
1100
- **Problema:** File con spazi o caratteri speciali non accessibili
1101
-
1102
- **Soluzione:**
1103
-
1104
- Il middleware gestisce automaticamente URI encoding. Assicurati che il client codifichi correttamente:
1105
-
1106
- ```javascript
1107
- // ❌ Non encodato
1108
- GET /my file.txt
1109
-
1110
- // ✅ Encodato (automatico nei browser)
1111
- GET /my%20file.txt
1112
- ```
1113
-
1114
- In JavaScript:
1115
- ```javascript
1116
- const filename = 'my file.txt';
1117
- const url = '/' + encodeURIComponent(filename);
1118
- // url = '/my%20file.txt'
1119
- ```
1120
-
1121
- ---
1122
-
1123
- ### Performance con molti file
1124
-
1125
- **Problema:** Directory listing lento con migliaia di file
1126
-
1127
- **Soluzioni:**
1128
-
1129
- 1. Disabilita directory listing:
1130
- ```javascript
1131
- dirListing: { enabled: false }
1132
- ```
1133
-
1134
- 2. Usa file index:
1135
- ```javascript
1136
- dirListing: { enabled: true },
1137
- index: 'index.html' // Carica index invece di listing
1138
- ```
1139
-
1140
- 3. Considera soluzioni alternative per grandi quantità di file (es. nginx, CDN)
1141
-
1142
- ---
1143
-
1144
- ## Problemi Noti
1145
-
1146
- ### 1. Status Code 404 Mancante
1147
-
1148
- **Problema:** Quando una risorsa non viene trovata, lo status code è 200 invece di 404
1149
-
1150
- **File:** `index.cjs:110`
1151
-
1152
- ```javascript
1153
- if (!fs.existsSync(toOpen)) {
1154
- ctx.body = requestedUrlNotFound();
1155
- // Manca: ctx.status = 404;
1156
- return;
1157
- }
1158
- ```
1159
-
1160
- **Workaround:**
1161
- ```javascript
1162
- // Middleware successivo per gestire 404
1163
- app.use(async (ctx) => {
1164
- if (!ctx.body) {
1165
- ctx.status = 404;
1166
- ctx.body = 'Not Found';
1167
- }
1168
- });
1169
- ```
1170
-
1171
- ---
1172
-
1173
- ### 2. URL Riservati con Spazi
1174
-
1175
- **Problema:** Il controllo URL riservati non funziona con percorsi contenenti spazi
1176
-
1177
- **File:** `index.cjs:87-96`
1178
-
1179
- **Limitazione:** Problemi con URI encoding negli URL riservati
1180
-
1181
- **Workaround:** Evita spazi nei nomi delle cartelle riservate:
1182
- ```javascript
1183
- // ❌ Non funziona
1184
- urlsReserved: ['/percorso riservato']
1185
-
1186
- // ✅ Funziona
1187
- urlsReserved: ['/percorso_riservato', '/percorso-riservato']
1188
- ```
1189
-
1190
- ---
1191
-
1192
- ### 3. URL Riservati Solo Primo Livello
1193
-
1194
- **Problema:** URL riservati funzionano solo per directory di primo livello, non per percorsi annidati
1195
-
1196
- **File:** `index.cjs:87-96`
1197
-
1198
- **Limitazione:** Design della funzionalità
1199
-
1200
- ```javascript
1201
- // ❌ Non supportato
1202
- urlsReserved: ['/path/to/protected']
1203
-
1204
- // ✅ Supportato
1205
- urlsReserved: ['/protected']
1206
- // Blocca: /protected, /protected/file.txt, /protected/sub/file.txt
1207
- ```
1208
-
1209
- ---
1210
-
1211
- ### 4. Test Coverage Incompleto
1212
-
1213
- **Problema:** Test non coprono completamente:
1214
- - Contenuto HTML del directory listing
1215
- - Tutti i casi di template rendering
1216
-
1217
- **File:** `__tests__/index.test.js:1-11`
1218
-
1219
- **Impatto:** Possibili bug non rilevati in queste aree
1220
-
1221
- ---
1222
-
1223
- ### 5. Single Index File
1224
-
1225
- **Problema:** Supporto per un solo nome file index, non array di fallback
1226
-
1227
- **Limitazione:** Design attuale
1228
-
1229
- ```javascript
1230
- // ❌ Non supportato
1231
- index: ['index.html', 'index.htm', 'default.html']
1232
-
1233
- // ✅ Supportato
1234
- index: 'index.html'
1235
- ```
1236
-
1237
- **Workaround:** Standardizza su un solo nome file index
1238
-
1239
- ---
1240
-
1241
- ## Best Practices
1242
-
1243
- ### 1. Sicurezza
1244
-
1245
- #### Usa Path Assoluti
1246
- ```javascript
1247
- // ✅ Corretto
1248
- const path = require('path');
1249
- app.use(koaClassicServer(path.join(__dirname, 'public')));
1250
-
1251
- // ❌ Evita path relativi
1252
- app.use(koaClassicServer('./public'));
1253
- ```
1254
-
1255
- #### Proteggi Directory Sensibili
1256
- ```javascript
1257
- app.use(koaClassicServer(__dirname + '/www', {
1258
- urlsReserved: [
1259
- '/config',
1260
- '/.env',
1261
- '/.git',
1262
- '/node_modules',
1263
- '/private',
1264
- '/admin'
1265
- ]
1266
- }));
1267
- ```
1268
-
1269
- #### Limita Metodi HTTP
1270
- ```javascript
1271
- // Solo lettura
1272
- method: ['GET', 'HEAD']
1273
-
1274
- // Evita metodi di scrittura per file server
1275
- // method: ['POST', 'PUT', 'DELETE'] ❌
1276
- ```
1277
-
1278
- #### Disabilita Directory Listing in Produzione
1279
- ```javascript
1280
- app.use(koaClassicServer(rootDir, {
1281
- dirListing: { enabled: process.env.NODE_ENV !== 'production' },
1282
- index: 'index.html'
1283
- }));
1284
- ```
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
-
1513
- ---
1514
-
1515
- ### 2. Performance
1516
-
1517
- #### Usa Variabili d'Ambiente
1518
- ```javascript
1519
- const isProduction = process.env.NODE_ENV === 'production';
1520
-
1521
- app.use(koaClassicServer(rootDir, {
1522
- dirListing: { enabled: !isProduction },
1523
- method: ['GET', 'HEAD']
1524
- }));
1525
- ```
1526
-
1527
- #### Middleware Logging Efficiente
1528
- ```javascript
1529
- // Solo in development
1530
- if (process.env.NODE_ENV !== 'production') {
1531
- app.use(async (ctx, next) => {
1532
- console.log(`${ctx.method} ${ctx.url}`);
1533
- await next();
1534
- });
1535
- }
1536
- ```
1537
-
1538
- #### Cache Headers (middleware aggiuntivo)
1539
- ```javascript
1540
- // Aggiungi cache headers per file statici
1541
- app.use(async (ctx, next) => {
1542
- await next();
1543
- if (ctx.method === 'GET' && ctx.status === 200) {
1544
- ctx.set('Cache-Control', 'public, max-age=86400'); // 24h
1545
- }
1546
- });
1547
-
1548
- app.use(koaClassicServer(rootDir));
1549
- ```
1550
-
1551
- ---
1552
-
1553
- ### 3. Organizzazione Codice
1554
-
1555
- #### Separa Configurazione
1556
- ```javascript
1557
- // config/fileServer.js
1558
- const path = require('path');
1559
-
1560
- module.exports = {
1561
- rootDir: path.join(__dirname, '../public'),
1562
- options: {
1563
- method: ['GET', 'HEAD'],
1564
- dirListing: { enabled: true },
1565
- index: 'index.html',
1566
- urlPrefix: '/static',
1567
- urlsReserved: ['/admin', '/config']
1568
- }
1569
- };
1570
-
1571
- // app.js
1572
- const fileServerConfig = require('./config/fileServer');
1573
- app.use(koaClassicServer(
1574
- fileServerConfig.rootDir,
1575
- fileServerConfig.options
1576
- ));
1577
- ```
1578
-
1579
- #### Template Rendering Modulare
1580
- ```javascript
1581
- // lib/templateRenderer.js
1582
- const ejs = require('ejs');
1583
-
1584
- module.exports = async function renderTemplate(ctx, next, filePath) {
1585
- try {
1586
- ctx.body = await ejs.renderFile(filePath, {
1587
- title: getPageTitle(filePath),
1588
- user: ctx.state.user,
1589
- ...ctx.state.templateData
1590
- });
1591
- } catch (error) {
1592
- console.error('Template error:', error);
1593
- ctx.status = 500;
1594
- ctx.body = 'Rendering Error';
1595
- }
1596
- };
1597
-
1598
- function getPageTitle(filePath) {
1599
- // Logica per estrarre titolo
1600
- return 'My Page';
1601
- }
1602
-
1603
- // app.js
1604
- const templateRenderer = require('./lib/templateRenderer');
1605
-
1606
- app.use(koaClassicServer(rootDir, {
1607
- template: {
1608
- render: templateRenderer,
1609
- ext: ['ejs', 'html']
1610
- }
1611
- }));
1612
- ```
1613
-
1614
- ---
1615
-
1616
- ### 4. Error Handling
1617
-
1618
- #### Global Error Handler
1619
- ```javascript
1620
- app.on('error', (err, ctx) => {
1621
- console.error('Server error:', {
1622
- error: err.message,
1623
- stack: err.stack,
1624
- url: ctx.url,
1625
- method: ctx.method,
1626
- ip: ctx.ip
1627
- });
1628
- });
1629
- ```
1630
-
1631
- #### Try-Catch in Template Render
1632
- ```javascript
1633
- template: {
1634
- render: async (ctx, next, filePath) => {
1635
- try {
1636
- ctx.body = await ejs.renderFile(filePath, data);
1637
- } catch (error) {
1638
- console.error('Render error:', error);
1639
- ctx.status = 500;
1640
- ctx.body = 'Template Error';
1641
- }
1642
- },
1643
- ext: ['ejs']
1644
- }
1645
- ```
1646
-
1647
- #### 404 Fallback
1648
- ```javascript
1649
- // Ultimo middleware
1650
- app.use(async (ctx) => {
1651
- ctx.status = 404;
1652
- ctx.type = 'html';
1653
- ctx.body = `
1654
- <!DOCTYPE html>
1655
- <html>
1656
- <head><title>404</title></head>
1657
- <body>
1658
- <h1>Pagina Non Trovata</h1>
1659
- <p>La risorsa richiesta non esiste.</p>
1660
- </body>
1661
- </html>
1662
- `;
1663
- });
1664
- ```
1665
-
1666
- ---
1667
-
1668
- ### 5. Development vs Production
1669
-
1670
- ```javascript
1671
- const Koa = require('koa');
1672
- const koaClassicServer = require('koa-classic-server');
1673
- const path = require('path');
1674
-
1675
- const app = new Koa();
1676
- const isDev = process.env.NODE_ENV !== 'production';
1677
-
1678
- // Logging solo in development
1679
- if (isDev) {
1680
- app.use(async (ctx, next) => {
1681
- const start = Date.now();
1682
- await next();
1683
- console.log(`${ctx.method} ${ctx.url} - ${Date.now() - start}ms`);
1684
- });
1685
- }
1686
-
1687
- // File server con configurazione ambiente-specifica
1688
- app.use(koaClassicServer(path.join(__dirname, 'public'), {
1689
- dirListing: { enabled: isDev }, // Solo in dev
1690
- index: 'index.html',
1691
- urlPrefix: isDev ? '' : '/static', // Prefix solo in prod
1692
- urlsReserved: ['/admin', '/config', '/.env'],
1693
- template: {
1694
- render: isDev ? devTemplateRender : prodTemplateRender,
1695
- ext: ['ejs']
1696
- }
1697
- }));
1698
-
1699
- // Error details solo in development
1700
- app.on('error', (err, ctx) => {
1701
- if (isDev) {
1702
- console.error('Error details:', err);
1703
- } else {
1704
- console.error('Error:', err.message);
1705
- }
1706
- });
1707
-
1708
- app.listen(process.env.PORT || 3000);
1709
- ```
1710
-
1711
- ---
1712
-
1713
- ### 6. Testing
1714
-
1715
- #### Testa Configurazioni Diverse
1716
- ```javascript
1717
- // __tests__/server.test.js
1718
- const configs = [
1719
- { name: 'base', options: {} },
1720
- { name: 'with-prefix', options: { urlPrefix: '/public' } },
1721
- { name: 'no-listing', options: { dirListing: { enabled: false } } }
1722
- ];
1723
-
1724
- configs.forEach(({ name, options }) => {
1725
- describe(`Config: ${name}`, () => {
1726
- let server;
1727
-
1728
- beforeAll(() => {
1729
- const app = new Koa();
1730
- app.use(koaClassicServer(testDir, options));
1731
- server = app.listen();
1732
- });
1733
-
1734
- test('serves files', async () => {
1735
- // test...
1736
- });
1737
-
1738
- afterAll(() => server.close());
1739
- });
1740
- });
1741
- ```
1742
-
1743
- ---
1744
-
1745
- ### 7. Documentazione
1746
-
1747
- #### Commenta Configurazioni Complesse
1748
- ```javascript
1749
- app.use(koaClassicServer(__dirname + '/public', {
1750
- // Mostra directory solo in development per sicurezza
1751
- dirListing: { enabled: process.env.NODE_ENV !== 'production' },
1752
-
1753
- // Protegge configurazioni sensibili
1754
- // Nota: funziona solo per directory di primo livello
1755
- urlsReserved: ['/config', '/private'],
1756
-
1757
- // Template EJS per pagine dinamiche
1758
- // Rendering con dati utente e query params
1759
- template: {
1760
- render: ejsRenderer,
1761
- ext: ['ejs']
1762
- }
1763
- }));
1764
- ```
1765
-
1766
- ---
1767
-
1768
- ## Informazioni Aggiuntive
1769
-
1770
- ### Versione
1771
- **2.4.0**
1772
-
1773
- ### Autore
1774
- **Italo Paesano**
1775
-
1776
- ### Licenza
1777
- **MIT**
1778
-
1779
- ### Keywords
1780
- - file
1781
- - server
1782
- - koa
1783
- - middleware
1784
- - static
1785
- - apache
1786
-
1787
- ### Compatibilità
1788
-
1789
- #### Node.js
1790
- - **Minimo:** 12.20+
1791
- - **Raccomandato:** 14+
1792
- - **Testato:** 14, 16, 18, 20
1793
-
1794
- #### Koa
1795
- - **Versione:** 2.x
1796
-
1797
- ### Conditional Exports
1798
-
1799
- Il modulo usa conditional exports per supportare sia CommonJS che ES Modules:
1800
-
1801
- ```json
1802
- {
1803
- "main": "index.cjs",
1804
- "exports": {
1805
- "import": "./index.mjs",
1806
- "require": "./index.cjs"
1807
- }
1808
- }
1809
- ```
1810
-
1811
- Questo permette:
1812
- ```javascript
1813
- // CommonJS
1814
- const koaClassicServer = require('koa-classic-server');
1815
-
1816
- // ES Modules
1817
- import koaClassicServer from 'koa-classic-server';
1818
- ```
1819
-
1820
- ### Repository
1821
-
1822
- Segnala bug o richiedi funzionalità tramite GitHub Issues.
1823
-
1824
- ### Contributing
1825
-
1826
- Contributi benvenuti! Per favore:
1827
- 1. Fork del repository
1828
- 2. Crea branch per feature (`git checkout -b feature/AmazingFeature`)
1829
- 3. Commit modifiche (`git commit -m 'Add AmazingFeature'`)
1830
- 4. Push al branch (`git push origin feature/AmazingFeature`)
1831
- 5. Apri Pull Request
1832
-
1833
- ### Changelog
1834
-
1835
- #### v1.2.0
1836
- - Fix: supporto completo link simbolici in `findIndexFile()` e directory listing
1837
- - Nuovi helper `isFileOrSymlinkToFile()` / `isDirOrSymlinkToDir()` (zero overhead per file regolari)
1838
- - Directory listing: indicatori `( Symlink )` e `( Broken Symlink )`, tipo effettivo (MIME/DIR) per symlink
1839
- - 17 nuovi test per tutti gli scenari symlink (NixOS, Docker, npm link, broken, circular)
1840
-
1841
- #### v1.1.0
1842
- - Supporto conditional exports
1843
- - Test suite completa
1844
-
1845
- ---
1846
-
1847
- ## Risorse Aggiuntive
1848
-
1849
- ### Link Utili
1850
-
1851
- - [Documentazione Koa](https://koajs.com/)
1852
- - [MIME Types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
1853
- - [Node.js Path Module](https://nodejs.org/api/path.html)
1854
- - [EJS Documentation](https://ejs.co/)
1855
-
1856
- ### Esempi Completi
1857
-
1858
- Repository con esempi completi disponibile nella cartella `customTest/` del progetto.
1859
-
1860
- ---
1861
-
1862
- **Documentazione generata per koa-classic-server v2.4.0**
1863
-
1864
- *Ultimo aggiornamento: 2026-02-28*