├── .eslintignore ├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ └── scorecard.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── .eslintrc ├── fixtures └── favicon.ico ├── support └── tempIcon.js └── test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard" 3 | } 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: monthly 12 | open-pull-requests-limit: 10 13 | ignore: 14 | - dependency-name: "*" 15 | update-types: ["version-update:semver-major"] 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | test: 12 | permissions: 13 | checks: write # for coverallsapp/github-action to create new checks 14 | contents: read # for actions/checkout to fetch code 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | name: 19 | - Node.js 0.8 20 | - Node.js 0.10 21 | - Node.js 0.12 22 | - io.js 1.x 23 | - io.js 2.x 24 | - io.js 3.x 25 | - Node.js 4.x 26 | - Node.js 5.x 27 | - Node.js 6.x 28 | - Node.js 7.x 29 | - Node.js 8.x 30 | - Node.js 9.x 31 | - Node.js 10.x 32 | - Node.js 11.x 33 | - Node.js 12.x 34 | - Node.js 13.x 35 | - Node.js 14.x 36 | - Node.js 15.x 37 | - Node.js 16.x 38 | - Node.js 17.x 39 | - Node.js 18.x 40 | - Node.js 19.x 41 | - Node.js 20.x 42 | - Node.js 21.x 43 | - Node.js 22.x 44 | - Node.js 23.x 45 | - Node.js 24.x 46 | 47 | include: 48 | - name: Node.js 0.8 49 | node-version: "0.8" 50 | npm-i: mocha@2.5.3 supertest@1.1.0 51 | npm-rm: nyc 52 | 53 | - name: Node.js 0.10 54 | node-version: "0.10" 55 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.1 56 | 57 | - name: Node.js 0.12 58 | node-version: "0.12" 59 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.1 60 | 61 | - name: io.js 1.x 62 | node-version: "1" 63 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.1 64 | 65 | - name: io.js 2.x 66 | node-version: "2" 67 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.1 68 | 69 | - name: io.js 3.x 70 | node-version: "3" 71 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.1 72 | 73 | - name: Node.js 4.x 74 | node-version: "4" 75 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 76 | 77 | - name: Node.js 5.x 78 | node-version: "5" 79 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 80 | 81 | - name: Node.js 6.x 82 | node-version: "6" 83 | npm-i: mocha@6.2.2 nyc@14.1.1 supertest@4.0.2 84 | 85 | - name: Node.js 7.x 86 | node-version: "7" 87 | npm-i: mocha@6.2.2 nyc@14.1.1 supertest@4.0.2 88 | 89 | - name: Node.js 8.x 90 | node-version: "8" 91 | npm-i: mocha@7.1.2 nyc@14.1.1 supertest@4.0.2 92 | 93 | - name: Node.js 9.x 94 | node-version: "9" 95 | npm-i: mocha@7.1.2 nyc@14.1.1 supertest@4.0.2 96 | 97 | - name: Node.js 10.x 98 | node-version: "10" 99 | npm-i: mocha@8.4.0 supertest@4.0.2 100 | 101 | - name: Node.js 11.x 102 | node-version: "11" 103 | npm-i: mocha@8.4.0 supertest@4.0.2 104 | 105 | - name: Node.js 12.x 106 | node-version: "12" 107 | npm-i: mocha@9.2.2 supertest@4.0.2 108 | 109 | - name: Node.js 13.x 110 | node-version: "13" 111 | npm-i: mocha@9.2.2 supertest@4.0.2 112 | 113 | - name: Node.js 14.x 114 | node-version: "14" 115 | npm-i: supertest@4.0.2 116 | 117 | - name: Node.js 15.x 118 | node-version: "15" 119 | npm-i: supertest@4.0.2 120 | 121 | - name: Node.js 16.x 122 | node-version: "16" 123 | npm-i: supertest@4.0.2 124 | 125 | - name: Node.js 17.x 126 | node-version: "17" 127 | npm-i: supertest@4.0.2 128 | 129 | - name: Node.js 18.x 130 | node-version: "18" 131 | 132 | - name: Node.js 19.x 133 | node-version: "19" 134 | 135 | - name: Node.js 20.x 136 | node-version: "20" 137 | 138 | - name: Node.js 21.x 139 | node-version: "21" 140 | 141 | - name: Node.js 22.x 142 | node-version: "22" 143 | 144 | - name: Node.js 23.x 145 | node-version: "23" 146 | 147 | - name: Node.js 24.x 148 | node-version: "24" 149 | 150 | steps: 151 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 152 | 153 | - name: Install Node.js ${{ matrix.node-version }} 154 | shell: bash -eo pipefail -l {0} 155 | run: | 156 | nvm install --default ${{ matrix.node-version }} 157 | if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then 158 | nvm install --alias=npm 0.10 159 | nvm use ${{ matrix.node-version }} 160 | if [[ "$(npm -v)" == 1.1.* ]]; then 161 | nvm exec npm npm install -g npm@1.1 162 | ln -fs "$(which npm)" "$(dirname "$(nvm which npm)")/npm" 163 | else 164 | sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" 165 | fi 166 | npm config set strict-ssl false 167 | fi 168 | dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" 169 | 170 | - name: Configure npm 171 | run: | 172 | if [[ "$(npm config get package-lock)" == "true" ]]; then 173 | npm config set package-lock false 174 | else 175 | npm config set shrinkwrap false 176 | fi 177 | 178 | - name: Remove npm module(s) ${{ matrix.npm-rm }} 179 | if: matrix.npm-rm != '' 180 | run: npm rm --silent --save-dev ${{ matrix.npm-rm }} 181 | 182 | - name: Install npm module(s) ${{ matrix.npm-i }} 183 | if: matrix.npm-i != '' 184 | run: npm install --save-dev ${{ matrix.npm-i }} 185 | 186 | - name: Setup Node.js version-specific dependencies 187 | shell: bash 188 | run: | 189 | # eslint for linting 190 | # - remove on Node.js < 12 191 | if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then 192 | node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ 193 | grep -E '^eslint(-|$)' | \ 194 | sort -r | \ 195 | xargs -n1 npm rm --silent --save-dev 196 | fi 197 | 198 | - name: Install Node.js dependencies 199 | run: npm install 200 | 201 | - name: List environment 202 | id: list_env 203 | shell: bash 204 | run: | 205 | echo "node@$(node -v)" 206 | echo "npm@$(npm -v)" 207 | npm -s ls ||: 208 | (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" 209 | 210 | - name: Run tests 211 | shell: bash 212 | run: | 213 | if npm -ps ls nyc | grep -q nyc; then 214 | npm run test-ci 215 | else 216 | npm test 217 | fi 218 | 219 | - name: Lint code 220 | if: steps.list_env.outputs.eslint != '' 221 | run: npm run lint 222 | 223 | - name: Collect code coverage 224 | uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # master 225 | if: steps.list_env.outputs.nyc != '' 226 | with: 227 | github-token: ${{ secrets.GITHUB_TOKEN }} 228 | flag-name: run-${{ matrix.test_number }} 229 | parallel: true 230 | 231 | coverage: 232 | permissions: 233 | checks: write # for coverallsapp/github-action to create new checks 234 | needs: test 235 | runs-on: ubuntu-latest 236 | steps: 237 | - name: Upload code coverage 238 | uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # master 239 | with: 240 | github-token: ${{ secrets.GITHUB_TOKEN }} 241 | parallel-finished: true 242 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["master"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["master"] 20 | schedule: 21 | - cron: "0 0 * * 1" 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: ["javascript"] 39 | # CodeQL supports [ $supported-codeql-languages ] 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Harden the runner (Audit all outbound calls) 44 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 45 | with: 46 | egress-policy: audit 47 | 48 | - name: Checkout repository 49 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 50 | 51 | # Initializes the CodeQL tools for scanning. 52 | - name: Initialize CodeQL 53 | uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 54 | with: 55 | languages: ${{ matrix.language }} 56 | # If you wish to specify custom queries, you can do so here or in a config file. 57 | # By default, queries listed here will override any specified in a config file. 58 | # Prefix the list here with "+" to use these queries and those in the config file. 59 | 60 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 61 | # If this step fails, then you should remove it and run the build manually (see below) 62 | - name: Autobuild 63 | uses: github/codeql-action/autobuild@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 64 | 65 | # ℹ️ Command-line programs to run using the OS shell. 66 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 67 | 68 | # If the Autobuild fails above, remove it and uncomment the following three lines. 69 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 70 | 71 | # - run: | 72 | # echo "Run, Build Application using script" 73 | # ./location_of_script_within_repo/buildscript.sh 74 | 75 | - name: Perform CodeQL Analysis 76 | uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 77 | with: 78 | category: "/language:${{matrix.language}}" 79 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | 7 | on: 8 | # For Branch-Protection check. Only the default branch is supported. See 9 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 10 | branch_protection_rule: 11 | # To guarantee Maintained check is occasionally updated. See 12 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 13 | schedule: 14 | - cron: '16 21 * * 1' 15 | push: 16 | branches: [ "master" ] 17 | 18 | # Declare default permissions as read only. 19 | permissions: read-all 20 | 21 | jobs: 22 | analysis: 23 | name: Scorecard analysis 24 | runs-on: ubuntu-latest 25 | permissions: 26 | # Needed to upload the results to code-scanning dashboard. 27 | security-events: write 28 | # Needed to publish results and get a badge (see publish_results below). 29 | id-token: write 30 | # Uncomment the permissions below if installing in a private repository. 31 | # contents: read 32 | # actions: read 33 | 34 | steps: 35 | - name: "Checkout code" 36 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 37 | with: 38 | persist-credentials: false 39 | 40 | - name: "Run analysis" 41 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 42 | with: 43 | results_file: results.sarif 44 | results_format: sarif 45 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 46 | # - you want to enable the Branch-Protection check on a *public* repository, or 47 | # - you are installing Scorecard on a *private* repository 48 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 49 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 50 | 51 | # Public repositories: 52 | # - Publish results to OpenSSF REST API for easy access by consumers 53 | # - Allows the repository to include the Scorecard badge. 54 | # - See https://github.com/ossf/scorecard-action#publishing-results. 55 | # For private repositories: 56 | # - `publish_results` will always be set to `false`, regardless 57 | # of the value entered here. 58 | publish_results: true 59 | 60 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 61 | # format to the repository Actions tab. 62 | - name: "Upload artifact" 63 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 64 | with: 65 | name: SARIF file 66 | path: results.sarif 67 | retention-days: 5 68 | 69 | # Upload the results to GitHub's code scanning dashboard. 70 | - name: "Upload to code-scanning" 71 | uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | npm-debug.log 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | unreleased 2 | ================== 3 | * safe-buffer@5.2.1 4 | * ms@2.1.3 5 | 6 | 7 | 2.5.0 / 2018-03-29 8 | ================== 9 | 10 | * Ignore requests without `url` property 11 | * deps: ms@2.1.1 12 | - Add `week` 13 | - Add `w` 14 | 15 | 2.4.5 / 2017-09-26 16 | ================== 17 | 18 | * deps: etag@~1.8.1 19 | - perf: replace regular expression with substring 20 | * deps: fresh@0.5.2 21 | - Fix regression matching multiple ETags in `If-None-Match` 22 | - perf: improve `If-None-Match` token parsing 23 | 24 | 2.4.4 / 2017-09-11 25 | ================== 26 | 27 | * deps: fresh@0.5.1 28 | - Fix handling of modified headers with invalid dates 29 | - perf: improve ETag match loop 30 | * deps: parseurl@~1.3.2 31 | - perf: reduce overhead for full URLs 32 | - perf: unroll the "fast-path" `RegExp` 33 | * deps: safe-buffer@5.1.1 34 | 35 | 2.4.3 / 2017-05-16 36 | ================== 37 | 38 | * Use `safe-buffer` for improved Buffer API 39 | * deps: ms@2.0.0 40 | 41 | 2.4.2 / 2017-03-24 42 | ================== 43 | 44 | * deps: ms@1.0.0 45 | 46 | 2.4.1 / 2017-02-27 47 | ================== 48 | 49 | * Remove usage of `res._headers` private field 50 | * deps: fresh@0.5.0 51 | - Fix incorrect result when `If-None-Match` has both `*` and ETags 52 | - Fix weak `ETag` matching to match spec 53 | - perf: skip checking modified time if ETag check failed 54 | - perf: skip parsing `If-None-Match` when no `ETag` header 55 | - perf: use `Date.parse` instead of `new Date` 56 | 57 | 2.4.0 / 2017-02-19 58 | ================== 59 | 60 | * deps: etag@~1.8.0 61 | - Use SHA1 instead of MD5 for ETag hashing 62 | - Works with FIPS 140-2 OpenSSL configuration 63 | * deps: fresh@0.4.0 64 | - Fix false detection of `no-cache` request directive 65 | - perf: enable strict mode 66 | - perf: hoist regular expressions 67 | - perf: remove duplicate conditional 68 | - perf: remove unnecessary boolean coercions 69 | * perf: simplify initial argument checking 70 | 71 | 2.3.2 / 2016-11-16 72 | ================== 73 | 74 | * deps: ms@0.7.2 75 | 76 | 2.3.1 / 2016-01-23 77 | ================== 78 | 79 | * deps: parseurl@~1.3.1 80 | - perf: enable strict mode 81 | 82 | 2.3.0 / 2015-06-13 83 | ================== 84 | 85 | * Send non-chunked response for `OPTIONS` 86 | * deps: etag@~1.7.0 87 | - Always include entity length in ETags for hash length extensions 88 | - Generate non-Stats ETags using MD5 only (no longer CRC32) 89 | - Remove base64 padding in ETags to shorten 90 | * deps: fresh@0.3.0 91 | - Add weak `ETag` matching support 92 | * perf: enable strict mode 93 | * perf: remove argument reassignment 94 | * perf: remove bitwise operations 95 | 96 | 2.2.1 / 2015-05-14 97 | ================== 98 | 99 | * deps: etag@~1.6.0 100 | - Improve support for JXcore 101 | - Support "fake" stats objects in environments without `fs` 102 | * deps: ms@0.7.1 103 | - Prevent extraordinarily long inputs 104 | 105 | 2.2.0 / 2014-12-18 106 | ================== 107 | 108 | * Support query string in the URL 109 | * deps: etag@~1.5.1 110 | - deps: crc@3.2.1 111 | * deps: ms@0.7.0 112 | - Add `milliseconds` 113 | - Add `msecs` 114 | - Add `secs` 115 | - Add `mins` 116 | - Add `hrs` 117 | - Add `yrs` 118 | 119 | 2.1.7 / 2014-11-19 120 | ================== 121 | 122 | * Avoid errors from enumerables on `Object.prototype` 123 | 124 | 2.1.6 / 2014-10-16 125 | ================== 126 | 127 | * deps: etag@~1.5.0 128 | 129 | 2.1.5 / 2014-09-24 130 | ================== 131 | 132 | * deps: etag@~1.4.0 133 | 134 | 2.1.4 / 2014-09-15 135 | ================== 136 | 137 | * Fix content headers being sent in 304 response 138 | * deps: etag@~1.3.1 139 | - Improve ETag generation speed 140 | 141 | 2.1.3 / 2014-09-07 142 | ================== 143 | 144 | * deps: fresh@0.2.4 145 | 146 | 2.1.2 / 2014-09-05 147 | ================== 148 | 149 | * deps: etag@~1.3.0 150 | - Improve ETag generation speed 151 | 152 | 2.1.1 / 2014-08-25 153 | ================== 154 | 155 | * Fix `ms` to be listed as a dependency 156 | 157 | 2.1.0 / 2014-08-24 158 | ================== 159 | 160 | * Accept string for `maxAge` (converted by `ms`) 161 | * Use `etag` to generate `ETag` header 162 | 163 | 2.0.1 / 2014-06-05 164 | ================== 165 | 166 | * Reduce byte size of `ETag` header 167 | 168 | 2.0.0 / 2014-05-02 169 | ================== 170 | 171 | * `path` argument is required; there is no default icon. 172 | * Accept `Buffer` of icon as first argument. 173 | * Non-GET and HEAD requests are denied. 174 | * Send valid max-age value 175 | * Support conditional requests 176 | * Support max-age=0 177 | * Support OPTIONS method 178 | * Throw if `path` argument is directory. 179 | 180 | 1.0.2 / 2014-03-16 181 | ================== 182 | 183 | * Fixed content of default icon. 184 | 185 | 1.0.1 / 2014-03-11 186 | ================== 187 | 188 | * Fixed path to default icon. 189 | 190 | 1.0.0 / 2014-02-15 191 | ================== 192 | 193 | * Initial release 194 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2010 Sencha Inc. 4 | Copyright (c) 2011 LearnBoost 5 | Copyright (c) 2011 TJ Holowaychuk 6 | Copyright (c) 2014-2017 Douglas Christopher Wilson 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining 9 | a copy of this software and associated documentation files (the 10 | 'Software'), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 22 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 24 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 25 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serve-favicon 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | [![NPM Downloads][downloads-image]][downloads-url] 5 | [![Linux Build Status][ci-image]][ci-url] 6 | [![Coverage Status][coveralls-image]][coveralls-url] 7 | [![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer] 8 | 9 | Node.js middleware for serving a favicon. 10 | 11 | A favicon is a visual cue that client software, like browsers, use to identify 12 | a site. For an example and more information, please visit 13 | [the Wikipedia article on favicons](https://en.wikipedia.org/wiki/Favicon). 14 | 15 | Why use this module? 16 | 17 | - User agents request `favicon.ico` frequently and indiscriminately, so you 18 | may wish to exclude these requests from your logs by using this middleware 19 | before your logger middleware. 20 | - This module caches the icon in memory to improve performance by skipping 21 | disk access. 22 | - This module provides an `ETag` based on the contents of the icon, rather 23 | than file system properties. 24 | - This module will serve with the most compatible `Content-Type`. 25 | 26 | **Note** This module is exclusively for serving the "default, implicit favicon", 27 | which is `GET /favicon.ico`. For additional vendor-specific icons that require 28 | HTML markup, additional middleware is required to serve the relevant files, for 29 | example [serve-static](https://npmjs.org/package/serve-static). 30 | 31 | ## Install 32 | 33 | This is a [Node.js](https://nodejs.org/en/) module available through the 34 | [npm registry](https://www.npmjs.com/). Installation is done using the 35 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 36 | 37 | ```sh 38 | $ npm install serve-favicon 39 | ``` 40 | 41 | ## API 42 | 43 | ### favicon(path, options) 44 | 45 | Create new middleware to serve a favicon from the given `path` to a favicon file. 46 | `path` may also be a `Buffer` of the icon to serve. 47 | 48 | #### Options 49 | 50 | Serve favicon accepts these properties in the options object. 51 | 52 | ##### maxAge 53 | 54 | The `cache-control` `max-age` directive in `ms`, defaulting to 1 year. This can 55 | also be a string accepted by the [ms](https://www.npmjs.org/package/ms#readme) 56 | module. 57 | 58 | ## Examples 59 | 60 | Typically this middleware will come very early in your stack (maybe even first) 61 | to avoid processing any other middleware if we already know the request is for 62 | `/favicon.ico`. 63 | 64 | ### express 65 | 66 | ```javascript 67 | var express = require('express') 68 | var favicon = require('serve-favicon') 69 | var path = require('path') 70 | 71 | var app = express() 72 | app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))) 73 | 74 | // Add your routes here, etc. 75 | 76 | app.listen(3000) 77 | ``` 78 | 79 | ### connect 80 | 81 | ```javascript 82 | var connect = require('connect') 83 | var favicon = require('serve-favicon') 84 | var path = require('path') 85 | 86 | var app = connect() 87 | app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))) 88 | 89 | // Add your middleware here, etc. 90 | 91 | app.listen(3000) 92 | ``` 93 | 94 | ### vanilla http server 95 | 96 | This middleware can be used anywhere, even outside express/connect. It takes 97 | `req`, `res`, and `callback`. 98 | 99 | ```javascript 100 | var http = require('http') 101 | var favicon = require('serve-favicon') 102 | var finalhandler = require('finalhandler') 103 | var path = require('path') 104 | 105 | var _favicon = favicon(path.join(__dirname, 'public', 'favicon.ico')) 106 | 107 | var server = http.createServer(function onRequest (req, res) { 108 | var done = finalhandler(req, res) 109 | 110 | _favicon(req, res, function onNext (err) { 111 | if (err) return done(err) 112 | 113 | // continue to process the request here, etc. 114 | 115 | res.statusCode = 404 116 | res.end('oops') 117 | }) 118 | }) 119 | 120 | server.listen(3000) 121 | ``` 122 | 123 | ## License 124 | 125 | [MIT](LICENSE) 126 | 127 | [ci-image]: https://badgen.net/github/checks/expressjs/serve-favicon/master?label=ci 128 | [ci-url]: https://github.com/expressjs/serve-favicon/actions/workflows/ci.yml 129 | [coveralls-image]: https://img.shields.io/coveralls/expressjs/serve-favicon.svg 130 | [coveralls-url]: https://coveralls.io/r/expressjs/serve-favicon?branch=master 131 | [downloads-image]: https://img.shields.io/npm/dm/serve-favicon.svg 132 | [downloads-url]: https://npmjs.org/package/serve-favicon 133 | [npm-image]: https://img.shields.io/npm/v/serve-favicon.svg 134 | [npm-url]: https://npmjs.org/package/serve-favicon 135 | [ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/serve-favicon/badge 136 | [ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/serve-favicon 137 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * serve-favicon 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * Copyright(c) 2014-2017 Douglas Christopher Wilson 6 | * MIT Licensed 7 | */ 8 | 9 | 'use strict' 10 | 11 | /** 12 | * Module dependencies. 13 | * @private 14 | */ 15 | 16 | var Buffer = require('safe-buffer').Buffer 17 | var etag = require('etag') 18 | var fresh = require('fresh') 19 | var fs = require('fs') 20 | var ms = require('ms') 21 | var parseUrl = require('parseurl') 22 | var path = require('path') 23 | var resolve = path.resolve 24 | 25 | /** 26 | * Module exports. 27 | * @public 28 | */ 29 | 30 | module.exports = favicon 31 | 32 | /** 33 | * Module variables. 34 | * @private 35 | */ 36 | 37 | var ONE_YEAR_MS = 60 * 60 * 24 * 365 * 1000 // 1 year 38 | 39 | /** 40 | * Serves the favicon located by the given `path`. 41 | * 42 | * @public 43 | * @param {String|Buffer} path 44 | * @param {Object} [options] 45 | * @return {Function} middleware 46 | */ 47 | 48 | function favicon (path, options) { 49 | var opts = options || {} 50 | 51 | var icon // favicon cache 52 | var maxAge = calcMaxAge(opts.maxAge) 53 | 54 | if (!path) { 55 | throw new TypeError('path to favicon.ico is required') 56 | } 57 | 58 | if (Buffer.isBuffer(path)) { 59 | icon = createIcon(Buffer.from(path), maxAge) 60 | } else if (typeof path === 'string') { 61 | path = resolveSync(path) 62 | } else { 63 | throw new TypeError('path to favicon.ico must be string or buffer') 64 | } 65 | 66 | return function favicon (req, res, next) { 67 | if (getPathname(req) !== '/favicon.ico') { 68 | next() 69 | return 70 | } 71 | 72 | if (req.method !== 'GET' && req.method !== 'HEAD') { 73 | res.statusCode = req.method === 'OPTIONS' ? 200 : 405 74 | res.setHeader('Allow', 'GET, HEAD, OPTIONS') 75 | res.setHeader('Content-Length', '0') 76 | res.end() 77 | return 78 | } 79 | 80 | if (icon) { 81 | send(req, res, icon) 82 | return 83 | } 84 | 85 | fs.readFile(path, function (err, buf) { 86 | if (err) return next(err) 87 | icon = createIcon(buf, maxAge) 88 | send(req, res, icon) 89 | }) 90 | } 91 | } 92 | 93 | /** 94 | * Calculate the max-age from a configured value. 95 | * 96 | * @private 97 | * @param {string|number} val 98 | * @return {number} 99 | */ 100 | 101 | function calcMaxAge (val) { 102 | var num = typeof val === 'string' 103 | ? ms(val) 104 | : val 105 | 106 | return num != null 107 | ? Math.min(Math.max(0, num), ONE_YEAR_MS) 108 | : ONE_YEAR_MS 109 | } 110 | 111 | /** 112 | * Create icon data from Buffer and max-age. 113 | * 114 | * @private 115 | * @param {Buffer} buf 116 | * @param {number} maxAge 117 | * @return {object} 118 | */ 119 | 120 | function createIcon (buf, maxAge) { 121 | return { 122 | body: buf, 123 | headers: { 124 | 'Cache-Control': 'public, max-age=' + Math.floor(maxAge / 1000), 125 | 'ETag': etag(buf) 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * Create EISDIR error. 132 | * 133 | * @private 134 | * @param {string} path 135 | * @return {Error} 136 | */ 137 | 138 | function createIsDirError (path) { 139 | var error = new Error('EISDIR, illegal operation on directory \'' + path + '\'') 140 | error.code = 'EISDIR' 141 | error.errno = 28 142 | error.path = path 143 | error.syscall = 'open' 144 | return error 145 | } 146 | 147 | /** 148 | * Get the request pathname. 149 | * 150 | * @param {object} req 151 | * @return {string} 152 | */ 153 | 154 | function getPathname (req) { 155 | try { 156 | return parseUrl(req).pathname 157 | } catch (e) { 158 | return undefined 159 | } 160 | } 161 | 162 | /** 163 | * Determine if the cached representation is fresh. 164 | * 165 | * @param {object} req 166 | * @param {object} res 167 | * @return {boolean} 168 | * @private 169 | */ 170 | 171 | function isFresh (req, res) { 172 | return fresh(req.headers, { 173 | 'etag': res.getHeader('ETag'), 174 | 'last-modified': res.getHeader('Last-Modified') 175 | }) 176 | } 177 | 178 | /** 179 | * Resolve the path to icon. 180 | * 181 | * @param {string} iconPath 182 | * @private 183 | */ 184 | 185 | function resolveSync (iconPath) { 186 | var path = resolve(iconPath) 187 | var stat = fs.statSync(path) 188 | 189 | if (stat.isDirectory()) { 190 | throw createIsDirError(path) 191 | } 192 | 193 | return path 194 | } 195 | 196 | /** 197 | * Send icon data in response to a request. 198 | * 199 | * @private 200 | * @param {IncomingMessage} req 201 | * @param {OutgoingMessage} res 202 | * @param {object} icon 203 | */ 204 | 205 | function send (req, res, icon) { 206 | // Set headers 207 | var headers = icon.headers 208 | var keys = Object.keys(headers) 209 | for (var i = 0; i < keys.length; i++) { 210 | var key = keys[i] 211 | res.setHeader(key, headers[key]) 212 | } 213 | 214 | // Validate freshness 215 | if (isFresh(req, res)) { 216 | res.statusCode = 304 217 | res.end() 218 | return 219 | } 220 | 221 | // Send icon 222 | res.statusCode = 200 223 | res.setHeader('Content-Length', icon.body.length) 224 | res.setHeader('Content-Type', 'image/x-icon') 225 | res.end(icon.body) 226 | } 227 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serve-favicon", 3 | "description": "favicon serving middleware with caching", 4 | "version": "2.5.0", 5 | "author": "Douglas Christopher Wilson ", 6 | "license": "MIT", 7 | "keywords": [ 8 | "express", 9 | "favicon", 10 | "middleware" 11 | ], 12 | "repository": "expressjs/serve-favicon", 13 | "dependencies": { 14 | "etag": "~1.8.1", 15 | "fresh": "0.5.2", 16 | "ms": "2.1.3", 17 | "parseurl": "~1.3.2", 18 | "safe-buffer": "5.2.1" 19 | }, 20 | "devDependencies": { 21 | "eslint": "4.19.1", 22 | "eslint-config-standard": "11.0.0", 23 | "eslint-plugin-import": "2.31.0", 24 | "eslint-plugin-markdown": "1.0.2", 25 | "eslint-plugin-node": "6.0.1", 26 | "eslint-plugin-promise": "3.8.0", 27 | "eslint-plugin-standard": "3.1.0", 28 | "mocha": "10.8.2", 29 | "nyc": "^15.1.0", 30 | "supertest": "7.1.1", 31 | "temp-path": "1.0.0" 32 | }, 33 | "files": [ 34 | "LICENSE", 35 | "HISTORY.md", 36 | "index.js" 37 | ], 38 | "engines": { 39 | "node": ">= 0.8.0" 40 | }, 41 | "scripts": { 42 | "lint": "eslint --plugin markdown --ext js,md .", 43 | "test": "mocha --reporter spec --bail --check-leaks test/", 44 | "test-ci": "nyc --reporter=lcov --reporter=text npm test", 45 | "test-cov": "nyc --reporter=html --reporter=text npm test" 46 | } 47 | } -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expressjs/serve-favicon/262b7bf9563e76aca445addd06638ac3d1f7e547/test/fixtures/favicon.ico -------------------------------------------------------------------------------- /test/support/tempIcon.js: -------------------------------------------------------------------------------- 1 | 2 | var crypto = require('crypto') 3 | var fs = require('fs') 4 | var tempPath = require('temp-path') 5 | 6 | module.exports = TempIcon 7 | 8 | function TempIcon () { 9 | this.data = crypto.pseudoRandomBytes(100) 10 | this.exists = false 11 | this.path = tempPath() 12 | } 13 | 14 | TempIcon.prototype.unlinkSync = function unlinkSync () { 15 | if (this.exists) { 16 | this.exists = false 17 | fs.unlinkSync(this.path) 18 | } 19 | } 20 | 21 | TempIcon.prototype.writeSync = function writeSync () { 22 | if (this.exists) { 23 | throw new Error('already written') 24 | } else { 25 | fs.writeFileSync(this.path, this.data) 26 | this.exists = true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert') 3 | var Buffer = require('safe-buffer').Buffer 4 | var favicon = require('..') 5 | var http = require('http') 6 | var path = require('path') 7 | var request = require('supertest') 8 | var TempIcon = require('./support/tempIcon') 9 | 10 | var FIXTURES_PATH = path.join(__dirname, 'fixtures') 11 | var ICON_PATH = path.join(FIXTURES_PATH, 'favicon.ico') 12 | 13 | describe('favicon()', function () { 14 | describe('arguments', function () { 15 | describe('path', function () { 16 | it('should be required', function () { 17 | assert.throws(favicon.bind(), /path.*required/) 18 | }) 19 | 20 | it('should accept file path', function () { 21 | assert.doesNotThrow(favicon.bind(null, path.join(FIXTURES_PATH, 'favicon.ico'))) 22 | }) 23 | 24 | it('should accept buffer', function () { 25 | assert.doesNotThrow(favicon.bind(null, Buffer.alloc(20))) 26 | }) 27 | 28 | it('should exist', function () { 29 | assert.throws(favicon.bind(null, path.join(FIXTURES_PATH, 'nothing')), /ENOENT.*nothing/) 30 | }) 31 | 32 | it('should not be dir', function () { 33 | assert.throws(favicon.bind(null, FIXTURES_PATH), /EISDIR.*fixtures/) 34 | }) 35 | 36 | it('should not be number', function () { 37 | assert.throws(favicon.bind(null, 12), /path.*must be.*string/) 38 | }) 39 | }) 40 | 41 | describe('options.maxAge', function () { 42 | it('should be in cache-control', function (done) { 43 | var server = createServer(null, {maxAge: 5000}) 44 | request(server) 45 | .get('/favicon.ico') 46 | .expect('Cache-Control', 'public, max-age=5') 47 | .expect(200, done) 48 | }) 49 | 50 | it('should have a default', function (done) { 51 | var server = createServer() 52 | request(server) 53 | .get('/favicon.ico') 54 | .expect('Cache-Control', /public, max-age=[0-9]+/) 55 | .expect(200, done) 56 | }) 57 | 58 | it('should accept 0', function (done) { 59 | var server = createServer(null, {maxAge: 0}) 60 | request(server) 61 | .get('/favicon.ico') 62 | .expect('Cache-Control', 'public, max-age=0') 63 | .expect(200, done) 64 | }) 65 | 66 | it('should accept string', function (done) { 67 | var server = createServer(null, {maxAge: '30d'}) 68 | request(server) 69 | .get('/favicon.ico') 70 | .expect('Cache-Control', 'public, max-age=2592000') 71 | .expect(200, done) 72 | }) 73 | 74 | it('should be valid delta-seconds', function (done) { 75 | var server = createServer(null, {maxAge: 1234}) 76 | request(server) 77 | .get('/favicon.ico') 78 | .expect('Cache-Control', 'public, max-age=1') 79 | .expect(200, done) 80 | }) 81 | 82 | it('should floor at 0', function (done) { 83 | var server = createServer(null, {maxAge: -4000}) 84 | request(server) 85 | .get('/favicon.ico') 86 | .expect('Cache-Control', 'public, max-age=0') 87 | .expect(200, done) 88 | }) 89 | 90 | it('should ceil at 1 year', function (done) { 91 | var server = createServer(null, {maxAge: 900000000000}) 92 | request(server) 93 | .get('/favicon.ico') 94 | .expect('Cache-Control', 'public, max-age=31536000') 95 | .expect(200, done) 96 | }) 97 | 98 | it('should accept Inifnity', function (done) { 99 | var server = createServer(null, {maxAge: Infinity}) 100 | request(server) 101 | .get('/favicon.ico') 102 | .expect('Cache-Control', 'public, max-age=31536000') 103 | .expect(200, done) 104 | }) 105 | }) 106 | }) 107 | 108 | describe('requests', function () { 109 | before(function () { 110 | this.server = createServer() 111 | }) 112 | 113 | it('should serve icon', function (done) { 114 | request(this.server) 115 | .get('/favicon.ico') 116 | .expect('Content-Type', 'image/x-icon') 117 | .expect(200, done) 118 | }) 119 | 120 | it('should include cache-control', function (done) { 121 | request(this.server) 122 | .get('/favicon.ico') 123 | .expect('Cache-Control', /public/) 124 | .expect(200, done) 125 | }) 126 | 127 | it('should include strong etag', function (done) { 128 | request(this.server) 129 | .get('/favicon.ico') 130 | .expect('ETag', /^"[^"]+"$/) 131 | .expect(200, done) 132 | }) 133 | 134 | it('should deny POST', function (done) { 135 | request(this.server) 136 | .post('/favicon.ico') 137 | .expect('Allow', 'GET, HEAD, OPTIONS') 138 | .expect(405, done) 139 | }) 140 | 141 | it('should understand OPTIONS', function (done) { 142 | request(this.server) 143 | .options('/favicon.ico') 144 | .expect('Allow', 'GET, HEAD, OPTIONS') 145 | .expect(200, done) 146 | }) 147 | 148 | it('should 304 when If-None-Match matches', function (done) { 149 | var server = this.server 150 | request(server) 151 | .get('/favicon.ico') 152 | .expect(200, function (err, res) { 153 | if (err) return done(err) 154 | request(server) 155 | .get('/favicon.ico') 156 | .set('If-None-Match', res.headers.etag) 157 | .expect(304, done) 158 | }) 159 | }) 160 | 161 | it('should 304 when If-None-Match matches weakly', function (done) { 162 | var server = this.server 163 | request(server) 164 | .get('/favicon.ico') 165 | .expect(200, function (err, res) { 166 | if (err) return done(err) 167 | request(server) 168 | .get('/favicon.ico') 169 | .set('If-None-Match', 'W/' + res.headers.etag) 170 | .expect(304, done) 171 | }) 172 | }) 173 | 174 | it('should ignore non-favicon requests', function (done) { 175 | request(this.server) 176 | .get('/') 177 | .expect(404, 'oops', done) 178 | }) 179 | 180 | it('should work with query string', function (done) { 181 | request(this.server) 182 | .get('/favicon.ico?v=1') 183 | .expect('Content-Type', 'image/x-icon') 184 | .expect(200, done) 185 | }) 186 | 187 | describe('missing req.url', function () { 188 | it('should ignore the request', function (done) { 189 | var fn = favicon(ICON_PATH) 190 | fn({}, {}, done) 191 | }) 192 | }) 193 | }) 194 | 195 | describe('icon', function () { 196 | describe('file', function () { 197 | beforeEach(function () { 198 | this.icon = new TempIcon() 199 | this.icon.writeSync() 200 | }) 201 | 202 | afterEach(function () { 203 | this.icon.unlinkSync() 204 | this.icon = undefined 205 | }) 206 | 207 | it('should be read on first request', function (done) { 208 | var icon = this.icon 209 | var server = createServer(icon.path) 210 | 211 | request(server) 212 | .get('/favicon.ico') 213 | .expect(200, icon.data, done) 214 | }) 215 | 216 | it('should cache for second request', function (done) { 217 | var icon = this.icon 218 | var server = createServer(icon.path) 219 | 220 | request(server) 221 | .get('/favicon.ico') 222 | .expect(200, icon.data, function (err) { 223 | if (err) return done(err) 224 | icon.unlinkSync() 225 | request(server) 226 | .get('/favicon.ico') 227 | .expect(200, icon.data, done) 228 | }) 229 | }) 230 | }) 231 | 232 | describe('file error', function () { 233 | beforeEach(function () { 234 | this.icon = new TempIcon() 235 | this.icon.writeSync() 236 | }) 237 | 238 | afterEach(function () { 239 | this.icon.unlinkSync() 240 | this.icon = undefined 241 | }) 242 | 243 | it('should next() file read errors', function (done) { 244 | var icon = this.icon 245 | var server = createServer(icon.path) 246 | 247 | icon.unlinkSync() 248 | request(server) 249 | .get('/favicon.ico') 250 | .expect(500, /ENOENT/, done) 251 | }) 252 | 253 | it('should retry reading file after error', function (done) { 254 | var icon = this.icon 255 | var server = createServer(icon.path) 256 | 257 | icon.unlinkSync() 258 | request(server) 259 | .get('/favicon.ico') 260 | .expect(500, /ENOENT/, function (err) { 261 | if (err) return done(err) 262 | icon.writeSync() 263 | request(server) 264 | .get('/favicon.ico') 265 | .expect(200, icon.data, done) 266 | }) 267 | }) 268 | }) 269 | 270 | describe('buffer', function () { 271 | it('should be served from buffer', function (done) { 272 | var buffer = Buffer.alloc(20, '#') 273 | var server = createServer(buffer) 274 | 275 | request(server) 276 | .get('/favicon.ico') 277 | .expect('Content-Length', '20') 278 | .expect(200, buffer, done) 279 | }) 280 | 281 | it('should be copied', function (done) { 282 | var buffer = Buffer.alloc(20, '#') 283 | var server = createServer(buffer) 284 | 285 | assert.equal(buffer.toString(), '####################') 286 | buffer.fill('?') 287 | assert.equal(buffer.toString(), '????????????????????') 288 | 289 | request(server) 290 | .get('/favicon.ico') 291 | .expect('Content-Length', '20') 292 | .expect(200, Buffer.from('####################'), done) 293 | }) 294 | }) 295 | }) 296 | }) 297 | 298 | function createServer (path, opts) { 299 | var _path = path || ICON_PATH 300 | var _favicon = favicon(_path, opts) 301 | var server = http.createServer(function onRequest (req, res) { 302 | _favicon(req, res, function onNext (err) { 303 | res.statusCode = err ? (err.status || 500) : 404 304 | res.end(err ? err.message : 'oops') 305 | }) 306 | }) 307 | 308 | return server 309 | } 310 | --------------------------------------------------------------------------------