From 8c39dca0fa72df0fef273a008ae9e751f874a541 Mon Sep 17 00:00:00 2001 From: nimesh0505 Date: Mon, 2 Sep 2024 13:32:37 +0530 Subject: [PATCH 1/5] feature: disable root path check when serve is false --- index.js | 8 ++++++-- test/static.test.js | 48 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 99d8e4e..0ed4b02 100644 --- a/index.js +++ b/index.js @@ -19,7 +19,7 @@ send.mime.default_type = 'application/octet-stream' async function fastifyStatic (fastify, opts) { opts.root = normalizeRoot(opts.root) - checkRootPathForErrors(fastify, opts.root) + checkRootPathForErrors(fastify, opts.root, opts.serve) const setHeaders = opts.setHeaders if (setHeaders !== undefined && typeof setHeaders !== 'function') { @@ -408,7 +408,11 @@ function normalizeRoot (root) { return root } -function checkRootPathForErrors (fastify, rootPath) { +function checkRootPathForErrors (fastify, rootPath, serve = true) { + if (serve === false) { + return + } + if (rootPath === undefined) { throw new Error('"root" option is required') } diff --git a/test/static.test.js b/test/static.test.js index 608d57a..678de91 100644 --- a/test/static.test.js +++ b/test/static.test.js @@ -4023,3 +4023,51 @@ t.test('content-length in head route should not return zero when using wildcard' }) }) }) + +t.test('serve: false disables root path check', (t) => { + t.plan(3) + + const pluginOptions = { + root: path.join(__dirname, '/static'), + prefix: '/static', + serve: false + } + const fastify = Fastify() + fastify.register(fastifyStatic, pluginOptions) + + t.teardown(fastify.close.bind(fastify)) + + fastify.listen({ port: 0 }, (err) => { + t.error(err) + + fastify.server.unref() + + t.test('/static/index.html', (t) => { + t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) + simple.concat({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + '/static/index.html' + }, (err, response, body) => { + t.error(err) + t.equal(response.statusCode, 404) + genericErrorResponseChecks(t, response) + t.end() + }) + }) + + t.test('/static/deep/path/for/test/purpose/foo.html', (t) => { + t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) + simple.concat({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + '/static/deep/path/for/test/purpose/foo.html' + }, (err, response, body) => { + t.error(err) + t.equal(response.statusCode, 404) + genericErrorResponseChecks(t, response) + t.end() + }) + }) + + t.end() + }) +}) From 387499cf97b2fb1a85f6effff6b8464d3b186c68 Mon Sep 17 00:00:00 2001 From: nimesh0505 Date: Tue, 3 Sep 2024 14:58:41 +0530 Subject: [PATCH 2/5] default to CWD when serve:false and no root --- index.js | 45 +++++++++++---------- test/static.test.js | 97 ++++++++++++++++++++++++++++----------------- 2 files changed, 85 insertions(+), 57 deletions(-) diff --git a/index.js b/index.js index 0ed4b02..c186030 100644 --- a/index.js +++ b/index.js @@ -18,8 +18,13 @@ const supportedEncodings = ['br', 'gzip', 'deflate'] send.mime.default_type = 'application/octet-stream' async function fastifyStatic (fastify, opts) { + if (opts.serve === false && opts.root === undefined) { + opts.root = process.cwd() + fastify.log.warn('No root path provided. Defaulting to current working directory. This may pose security risks if not intended.') + } + opts.root = normalizeRoot(opts.root) - checkRootPathForErrors(fastify, opts.root, opts.serve) + checkRootPathForErrors(fastify, opts.root, opts.serve === false) const setHeaders = opts.setHeaders if (setHeaders !== undefined && typeof setHeaders !== 'function') { @@ -408,11 +413,7 @@ function normalizeRoot (root) { return root } -function checkRootPathForErrors (fastify, rootPath, serve = true) { - if (serve === false) { - return - } - +function checkRootPathForErrors (fastify, rootPath, skipExistenceCheck) { if (rootPath === undefined) { throw new Error('"root" option is required') } @@ -429,18 +430,18 @@ function checkRootPathForErrors (fastify, rootPath, serve = true) { } // check each path and fail at first invalid - rootPath.map((path) => checkPath(fastify, path)) + rootPath.map((path) => checkPath(fastify, path, skipExistenceCheck)) return } if (typeof rootPath === 'string') { - return checkPath(fastify, rootPath) + return checkPath(fastify, rootPath, skipExistenceCheck) } throw new Error('"root" option must be a string or array of strings') } -function checkPath (fastify, rootPath) { +function checkPath (fastify, rootPath, skipExistenceCheck) { if (typeof rootPath !== 'string') { throw new Error('"root" option must be a string') } @@ -448,21 +449,23 @@ function checkPath (fastify, rootPath) { throw new Error('"root" option must be an absolute path') } - let pathStat + if (!skipExistenceCheck) { + let pathStat - try { - pathStat = statSync(rootPath) - } catch (e) { - if (e.code === 'ENOENT') { - fastify.log.warn(`"root" path "${rootPath}" must exist`) - return - } + try { + pathStat = statSync(rootPath) + } catch (e) { + if (e.code === 'ENOENT') { + fastify.log.warn(`"root" path "${rootPath}" must exist`) + return + } - throw e - } + throw e + } - if (pathStat.isDirectory() === false) { - throw new Error('"root" option must point to a directory') + if (pathStat.isDirectory() === false) { + throw new Error('"root" option must point to a directory') + } } } diff --git a/test/static.test.js b/test/static.test.js index 678de91..c532dbe 100644 --- a/test/static.test.js +++ b/test/static.test.js @@ -4024,50 +4024,75 @@ t.test('content-length in head route should not return zero when using wildcard' }) }) -t.test('serve: false disables root path check', (t) => { - t.plan(3) +t.test('serve: false disables root path check', t => { + t.plan(4) - const pluginOptions = { - root: path.join(__dirname, '/static'), - prefix: '/static', - serve: false - } - const fastify = Fastify() - fastify.register(fastifyStatic, pluginOptions) + t.test('should default to CWD when no root is provided', async (t) => { + t.plan(3) + const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) + let warningLogged = false + fastify.log.warn = (message) => { + if (message.includes('No root path provided')) { + warningLogged = true + } + } - fastify.listen({ port: 0 }, (err) => { - t.error(err) + await fastify.register(fastifyStatic, { serve: false }) - fastify.server.unref() + t.ok(warningLogged, 'should log a warning about defaulting to CWD') - t.test('/static/index.html', (t) => { - t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) - simple.concat({ - method: 'GET', - url: 'http://localhost:' + fastify.server.address().port + '/static/index.html' - }, (err, response, body) => { - t.error(err) - t.equal(response.statusCode, 404) - genericErrorResponseChecks(t, response) - t.end() - }) + fastify.get('/test', (req, reply) => { + reply.sendFile('test/static/index.html') }) - t.test('/static/deep/path/for/test/purpose/foo.html', (t) => { - t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) - simple.concat({ - method: 'GET', - url: 'http://localhost:' + fastify.server.address().port + '/static/deep/path/for/test/purpose/foo.html' - }, (err, response, body) => { - t.error(err) - t.equal(response.statusCode, 404) - genericErrorResponseChecks(t, response) - t.end() - }) + const response = await fastify.inject('/test') + t.equal(response.statusCode, 200, 'should serve file from CWD') + + const fs = require('fs') + const expectedContent = fs.readFileSync('test/static/index.html', 'utf8') + t.equal(response.payload, expectedContent, 'should serve correct file content') + }) + + t.test('should not throw when root is non-existent directory and serve is false', async (t) => { + t.plan(1) + const fastify = Fastify() + const nonExistentPath = path.join(__dirname, 'definitely-non-existent-directory') + + await fastify.register(fastifyStatic, { + serve: false, + root: nonExistentPath }) + t.pass('should not throw for non-existent directory') + }) - t.end() + t.test('should still validate root is a string or array of strings', async (t) => { + t.plan(1) + const fastify = Fastify() + + try { + await fastify.register(fastifyStatic, { + serve: false, + root: 123 + }) + t.fail('Should have thrown an error for non-string root') + } catch (error) { + t.match(error.message, /"root" option must be a string or array of strings/, 'Should throw for non-string root') + } + }) + + t.test('should still validate root is an absolute path', async (t) => { + t.plan(1) + const fastify = Fastify() + + try { + await fastify.register(fastifyStatic, { + serve: false, + root: 'relative/path' + }) + t.fail('Should have thrown an error for relative path') + } catch (error) { + t.match(error.message, /"root" option must be an absolute path/, 'Should throw for relative path') + } }) }) From 64bc7f8999e5d2b79ce42f996ad5be914cf65f28 Mon Sep 17 00:00:00 2001 From: nimesh0505 Date: Tue, 3 Sep 2024 15:04:35 +0530 Subject: [PATCH 3/5] update README with CWD behavior for serve:false option --- README.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c52fa9b..0a99ab4 100644 --- a/README.md +++ b/README.md @@ -435,10 +435,36 @@ Assume this structure with the compressed asset as a sibling of the un-compresse └── index.html ``` -#### Disable serving +#### Disable serving and CWD behavior If you would just like to use the reply decorator and not serve whole directories automatically, you can simply pass the option `{ serve: false }`. This will prevent the plugin from serving everything under `root`. +When `serve: false` is passed: + +1. The plugin will not perform the usual directory existence check for the `root` option. +2. If no `root` is provided, the plugin will default to using the current working directory (CWD) as the root. +3. A warning will be logged if no `root` is provided, informing that the CWD is being used as the default. + +Example usage: + +```js +const fastify = require('fastify')() +const path = require('node:path') + +fastify.register(require('@fastify/static'), { + serve: false, + // root is optional when serve is false, will default to CWD if not provided + root: path.join(__dirname, 'public') +}) + +fastify.get('/file', (req, reply) => { + // This will serve the file from the CWD if no root was provided + reply.sendFile('myFile.html') +}) +``` + +This configuration allows you to use the `sendFile` decorator without automatically serving an entire directory, giving you more control over which files are accessible. + #### Disabling reply decorator The reply object is decorated with a `sendFile` function by default. If you want to From 513b8c47aa72970d07eb72827a926837731ceef5 Mon Sep 17 00:00:00 2001 From: nimesh0505 Date: Wed, 4 Sep 2024 11:30:34 +0530 Subject: [PATCH 4/5] optimize root path checks for CWD scenario --- index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/index.js b/index.js index c186030..6b4857e 100644 --- a/index.js +++ b/index.js @@ -442,6 +442,10 @@ function checkRootPathForErrors (fastify, rootPath, skipExistenceCheck) { } function checkPath (fastify, rootPath, skipExistenceCheck) { + // skip all checks if rootPath is the CWD + if (rootPath === process.cwd()) { + return + } if (typeof rootPath !== 'string') { throw new Error('"root" option must be a string') } From da2fb25f7687fc5fae6f69595f9dba49ff8f02fb Mon Sep 17 00:00:00 2001 From: nimesh0505 Date: Tue, 10 Sep 2024 14:13:43 +0530 Subject: [PATCH 5/5] updated the readme.md --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0a99ab4..efe6105 100644 --- a/README.md +++ b/README.md @@ -437,13 +437,12 @@ Assume this structure with the compressed asset as a sibling of the un-compresse #### Disable serving and CWD behavior -If you would just like to use the reply decorator and not serve whole directories automatically, you can simply pass the option `{ serve: false }`. This will prevent the plugin from serving everything under `root`. +If you want to use only the reply decorator without automatically serving whole directories, pass the option `{ serve: false }`. This prevents the plugin from serving everything under `root`. -When `serve: false` is passed: +When `serve: false` is used: -1. The plugin will not perform the usual directory existence check for the `root` option. -2. If no `root` is provided, the plugin will default to using the current working directory (CWD) as the root. -3. A warning will be logged if no `root` is provided, informing that the CWD is being used as the default. +- If no `root` is provided, the plugin will use the current working directory (CWD) as the default root. +- The `sendFile` method will send files relative to the CWD or the specified `root`. Example usage: @@ -463,7 +462,7 @@ fastify.get('/file', (req, reply) => { }) ``` -This configuration allows you to use the `sendFile` decorator without automatically serving an entire directory, giving you more control over which files are accessible. +This configuration allows you to use the `sendFile` decorator without automatically serving an entire directory. #### Disabling reply decorator