Aktueller Stand

This commit is contained in:
2026-01-22 19:05:45 +01:00
parent 85dee61a4d
commit e280e4eadb
1967 changed files with 397327 additions and 74093 deletions

2
backend/node_modules/@fastify/send/.gitattributes generated vendored Normal file
View File

@@ -0,0 +1,2 @@
# Set default behavior to automatically convert line endings
* text=auto eol=lf

View File

@@ -9,5 +9,5 @@ updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
interval: "monthly"
open-pull-requests-limit: 10

View File

@@ -2,6 +2,10 @@ name: CI
on:
push:
branches:
- main
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
@@ -10,9 +14,15 @@ on:
- 'docs/**'
- '*.md'
permissions:
contents: read
jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
permissions:
contents: write
pull-requests: write
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
with:
license-check: true
lint: true
lint: true

View File

@@ -1,2 +0,0 @@
files:
- test/**/*.test.js

View File

@@ -480,37 +480,37 @@
* update range-parser and fresh
0.1.4 / 2013-08-11
0.1.4 / 2013-08-11
==================
* update fresh
0.1.3 / 2013-07-08
0.1.3 / 2013-07-08
==================
* Revert "Fix fd leak"
0.1.2 / 2013-07-03
0.1.2 / 2013-07-03
==================
* Fix fd leak
0.1.0 / 2012-08-25
0.1.0 / 2012-08-25
==================
* add options parameter to send() that is passed to fs.createReadStream() [kanongil]
0.0.4 / 2012-08-16
0.0.4 / 2012-08-16
==================
* allow custom "Accept-Ranges" definition
0.0.3 / 2012-07-16
0.0.3 / 2012-07-16
==================
* fix normalization of the root directory. Closes #3
0.0.2 / 2012-07-09
0.0.2 / 2012-07-09
==================
* add passing of req explicitly for now (YUCK)

View File

@@ -1,8 +1,8 @@
# @fastify/send
![CI](https://github.com/fastify/send/workflows/CI/badge.svg)
[![CI](https://github.com/fastify/send/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/send/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/@fastify/send.svg?style=flat)](https://www.npmjs.com/package/@fastify/send)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
Send is a library for streaming files from the file system as an HTTP response
supporting partial responses (Ranges), conditional-GET negotiation (If-Match,
@@ -20,17 +20,26 @@ This is a [Node.js](https://nodejs.org/en/) module available through the
$ npm install @fastify/send
```
### TypeScript
`@types/mime@3` must be used if wanting to use TypeScript;
`@types/mime@4` removed the `mime` types.
```bash
$ npm install -D @types/mime@3
```
## API
```js
var send = require('@fastify/send')
const send = require('@fastify/send')
```
### send(req, path, [options])
Create a new `SendStream` for the given path to send to a `res`. The `req` is
the Node.js HTTP request and the `path` is a urlencoded path to send (urlencoded,
not the actual file-system path).
Provide `statusCode`, `headers`, and `stream` for the given path to send to a
`res`. The `req` is the Node.js HTTP request and the `path `is a urlencoded path
to send (urlencoded, not the actual file-system path).
#### Options
@@ -45,6 +54,14 @@ of the `Range` request header.
Enable or disable setting `Cache-Control` response header, defaults to
true. Disabling this will ignore the `immutable` and `maxAge` options.
##### contentType
By default, this library uses the `mime` module to set the `Content-Type`
of the response based on the file extension of the requested file.
To disable this functionality, set `contentType` to `false`.
The `Content-Type` header will need to be set manually if disabled.
##### dotfiles
Set how "dotfiles" are treated when encountered. A dotfile is a file
@@ -104,6 +121,11 @@ Provide a max-age in milliseconds for HTTP caching, defaults to 0.
This can also be a string accepted by the
[ms](https://www.npmjs.org/package/ms#readme) module.
##### maxContentRangeChunkSize
Specify the maximum response content size, defaults to the entire file size.
This will be used when `acceptRanges` is true.
##### root
Serve files relative to `path`.
@@ -113,21 +135,12 @@ Serve files relative to `path`.
Byte offset at which the stream starts, defaults to 0. The start is inclusive,
meaning `start: 2` will include the 3rd byte in the stream.
#### Events
##### highWaterMark
The `SendStream` is an event emitter and will emit the following events:
- `error` an error occurred `(err)`
- `directory` a directory was requested `(res, path)`
- `file` a file was requested `(path, stat)`
- `headers` the headers are about to be set on a file `(res, path, stat)`
- `stream` file streaming has started `(stream)`
- `end` streaming has completed
#### .pipe
The `pipe` method is used to pipe the response into the Node.js HTTP response
object, typically `send(req, path, options).pipe(res)`.
When provided, this option sets the maximum number of bytes that the internal
buffer will hold before pausing reads from the underlying resource.
If you omit this option (or pass undefined), Node.js falls back to
its built-in default for readable binary streams.
### .mime
@@ -138,12 +151,6 @@ This is used to configure the MIME types that are associated with file extension
as well as other options for how to resolve the MIME type of a file (like the
default type to use for an unknown file extension).
## Error-handling
By default when no `error` listeners are present an automatic response will be
made, otherwise you have full control over the response, aka you may show a 5xx
page etc.
## Caching
It does _not_ perform internal caching, you should use a reverse proxy cache
@@ -173,12 +180,13 @@ $ npm test
This simple example will send a specific file to all requests.
```js
var http = require('http')
var send = require('send')
const http = require('node:http')
const send = require('send')
var server = http.createServer(function onRequest (req, res) {
send(req, '/path/to/index.html')
.pipe(res)
const server = http.createServer(async function onRequest (req, res) {
const { statusCode, headers, stream } = await send(req, '/path/to/index.html')
res.writeHead(statusCode, headers)
stream.pipe(res)
})
server.listen(3000)
@@ -191,13 +199,14 @@ given directory as the top-level. For example, a request
`GET /foo.txt` will send back `/www/public/foo.txt`.
```js
var http = require('http')
var parseUrl = require('parseurl')
var send = require('@fastify/send')
const http = require('node:http')
const parseUrl = require('parseurl')
const send = require('@fastify/send')
var server = http.createServer(function onRequest (req, res) {
send(req, parseUrl(req).pathname, { root: '/www/public' })
.pipe(res)
const server = http.createServer(async function onRequest (req, res) {
const { statusCode, headers, stream } = await send(req, parseUrl(req).pathname, { root: '/www/public' })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
server.listen(3000)
@@ -206,9 +215,9 @@ server.listen(3000)
### Custom file types
```js
var http = require('http')
var parseUrl = require('parseurl')
var send = require('@fastify/send')
const http = require('node:http')
const parseUrl = require('parseurl')
const send = require('@fastify/send')
// Default unknown types to text/plain
send.mime.default_type = 'text/plain'
@@ -218,9 +227,10 @@ send.mime.define({
'application/x-my-type': ['x-mt', 'x-mtt']
})
var server = http.createServer(function onRequest (req, res) {
send(req, parseUrl(req).pathname, { root: '/www/public' })
.pipe(res)
const server = http.createServer(function onRequest (req, res) {
const { statusCode, headers, stream } = await send(req, parseUrl(req).pathname, { root: '/www/public' })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
server.listen(3000)
@@ -232,75 +242,64 @@ This is an example of serving up a structure of directories with a
custom function to render a listing of a directory.
```js
var http = require('http')
var fs = require('fs')
var parseUrl = require('parseurl')
var send = require('@fastify/send')
const http = require('node:http')
const fs = require('node:fs')
const parseUrl = require('parseurl')
const send = require('@fastify/send')
// Transfer arbitrary files from within /www/example.com/public/*
// with a custom handler for directory listing
var server = http.createServer(function onRequest (req, res) {
send(req, parseUrl(req).pathname, { index: false, root: '/www/public' })
.once('directory', directory)
.pipe(res)
const server = http.createServer(async function onRequest (req, res) {
const { statusCode, headers, stream, type, metadata } = await send(req, parseUrl(req).pathname, { index: false, root: '/www/public' })
if(type === 'directory') {
// get directory list
const list = await readdir(metadata.path)
// render an index for the directory
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
res.end(list.join('\n') + '\n')
} else {
res.writeHead(statusCode, headers)
stream.pipe(res)
}
})
server.listen(3000)
// Custom directory handler
function directory (res, path) {
var stream = this
// redirect to trailing slash for consistent url
if (!stream.hasTrailingSlash()) {
return stream.redirect(path)
}
// get directory list
fs.readdir(path, function onReaddir (err, list) {
if (err) return stream.error(err)
// render an index for the directory
res.setHeader('Content-Type', 'text/plain; charset=UTF-8')
res.end(list.join('\n') + '\n')
})
}
```
### Serving from a root directory with custom error-handling
```js
var http = require('http')
var parseUrl = require('parseurl')
var send = require('@fastify/send')
var server = http.createServer(function onRequest (req, res) {
// your custom error-handling logic:
function error (err) {
res.statusCode = err.status || 500
res.end(err.message)
}
// your custom headers
function headers (res, path, stat) {
// serve all files for download
res.setHeader('Content-Disposition', 'attachment')
}
// your custom directory handling logic:
function redirect () {
res.statusCode = 301
res.setHeader('Location', req.url + '/')
res.end('Redirecting to ' + req.url + '/')
}
const http = require('node:http')
const parseUrl = require('parseurl')
const send = require('@fastify/send')
const server = http.createServer(async function onRequest (req, res) {
// transfer arbitrary files from within
// /www/example.com/public/*
send(req, parseUrl(req).pathname, { root: '/www/public' })
.on('error', error)
.on('directory', redirect)
.on('headers', headers)
.pipe(res)
const { statusCode, headers, stream, type, metadata } = await send(req, parseUrl(req).pathname, { root: '/www/public' })
switch (type) {
case 'directory': {
// your custom directory handling logic:
res.writeHead(301, {
'Location': metadata.requestPath + '/'
})
res.end('Redirecting to ' + metadata.requestPath + '/')
break
}
case 'error': {
// your custom error-handling logic:
res.writeHead(metadata.error.status ?? 500, {})
res.end(metadata.error.message)
break
}
default: {
// your custom headers
// serve all files for download
res.setHeader('Content-Disposition', 'attachment')
res.writeHead(statusCode, headers)
stream.pipe(res)
}
}
})
server.listen(3000)
@@ -308,4 +307,4 @@ server.listen(3000)
## License
[MIT](LICENSE)
Licensed under [MIT](./LICENSE).

6
backend/node_modules/@fastify/send/eslint.config.js generated vendored Normal file
View File

@@ -0,0 +1,6 @@
'use strict'
module.exports = require('neostandard')({
ignores: require('neostandard').resolveIgnoresFromGitignore(),
ts: true
})

View File

@@ -1,14 +1,15 @@
'use strict'
const http = require('http')
const http = require('node:http')
const send = require('..')
const path = require('path')
const path = require('node:path')
const indexPath = path.join(__dirname, 'index.html')
const server = http.createServer(function onRequest (req, res) {
send(req, indexPath)
.pipe(res)
const server = http.createServer(async function onRequest (req, res) {
const { statusCode, headers, stream } = await send(req, indexPath)
res.writeHead(statusCode, headers)
stream.pipe(res)
})
server.listen(3000)

View File

@@ -13,21 +13,7 @@
*/
const isUtf8MimeType = require('./lib/isUtf8MimeType').isUtf8MimeType
const mime = require('mime')
const SendStream = require('./lib/SendStream')
/**
* Return a `SendStream` for `req` and `path`.
*
* @param {object} req
* @param {string} path
* @param {object} [options]
* @return {SendStream}
* @public
*/
function send (req, path, options) {
return new SendStream(req, path, options)
}
const send = require('./lib/send').send
/**
* Module exports.
@@ -37,7 +23,6 @@ function send (req, path, options) {
module.exports = send
module.exports.default = send
module.exports.send = send
module.exports.SendStream = SendStream
module.exports.isUtf8MimeType = isUtf8MimeType
module.exports.mime = mime

View File

@@ -1,903 +0,0 @@
/*!
* send
* Copyright(c) 2012 TJ Holowaychuk
* Copyright(c) 2014-2022 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict'
const fs = require('node:fs')
const path = require('node:path')
const Stream = require('node:stream')
const util = require('node:util')
const debug = require('node:util').debuglog('send')
const decode = require('fast-decode-uri-component')
const escapeHtml = require('escape-html')
const mime = require('mime')
const ms = require('@lukeed/ms')
const { clearHeaders } = require('./clearHeaders')
const { collapseLeadingSlashes } = require('./collapseLeadingSlashes')
const { containsDotFile } = require('./containsDotFile')
const { contentRange } = require('./contentRange')
const { createHtmlDocument } = require('./createHtmlDocument')
const { createHttpError } = require('./createHttpError')
const { isUtf8MimeType } = require('./isUtf8MimeType')
const { normalizeList } = require('./normalizeList')
const { parseBytesRange } = require('./parseBytesRange')
const { parseTokenList } = require('./parseTokenList')
const { setHeaders } = require('./setHeaders')
/**
* Path function references.
* @private
*/
const extname = path.extname
const join = path.join
const normalize = path.normalize
const resolve = path.resolve
const sep = path.sep
/**
* Regular expression for identifying a bytes Range header.
* @private
*/
const BYTES_RANGE_REGEXP = /^ *bytes=/
/**
* Maximum value allowed for the max age.
* @private
*/
const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year
/**
* Regular expression to match a path with a directory up component.
* @private
*/
const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/
const ERROR_RESPONSES = {
400: createHtmlDocument('Error', 'Bad Request'),
403: createHtmlDocument('Error', 'Forbidden'),
404: createHtmlDocument('Error', 'Not Found'),
412: createHtmlDocument('Error', 'Precondition Failed'),
416: createHtmlDocument('Error', 'Range Not Satisfiable'),
500: createHtmlDocument('Error', 'Internal Server Error')
}
const validDotFilesOptions = [
'allow',
'ignore',
'deny'
]
/**
* Initialize a `SendStream` with the given `path`.
*
* @param {Request} req
* @param {String} path
* @param {object} [options]
* @private
*/
function SendStream (req, path, options) {
if (!new.target) {
return new SendStream(req, path, options)
}
Stream.call(this)
const opts = options || {}
this.options = opts
this.path = path
this.req = req
this._acceptRanges = opts.acceptRanges !== undefined
? Boolean(opts.acceptRanges)
: true
this._cacheControl = opts.cacheControl !== undefined
? Boolean(opts.cacheControl)
: true
this._etag = opts.etag !== undefined
? Boolean(opts.etag)
: true
this._dotfiles = opts.dotfiles !== undefined
? validDotFilesOptions.indexOf(opts.dotfiles)
: 1 // 'ignore'
if (this._dotfiles === -1) {
throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"')
}
this._extensions = opts.extensions !== undefined
? normalizeList(opts.extensions, 'extensions option')
: []
this._immutable = opts.immutable !== undefined
? Boolean(opts.immutable)
: false
this._index = opts.index !== undefined
? normalizeList(opts.index, 'index option')
: ['index.html']
this._lastModified = opts.lastModified !== undefined
? Boolean(opts.lastModified)
: true
this._maxage = opts.maxAge || opts.maxage
this._maxage = typeof this._maxage === 'string'
? ms.parse(this._maxage)
: Number(this._maxage)
// eslint-disable-next-line no-self-compare
this._maxage = this._maxage === this._maxage // fast path of isNaN(number)
? Math.min(Math.max(0, this._maxage), MAX_MAXAGE)
: 0
this._root = opts.root
? resolve(opts.root)
: null
}
/**
* Inherits from `Stream`.
*/
util.inherits(SendStream, Stream)
/**
* Set root `path`.
*
* @param {String} path
* @return {SendStream}
* @api private
*/
SendStream.prototype.root = function root (path) {
this._root = resolve(String(path))
debug('root %s', this._root)
return this
}
/**
* Emit error with `status`.
*
* @memberof SendStream
* @param {number} status
* @param {Error} [err]
* @this {Stream}
* @private
*/
SendStream.prototype.error = function error (status, err) {
// emit if listeners instead of responding
if (this.listenerCount('error') > 0) {
return this.emit('error', createHttpError(status, err))
}
const res = this.res
// clear existing headers
clearHeaders(res)
// add error headers
if (err && err.headers) {
setHeaders(res, err.headers)
}
const doc = ERROR_RESPONSES[status]
// send basic response
res.statusCode = status
res.setHeader('Content-Type', 'text/html; charset=UTF-8')
res.setHeader('Content-Length', doc[1])
res.setHeader('Content-Security-Policy', "default-src 'none'")
res.setHeader('X-Content-Type-Options', 'nosniff')
res.end(doc[0])
}
/**
* Check if the pathname ends with "/".
*
* @return {boolean}
* @private
*/
SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () {
return this.path[this.path.length - 1] === '/'
}
/**
* Check if this is a conditional GET request.
*
* @return {Boolean}
* @api private
*/
SendStream.prototype.isConditionalGET = function isConditionalGET () {
return this.req.headers['if-match'] ||
this.req.headers['if-unmodified-since'] ||
this.req.headers['if-none-match'] ||
this.req.headers['if-modified-since']
}
SendStream.prototype.isNotModifiedFailure = function isNotModifiedFailure () {
const req = this.req
const res = this.res
// Always return stale when Cache-Control: no-cache
// to support end-to-end reload requests
// https://tools.ietf.org/html/rfc2616#section-14.9.4
if (
'cache-control' in req.headers &&
req.headers['cache-control'].indexOf('no-cache') !== -1
) {
return false
}
// if-none-match
if ('if-none-match' in req.headers) {
const ifNoneMatch = req.headers['if-none-match']
if (ifNoneMatch === '*') {
return true
}
const etag = res.getHeader('etag')
if (typeof etag !== 'string') {
return false
}
const etagL = etag.length
const isMatching = parseTokenList(ifNoneMatch, function (match) {
const mL = match.length
if (
(etagL === mL && match === etag) ||
(etagL > mL && 'W/' + match === etag)
) {
return true
}
})
if (isMatching) {
return true
}
/**
* A recipient MUST ignore If-Modified-Since if the request contains an
* If-None-Match header field; the condition in If-None-Match is considered
* to be a more accurate replacement for the condition in If-Modified-Since,
* and the two are only combined for the sake of interoperating with older
* intermediaries that might not implement If-None-Match.
*
* @see RFC 9110 section 13.1.3
*/
return false
}
// if-modified-since
if ('if-modified-since' in req.headers) {
const ifModifiedSince = req.headers['if-modified-since']
const lastModified = res.getHeader('last-modified')
if (!lastModified || (Date.parse(lastModified) <= Date.parse(ifModifiedSince))) {
return true
}
}
return false
}
/**
* Check if the request preconditions failed.
*
* @return {boolean}
* @private
*/
SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () {
const req = this.req
const res = this.res
// if-match
const ifMatch = req.headers['if-match']
if (ifMatch) {
const etag = res.getHeader('ETag')
if (ifMatch !== '*') {
const isMatching = parseTokenList(ifMatch, function (match) {
if (
match === etag ||
'W/' + match === etag
) {
return true
}
}) || false
if (isMatching !== true) {
return true
}
}
}
// if-unmodified-since
if ('if-unmodified-since' in req.headers) {
const ifUnmodifiedSince = req.headers['if-unmodified-since']
const unmodifiedSince = Date.parse(ifUnmodifiedSince)
// eslint-disable-next-line no-self-compare
if (unmodifiedSince === unmodifiedSince) { // fast path of isNaN(number)
const lastModified = Date.parse(res.getHeader('Last-Modified'))
if (
// eslint-disable-next-line no-self-compare
lastModified !== lastModified ||// fast path of isNaN(number)
lastModified > unmodifiedSince
) {
return true
}
}
}
return false
}
/**
* Strip various content header fields for a change in entity.
*
* @private
*/
SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () {
const res = this.res
res.removeHeader('Content-Encoding')
res.removeHeader('Content-Language')
res.removeHeader('Content-Length')
res.removeHeader('Content-Range')
res.removeHeader('Content-Type')
}
/**
* Respond with 304 not modified.
*
* @api private
*/
SendStream.prototype.notModified = function notModified () {
const res = this.res
debug('not modified')
this.removeContentHeaderFields()
res.statusCode = 304
res.end()
}
/**
* Raise error that headers already sent.
*
* @api private
*/
SendStream.prototype.headersAlreadySent = function headersAlreadySent () {
const err = new Error('Can\'t set headers after they are sent.')
debug('headers already sent')
this.error(500, err)
}
/**
* Check if the request is cacheable, aka
* responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
*
* @return {Boolean}
* @api private
*/
SendStream.prototype.isCachable = function isCachable () {
const statusCode = this.res.statusCode
return (statusCode >= 200 && statusCode < 300) ||
statusCode === 304
}
/**
* Handle stat() error.
*
* @param {Error} error
* @private
*/
SendStream.prototype.onStatError = function onStatError (error) {
// POSIX throws ENAMETOOLONG and ENOTDIR, Windows only ENOENT
/* istanbul ignore next */
switch (error.code) {
case 'ENAMETOOLONG':
case 'ENOTDIR':
case 'ENOENT':
this.error(404, error)
break
default:
this.error(500, error)
break
}
}
/**
* Check if the range is fresh.
*
* @return {Boolean}
* @api private
*/
SendStream.prototype.isRangeFresh = function isRangeFresh () {
if (!('if-range' in this.req.headers)) {
return true
}
const ifRange = this.req.headers['if-range']
// if-range as etag
if (ifRange.indexOf('"') !== -1) {
const etag = this.res.getHeader('ETag')
return (etag && ifRange.indexOf(etag) !== -1) || false
}
const ifRangeTimestamp = Date.parse(ifRange)
// eslint-disable-next-line no-self-compare
if (ifRangeTimestamp !== ifRangeTimestamp) { // fast path of isNaN(number)
return false
}
// if-range as modified date
const lastModified = Date.parse(this.res.getHeader('Last-Modified'))
return (
// eslint-disable-next-line no-self-compare
lastModified !== lastModified || // fast path of isNaN(number)
lastModified <= ifRangeTimestamp
)
}
/**
* Redirect to path.
*
* @param {string} path
* @private
*/
SendStream.prototype.redirect = function redirect (path) {
const res = this.res
if (this.listenerCount('directory') > 0) {
this.emit('directory', res, path)
return
}
if (this.hasTrailingSlash()) {
this.error(403)
return
}
const loc = encodeURI(collapseLeadingSlashes(this.path + '/'))
const doc = createHtmlDocument('Redirecting', 'Redirecting to <a href="' + escapeHtml(loc) + '">' +
escapeHtml(loc) + '</a>')
// redirect
res.statusCode = 301
res.setHeader('Content-Type', 'text/html; charset=UTF-8')
res.setHeader('Content-Length', doc[1])
res.setHeader('Content-Security-Policy', "default-src 'none'")
res.setHeader('X-Content-Type-Options', 'nosniff')
res.setHeader('Location', loc)
res.end(doc[0])
}
/**
* Pipe to `res.
*
* @param {Stream} res
* @return {Stream} res
* @api public
*/
SendStream.prototype.pipe = function pipe (res) {
// root path
const root = this._root
// references
this.res = res
// decode the path
let path = decode(this.path)
if (path === null) {
this.error(400)
return res
}
// null byte(s)
if (~path.indexOf('\0')) {
this.error(400)
return res
}
let parts
if (root !== null) {
// normalize
if (path) {
path = normalize('.' + sep + path)
}
// malicious path
if (UP_PATH_REGEXP.test(path)) {
debug('malicious path "%s"', path)
this.error(403)
return res
}
// explode path parts
parts = path.split(sep)
// join / normalize from optional root dir
path = normalize(join(root, path))
} else {
// ".." is malicious without "root"
if (UP_PATH_REGEXP.test(path)) {
debug('malicious path "%s"', path)
this.error(403)
return res
}
// explode path parts
parts = normalize(path).split(sep)
// resolve the path
path = resolve(path)
}
// dotfile handling
if (
(
debug.enabled || // if debugging is enabled, then check for all cases to log allow case
this._dotfiles !== 0 // if debugging is not enabled, then only check if 'deny' or 'ignore' is set
) &&
containsDotFile(parts)
) {
switch (this._dotfiles) {
/* istanbul ignore next: unreachable, because NODE_DEBUG can not be set after process is running */
case 0: // 'allow'
debug('allow dotfile "%s"', path)
break
case 2: // 'deny'
debug('deny dotfile "%s"', path)
this.error(403)
return res
case 1: // 'ignore'
default:
debug('ignore dotfile "%s"', path)
this.error(404)
return res
}
}
// index file support
if (this._index.length && this.hasTrailingSlash()) {
this.sendIndex(path)
return res
}
this.sendFile(path)
return res
}
/**
* Transfer `path`.
*
* @param {String} path
* @api public
*/
SendStream.prototype.send = function send (path, stat) {
let len = stat.size
const options = this.options
const opts = {}
const res = this.res
const req = this.req
let offset = options.start || 0
if (res.headersSent) {
// impossible to send now
this.headersAlreadySent()
return
}
debug('pipe "%s"', path)
// set header fields
this.setHeader(path, stat)
// set content-type
this.type(path)
// conditional GET support
if (this.isConditionalGET()) {
if (this.isPreconditionFailure()) {
this.error(412)
return
}
if (this.isCachable() && this.isNotModifiedFailure()) {
this.notModified()
return
}
}
// adjust len to start/end options
len = Math.max(0, len - offset)
if (options.end !== undefined) {
const bytes = options.end - offset + 1
if (len > bytes) len = bytes
}
// Range support
if (this._acceptRanges) {
const rangeHeader = req.headers.range
if (
rangeHeader !== undefined &&
BYTES_RANGE_REGEXP.test(rangeHeader)
) {
// If-Range support
if (this.isRangeFresh()) {
// parse
const ranges = parseBytesRange(len, rangeHeader)
// unsatisfiable
if (ranges.length === 0) {
debug('range unsatisfiable')
// Content-Range
res.setHeader('Content-Range', contentRange('bytes', len))
// 416 Requested Range Not Satisfiable
return this.error(416, {
headers: { 'Content-Range': res.getHeader('Content-Range') }
})
// valid (syntactically invalid/multiple ranges are treated as a regular response)
} else if (ranges.length === 1) {
debug('range %j', ranges)
// Content-Range
res.statusCode = 206
res.setHeader('Content-Range', contentRange('bytes', len, ranges[0]))
// adjust for requested range
offset += ranges[0].start
len = ranges[0].end - ranges[0].start + 1
}
} else {
debug('range stale')
}
}
}
// clone options
for (const prop in options) {
opts[prop] = options[prop]
}
// set read options
opts.start = offset
opts.end = Math.max(offset, offset + len - 1)
// content-length
res.setHeader('Content-Length', len)
// HEAD support
if (req.method === 'HEAD') {
res.end()
return
}
this.stream(path, opts)
}
/**
* Transfer file for `path`.
*
* @param {String} path
* @api private
*/
SendStream.prototype.sendFile = function sendFile (path) {
let i = 0
const self = this
debug('stat "%s"', path)
fs.stat(path, function onstat (err, stat) {
if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {
// not found, check extensions
return next(err)
}
if (err) return self.onStatError(err)
if (stat.isDirectory()) return self.redirect(path)
self.emit('file', path, stat)
self.send(path, stat)
})
function next (err) {
if (self._extensions.length <= i) {
return err
? self.onStatError(err)
: self.error(404)
}
const p = path + '.' + self._extensions[i++]
debug('stat "%s"', p)
fs.stat(p, function (err, stat) {
if (err) return next(err)
if (stat.isDirectory()) return next()
self.emit('file', p, stat)
self.send(p, stat)
})
}
}
/**
* Transfer index for `path`.
*
* @param {String} path
* @api private
*/
SendStream.prototype.sendIndex = function sendIndex (path) {
let i = -1
const self = this
function next (err) {
if (++i >= self._index.length) {
if (err) return self.onStatError(err)
return self.error(404)
}
const p = join(path, self._index[i])
debug('stat "%s"', p)
fs.stat(p, function (err, stat) {
if (err) return next(err)
if (stat.isDirectory()) return next()
self.emit('file', p, stat)
self.send(p, stat)
})
}
next()
}
/**
* Stream `path` to the response.
*
* @param {String} path
* @param {Object} options
* @api private
*/
SendStream.prototype.stream = function stream (path, options) {
const self = this
const res = this.res
// pipe
const stream = fs.createReadStream(path, options)
this.emit('stream', stream)
stream.pipe(res)
let destroyed = false
// destroy piped stream
function destroy () {
if (destroyed) {
return
}
destroyed = true
stream.destroy()
}
res.once('finish', destroy)
// error handling
stream.on('error', function onerror (err) {
// clean up stream early
destroy()
// error
self.onStatError(err)
})
// end
stream.on('end', function onend () {
self.emit('end')
})
}
/**
* Set content-type based on `path`
* if it hasn't been explicitly set.
*
* @param {String} path
* @api private
*/
SendStream.prototype.type = function type (path) {
const res = this.res
if (res.getHeader('Content-Type')) return
const type = mime.getType(path) || mime.default_type
if (!type) {
debug('no content-type')
return
}
debug('content-type %s', type)
if (isUtf8MimeType(type)) {
res.setHeader('Content-Type', type + '; charset=UTF-8')
} else {
res.setHeader('Content-Type', type)
}
}
/**
* Set response header fields, most
* fields may be pre-defined.
*
* @param {String} path
* @param {Object} stat
* @api private
*/
SendStream.prototype.setHeader = function setHeader (path, stat) {
const res = this.res
this.emit('headers', res, path, stat)
if (this._acceptRanges && !res.getHeader('Accept-Ranges')) {
debug('accept ranges')
res.setHeader('Accept-Ranges', 'bytes')
}
if (this._cacheControl && !res.getHeader('Cache-Control')) {
let cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000)
if (this._immutable) {
cacheControl += ', immutable'
}
debug('cache-control %s', cacheControl)
res.setHeader('Cache-Control', cacheControl)
}
if (this._lastModified && !res.getHeader('Last-Modified')) {
const modified = stat.mtime.toUTCString()
debug('modified %s', modified)
res.setHeader('Last-Modified', modified)
}
if (this._etag && !res.getHeader('ETag')) {
const etag = 'W/"' + stat.size.toString(16) + '-' + stat.mtime.getTime().toString(16) + '"'
debug('etag %s', etag)
res.setHeader('ETag', etag)
}
}
/**
* Module exports.
* @public
*/
module.exports = SendStream

View File

@@ -1,21 +0,0 @@
/*!
* send
* Copyright(c) 2012 TJ Holowaychuk
* Copyright(c) 2014-2022 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict'
/**
* Clear all headers from a response.
*
* @param {object} res
* @private
*/
function clearHeaders (res) {
const headers = res.getHeaderNames()
for (let i = 0; i < headers.length; i++) {
res.removeHeader(headers[i])
}
}
exports.clearHeaders = clearHeaders

View File

@@ -19,6 +19,7 @@ function collapseLeadingSlashes (str) {
return str.slice(i - 1)
}
}
/* c8 ignore next */
}
module.exports.collapseLeadingSlashes = collapseLeadingSlashes

729
backend/node_modules/@fastify/send/lib/send.js generated vendored Normal file
View File

@@ -0,0 +1,729 @@
'use strict'
const fs = require('node:fs')
const path = require('node:path')
const stream = require('node:stream')
const debug = require('node:util').debuglog('send')
const decode = require('fast-decode-uri-component')
const escapeHtml = require('escape-html')
const mime = require('mime')
const ms = require('@lukeed/ms')
const { collapseLeadingSlashes } = require('./collapseLeadingSlashes')
const { containsDotFile } = require('../lib/containsDotFile')
const { contentRange } = require('../lib/contentRange')
const { createHtmlDocument } = require('../lib/createHtmlDocument')
const { isUtf8MimeType } = require('../lib/isUtf8MimeType')
const { normalizeList } = require('../lib/normalizeList')
const { parseBytesRange } = require('../lib/parseBytesRange')
const { parseTokenList } = require('./parseTokenList')
const { createHttpError } = require('./createHttpError')
/**
* Path function references.
* @private
*/
const extname = path.extname
const join = path.join
const normalize = path.normalize
const resolve = path.resolve
const sep = path.sep
/**
* Stream function references.
* @private
*/
const Readable = stream.Readable
/**
* Regular expression for identifying a bytes Range header.
* @private
*/
const BYTES_RANGE_REGEXP = /^ *bytes=/
/**
* Maximum value allowed for the max age.
* @private
*/
const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year
/**
* Regular expression to match a path with a directory up component.
* @private
*/
const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/
const ERROR_RESPONSES = {
400: createHtmlDocument('Error', 'Bad Request'),
403: createHtmlDocument('Error', 'Forbidden'),
404: createHtmlDocument('Error', 'Not Found'),
412: createHtmlDocument('Error', 'Precondition Failed'),
416: createHtmlDocument('Error', 'Range Not Satisfiable'),
500: createHtmlDocument('Error', 'Internal Server Error')
}
const validDotFilesOptions = [
'allow',
'ignore',
'deny'
]
function normalizeMaxAge (_maxage) {
let maxage
if (typeof _maxage === 'string') {
maxage = ms.parse(_maxage)
} else {
maxage = Number(_maxage)
}
// eslint-disable-next-line no-self-compare
if (maxage !== maxage) {
// fast path of isNaN(number)
return 0
}
return Math.min(Math.max(0, maxage), MAX_MAXAGE)
}
function normalizeOptions (options) {
options = options ?? {}
const acceptRanges = options.acceptRanges !== undefined
? Boolean(options.acceptRanges)
: true
const cacheControl = options.cacheControl !== undefined
? Boolean(options.cacheControl)
: true
const contentType = options.contentType !== undefined
? Boolean(options.contentType)
: true
const etag = options.etag !== undefined
? Boolean(options.etag)
: true
const dotfiles = options.dotfiles !== undefined
? validDotFilesOptions.indexOf(options.dotfiles)
: 1 // 'ignore'
if (dotfiles === -1) {
throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"')
}
const extensions = options.extensions !== undefined
? normalizeList(options.extensions, 'extensions option')
: []
const immutable = options.immutable !== undefined
? Boolean(options.immutable)
: false
const index = options.index !== undefined
? normalizeList(options.index, 'index option')
: ['index.html']
const lastModified = options.lastModified !== undefined
? Boolean(options.lastModified)
: true
const maxage = normalizeMaxAge(options.maxAge ?? options.maxage)
const maxContentRangeChunkSize = options.maxContentRangeChunkSize !== undefined
? Number(options.maxContentRangeChunkSize)
: null
const root = options.root
? resolve(options.root)
: null
const highWaterMark = Number.isSafeInteger(options.highWaterMark) && options.highWaterMark > 0
? options.highWaterMark
: null
return {
acceptRanges,
cacheControl,
contentType,
etag,
dotfiles,
extensions,
immutable,
index,
lastModified,
maxage,
maxContentRangeChunkSize,
root,
highWaterMark,
start: options.start,
end: options.end
}
}
function normalizePath (_path, root) {
// decode the path
let path = decode(_path)
if (path == null) {
return { statusCode: 400 }
}
// null byte(s)
if (~path.indexOf('\0')) {
return { statusCode: 400 }
}
let parts
if (root !== null) {
// normalize
if (path) {
path = normalize('.' + sep + path)
}
// malicious path
if (UP_PATH_REGEXP.test(path)) {
debug('malicious path "%s"', path)
return { statusCode: 403 }
}
// explode path parts
parts = path.split(sep)
// join / normalize from optional root dir
path = normalize(join(root, path))
} else {
// ".." is malicious without "root"
if (UP_PATH_REGEXP.test(path)) {
debug('malicious path "%s"', path)
return { statusCode: 403 }
}
// explode path parts
parts = normalize(path).split(sep)
// resolve the path
path = resolve(path)
}
return { path, parts }
}
/**
* Check if the pathname ends with "/".
*
* @return {boolean}
* @private
*/
function hasTrailingSlash (path) {
return path[path.length - 1] === '/'
}
/**
* Check if this is a conditional GET request.
*
* @return {Boolean}
* @api private
*/
function isConditionalGET (request) {
return request.headers['if-match'] ||
request.headers['if-unmodified-since'] ||
request.headers['if-none-match'] ||
request.headers['if-modified-since']
}
function isNotModifiedFailure (request, headers) {
// Always return stale when Cache-Control: no-cache
// to support end-to-end reload requests
// https://tools.ietf.org/html/rfc2616#section-14.9.4
if (
'cache-control' in request.headers &&
request.headers['cache-control'].indexOf('no-cache') !== -1
) {
return false
}
// if-none-match
if ('if-none-match' in request.headers) {
const ifNoneMatch = request.headers['if-none-match']
if (ifNoneMatch === '*') {
return true
}
const etag = headers.ETag
if (typeof etag !== 'string') {
return false
}
const etagL = etag.length
const isMatching = parseTokenList(ifNoneMatch, function (match) {
const mL = match.length
if (
(etagL === mL && match === etag) ||
(etagL > mL && 'W/' + match === etag)
) {
return true
}
})
if (isMatching) {
return true
}
/**
* A recipient MUST ignore If-Modified-Since if the request contains an
* If-None-Match header field; the condition in If-None-Match is considered
* to be a more accurate replacement for the condition in If-Modified-Since,
* and the two are only combined for the sake of interoperating with older
* intermediaries that might not implement If-None-Match.
*
* @see RFC 9110 section 13.1.3
*/
return false
}
// if-modified-since
if ('if-modified-since' in request.headers) {
const ifModifiedSince = request.headers['if-modified-since']
const lastModified = headers['Last-Modified']
if (!lastModified || (Date.parse(lastModified) <= Date.parse(ifModifiedSince))) {
return true
}
}
return false
}
/**
* Check if the request preconditions failed.
*
* @return {boolean}
* @private
*/
function isPreconditionFailure (request, headers) {
// if-match
const ifMatch = request.headers['if-match']
if (ifMatch) {
const etag = headers.ETag
if (ifMatch !== '*') {
const isMatching = parseTokenList(ifMatch, function (match) {
if (
match === etag ||
'W/' + match === etag
) {
return true
}
}) || false
if (isMatching !== true) {
return true
}
}
}
// if-unmodified-since
if ('if-unmodified-since' in request.headers) {
const ifUnmodifiedSince = request.headers['if-unmodified-since']
const unmodifiedSince = Date.parse(ifUnmodifiedSince)
// eslint-disable-next-line no-self-compare
if (unmodifiedSince === unmodifiedSince) { // fast path of isNaN(number)
const lastModified = Date.parse(headers['Last-Modified'])
if (
// eslint-disable-next-line no-self-compare
lastModified !== lastModified ||// fast path of isNaN(number)
lastModified > unmodifiedSince
) {
return true
}
}
}
return false
}
/**
* Check if the range is fresh.
*
* @return {Boolean}
* @api private
*/
function isRangeFresh (request, headers) {
if (!('if-range' in request.headers)) {
return true
}
const ifRange = request.headers['if-range']
// if-range as etag
if (ifRange.indexOf('"') !== -1) {
const etag = headers.ETag
return (etag && ifRange.indexOf(etag) !== -1) || false
}
const ifRangeTimestamp = Date.parse(ifRange)
// eslint-disable-next-line no-self-compare
if (ifRangeTimestamp !== ifRangeTimestamp) { // fast path of isNaN(number)
return false
}
// if-range as modified date
const lastModified = Date.parse(headers['Last-Modified'])
return (
// eslint-disable-next-line no-self-compare
lastModified !== lastModified || // fast path of isNaN(number)
lastModified <= ifRangeTimestamp
)
}
// we provide stat function that will always resolve
// without throwing
function tryStat (path) {
return new Promise((resolve) => {
fs.stat(path, function onstat (error, stat) {
resolve({ error, stat })
})
})
}
function sendError (statusCode, err) {
const headers = {}
// add error headers
if (err && err.headers) {
for (const headerName in err.headers) {
headers[headerName] = err.headers[headerName]
}
}
const doc = ERROR_RESPONSES[statusCode]
// basic response
headers['Content-Type'] = 'text/html; charset=utf-8'
headers['Content-Length'] = doc[1]
headers['Content-Security-Policy'] = "default-src 'none'"
headers['X-Content-Type-Options'] = 'nosniff'
return {
statusCode,
headers,
stream: Readable.from(doc[0]),
// metadata
type: 'error',
metadata: { error: createHttpError(statusCode, err) }
}
}
function sendStatError (err) {
// POSIX throws ENAMETOOLONG and ENOTDIR, Windows only ENOENT
/* c8 ignore start */
switch (err.code) {
case 'ENAMETOOLONG':
case 'ENOTDIR':
case 'ENOENT':
return sendError(404, err)
default:
return sendError(500, err)
}
/* c8 ignore stop */
}
/**
* Respond with 304 not modified.
*
* @api private
*/
function sendNotModified (headers, path, stat) {
debug('not modified')
delete headers['Content-Encoding']
delete headers['Content-Language']
delete headers['Content-Length']
delete headers['Content-Range']
delete headers['Content-Type']
return {
statusCode: 304,
headers,
stream: Readable.from(''),
// metadata
type: 'file',
metadata: { path, stat }
}
}
function sendFileDirectly (request, path, stat, options) {
let len = stat.size
let offset = options.start ?? 0
let statusCode = 200
const headers = {}
debug('send "%s"', path)
// set header fields
if (options.acceptRanges) {
debug('accept ranges')
headers['Accept-Ranges'] = 'bytes'
}
if (options.cacheControl) {
let cacheControl = 'public, max-age=' + Math.floor(options.maxage / 1000)
if (options.immutable) {
cacheControl += ', immutable'
}
debug('cache-control %s', cacheControl)
headers['Cache-Control'] = cacheControl
}
if (options.lastModified) {
const modified = stat.mtime.toUTCString()
debug('modified %s', modified)
headers['Last-Modified'] = modified
}
if (options.etag) {
const etag = 'W/"' + stat.size.toString(16) + '-' + stat.mtime.getTime().toString(16) + '"'
debug('etag %s', etag)
headers.ETag = etag
}
// set content-type
if (options.contentType) {
let type = mime.getType(path) || mime.default_type
debug('content-type %s', type)
if (type && isUtf8MimeType(type)) {
type += '; charset=utf-8'
}
if (type) {
headers['Content-Type'] = type
}
}
// conditional GET support
if (isConditionalGET(request)) {
if (isPreconditionFailure(request, headers)) {
return sendError(412)
}
if (isNotModifiedFailure(request, headers)) {
return sendNotModified(headers, path, stat)
}
}
// adjust len to start/end options
len = Math.max(0, len - offset)
if (options.end !== undefined) {
const bytes = options.end - offset + 1
if (len > bytes) len = bytes
}
// Range support
if (options.acceptRanges) {
const rangeHeader = request.headers.range
if (
rangeHeader !== undefined &&
BYTES_RANGE_REGEXP.test(rangeHeader)
) {
// If-Range support
if (isRangeFresh(request, headers)) {
// parse
const ranges = parseBytesRange(len, rangeHeader)
// unsatisfiable
if (ranges.length === 0) {
debug('range unsatisfiable')
// Content-Range
headers['Content-Range'] = contentRange('bytes', len)
// 416 Requested Range Not Satisfiable
return sendError(416, {
headers: { 'Content-Range': headers['Content-Range'] }
})
// valid (syntactically invalid/multiple ranges are treated as a regular response)
} else if (ranges.length === 1) {
debug('range %j', ranges)
// Content-Range
statusCode = 206
if (options.maxContentRangeChunkSize) {
ranges[0].end = Math.min(ranges[0].end, ranges[0].start + options.maxContentRangeChunkSize - 1)
}
headers['Content-Range'] = contentRange('bytes', len, ranges[0])
// adjust for requested range
offset += ranges[0].start
len = ranges[0].end - ranges[0].start + 1
}
} else {
debug('range stale')
}
}
}
// content-length
headers['Content-Length'] = len
// HEAD support
if (request.method === 'HEAD') {
return {
statusCode,
headers,
stream: Readable.from(''),
// metadata
type: 'file',
metadata: { path, stat }
}
}
const stream = fs.createReadStream(path, {
highWaterMark: options.highWaterMark,
start: offset,
end: Math.max(offset, offset + len - 1)
})
return {
statusCode,
headers,
stream,
// metadata
type: 'file',
metadata: { path, stat }
}
}
function sendRedirect (path, options) {
if (hasTrailingSlash(options.path)) {
return sendError(403)
}
const loc = encodeURI(collapseLeadingSlashes(options.path + '/'))
const doc = createHtmlDocument('Redirecting', 'Redirecting to ' + escapeHtml(loc))
const headers = {}
headers['Content-Type'] = 'text/html; charset=utf-8'
headers['Content-Length'] = doc[1]
headers['Content-Security-Policy'] = "default-src 'none'"
headers['X-Content-Type-Options'] = 'nosniff'
headers.Location = loc
return {
statusCode: 301,
headers,
stream: Readable.from(doc[0]),
// metadata
type: 'directory',
metadata: { requestPath: options.path, path }
}
}
async function sendIndex (request, path, options) {
let err
for (let i = 0; i < options.index.length; i++) {
const index = options.index[i]
const p = join(path, index)
const { error, stat } = await tryStat(p)
if (error) {
err = error
continue
}
if (stat.isDirectory()) continue
return sendFileDirectly(request, p, stat, options)
}
if (err) {
return sendStatError(err)
}
return sendError(404)
}
async function sendFile (request, path, options) {
const { error, stat } = await tryStat(path)
if (error && error.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {
let err = error
// not found, check extensions
for (let i = 0; i < options.extensions.length; i++) {
const extension = options.extensions[i]
const p = path + '.' + extension
const { error, stat } = await tryStat(p)
if (error) {
err = error
continue
}
if (stat.isDirectory()) {
err = null
continue
}
return sendFileDirectly(request, p, stat, options)
}
if (err) {
return sendStatError(err)
}
return sendError(404)
}
if (error) return sendStatError(error)
if (stat.isDirectory()) return sendRedirect(path, options)
return sendFileDirectly(request, path, stat, options)
}
async function send (request, _path, options) {
const opts = normalizeOptions(options)
opts.path = _path
const parsed = normalizePath(_path, opts.root)
const { path, parts } = parsed
if (parsed.statusCode !== undefined) {
return sendError(parsed.statusCode)
}
// dotfile handling
if (
(
debug.enabled || // if debugging is enabled, then check for all cases to log allow case
opts.dotfiles !== 0 // if debugging is not enabled, then only check if 'deny' or 'ignore' is set
) &&
containsDotFile(parts)
) {
switch (opts.dotfiles) {
/* c8 ignore start */ /* unreachable, because NODE_DEBUG can not be set after process is running */
case 0: // 'allow'
debug('allow dotfile "%s"', path)
break
/* c8 ignore stop */
case 2: // 'deny'
debug('deny dotfile "%s"', path)
return sendError(403)
case 1: // 'ignore'
default:
debug('ignore dotfile "%s"', path)
return sendError(404)
}
}
// index file support
if (opts.index.length && hasTrailingSlash(_path)) {
return sendIndex(request, path, opts)
}
return sendFile(request, path, opts)
}
module.exports.send = send

View File

@@ -1,20 +0,0 @@
'use strict'
/**
* Set an object of headers on a response.
*
* @param {object} res
* @param {object} headers
* @private
*/
function setHeaders (res, headers) {
const keys = Object.keys(headers)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
res.setHeader(key, headers[key])
}
}
module.exports.setHeaders = setHeaders

View File

@@ -1,50 +1,78 @@
{
"name": "@fastify/send",
"description": "Better streaming static file server with Range and conditional-GET support",
"version": "2.1.0",
"version": "4.1.0",
"author": "TJ Holowaychuk <tj@vision-media.ca>",
"contributors": [
"Douglas Christopher Wilson <doug@somethingdoug.com>",
"James Wyatt Cready <jcready@gmail.com>",
"Jesús Leganés Combarro <piranna@gmail.com>"
"Jesús Leganés Combarro <piranna@gmail.com>",
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
},
{
"name": "Aras Abbasi",
"email": "aras.abbasi@gmail.com"
}
],
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/send.git"
},
"bugs": {
"url": "https://github.com/fastify/send/issues"
},
"homepage": "https://github.com/fastify/send#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"keywords": [
"static",
"file",
"server"
],
"dependencies": {
"@lukeed/ms": "^2.0.2",
"escape-html": "~1.0.3",
"fast-decode-uri-component": "^1.0.1",
"http-errors": "2.0.0",
"mime": "^3.0.0",
"@lukeed/ms": "^2.0.1"
"http-errors": "^2.0.0",
"mime": "^3"
},
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
"@types/node": "^18.11.18",
"@fastify/pre-commit": "^2.1.0",
"@types/node": "^22.0.0",
"after": "0.8.2",
"benchmark": "^2.1.4",
"snazzy": "^9.0.0",
"standard": "^17.0.0",
"supertest": "6.3.3",
"tap": "^16.3.3",
"tsd": "^0.28.0"
"c8": "^10.1.3",
"eslint": "^9.17.0",
"neostandard": "^0.12.0",
"supertest": "6.3.4",
"tsd": "^0.32.0"
},
"scripts": {
"lint": "standard | snazzy",
"lint:fix": "standard --fix | snazzy",
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run test:unit && npm run test:typescript",
"test:coverage": "tap --coverage-report=html",
"test:coverage": "c8 --reporter html node --test",
"test:typescript": "tsd",
"test:unit": "tap"
"test:unit": "c8 --100 node --test"
},
"pre-commit": [
"lint",

File diff suppressed because it is too large Load Diff

View File

@@ -1,628 +0,0 @@
'use strict'
const { test } = require('tap')
const fs = require('fs')
const http = require('http')
const path = require('path')
const request = require('supertest')
const SendStream = require('..').SendStream
const { shouldNotHaveHeader, createServer } = require('./utils')
// test server
const fixtures = path.join(__dirname, 'fixtures')
test('SendStream(file, options)', function (t) {
t.plan(10)
t.test('acceptRanges', function (t) {
t.plan(2)
t.test('should support disabling accept-ranges', function (t) {
t.plan(2)
request(createServer({ acceptRanges: false, root: fixtures }))
.get('/nums.txt')
.expect(shouldNotHaveHeader('Accept-Ranges', t))
.expect(200, err => t.error(err))
})
t.test('should ignore requested range', function (t) {
t.plan(3)
request(createServer({ acceptRanges: false, root: fixtures }))
.get('/nums.txt')
.set('Range', 'bytes=0-2')
.expect(shouldNotHaveHeader('Accept-Ranges', t))
.expect(shouldNotHaveHeader('Content-Range', t))
.expect(200, '123456789', err => t.error(err))
})
})
t.test('cacheControl', function (t) {
t.plan(2)
t.test('should support disabling cache-control', function (t) {
t.plan(2)
request(createServer({ cacheControl: false, root: fixtures }))
.get('/name.txt')
.expect(shouldNotHaveHeader('Cache-Control', t))
.expect(200, err => t.error(err))
})
t.test('should ignore maxAge option', function (t) {
t.plan(2)
request(createServer({ cacheControl: false, maxAge: 1000, root: fixtures }))
.get('/name.txt')
.expect(shouldNotHaveHeader('Cache-Control', t))
.expect(200, err => t.error(err))
})
})
t.test('etag', function (t) {
t.plan(1)
t.test('should support disabling etags', function (t) {
t.plan(2)
request(createServer({ etag: false, root: fixtures }))
.get('/name.txt')
.expect(shouldNotHaveHeader('ETag', t))
.expect(200, err => t.error(err))
})
})
t.test('extensions', function (t) {
t.plan(9)
t.test('should reject numbers', function (t) {
t.plan(1)
request(createServer({ extensions: 42, root: fixtures }))
.get('/pets/')
.expect(500, /TypeError: extensions option/, err => t.error(err))
})
t.test('should reject true', function (t) {
t.plan(1)
request(createServer({ extensions: true, root: fixtures }))
.get('/pets/')
.expect(500, /TypeError: extensions option/, err => t.error(err))
})
t.test('should be not be enabled by default', function (t) {
t.plan(1)
request(createServer({ root: fixtures }))
.get('/tobi')
.expect(404, err => t.error(err))
})
t.test('should be configurable', function (t) {
t.plan(1)
request(createServer({ extensions: 'txt', root: fixtures }))
.get('/name')
.expect(200, 'tobi', err => t.error(err))
})
t.test('should support disabling extensions', function (t) {
t.plan(1)
request(createServer({ extensions: false, root: fixtures }))
.get('/name')
.expect(404, err => t.error(err))
})
t.test('should support fallbacks', function (t) {
t.plan(1)
request(createServer({ extensions: ['htm', 'html', 'txt'], root: fixtures }))
.get('/name')
.expect(200, '<p>tobi</p>', err => t.error(err))
})
t.test('should 404 if nothing found', function (t) {
t.plan(1)
request(createServer({ extensions: ['htm', 'html', 'txt'], root: fixtures }))
.get('/bob')
.expect(404, err => t.error(err))
})
t.test('should skip directories', function (t) {
t.plan(1)
request(createServer({ extensions: ['file', 'dir'], root: fixtures }))
.get('/name')
.expect(404, err => t.error(err))
})
t.test('should not search if file has extension', function (t) {
t.plan(1)
request(createServer({ extensions: 'html', root: fixtures }))
.get('/thing.html')
.expect(404, err => t.error(err))
})
})
t.test('lastModified', function (t) {
t.plan(1)
t.test('should support disabling last-modified', function (t) {
t.plan(2)
request(createServer({ lastModified: false, root: fixtures }))
.get('/name.txt')
.expect(shouldNotHaveHeader('Last-Modified', t))
.expect(200, err => t.error(err))
})
})
t.test('dotfiles', function (t) {
t.plan(5)
t.test('should default to "ignore"', function (t) {
t.plan(1)
request(createServer({ root: fixtures }))
.get('/.hidden.txt')
.expect(404, err => t.error(err))
})
t.test('should reject bad value', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'bogus' }))
.get('/name.txt')
.expect(500, /dotfiles/, err => t.error(err))
})
t.test('when "allow"', function (t) {
t.plan(3)
t.test('should SendStream dotfile', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'allow', root: fixtures }))
.get('/.hidden.txt')
.expect(200, 'secret', err => t.error(err))
})
t.test('should SendStream within dotfile directory', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'allow', root: fixtures }))
.get('/.mine/name.txt')
.expect(200, /tobi/, err => t.error(err))
})
t.test('should 404 for non-existent dotfile', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'allow', root: fixtures }))
.get('/.nothere')
.expect(404, err => t.error(err))
})
})
t.test('when "deny"', function (t) {
t.plan(10)
t.test('should 403 for dotfile', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.hidden.txt')
.expect(403, err => t.error(err))
})
t.test('should 403 for dotfile directory', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.mine')
.expect(403, err => t.error(err))
})
t.test('should 403 for dotfile directory with trailing slash', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.mine/')
.expect(403, err => t.error(err))
})
t.test('should 403 for file within dotfile directory', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.mine/name.txt')
.expect(403, err => t.error(err))
})
t.test('should 403 for non-existent dotfile', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.nothere')
.expect(403, err => t.error(err))
})
t.test('should 403 for non-existent dotfile directory', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.what/name.txt')
.expect(403, err => t.error(err))
})
t.test('should 403 for dotfile in directory', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/pets/.hidden')
.expect(403, err => t.error(err))
})
t.test('should 403 for dotfile in dotfile directory', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.mine/.hidden')
.expect(403, err => t.error(err))
})
t.test('should SendStream files in root dotfile directory', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'deny', root: path.join(fixtures, '.mine') }))
.get('/name.txt')
.expect(200, /tobi/, err => t.error(err))
})
t.test('should 403 for dotfile without root', function (t) {
t.plan(1)
const server = http.createServer(function onRequest (req, res) {
new SendStream(req, fixtures + '/.mine' + req.url, { dotfiles: 'deny' }).pipe(res)
})
request(server)
.get('/name.txt')
.expect(403, err => t.error(err))
})
})
t.test('when "ignore"', function (t) {
t.plan(8)
t.test('should 404 for dotfile', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.hidden.txt')
.expect(404, err => t.error(err))
})
t.test('should 404 for dotfile directory', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.mine')
.expect(404, err => t.error(err))
})
t.test('should 404 for dotfile directory with trailing slash', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.mine/')
.expect(404, err => t.error(err))
})
t.test('should 404 for file within dotfile directory', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.mine/name.txt')
.expect(404, err => t.error(err))
})
t.test('should 404 for non-existent dotfile', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.nothere')
.expect(404, err => t.error(err))
})
t.test('should 404 for non-existent dotfile directory', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.what/name.txt')
.expect(404, err => t.error(err))
})
t.test('should SendStream files in root dotfile directory', function (t) {
t.plan(1)
request(createServer({ dotfiles: 'ignore', root: path.join(fixtures, '.mine') }))
.get('/name.txt')
.expect(200, /tobi/, err => t.error(err))
})
t.test('should 404 for dotfile without root', function (t) {
t.plan(1)
const server = http.createServer(function onRequest (req, res) {
new SendStream(req, fixtures + '/.mine' + req.url, { dotfiles: 'ignore' }).pipe(res)
})
request(server)
.get('/name.txt')
.expect(404, err => t.error(err))
})
})
})
t.test('immutable', function (t) {
t.plan(2)
t.test('should default to false', function (t) {
t.plan(1)
request(createServer({ root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=0', err => t.error(err))
})
t.test('should set immutable directive in Cache-Control', function (t) {
t.plan(1)
request(createServer({ immutable: true, maxAge: '1h', root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=3600, immutable', err => t.error(err))
})
})
t.test('maxAge', function (t) {
t.plan(4)
t.test('should default to 0', function (t) {
t.plan(1)
request(createServer({ root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=0', err => t.error(err))
})
t.test('should floor to integer', function (t) {
t.plan(1)
request(createServer({ maxAge: 123956, root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=123', err => t.error(err))
})
t.test('should accept string', function (t) {
t.plan(1)
request(createServer({ maxAge: '30d', root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=2592000', err => t.error(err))
})
t.test('should max at 1 year', function (t) {
t.plan(1)
request(createServer({ maxAge: '2y', root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=31536000', err => t.error(err))
})
})
t.test('index', function (t) {
t.plan(10)
t.test('should reject numbers', function (t) {
t.plan(1)
request(createServer({ root: fixtures, index: 42 }))
.get('/pets/')
.expect(500, /TypeError: index option/, err => t.error(err))
})
t.test('should reject true', function (t) {
t.plan(1)
request(createServer({ root: fixtures, index: true }))
.get('/pets/')
.expect(500, /TypeError: index option/, err => t.error(err))
})
t.test('should default to index.html', function (t) {
t.plan(1)
request(createServer({ root: fixtures }))
.get('/pets/')
.expect(fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'), err => t.error(err))
})
t.test('should be configurable', function (t) {
t.plan(1)
request(createServer({ root: fixtures, index: 'tobi.html' }))
.get('/')
.expect(200, '<p>tobi</p>', err => t.error(err))
})
t.test('should support disabling', function (t) {
t.plan(1)
request(createServer({ root: fixtures, index: false }))
.get('/pets/')
.expect(403, err => t.error(err))
})
t.test('should support fallbacks', function (t) {
t.plan(1)
request(createServer({ root: fixtures, index: ['default.htm', 'index.html'] }))
.get('/pets/')
.expect(200, fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'), err => t.error(err))
})
t.test('should 404 if no index file found (file)', function (t) {
t.plan(1)
request(createServer({ root: fixtures, index: 'default.htm' }))
.get('/pets/')
.expect(404, err => t.error(err))
})
t.test('should 404 if no index file found (dir)', function (t) {
t.plan(1)
request(createServer({ root: fixtures, index: 'pets' }))
.get('/')
.expect(404, err => t.error(err))
})
t.test('should not follow directories', function (t) {
t.plan(1)
request(createServer({ root: fixtures, index: ['pets', 'name.txt'] }))
.get('/')
.expect(200, 'tobi', err => t.error(err))
})
t.test('should work without root', function (t) {
t.plan(1)
const server = http.createServer(function (req, res) {
const p = path.join(fixtures, 'pets').replace(/\\/g, '/') + '/'
new SendStream(req, p, { index: ['index.html'] })
.pipe(res)
})
request(server)
.get('/')
.expect(200, /tobi/, err => t.error(err))
})
})
t.test('root', function (t) {
t.plan(2)
t.test('when given', function (t) {
t.plan(8)
t.test('should join root', function (t) {
t.plan(1)
request(createServer({ root: fixtures }))
.get('/pets/../name.txt')
.expect(200, 'tobi', err => t.error(err))
})
t.test('should work with trailing slash', function (t) {
t.plan(1)
const app = http.createServer(function (req, res) {
new SendStream(req, req.url, { root: fixtures + '/' })
.pipe(res)
})
request(app)
.get('/name.txt')
.expect(200, 'tobi', err => t.error(err))
})
t.test('should work with empty path', function (t) {
t.plan(1)
const app = http.createServer(function (req, res) {
new SendStream(req, '', { root: fixtures })
.pipe(res)
})
request(app)
.get('/name.txt')
.expect(301, /Redirecting to/, err => t.error(err))
})
//
// NOTE: This is not a real part of the API, but
// over time this has become something users
// are doing, so this will prevent unseen
// regressions around this use-case.
//
t.test('should try as file with empty path', function (t) {
t.plan(1)
const app = http.createServer(function (req, res) {
new SendStream(req, '', { root: path.join(fixtures, 'name.txt') })
.pipe(res)
})
request(app)
.get('/')
.expect(200, 'tobi', err => t.error(err))
})
t.test('should restrict paths to within root', function (t) {
t.plan(1)
request(createServer({ root: fixtures }))
.get('/pets/../../SendStream.js')
.expect(403, err => t.error(err))
})
t.test('should allow .. in root', function (t) {
t.plan(1)
const app = http.createServer(function (req, res) {
new SendStream(req, req.url, { root: fixtures + '/../fixtures' })
.pipe(res)
})
request(app)
.get('/pets/../../SendStream.js')
.expect(403, err => t.error(err))
})
t.test('should not allow root transversal', function (t) {
t.plan(1)
request(createServer({ root: path.join(fixtures, 'name.d') }))
.get('/../name.dir/name.txt')
.expect(403, err => t.error(err))
})
t.test('should not allow root path disclosure', function (t) {
t.plan(1)
request(createServer({ root: fixtures }))
.get('/pets/../../fixtures/name.txt')
.expect(403, err => t.error(err))
})
})
t.test('when missing', function (t) {
t.plan(2)
t.test('should consider .. malicious', function (t) {
t.plan(1)
const app = http.createServer(function (req, res) {
new SendStream(req, fixtures + req.url)
.pipe(res)
})
request(app)
.get('/../SendStream.js')
.expect(403, err => t.error(err))
})
t.test('should still serve files with dots in name', function (t) {
t.plan(1)
const app = http.createServer(function (req, res) {
new SendStream(req, fixtures + req.url)
.pipe(res)
})
request(app)
.get('/do..ts.txt')
.expect(200, '...', err => t.error(err))
})
})
})
})

View File

@@ -1,6 +1,6 @@
'use strict'
const { test } = require('tap')
const { test } = require('node:test')
const { collapseLeadingSlashes } = require('../lib/collapseLeadingSlashes')
test('collapseLeadingSlashes', function (t) {
@@ -16,7 +16,7 @@ test('collapseLeadingSlashes', function (t) {
]
t.plan(testCases.length)
for (let i = 0; i < testCases.length; ++i) {
t.strictSame(collapseLeadingSlashes(testCases[i][0]), testCases[i][1])
for (const testCase of testCases) {
t.assert.deepStrictEqual(collapseLeadingSlashes(testCase[0]), testCase[1])
}
})

View File

@@ -1,13 +0,0 @@
'use strict'
const { test } = require('tap')
const SendStream = require('../index').SendStream
test('constructor', function (t) {
t.plan(1)
t.test('SendStream without new returns SendStream instance', function (t) {
t.plan(1)
t.ok(SendStream({}, '/', {}) instanceof SendStream)
})
})

View File

@@ -1,6 +1,6 @@
'use strict'
const { test } = require('tap')
const { test } = require('node:test')
const { containsDotFile } = require('../lib/containsDotFile')
test('containsDotFile', function (t) {
@@ -12,7 +12,7 @@ test('containsDotFile', function (t) {
]
t.plan(testCases.length)
for (let i = 0; i < testCases.length; ++i) {
t.strictSame(containsDotFile(testCases[i][0].split('/')), testCases[i][1], testCases[i][0])
for (const testCase of testCases) {
t.assert.deepStrictEqual(containsDotFile(testCase[0].split('/')), testCase[1], testCase[0])
}
})

View File

@@ -1,6 +1,6 @@
'use strict'
const { test } = require('tap')
const { test } = require('node:test')
const { isUtf8MimeType } = require('../lib/isUtf8MimeType')
test('isUtf8MimeType', function (t) {
@@ -16,7 +16,7 @@ test('isUtf8MimeType', function (t) {
]
t.plan(testCases.length)
for (let i = 0; i < testCases.length; ++i) {
t.strictSame(isUtf8MimeType(testCases[i][0], 'test'), testCases[i][1])
for (const testCase of testCases) {
t.assert.deepStrictEqual(isUtf8MimeType(testCase[0], 'test'), testCase[1])
}
})

View File

@@ -1,59 +1,56 @@
'use strict'
const { test } = require('tap')
const path = require('path')
const { test } = require('node:test')
const path = require('node:path')
const request = require('supertest')
const send = require('..')
const { shouldNotHaveHeader, createServer } = require('./utils')
const fixtures = path.join(__dirname, 'fixtures')
test('send.mime', function (t) {
test('send.mime', async function (t) {
t.plan(2)
t.test('should be exposed', function (t) {
await t.test('should be exposed', function (t) {
t.plan(1)
t.ok(send.mime)
t.assert.ok(send.mime)
})
t.test('.default_type', function (t) {
await t.test('.default_type', async function (t) {
t.plan(3)
t.before(function () {
t.before(() => {
this.default_type = send.mime.default_type
})
t.afterEach(function () {
t.afterEach(() => {
send.mime.default_type = this.default_type
})
t.test('should change the default type', function (t) {
t.plan(1)
await t.test('should change the default type', async function (t) {
send.mime.default_type = 'text/plain'
request(createServer({ root: fixtures }))
await request(createServer({ root: fixtures }))
.get('/no_ext')
.expect('Content-Type', 'text/plain; charset=UTF-8')
.expect(200, err => t.error(err))
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect(200)
})
t.test('should not add Content-Type for undefined default', function (t) {
t.plan(2)
await t.test('should not add Content-Type for undefined default', async function (t) {
t.plan(1)
send.mime.default_type = undefined
request(createServer({ root: fixtures }))
await request(createServer({ root: fixtures }))
.get('/no_ext')
.expect(shouldNotHaveHeader('Content-Type', t))
.expect(200, err => t.error(err))
.expect(200)
})
t.test('should return Content-Type without charset', function (t) {
t.plan(1)
request(createServer({ root: fixtures }))
await t.test('should return Content-Type without charset', async function (t) {
await request(createServer({ root: fixtures }))
.get('/images/node-js.png')
.expect('Content-Type', 'image/png')
.expect(200, err => t.error(err))
.expect(200)
})
})
})

View File

@@ -1,28 +1,28 @@
'use strict'
const { test } = require('tap')
const { test } = require('node:test')
const { normalizeList } = require('../lib/normalizeList')
test('normalizeList', function (t) {
const testCases = [
[undefined, new Error('test must be array of strings or false')],
[undefined, new TypeError('test must be array of strings or false')],
[false, []],
[[], []],
['', ['']],
[[''], ['']],
[['a'], ['a']],
['a', ['a']],
[true, new Error('test must be array of strings or false')],
[1, new Error('test must be array of strings or false')],
[[1], new Error('test must be array of strings or false')]
[true, new TypeError('test must be array of strings or false')],
[1, new TypeError('test must be array of strings or false')],
[[1], new TypeError('test must be array of strings or false')]
]
t.plan(testCases.length)
for (let i = 0; i < testCases.length; ++i) {
if (testCases[i][1] instanceof Error) {
t.throws(() => normalizeList(testCases[i][0], 'test'), testCases[i][1])
for (const testCase of testCases) {
if (testCase[1] instanceof Error) {
t.assert.throws(() => normalizeList(testCase[0], 'test'), testCase[1])
} else {
t.strictSame(normalizeList(testCases[i][0], 'test'), testCases[i][1])
t.assert.deepStrictEqual(normalizeList(testCase[0], 'test'), testCase[1])
}
}
})

View File

@@ -1,103 +1,103 @@
'use strict'
const { test } = require('tap')
const { test } = require('node:test')
const { parseBytesRange } = require('../lib/parseBytesRange')
test('parseBytesRange', function (t) {
test('parseBytesRange', async function (t) {
t.plan(13)
t.test('should return empty array if all specified ranges are invalid', function (t) {
await t.test('should return empty array if all specified ranges are invalid', function (t) {
t.plan(3)
t.strictSame(parseBytesRange(200, 'bytes=500-20'), [])
t.strictSame(parseBytesRange(200, 'bytes=500-999'), [])
t.strictSame(parseBytesRange(200, 'bytes=500-999,1000-1499'), [])
t.assert.deepStrictEqual(parseBytesRange(200, 'bytes=500-20'), [])
t.assert.deepStrictEqual(parseBytesRange(200, 'bytes=500-999'), [])
t.assert.deepStrictEqual(parseBytesRange(200, 'bytes=500-999,1000-1499'), [])
})
t.test('should parse str', function (t) {
await t.test('should parse str', function (t) {
t.plan(2)
const range = parseBytesRange(1000, 'bytes=0-499')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 0, end: 499, index: 0 })
t.assert.deepStrictEqual(range.length, 1)
t.assert.deepStrictEqual(range[0], { start: 0, end: 499, index: 0 })
})
t.test('should cap end at size', function (t) {
await t.test('should cap end at size', function (t) {
t.plan(2)
const range = parseBytesRange(200, 'bytes=0-499')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 0, end: 199, index: 0 })
t.assert.deepStrictEqual(range.length, 1)
t.assert.deepStrictEqual(range[0], { start: 0, end: 199, index: 0 })
})
t.test('should parse str', function (t) {
await t.test('should parse str', function (t) {
t.plan(2)
const range = parseBytesRange(1000, 'bytes=40-80')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 40, end: 80, index: 0 })
t.assert.deepStrictEqual(range.length, 1)
t.assert.deepStrictEqual(range[0], { start: 40, end: 80, index: 0 })
})
t.test('should parse str asking for last n bytes', function (t) {
await t.test('should parse str asking for last n bytes', function (t) {
t.plan(2)
const range = parseBytesRange(1000, 'bytes=-400')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 600, end: 999, index: 0 })
t.assert.deepStrictEqual(range.length, 1)
t.assert.deepStrictEqual(range[0], { start: 600, end: 999, index: 0 })
})
t.test('should parse str with only start', function (t) {
await t.test('should parse str with only start', function (t) {
t.plan(2)
const range = parseBytesRange(1000, 'bytes=400-')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 400, end: 999, index: 0 })
t.assert.deepStrictEqual(range.length, 1)
t.assert.deepStrictEqual(range[0], { start: 400, end: 999, index: 0 })
})
t.test('should parse "bytes=0-"', function (t) {
await t.test('should parse "bytes=0-"', function (t) {
t.plan(2)
const range = parseBytesRange(1000, 'bytes=0-')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 0, end: 999, index: 0 })
t.assert.deepStrictEqual(range.length, 1)
t.assert.deepStrictEqual(range[0], { start: 0, end: 999, index: 0 })
})
t.test('should parse str with no bytes', function (t) {
await t.test('should parse str with no bytes', function (t) {
t.plan(2)
const range = parseBytesRange(1000, 'bytes=0-0')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 0, end: 0, index: 0 })
t.assert.deepStrictEqual(range.length, 1)
t.assert.deepStrictEqual(range[0], { start: 0, end: 0, index: 0 })
})
t.test('should parse str asking for last byte', function (t) {
await t.test('should parse str asking for last byte', function (t) {
t.plan(2)
const range = parseBytesRange(1000, 'bytes=-1')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 999, end: 999, index: 0 })
t.assert.deepStrictEqual(range.length, 1)
t.assert.deepStrictEqual(range[0], { start: 999, end: 999, index: 0 })
})
t.test('should parse str with some invalid ranges', function (t) {
await t.test('should parse str with some invalid ranges', function (t) {
t.plan(2)
const range = parseBytesRange(200, 'bytes=0-499,1000-,500-999')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 0, end: 199, index: 0 })
t.assert.deepStrictEqual(range.length, 1)
t.assert.deepStrictEqual(range[0], { start: 0, end: 199, index: 0 })
})
t.test('should combine overlapping ranges', function (t) {
await t.test('should combine overlapping ranges', function (t) {
t.plan(3)
const range = parseBytesRange(150, 'bytes=0-4,90-99,5-75,100-199,101-102')
t.equal(range.length, 2)
t.strictSame(range[0], { start: 0, end: 75, index: 0 })
t.strictSame(range[1], { start: 90, end: 149, index: 1 })
t.assert.deepStrictEqual(range.length, 2)
t.assert.deepStrictEqual(range[0], { start: 0, end: 75, index: 0 })
t.assert.deepStrictEqual(range[1], { start: 90, end: 149, index: 1 })
})
t.test('should retain original order /1', function (t) {
await t.test('should retain original order /1', function (t) {
t.plan(3)
const range = parseBytesRange(150, 'bytes=90-99,5-75,100-199,101-102,0-4')
t.equal(range.length, 2)
t.strictSame(range[0], { start: 90, end: 149, index: 0 })
t.strictSame(range[1], { start: 0, end: 75, index: 1 })
t.assert.deepStrictEqual(range.length, 2)
t.assert.deepStrictEqual(range[0], { start: 90, end: 149, index: 0 })
t.assert.deepStrictEqual(range[1], { start: 0, end: 75, index: 1 })
})
t.test('should retain original order /2', function (t) {
await t.test('should retain original order /2', function (t) {
t.plan(4)
const range = parseBytesRange(150, 'bytes=-1,20-100,0-1,101-120')
t.equal(range.length, 3)
t.strictSame(range[0], { start: 149, end: 149, index: 0 })
t.strictSame(range[1], { start: 20, end: 120, index: 1 })
t.strictSame(range[2], { start: 0, end: 1, index: 2 })
t.assert.deepStrictEqual(range.length, 3)
t.assert.deepStrictEqual(range[0], { start: 149, end: 149, index: 0 })
t.assert.deepStrictEqual(range[1], { start: 20, end: 120, index: 1 })
t.assert.deepStrictEqual(range[2], { start: 0, end: 1, index: 2 })
})
})

646
backend/node_modules/@fastify/send/test/send.1.test.js generated vendored Normal file
View File

@@ -0,0 +1,646 @@
'use strict'
const { test } = require('node:test')
const fs = require('node:fs')
const http = require('node:http')
const path = require('node:path')
const request = require('supertest')
const { send } = require('..')
const { shouldNotHaveHeader, createServer } = require('./utils')
const { getDefaultHighWaterMark } = require('node:stream')
// test server
const fixtures = path.join(__dirname, 'fixtures')
test('send(file, options)', async function (t) {
t.plan(12)
await t.test('acceptRanges', async function (t) {
t.plan(6)
await t.test('should support disabling accept-ranges', async function (t) {
t.plan(1)
await request(createServer({ acceptRanges: false, root: fixtures }))
.get('/nums.txt')
.expect(shouldNotHaveHeader('Accept-Ranges', t))
.expect(200)
})
await t.test('should ignore requested range', async function (t) {
t.plan(2)
await request(createServer({ acceptRanges: false, root: fixtures }))
.get('/nums.txt')
.set('Range', 'bytes=0-2')
.expect(shouldNotHaveHeader('Accept-Ranges', t))
.expect(shouldNotHaveHeader('Content-Range', t))
.expect(200, '123456789')
})
await t.test('should limit high return size /1', async function (t) {
t.plan(3)
await request(createServer({ acceptRanges: true, maxContentRangeChunkSize: 1, root: fixtures }))
.get('/nums.txt')
.set('Range', 'bytes=0-2')
.expect((res) => t.assert.deepStrictEqual(res.headers['accept-ranges'], 'bytes'))
.expect((res) => t.assert.deepStrictEqual(res.headers['content-range'], 'bytes 0-0/9'))
.expect((res) => t.assert.deepStrictEqual(res.headers['content-length'], '1', 'should content-length must be as same as maxContentRangeChunkSize'))
.expect(206, '1')
})
await t.test('should limit high return size /2', async function (t) {
t.plan(3)
await request(createServer({ acceptRanges: true, maxContentRangeChunkSize: 1, root: fixtures }))
.get('/nums.txt')
.set('Range', 'bytes=1-2')
.expect((res) => t.assert.deepStrictEqual(res.headers['accept-ranges'], 'bytes'))
.expect((res) => t.assert.deepStrictEqual(res.headers['content-range'], 'bytes 1-1/9'))
.expect((res) => t.assert.deepStrictEqual(res.headers['content-length'], '1', 'should content-length must be as same as maxContentRangeChunkSize'))
.expect(206, '2')
})
await t.test('should limit high return size /3', async function (t) {
t.plan(3)
await request(createServer({ acceptRanges: true, maxContentRangeChunkSize: 1, root: fixtures }))
.get('/nums.txt')
.set('Range', 'bytes=1-3')
.expect((res) => t.assert.deepStrictEqual(res.headers['accept-ranges'], 'bytes'))
.expect((res) => t.assert.deepStrictEqual(res.headers['content-range'], 'bytes 1-1/9'))
.expect((res) => t.assert.deepStrictEqual(res.headers['content-length'], '1', 'should content-length must be as same as maxContentRangeChunkSize'))
.expect(206, '2')
})
await t.test('should limit high return size /4', async function (t) {
t.plan(3)
await request(createServer({ acceptRanges: true, maxContentRangeChunkSize: 4, root: fixtures }))
.get('/nums.txt')
.set('Range', 'bytes=1-2,3-6')
.expect((res) => t.assert.deepStrictEqual(res.headers['accept-ranges'], 'bytes'))
.expect((res) => t.assert.deepStrictEqual(res.headers['content-range'], 'bytes 1-4/9'))
.expect((res) => t.assert.deepStrictEqual(res.headers['content-length'], '4', 'should content-length must be as same as maxContentRangeChunkSize'))
.expect(206, '2345')
})
})
await t.test('cacheControl', async function (t) {
t.plan(2)
await t.test('should support disabling cache-control', async function (t) {
t.plan(1)
await request(createServer({ cacheControl: false, root: fixtures }))
.get('/name.txt')
.expect(shouldNotHaveHeader('Cache-Control', t))
.expect(200)
})
await t.test('should ignore maxAge option', async function (t) {
t.plan(1)
await request(createServer({ cacheControl: false, maxAge: 1000, root: fixtures }))
.get('/name.txt')
.expect(shouldNotHaveHeader('Cache-Control', t))
.expect(200)
})
})
await t.test('contentType', async function (t) {
t.plan(1)
await t.test('should support disabling content-type', async function (t) {
t.plan(1)
await request(createServer({ contentType: false, root: fixtures }))
.get('/name.txt')
.expect(shouldNotHaveHeader('Content-Type', t))
.expect(200)
})
})
await t.test('etag', async function (t) {
t.plan(1)
await t.test('should support disabling etags', async function (t) {
t.plan(1)
await request(createServer({ etag: false, root: fixtures }))
.get('/name.txt')
.expect(shouldNotHaveHeader('ETag', t))
.expect(200)
})
})
await t.test('extensions', async function (t) {
t.plan(9)
await t.test('should reject numbers', async function (t) {
await request(createServer({ extensions: 42, root: fixtures }))
.get('/pets/')
.expect(500, /TypeError: extensions option/)
})
await t.test('should reject true', async function (t) {
await request(createServer({ extensions: true, root: fixtures }))
.get('/pets/')
.expect(500, /TypeError: extensions option/)
})
await t.test('should be not be enabled by default', async function (t) {
await request(createServer({ root: fixtures }))
.get('/tobi')
.expect(404)
})
await t.test('should be configurable', async function (t) {
await request(createServer({ extensions: 'txt', root: fixtures }))
.get('/name')
.expect(200, 'tobi')
})
await t.test('should support disabling extensions', async function (t) {
await request(createServer({ extensions: false, root: fixtures }))
.get('/name')
.expect(404)
})
await t.test('should support fallbacks', async function (t) {
await request(createServer({ extensions: ['htm', 'html', 'txt'], root: fixtures }))
.get('/name')
.expect(200, '<p>tobi</p>')
})
await t.test('should 404 if nothing found', async function (t) {
await request(createServer({ extensions: ['htm', 'html', 'txt'], root: fixtures }))
.get('/bob')
.expect(404)
})
await t.test('should skip directories', async function (t) {
await request(createServer({ extensions: ['file', 'dir'], root: fixtures }))
.get('/name')
.expect(404)
})
await t.test('should not search if file has extension', async function (t) {
await request(createServer({ extensions: 'html', root: fixtures }))
.get('/thing.html')
.expect(404)
})
})
await t.test('lastModified', async function (t) {
t.plan(1)
await t.test('should support disabling last-modified', async function (t) {
t.plan(1)
await request(createServer({ lastModified: false, root: fixtures }))
.get('/name.txt')
.expect(shouldNotHaveHeader('Last-Modified', t))
.expect(200)
})
})
await t.test('dotfiles', async function (t) {
t.plan(5)
await t.test('should default to "ignore"', async function (t) {
await request(createServer({ root: fixtures }))
.get('/.hidden.txt')
.expect(404)
})
await t.test('should reject bad value', async function (t) {
await request(createServer({ dotfiles: 'bogus' }))
.get('/name.txt')
.expect(500, /dotfiles/)
})
await t.test('when "allow"', async function (t) {
t.plan(3)
await t.test('should send dotfile', async function (t) {
await request(createServer({ dotfiles: 'allow', root: fixtures }))
.get('/.hidden.txt')
.expect(200, 'secret')
})
await t.test('should send within dotfile directory', async function (t) {
await request(createServer({ dotfiles: 'allow', root: fixtures }))
.get('/.mine/name.txt')
.expect(200, /tobi/)
})
await t.test('should 404 for non-existent dotfile', async function (t) {
await request(createServer({ dotfiles: 'allow', root: fixtures }))
.get('/.nothere')
.expect(404)
})
})
await t.test('when "deny"', async function (t) {
t.plan(10)
await t.test('should 403 for dotfile', async function (t) {
await request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.hidden.txt')
.expect(403)
})
await t.test('should 403 for dotfile directory', async function (t) {
await request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.mine')
.expect(403)
})
await t.test('should 403 for dotfile directory with trailing slash', async function (t) {
await request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.mine/')
.expect(403)
})
await t.test('should 403 for file within dotfile directory', async function (t) {
await request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.mine/name.txt')
.expect(403)
})
await t.test('should 403 for non-existent dotfile', async function (t) {
await request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.nothere')
.expect(403)
})
await t.test('should 403 for non-existent dotfile directory', async function (t) {
await request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.what/name.txt')
.expect(403)
})
await t.test('should 403 for dotfile in directory', async function (t) {
await request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/pets/.hidden.txt')
.expect(403)
})
await t.test('should 403 for dotfile in dotfile directory', async function (t) {
await request(createServer({ dotfiles: 'deny', root: fixtures }))
.get('/.mine/.hidden.txt')
.expect(403)
})
await t.test('should send files in root dotfile directory', async function (t) {
await request(createServer({ dotfiles: 'deny', root: path.join(fixtures, '.mine') }))
.get('/name.txt')
.expect(200, /tobi/)
})
await t.test('should 403 for dotfile without root', async function (t) {
const server = http.createServer(async function onRequest (req, res) {
const { statusCode, headers, stream } = await send(req, fixtures + '/.mine' + req.url, { dotfiles: 'deny' })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(server)
.get('/name.txt')
.expect(403)
})
})
await t.test('when "ignore"', async function (t) {
t.plan(8)
await t.test('should 404 for dotfile', async function (t) {
await request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.hidden.txt')
.expect(404)
})
await t.test('should 404 for dotfile directory', async function (t) {
await request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.mine')
.expect(404)
})
await t.test('should 404 for dotfile directory with trailing slash', async function (t) {
await request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.mine/')
.expect(404)
})
await t.test('should 404 for file within dotfile directory', async function (t) {
await request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.mine/name.txt')
.expect(404)
})
await t.test('should 404 for non-existent dotfile', async function (t) {
await request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.nothere')
.expect(404)
})
await t.test('should 404 for non-existent dotfile directory', async function (t) {
await request(createServer({ dotfiles: 'ignore', root: fixtures }))
.get('/.what/name.txt')
.expect(404)
})
await t.test('should send files in root dotfile directory', async function (t) {
await request(createServer({ dotfiles: 'ignore', root: path.join(fixtures, '.mine') }))
.get('/name.txt')
.expect(200, /tobi/)
})
await t.test('should 404 for dotfile without root', async function (t) {
const server = http.createServer(async function onRequest (req, res) {
const { statusCode, headers, stream } = await send(req, fixtures + '/.mine' + req.url, { dotfiles: 'ignore' })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(server)
.get('/name.txt')
.expect(404)
})
})
})
await t.test('immutable', async function (t) {
t.plan(2)
await t.test('should default to false', async function (t) {
await request(createServer({ root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=0')
})
await t.test('should set immutable directive in Cache-Control', async function (t) {
await request(createServer({ immutable: true, maxAge: '1h', root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=3600, immutable')
})
})
await t.test('maxAge', async function (t) {
t.plan(4)
await t.test('should default to 0', async function (t) {
await request(createServer({ root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=0')
})
await t.test('should floor to integer', async function (t) {
await request(createServer({ maxAge: 123956, root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=123')
})
await t.test('should accept string', async function (t) {
await request(createServer({ maxAge: '30d', root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=2592000')
})
await t.test('should max at 1 year', async function (t) {
await request(createServer({ maxAge: '2y', root: fixtures }))
.get('/name.txt')
.expect('Cache-Control', 'public, max-age=31536000')
})
})
await t.test('index', async function (t) {
t.plan(10)
await t.test('should reject numbers', async function (t) {
await request(createServer({ root: fixtures, index: 42 }))
.get('/pets/')
.expect(500, /TypeError: index option/)
})
await t.test('should reject true', async function (t) {
await request(createServer({ root: fixtures, index: true }))
.get('/pets/')
.expect(500, /TypeError: index option/)
})
await t.test('should default to index.html', async function (t) {
await request(createServer({ root: fixtures }))
.get('/pets/')
.expect(fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'))
})
await t.test('should be configurable', async function (t) {
await request(createServer({ root: fixtures, index: 'tobi.html' }))
.get('/')
.expect(200, '<p>tobi</p>')
})
await t.test('should support disabling', async function (t) {
await request(createServer({ root: fixtures, index: false }))
.get('/pets/')
.expect(403)
})
await t.test('should support fallbacks', async function (t) {
await request(createServer({ root: fixtures, index: ['default.htm', 'index.html'] }))
.get('/pets/')
.expect(200, fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'))
})
await t.test('should 404 if no index file found (file)', async function (t) {
await request(createServer({ root: fixtures, index: 'default.htm' }))
.get('/pets/')
.expect(404)
})
await t.test('should 404 if no index file found (dir)', async function (t) {
await request(createServer({ root: fixtures, index: 'pets' }))
.get('/')
.expect(404)
})
await t.test('should not follow directories', async function (t) {
await request(createServer({ root: fixtures, index: ['pets', 'name.txt'] }))
.get('/')
.expect(200, 'tobi')
})
await t.test('should work without root', async function (t) {
const server = http.createServer(async function (req, res) {
const p = path.join(fixtures, 'pets').replace(/\\/g, '/') + '/'
const { statusCode, headers, stream } = await send(req, p, { index: ['index.html'] })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(server)
.get('/')
.expect(200, /tobi/)
})
})
await t.test('root', async function (t) {
t.plan(2)
await t.test('when given', async function (t) {
t.plan(8)
await t.test('should join root', async function (t) {
await request(createServer({ root: fixtures }))
.get('/pets/../name.txt')
.expect(200, 'tobi')
})
await t.test('should work with trailing slash', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures + '/' })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect(200, 'tobi')
})
await t.test('should work with empty path', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, '', { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect(301, /Redirecting to/)
})
//
// NOTE: This is not a real part of the API, but
// over time this has become something users
// are doing, so this will prevent unseen
// regressions around this use-case.
//
await t.test('should try as file with empty path', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, '', { root: path.join(fixtures, 'name.txt') })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/')
.expect(200, 'tobi')
})
await t.test('should restrict paths to within root', async function (t) {
await request(createServer({ root: fixtures }))
.get('/pets/../../send.js')
.expect(403)
})
await t.test('should allow .. in root', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures + '/../fixtures' })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/pets/../../send.js')
.expect(403)
})
await t.test('should not allow root transversal', async function (t) {
await request(createServer({ root: path.join(fixtures, 'name.d') }))
.get('/../name.dir/name.txt')
.expect(403)
})
await t.test('should not allow root path disclosure', async function (t) {
await request(createServer({ root: fixtures }))
.get('/pets/../../fixtures/name.txt')
.expect(403)
})
})
await t.test('when missing', async function (t) {
t.plan(2)
await t.test('should consider .. malicious', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, fixtures + req.url)
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/../send.js')
.expect(403)
})
await t.test('should still serve files with dots in name', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, fixtures + req.url)
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/do..ts.txt')
.expect(200, '...')
})
})
})
await t.test('highWaterMark', async function (t) {
t.plan(3)
await t.test('should support highWaterMark', async function (t) {
t.plan(1)
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { highWaterMark: 512 * 1024, root: fixtures + '/' })
res.writeHead(statusCode, headers)
t.assert.deepStrictEqual(stream.readableHighWaterMark, 524288)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect(200, 'tobi')
})
await t.test('should use default value', async function (t) {
t.plan(1)
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures + '/' })
res.writeHead(statusCode, headers)
t.assert.deepStrictEqual(stream.readableHighWaterMark, getDefaultHighWaterMark(false))
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect(200, 'tobi')
})
await t.test('should ignore negative number', async function (t) {
t.plan(1)
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { highWaterMark: -54, root: fixtures + '/' })
res.writeHead(statusCode, headers)
t.assert.deepStrictEqual(stream.readableHighWaterMark, getDefaultHighWaterMark(false))
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect(200, 'tobi')
})
})
})

977
backend/node_modules/@fastify/send/test/send.2.test.js generated vendored Normal file
View File

@@ -0,0 +1,977 @@
'use strict'
const { test } = require('node:test')
const http = require('node:http')
const path = require('node:path')
const request = require('supertest')
const send = require('../lib/send').send
const { shouldNotHaveBody, createServer, shouldNotHaveHeader } = require('./utils')
const dateRegExp = /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/
const fixtures = path.join(__dirname, 'fixtures')
test('send(file)', async function (t) {
t.plan(22)
await t.test('should stream the file contents', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect('Content-Length', '4')
.expect(200, 'tobi')
})
await t.test('should stream a zero-length file', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/empty.txt')
.expect('Content-Length', '0')
.expect(200, '')
})
await t.test('should decode the given path as a URI', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/some%20thing.txt')
.expect(200, 'hey')
})
await t.test('should serve files with dots in name', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/do..ts.txt')
.expect(200, '...')
})
await t.test('should treat a malformed URI as a bad request', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/some%99thing.txt')
.expect(400, /Bad Request/)
})
await t.test('should 400 on NULL bytes', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/some%00thing.txt')
.expect(400, /Bad Request/)
})
await t.test('should treat an ENAMETOOLONG as a 404', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const path = Array(100).join('foobar')
await request(app)
.get('/' + path)
.expect(404)
})
await t.test('should support HEAD', async function (t) {
t.plan(1)
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.head('/name.txt')
.expect(200)
.expect('Content-Length', '4')
.expect(shouldNotHaveBody(t))
})
await t.test('should add an ETag header field', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect('etag', /^W\/"[^"]+"$/)
})
await t.test('should add a Date header field', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect('date', dateRegExp)
})
await t.test('should add a Last-Modified header field', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect('last-modified', dateRegExp)
})
await t.test('should add a Accept-Ranges header field', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect('Accept-Ranges', 'bytes')
})
await t.test('should 404 if the file does not exist', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/meow')
.expect(404, /Not Found/)
})
await t.test('should 404 if the filename is too long', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const longFilename = new Array(512).fill('a').join('')
await request(app)
.get('/' + longFilename)
.expect(404, /Not Found/)
})
await t.test('should 404 if the requested resource is not a directory', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt/invalid')
.expect(404, /Not Found/)
})
await t.test('should not override content-type', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, {
...headers,
'Content-Type': 'application/x-custom'
})
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect('Content-Type', 'application/x-custom')
})
await t.test('should set Content-Type via mime map', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect(200)
await request(app)
.get('/tobi.html')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(200)
})
await t.test('send directory', async function (t) {
t.plan(5)
await t.test('should redirect directories to trailing slash', async function (t) {
await request(createServer({ root: fixtures }))
.get('/pets')
.expect('Location', '/pets/')
.expect(301)
})
await t.test('should respond with an HTML redirect', async function (t) {
await request(createServer({ root: fixtures }))
.get('/pets')
.expect('Location', '/pets/')
.expect('Content-Type', /html/)
.expect(301, />Redirecting to \/pets\/</)
})
await t.test('should respond with default Content-Security-Policy', async function (t) {
await request(createServer({ root: fixtures }))
.get('/pets')
.expect('Location', '/pets/')
.expect('Content-Security-Policy', "default-src 'none'")
.expect(301)
})
await t.test('should not redirect to protocol-relative locations', async function (t) {
await request(createServer({ root: fixtures }))
.get('//pets')
.expect('Location', '/pets/')
.expect(301)
})
await t.test('should respond with an HTML redirect', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url.replace('/snow', '/snow ☃'), { root: 'test/fixtures' })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/snow')
.expect('Location', '/snow%20%E2%98%83/')
.expect('Content-Type', /html/)
.expect(301, />Redirecting to \/snow%20%E2%98%83\/</)
})
})
await t.test('send error', async function (t) {
t.plan(2)
await t.test('should respond to errors directly', async function (t) {
await request(createServer({ root: fixtures }))
.get('/foobar')
.expect(404, />Not Found</)
})
await t.test('should respond with default Content-Security-Policy', async function (t) {
await request(createServer({ root: fixtures }))
.get('/foobar')
.expect('Content-Security-Policy', "default-src 'none'")
.expect(404)
})
})
await t.test('with conditional-GET', async function (t) {
t.plan(6)
await t.test('should remove Content headers with 304', async function (t) {
const server = createServer({ root: fixtures }, function (_req, res) {
res.setHeader('Content-Language', 'en-US')
res.setHeader('Content-Location', 'http://localhost/name.txt')
res.setHeader('Contents', 'foo')
})
const res = await request(server)
.get('/name.txt')
.expect(200)
await request(server)
.get('/name.txt')
.set('If-None-Match', res.headers.etag)
.expect('Content-Location', 'http://localhost/name.txt')
.expect('Contents', 'foo')
.expect(304)
})
await t.test('should not remove all Content-* headers', async function (t) {
const server = createServer({ root: fixtures }, function (_req, res) {
res.setHeader('Content-Location', 'http://localhost/name.txt')
res.setHeader('Content-Security-Policy', 'default-src \'self\'')
})
const res = await request(server)
.get('/name.txt')
.expect(200)
await request(server)
.get('/name.txt')
.set('If-None-Match', res.headers.etag)
.expect('Content-Location', 'http://localhost/name.txt')
.expect('Content-Security-Policy', 'default-src \'self\'')
.expect(304)
})
await t.test('where "If-Match" is set', async function (t) {
t.plan(4)
await t.test('should respond with 200 when "*"', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.set('If-Match', '*')
.expect(200)
})
await t.test('should respond with 412 when ETag unmatched', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.set('If-Match', ' "foo",, "bar" ,')
.expect(412)
})
await t.test('should respond with 200 when ETag matched /1', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/name.txt')
.expect(200)
await request(app)
.get('/name.txt')
.set('If-Match', '"foo", "bar", ' + res.headers.etag)
.expect(200)
})
await t.test('should respond with 200 when ETag matched /2', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/name.txt')
.expect(200)
await request(app)
.get('/name.txt')
.set('If-Match', '"foo", ' + res.headers.etag + ', "bar"')
.expect(200)
})
})
await t.test('where "If-Modified-Since" is set', async function (t) {
t.plan(3)
await t.test('should respond with 304 when unmodified', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/name.txt')
.expect(200)
await request(app)
.get('/name.txt')
.set('If-Modified-Since', res.headers['last-modified'])
.expect(304)
})
await t.test('should respond with 200 when modified', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/name.txt')
.expect(200)
const lmod = new Date(res.headers['last-modified'])
const date = new Date(lmod - 60000)
await request(app)
.get('/name.txt')
.set('If-Modified-Since', date.toUTCString())
.expect(200, 'tobi')
})
await t.test('should respond with 200 when modified', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/name.txt')
.expect(200)
await request(app)
.get('/name.txt')
.set('If-Modified-Since', res.headers['last-modified'])
.set('cache-control', 'no-cache')
.expect(200, 'tobi')
})
})
await t.test('where "If-None-Match" is set', async function (t) {
t.plan(6)
await t.test('should respond with 304 when ETag matched', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/name.txt')
.expect(200)
await request(app)
.get('/name.txt')
.set('If-None-Match', res.headers.etag)
.expect(304)
})
await t.test('should respond with 200 when ETag unmatched', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect(200)
await request(app)
.get('/name.txt')
.set('If-None-Match', '"123"')
.expect(200, 'tobi')
})
await t.test('should respond with 200 when ETag is not generated', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { etag: false, root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect(200)
await request(app)
.get('/name.txt')
.set('If-None-Match', '"123"')
.expect(200, 'tobi')
})
await t.test('should respond with 306 Not Modified when using wildcard * on existing file', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { etag: false, root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect(200)
await request(app)
.get('/name.txt')
.set('If-None-Match', '*')
.expect(304, '')
})
await t.test('should respond with 404 Not Found when using wildcard * on non-existing file', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { etag: false, root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/asdf.txt')
.set('If-None-Match', '*')
.expect(404, /Not Found/)
})
await t.test('should respond with 200 cache-control is set to no-cache', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/name.txt')
.expect(200)
await request(app)
.get('/name.txt')
.set('If-None-Match', res.headers.etag)
.set('cache-control', 'no-cache')
.expect(200, 'tobi')
})
})
await t.test('where "If-Unmodified-Since" is set', async function (t) {
t.plan(3)
await t.test('should respond with 200 when unmodified', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/name.txt')
.expect(200)
await request(app)
.get('/name.txt')
.set('If-Unmodified-Since', res.headers['last-modified'])
.expect(200)
})
await t.test('should respond with 412 when modified', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/name.txt')
.expect(200)
const lmod = new Date(res.headers['last-modified'])
const date = new Date(lmod - 60000).toUTCString()
await request(app)
.get('/name.txt')
.set('If-Unmodified-Since', date)
.expect(412)
})
await t.test('should respond with 200 when invalid date', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.set('If-Unmodified-Since', 'foo')
.expect(200)
})
})
})
await t.test('with Range request', async function (t) {
t.plan(13)
await t.test('should support byte ranges', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=0-4')
.expect(206, '12345')
})
await t.test('should ignore non-byte ranges', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'items=0-4')
.expect(200, '123456789')
})
await t.test('should be inclusive', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=0-0')
.expect(206, '1')
})
await t.test('should set Content-Range', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=2-5')
.expect('Content-Range', 'bytes 2-5/9')
.expect(206)
})
await t.test('should support -n', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=-3')
.expect(206, '789')
})
await t.test('should support n-', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=3-')
.expect(206, '456789')
})
await t.test('should respond with 206 "Partial Content"', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=0-4')
.expect(206)
})
await t.test('should set Content-Length to the # of octets transferred', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=2-3')
.expect('Content-Length', '2')
.expect(206, '34')
})
await t.test('when last-byte-pos of the range is greater the length', async function (t) {
t.plan(2)
await t.test('is taken to be equal to one less than the length', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=2-50')
.expect('Content-Range', 'bytes 2-8/9')
.expect(206)
})
await t.test('should adapt the Content-Length accordingly', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=2-50')
.expect('Content-Length', '7')
.expect(206)
})
})
await t.test('when the first- byte-pos of the range is greater length', async function (t) {
t.plan(2)
await t.test('should respond with 416', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=9-50')
.expect('Content-Range', 'bytes */9')
.expect(416)
})
await t.test('should emit error 416 with content-range header', async function (t) {
const server = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, {
...headers,
'X-Content-Range': headers['Content-Range']
})
stream.pipe(res)
})
await request(server)
.get('/nums.txt')
.set('Range', 'bytes=9-50')
.expect('X-Content-Range', 'bytes */9')
.expect(416)
})
})
await t.test('when syntactically invalid', async function (t) {
t.plan(1)
await t.test('should respond with 200 and the entire contents', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'asdf')
.expect(200, '123456789')
})
})
await t.test('when multiple ranges', async function (t) {
t.plan(2)
await t.test('should respond with 200 and the entire contents', async function (t) {
t.plan(1)
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=1-1,3-')
.expect(shouldNotHaveHeader('Content-Range', t))
.expect(200, '123456789')
})
await t.test('should respond with 206 is all ranges can be combined', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('Range', 'bytes=1-2,3-5')
.expect('Content-Range', 'bytes 1-5/9')
.expect(206, '23456')
})
})
await t.test('when if-range present', async function (t) {
t.plan(5)
await t.test('should respond with parts when etag unchanged', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/nums.txt')
.expect(200)
const etag = res.headers.etag
await request(app)
.get('/nums.txt')
.set('If-Range', etag)
.set('Range', 'bytes=0-0')
.expect(206, '1')
})
await t.test('should respond with 200 when etag changed', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/nums.txt')
.expect(200)
const etag = res.headers.etag.replace(/"(.)/, '"0$1')
await request(app)
.get('/nums.txt')
.set('If-Range', etag)
.set('Range', 'bytes=0-0')
.expect(200, '123456789')
})
await t.test('should respond with parts when modified unchanged', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/nums.txt')
.expect(200)
const modified = res.headers['last-modified']
await request(app)
.get('/nums.txt')
.set('If-Range', modified)
.set('Range', 'bytes=0-0')
.expect(206, '1')
})
await t.test('should respond with 200 when modified changed', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const res = await request(app)
.get('/nums.txt')
.expect(200)
const modified = Date.parse(res.headers['last-modified']) - 20000
await request(app)
.get('/nums.txt')
.set('If-Range', new Date(modified).toUTCString())
.set('Range', 'bytes=0-0')
.expect(200, '123456789')
})
await t.test('should respond with 200 when invalid value', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream } = await send(req, req.url, { root: fixtures })
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/nums.txt')
.set('If-Range', 'foo')
.set('Range', 'bytes=0-0')
.expect(200, '123456789')
})
})
})
await t.test('when "options" is specified', async function (t) {
t.plan(4)
await t.test('should support start/end', async function (t) {
await request(createServer({ root: fixtures, start: 3, end: 5 }))
.get('/nums.txt')
.expect(200, '456')
})
await t.test('should adjust too large end', async function (t) {
await request(createServer({ root: fixtures, start: 3, end: 90 }))
.get('/nums.txt')
.expect(200, '456789')
})
await t.test('should support start/end with Range request', async function (t) {
await request(createServer({ root: fixtures, start: 0, end: 2 }))
.get('/nums.txt')
.set('Range', 'bytes=-2')
.expect(206, '23')
})
await t.test('should support start/end with unsatisfiable Range request', async function (t) {
await request(createServer({ root: fixtures, start: 0, end: 2 }))
.get('/nums.txt')
.set('Range', 'bytes=5-9')
.expect('Content-Range', 'bytes */3')
.expect(416)
})
})
})

133
backend/node_modules/@fastify/send/test/send.3.test.js generated vendored Normal file
View File

@@ -0,0 +1,133 @@
'use strict'
const { test } = require('node:test')
const http = require('node:http')
const path = require('node:path')
const request = require('supertest')
const { readdir } = require('node:fs/promises')
const send = require('../lib/send').send
const fixtures = path.join(__dirname, 'fixtures')
test('send(file)', async function (t) {
t.plan(5)
await t.test('file type', async function (t) {
t.plan(5)
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
t.assert.deepStrictEqual(type, 'file')
t.assert.ok(metadata.path)
t.assert.ok(metadata.stat)
t.assert.ok(!metadata.error)
t.assert.ok(!metadata.requestPath)
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/name.txt')
.expect('Content-Length', '4')
.expect(200, 'tobi')
})
await t.test('directory type', async function (t) {
t.plan(5)
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
t.assert.deepStrictEqual(type, 'directory')
t.assert.ok(metadata.path)
t.assert.ok(!metadata.stat)
t.assert.ok(!metadata.error)
t.assert.ok(metadata.requestPath)
res.writeHead(statusCode, headers)
stream.pipe(res)
})
await request(app)
.get('/pets')
.expect('Location', '/pets/')
.expect(301)
})
await t.test('error type', async function (t) {
t.plan(5)
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
t.assert.deepStrictEqual(type, 'error')
t.assert.ok(!metadata.path)
t.assert.ok(!metadata.stat)
t.assert.ok(metadata.error)
t.assert.ok(!metadata.requestPath)
res.writeHead(statusCode, headers)
stream.pipe(res)
})
const path = Array(100).join('foobar')
await request(app)
.get('/' + path)
.expect(404)
})
await t.test('custom directory index view', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
if (type === 'directory') {
const list = await readdir(metadata.path)
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
res.end(list.join('\n') + '\n')
} else {
res.writeHead(statusCode, headers)
stream.pipe(res)
}
})
await request(app)
.get('/pets')
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect(200, '.hidden.txt\nindex.html\n')
})
await t.test('serving from a root directory with custom error-handling', async function (t) {
const app = http.createServer(async function (req, res) {
const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
switch (type) {
case 'directory': {
res.writeHead(301, {
Location: metadata.requestPath + '/'
})
res.end('Redirecting to ' + metadata.requestPath + '/')
break
}
case 'error': {
res.writeHead(metadata.error.status ?? 500, {})
res.end(metadata.error.message)
break
}
default: {
// serve all files for download
res.setHeader('Content-Disposition', 'attachment')
res.writeHead(statusCode, headers)
stream.pipe(res)
}
}
})
await request(app)
.get('/pets')
.expect('Location', '/pets/')
.expect(301)
await request(app)
.get('/not-exists')
.expect(404)
await request(app)
.get('/pets/index.html')
.expect('Content-Disposition', 'attachment')
.expect(200)
})
})

View File

@@ -1,19 +1,27 @@
'use strict'
const http = require('http')
const http = require('node:http')
const send = require('..')
module.exports.shouldNotHaveHeader = function shouldNotHaveHeader (header, t) {
return function (res) {
t.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header)
t.assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header)
}
}
module.exports.shouldHaveHeader = function shouldHaveHeader (header, t) {
return function (res) {
t.assert.ok((header.toLowerCase() in res.headers), 'should have header ' + header)
}
}
module.exports.createServer = function createServer (opts, fn) {
return http.createServer(function onRequest (req, res) {
return http.createServer(async function onRequest (req, res) {
try {
fn && fn(req, res)
send(req, req.url, opts).pipe(res)
fn?.(req, res)
const { statusCode, headers, stream } = await send(req, req.url, opts)
res.writeHead(statusCode, headers)
stream.pipe(res)
} catch (err) {
res.statusCode = 500
res.end(String(err))
@@ -23,6 +31,6 @@ module.exports.createServer = function createServer (opts, fn) {
module.exports.shouldNotHaveBody = function shouldNotHaveBody (t) {
return function (res) {
t.ok(res.text === '' || res.text === undefined)
t.assert.ok(res.text === '' || res.text === undefined)
}
}

View File

@@ -4,23 +4,23 @@
/// <reference types="node" />
import * as stream from "stream";
import * as fs from "fs";
import { Dirent } from 'node:fs'
import * as stream from 'node:stream'
/**
* Create a new SendStream for the given path to send to a res.
* The req is the Node.js HTTP request and the path is a urlencoded path to send (urlencoded, not the actual file-system path).
*/
declare function send(req: stream.Readable, path: string, options?: send.SendOptions): send.SendStream;
declare function send (req: stream.Readable, path: string, options?: send.SendOptions): Promise<send.SendResult>
type Send = typeof send;
type Send = typeof send
declare class Mime {
constructor(typeMap: TypeMap, ...mimes: TypeMap[]);
constructor (typeMap: TypeMap, ...mimes: TypeMap[])
getType(path: string): string | null;
getExtension(mime: string): string | null;
define(typeMap: TypeMap, force?: boolean): void;
getType (path: string): string | null
getExtension (mime: string): string | null
define (typeMap: TypeMap, force?: boolean): void
}
interface TypeMap {
@@ -28,8 +28,8 @@ interface TypeMap {
}
declare namespace send {
export const mime: Mime;
export const isUtf8MimeType: (value: string) => boolean;
export const mime: Mime
export const isUtf8MimeType: (value: string) => boolean
export interface SendOptions {
/**
@@ -44,6 +44,11 @@ declare namespace send {
*/
cacheControl?: boolean | undefined;
/**
* Enable or disable setting Content-Type response header, defaults to true.
*/
contentType?: boolean | undefined;
/**
* Set how "dotfiles" are treated when encountered.
* A dotfile is a file or directory that begins with a dot (".").
@@ -54,7 +59,7 @@ declare namespace send {
* 'ignore' Pretend like the dotfile does not exist and 404.
* The default value is similar to 'ignore', with the exception that this default will not ignore the files within a directory that begins with a dot, for backward-compatibility.
*/
dotfiles?: "allow" | "deny" | "ignore" | undefined;
dotfiles?: 'allow' | 'deny' | 'ignore' | undefined;
/**
* Byte offset at which the stream ends, defaults to the length of the file minus 1.
@@ -100,6 +105,11 @@ declare namespace send {
*/
maxAge?: string | number | undefined;
/**
* Limit max response content size when acceptRanges is true, defaults to the entire file size.
*/
maxContentRangeChunkSize?: number | undefined;
/**
* Serve files relative to path.
*/
@@ -110,106 +120,48 @@ declare namespace send {
* The start is inclusive, meaning start: 2 will include the 3rd byte in the stream.
*/
start?: number | undefined;
/**
* Maximum number of bytes that the internal buffer will hold.
* If omitted, Node.js falls back to its built-in default.
*/
highWaterMark?: number | undefined;
}
export class SendStream extends stream.Stream {
constructor(req: stream.Readable, path: string, options?: SendOptions);
/**
* Emit error with `status`.
*/
error(status: number, error?: Error): void;
/**
* Check if the pathname ends with "/".
*/
hasTrailingSlash(): boolean;
/**
* Check if this is a conditional GET request.
*/
isConditionalGET(): boolean;
/**
* Strip content-* header fields.
*/
removeContentHeaderFields(): void;
/**
* Respond with 304 not modified.
*/
notModified(): void;
/**
* Raise error that headers already sent.
*/
headersAlreadySent(): void;
/**
* Check if the request is cacheable, aka responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
*/
isCachable(): boolean;
/**
* Handle stat() error.
*/
onStatError(error: Error): void;
/**
* Check if the cache is fresh.
*/
isFresh(): boolean;
/**
* Check if the range is fresh.
*/
isRangeFresh(): boolean;
/**
* Redirect to path.
*/
redirect(path: string): void;
/**
* Pipe to `res`.
*/
pipe<T extends NodeJS.WritableStream>(res: T): T;
/**
* Transfer `path`.
*/
send(path: string, stat?: fs.Stats): void;
/**
* Transfer file for `path`.
*/
sendFile(path: string): void;
/**
* Transfer index for `path`.
*/
sendIndex(path: string): void;
/**
* Transfer index for `path`.
*/
stream(path: string, options?: {}): void;
/**
* Set content-type based on `path` if it hasn't been explicitly set.
*/
type(path: string): void;
/**
* Set response header fields, most fields may be pre-defined.
*/
setHeader(path: string, stat: fs.Stats): void;
export interface BaseSendResult {
statusCode: number
headers: Record<string, string>
stream: stream.Readable
}
export interface FileSendResult extends BaseSendResult {
type: 'file'
metadata: {
path: string
stat: Dirent
}
}
export interface DirectorySendResult extends BaseSendResult {
type: 'directory'
metadata: {
path: string
requestPath: string
}
}
export interface ErrorSendResult extends BaseSendResult {
type: 'error'
metadata: {
error: Error
}
}
export type SendResult = FileSendResult | DirectorySendResult | ErrorSendResult
export const send: Send
export { send as default }
}
export = send;
export = send

View File

@@ -1,36 +1,67 @@
import { Dirent } from 'node:fs'
import { resolve } from 'node:path'
import { Readable } from 'node:stream'
import { expectType } from 'tsd'
import send from '..'
import { SendStream } from '..';
import send, { DirectorySendResult, ErrorSendResult, FileSendResult, SendResult } from '..'
send.mime.define({
'application/x-my-type': ['x-mt', 'x-mtt']
});
})
expectType<(value: string) => boolean>(send.isUtf8MimeType)
expectType<boolean>(send.isUtf8MimeType('application/json'))
const req: any = {}
const res: any = {}
send(req, '/test.html', {
immutable: true,
maxAge: 0,
root: __dirname + '/wwwroot'
}).pipe(res);
{
const result = await send(req, '/test.html', {
acceptRanges: true,
maxContentRangeChunkSize: 10,
immutable: true,
maxAge: 0,
root: resolve(__dirname, '/wwwroot')
})
send(req, '/test.html', { maxAge: 0, root: __dirname + '/wwwroot' })
.on('error', (err: any) => {
res.statusCode = err.status || 500;
res.end(err.message);
})
.on('directory', () => {
res.statusCode = 301;
res.setHeader('Location', req.url + '/');
res.end(`Redirecting to ${req.url}/`);
})
.on('headers', (res: any, path: string, stat: any) => {
res.setHeader('Content-Disposition', 'attachment');
})
.pipe(res);
expectType<SendResult>(result)
expectType<number>(result.statusCode)
expectType<Record<string, string>>(result.headers)
expectType<Readable>(result.stream)
}
const test = new SendStream(req, '/test.html', { maxAge: 0, root: __dirname + '/wwwroot' });
{
const result = await send(req, '/test.html', { contentType: true, maxAge: 0, root: resolve(__dirname, '/wwwroot') })
expectType<SendResult>(result)
expectType<number>(result.statusCode)
expectType<Record<string, string>>(result.headers)
expectType<Readable>(result.stream)
}
{
const result = await send(req, '/test.html', { contentType: false, root: resolve(__dirname, '/wwwroot') })
expectType<SendResult>(result)
expectType<number>(result.statusCode)
expectType<Record<string, string>>(result.headers)
expectType<Readable>(result.stream)
}
const result = await send(req, '/test.html')
switch (result.type) {
case 'file': {
expectType<FileSendResult>(result)
expectType<string>(result.metadata.path)
expectType<Dirent>(result.metadata.stat)
break
}
case 'directory': {
expectType<DirectorySendResult>(result)
expectType<string>(result.metadata.path)
expectType<string>(result.metadata.requestPath)
break
}
case 'error': {
expectType<ErrorSendResult>(result)
expectType<Error>(result.metadata.error)
}
}