Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge next into master #454

Merged
merged 5 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:

jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5.0.0
with:
license-check: true
lint: true
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,13 +477,13 @@ If an error occurs while trying to send a file, the error will be passed
to Fastify's error handler. You can set a custom error handler with
[`fastify.setErrorHandler()`](https://fastify.dev/docs/latest/Reference/Server/#seterrorhandler).

### Payload `stream.filename`
### Payload `stream.path`

If you need to access the filename inside the `onSend` hook, you can use `payload.filename`.
If you need to access the file path inside the `onSend` hook, you can use `payload.path`.

```js
fastify.addHook('onSend', function (req, reply, payload, next) {
console.log(payload.filename)
console.log(payload.path)
next()
})
```
Expand Down
274 changes: 126 additions & 148 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict'

const { PassThrough } = require('node:stream')
const path = require('node:path')
const { fileURLToPath } = require('node:url')
const { statSync } = require('node:fs')
Expand Down Expand Up @@ -170,7 +169,7 @@

const allowedPath = opts.allowedPath

function pumpSendToReply (
async function pumpSendToReply (
request,
reply,
pathname,
Expand Down Expand Up @@ -222,164 +221,148 @@
}

// `send(..., path, ...)` will URI-decode path so we pass an encoded path here
const stream = send(request.raw, encodeURI(pathnameForSend), options)
let resolvedFilename
stream.on('file', function (file) {
resolvedFilename = file
})

const wrap = new PassThrough({
flush (cb) {
this.finished = true
if (reply.raw.statusCode === 304) {
reply.send('')
const {
statusCode,
headers,
stream,
type,
metadata
} = await send(request.raw, encodeURI(pathnameForSend), options)
switch (type) {
case 'directory': {
const path = metadata.path
if (opts.list) {
await dirList.send({
reply,
dir: path,
options: opts.list,
route: pathname,
prefix,
dotfiles: opts.dotfiles
}).catch((err) => reply.send(err))
}
cb()
}
})

wrap.getHeader = reply.getHeader.bind(reply)
wrap.setHeader = reply.header.bind(reply)
wrap.removeHeader = () => {}
wrap.finished = false
if (opts.redirect === true) {
try {
reply.redirect(301, getRedirectUrl(request.raw.url))
} /* c8 ignore start */ catch (error) {
// the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
await reply.send(error)

Check warning

Code scanning / CodeQL

Information exposure through a stack trace Medium

This information exposed to the user depends on
stack trace information
.
} /* c8 ignore stop */
} else {
// if is a directory path without a trailing slash, and has an index file, reply as if it has a trailing slash
if (!pathname.endsWith('/') && findIndexFile(pathname, options.root, options.index)) {
return pumpSendToReply(
request,
reply,
pathname + '/',
rootPath,
undefined,
undefined,
checkedEncodings
)
}

Object.defineProperty(wrap, 'filename', {
get () {
return resolvedFilename
}
})
Object.defineProperty(wrap, 'statusCode', {
get () {
return reply.raw.statusCode
},
set (code) {
reply.code(code)
reply.callNotFound()
}
break
}
})

if (request.method === 'HEAD') {
wrap.on('finish', reply.send.bind(reply))
} else {
wrap.on('pipe', function () {
if (encoding) {
reply.header('content-type', getContentType(pathname))
reply.header('content-encoding', encoding)
case 'error': {
if (
statusCode === 403 &&
(!options.index || !options.index.length) &&
pathnameForSend[pathnameForSend.length - 1] === '/'
) {
if (opts.list) {
await dirList.send({
reply,
dir: dirList.path(opts.root, pathname),
options: opts.list,
route: pathname,
prefix,
dotfiles: opts.dotfiles
}).catch((err) => reply.send(err))
}
}
reply.send(wrap)
})
}

if (setHeaders !== undefined) {
stream.on('headers', setHeaders)
}

stream.on('directory', function (_, path) {
if (opts.list) {
dirList.send({
reply,
dir: path,
options: opts.list,
route: pathname,
prefix,
dotfiles: opts.dotfiles
}).catch((err) => reply.send(err))
return
}
if (metadata.error.code === 'ENOENT') {
// when preCompress is enabled and the path is a directory without a trailing slash
if (opts.preCompressed && encoding) {
const indexPathname = findIndexFile(pathname, options.root, options.index)
if (indexPathname) {
return pumpSendToReply(
request,
reply,
pathname + '/',
rootPath,
undefined,
undefined,
checkedEncodings
)
}
}

if (opts.redirect === true) {
try {
reply.redirect(301, getRedirectUrl(request.raw.url))
} catch (error) {
// the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
/* istanbul ignore next */
reply.send(error)
}
} else {
// if is a directory path without a trailing slash, and has an index file, reply as if it has a trailing slash
if (!pathname.endsWith('/') && findIndexFile(pathname, options.root, options.index)) {
return pumpSendToReply(
request,
reply,
pathname + '/',
rootPath,
undefined,
undefined,
checkedEncodings
)
}
// if file exists, send real file, otherwise send dir list if name match
if (opts.list && dirList.handle(pathname, opts.list)) {
await dirList.send({
reply,
dir: dirList.path(opts.root, pathname),
options: opts.list,
route: pathname,
prefix,
dotfiles: opts.dotfiles
}).catch((err) => reply.send(err))
return
}

reply.callNotFound()
}
})
// root paths left to try?
if (Array.isArray(rootPath) && rootPathOffset < (rootPath.length - 1)) {
return pumpSendToReply(request, reply, pathname, rootPath, rootPathOffset + 1)
}

stream.on('error', function (err) {
if (err.code === 'ENOENT') {
// when preCompress is enabled and the path is a directory without a trailing slash
if (opts.preCompressed && encoding) {
const indexPathname = findIndexFile(pathname, options.root, options.index)
if (indexPathname) {
if (opts.preCompressed && !checkedEncodings.has(encoding)) {
checkedEncodings.add(encoding)
return pumpSendToReply(
request,
reply,
pathname + '/',
pathnameOrig,
rootPath,
undefined,
rootPathOffset,
undefined,
checkedEncodings
)
}
}

// if file exists, send real file, otherwise send dir list if name match
if (opts.list && dirList.handle(pathname, opts.list)) {
dirList.send({
reply,
dir: dirList.path(opts.root, pathname),
options: opts.list,
route: pathname,
prefix,
dotfiles: opts.dotfiles
}).catch((err) => reply.send(err))
return
return reply.callNotFound()
}

// root paths left to try?
if (Array.isArray(rootPath) && rootPathOffset < (rootPath.length - 1)) {
return pumpSendToReply(request, reply, pathname, rootPath, rootPathOffset + 1)
// The `send` library terminates the request with a 404 if the requested
// path contains a dotfile and `send` is initialized with `{dotfiles:
// 'ignore'}`. `send` aborts the request before getting far enough to
// check if the file exists (hence, a 404 `NotFoundError` instead of
// `ENOENT`).
// https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L582
if (metadata.error.status === 404) {
return reply.callNotFound()
}

if (opts.preCompressed && !checkedEncodings.has(encoding)) {
checkedEncodings.add(encoding)
return pumpSendToReply(
request,
reply,
pathnameOrig,
rootPath,
rootPathOffset,
undefined,
checkedEncodings
)
}

return reply.callNotFound()
await reply.send(metadata.error)
break
}

// The `send` library terminates the request with a 404 if the requested
// path contains a dotfile and `send` is initialized with `{dotfiles:
// 'ignore'}`. `send` aborts the request before getting far enough to
// check if the file exists (hence, a 404 `NotFoundError` instead of
// `ENOENT`).
// https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L582
if (err.status === 404) {
return reply.callNotFound()
case 'file': {
reply.code(statusCode)
if (setHeaders !== undefined) {
setHeaders(reply.raw, metadata.path, metadata.stat)
}
reply.headers(headers)
if (encoding) {
reply.header('content-type', getContentType(pathname))
reply.header('content-encoding', encoding)
}
await reply.send(stream)
break
}

reply.send(err)
})

// we cannot use pump, because send error
// handling is not compatible
stream.pipe(wrap)
}
}

function setUpHeadAndGet (routeOpts, route, file, rootPath) {
Expand All @@ -394,11 +377,11 @@
fastify.route(toSetUp)
}

function serveFileHandler (req, reply) {
async function serveFileHandler (req, reply) {
// TODO: remove the fallback branch when bump major
/* istanbul ignore next */
/* c8 ignore next */
const routeConfig = req.routeOptions?.config || req.routeConfig
pumpSendToReply(req, reply, routeConfig.file, routeConfig.rootPath)
return pumpSendToReply(req, reply, routeConfig.file, routeConfig.rootPath)
}
}

Expand Down Expand Up @@ -489,8 +472,6 @@
}

function findIndexFile (pathname, root, indexFiles = ['index.html']) {
// TODO remove istanbul ignore
/* istanbul ignore else */
if (Array.isArray(indexFiles)) {
return indexFiles.find(filename => {
const p = path.join(root, pathname, filename)
Expand All @@ -502,7 +483,7 @@
}
})
}
/* istanbul ignore next */
/* c8 ignore next */
return false
}

Expand Down Expand Up @@ -541,19 +522,16 @@
const parsed = new URL(url, 'http://localhost.com/')
const parsedPathname = parsed.pathname
return parsedPathname + (parsedPathname[parsedPathname.length - 1] !== '/' ? '/' : '') + (parsed.search || '')
} catch {
} /* c8 ignore start */ catch {
// the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
/* istanbul ignore next */
const err = new Error(`Invalid redirect URL: ${url}`)
/* istanbul ignore next */
err.statusCode = 400
/* istanbul ignore next */
throw err
}
} /* c8 ignore stop */
}

module.exports = fp(fastifyStatic, {
fastify: '4.x',
// fastify: '4.x',
name: '@fastify/static'
})
module.exports.default = fastifyStatic
Expand Down
Loading
Loading