kempo-server 1.7.1 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -213,6 +213,11 @@ export default async function(request, response) {
213
213
 
214
214
  To configure the server, create a configuration file (by default `.config.json`) within the root directory of your server (`public` in the start example [above](#getting-started)). You can specify a different configuration file using the `--config` flag.
215
215
 
216
+ **Important:**
217
+ - When using a relative path for the `--config` flag, the config file must be located within the server root directory
218
+ - When using an absolute path for the `--config` flag, the config file can be located anywhere on the filesystem
219
+ - The server will throw an error if you attempt to use a relative config file path that points outside the root directory
220
+
216
221
  This json file can have any of the following properties, any property not defined will use the "Default Config".
217
222
 
218
223
  - [allowedMimes](#allowedmimes)
@@ -558,35 +563,41 @@ An array of regex patterns for paths that should not trigger a file system resca
558
563
 
559
564
  An object mapping custom route paths to file paths. Useful for aliasing or serving files from outside the document root.
560
565
 
566
+ **Note:** All file paths in customRoutes are resolved relative to the server root directory (the `--root` path). This allows you to reference files both inside and outside the document root.
567
+
561
568
  **Basic Routes:**
562
569
  ```json
563
570
  {
564
571
  "customRoutes": {
565
- "/vendor/bootstrap.css": "./node_modules/bootstrap/dist/css/bootstrap.min.css",
572
+ "/vendor/bootstrap.css": "../node_modules/bootstrap/dist/css/bootstrap.min.css",
566
573
  "/api/status": "./status.js"
567
574
  }
568
575
  }
569
576
  ```
570
577
 
571
578
  **Wildcard Routes:**
572
- Wildcard routes allow you to map entire directory structures using the `*` wildcard:
579
+ Wildcard routes allow you to map entire directory structures using the `*` and `**` wildcards:
573
580
 
574
581
  ```json
575
582
  {
576
583
  "customRoutes": {
577
- "kempo/*": "./node_modules/kempo/dust/*",
578
- "assets/*": "./static-files/*",
579
- "docs/*": "./documentation/*"
584
+ "kempo/*": "../node_modules/kempo/dist/*",
585
+ "assets/*": "../static-files/*",
586
+ "docs/*": "../documentation/*",
587
+ "src/**": "../src/**"
580
588
  }
581
589
  }
582
590
  ```
583
591
 
584
592
  With wildcard routes:
585
- - `kempo/styles.css` would serve `./node_modules/kempo/dust/styles.css`
586
- - `assets/logo.png` would serve `./static-files/logo.png`
587
- - `docs/readme.md` would serve `./documentation/readme.md`
588
-
589
- The `*` wildcard matches any single path segment (anything between `/` characters). Multiple wildcards can be used in a single route pattern.
593
+ - `kempo/styles.css` would serve `../node_modules/kempo/dist/styles.css`
594
+ - `assets/logo.png` would serve `../static-files/logo.png`
595
+ - `docs/readme.md` would serve `../documentation/readme.md`
596
+ - `src/components/Button.js` would serve `../src/components/Button.js`
597
+
598
+ The `*` wildcard matches any single path segment (anything between `/` characters).
599
+ The `**` wildcard matches any number of path segments, allowing you to map entire directory trees.
600
+ Multiple wildcards can be used in a single route pattern.
590
601
 
591
602
  ### maxRescanAttempts
592
603
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kempo-server",
3
3
  "type": "module",
4
- "version": "1.7.1",
4
+ "version": "1.7.2",
5
5
  "description": "A lightweight, zero-dependency, file based routing server.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -88,8 +88,8 @@ export default {
88
88
  ],
89
89
  maxRescanAttempts: 3,
90
90
  customRoutes: {
91
- // Example: "/vendor/bootstrap.css": "./node_modules/bootstrap/dist/css/bootstrap.min.css"
92
- // Wildcard example: "kempo/*": "./node_modules/kempo/dust/*"
91
+ // Example: "/vendor/bootstrap.css": "../node_modules/bootstrap/dist/css/bootstrap.min.css"
92
+ // Wildcard example: "kempo/*": "../node_modules/kempo/dust/*"
93
93
  },
94
94
  middleware: {
95
95
  // Built-in middleware configuration
package/src/router.js CHANGED
@@ -27,6 +27,25 @@ export default async (flags, log) => {
27
27
  const configPath = path.isAbsolute(configFileName)
28
28
  ? configFileName
29
29
  : path.join(rootPath, configFileName);
30
+
31
+ log(`Config file name: ${configFileName}`, 2);
32
+ log(`Config path: ${configPath}`, 2);
33
+
34
+ // Validate that config file is within the server root directory
35
+ // Allow absolute paths (user explicitly specified location)
36
+ if (!path.isAbsolute(configFileName)) {
37
+ const relativeConfigPath = path.relative(rootPath, configPath);
38
+ log(`Relative config path: ${relativeConfigPath}`, 2);
39
+ log(`Starts with '..': ${relativeConfigPath.startsWith('..')}`, 2);
40
+ if (relativeConfigPath.startsWith('..') || path.isAbsolute(relativeConfigPath)) {
41
+ log(`Validation failed - throwing error`, 2);
42
+ throw new Error(`Config file must be within the server root directory. Config path: ${configPath}, Root path: ${rootPath}`);
43
+ }
44
+ log(`Validation passed`, 2);
45
+ } else {
46
+ log(`Config file name is absolute, skipping validation`, 2);
47
+ }
48
+
30
49
  log(`Loading config from: ${configPath}`, 2);
31
50
  const configContent = await readFile(configPath, 'utf8');
32
51
  const userConfig = JSON.parse(configContent);
@@ -53,6 +72,11 @@ export default async (flags, log) => {
53
72
  };
54
73
  log('User config loaded and merged with defaults', 2);
55
74
  } catch (e){
75
+ // Only fall back to default config for file reading/parsing errors
76
+ // Let validation errors propagate up
77
+ if (e.message.includes('Config file must be within the server root directory')) {
78
+ throw e;
79
+ }
56
80
  log('Using default config (no config file found)', 2);
57
81
  }
58
82
 
@@ -139,14 +163,14 @@ export default async (flags, log) => {
139
163
  for (const [urlPath, filePath] of Object.entries(config.customRoutes)) {
140
164
  // Check if this is a wildcard route
141
165
  if (urlPath.includes('*')) {
142
- // Resolve the file path relative to the current working directory
143
- const resolvedPath = path.resolve(filePath);
166
+ // Resolve the file path relative to rootPath if relative, otherwise use absolute path
167
+ const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(rootPath, filePath);
144
168
  // Store wildcard routes separately for pattern matching
145
169
  wildcardRoutes.set(urlPath, resolvedPath);
146
170
  log(`Wildcard route mapped: ${urlPath} -> ${resolvedPath}`, 2);
147
171
  } else {
148
- // Resolve the file path relative to the current working directory
149
- const resolvedPath = path.resolve(filePath);
172
+ // Resolve the file path relative to rootPath if relative, otherwise use absolute path
173
+ const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(rootPath, filePath);
150
174
  customRoutes.set(urlPath, resolvedPath);
151
175
  log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`, 2);
152
176
  }
@@ -179,7 +203,9 @@ export default async (flags, log) => {
179
203
  }
180
204
  }
181
205
 
182
- return path.resolve(resolvedPath);
206
+ // If the path is already absolute, return it as-is
207
+ // If it's relative, resolve it relative to rootPath
208
+ return path.isAbsolute(resolvedPath) ? resolvedPath : path.resolve(rootPath, resolvedPath);
183
209
  };
184
210
 
185
211
  // Helper function to find matching wildcard route
@@ -317,5 +317,56 @@ export default {
317
317
  process.chdir(prev);
318
318
  }
319
319
  });
320
+ },
321
+
322
+ 'router throws error when config file is outside server root': async ({pass, fail, log}) => {
323
+ await withTempDir(async (dir) => {
324
+ // Create a config file outside the server root
325
+ const configDir = path.join(dir, '..', 'config-outside-root');
326
+ const configFilePath = await write(configDir, 'outside.config.json', '{"allowedMimes": {"test": "application/test"}}');
327
+
328
+ // Create a file in the server root to verify it doesn't start
329
+ await write(dir, 'index.html', '<h1>Home</h1>');
330
+
331
+ const prev = process.cwd();
332
+ process.chdir(dir);
333
+
334
+ try {
335
+ // Try to use config file outside server root with relative path
336
+ const flags = {root: '.', logging: 0, scan: false, config: '../config-outside-root/outside.config.json'};
337
+
338
+ log('Test setup:');
339
+ log('dir: ' + dir);
340
+ log('configDir: ' + configDir);
341
+ log('configFilePath: ' + configFilePath);
342
+ log('flags.root: ' + flags.root);
343
+ log('flags.config: ' + flags.config);
344
+
345
+ // Check if file exists
346
+ const fs = await import('fs/promises');
347
+ try {
348
+ await fs.access(configFilePath);
349
+ log('Config file exists at: ' + configFilePath);
350
+ } catch (e) {
351
+ log('Config file does NOT exist at: ' + configFilePath);
352
+ }
353
+
354
+ // This should throw an error
355
+ await router(flags, log);
356
+
357
+ // If we reach here, the test failed
358
+ return fail('router should have thrown error for config file outside root');
359
+ } catch (error) {
360
+ log('Error caught: ' + error.message);
361
+ // Verify the error message contains expected text
362
+ if (!error.message.includes('Config file must be within the server root directory')) {
363
+ return fail(`unexpected error message: ${error.message}`);
364
+ }
365
+
366
+ pass('router correctly throws error for config file outside server root');
367
+ } finally {
368
+ process.chdir(prev);
369
+ }
370
+ });
320
371
  }
321
372
  };
@@ -21,7 +21,7 @@ export default {
21
21
  await mkdir(path.join(rootDir, 'src'), { recursive: true });
22
22
  await writeFile(path.join(rootDir, 'src', 'file.txt'), 'static');
23
23
  // Create custom file outside rootPath, matching resolved customRoute
24
- const customFilePath = path.resolve(dir, '..', 'src', 'file.txt');
24
+ const customFilePath = path.resolve(rootDir, '..', 'src', 'file.txt');
25
25
  await mkdir(path.dirname(customFilePath), { recursive: true });
26
26
  await writeFile(customFilePath, 'custom');
27
27
  log('Custom file path: ' + customFilePath);
@@ -25,7 +25,7 @@ export default {
25
25
  // Configure double asterisk wildcard route
26
26
  const config = {
27
27
  customRoutes: {
28
- '/src/**': './src/**'
28
+ '/src/**': '../src/**'
29
29
  }
30
30
  };
31
31
 
@@ -86,7 +86,7 @@ export default {
86
86
  // Configure single asterisk wildcard route
87
87
  const config = {
88
88
  customRoutes: {
89
- '/src/*': './src/*'
89
+ '/src/*': '../src/*'
90
90
  }
91
91
  };
92
92
 
@@ -140,7 +140,7 @@ export default {
140
140
  // Configure wildcard route that overrides static file
141
141
  const config = {
142
142
  customRoutes: {
143
- '/api/**': './custom/**'
143
+ '/api/**': '../custom/**'
144
144
  }
145
145
  };
146
146
 
@@ -25,7 +25,7 @@ export default {
25
25
  // Configure double asterisk wildcard route
26
26
  const config = {
27
27
  customRoutes: {
28
- '/src/**': './src/**'
28
+ '/src/**': '../src/**'
29
29
  }
30
30
  };
31
31
 
@@ -86,7 +86,7 @@ export default {
86
86
  // Configure single asterisk wildcard route
87
87
  const config = {
88
88
  customRoutes: {
89
- '/src/*': './src/*'
89
+ '/src/*': '../src/*'
90
90
  }
91
91
  };
92
92
 
@@ -140,7 +140,7 @@ export default {
140
140
  // Configure wildcard route that overrides static file
141
141
  const config = {
142
142
  customRoutes: {
143
- '/api/**': './custom/**'
143
+ '/api/**': '../custom/**'
144
144
  }
145
145
  };
146
146