diff --git a/README.md b/README.md index 7227525..770cf68 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,12 @@ fastify.get('/favicon.ico', function (req, reply) { ### Options -#### `root` (required) +#### `serve` +Default: `true` + +If set to `false`, the plugin will not serve files from the `root` directory. + +#### `root` (required if `serve` is not false) The absolute path of the directory containing the files to serve. The file to serve is determined by combining `req.url` with the diff --git a/index.js b/index.js index a4e81f6..0ff84c5 100644 --- a/index.js +++ b/index.js @@ -19,8 +19,10 @@ send.mime.default_type = 'application/octet-stream' /** @type {import("fastify").FastifyPluginAsync} */ async function fastifyStatic (fastify, opts) { - opts.root = normalizeRoot(opts.root) - checkRootPathForErrors(fastify, opts.root) + if (opts.serve !== false || opts.root !== undefined) { + opts.root = normalizeRoot(opts.root) + checkRootPathForErrors(fastify, opts.root) + } const setHeaders = opts.setHeaders if (setHeaders !== undefined && typeof setHeaders !== 'function') { @@ -199,6 +201,8 @@ async function fastifyStatic (fastify, opts) { } else { options.root = rootPath } + } else if (path.isAbsolute(pathname) === false) { + return reply.callNotFound() } if (allowedPath && !allowedPath(pathname, options.root, request)) { diff --git a/test/static.test.js b/test/static.test.js index 8077c00..cd5e147 100644 --- a/test/static.test.js +++ b/test/static.test.js @@ -753,6 +753,51 @@ test('serving disabled', async (t) => { }) }) +test('serving disabled without root', async (t) => { + t.plan(2) + + const pluginOptions = { + prefix: '/static/', + serve: false + } + const fastify = Fastify() + fastify.register(fastifyStatic, pluginOptions) + + t.after(() => fastify.close()) + + fastify.get('/foo/bar/r', (_request, reply) => { + reply.sendFile('index.html') + }) + + fastify.get('/foo/bar/a', (_request, reply) => { + reply.sendFile(path.join(__dirname, pluginOptions.prefix, 'index.html')) + }) + + t.after(() => fastify.close()) + + await fastify.listen({ port: 0 }) + + fastify.server.unref() + + await t.test('/static/index.html via sendFile not found', async (t) => { + t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) + + const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/a') + t.assert.ok(response.ok) + t.assert.deepStrictEqual(response.status, 200) + t.assert.deepStrictEqual(await response.text(), indexContent) + genericResponseChecks(t, response) + }) + + await t.test('/static/index.html via sendFile with relative path not found', async (t) => { + t.plan(2) + + const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/r') + t.assert.ok(!response.ok) + t.assert.deepStrictEqual(response.status, 404) + }) +}) + test('sendFile', async (t) => { t.plan(4) @@ -1215,7 +1260,7 @@ test('maxAge option', async (t) => { }) test('errors', async (t) => { - t.plan(11) + t.plan(12) await t.test('no root', async (t) => { t.plan(1) @@ -1280,6 +1325,16 @@ test('errors', async (t) => { await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) + await t.test('no root and serve: false', async (t) => { + t.plan(1) + const pluginOptions = { + serve: false, + root: [] + } + const fastify = Fastify({ logger: false }) + await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) + }) + await t.test('duplicate root paths are not allowed', async (t) => { t.plan(1) const pluginOptions = { diff --git a/types/index.d.ts b/types/index.d.ts index d410bb2..6aef4a0 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -84,40 +84,52 @@ declare namespace fastifyStatic { serveDotFiles?: boolean; } - export interface FastifyStaticOptions extends SendOptions { - root: string | string[] | URL | URL[]; - prefix?: string; - prefixAvoidTrailingSlash?: boolean; - serve?: boolean; - decorateReply?: boolean; - schemaHide?: boolean; - setHeaders?: (res: SetHeadersResponse, path: string, stat: Stats) => void; - redirect?: boolean; - wildcard?: boolean; - globIgnore?: string[]; - list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat; - allowedPath?: (pathName: string, root: string, request: FastifyRequest) => boolean; - /** - * @description - * Opt-in to looking for pre-compressed files - */ - preCompressed?: boolean; - - // Passed on to `send` - acceptRanges?: boolean; - contentType?: boolean; - cacheControl?: boolean; - dotfiles?: 'allow' | 'deny' | 'ignore'; - etag?: boolean; - extensions?: string[]; - immutable?: boolean; - index?: string[] | string | false; - lastModified?: boolean; - maxAge?: string | number; - constraints?: RouteOptions['constraints']; - logLevel?: RouteOptions['logLevel']; + type Root = string | string[] | URL | URL[] + + type RootOptions = { + serve: true; + root: Root; + } | { + serve?: false; + root?: Root; } + export type FastifyStaticOptions = + SendOptions + & RootOptions + & { + // Added by this plugin + prefix?: string; + prefixAvoidTrailingSlash?: boolean; + decorateReply?: boolean; + schemaHide?: boolean; + setHeaders?: (res: SetHeadersResponse, path: string, stat: Stats) => void; + redirect?: boolean; + wildcard?: boolean; + globIgnore?: string[]; + list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat; + allowedPath?: (pathName: string, root: string, request: FastifyRequest) => boolean; + /** + * @description + * Opt-in to looking for pre-compressed files + */ + preCompressed?: boolean; + + // Passed on to `send` + acceptRanges?: boolean; + contentType?: boolean; + cacheControl?: boolean; + dotfiles?: 'allow' | 'deny' | 'ignore'; + etag?: boolean; + extensions?: string[]; + immutable?: boolean; + index?: string[] | string | false; + lastModified?: boolean; + maxAge?: string | number; + constraints?: RouteOptions['constraints']; + logLevel?: RouteOptions['logLevel']; + } + export const fastifyStatic: FastifyStaticPlugin export { fastifyStatic as default } diff --git a/types/index.test-d.ts b/types/index.test-d.ts index eafe4fd..ff5b103 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -120,6 +120,19 @@ expectAssignable({ root: [new URL('')] }) +expectError({ + serve: true +}) + +expectAssignable({ + serve: true, + root: '' +}) + +expectAssignable({ + serve: false +}) + appWithImplicitHttp .register(fastifyStatic, options) .after(() => {