You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					510 lines
				
				14 KiB
			
		
		
			
		
	
	
					510 lines
				
				14 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								'use strict'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const path = require('path')
							 | 
						||
| 
								 | 
							
								const statSync = require('fs').statSync
							 | 
						||
| 
								 | 
							
								const { PassThrough } = require('readable-stream')
							 | 
						||
| 
								 | 
							
								const glob = require('glob')
							 | 
						||
| 
								 | 
							
								const send = require('send')
							 | 
						||
| 
								 | 
							
								const contentDisposition = require('content-disposition')
							 | 
						||
| 
								 | 
							
								const fp = require('fastify-plugin')
							 | 
						||
| 
								 | 
							
								const util = require('util')
							 | 
						||
| 
								 | 
							
								const globPromise = util.promisify(glob)
							 | 
						||
| 
								 | 
							
								const encodingNegotiator = require('encoding-negotiator')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const dirList = require('./lib/dirList')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async function fastifyStatic (fastify, opts) {
							 | 
						||
| 
								 | 
							
								  checkRootPathForErrors(fastify, opts.root)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const setHeaders = opts.setHeaders
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (setHeaders !== undefined && typeof setHeaders !== 'function') {
							 | 
						||
| 
								 | 
							
								    throw new TypeError('The `setHeaders` option must be a function')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const invalidDirListOpts = dirList.validateOptions(opts.list)
							 | 
						||
| 
								 | 
							
								  if (invalidDirListOpts) {
							 | 
						||
| 
								 | 
							
								    throw invalidDirListOpts
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const sendOptions = {
							 | 
						||
| 
								 | 
							
								    root: opts.root,
							 | 
						||
| 
								 | 
							
								    acceptRanges: opts.acceptRanges,
							 | 
						||
| 
								 | 
							
								    cacheControl: opts.cacheControl,
							 | 
						||
| 
								 | 
							
								    dotfiles: opts.dotfiles,
							 | 
						||
| 
								 | 
							
								    etag: opts.etag,
							 | 
						||
| 
								 | 
							
								    extensions: opts.extensions,
							 | 
						||
| 
								 | 
							
								    immutable: opts.immutable,
							 | 
						||
| 
								 | 
							
								    index: opts.index,
							 | 
						||
| 
								 | 
							
								    lastModified: opts.lastModified,
							 | 
						||
| 
								 | 
							
								    maxAge: opts.maxAge
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const allowedPath = opts.allowedPath
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (opts.prefix === undefined) opts.prefix = '/'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let prefix = opts.prefix
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (!opts.prefixAvoidTrailingSlash) {
							 | 
						||
| 
								 | 
							
								    prefix =
							 | 
						||
| 
								 | 
							
								      opts.prefix[opts.prefix.length - 1] === '/'
							 | 
						||
| 
								 | 
							
								        ? opts.prefix
							 | 
						||
| 
								 | 
							
								        : opts.prefix + '/'
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  function pumpSendToReply (
							 | 
						||
| 
								 | 
							
								    request,
							 | 
						||
| 
								 | 
							
								    reply,
							 | 
						||
| 
								 | 
							
								    pathname,
							 | 
						||
| 
								 | 
							
								    rootPath,
							 | 
						||
| 
								 | 
							
								    rootPathOffset = 0,
							 | 
						||
| 
								 | 
							
								    pumpOptions = {},
							 | 
						||
| 
								 | 
							
								    checkedEncodings
							 | 
						||
| 
								 | 
							
								  ) {
							 | 
						||
| 
								 | 
							
								    const options = Object.assign({}, sendOptions, pumpOptions)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (rootPath) {
							 | 
						||
| 
								 | 
							
								      if (Array.isArray(rootPath)) {
							 | 
						||
| 
								 | 
							
								        options.root = rootPath[rootPathOffset]
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        options.root = rootPath
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (allowedPath && !allowedPath(pathname, options.root)) {
							 | 
						||
| 
								 | 
							
								      return reply.callNotFound()
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    let encoding
							 | 
						||
| 
								 | 
							
								    let pathnameForSend = pathname
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (opts.preCompressed) {
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * We conditionally create this structure to track our attempts
							 | 
						||
| 
								 | 
							
								       * at sending pre-compressed assets
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      if (!checkedEncodings) {
							 | 
						||
| 
								 | 
							
								        checkedEncodings = new Set()
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      encoding = getEncodingHeader(request.headers, checkedEncodings)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (encoding) {
							 | 
						||
| 
								 | 
							
								        if (pathname.endsWith('/')) {
							 | 
						||
| 
								 | 
							
								          pathname = findIndexFile(pathname, options.root, options.index)
							 | 
						||
| 
								 | 
							
								          if (!pathname) {
							 | 
						||
| 
								 | 
							
								            return reply.callNotFound()
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								          pathnameForSend = pathnameForSend + pathname + '.' + getEncodingExtension(encoding)
							 | 
						||
| 
								 | 
							
								        } else {
							 | 
						||
| 
								 | 
							
								          pathnameForSend = pathname + '.' + getEncodingExtension(encoding)
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const stream = send(request.raw, pathnameForSend, options)
							 | 
						||
| 
								 | 
							
								    let resolvedFilename
							 | 
						||
| 
								 | 
							
								    stream.on('file', function (file) {
							 | 
						||
| 
								 | 
							
								      resolvedFilename = file
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const wrap = new PassThrough({
							 | 
						||
| 
								 | 
							
								      flush (cb) {
							 | 
						||
| 
								 | 
							
								        this.finished = true
							 | 
						||
| 
								 | 
							
								        if (reply.raw.statusCode === 304) {
							 | 
						||
| 
								 | 
							
								          reply.send('')
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        cb()
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    wrap.getHeader = reply.getHeader.bind(reply)
							 | 
						||
| 
								 | 
							
								    wrap.setHeader = reply.header.bind(reply)
							 | 
						||
| 
								 | 
							
								    wrap.finished = false
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Object.defineProperty(wrap, 'filename', {
							 | 
						||
| 
								 | 
							
								      get () {
							 | 
						||
| 
								 | 
							
								        return resolvedFilename
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								    Object.defineProperty(wrap, 'statusCode', {
							 | 
						||
| 
								 | 
							
								      get () {
							 | 
						||
| 
								 | 
							
								        return reply.raw.statusCode
							 | 
						||
| 
								 | 
							
								      },
							 | 
						||
| 
								 | 
							
								      set (code) {
							 | 
						||
| 
								 | 
							
								        reply.code(code)
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (request.method === 'HEAD') {
							 | 
						||
| 
								 | 
							
								      wrap.on('finish', reply.send.bind(reply))
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      wrap.on('pipe', function () {
							 | 
						||
| 
								 | 
							
								        if (encoding) {
							 | 
						||
| 
								 | 
							
								          reply.header('content-type', getContentType(pathname))
							 | 
						||
| 
								 | 
							
								          reply.header('content-encoding', encoding)
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        reply.send(wrap)
							 | 
						||
| 
								 | 
							
								      })
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (setHeaders !== undefined) {
							 | 
						||
| 
								 | 
							
								      stream.on('headers', setHeaders)
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    stream.on('directory', function (_, path) {
							 | 
						||
| 
								 | 
							
								      if (opts.list) {
							 | 
						||
| 
								 | 
							
								        dirList.send({
							 | 
						||
| 
								 | 
							
								          reply,
							 | 
						||
| 
								 | 
							
								          dir: path,
							 | 
						||
| 
								 | 
							
								          options: opts.list,
							 | 
						||
| 
								 | 
							
								          route: pathname,
							 | 
						||
| 
								 | 
							
								          prefix
							 | 
						||
| 
								 | 
							
								        }).catch((err) => reply.send(err))
							 | 
						||
| 
								 | 
							
								        return
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (opts.redirect === true) {
							 | 
						||
| 
								 | 
							
								        try {
							 | 
						||
| 
								 | 
							
								          reply.redirect(301, getRedirectUrl(request.raw.url))
							 | 
						||
| 
								 | 
							
								        } catch (error) {
							 | 
						||
| 
								 | 
							
								          // the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
							 | 
						||
| 
								 | 
							
								          /* istanbul ignore next */
							 | 
						||
| 
								 | 
							
								          reply.send(error)
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        // if is a directory path without a trailing slash, and has an index file, reply as if it has a trailing slash
							 | 
						||
| 
								 | 
							
								        if (!pathname.endsWith('/') && findIndexFile(pathname, options.root, options.index)) {
							 | 
						||
| 
								 | 
							
								          return pumpSendToReply(
							 | 
						||
| 
								 | 
							
								            request,
							 | 
						||
| 
								 | 
							
								            reply,
							 | 
						||
| 
								 | 
							
								            pathname + '/',
							 | 
						||
| 
								 | 
							
								            rootPath,
							 | 
						||
| 
								 | 
							
								            undefined,
							 | 
						||
| 
								 | 
							
								            undefined,
							 | 
						||
| 
								 | 
							
								            checkedEncodings
							 | 
						||
| 
								 | 
							
								          )
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        reply.callNotFound()
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    stream.on('error', function (err) {
							 | 
						||
| 
								 | 
							
								      if (err.code === 'ENOENT') {
							 | 
						||
| 
								 | 
							
								        // when preCompress is enabled and the path is a directoy without a trailing shash
							 | 
						||
| 
								 | 
							
								        if (opts.preCompressed && encoding) {
							 | 
						||
| 
								 | 
							
								          const indexPathname = findIndexFile(pathname, options.root, options.index)
							 | 
						||
| 
								 | 
							
								          if (indexPathname) {
							 | 
						||
| 
								 | 
							
								            return pumpSendToReply(
							 | 
						||
| 
								 | 
							
								              request,
							 | 
						||
| 
								 | 
							
								              reply,
							 | 
						||
| 
								 | 
							
								              pathname + '/',
							 | 
						||
| 
								 | 
							
								              rootPath,
							 | 
						||
| 
								 | 
							
								              undefined,
							 | 
						||
| 
								 | 
							
								              undefined,
							 | 
						||
| 
								 | 
							
								              checkedEncodings
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        // if file exists, send real file, otherwise send dir list if name match
							 | 
						||
| 
								 | 
							
								        if (opts.list && dirList.handle(pathname, opts.list)) {
							 | 
						||
| 
								 | 
							
								          dirList.send({
							 | 
						||
| 
								 | 
							
								            reply,
							 | 
						||
| 
								 | 
							
								            dir: dirList.path(opts.root, pathname),
							 | 
						||
| 
								 | 
							
								            options: opts.list,
							 | 
						||
| 
								 | 
							
								            route: pathname,
							 | 
						||
| 
								 | 
							
								            prefix
							 | 
						||
| 
								 | 
							
								          }).catch((err) => reply.send(err))
							 | 
						||
| 
								 | 
							
								          return
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        // root paths left to try?
							 | 
						||
| 
								 | 
							
								        if (Array.isArray(rootPath) && rootPathOffset < (rootPath.length - 1)) {
							 | 
						||
| 
								 | 
							
								          return pumpSendToReply(request, reply, pathname, rootPath, rootPathOffset + 1)
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (opts.preCompressed && !checkedEncodings.has(encoding)) {
							 | 
						||
| 
								 | 
							
								          checkedEncodings.add(encoding)
							 | 
						||
| 
								 | 
							
								          return pumpSendToReply(
							 | 
						||
| 
								 | 
							
								            request,
							 | 
						||
| 
								 | 
							
								            reply,
							 | 
						||
| 
								 | 
							
								            pathname,
							 | 
						||
| 
								 | 
							
								            rootPath,
							 | 
						||
| 
								 | 
							
								            undefined,
							 | 
						||
| 
								 | 
							
								            undefined,
							 | 
						||
| 
								 | 
							
								            checkedEncodings
							 | 
						||
| 
								 | 
							
								          )
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return reply.callNotFound()
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      // The `send` library terminates the request with a 404 if the requested
							 | 
						||
| 
								 | 
							
								      // path contains a dotfile and `send` is initialized with `{dotfiles:
							 | 
						||
| 
								 | 
							
								      // 'ignore'}`. `send` aborts the request before getting far enough to
							 | 
						||
| 
								 | 
							
								      // check if the file exists (hence, a 404 `NotFoundError` instead of
							 | 
						||
| 
								 | 
							
								      // `ENOENT`).
							 | 
						||
| 
								 | 
							
								      // https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L582
							 | 
						||
| 
								 | 
							
								      if (err.status === 404) {
							 | 
						||
| 
								 | 
							
								        return reply.callNotFound()
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      reply.send(err)
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // we cannot use pump, because send error
							 | 
						||
| 
								 | 
							
								    // handling is not compatible
							 | 
						||
| 
								 | 
							
								    stream.pipe(wrap)
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const errorHandler = (error, request, reply) => {
							 | 
						||
| 
								 | 
							
								    if (error && error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
							 | 
						||
| 
								 | 
							
								      reply.request.raw.destroy()
							 | 
						||
| 
								 | 
							
								      return
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    fastify.errorHandler(error, request, reply)
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Set the schema hide property if defined in opts or true by default
							 | 
						||
| 
								 | 
							
								  const routeOpts = {
							 | 
						||
| 
								 | 
							
								    schema: {
							 | 
						||
| 
								 | 
							
								      hide: typeof opts.schemaHide !== 'undefined' ? opts.schemaHide : true
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								    errorHandler: fastify.errorHandler ? errorHandler : undefined
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (opts.decorateReply !== false) {
							 | 
						||
| 
								 | 
							
								    fastify.decorateReply('sendFile', function (filePath, rootPath, options) {
							 | 
						||
| 
								 | 
							
								      const opts = typeof rootPath === 'object' ? rootPath : options
							 | 
						||
| 
								 | 
							
								      const root = typeof rootPath === 'string' ? rootPath : opts && opts.root
							 | 
						||
| 
								 | 
							
								      pumpSendToReply(
							 | 
						||
| 
								 | 
							
								        this.request,
							 | 
						||
| 
								 | 
							
								        this,
							 | 
						||
| 
								 | 
							
								        filePath,
							 | 
						||
| 
								 | 
							
								        root || sendOptions.root,
							 | 
						||
| 
								 | 
							
								        0,
							 | 
						||
| 
								 | 
							
								        opts
							 | 
						||
| 
								 | 
							
								      )
							 | 
						||
| 
								 | 
							
								      return this
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    fastify.decorateReply(
							 | 
						||
| 
								 | 
							
								      'download',
							 | 
						||
| 
								 | 
							
								      function (filePath, fileName, options = {}) {
							 | 
						||
| 
								 | 
							
								        const { root, ...opts } =
							 | 
						||
| 
								 | 
							
								          typeof fileName === 'object' ? fileName : options
							 | 
						||
| 
								 | 
							
								        fileName = typeof fileName === 'string' ? fileName : filePath
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        // Set content disposition header
							 | 
						||
| 
								 | 
							
								        this.header('content-disposition', contentDisposition(fileName))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        pumpSendToReply(this.request, this, filePath, root, 0, opts)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return this
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    )
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (opts.serve !== false) {
							 | 
						||
| 
								 | 
							
								    if (opts.wildcard && typeof opts.wildcard !== 'boolean') {
							 | 
						||
| 
								 | 
							
								      throw new Error('"wildcard" option must be a boolean')
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    if (opts.wildcard === undefined || opts.wildcard === true) {
							 | 
						||
| 
								 | 
							
								      fastify.head(prefix + '*', routeOpts, function (req, reply) {
							 | 
						||
| 
								 | 
							
								        pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
							 | 
						||
| 
								 | 
							
								      })
							 | 
						||
| 
								 | 
							
								      fastify.get(prefix + '*', routeOpts, function (req, reply) {
							 | 
						||
| 
								 | 
							
								        pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
							 | 
						||
| 
								 | 
							
								      })
							 | 
						||
| 
								 | 
							
								      if (opts.redirect === true && prefix !== opts.prefix) {
							 | 
						||
| 
								 | 
							
								        fastify.get(opts.prefix, routeOpts, function (req, reply) {
							 | 
						||
| 
								 | 
							
								          reply.redirect(301, getRedirectUrl(req.raw.url))
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      const globPattern = '**/*'
							 | 
						||
| 
								 | 
							
								      const indexDirs = new Map()
							 | 
						||
| 
								 | 
							
								      const routes = new Set()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      for (const rootPath of Array.isArray(sendOptions.root) ? sendOptions.root : [sendOptions.root]) {
							 | 
						||
| 
								 | 
							
								        const files = await globPromise(path.join(rootPath, globPattern), { nodir: true })
							 | 
						||
| 
								 | 
							
								        const indexes = typeof opts.index === 'undefined' ? ['index.html'] : [].concat(opts.index)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        for (let file of files) {
							 | 
						||
| 
								 | 
							
								          file = file
							 | 
						||
| 
								 | 
							
								            .replace(rootPath.replace(/\\/g, '/'), '')
							 | 
						||
| 
								 | 
							
								            .replace(/^\//, '')
							 | 
						||
| 
								 | 
							
								          const route = encodeURI(prefix + file).replace(/\/\//g, '/')
							 | 
						||
| 
								 | 
							
								          if (routes.has(route)) {
							 | 
						||
| 
								 | 
							
								            continue
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								          routes.add(route)
							 | 
						||
| 
								 | 
							
								          fastify.head(route, routeOpts, function (req, reply) {
							 | 
						||
| 
								 | 
							
								            pumpSendToReply(req, reply, '/' + file, rootPath)
							 | 
						||
| 
								 | 
							
								          })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          fastify.get(route, routeOpts, function (req, reply) {
							 | 
						||
| 
								 | 
							
								            pumpSendToReply(req, reply, '/' + file, rootPath)
							 | 
						||
| 
								 | 
							
								          })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          const key = path.posix.basename(route)
							 | 
						||
| 
								 | 
							
								          if (indexes.includes(key) && !indexDirs.has(key)) {
							 | 
						||
| 
								 | 
							
								            indexDirs.set(path.posix.dirname(route), rootPath)
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      for (const [dirname, rootPath] of indexDirs.entries()) {
							 | 
						||
| 
								 | 
							
								        const pathname = dirname + (dirname.endsWith('/') ? '' : '/')
							 | 
						||
| 
								 | 
							
								        const file = '/' + pathname.replace(prefix, '')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        fastify.head(pathname, routeOpts, function (req, reply) {
							 | 
						||
| 
								 | 
							
								          pumpSendToReply(req, reply, file, rootPath)
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        fastify.get(pathname, routeOpts, function (req, reply) {
							 | 
						||
| 
								 | 
							
								          pumpSendToReply(req, reply, file, rootPath)
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (opts.redirect === true) {
							 | 
						||
| 
								 | 
							
								          fastify.head(pathname.replace(/\/$/, ''), routeOpts, function (req, reply) {
							 | 
						||
| 
								 | 
							
								            pumpSendToReply(req, reply, file.replace(/\/$/, ''), rootPath)
							 | 
						||
| 
								 | 
							
								          })
							 | 
						||
| 
								 | 
							
								          fastify.get(pathname.replace(/\/$/, ''), routeOpts, function (req, reply) {
							 | 
						||
| 
								 | 
							
								            pumpSendToReply(req, reply, file.replace(/\/$/, ''), rootPath)
							 | 
						||
| 
								 | 
							
								          })
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function checkRootPathForErrors (fastify, rootPath) {
							 | 
						||
| 
								 | 
							
								  if (rootPath === undefined) {
							 | 
						||
| 
								 | 
							
								    throw new Error('"root" option is required')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (Array.isArray(rootPath)) {
							 | 
						||
| 
								 | 
							
								    if (!rootPath.length) {
							 | 
						||
| 
								 | 
							
								      throw new Error('"root" option array requires one or more paths')
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if ([...new Set(rootPath)].length !== rootPath.length) {
							 | 
						||
| 
								 | 
							
								      throw new Error(
							 | 
						||
| 
								 | 
							
								        '"root" option array contains one or more duplicate paths'
							 | 
						||
| 
								 | 
							
								      )
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // check each path and fail at first invalid
							 | 
						||
| 
								 | 
							
								    rootPath.map((path) => checkPath(fastify, path))
							 | 
						||
| 
								 | 
							
								    return
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (typeof rootPath === 'string') {
							 | 
						||
| 
								 | 
							
								    return checkPath(fastify, rootPath)
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  throw new Error('"root" option must be a string or array of strings')
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function checkPath (fastify, rootPath) {
							 | 
						||
| 
								 | 
							
								  if (typeof rootPath !== 'string') {
							 | 
						||
| 
								 | 
							
								    throw new Error('"root" option must be a string')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  if (path.isAbsolute(rootPath) === false) {
							 | 
						||
| 
								 | 
							
								    throw new Error('"root" option must be an absolute path')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let pathStat
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  try {
							 | 
						||
| 
								 | 
							
								    pathStat = statSync(rootPath)
							 | 
						||
| 
								 | 
							
								  } catch (e) {
							 | 
						||
| 
								 | 
							
								    if (e.code === 'ENOENT') {
							 | 
						||
| 
								 | 
							
								      fastify.log.warn(`"root" path "${rootPath}" must exist`)
							 | 
						||
| 
								 | 
							
								      return
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    throw e
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (pathStat.isDirectory() === false) {
							 | 
						||
| 
								 | 
							
								    throw new Error('"root" option must point to a directory')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const supportedEncodings = ['br', 'gzip', 'deflate']
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function getContentType (path) {
							 | 
						||
| 
								 | 
							
								  const type = send.mime.lookup(path)
							 | 
						||
| 
								 | 
							
								  const charset = send.mime.charsets.lookup(type)
							 | 
						||
| 
								 | 
							
								  if (!charset) {
							 | 
						||
| 
								 | 
							
								    return type
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  return `${type}; charset=${charset}`
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function findIndexFile (pathname, root, indexFiles = ['index.html']) {
							 | 
						||
| 
								 | 
							
								  return indexFiles.find(filename => {
							 | 
						||
| 
								 | 
							
								    const p = path.join(root, pathname, filename)
							 | 
						||
| 
								 | 
							
								    try {
							 | 
						||
| 
								 | 
							
								      const stats = statSync(p)
							 | 
						||
| 
								 | 
							
								      return !stats.isDirectory()
							 | 
						||
| 
								 | 
							
								    } catch (e) {
							 | 
						||
| 
								 | 
							
								      return false
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// Adapted from https://github.com/fastify/fastify-compress/blob/665e132fa63d3bf05ad37df3c20346660b71a857/index.js#L451
							 | 
						||
| 
								 | 
							
								function getEncodingHeader (headers, checked) {
							 | 
						||
| 
								 | 
							
								  if (!('accept-encoding' in headers)) return
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const header = headers['accept-encoding'].toLowerCase().replace(/\*/g, 'gzip')
							 | 
						||
| 
								 | 
							
								  return encodingNegotiator.negotiate(
							 | 
						||
| 
								 | 
							
								    header,
							 | 
						||
| 
								 | 
							
								    supportedEncodings.filter((enc) => !checked.has(enc))
							 | 
						||
| 
								 | 
							
								  )
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function getEncodingExtension (encoding) {
							 | 
						||
| 
								 | 
							
								  switch (encoding) {
							 | 
						||
| 
								 | 
							
								    case 'br':
							 | 
						||
| 
								 | 
							
								      return 'br'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    case 'gzip':
							 | 
						||
| 
								 | 
							
								      return 'gz'
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function getRedirectUrl (url) {
							 | 
						||
| 
								 | 
							
								  let i = 0
							 | 
						||
| 
								 | 
							
								  // we detech how many slash before a valid path
							 | 
						||
| 
								 | 
							
								  for (i; i < url.length; i++) {
							 | 
						||
| 
								 | 
							
								    if (url[i] !== '/' && url[i] !== '\\') break
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  // turns all leading / or \ into a single /
							 | 
						||
| 
								 | 
							
								  url = '/' + url.substr(i)
							 | 
						||
| 
								 | 
							
								  try {
							 | 
						||
| 
								 | 
							
								    const parsed = new URL(url, 'http://localhost.com/')
							 | 
						||
| 
								 | 
							
								    return parsed.pathname + (parsed.pathname[parsed.pathname.length - 1] !== '/' ? '/' : '') + (parsed.search || '')
							 | 
						||
| 
								 | 
							
								  } catch (error) {
							 | 
						||
| 
								 | 
							
								    // the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
							 | 
						||
| 
								 | 
							
								    /* istanbul ignore next */
							 | 
						||
| 
								 | 
							
								    const err = new Error(`Invalid redirect URL: ${url}`)
							 | 
						||
| 
								 | 
							
								    /* istanbul ignore next */
							 | 
						||
| 
								 | 
							
								    err.statusCode = 400
							 | 
						||
| 
								 | 
							
								    /* istanbul ignore next */
							 | 
						||
| 
								 | 
							
								    throw err
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = fp(fastifyStatic, {
							 | 
						||
| 
								 | 
							
								  fastify: '3.x',
							 | 
						||
| 
								 | 
							
								  name: 'fastify-static'
							 | 
						||
| 
								 | 
							
								})
							 |