diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 62562b7..0000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -coverage -node_modules diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index e4f03fb..0000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,11 +0,0 @@ -root: true -extends: - - standard - - plugin:markdown/recommended -plugins: - - markdown -overrides: - - files: '**/*.md' - processor: 'markdown/markdown' -rules: - no-param-reassign: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d9cd8d..079a8f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,110 +1,18 @@ -name: ci +name: CI on: -- pull_request -- push + push: + paths-ignore: + - 'docs/**' + - '*.md' + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' jobs: test: - runs-on: ubuntu-latest - strategy: - matrix: - name: - - Node.js 14.x - - Node.js 15.x - - Node.js 16.x - - Node.js 17.x - - include: - - name: Node.js 14.x - node-version: "14.19" - - - name: Node.js 15.x - node-version: "15.14" - - - name: Node.js 16.x - node-version: "16.14" - - - name: Node.js 17.x - node-version: "17.7" - - steps: - - uses: actions/checkout@v2 - - - name: Install Node.js ${{ matrix.node-version }} - shell: bash -eo pipefail -l {0} - run: | - nvm install --default ${{ matrix.node-version }} - if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then - nvm install --alias=npm 0.10 - nvm use ${{ matrix.node-version }} - sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" - npm config set strict-ssl false - fi - dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" - - - name: Configure npm - run: npm config set shrinkwrap false - - - name: Remove npm module(s) ${{ matrix.npm-rm }} - run: npm rm --silent --save-dev ${{ matrix.npm-rm }} - if: matrix.npm-rm != '' - - - name: Install npm module(s) ${{ matrix.npm-i }} - run: npm install --save-dev ${{ matrix.npm-i }} - if: matrix.npm-i != '' - - - name: Setup Node.js version-specific dependencies - shell: bash - run: | - # eslint for linting - # - remove on Node.js < 8 - if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then - node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ - grep -E '^eslint(-|$)' | \ - sort -r | \ - xargs -n1 npm rm --silent --save-dev - fi - - - name: Install Node.js dependencies - run: npm install - - - name: List environment - id: list_env - shell: bash - run: | - echo "node@$(node -v)" - echo "npm@$(npm -v)" - npm -s ls ||: - (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }' - - - name: Run tests - shell: bash - run: | - if npm -ps ls nyc | grep -q nyc; then - npm run test-ci - else - npm test - fi - - - name: Lint code - if: steps.list_env.outputs.eslint != '' - run: npm run lint - - - name: Collect code coverage - uses: coverallsapp/github-action@master - if: steps.list_env.outputs.nyc != '' - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: run-${{ matrix.test_number }} - parallel: true - - coverage: - needs: test - runs-on: ubuntu-latest - steps: - - name: Uploade code coverage - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.github_token }} - parallel-finished: true + uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3 + with: + license-check: true + lint: true diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.taprc b/.taprc new file mode 100644 index 0000000..bd8157a --- /dev/null +++ b/.taprc @@ -0,0 +1,4 @@ +files: + - test/**/*.test.js + +check-coverage: false diff --git a/LICENSE b/LICENSE index b6ea1c1..543bed6 100644 --- a/LICENSE +++ b/LICENSE @@ -2,6 +2,7 @@ Copyright (c) 2012 TJ Holowaychuk Copyright (c) 2014-2022 Douglas Christopher Wilson +Copyright (c) 2023 The Fastify Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 1332a99..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,89 +0,0 @@ -environment: - matrix: - - nodejs_version: "0.10" - - nodejs_version: "0.12" - - nodejs_version: "1.8" - - nodejs_version: "2.5" - - nodejs_version: "3.3" - - nodejs_version: "4.9" - - nodejs_version: "5.12" - - nodejs_version: "6.17" - - nodejs_version: "7.10" - - nodejs_version: "8.16" - - nodejs_version: "9.11" - - nodejs_version: "10.24" - - nodejs_version: "11.15" - - nodejs_version: "12.22" - - nodejs_version: "13.14" - - nodejs_version: "14.19" - - nodejs_version: "15.14" - - nodejs_version: "16.14" - - nodejs_version: "17.7" -cache: - - node_modules -install: - # Install Node.js - - ps: >- - try { Install-Product node $env:nodejs_version -ErrorAction Stop } - catch { Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) } - # Configure npm - - ps: | - # Skip updating shrinkwrap / lock - npm config set shrinkwrap false - # Remove all non-test dependencies - - ps: | - # Remove coverage dependency - npm rm --silent --save-dev nyc - # Remove lint dependencies - cmd.exe /c "node -pe `"Object.keys(require('./package').devDependencies).join('\n')`"" | ` - sls "^eslint(-|$)" | ` - %{ npm rm --silent --save-dev $_ } - # Setup Node.js version-specific dependencies - - ps: | - # mocha for testing - # - use 3.x for Node.js < 4 - # - use 5.x for Node.js < 6 - # - use 6.x for Node.js < 8 - # - use 7.x for Node.js < 10 - # - use 8.x for Node.js < 12 - if ([int]$env:nodejs_version.split(".")[0] -lt 4) { - npm install --silent --save-dev mocha@3.5.3 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) { - npm install --silent --save-dev mocha@5.2.0 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) { - npm install --silent --save-dev mocha@6.2.3 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 10) { - npm install --silent --save-dev mocha@7.2.0 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 12) { - npm install --silent --save-dev mocha@8.4.0 - } - - ps: | - # supertest for http calls - # - use 2.0.0 for Node.js < 4 - # - use 3.4.2 for Node.js < 6 - # - use 6.1.6 for Node.js < 8 - if ([int]$env:nodejs_version.split(".")[0] -lt 4) { - npm install --silent --save-dev supertest@2.0.0 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) { - npm install --silent --save-dev supertest@3.4.2 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) { - npm install --silent --save-dev supertest@6.1.6 - } - # Update Node.js modules - - ps: | - # Prune & rebuild node_modules - if (Test-Path -Path node_modules) { - npm prune - npm rebuild - } - # Install Node.js modules - - npm install -build: off -test_script: - # Output version data - - ps: | - node --version - npm --version - # Run test script - - npm test -version: "{build}" diff --git a/index.js b/index.js index 89afd7e..f4985d2 100644 --- a/index.js +++ b/index.js @@ -12,55 +12,55 @@ * @private */ -var createError = require('http-errors') -var debug = require('debug')('send') -var deprecate = require('depd')('send') -var destroy = require('destroy') -var encodeUrl = require('encodeurl') -var escapeHtml = require('escape-html') -var etag = require('etag') -var fresh = require('fresh') -var fs = require('fs') -var mime = require('mime') -var ms = require('ms') -var onFinished = require('on-finished') -var parseRange = require('range-parser') -var path = require('path') -var statuses = require('statuses') -var Stream = require('stream') -var util = require('util') +const createError = require('http-errors') +const debug = require('debug')('send') +const deprecate = require('depd')('send') +const destroy = require('destroy') +const encodeUrl = require('encodeurl') +const escapeHtml = require('escape-html') +const etag = require('etag') +const fresh = require('fresh') +const fs = require('fs') +const mime = require('mime') +const ms = require('ms') +const onFinished = require('on-finished') +const parseRange = require('range-parser') +const path = require('path') +const statuses = require('statuses') +const Stream = require('stream') +const util = require('util') /** * Path function references. * @private */ -var extname = path.extname -var join = path.join -var normalize = path.normalize -var resolve = path.resolve -var sep = path.sep +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 */ -var BYTES_RANGE_REGEXP = /^ *bytes=/ +const BYTES_RANGE_REGEXP = /^ *bytes=/ /** * Maximum value allowed for the max age. * @private */ -var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year +const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year /** * Regular expression to match a path with a directory up component. * @private */ -var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/ +const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/ /** * Module exports. @@ -96,7 +96,7 @@ function send (req, path, options) { function SendStream (req, path, options) { Stream.call(this) - var opts = options || {} + const opts = options || {} this.options = opts this.path = path @@ -211,7 +211,7 @@ SendStream.prototype.hidden = deprecate.function(function hidden (val) { */ SendStream.prototype.index = deprecate.function(function index (paths) { - var index = !paths ? [] : normalizeList(paths, 'paths argument') + const index = !paths ? [] : normalizeList(paths, 'paths argument') debug('index %o', paths) this._index = index return this @@ -270,9 +270,9 @@ SendStream.prototype.error = function error (status, err) { return this.emit('error', createHttpError(status, err)) } - var res = this.res - var msg = statuses.message[status] || String(status) - var doc = createHtmlDocument('Error', escapeHtml(msg)) + const res = this.res + const msg = statuses.message[status] || String(status) + const doc = createHtmlDocument('Error', escapeHtml(msg)) // clear existing headers clearHeaders(res) @@ -324,22 +324,22 @@ SendStream.prototype.isConditionalGET = function isConditionalGET () { */ SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () { - var req = this.req - var res = this.res + const req = this.req + const res = this.res // if-match - var match = req.headers['if-match'] + const match = req.headers['if-match'] if (match) { - var etag = res.getHeader('ETag') + const etag = res.getHeader('ETag') return !etag || (match !== '*' && parseTokenList(match).every(function (match) { return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag })) } // if-unmodified-since - var unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']) + const unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']) if (!isNaN(unmodifiedSince)) { - var lastModified = parseHttpDate(res.getHeader('Last-Modified')) + const lastModified = parseHttpDate(res.getHeader('Last-Modified')) return isNaN(lastModified) || lastModified > unmodifiedSince } @@ -353,7 +353,7 @@ SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () { */ SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () { - var res = this.res + const res = this.res res.removeHeader('Content-Encoding') res.removeHeader('Content-Language') @@ -369,7 +369,7 @@ SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFie */ SendStream.prototype.notModified = function notModified () { - var res = this.res + const res = this.res debug('not modified') this.removeContentHeaderFields() res.statusCode = 304 @@ -383,7 +383,7 @@ SendStream.prototype.notModified = function notModified () { */ SendStream.prototype.headersAlreadySent = function headersAlreadySent () { - var err = new Error('Can\'t set headers after they are sent.') + const err = new Error('Can\'t set headers after they are sent.') debug('headers already sent') this.error(500, err) } @@ -397,7 +397,7 @@ SendStream.prototype.headersAlreadySent = function headersAlreadySent () { */ SendStream.prototype.isCachable = function isCachable () { - var statusCode = this.res.statusCode + const statusCode = this.res.statusCode return (statusCode >= 200 && statusCode < 300) || statusCode === 304 } @@ -444,7 +444,7 @@ SendStream.prototype.isFresh = function isFresh () { */ SendStream.prototype.isRangeFresh = function isRangeFresh () { - var ifRange = this.req.headers['if-range'] + const ifRange = this.req.headers['if-range'] if (!ifRange) { return true @@ -452,12 +452,12 @@ SendStream.prototype.isRangeFresh = function isRangeFresh () { // if-range as etag if (ifRange.indexOf('"') !== -1) { - var etag = this.res.getHeader('ETag') + const etag = this.res.getHeader('ETag') return Boolean(etag && ifRange.indexOf(etag) !== -1) } // if-range as modified date - var lastModified = this.res.getHeader('Last-Modified') + const lastModified = this.res.getHeader('Last-Modified') return parseHttpDate(lastModified) <= parseHttpDate(ifRange) } @@ -469,7 +469,7 @@ SendStream.prototype.isRangeFresh = function isRangeFresh () { */ SendStream.prototype.redirect = function redirect (path) { - var res = this.res + const res = this.res if (hasListeners(this, 'directory')) { this.emit('directory', res, path) @@ -481,8 +481,8 @@ SendStream.prototype.redirect = function redirect (path) { return } - var loc = encodeUrl(collapseLeadingSlashes(this.path + '/')) - var doc = createHtmlDocument('Redirecting', 'Redirecting to ' + + const loc = encodeUrl(collapseLeadingSlashes(this.path + '/')) + const doc = createHtmlDocument('Redirecting', 'Redirecting to ' + escapeHtml(loc) + '') // redirect @@ -505,13 +505,13 @@ SendStream.prototype.redirect = function redirect (path) { SendStream.prototype.pipe = function pipe (res) { // root path - var root = this._root + const root = this._root // references this.res = res // decode the path - var path = decode(this.path) + let path = decode(this.path) if (path === -1) { this.error(400) return res @@ -523,7 +523,7 @@ SendStream.prototype.pipe = function pipe (res) { return res } - var parts + let parts if (root !== null) { // normalize if (path) { @@ -559,7 +559,7 @@ SendStream.prototype.pipe = function pipe (res) { // dotfile handling if (containsDotFile(parts)) { - var access = this._dotfiles + let access = this._dotfiles // legacy support if (access === undefined) { @@ -600,13 +600,13 @@ SendStream.prototype.pipe = function pipe (res) { */ SendStream.prototype.send = function send (path, stat) { - var len = stat.size - var options = this.options - var opts = {} - var res = this.res - var req = this.req - var ranges = req.headers.range - var offset = options.start || 0 + let len = stat.size + const options = this.options + const opts = {} + const res = this.res + const req = this.req + let ranges = req.headers.range + let offset = options.start || 0 if (headersSent(res)) { // impossible to send now @@ -638,7 +638,7 @@ SendStream.prototype.send = function send (path, stat) { // adjust len to start/end options len = Math.max(0, len - offset) if (options.end !== undefined) { - var bytes = options.end - offset + 1 + const bytes = options.end - offset + 1 if (len > bytes) len = bytes } @@ -683,7 +683,7 @@ SendStream.prototype.send = function send (path, stat) { } // clone options - for (var prop in options) { + for (const prop in options) { opts[prop] = options[prop] } @@ -710,8 +710,8 @@ SendStream.prototype.send = function send (path, stat) { * @api private */ SendStream.prototype.sendFile = function sendFile (path) { - var i = 0 - var self = this + let i = 0 + const self = this debug('stat "%s"', path) fs.stat(path, function onstat (err, stat) { @@ -732,7 +732,7 @@ SendStream.prototype.sendFile = function sendFile (path) { : self.error(404) } - var p = path + '.' + self._extensions[i++] + const p = path + '.' + self._extensions[i++] debug('stat "%s"', p) fs.stat(p, function (err, stat) { @@ -751,8 +751,8 @@ SendStream.prototype.sendFile = function sendFile (path) { * @api private */ SendStream.prototype.sendIndex = function sendIndex (path) { - var i = -1 - var self = this + let i = -1 + const self = this function next (err) { if (++i >= self._index.length) { @@ -760,7 +760,7 @@ SendStream.prototype.sendIndex = function sendIndex (path) { return self.error(404) } - var p = join(path, self._index[i]) + const p = join(path, self._index[i]) debug('stat "%s"', p) fs.stat(p, function (err, stat) { @@ -783,11 +783,11 @@ SendStream.prototype.sendIndex = function sendIndex (path) { */ SendStream.prototype.stream = function stream (path, options) { - var self = this - var res = this.res + const self = this + const res = this.res // pipe - var stream = fs.createReadStream(path, options) + const stream = fs.createReadStream(path, options) this.emit('stream', stream) stream.pipe(res) @@ -823,18 +823,18 @@ SendStream.prototype.stream = function stream (path, options) { */ SendStream.prototype.type = function type (path) { - var res = this.res + const res = this.res if (res.getHeader('Content-Type')) return - var type = mime.lookup(path) + const type = mime.lookup(path) if (!type) { debug('no content-type') return } - var charset = mime.charsets.lookup(type) + const charset = mime.charsets.lookup(type) debug('content-type %s', type) res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : '')) @@ -850,7 +850,7 @@ SendStream.prototype.type = function type (path) { */ SendStream.prototype.setHeader = function setHeader (path, stat) { - var res = this.res + const res = this.res this.emit('headers', res, path, stat) @@ -860,7 +860,7 @@ SendStream.prototype.setHeader = function setHeader (path, stat) { } if (this._cacheControl && !res.getHeader('Cache-Control')) { - var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000) + let cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000) if (this._immutable) { cacheControl += ', immutable' @@ -871,13 +871,13 @@ SendStream.prototype.setHeader = function setHeader (path, stat) { } if (this._lastModified && !res.getHeader('Last-Modified')) { - var modified = stat.mtime.toUTCString() + const modified = stat.mtime.toUTCString() debug('modified %s', modified) res.setHeader('Last-Modified', modified) } if (this._etag && !res.getHeader('ETag')) { - var val = etag(stat) + const val = etag(stat) debug('etag %s', val) res.setHeader('ETag', val) } @@ -891,9 +891,9 @@ SendStream.prototype.setHeader = function setHeader (path, stat) { */ function clearHeaders (res) { - var headers = getHeaderNames(res) + const headers = getHeaderNames(res) - for (var i = 0; i < headers.length; i++) { + for (let i = 0; i < headers.length; i++) { res.removeHeader(headers[i]) } } @@ -905,7 +905,9 @@ function clearHeaders (res) { * @private */ function collapseLeadingSlashes (str) { - for (var i = 0; i < str.length; i++) { + let i = 0 + const il = str.length + for (; i < il; ++i) { if (str[i] !== '/') { break } @@ -923,8 +925,8 @@ function collapseLeadingSlashes (str) { */ function containsDotFile (parts) { - for (var i = 0; i < parts.length; i++) { - var part = parts[i] + for (let i = 0; i < parts.length; i++) { + const part = parts[i] if (part.length > 1 && part[0] === '.') { return true } @@ -1029,7 +1031,7 @@ function getHeaderNames (res) { */ function hasListeners (emitter, type) { - var count = typeof emitter.listenerCount !== 'function' + const count = typeof emitter.listenerCount !== 'function' ? emitter.listeners(type).length : emitter.listenerCount(type) @@ -1059,9 +1061,9 @@ function headersSent (res) { */ function normalizeList (val, name) { - var list = [].concat(val || []) + const list = [].concat(val || []) - for (var i = 0; i < list.length; i++) { + for (let i = 0; i < list.length; i++) { if (typeof list[i] !== 'string') { throw new TypeError(name + ' must be array of strings or false') } @@ -1078,7 +1080,7 @@ function normalizeList (val, name) { */ function parseHttpDate (date) { - var timestamp = date && Date.parse(date) + const timestamp = date && Date.parse(date) return typeof timestamp === 'number' ? timestamp @@ -1093,12 +1095,12 @@ function parseHttpDate (date) { */ function parseTokenList (str) { - var end = 0 - var list = [] - var start = 0 + let end = 0 + const list = [] + let start = 0 // gather tokens - for (var i = 0, len = str.length; i < len; i++) { + for (let i = 0, len = str.length; i < len; i++) { switch (str.charCodeAt(i)) { case 0x20: /* */ if (start === end) { @@ -1134,10 +1136,10 @@ function parseTokenList (str) { */ function setHeaders (res, headers) { - var keys = Object.keys(headers) + const keys = Object.keys(headers) - for (var i = 0; i < keys.length; i++) { - var key = keys[i] + for (let i = 0; i < keys.length; i++) { + const key = keys[i] res.setHeader(key, headers[key]) } } diff --git a/package.json b/package.json index 2706bcc..89ec1dc 100644 --- a/package.json +++ b/package.json @@ -32,31 +32,14 @@ }, "devDependencies": { "after": "0.8.2", - "eslint": "7.32.0", - "eslint-config-standard": "14.1.1", - "eslint-plugin-import": "2.25.4", - "eslint-plugin-markdown": "2.2.1", - "eslint-plugin-node": "11.1.0", - "eslint-plugin-promise": "5.2.0", - "eslint-plugin-standard": "4.1.0", - "mocha": "9.2.2", - "nyc": "15.1.0", - "supertest": "6.2.2" - }, - "files": [ - "HISTORY.md", - "LICENSE", - "README.md", - "SECURITY.md", - "index.js" - ], - "engines": { - "node": ">= 0.8.0" + "standard": "^17.0.0", + "supertest": "6.2.2", + "tap": "^16.3.3" }, "scripts": { - "lint": "eslint .", - "test": "mocha --check-leaks --reporter spec --bail", - "test-ci": "nyc --reporter=lcov --reporter=text npm test", - "test-cov": "nyc --reporter=html --reporter=text npm test" + "lint": "standard", + "lint:fix": "standard --fix", + "test": "npm run test:unit", + "test:unit": "tap" } } diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml deleted file mode 100644 index 9808c3b..0000000 --- a/test/.eslintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -env: - mocha: true diff --git a/test/mime.test.js b/test/mime.test.js new file mode 100644 index 0000000..b904c24 --- /dev/null +++ b/test/mime.test.js @@ -0,0 +1,52 @@ +'use strict' + +process.env.NO_DEPRECATION = 'send' + +const { test } = require('tap') +const path = require('path') +const request = require('supertest') +const send = require('..') +const { shouldNotHaveHeader, createServer } = require('./utils') + +const fixtures = path.join(__dirname, 'fixtures') + +test('send.mime', function (t) { + t.plan(2) + + t.test('should be exposed', function (t) { + t.plan(1) + t.ok(send.mime) + }) + + t.test('.default_type', function (t) { + t.plan(2) + + t.before(function () { + this.default_type = send.mime.default_type + }) + + t.afterEach(function () { + send.mime.default_type = this.default_type + }) + + t.test('should change the default type', function (t) { + t.plan(1) + send.mime.default_type = 'text/plain' + + request(createServer({ root: fixtures })) + .get('/no_ext') + .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect(200, () => t.pass()) + }) + + t.test('should not add Content-Type for undefined default', function (t) { + t.plan(2) + send.mime.default_type = undefined + + request(createServer({ root: fixtures })) + .get('/no_ext') + .expect(shouldNotHaveHeader('Content-Type', t)) + .expect(200, () => t.pass()) + }) + }) +}) diff --git a/test/send-pipe.test.js b/test/send-pipe.test.js new file mode 100644 index 0000000..69d3ce6 --- /dev/null +++ b/test/send-pipe.test.js @@ -0,0 +1,1623 @@ +'use strict' + +process.env.NO_DEPRECATION = 'send' + +const { test } = require('tap') +const after = require('after') +const fs = require('fs') +const http = require('http') +const path = require('path') +const request = require('supertest') +const send = require('..') +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).pipe(res)', function (t) { + t.plan(32) + + t.test('should stream the file contents', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect('Content-Length', '4') + .expect(200, 'tobi', () => t.pass()) + }) + + t.test('should stream a zero-length file', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/empty.txt') + .expect('Content-Length', '0') + .expect(200, '', () => t.pass()) + }) + + t.test('should decode the given path as a URI', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/some%20thing.txt') + .expect(200, 'hey', () => t.pass()) + }) + + t.test('should serve files with dots in name', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/do..ts.txt') + .expect(200, '...', () => t.pass()) + }) + + t.test('should treat a malformed URI as a bad request', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/some%99thing.txt') + .expect(400, 'Bad Request', () => t.pass()) + }) + + t.test('should 400 on NULL bytes', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/some%00thing.txt') + .expect(400, 'Bad Request', () => t.pass()) + }) + + t.test('should treat an ENAMETOOLONG as a 404', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + const path = Array(100).join('foobar') + request(app) + .get('/' + path) + .expect(404, () => t.pass()) + }) + + t.test('should handle headers already sent error', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + res.write('0') + send(req, req.url, { root: fixtures }) + .on('error', function (err) { res.end(' - ' + err.message) }) + .pipe(res) + }) + request(app) + .get('/name.txt') + .expect(200, '0 - Can\'t set headers after they are sent.', () => t.pass()) + }) + + t.test('should support HEAD', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .head('/name.txt') + .expect(200) + .expect('Content-Length', '4') + .expect(shouldNotHaveBody(t)) + .end(() => t.pass()) + }) + + t.test('should add an ETag header field', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect('etag', /^W\/"[^"]+"$/) + .end(() => t.pass()) + }) + + t.test('should add a Date header field', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect('date', dateRegExp, () => t.pass()) + }) + + t.test('should add a Last-Modified header field', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect('last-modified', dateRegExp, () => t.pass()) + }) + + t.test('should add a Accept-Ranges header field', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect('Accept-Ranges', 'bytes', () => t.pass()) + }) + + t.test('should 404 if the file does not exist', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/meow') + .expect(404, 'Not Found', () => t.pass()) + }) + + t.test('should emit ENOENT if the file does not exist', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .on('error', function (err) { res.end(err.statusCode + ' ' + err.code) }) + .pipe(res) + }) + + request(app) + .get('/meow') + .expect(200, '404 ENOENT', () => t.pass()) + }) + + t.test('should not override content-type', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + res.setHeader('Content-Type', 'application/x-custom') + send(req, req.url, { root: fixtures }).pipe(res) + }) + request(app) + .get('/name.txt') + .expect('Content-Type', 'application/x-custom', () => t.pass()) + }) + + t.test('should set Content-Type via mime map', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect(200, function (err) { + t.error(err) + request(app) + .get('/tobi.html') + .expect('Content-Type', 'text/html; charset=UTF-8') + .expect(200, () => t.pass()) + }) + }) + + t.test('should 404 if file disappears after stat, before open', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url, { root: 'test/fixtures' }) + .on('file', function () { + // simulate file ENOENT after on open, after stat + const fn = this.send + this.send = function (path, stat) { + fn.call(this, (path + '__xxx_no_exist'), stat) + } + }) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(404, () => t.pass()) + }) + + t.test('should 500 on file stream error', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url, { root: 'test/fixtures' }) + .on('stream', function (stream) { + // simulate file error + stream.on('open', function () { + stream.emit('error', new Error('boom!')) + }) + }) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(500, () => t.pass()) + }) + + t.test('"headers" event', function (t) { + t.plan(7) + t.test('should fire when sending file', function (t) { + t.plan(1) + const cb = after(2, () => t.pass()) + const server = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .on('headers', function () { cb() }) + .pipe(res) + }) + + request(server) + .get('/name.txt') + .expect(200, 'tobi', cb) + }) + + t.test('should not fire on 404', function (t) { + t.plan(1) + const cb = after(1, () => t.pass()) + const server = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .on('headers', function () { cb() }) + .pipe(res) + }) + + request(server) + .get('/bogus') + .expect(404, cb) + }) + + t.test('should fire on index', function (t) { + t.plan(1) + const cb = after(2, () => t.pass()) + const server = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .on('headers', function () { cb() }) + .pipe(res) + }) + + request(server) + .get('/pets/') + .expect(200, /tobi/, cb) + }) + + t.test('should not fire on redirect', function (t) { + t.plan(1) + const cb = after(1, () => t.pass()) + const server = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .on('headers', function () { cb() }) + .pipe(res) + }) + + request(server) + .get('/pets') + .expect(301, cb) + }) + + t.test('should provide path', function (t) { + t.plan(3) + const cb = after(2, () => t.pass()) + const server = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .on('headers', onHeaders) + .pipe(res) + }) + + function onHeaders (res, filePath) { + t.ok(filePath) + t.strictSame(path.normalize(filePath), path.normalize(path.join(fixtures, 'name.txt'))) + cb() + } + + request(server) + .get('/name.txt') + .expect(200, 'tobi', cb) + }) + + t.test('should provide stat', function (t) { + t.plan(4) + const cb = after(2, () => t.pass()) + const server = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .on('headers', onHeaders) + .pipe(res) + }) + + function onHeaders (res, path, stat) { + t.ok(stat) + t.ok('ctime' in stat) + t.ok('mtime' in stat) + cb() + } + + request(server) + .get('/name.txt') + .expect(200, 'tobi', cb) + }) + + t.test('should allow altering headers', function (t) { + t.plan(1) + const server = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .on('headers', onHeaders) + .pipe(res) + }) + + function onHeaders (res, path, stat) { + res.setHeader('Cache-Control', 'no-cache') + res.setHeader('Content-Type', 'text/x-custom') + res.setHeader('ETag', 'W/"everything"') + res.setHeader('X-Created', stat.ctime.toUTCString()) + } + + request(server) + .get('/name.txt') + .expect(200) + .expect('Cache-Control', 'no-cache') + .expect('Content-Type', 'text/x-custom') + .expect('ETag', 'W/"everything"') + .expect('X-Created', dateRegExp) + .expect('tobi') + .end(() => t.pass()) + }) + }) + + t.test('when "directory" listeners are present', function (t) { + t.plan(2) + + t.test('should be called when sending directory', function (t) { + t.plan(1) + const server = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .on('directory', onDirectory) + .pipe(res) + }) + + function onDirectory (res) { + res.statusCode = 400 + res.end('No directory for you') + } + + request(server) + .get('/pets') + .expect(400, 'No directory for you', () => t.pass()) + }) + + t.test('should be called with path', function (t) { + t.plan(1) + const server = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .on('directory', onDirectory) + .pipe(res) + }) + + function onDirectory (res, dirPath) { + res.end(path.normalize(dirPath)) + } + + request(server) + .get('/pets') + .expect(200, path.normalize(path.join(fixtures, 'pets')), () => t.pass()) + }) + }) + + t.test('when no "directory" listeners are present', function (t) { + t.plan(5) + + t.test('should redirect directories to trailing slash', function (t) { + t.plan(1) + + request(createServer({ root: fixtures })) + .get('/pets') + .expect('Location', '/pets/') + .expect(301, () => t.pass()) + }) + + t.test('should respond with an HTML redirect', function (t) { + t.plan(1) + + request(createServer({ root: fixtures })) + .get('/pets') + .expect('Location', '/pets/') + .expect('Content-Type', /html/) + .expect(301, />Redirecting to \/pets\/<\/a> t.pass()) + }) + + t.test('should respond with default Content-Security-Policy', function (t) { + t.plan(1) + + request(createServer({ root: fixtures })) + .get('/pets') + .expect('Location', '/pets/') + .expect('Content-Security-Policy', "default-src 'none'") + .expect(301, () => t.pass()) + }) + + t.test('should not redirect to protocol-relative locations', function (t) { + t.plan(1) + + request(createServer({ root: fixtures })) + .get('//pets') + .expect('Location', '/pets/') + .expect(301, () => t.pass()) + }) + + t.test('should respond with an HTML redirect', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url.replace('/snow', '/snow ☃'), { root: 'test/fixtures' }) + .pipe(res) + }) + + request(app) + .get('/snow') + .expect('Location', '/snow%20%E2%98%83/') + .expect('Content-Type', /html/) + .expect(301, />Redirecting to \/snow%20%E2%98%83\/<\/a> t.pass()) + }) + }) + + t.test('when no "error" listeners are present', function (t) { + t.plan(3) + + t.test('should respond to errors directly', function (t) { + t.plan(1) + + request(createServer({ root: fixtures })) + .get('/foobar') + .expect(404, />Not Found t.pass()) + }) + + t.test('should respond with default Content-Security-Policy', function (t) { + t.plan(1) + + request(createServer({ root: fixtures })) + .get('/foobar') + .expect('Content-Security-Policy', "default-src 'none'") + .expect(404, () => t.pass()) + }) + + t.test('should remove all previously-set headers', function (t) { + t.plan(2) + + const server = createServer({ root: fixtures }, function (req, res) { + res.setHeader('X-Foo', 'bar') + }) + + request(server) + .get('/foobar') + .expect(shouldNotHaveHeader('X-Foo', t)) + .expect(404, () => t.pass()) + }) + }) + + t.test('with conditional-GET', function (t) { + t.plan(6) + + t.test('should remove Content headers with 304', function (t) { + t.plan(5) + + 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') + }) + + request(server) + .get('/name.txt') + .expect(200, function (err, res) { + t.error(err) + request(server) + .get('/name.txt') + .set('If-None-Match', res.headers.etag) + .expect(shouldNotHaveHeader('Content-Language', t)) + .expect(shouldNotHaveHeader('Content-Length', t)) + .expect(shouldNotHaveHeader('Content-Type', t)) + .expect('Content-Location', 'http://localhost/name.txt') + .expect('Contents', 'foo') + .expect(304, () => t.pass()) + }) + }) + + t.test('should not remove all Content-* headers', function (t) { + t.plan(4) + + const server = createServer({ root: fixtures }, function (req, res) { + res.setHeader('Content-Location', 'http://localhost/name.txt') + res.setHeader('Content-Security-Policy', 'default-src \'self\'') + }) + + request(server) + .get('/name.txt') + .expect(200, function (err, res) { + t.error(err) + request(server) + .get('/name.txt') + .set('If-None-Match', res.headers.etag) + .expect(shouldNotHaveHeader('Content-Length', t)) + .expect(shouldNotHaveHeader('Content-Type', t)) + .expect('Content-Location', 'http://localhost/name.txt') + .expect('Content-Security-Policy', 'default-src \'self\'') + .expect(304, () => t.pass()) + }) + }) + + t.test('where "If-Match" is set', function (t) { + t.plan(3) + + t.test('should respond with 200 when "*"', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .set('If-Match', '*') + .expect(200, () => t.pass()) + }) + + t.test('should respond with 412 when ETag unmatched', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .set('If-Match', ' "foo",, "bar" ,') + .expect(412, () => t.pass()) + }) + + t.test('should respond with 200 when ETag matched', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(200, function (err, res) { + t.error(err) + request(app) + .get('/name.txt') + .set('If-Match', '"foo", "bar", ' + res.headers.etag) + .expect(200, () => t.pass()) + }) + }) + }) + + t.test('where "If-Modified-Since" is set', function (t) { + t.plan(2) + + t.test('should respond with 304 when unmodified', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(200, function (err, res) { + t.error(err) + request(app) + .get('/name.txt') + .set('If-Modified-Since', res.headers['last-modified']) + .expect(304, () => t.pass()) + }) + }) + + t.test('should respond with 200 when modified', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(200, function (err, res) { + t.error(err) + const lmod = new Date(res.headers['last-modified']) + const date = new Date(lmod - 60000) + request(app) + .get('/name.txt') + .set('If-Modified-Since', date.toUTCString()) + .expect(200, 'tobi', () => t.pass()) + }) + }) + }) + + t.test('where "If-None-Match" is set', function (t) { + t.plan(2) + + t.test('should respond with 304 when ETag matched', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(200, function (err, res) { + t.error(err) + request(app) + .get('/name.txt') + .set('If-None-Match', res.headers.etag) + .expect(304, () => t.pass()) + }) + }) + + t.test('should respond with 200 when ETag unmatched', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(200, function (err, res) { + t.error(err) + request(app) + .get('/name.txt') + .set('If-None-Match', '"123"') + .expect(200, 'tobi', () => t.pass()) + }) + }) + }) + + t.test('where "If-Unmodified-Since" is set', function (t) { + t.plan(3) + + t.test('should respond with 200 when unmodified', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(200, function (err, res) { + t.error(err) + request(app) + .get('/name.txt') + .set('If-Unmodified-Since', res.headers['last-modified']) + .expect(200, () => t.pass()) + }) + }) + + t.test('should respond with 412 when modified', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(200, function (err, res) { + t.error(err) + const lmod = new Date(res.headers['last-modified']) + const date = new Date(lmod - 60000).toUTCString() + request(app) + .get('/name.txt') + .set('If-Unmodified-Since', date) + .expect(412, () => t.pass()) + }) + }) + + t.test('should respond with 200 when invalid date', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .set('If-Unmodified-Since', 'foo') + .expect(200, () => t.pass()) + }) + }) + }) + + t.test('with Range request', function (t) { + t.plan(13) + + t.test('should support byte ranges', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=0-4') + .expect(206, '12345', () => t.pass()) + }) + + t.test('should ignore non-byte ranges', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'items=0-4') + .expect(200, '123456789', () => t.pass()) + }) + + t.test('should be inclusive', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=0-0') + .expect(206, '1', () => t.pass()) + }) + + t.test('should set Content-Range', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=2-5') + .expect('Content-Range', 'bytes 2-5/9') + .expect(206, () => t.pass()) + }) + + t.test('should support -n', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=-3') + .expect(206, '789', () => t.pass()) + }) + + t.test('should support n-', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=3-') + .expect(206, '456789', () => t.pass()) + }) + + t.test('should respond with 206 "Partial Content"', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=0-4') + .expect(206, () => t.pass()) + }) + + t.test('should set Content-Length to the # of octets transferred', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=2-3') + .expect('Content-Length', '2') + .expect(206, '34', () => t.pass()) + }) + + t.test('when last-byte-pos of the range is greater the length', function (t) { + t.plan(2) + + t.test('is taken to be equal to one less than the length', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=2-50') + .expect('Content-Range', 'bytes 2-8/9') + .expect(206, () => t.pass()) + }) + + t.test('should adapt the Content-Length accordingly', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=2-50') + .expect('Content-Length', '7') + .expect(206, () => t.pass()) + }) + }) + + t.test('when the first- byte-pos of the range is greater length', function (t) { + t.plan(2) + + t.test('should respond with 416', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=9-50') + .expect('Content-Range', 'bytes */9') + .expect(416, () => t.pass()) + }) + + t.test('should emit error 416 with content-range header', function (t) { + t.plan(1) + + const server = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .on('error', function (err) { + res.setHeader('X-Content-Range', err.headers['Content-Range']) + res.statusCode = err.statusCode + res.end(err.message) + }) + .pipe(res) + }) + + request(server) + .get('/nums.txt') + .set('Range', 'bytes=9-50') + .expect('X-Content-Range', 'bytes */9') + .expect(416, () => t.pass()) + }) + }) + + t.test('when syntactically invalid', function (t) { + t.plan(1) + + t.test('should respond with 200 and the entire contents', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'asdf') + .expect(200, '123456789', () => t.pass()) + }) + }) + + t.test('when multiple ranges', function (t) { + t.plan(2) + + t.test('should respond with 200 and the entire contents', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=1-1,3-') + .expect(shouldNotHaveHeader('Content-Range', t)) + .expect(200, '123456789', () => t.pass()) + }) + + t.test('should respond with 206 is all ranges can be combined', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('Range', 'bytes=1-2,3-5') + .expect('Content-Range', 'bytes 1-5/9') + .expect(206, '23456', () => t.pass()) + }) + }) + + t.test('when if-range present', function (t) { + t.plan(5) + + t.test('should respond with parts when etag unchanged', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .expect(200, function (err, res) { + t.error(err) + const etag = res.headers.etag + + request(app) + .get('/nums.txt') + .set('If-Range', etag) + .set('Range', 'bytes=0-0') + .expect(206, '1', () => t.pass()) + }) + }) + + t.test('should respond with 200 when etag changed', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .expect(200, function (err, res) { + t.error(err) + const etag = res.headers.etag.replace(/"(.)/, '"0$1') + + request(app) + .get('/nums.txt') + .set('If-Range', etag) + .set('Range', 'bytes=0-0') + .expect(200, '123456789', () => t.pass()) + }) + }) + + t.test('should respond with parts when modified unchanged', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .expect(200, function (err, res) { + t.error(err) + const modified = res.headers['last-modified'] + + request(app) + .get('/nums.txt') + .set('If-Range', modified) + .set('Range', 'bytes=0-0') + .expect(206, '1', () => t.pass()) + }) + }) + + t.test('should respond with 200 when modified changed', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .expect(200, function (err, res) { + t.error(err) + const modified = Date.parse(res.headers['last-modified']) - 20000 + + request(app) + .get('/nums.txt') + .set('If-Range', new Date(modified).toUTCString()) + .set('Range', 'bytes=0-0') + .expect(200, '123456789', () => t.pass()) + }) + }) + + t.test('should respond with 200 when invalid value', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + + request(app) + .get('/nums.txt') + .set('If-Range', 'foo') + .set('Range', 'bytes=0-0') + .expect(200, '123456789', () => t.pass()) + }) + }) + }) + + t.test('when "options" is specified', function (t) { + t.plan(4) + + t.test('should support start/end', function (t) { + t.plan(1) + + request(createServer({ root: fixtures, start: 3, end: 5 })) + .get('/nums.txt') + .expect(200, '456', () => t.pass()) + }) + + t.test('should adjust too large end', function (t) { + t.plan(1) + + request(createServer({ root: fixtures, start: 3, end: 90 })) + .get('/nums.txt') + .expect(200, '456789', () => t.pass()) + }) + + t.test('should support start/end with Range request', function (t) { + t.plan(1) + + request(createServer({ root: fixtures, start: 0, end: 2 })) + .get('/nums.txt') + .set('Range', 'bytes=-2') + .expect(206, '23', () => t.pass()) + }) + + t.test('should support start/end with unsatisfiable Range request', function (t) { + t.plan(1) + + request(createServer({ root: fixtures, start: 0, end: 2 })) + .get('/nums.txt') + .set('Range', 'bytes=5-9') + .expect('Content-Range', 'bytes */3') + .expect(416, () => t.pass()) + }) + }) + + t.test('.etag()', function (t) { + t.plan(1) + + t.test('should support disabling etags', function (t) { + t.plan(2) + + const app = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .etag(false) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(shouldNotHaveHeader('ETag', t)) + .expect(200, () => t.pass()) + }) + }) + + t.test('.from()', function (t) { + t.plan(1) + + t.test('should set with deprecated from', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url) + .from(fixtures) + .pipe(res) + }) + + request(app) + .get('/pets/../name.txt') + .expect(200, 'tobi', () => t.pass()) + }) + }) + + t.test('.hidden()', function (t) { + t.plan(1) + + t.test('should default support sending hidden files', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .hidden(true) + .pipe(res) + }) + + request(app) + .get('/.hidden.txt') + .expect(200, 'secret', () => t.pass()) + }) + }) + + t.test('.index()', function (t) { + t.plan(3) + + t.test('should be configurable', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .index('tobi.html') + .pipe(res) + }) + + request(app) + .get('/') + .expect(200, '

tobi

', () => t.pass()) + }) + + t.test('should support disabling', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .index(false) + .pipe(res) + }) + + request(app) + .get('/pets/') + .expect(403, () => t.pass()) + }) + + t.test('should support fallbacks', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures }) + .index(['default.htm', 'index.html']) + .pipe(res) + }) + + request(app) + .get('/pets/') + .expect(200, fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'), () => t.pass()) + }) + }) + + t.test('.maxage()', function (t) { + t.plan(4) + + t.test('should default to 0', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, 'test/fixtures/name.txt') + .maxage(undefined) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect('Cache-Control', 'public, max-age=0', () => t.pass()) + }) + + t.test('should floor to integer', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, 'test/fixtures/name.txt') + .maxage(1234) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect('Cache-Control', 'public, max-age=1', () => t.pass()) + }) + + t.test('should accept string', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, 'test/fixtures/name.txt') + .maxage('30d') + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect('Cache-Control', 'public, max-age=2592000', () => t.pass()) + }) + + t.test('should max at 1 year', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, 'test/fixtures/name.txt') + .maxage(Infinity) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect('Cache-Control', 'public, max-age=31536000', () => t.pass()) + }) + }) + + t.test('.root()', function (t) { + t.plan(1) + + t.test('should set root', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url) + .root(fixtures) + .pipe(res) + }) + + request(app) + .get('/pets/../name.txt') + .expect(200, 'tobi', () => t.pass()) + }) + }) +}) diff --git a/test/send.js b/test/send.js deleted file mode 100644 index d419f8f..0000000 --- a/test/send.js +++ /dev/null @@ -1,1518 +0,0 @@ - -process.env.NO_DEPRECATION = 'send' - -var after = require('after') -var assert = require('assert') -var fs = require('fs') -var http = require('http') -var path = require('path') -var request = require('supertest') -var send = require('..') - -// test server - -var dateRegExp = /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/ -var fixtures = path.join(__dirname, 'fixtures') -var app = http.createServer(function (req, res) { - function error (err) { - res.statusCode = err.status - res.end(http.STATUS_CODES[err.status]) - } - - send(req, req.url, { root: fixtures }) - .on('error', error) - .pipe(res) -}) - -describe('send(file).pipe(res)', function () { - it('should stream the file contents', function (done) { - request(app) - .get('/name.txt') - .expect('Content-Length', '4') - .expect(200, 'tobi', done) - }) - - it('should stream a zero-length file', function (done) { - request(app) - .get('/empty.txt') - .expect('Content-Length', '0') - .expect(200, '', done) - }) - - it('should decode the given path as a URI', function (done) { - request(app) - .get('/some%20thing.txt') - .expect(200, 'hey', done) - }) - - it('should serve files with dots in name', function (done) { - request(app) - .get('/do..ts.txt') - .expect(200, '...', done) - }) - - it('should treat a malformed URI as a bad request', function (done) { - request(app) - .get('/some%99thing.txt') - .expect(400, 'Bad Request', done) - }) - - it('should 400 on NULL bytes', function (done) { - request(app) - .get('/some%00thing.txt') - .expect(400, 'Bad Request', done) - }) - - it('should treat an ENAMETOOLONG as a 404', function (done) { - var path = Array(100).join('foobar') - request(app) - .get('/' + path) - .expect(404, done) - }) - - it('should handle headers already sent error', function (done) { - var app = http.createServer(function (req, res) { - res.write('0') - send(req, req.url, { root: fixtures }) - .on('error', function (err) { res.end(' - ' + err.message) }) - .pipe(res) - }) - request(app) - .get('/name.txt') - .expect(200, '0 - Can\'t set headers after they are sent.', done) - }) - - it('should support HEAD', function (done) { - request(app) - .head('/name.txt') - .expect(200) - .expect('Content-Length', '4') - .expect(shouldNotHaveBody()) - .end(done) - }) - - it('should add an ETag header field', function (done) { - request(app) - .get('/name.txt') - .expect('etag', /^W\/"[^"]+"$/) - .end(done) - }) - - it('should add a Date header field', function (done) { - request(app) - .get('/name.txt') - .expect('date', dateRegExp, done) - }) - - it('should add a Last-Modified header field', function (done) { - request(app) - .get('/name.txt') - .expect('last-modified', dateRegExp, done) - }) - - it('should add a Accept-Ranges header field', function (done) { - request(app) - .get('/name.txt') - .expect('Accept-Ranges', 'bytes', done) - }) - - it('should 404 if the file does not exist', function (done) { - request(app) - .get('/meow') - .expect(404, 'Not Found', done) - }) - - it('should emit ENOENT if the file does not exist', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .on('error', function (err) { res.end(err.statusCode + ' ' + err.code) }) - .pipe(res) - }) - - request(app) - .get('/meow') - .expect(200, '404 ENOENT', done) - }) - - it('should not override content-type', function (done) { - var app = http.createServer(function (req, res) { - res.setHeader('Content-Type', 'application/x-custom') - send(req, req.url, { root: fixtures }).pipe(res) - }) - request(app) - .get('/name.txt') - .expect('Content-Type', 'application/x-custom', done) - }) - - it('should set Content-Type via mime map', function (done) { - request(app) - .get('/name.txt') - .expect('Content-Type', 'text/plain; charset=UTF-8') - .expect(200, function (err) { - if (err) return done(err) - request(app) - .get('/tobi.html') - .expect('Content-Type', 'text/html; charset=UTF-8') - .expect(200, done) - }) - }) - - it('should 404 if file disappears after stat, before open', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: 'test/fixtures' }) - .on('file', function () { - // simulate file ENOENT after on open, after stat - var fn = this.send - this.send = function (path, stat) { - fn.call(this, (path + '__xxx_no_exist'), stat) - } - }) - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect(404, done) - }) - - it('should 500 on file stream error', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: 'test/fixtures' }) - .on('stream', function (stream) { - // simulate file error - stream.on('open', function () { - stream.emit('error', new Error('boom!')) - }) - }) - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect(500, done) - }) - - describe('"headers" event', function () { - it('should fire when sending file', function (done) { - var cb = after(2, done) - var server = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .on('headers', function () { cb() }) - .pipe(res) - }) - - request(server) - .get('/name.txt') - .expect(200, 'tobi', cb) - }) - - it('should not fire on 404', function (done) { - var cb = after(1, done) - var server = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .on('headers', function () { cb() }) - .pipe(res) - }) - - request(server) - .get('/bogus') - .expect(404, cb) - }) - - it('should fire on index', function (done) { - var cb = after(2, done) - var server = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .on('headers', function () { cb() }) - .pipe(res) - }) - - request(server) - .get('/pets/') - .expect(200, /tobi/, cb) - }) - - it('should not fire on redirect', function (done) { - var cb = after(1, done) - var server = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .on('headers', function () { cb() }) - .pipe(res) - }) - - request(server) - .get('/pets') - .expect(301, cb) - }) - - it('should provide path', function (done) { - var cb = after(2, done) - var server = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .on('headers', onHeaders) - .pipe(res) - }) - - function onHeaders (res, filePath) { - assert.ok(filePath) - assert.strictEqual(path.normalize(filePath), path.normalize(path.join(fixtures, 'name.txt'))) - cb() - } - - request(server) - .get('/name.txt') - .expect(200, 'tobi', cb) - }) - - it('should provide stat', function (done) { - var cb = after(2, done) - var server = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .on('headers', onHeaders) - .pipe(res) - }) - - function onHeaders (res, path, stat) { - assert.ok(stat) - assert.ok('ctime' in stat) - assert.ok('mtime' in stat) - cb() - } - - request(server) - .get('/name.txt') - .expect(200, 'tobi', cb) - }) - - it('should allow altering headers', function (done) { - var server = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .on('headers', onHeaders) - .pipe(res) - }) - - function onHeaders (res, path, stat) { - res.setHeader('Cache-Control', 'no-cache') - res.setHeader('Content-Type', 'text/x-custom') - res.setHeader('ETag', 'W/"everything"') - res.setHeader('X-Created', stat.ctime.toUTCString()) - } - - request(server) - .get('/name.txt') - .expect(200) - .expect('Cache-Control', 'no-cache') - .expect('Content-Type', 'text/x-custom') - .expect('ETag', 'W/"everything"') - .expect('X-Created', dateRegExp) - .expect('tobi') - .end(done) - }) - }) - - describe('when "directory" listeners are present', function () { - it('should be called when sending directory', function (done) { - var server = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .on('directory', onDirectory) - .pipe(res) - }) - - function onDirectory (res) { - res.statusCode = 400 - res.end('No directory for you') - } - - request(server) - .get('/pets') - .expect(400, 'No directory for you', done) - }) - - it('should be called with path', function (done) { - var server = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .on('directory', onDirectory) - .pipe(res) - }) - - function onDirectory (res, dirPath) { - res.end(path.normalize(dirPath)) - } - - request(server) - .get('/pets') - .expect(200, path.normalize(path.join(fixtures, 'pets')), done) - }) - }) - - describe('when no "directory" listeners are present', function () { - it('should redirect directories to trailing slash', function (done) { - request(createServer({ root: fixtures })) - .get('/pets') - .expect('Location', '/pets/') - .expect(301, done) - }) - - it('should respond with an HTML redirect', function (done) { - request(createServer({ root: fixtures })) - .get('/pets') - .expect('Location', '/pets/') - .expect('Content-Type', /html/) - .expect(301, />Redirecting to
\/pets\/<\/a>Redirecting to \/snow%20%E2%98%83\/<\/a>Not Foundtobi

', done) - }) - - it('should support disabling', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .index(false) - .pipe(res) - }) - - request(app) - .get('/pets/') - .expect(403, done) - }) - - it('should support fallbacks', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .index(['default.htm', 'index.html']) - .pipe(res) - }) - - request(app) - .get('/pets/') - .expect(200, fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'), done) - }) - }) - - describe('.maxage()', function () { - it('should default to 0', function (done) { - var app = http.createServer(function (req, res) { - send(req, 'test/fixtures/name.txt') - .maxage(undefined) - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=0', done) - }) - - it('should floor to integer', function (done) { - var app = http.createServer(function (req, res) { - send(req, 'test/fixtures/name.txt') - .maxage(1234) - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=1', done) - }) - - it('should accept string', function (done) { - var app = http.createServer(function (req, res) { - send(req, 'test/fixtures/name.txt') - .maxage('30d') - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=2592000', done) - }) - - it('should max at 1 year', function (done) { - var app = http.createServer(function (req, res) { - send(req, 'test/fixtures/name.txt') - .maxage(Infinity) - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=31536000', done) - }) - }) - - describe('.root()', function () { - it('should set root', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url) - .root(fixtures) - .pipe(res) - }) - - request(app) - .get('/pets/../name.txt') - .expect(200, 'tobi', done) - }) - }) -}) - -describe('send(file, options)', function () { - describe('acceptRanges', function () { - it('should support disabling accept-ranges', function (done) { - request(createServer({ acceptRanges: false, root: fixtures })) - .get('/nums.txt') - .expect(shouldNotHaveHeader('Accept-Ranges')) - .expect(200, done) - }) - - it('should ignore requested range', function (done) { - request(createServer({ acceptRanges: false, root: fixtures })) - .get('/nums.txt') - .set('Range', 'bytes=0-2') - .expect(shouldNotHaveHeader('Accept-Ranges')) - .expect(shouldNotHaveHeader('Content-Range')) - .expect(200, '123456789', done) - }) - }) - - describe('cacheControl', function () { - it('should support disabling cache-control', function (done) { - request(createServer({ cacheControl: false, root: fixtures })) - .get('/name.txt') - .expect(shouldNotHaveHeader('Cache-Control')) - .expect(200, done) - }) - - it('should ignore maxAge option', function (done) { - request(createServer({ cacheControl: false, maxAge: 1000, root: fixtures })) - .get('/name.txt') - .expect(shouldNotHaveHeader('Cache-Control')) - .expect(200, done) - }) - }) - - describe('etag', function () { - it('should support disabling etags', function (done) { - request(createServer({ etag: false, root: fixtures })) - .get('/name.txt') - .expect(shouldNotHaveHeader('ETag')) - .expect(200, done) - }) - }) - - describe('extensions', function () { - it('should reject numbers', function (done) { - request(createServer({ extensions: 42, root: fixtures })) - .get('/pets/') - .expect(500, /TypeError: extensions option/, done) - }) - - it('should reject true', function (done) { - request(createServer({ extensions: true, root: fixtures })) - .get('/pets/') - .expect(500, /TypeError: extensions option/, done) - }) - - it('should be not be enabled by default', function (done) { - request(createServer({ root: fixtures })) - .get('/tobi') - .expect(404, done) - }) - - it('should be configurable', function (done) { - request(createServer({ extensions: 'txt', root: fixtures })) - .get('/name') - .expect(200, 'tobi', done) - }) - - it('should support disabling extensions', function (done) { - request(createServer({ extensions: false, root: fixtures })) - .get('/name') - .expect(404, done) - }) - - it('should support fallbacks', function (done) { - request(createServer({ extensions: ['htm', 'html', 'txt'], root: fixtures })) - .get('/name') - .expect(200, '

tobi

', done) - }) - - it('should 404 if nothing found', function (done) { - request(createServer({ extensions: ['htm', 'html', 'txt'], root: fixtures })) - .get('/bob') - .expect(404, done) - }) - - it('should skip directories', function (done) { - request(createServer({ extensions: ['file', 'dir'], root: fixtures })) - .get('/name') - .expect(404, done) - }) - - it('should not search if file has extension', function (done) { - request(createServer({ extensions: 'html', root: fixtures })) - .get('/thing.html') - .expect(404, done) - }) - }) - - describe('lastModified', function () { - it('should support disabling last-modified', function (done) { - request(createServer({ lastModified: false, root: fixtures })) - .get('/name.txt') - .expect(shouldNotHaveHeader('Last-Modified')) - .expect(200, done) - }) - }) - - describe('from', function () { - it('should set with deprecated from', function (done) { - request(createServer({ from: fixtures })) - .get('/pets/../name.txt') - .expect(200, 'tobi', done) - }) - }) - - describe('dotfiles', function () { - it('should default to "ignore"', function (done) { - request(createServer({ root: fixtures })) - .get('/.hidden.txt') - .expect(404, done) - }) - - it('should allow file within dotfile directory for back-compat', function (done) { - request(createServer({ root: fixtures })) - .get('/.mine/name.txt') - .expect(200, /tobi/, done) - }) - - it('should reject bad value', function (done) { - request(createServer({ dotfiles: 'bogus' })) - .get('/name.txt') - .expect(500, /dotfiles/, done) - }) - - describe('when "allow"', function (done) { - it('should send dotfile', function (done) { - request(createServer({ dotfiles: 'allow', root: fixtures })) - .get('/.hidden.txt') - .expect(200, 'secret', done) - }) - - it('should send within dotfile directory', function (done) { - request(createServer({ dotfiles: 'allow', root: fixtures })) - .get('/.mine/name.txt') - .expect(200, /tobi/, done) - }) - - it('should 404 for non-existent dotfile', function (done) { - request(createServer({ dotfiles: 'allow', root: fixtures })) - .get('/.nothere') - .expect(404, done) - }) - }) - - describe('when "deny"', function (done) { - it('should 403 for dotfile', function (done) { - request(createServer({ dotfiles: 'deny', root: fixtures })) - .get('/.hidden.txt') - .expect(403, done) - }) - - it('should 403 for dotfile directory', function (done) { - request(createServer({ dotfiles: 'deny', root: fixtures })) - .get('/.mine') - .expect(403, done) - }) - - it('should 403 for dotfile directory with trailing slash', function (done) { - request(createServer({ dotfiles: 'deny', root: fixtures })) - .get('/.mine/') - .expect(403, done) - }) - - it('should 403 for file within dotfile directory', function (done) { - request(createServer({ dotfiles: 'deny', root: fixtures })) - .get('/.mine/name.txt') - .expect(403, done) - }) - - it('should 403 for non-existent dotfile', function (done) { - request(createServer({ dotfiles: 'deny', root: fixtures })) - .get('/.nothere') - .expect(403, done) - }) - - it('should 403 for non-existent dotfile directory', function (done) { - request(createServer({ dotfiles: 'deny', root: fixtures })) - .get('/.what/name.txt') - .expect(403, done) - }) - - it('should 403 for dotfile in directory', function (done) { - request(createServer({ dotfiles: 'deny', root: fixtures })) - .get('/pets/.hidden') - .expect(403, done) - }) - - it('should 403 for dotfile in dotfile directory', function (done) { - request(createServer({ dotfiles: 'deny', root: fixtures })) - .get('/.mine/.hidden') - .expect(403, done) - }) - - it('should send files in root dotfile directory', function (done) { - request(createServer({ dotfiles: 'deny', root: path.join(fixtures, '.mine') })) - .get('/name.txt') - .expect(200, /tobi/, done) - }) - - it('should 403 for dotfile without root', function (done) { - var server = http.createServer(function onRequest (req, res) { - send(req, fixtures + '/.mine' + req.url, { dotfiles: 'deny' }).pipe(res) - }) - - request(server) - .get('/name.txt') - .expect(403, done) - }) - }) - - describe('when "ignore"', function (done) { - it('should 404 for dotfile', function (done) { - request(createServer({ dotfiles: 'ignore', root: fixtures })) - .get('/.hidden.txt') - .expect(404, done) - }) - - it('should 404 for dotfile directory', function (done) { - request(createServer({ dotfiles: 'ignore', root: fixtures })) - .get('/.mine') - .expect(404, done) - }) - - it('should 404 for dotfile directory with trailing slash', function (done) { - request(createServer({ dotfiles: 'ignore', root: fixtures })) - .get('/.mine/') - .expect(404, done) - }) - - it('should 404 for file within dotfile directory', function (done) { - request(createServer({ dotfiles: 'ignore', root: fixtures })) - .get('/.mine/name.txt') - .expect(404, done) - }) - - it('should 404 for non-existent dotfile', function (done) { - request(createServer({ dotfiles: 'ignore', root: fixtures })) - .get('/.nothere') - .expect(404, done) - }) - - it('should 404 for non-existent dotfile directory', function (done) { - request(createServer({ dotfiles: 'ignore', root: fixtures })) - .get('/.what/name.txt') - .expect(404, done) - }) - - it('should send files in root dotfile directory', function (done) { - request(createServer({ dotfiles: 'ignore', root: path.join(fixtures, '.mine') })) - .get('/name.txt') - .expect(200, /tobi/, done) - }) - - it('should 404 for dotfile without root', function (done) { - var server = http.createServer(function onRequest (req, res) { - send(req, fixtures + '/.mine' + req.url, { dotfiles: 'ignore' }).pipe(res) - }) - - request(server) - .get('/name.txt') - .expect(404, done) - }) - }) - }) - - describe('hidden', function () { - it('should default to false', function (done) { - request(app) - .get('/.hidden.txt') - .expect(404, 'Not Found', done) - }) - - it('should default support sending hidden files', function (done) { - request(createServer({ hidden: true, root: fixtures })) - .get('/.hidden.txt') - .expect(200, 'secret', done) - }) - }) - - describe('immutable', function () { - it('should default to false', function (done) { - request(createServer({ root: fixtures })) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=0', done) - }) - - it('should set immutable directive in Cache-Control', function (done) { - request(createServer({ immutable: true, maxAge: '1h', root: fixtures })) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=3600, immutable', done) - }) - }) - - describe('maxAge', function () { - it('should default to 0', function (done) { - request(createServer({ root: fixtures })) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=0', done) - }) - - it('should floor to integer', function (done) { - request(createServer({ maxAge: 123956, root: fixtures })) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=123', done) - }) - - it('should accept string', function (done) { - request(createServer({ maxAge: '30d', root: fixtures })) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=2592000', done) - }) - - it('should max at 1 year', function (done) { - request(createServer({ maxAge: '2y', root: fixtures })) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=31536000', done) - }) - }) - - describe('index', function () { - it('should reject numbers', function (done) { - request(createServer({ root: fixtures, index: 42 })) - .get('/pets/') - .expect(500, /TypeError: index option/, done) - }) - - it('should reject true', function (done) { - request(createServer({ root: fixtures, index: true })) - .get('/pets/') - .expect(500, /TypeError: index option/, done) - }) - - it('should default to index.html', function (done) { - request(createServer({ root: fixtures })) - .get('/pets/') - .expect(fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'), done) - }) - - it('should be configurable', function (done) { - request(createServer({ root: fixtures, index: 'tobi.html' })) - .get('/') - .expect(200, '

tobi

', done) - }) - - it('should support disabling', function (done) { - request(createServer({ root: fixtures, index: false })) - .get('/pets/') - .expect(403, done) - }) - - it('should support fallbacks', function (done) { - request(createServer({ root: fixtures, index: ['default.htm', 'index.html'] })) - .get('/pets/') - .expect(200, fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'), done) - }) - - it('should 404 if no index file found (file)', function (done) { - request(createServer({ root: fixtures, index: 'default.htm' })) - .get('/pets/') - .expect(404, done) - }) - - it('should 404 if no index file found (dir)', function (done) { - request(createServer({ root: fixtures, index: 'pets' })) - .get('/') - .expect(404, done) - }) - - it('should not follow directories', function (done) { - request(createServer({ root: fixtures, index: ['pets', 'name.txt'] })) - .get('/') - .expect(200, 'tobi', done) - }) - - it('should work without root', function (done) { - var server = http.createServer(function (req, res) { - var p = path.join(fixtures, 'pets').replace(/\\/g, '/') + '/' - send(req, p, { index: ['index.html'] }) - .pipe(res) - }) - - request(server) - .get('/') - .expect(200, /tobi/, done) - }) - }) - - describe('root', function () { - describe('when given', function () { - it('should join root', function (done) { - request(createServer({ root: fixtures })) - .get('/pets/../name.txt') - .expect(200, 'tobi', done) - }) - - it('should work with trailing slash', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures + '/' }) - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect(200, 'tobi', done) - }) - - it('should work with empty path', function (done) { - var app = http.createServer(function (req, res) { - send(req, '', { root: fixtures }) - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect(301, /Redirecting to/, done) - }) - - // - // 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. - // - it('should try as file with empty path', function (done) { - var app = http.createServer(function (req, res) { - send(req, '', { root: path.join(fixtures, 'name.txt') }) - .pipe(res) - }) - - request(app) - .get('/') - .expect(200, 'tobi', done) - }) - - it('should restrict paths to within root', function (done) { - request(createServer({ root: fixtures })) - .get('/pets/../../send.js') - .expect(403, done) - }) - - it('should allow .. in root', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures + '/../fixtures' }) - .pipe(res) - }) - - request(app) - .get('/pets/../../send.js') - .expect(403, done) - }) - - it('should not allow root transversal', function (done) { - request(createServer({ root: path.join(fixtures, 'name.d') })) - .get('/../name.dir/name.txt') - .expect(403, done) - }) - - it('should not allow root path disclosure', function (done) { - request(createServer({ root: fixtures })) - .get('/pets/../../fixtures/name.txt') - .expect(403, done) - }) - }) - - describe('when missing', function () { - it('should consider .. malicious', function (done) { - var app = http.createServer(function (req, res) { - send(req, fixtures + req.url) - .pipe(res) - }) - - request(app) - .get('/../send.js') - .expect(403, done) - }) - - it('should still serve files with dots in name', function (done) { - var app = http.createServer(function (req, res) { - send(req, fixtures + req.url) - .pipe(res) - }) - - request(app) - .get('/do..ts.txt') - .expect(200, '...', done) - }) - }) - }) -}) - -describe('send.mime', function () { - it('should be exposed', function () { - assert.ok(send.mime) - }) - - describe('.default_type', function () { - before(function () { - this.default_type = send.mime.default_type - }) - - afterEach(function () { - send.mime.default_type = this.default_type - }) - - it('should change the default type', function (done) { - send.mime.default_type = 'text/plain' - - request(createServer({ root: fixtures })) - .get('/no_ext') - .expect('Content-Type', 'text/plain; charset=UTF-8') - .expect(200, done) - }) - - it('should not add Content-Type for undefined default', function (done) { - send.mime.default_type = undefined - - request(createServer({ root: fixtures })) - .get('/no_ext') - .expect(shouldNotHaveHeader('Content-Type')) - .expect(200, done) - }) - }) -}) - -function createServer (opts, fn) { - return http.createServer(function onRequest (req, res) { - try { - fn && fn(req, res) - send(req, req.url, opts).pipe(res) - } catch (err) { - res.statusCode = 500 - res.end(String(err)) - } - }) -} - -function shouldNotHaveBody () { - return function (res) { - assert.ok(res.text === '' || res.text === undefined) - } -} - -function shouldNotHaveHeader (header) { - return function (res) { - assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header) - } -} diff --git a/test/send.test.js b/test/send.test.js new file mode 100644 index 0000000..6ca1205 --- /dev/null +++ b/test/send.test.js @@ -0,0 +1,679 @@ +'use strict' + +process.env.NO_DEPRECATION = 'send' + +const { test } = require('tap') +const fs = require('fs') +const http = require('http') +const path = require('path') +const request = require('supertest') +const send = require('..') +const { shouldNotHaveHeader, createServer } = require('./utils') + +// test server + +const fixtures = path.join(__dirname, 'fixtures') + +test('send(file, options)', function (t) { + t.plan(12) + + 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, () => t.pass()) + }) + + 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', () => t.pass()) + }) + }) + + 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, () => t.pass()) + }) + + 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, () => t.pass()) + }) + }) + + 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, () => t.pass()) + }) + }) + + 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/, () => t.pass()) + }) + + t.test('should reject true', function (t) { + t.plan(1) + + request(createServer({ extensions: true, root: fixtures })) + .get('/pets/') + .expect(500, /TypeError: extensions option/, () => t.pass()) + }) + + t.test('should be not be enabled by default', function (t) { + t.plan(1) + + request(createServer({ root: fixtures })) + .get('/tobi') + .expect(404, () => t.pass()) + }) + + t.test('should be configurable', function (t) { + t.plan(1) + + request(createServer({ extensions: 'txt', root: fixtures })) + .get('/name') + .expect(200, 'tobi', () => t.pass()) + }) + + t.test('should support disabling extensions', function (t) { + t.plan(1) + + request(createServer({ extensions: false, root: fixtures })) + .get('/name') + .expect(404, () => t.pass()) + }) + + t.test('should support fallbacks', function (t) { + t.plan(1) + + request(createServer({ extensions: ['htm', 'html', 'txt'], root: fixtures })) + .get('/name') + .expect(200, '

tobi

', () => t.pass()) + }) + + t.test('should 404 if nothing found', function (t) { + t.plan(1) + + request(createServer({ extensions: ['htm', 'html', 'txt'], root: fixtures })) + .get('/bob') + .expect(404, () => t.pass()) + }) + + t.test('should skip directories', function (t) { + t.plan(1) + + request(createServer({ extensions: ['file', 'dir'], root: fixtures })) + .get('/name') + .expect(404, () => t.pass()) + }) + + 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, () => t.pass()) + }) + }) + + 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, () => t.pass()) + }) + }) + + t.test('from', function (t) { + t.plan(1) + + t.test('should set with deprecated from', function (t) { + t.plan(1) + + request(createServer({ from: fixtures })) + .get('/pets/../name.txt') + .expect(200, 'tobi', () => t.pass()) + }) + }) + + t.test('dotfiles', function (t) { + t.plan(6) + + t.test('should default to "ignore"', function (t) { + t.plan(1) + + request(createServer({ root: fixtures })) + .get('/.hidden.txt') + .expect(404, () => t.pass()) + }) + + t.test('should allow file within dotfile directory for back-compat', function (t) { + t.plan(1) + + request(createServer({ root: fixtures })) + .get('/.mine/name.txt') + .expect(200, /tobi/, () => t.pass()) + }) + + t.test('should reject bad value', function (t) { + t.plan(1) + + request(createServer({ dotfiles: 'bogus' })) + .get('/name.txt') + .expect(500, /dotfiles/, () => t.pass()) + }) + + t.test('when "allow"', function (t) { + t.plan(3) + + t.test('should send dotfile', function (t) { + t.plan(1) + request(createServer({ dotfiles: 'allow', root: fixtures })) + .get('/.hidden.txt') + .expect(200, 'secret', () => t.pass()) + }) + + t.test('should send within dotfile directory', function (t) { + t.plan(1) + request(createServer({ dotfiles: 'allow', root: fixtures })) + .get('/.mine/name.txt') + .expect(200, /tobi/, () => t.pass()) + }) + + t.test('should 404 for non-existent dotfile', function (t) { + t.plan(1) + request(createServer({ dotfiles: 'allow', root: fixtures })) + .get('/.nothere') + .expect(404, () => t.pass()) + }) + }) + + 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, () => t.pass()) + }) + + t.test('should 403 for dotfile directory', function (t) { + t.plan(1) + request(createServer({ dotfiles: 'deny', root: fixtures })) + .get('/.mine') + .expect(403, () => t.pass()) + }) + + 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, () => t.pass()) + }) + + 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, () => t.pass()) + }) + + t.test('should 403 for non-existent dotfile', function (t) { + t.plan(1) + request(createServer({ dotfiles: 'deny', root: fixtures })) + .get('/.nothere') + .expect(403, () => t.pass()) + }) + + 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, () => t.pass()) + }) + + t.test('should 403 for dotfile in directory', function (t) { + t.plan(1) + request(createServer({ dotfiles: 'deny', root: fixtures })) + .get('/pets/.hidden') + .expect(403, () => t.pass()) + }) + + 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, () => t.pass()) + }) + + t.test('should send 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/, () => t.pass()) + }) + + t.test('should 403 for dotfile without root', function (t) { + t.plan(1) + const server = http.createServer(function onRequest (req, res) { + send(req, fixtures + '/.mine' + req.url, { dotfiles: 'deny' }).pipe(res) + }) + + request(server) + .get('/name.txt') + .expect(403, () => t.pass()) + }) + }) + + 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, () => t.pass()) + }) + + t.test('should 404 for dotfile directory', function (t) { + t.plan(1) + + request(createServer({ dotfiles: 'ignore', root: fixtures })) + .get('/.mine') + .expect(404, () => t.pass()) + }) + + 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, () => t.pass()) + }) + + 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, () => t.pass()) + }) + + t.test('should 404 for non-existent dotfile', function (t) { + t.plan(1) + + request(createServer({ dotfiles: 'ignore', root: fixtures })) + .get('/.nothere') + .expect(404, () => t.pass()) + }) + + 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, () => t.pass()) + }) + + t.test('should send 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/, () => t.pass()) + }) + + t.test('should 404 for dotfile without root', function (t) { + t.plan(1) + + const server = http.createServer(function onRequest (req, res) { + send(req, fixtures + '/.mine' + req.url, { dotfiles: 'ignore' }).pipe(res) + }) + + request(server) + .get('/name.txt') + .expect(404, () => t.pass()) + }) + }) + }) + + t.test('hidden', function (t) { + t.plan(2) + + t.test('should default to false', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + function error (err) { + res.statusCode = err.status + res.end(http.STATUS_CODES[err.status]) + } + + send(req, req.url, { root: fixtures }) + .on('error', error) + .pipe(res) + }) + request(app) + .get('/.hidden.txt') + .expect(404, 'Not Found', () => t.pass()) + }) + + t.test('should default support sending hidden files', function (t) { + t.plan(1) + request(createServer({ hidden: true, root: fixtures })) + .get('/.hidden.txt') + .expect(200, 'secret', () => t.pass()) + }) + }) + + 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', () => t.pass()) + }) + + 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', () => t.pass()) + }) + }) + + 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', () => t.pass()) + }) + + 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', () => t.pass()) + }) + + 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', () => t.pass()) + }) + + 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', () => t.pass()) + }) + }) + + 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/, () => t.pass()) + }) + + t.test('should reject true', function (t) { + t.plan(1) + + request(createServer({ root: fixtures, index: true })) + .get('/pets/') + .expect(500, /TypeError: index option/, () => t.pass()) + }) + + 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'), () => t.pass()) + }) + + t.test('should be configurable', function (t) { + t.plan(1) + + request(createServer({ root: fixtures, index: 'tobi.html' })) + .get('/') + .expect(200, '

tobi

', () => t.pass()) + }) + + t.test('should support disabling', function (t) { + t.plan(1) + + request(createServer({ root: fixtures, index: false })) + .get('/pets/') + .expect(403, () => t.pass()) + }) + + 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'), () => t.pass()) + }) + + 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, () => t.pass()) + }) + + t.test('should 404 if no index file found (dir)', function (t) { + t.plan(1) + + request(createServer({ root: fixtures, index: 'pets' })) + .get('/') + .expect(404, () => t.pass()) + }) + + t.test('should not follow directories', function (t) { + t.plan(1) + + request(createServer({ root: fixtures, index: ['pets', 'name.txt'] })) + .get('/') + .expect(200, 'tobi', () => t.pass()) + }) + + 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, '/') + '/' + send(req, p, { index: ['index.html'] }) + .pipe(res) + }) + + request(server) + .get('/') + .expect(200, /tobi/, () => t.pass()) + }) + }) + + 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', () => t.pass()) + }) + + t.test('should work with trailing slash', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures + '/' }) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(200, 'tobi', () => t.pass()) + }) + + t.test('should work with empty path', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, '', { root: fixtures }) + .pipe(res) + }) + + request(app) + .get('/name.txt') + .expect(301, /Redirecting to/, () => t.pass()) + }) + + // + // 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) { + send(req, '', { root: path.join(fixtures, 'name.txt') }) + .pipe(res) + }) + + request(app) + .get('/') + .expect(200, 'tobi', () => t.pass()) + }) + + t.test('should restrict paths to within root', function (t) { + t.plan(1) + + request(createServer({ root: fixtures })) + .get('/pets/../../send.js') + .expect(403, () => t.pass()) + }) + + t.test('should allow .. in root', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, req.url, { root: fixtures + '/../fixtures' }) + .pipe(res) + }) + + request(app) + .get('/pets/../../send.js') + .expect(403, () => t.pass()) + }) + + 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, () => t.pass()) + }) + + t.test('should not allow root path disclosure', function (t) { + t.plan(1) + + request(createServer({ root: fixtures })) + .get('/pets/../../fixtures/name.txt') + .expect(403, () => t.pass()) + }) + }) + + 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) { + send(req, fixtures + req.url) + .pipe(res) + }) + + request(app) + .get('/../send.js') + .expect(403, () => t.pass()) + }) + + t.test('should still serve files with dots in name', function (t) { + t.plan(1) + + const app = http.createServer(function (req, res) { + send(req, fixtures + req.url) + .pipe(res) + }) + + request(app) + .get('/do..ts.txt') + .expect(200, '...', () => t.pass()) + }) + }) + }) +}) diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..923905f --- /dev/null +++ b/test/utils.js @@ -0,0 +1,28 @@ +'use strict' + +const http = require('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) + } +} + +module.exports.createServer = function createServer (opts, fn) { + return http.createServer(function onRequest (req, res) { + try { + fn && fn(req, res) + send(req, req.url, opts).pipe(res) + } catch (err) { + res.statusCode = 500 + res.end(String(err)) + } + }) +} + +module.exports.shouldNotHaveBody = function shouldNotHaveBody (t) { + return function (res) { + t.ok(res.text === '' || res.text === undefined) + } +}