diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index e3578aa..0000000 --- a/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "standard" -} diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..cf3015f --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,9 @@ +root: true +extends: + - standard + - plugin:markdown/recommended +plugins: + - markdown +overrides: + - files: '**/*.md' + processor: 'markdown/markdown' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2eaea5a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,189 @@ +name: ci + +on: +- pull_request +- push + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + name: + - Node.js 0.8 + - Node.js 0.10 + - Node.js 0.12 + - io.js 1.x + - io.js 2.x + - io.js 3.x + - Node.js 4.x + - Node.js 5.x + - Node.js 6.x + - Node.js 7.x + - Node.js 8.x + - Node.js 9.x + - Node.js 10.x + - Node.js 11.x + - Node.js 12.x + - Node.js 13.x + - Node.js 14.x + - Node.js 15.x + - Node.js 16.x + - Node.js 17.x + + include: + - name: Node.js 0.8 + node-version: "0.8" + npm-i: mocha@2.5.3 supertest@1.1.0 + npm-rm: nyc + + - name: Node.js 0.10 + node-version: "0.10" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: Node.js 0.12 + node-version: "0.12" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 1.x + node-version: "1.8" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 2.x + node-version: "2.5" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 3.x + node-version: "3.3" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: Node.js 4.x + node-version: "4.9" + npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + + - name: Node.js 5.x + node-version: "5.12" + npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + + - name: Node.js 6.x + node-version: "6.17" + npm-i: mocha@6.2.3 nyc@14.1.1 supertest@6.1.6 + + - name: Node.js 7.x + node-version: "7.10" + npm-i: mocha@6.2.3 nyc@14.1.1 supertest@6.1.6 + + - name: Node.js 8.x + node-version: "8.16" + npm-i: mocha@7.2.0 + + - name: Node.js 9.x + node-version: "9.11" + npm-i: mocha@7.2.0 + + - name: Node.js 10.x + node-version: "10.24" + npm-i: mocha@8.4.0 + + - name: Node.js 11.x + node-version: "11.15" + npm-i: mocha@8.4.0 + + - name: Node.js 12.x + node-version: "12.22" + + - name: Node.js 13.x + node-version: "13.14" + + - 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.8" + + 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 < 10 + 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 diff --git a/.gitignore b/.gitignore index 3cd27af..f15b98e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.nyc_output/ coverage/ node_modules/ npm-debug.log +package-lock.json diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 143d23c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -language: node_js -node_js: - - "0.8" - - "0.10" - - "0.12" - - "1.8" - - "2.5" - - "3.3" - - "4.7" - - "5.12" - - "6.9" - - "7.4" -sudo: false -cache: - directories: - - node_modules -before_install: - # Setup Node.js version-specific dependencies - - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev istanbul" - - "test $(echo $TRAVIS_NODE_VERSION | cut -d. -f1) -ge 4 || npm rm --save-dev eslint eslint-config-standard eslint-plugin-promise eslint-plugin-standard" - - # Update Node.js modules - - "test ! -d node_modules || npm prune" - - "test ! -d node_modules || npm rebuild" -script: - # Run test script, depending on istanbul install - - "test ! -z $(npm -ps ls istanbul) || npm test" - - "test -z $(npm -ps ls istanbul) || npm run-script test-ci" - - "test -z $(npm -ps ls eslint ) || npm run-script lint" -after_script: - - "test -e ./coverage/lcov.info && npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls" diff --git a/HISTORY.md b/HISTORY.md index b8201f3..fde1d72 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,140 @@ +1.16.0 / 2024-09-10 +=================== + +* Remove link renderization in html while redirecting + + +1.15.0 / 2022-03-24 +=================== + + * deps: send@0.18.0 + - Fix emitted 416 error missing headers property + - Limit the headers removed for 304 response + - deps: depd@2.0.0 + - deps: destroy@1.2.0 + - deps: http-errors@2.0.0 + - deps: on-finished@2.4.1 + - deps: statuses@2.0.1 + +1.14.2 / 2021-12-15 +=================== + + * deps: send@0.17.2 + - deps: http-errors@1.8.1 + - deps: ms@2.1.3 + - pref: ignore empty http tokens + +1.14.1 / 2019-05-10 +=================== + + * Set stricter CSP header in redirect response + * deps: send@0.17.1 + - deps: range-parser@~1.2.1 + +1.14.0 / 2019-05-07 +=================== + + * deps: parseurl@~1.3.3 + * deps: send@0.17.0 + - deps: http-errors@~1.7.2 + - deps: mime@1.6.0 + - deps: ms@2.1.1 + - deps: statuses@~1.5.0 + - perf: remove redundant `path.normalize` call + +1.13.2 / 2018-02-07 +=================== + + * Fix incorrect end tag in redirects + * deps: encodeurl@~1.0.2 + - Fix encoding `%` as last character + * deps: send@0.16.2 + - deps: depd@~1.1.2 + - deps: encodeurl@~1.0.2 + - deps: statuses@~1.4.0 + +1.13.1 / 2017-09-29 +=================== + + * Fix regression when `root` is incorrectly set to a file + * deps: send@0.16.1 + +1.13.0 / 2017-09-27 +=================== + + * deps: send@0.16.0 + - Add 70 new types for file extensions + - Add `immutable` option + - Fix missing `` in default error & redirects + - Set charset as "UTF-8" for .js and .json + - Use instance methods on steam to check for listeners + - deps: mime@1.4.1 + - perf: improve path validation speed + +1.12.6 / 2017-09-22 +=================== + + * deps: send@0.15.6 + - deps: debug@2.6.9 + - perf: improve `If-Match` token parsing + * perf: improve slash collapsing + +1.12.5 / 2017-09-21 +=================== + + * deps: parseurl@~1.3.2 + - perf: reduce overhead for full URLs + - perf: unroll the "fast-path" `RegExp` + * deps: send@0.15.5 + - Fix handling of modified headers with invalid dates + - deps: etag@~1.8.1 + - deps: fresh@0.5.2 + +1.12.4 / 2017-08-05 +=================== + + * deps: send@0.15.4 + - deps: debug@2.6.8 + - deps: depd@~1.1.1 + - deps: http-errors@~1.6.2 + +1.12.3 / 2017-05-16 +=================== + + * deps: send@0.15.3 + - deps: debug@2.6.7 + +1.12.2 / 2017-04-26 +=================== + + * deps: send@0.15.2 + - deps: debug@2.6.4 + +1.12.1 / 2017-03-04 +=================== + + * deps: send@0.15.1 + - Fix issue when `Date.parse` does not return `NaN` on invalid date + - Fix strict violation in broken environments + +1.12.0 / 2017-02-25 +=================== + + * Send complete HTML document in redirect response + * Set default CSP header in redirect response + * deps: send@0.15.0 + - Fix false detection of `no-cache` request directive + - Fix incorrect result when `If-None-Match` has both `*` and ETags + - Fix weak `ETag` matching to match spec + - Remove usage of `res._headers` private field + - Support `If-Match` and `If-Unmodified-Since` headers + - Use `res.getHeaderNames()` when available + - Use `res.headersSent` when available + - deps: debug@2.6.1 + - deps: etag@~1.8.0 + - deps: fresh@0.5.0 + - deps: http-errors@~1.6.1 + 1.11.2 / 2017-01-23 =================== diff --git a/README.md b/README.md index 6fef5d9..262d944 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # serve-static -[![NPM Version][npm-image]][npm-url] -[![NPM Downloads][downloads-image]][downloads-url] -[![Linux Build][travis-image]][travis-url] +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Linux Build][github-actions-ci-image]][github-actions-ci-url] [![Windows Build][appveyor-image]][appveyor-url] [![Test Coverage][coveralls-image]][coveralls-url] -[![Gratipay][gratipay-image]][gratipay-url] ## Install @@ -42,7 +41,7 @@ of the `Range` request header. ##### cacheControl Enable or disable setting `Cache-Control` response header, defaults to -true. Disabling this will ignore the `maxAge` option. +true. Disabling this will ignore the `immutable` and `maxAge` options. ##### dotfiles @@ -91,6 +90,14 @@ all methods. The default value is `true`. +##### immutable + +Enable or disable the `immutable` directive in the `Cache-Control` response +header, defaults to `false`. If set to `true`, the `maxAge` option should +also be specified to enable caching. The `immutable` directive will prevent +supported clients from making conditional requests during the life of the +`maxAge` option to check if the file has changed. + ##### index By default this module will send "index.html" files in response to a request @@ -132,7 +139,7 @@ var http = require('http') var serveStatic = require('serve-static') // Serve up public/ftp folder -var serve = serveStatic('public/ftp', {'index': ['index.html', 'index.htm']}) +var serve = serveStatic('public/ftp', { index: ['index.html', 'index.htm'] }) // Create server var server = http.createServer(function onRequest (req, res) { @@ -153,12 +160,12 @@ var serveStatic = require('serve-static') // Serve up public/ftp folder var serve = serveStatic('public/ftp', { - 'index': false, - 'setHeaders': setHeaders + index: false, + setHeaders: setHeaders }) // Set header to force download -function setHeaders(res, path) { +function setHeaders (res, path) { res.setHeader('Content-Disposition', contentDisposition(path)) } @@ -183,24 +190,25 @@ var serveStatic = require('serve-static') var app = express() -app.use(serveStatic('public/ftp', {'index': ['default.html', 'default.htm']})) +app.use(serveStatic('public/ftp', { index: ['default.html', 'default.htm'] })) app.listen(3000) ``` #### Multiple roots This example shows a simple way to search through multiple directories. -Files are look for in `public-optimized/` first, then `public/` second as -a fallback. +Files are searched for in `public-optimized/` first, then `public/` second +as a fallback. ```js var express = require('express') +var path = require('path') var serveStatic = require('serve-static') var app = express() -app.use(serveStatic(__dirname + '/public-optimized')) -app.use(serveStatic(__dirname + '/public')) +app.use(serveStatic(path.join(__dirname, 'public-optimized'))) +app.use(serveStatic(path.join(__dirname, 'public'))) app.listen(3000) ``` @@ -212,11 +220,12 @@ is for 1 day. ```js var express = require('express') +var path = require('path') var serveStatic = require('serve-static') var app = express() -app.use(serveStatic(__dirname + '/public', { +app.use(serveStatic(path.join(__dirname, 'public'), { maxAge: '1d', setHeaders: setCustomCacheControl })) @@ -235,15 +244,14 @@ function setCustomCacheControl (res, path) { [MIT](LICENSE) -[npm-image]: https://img.shields.io/npm/v/serve-static.svg -[npm-url]: https://npmjs.org/package/serve-static -[travis-image]: https://img.shields.io/travis/expressjs/serve-static/master.svg?label=linux -[travis-url]: https://travis-ci.org/expressjs/serve-static -[appveyor-image]: https://img.shields.io/appveyor/ci/dougwilson/serve-static/master.svg?label=windows +[appveyor-image]: https://badgen.net/appveyor/ci/dougwilson/serve-static/master?label=windows [appveyor-url]: https://ci.appveyor.com/project/dougwilson/serve-static -[coveralls-image]: https://img.shields.io/coveralls/expressjs/serve-static/master.svg -[coveralls-url]: https://coveralls.io/r/expressjs/serve-static -[downloads-image]: https://img.shields.io/npm/dm/serve-static.svg -[downloads-url]: https://npmjs.org/package/serve-static -[gratipay-image]: https://img.shields.io/gratipay/dougwilson.svg -[gratipay-url]: https://gratipay.com/dougwilson/ +[coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/serve-static/master +[coveralls-url]: https://coveralls.io/r/expressjs/serve-static?branch=master +[github-actions-ci-image]: https://badgen.net/github/checks/expressjs/serve-static/master?label=linux +[github-actions-ci-url]: https://github.com/expressjs/serve-static/actions/workflows/ci.yml +[node-image]: https://badgen.net/npm/node/serve-static +[node-url]: https://nodejs.org/en/download/ +[npm-downloads-image]: https://badgen.net/npm/dm/serve-static +[npm-url]: https://npmjs.org/package/serve-static +[npm-version-image]: https://badgen.net/npm/v/serve-static diff --git a/appveyor.yml b/appveyor.yml index 107453c..d1d6862 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,29 +1,95 @@ environment: matrix: - - nodejs_version: "0.8" - nodejs_version: "0.10" - nodejs_version: "0.12" - nodejs_version: "1.8" - nodejs_version: "2.5" - nodejs_version: "3.3" - - nodejs_version: "4.7" + - nodejs_version: "4.9" - nodejs_version: "5.12" - - nodejs_version: "6.9" - - nodejs_version: "7.4" + - 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.8" cache: - node_modules install: - - ps: Install-Product node $env:nodejs_version - - if "%nodejs_version%" equ "0.8" npm rm --save-dev istanbul - - npm rm --save-dev eslint eslint-config-standard eslint-plugin-promise eslint-plugin-standard - - if exist node_modules npm prune - - if exist node_modules npm rebuild + # 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 2.x for Node.js < 0.10 + # - 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] -eq 0 -and [int]$env:nodejs_version.split(".")[1] -lt 10) { + npm install --silent --save-dev mocha@2.5.3 + } elseif ([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 1.1.0 for Node.js < 0.10 + # - 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] -eq 0 -and [int]$env:nodejs_version.split(".")[1] -lt 10) { + npm install --silent --save-dev supertest@1.1.0 + } elseif ([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: - - node --version - - npm --version - - set npm_test_command=test - - for /f %%l in ('npm -ps ls istanbul') do set npm_test_command=test-ci - - npm run %npm_test_command% + # Output version data + - ps: | + node --version + npm --version + # Run test script + - npm test version: "{build}" diff --git a/index.js b/index.js index 83c5e4f..3f3e64e 100644 --- a/index.js +++ b/index.js @@ -132,7 +132,7 @@ function serveStatic (root, options) { */ function collapseLeadingSlashes (str) { for (var i = 0; i < str.length; i++) { - if (str[i] !== '/') { + if (str.charCodeAt(i) !== 0x2f /* / */) { break } } @@ -142,6 +142,27 @@ function collapseLeadingSlashes (str) { : str } +/** + * Create a minimal HTML document. + * + * @param {string} title + * @param {string} body + * @private + */ + +function createHtmlDocument (title, body) { + return '\n' + + '\n' + + '\n' + + '\n' + + 'Codestin Search App\n' + + '\n' + + '\n' + + '
' + body + '
\n' + + '\n' + + '\n' +} + /** * Create a directory listener that just 404s. * @private @@ -159,7 +180,7 @@ function createNotFoundDirectoryListener () { */ function createRedirectDirectoryListener () { - return function redirect () { + return function redirect (res) { if (this.hasTrailingSlash()) { this.error(404) return @@ -174,15 +195,15 @@ function createRedirectDirectoryListener () { // reformat the URL var loc = encodeUrl(url.format(originalUrl)) - var msg = 'Redirecting to ' + escapeHtml(loc) + '\n' - var res = this.res + var doc = createHtmlDocument('Redirecting', 'Redirecting to ' + escapeHtml(loc)) // send redirect response res.statusCode = 301 res.setHeader('Content-Type', 'text/html; charset=UTF-8') - res.setHeader('Content-Length', Buffer.byteLength(msg)) + res.setHeader('Content-Length', Buffer.byteLength(doc)) + res.setHeader('Content-Security-Policy', "default-src 'none'") res.setHeader('X-Content-Type-Options', 'nosniff') res.setHeader('Location', loc) - res.end(msg) + res.end(doc) } } diff --git a/package.json b/package.json index a790067..47d9789 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,28 @@ { "name": "serve-static", "description": "Serve static files", - "version": "1.11.2", + "version": "1.16.0", "author": "Douglas Christopher Wilson ", "license": "MIT", "repository": "expressjs/serve-static", "dependencies": { - "encodeurl": "~1.0.1", + "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "parseurl": "~1.3.1", - "send": "0.14.2" + "parseurl": "~1.3.3", + "send": "0.18.0" }, "devDependencies": { - "eslint": "3.14.0", - "eslint-config-standard": "6.2.1", - "eslint-plugin-promise": "3.4.0", - "eslint-plugin-standard": "2.0.1", - "istanbul": "0.4.5", - "mocha": "2.5.3", - "supertest": "1.1.0" + "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", + "safe-buffer": "5.2.1", + "supertest": "6.2.2" }, "files": [ "LICENSE", @@ -31,7 +35,8 @@ "scripts": { "lint": "eslint .", "test": "mocha --reporter spec --bail --check-leaks test/", - "test-ci": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/", - "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/" + "test-ci": "nyc --reporter=lcov --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test", + "version": "node scripts/version-history.js && git add HISTORY.md" } } diff --git a/scripts/version-history.js b/scripts/version-history.js new file mode 100644 index 0000000..b8a2b0e --- /dev/null +++ b/scripts/version-history.js @@ -0,0 +1,63 @@ +'use strict' + +var fs = require('fs') +var path = require('path') + +var HISTORY_FILE_PATH = path.join(__dirname, '..', 'HISTORY.md') +var MD_HEADER_REGEXP = /^====*$/ +var VERSION = process.env.npm_package_version +var VERSION_PLACEHOLDER_REGEXP = /^(?:unreleased|(\d+\.)+x)$/ + +var historyFileLines = fs.readFileSync(HISTORY_FILE_PATH, 'utf-8').split('\n') + +if (!MD_HEADER_REGEXP.test(historyFileLines[1])) { + console.error('Missing header in HISTORY.md') + process.exit(1) +} + +if (!VERSION_PLACEHOLDER_REGEXP.test(historyFileLines[0])) { + console.error('Missing placegolder version in HISTORY.md') + process.exit(1) +} + +if (historyFileLines[0].indexOf('x') !== -1) { + var versionCheckRegExp = new RegExp('^' + historyFileLines[0].replace('x', '.+') + '$') + + if (!versionCheckRegExp.test(VERSION)) { + console.error('Version %s does not match placeholder %s', VERSION, historyFileLines[0]) + process.exit(1) + } +} + +historyFileLines[0] = VERSION + ' / ' + getLocaleDate() +historyFileLines[1] = repeat('=', historyFileLines[0].length) + +fs.writeFileSync(HISTORY_FILE_PATH, historyFileLines.join('\n')) + +function getLocaleDate () { + var now = new Date() + + return zeroPad(now.getFullYear(), 4) + '-' + + zeroPad(now.getMonth() + 1, 2) + '-' + + zeroPad(now.getDate(), 2) +} + +function repeat (str, length) { + var out = '' + + for (var i = 0; i < length; i++) { + out += str + } + + return out +} + +function zeroPad (number, length) { + var num = number.toString() + + while (num.length < length) { + num = '0' + num + } + + return num +} diff --git a/test/fixtures/nums b/test/fixtures/nums.txt similarity index 100% rename from test/fixtures/nums rename to test/fixtures/nums.txt diff --git a/test/test.js b/test/test.js index 9fc327a..7bce038 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,6 @@ var assert = require('assert') +var Buffer = require('safe-buffer').Buffer var http = require('http') var path = require('path') var request = require('supertest') @@ -27,97 +28,108 @@ describe('serveStatic()', function () { it('should serve static files', function (done) { request(server) - .get('/todo.txt') - .expect(200, '- groceries', done) + .get('/todo.txt') + .expect(200, '- groceries', done) }) it('should support nesting', function (done) { request(server) - .get('/users/tobi.txt') - .expect(200, 'ferret', done) + .get('/users/tobi.txt') + .expect(200, 'ferret', done) }) it('should set Content-Type', function (done) { request(server) - .get('/todo.txt') - .expect('Content-Type', 'text/plain; charset=UTF-8') - .expect(200, done) + .get('/todo.txt') + .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect(200, done) }) it('should set Last-Modified', function (done) { request(server) - .get('/todo.txt') - .expect('Last-Modified', /\d{2} \w{3} \d{4}/) - .expect(200, done) + .get('/todo.txt') + .expect('Last-Modified', /\d{2} \w{3} \d{4}/) + .expect(200, done) }) it('should default max-age=0', function (done) { request(server) - .get('/todo.txt') - .expect('Cache-Control', 'public, max-age=0') - .expect(200, done) + .get('/todo.txt') + .expect('Cache-Control', 'public, max-age=0') + .expect(200, done) }) it('should support urlencoded pathnames', function (done) { request(server) - .get('/foo%20bar') - .expect(200, 'baz', done) + .get('/foo%20bar') + .expect(200) + .expect(shouldHaveBody(Buffer.from('baz'))) + .end(done) }) it('should not choke on auth-looking URL', function (done) { request(server) - .get('//todo@txt') - .expect(404, done) + .get('//todo@txt') + .expect(404, done) }) it('should support index.html', function (done) { request(server) - .get('/users/') - .expect(200) - .expect('Content-Type', /html/) - .expect('

tobi, loki, jane

', done) + .get('/users/') + .expect(200) + .expect('Content-Type', /html/) + .expect('

tobi, loki, jane

', done) }) it('should support ../', function (done) { request(server) - .get('/users/../todo.txt') - .expect(200, '- groceries', done) + .get('/users/../todo.txt') + .expect(200, '- groceries', done) }) it('should support HEAD', function (done) { request(server) - .head('/todo.txt') - .expect(200, '', done) + .head('/todo.txt') + .expect(200) + .expect(shouldNotHaveBody()) + .end(done) }) it('should skip POST requests', function (done) { request(server) - .post('/todo.txt') - .expect(404, 'sorry!', done) + .post('/todo.txt') + .expect(404, 'sorry!', done) }) it('should support conditional requests', function (done) { request(server) - .get('/todo.txt') - .end(function (err, res) { - if (err) throw err - request(server) .get('/todo.txt') - .set('If-None-Match', res.headers.etag) - .expect(304, done) - }) + .end(function (err, res) { + if (err) throw err + request(server) + .get('/todo.txt') + .set('If-None-Match', res.headers.etag) + .expect(304, done) + }) + }) + + it('should support precondition checks', function (done) { + request(server) + .get('/todo.txt') + .set('If-Match', '"foo"') + .expect(412, done) }) it('should serve zero-length files', function (done) { request(server) - .get('/empty.txt') - .expect(200, '', done) + .get('/empty.txt') + .expect(200, '', done) }) it('should ignore hidden files', function (done) { request(server) - .get('/.hidden') - .expect(404, done) + .get('/.hidden') + .expect(404, done) }) }); @@ -130,45 +142,45 @@ describe('serveStatic()', function () { it('should be served with "."', function (done) { var dest = relative.split(path.sep).join('/') request(server) - .get('/' + dest + '/todo.txt') - .expect(200, '- groceries', done) + .get('/' + dest + '/todo.txt') + .expect(200, '- groceries', done) }) }) describe('acceptRanges', function () { describe('when false', function () { it('should not include Accept-Ranges', function (done) { - request(createServer(fixtures, {'acceptRanges': false})) - .get('/nums') - .expect(shouldNotHaveHeader('Accept-Ranges')) - .expect(200, '123456789', done) + request(createServer(fixtures, { acceptRanges: false })) + .get('/nums.txt') + .expect(shouldNotHaveHeader('Accept-Ranges')) + .expect(200, '123456789', done) }) it('should ignore Rage request header', function (done) { - request(createServer(fixtures, {'acceptRanges': false})) - .get('/nums') - .set('Range', 'bytes=0-3') - .expect(shouldNotHaveHeader('Accept-Ranges')) - .expect(shouldNotHaveHeader('Content-Range')) - .expect(200, '123456789', done) + request(createServer(fixtures, { acceptRanges: false })) + .get('/nums.txt') + .set('Range', 'bytes=0-3') + .expect(shouldNotHaveHeader('Accept-Ranges')) + .expect(shouldNotHaveHeader('Content-Range')) + .expect(200, '123456789', done) }) }) describe('when true', function () { it('should include Accept-Ranges', function (done) { - request(createServer(fixtures, {'acceptRanges': true})) - .get('/nums') - .expect('Accept-Ranges', 'bytes') - .expect(200, '123456789', done) + request(createServer(fixtures, { acceptRanges: true })) + .get('/nums.txt') + .expect('Accept-Ranges', 'bytes') + .expect(200, '123456789', done) }) it('should obey Rage request header', function (done) { - request(createServer(fixtures, {'acceptRanges': true})) - .get('/nums') - .set('Range', 'bytes=0-3') - .expect('Accept-Ranges', 'bytes') - .expect('Content-Range', 'bytes 0-3/9') - .expect(206, '1234', done) + request(createServer(fixtures, { acceptRanges: true })) + .get('/nums.txt') + .set('Range', 'bytes=0-3') + .expect('Accept-Ranges', 'bytes') + .expect('Content-Range', 'bytes 0-3/9') + .expect(206, '1234', done) }) }) }) @@ -176,26 +188,26 @@ describe('serveStatic()', function () { describe('cacheControl', function () { describe('when false', function () { it('should not include Cache-Control', function (done) { - request(createServer(fixtures, {'cacheControl': false})) - .get('/nums') - .expect(shouldNotHaveHeader('Cache-Control')) - .expect(200, '123456789', done) + request(createServer(fixtures, { cacheControl: false })) + .get('/nums.txt') + .expect(shouldNotHaveHeader('Cache-Control')) + .expect(200, '123456789', done) }) it('should ignore maxAge', function (done) { - request(createServer(fixtures, {'cacheControl': false, 'maxAge': 12000})) - .get('/nums') - .expect(shouldNotHaveHeader('Cache-Control')) - .expect(200, '123456789', done) + request(createServer(fixtures, { cacheControl: false, maxAge: 12000 })) + .get('/nums.txt') + .expect(shouldNotHaveHeader('Cache-Control')) + .expect(200, '123456789', done) }) }) describe('when true', function () { it('should include Cache-Control', function (done) { - request(createServer(fixtures, {'cacheControl': true})) - .get('/nums') - .expect('Cache-Control', 'public, max-age=0') - .expect(200, '123456789', done) + request(createServer(fixtures, { cacheControl: true })) + .get('/nums.txt') + .expect('Cache-Control', 'public, max-age=0') + .expect(200, '123456789', done) }) }) }) @@ -205,179 +217,183 @@ describe('serveStatic()', function () { var server = createServer(fixtures) request(server) - .get('/todo') - .expect(404, done) + .get('/todo') + .expect(404, done) }) it('should be configurable', function (done) { - var server = createServer(fixtures, {'extensions': 'txt'}) + var server = createServer(fixtures, { extensions: 'txt' }) request(server) - .get('/todo') - .expect(200, '- groceries', done) + .get('/todo') + .expect(200, '- groceries', done) }) it('should support disabling extensions', function (done) { - var server = createServer(fixtures, {'extensions': false}) + var server = createServer(fixtures, { extensions: false }) request(server) - .get('/todo') - .expect(404, done) + .get('/todo') + .expect(404, done) }) it('should support fallbacks', function (done) { - var server = createServer(fixtures, {'extensions': ['htm', 'html', 'txt']}) + var server = createServer(fixtures, { extensions: ['htm', 'html', 'txt'] }) request(server) - .get('/todo') - .expect(200, '
  • groceries
  • ', done) + .get('/todo') + .expect(200, '
  • groceries
  • ', done) }) it('should 404 if nothing found', function (done) { - var server = createServer(fixtures, {'extensions': ['htm', 'html', 'txt']}) + var server = createServer(fixtures, { extensions: ['htm', 'html', 'txt'] }) request(server) - .get('/bob') - .expect(404, done) + .get('/bob') + .expect(404, done) }) }) describe('fallthrough', function () { it('should default to true', function (done) { request(createServer()) - .get('/does-not-exist') - .expect(404, 'sorry!', done) + .get('/does-not-exist') + .expect(404, 'sorry!', done) }) describe('when true', function () { before(function () { - this.server = createServer(fixtures, {'fallthrough': true}) + this.server = createServer(fixtures, { fallthrough: true }) }) it('should fall-through when OPTIONS request', function (done) { request(this.server) - .options('/todo.txt') - .expect(404, 'sorry!', done) + .options('/todo.txt') + .expect(404, 'sorry!', done) }) it('should fall-through when URL malformed', function (done) { request(this.server) - .get('/%') - .expect(404, 'sorry!', done) + .get('/%') + .expect(404, 'sorry!', done) }) it('should fall-through when traversing past root', function (done) { request(this.server) - .get('/users/../../todo.txt') - .expect(404, 'sorry!', done) + .get('/users/../../todo.txt') + .expect(404, 'sorry!', done) }) it('should fall-through when URL too long', function (done) { - request(this.server) - .get('/' + Array(8192).join('foobar')) - .expect(404, 'sorry!', done) + var root = fixtures + Array(10000).join('/foobar') + + request(createServer(root, { fallthrough: true })) + .get('/') + .expect(404, 'sorry!', done) }) describe('with redirect: true', function () { before(function () { - this.server = createServer(fixtures, {'fallthrough': true, 'redirect': true}) + this.server = createServer(fixtures, { fallthrough: true, redirect: true }) }) it('should fall-through when directory', function (done) { request(this.server) - .get('/pets/') - .expect(404, 'sorry!', done) + .get('/pets/') + .expect(404, 'sorry!', done) }) it('should redirect when directory without slash', function (done) { request(this.server) - .get('/pets') - .expect(301, /Redirecting/, done) + .get('/pets') + .expect(301, /Redirecting/, done) }) }) describe('with redirect: false', function () { before(function () { - this.server = createServer(fixtures, {'fallthrough': true, 'redirect': false}) + this.server = createServer(fixtures, { fallthrough: true, redirect: false }) }) it('should fall-through when directory', function (done) { request(this.server) - .get('/pets/') - .expect(404, 'sorry!', done) + .get('/pets/') + .expect(404, 'sorry!', done) }) it('should fall-through when directory without slash', function (done) { request(this.server) - .get('/pets') - .expect(404, 'sorry!', done) + .get('/pets') + .expect(404, 'sorry!', done) }) }) }) describe('when false', function () { before(function () { - this.server = createServer(fixtures, {'fallthrough': false}) + this.server = createServer(fixtures, { fallthrough: false }) }) it('should 405 when OPTIONS request', function (done) { request(this.server) - .options('/todo.txt') - .expect('Allow', 'GET, HEAD') - .expect(405, done) + .options('/todo.txt') + .expect('Allow', 'GET, HEAD') + .expect(405, done) }) it('should 400 when URL malformed', function (done) { request(this.server) - .get('/%') - .expect(400, /BadRequestError/, done) + .get('/%') + .expect(400, /BadRequestError/, done) }) it('should 403 when traversing past root', function (done) { request(this.server) - .get('/users/../../todo.txt') - .expect(403, /ForbiddenError/, done) + .get('/users/../../todo.txt') + .expect(403, /ForbiddenError/, done) }) it('should 404 when URL too long', function (done) { - request(this.server) - .get('/' + Array(8192).join('foobar')) - .expect(404, /ENAMETOOLONG/, done) + var root = fixtures + Array(10000).join('/foobar') + + request(createServer(root, { fallthrough: false })) + .get('/') + .expect(404, /ENAMETOOLONG/, done) }) describe('with redirect: true', function () { before(function () { - this.server = createServer(fixtures, {'fallthrough': false, 'redirect': true}) + this.server = createServer(fixtures, { fallthrough: false, redirect: true }) }) it('should 404 when directory', function (done) { request(this.server) - .get('/pets/') - .expect(404, /NotFoundError|ENOENT/, done) + .get('/pets/') + .expect(404, /NotFoundError|ENOENT/, done) }) it('should redirect when directory without slash', function (done) { request(this.server) - .get('/pets') - .expect(301, /Redirecting/, done) + .get('/pets') + .expect(301, /Redirecting/, done) }) }) describe('with redirect: false', function () { before(function () { - this.server = createServer(fixtures, {'fallthrough': false, 'redirect': false}) + this.server = createServer(fixtures, { fallthrough: false, redirect: false }) }) it('should 404 when directory', function (done) { request(this.server) - .get('/pets/') - .expect(404, /NotFoundError|ENOENT/, done) + .get('/pets/') + .expect(404, /NotFoundError|ENOENT/, done) }) it('should 404 when directory without slash', function (done) { request(this.server) - .get('/pets') - .expect(404, /NotFoundError|ENOENT/, done) + .get('/pets') + .expect(404, /NotFoundError|ENOENT/, done) }) }) }) @@ -386,49 +402,65 @@ describe('serveStatic()', function () { describe('hidden files', function () { var server before(function () { - server = createServer(fixtures, {'dotfiles': 'allow'}) + server = createServer(fixtures, { dotfiles: 'allow' }) }) it('should be served when dotfiles: "allow" is given', function (done) { request(server) - .get('/.hidden') - .expect(200, 'I am hidden', done) + .get('/.hidden') + .expect(200) + .expect(shouldHaveBody(Buffer.from('I am hidden'))) + .end(done) + }) + }) + + describe('immutable', function () { + it('should default to false', function (done) { + request(createServer(fixtures)) + .get('/nums.txt') + .expect('Cache-Control', 'public, max-age=0', done) + }) + + it('should set immutable directive in Cache-Control', function (done) { + request(createServer(fixtures, { immutable: true, maxAge: '1h' })) + .get('/nums.txt') + .expect('Cache-Control', 'public, max-age=3600, immutable', done) }) }) describe('lastModified', function () { describe('when false', function () { it('should not include Last-Modifed', function (done) { - request(createServer(fixtures, {'lastModified': false})) - .get('/nums') - .expect(shouldNotHaveHeader('Last-Modified')) - .expect(200, '123456789', done) + request(createServer(fixtures, { lastModified: false })) + .get('/nums.txt') + .expect(shouldNotHaveHeader('Last-Modified')) + .expect(200, '123456789', done) }) }) describe('when true', function () { it('should include Last-Modifed', function (done) { - request(createServer(fixtures, {'lastModified': true})) - .get('/nums') - .expect('Last-Modified', /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/) - .expect(200, '123456789', done) + request(createServer(fixtures, { lastModified: true })) + .get('/nums.txt') + .expect('Last-Modified', /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/) + .expect(200, '123456789', done) }) }) }) describe('maxAge', function () { it('should accept string', function (done) { - request(createServer(fixtures, {'maxAge': '30d'})) - .get('/todo.txt') - .expect('cache-control', 'public, max-age=' + 60 * 60 * 24 * 30) - .expect(200, done) + request(createServer(fixtures, { maxAge: '30d' })) + .get('/todo.txt') + .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 30)) + .expect(200, done) }) it('should be reasonable when infinite', function (done) { - request(createServer(fixtures, {'maxAge': Infinity})) - .get('/todo.txt') - .expect('cache-control', 'public, max-age=' + 60 * 60 * 24 * 365) - .expect(200, done) + request(createServer(fixtures, { maxAge: Infinity })) + .get('/todo.txt') + .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 365)) + .expect(200, done) }) }) @@ -442,114 +474,146 @@ describe('serveStatic()', function () { it('should redirect directories', function (done) { request(server) - .get('/users') - .expect('Location', '/users/') - .expect(301, done) + .get('/users') + .expect('Location', '/users/') + .expect(301, done) }) it('should include HTML link', function (done) { request(server) - .get('/users') - .expect('Location', '/users/') - .expect(301, //, done) + .get('/users') + .expect('Location', '/users/') + .expect(301, /\/users\//, done) }) it('should redirect directories with query string', function (done) { request(server) - .get('/users?name=john') - .expect('Location', '/users/?name=john') - .expect(301, done) + .get('/users?name=john') + .expect('Location', '/users/?name=john') + .expect(301, done) }) it('should not redirect to protocol-relative locations', function (done) { request(server) - .get('//users') - .expect('Location', '/users/') - .expect(301, done) + .get('//users') + .expect('Location', '/users/') + .expect(301, done) }) it('should ensure redirect URL is properly encoded', function (done) { request(server) - .get('/snow') - .expect('Location', '/snow%20%E2%98%83/') - .expect('Content-Type', /html/) - .expect(301, 'Redirecting to /snow%20%E2%98%83/\n', done) + .get('/snow') + .expect('Location', '/snow%20%E2%98%83/') + .expect('Content-Type', /html/) + .expect(301, />Redirecting to \/snow%20%E2%98%83\/