diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd93ab223d2..b3ddd6b23ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,179 +1,190 @@ name: ci on: -- pull_request -- push + push: + branches: + - master + - develop + - '4.x' + - '5.x' + paths-ignore: + - '*.md' + pull_request: + paths-ignore: + - '*.md' + +# Cancel in progress workflows +# in the scenario where we already had a run going for that PR/branch/tag but then triggered a new run +concurrency: + group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true jobs: - test: + lint: + name: Lint runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js {{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + persist-credentials: false + + - name: Install dependencies + run: npm install --ignore-scripts --only=dev + + - name: Run lint + run: npm run lint + + test: + name: Run tests strategy: fail-fast: false matrix: - name: - - 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 - - Node.js 18.x - + os: [ubuntu-latest, windows-latest] + node-version: + - "0.10" + - "0.12" + - "4" + - "5" + - "6" + - "7" + - "8" + - "9" + - "10" + - "11" + - "12" + - "13" + - "14" + - "15" + - "16" + - "17" + - "18" + - "19" + - "20" + - "21" + - "22" + # Use supported versions of our testing tools under older versions of Node + # Install npm in some specific cases where we need to include: - - 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 + - node-version: "0.10" + npm-i: "mocha@3.5.3 nyc@10.3.2 supertest@2.0.0" + # Npm isn't being installed on windows w/ setup-node for + # 0.10 and 0.12, which will end up choking when npm uses es6 + npm-version: "npm@2.15.1" - - name: io.js 2.x - node-version: "2.5" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + - node-version: "0.12" + npm-i: "mocha@3.5.3 nyc@10.3.2 supertest@2.0.0" + npm-version: "npm@2.15.11" - - name: io.js 3.x - node-version: "3.3" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + - node-version: "4" + npm-i: "mocha@5.2.0 nyc@11.9.0 supertest@3.4.2" - - name: Node.js 4.x - node-version: "4.9" - npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + - node-version: "5" + npm-i: "mocha@5.2.0 nyc@11.9.0 supertest@3.4.2" + # fixes https://github.com/npm/cli/issues/681 + npm-version: "npm@3.10.10" - - name: Node.js 5.x - node-version: "5.12" - npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + - node-version: "6" + npm-i: "mocha@6.2.2 nyc@14.1.1 supertest@3.4.2" - - name: Node.js 6.x - node-version: "6.17" - npm-i: mocha@6.2.2 nyc@14.1.1 supertest@3.4.2 + - node-version: "7" + npm-i: "mocha@6.2.2 nyc@14.1.1 supertest@6.1.6" - - name: Node.js 7.x - node-version: "7.10" - npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 + - node-version: "8" + npm-i: "mocha@7.2.0 nyc@14.1.1" - - name: Node.js 8.x - node-version: "8.17" - npm-i: mocha@7.2.0 + - node-version: "9" + npm-i: "mocha@7.2.0 nyc@14.1.1" - - name: Node.js 9.x - node-version: "9.11" - npm-i: mocha@7.2.0 + - node-version: "10" + npm-i: "mocha@8.4.0" - - name: Node.js 10.x - node-version: "10.24" - npm-i: mocha@8.4.0 + - node-version: "11" + npm-i: "mocha@8.4.0" - - name: Node.js 11.x - node-version: "11.15" - npm-i: mocha@8.4.0 + - node-version: "12" + npm-i: "mocha@9.2.2" - - name: Node.js 12.x - node-version: "12.22" - npm-i: mocha@9.2.2 - - - name: Node.js 13.x - node-version: "13.14" - npm-i: mocha@9.2.2 - - - name: Node.js 14.x - node-version: "14.20" - - - name: Node.js 15.x - node-version: "15.14" - - - name: Node.js 16.x - node-version: "16.17" - - - name: Node.js 17.x - node-version: "17.9" - - - name: Node.js 18.x - node-version: "18.10" + - node-version: "13" + npm-i: "mocha@9.2.2" + runs-on: ${{ matrix.os }} 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 }} - dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" - - - name: Configure npm - run: | - npm config set loglevel error - npm config set shrinkwrap false + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Npm version fixes + if: ${{matrix.npm-version != ''}} + run: npm install -g ${{ matrix.npm-version }} + + - name: Configure npm loglevel + run: | + npm config set loglevel error + shell: bash + + - name: Install dependencies + run: npm install + + - name: Install Node version specific dev deps + if: ${{ matrix.npm-i != '' }} + run: npm install --save-dev ${{ matrix.npm-i }} + + - name: Remove non-test dependencies + run: npm rm --silent --save-dev connect-redis + + - name: Output Node and NPM versions + run: | + echo "Node.js version: $(node -v)" + echo "NPM version: $(npm -v)" + + - name: Run tests + shell: bash + run: | + npm run test-ci + cp coverage/lcov.info "coverage/${{ matrix.node-version }}.lcov" + + - name: Collect code coverage + run: | + mv ./coverage "./${{ matrix.node-version }}" + mkdir ./coverage + mv "./${{ matrix.node-version }}" "./coverage/${{ matrix.node-version }}" + + - name: Upload code coverage + uses: actions/upload-artifact@v3 + with: + name: coverage + path: ./coverage + retention-days: 1 - - name: Install npm module(s) ${{ matrix.npm-i }} - run: npm install --save-dev ${{ matrix.npm-i }} - if: matrix.npm-i != '' - - - name: Remove non-test dependencies - run: npm rm --silent --save-dev connect-redis + coverage: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 - - name: Setup Node.js version-specific dependencies - shell: bash - run: | - # eslint for linting - # - remove on Node.js < 12 - if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; 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 + - name: Install lcov 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 }' + run: sudo apt-get -y install lcov - - name: Run tests - shell: bash - run: npm run test-ci + - name: Collect coverage reports + uses: actions/download-artifact@v3 + with: + name: coverage + path: ./coverage - - name: Lint code - if: steps.list_env.outputs.eslint != '' - run: npm run lint + - name: Merge coverage reports + shell: bash + run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./coverage/lcov.info - - name: Collect code coverage + - name: Upload coverage report uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: run-${{ matrix.test_number }} - parallel: true - - coverage: - needs: test - runs-on: ubuntu-latest - steps: - - name: Upload code coverage - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.github_token }} - parallel-finished: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..db4e01aff56 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,66 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["master"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["master"] + schedule: + - cron: "0 0 * * 1" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 + with: + languages: javascript + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # uses: github/codeql-action/autobuild@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 + with: + category: "/language:javascript" diff --git a/.github/workflows/iojs.yml b/.github/workflows/iojs.yml new file mode 100644 index 00000000000..c1268abd689 --- /dev/null +++ b/.github/workflows/iojs.yml @@ -0,0 +1,69 @@ +name: iojs-ci + +on: + push: + branches: + - master + - '4.x' + paths-ignore: + - '*.md' + pull_request: + paths-ignore: + - '*.md' + +concurrency: + group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ["1.8", "2.5", "3.3"] + include: + - node-version: "1.8" + npm-i: "mocha@3.5.3 nyc@10.3.2 supertest@2.0.0" + - node-version: "2.5" + npm-i: "mocha@3.5.3 nyc@10.3.2 supertest@2.0.0" + - node-version: "3.3" + npm-i: "mocha@3.5.3 nyc@10.3.2 supertest@2.0.0" + + steps: + - uses: actions/checkout@v4 + + - name: Install iojs ${{ matrix.node-version }} + shell: bash -eo pipefail -l {0} + run: | + nvm install --default ${{ matrix.node-version }} + dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" + + - name: Configure npm + run: | + npm config set loglevel error + npm config set shrinkwrap false + + - name: Install npm module(s) ${{ matrix.npm-i }} + run: npm install --save-dev ${{ matrix.npm-i }} + if: matrix.npm-i != '' + + - name: Remove non-test dependencies + run: npm rm --silent --save-dev connect-redis + + - 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 $2 "=" $3 }' >> "$GITHUB_OUTPUT" + + - name: Run tests + shell: bash + run: npm run test + diff --git a/.gitignore b/.gitignore index 3a673d9cc09..1bd5c02b28b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # npm node_modules package-lock.json +npm-shrinkwrap.json *.log *.gz diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..43c97e719a5 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/Code-Of-Conduct.md b/Code-Of-Conduct.md index bbb8996a659..ca4c6b31468 100644 --- a/Code-Of-Conduct.md +++ b/Code-Of-Conduct.md @@ -127,7 +127,7 @@ project community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant, version 2.0](cc-20-doc). +This Code of Conduct is adapted from the [Contributor Covenant, version 2.0][cc-20-doc]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). diff --git a/Contributing.md b/Contributing.md index 485dee597e1..1654cee02f2 100644 --- a/Contributing.md +++ b/Contributing.md @@ -12,6 +12,7 @@ contributors can be involved in decision making. * A **Contributor** is any individual creating or commenting on an issue or pull request. * A **Committer** is a subset of contributors who have been given write access to the repository. +* A **Project Captain** is the lead maintainer of a repository. * A **TC (Technical Committee)** is a group of committers representing the required technical expertise to resolve rare disputes. * A **Triager** is a subset of contributors who have been given triage access to the repository. @@ -62,29 +63,14 @@ compromise among committers be the default resolution mechanism. Anyone can become a triager! Read more about the process of being a triager in [the triage process document](Triager-Guide.md). -[Open an issue in `expressjs/express` repo](https://github.com/expressjs/express/issues/new) -to request the triage role. State that you have read and agree to the -[Code of Conduct](Code-Of-Conduct.md) and details of the role. +Currently, any existing [organization member](https://github.com/orgs/expressjs/people) can nominate +a new triager. If you are interested in becoming a triager, our best advice is to actively participate +in the community by helping triaging issues and pull requests. As well we recommend +to engage in other community activities like attending the TC meetings, and participating in the Slack +discussions. -Here is an example issue content you can copy and paste: - -``` -Title: Request triager role for - -I have read and understood the project's Code of Conduct. -I also have read and understood the process and best practices around Express triaging. - -I request for a triager role for the following GitHub organizations: - -jshttp -pillarjs -express -``` - -Once you have opened your issue, a member of the TC will add you to the `triage` team in -the organizations requested. They will then close the issue. - -Happy triaging! +You can also reach out to any of the [organization members](https://github.com/orgs/expressjs/people) +if you have questions or need guidance. ## Becoming a Committer @@ -102,12 +88,122 @@ If a consensus cannot be reached that has no objections then a majority wins vot is called. It is also expected that the majority of decisions made by the TC are via a consensus seeking process and that voting is only used as a last-resort. -Resolution may involve returning the issue to committers with suggestions on how to -move forward towards a consensus. It is not expected that a meeting of the TC +Resolution may involve returning the issue to project captains with suggestions on +how to move forward towards a consensus. It is not expected that a meeting of the TC will resolve all issues on its agenda during that meeting and may prefer to continue -the discussion happening among the committers. +the discussion happening among the project captains. -Members can be added to the TC at any time. Any committer can nominate another committer +Members can be added to the TC at any time. Any TC member can nominate another committer to the TC and the TC uses its standard consensus seeking process to evaluate whether or -not to add this new member. Members who do not participate consistently at the level of -a majority of the other members are expected to resign. +not to add this new member. The TC will consist of a minimum of 3 active members and a +maximum of 10. If the TC should drop below 5 members the active TC members should nominate +someone new. If a TC member is stepping down, they are encouraged (but not required) to +nominate someone to take their place. + +TC members will be added as admin's on the Github orgs, npm orgs, and other resources as +necessary to be effective in the role. + +To remain "active" a TC member should have participation within the last 12 months and miss +no more than six consecutive TC meetings. Our goal is to increase participation, not punish +people for any lack of participation, this guideline should be only be used as such +(replace an inactive member with a new active one, for example). Members who do not meet this +are expected to step down. If A TC member does not step down, an issue can be opened in the +discussions repo to move them to inactive status. TC members who step down or are removed due +to inactivity will be moved into inactive status. + +Inactive status members can become active members by self nomination if the TC is not already +larger than the maximum of 10. They will also be given preference if, while at max size, an +active member steps down. + +## Project Captains + +The Express TC can designate captains for individual projects/repos in the +organizations. These captains are responsible for being the primary +day-to-day maintainers of the repo on a technical and community front. +Repo captains are empowered with repo ownership and package publication rights. +When there are conflicts, especially on topics that effect the Express project +at large, captains are responsible to raise it up to the TC and drive +those conflicts to resolution. Captains are also responsible for making sure +community members follow the community guidelines, maintaining the repo +and the published package, as well as in providing user support. + +Like TC members, Repo captains are a subset of committers. + +To become a captain for a project the candidate is expected to participate in that +project for at least 6 months as a committer prior to the request. They should have +helped with code contributions as well as triaging issues. They are also required to +have 2FA enabled on both their GitHub and npm accounts. Any TC member or existing +captain on the repo can nominate another committer to the captain role, submit a PR to +this doc, under `Current Project Captains` section (maintaining the sort order) with +the project, their GitHub handle and npm username (if different). The PR will require +at least 2 approvals from TC members and 2 weeks hold time to allow for comment and/or +dissent. When the PR is merged, a TC member will add them to the proper GitHub/npm groups. + +### Active Projects and Captains + +- `expressjs/badgeboard`: @wesleytodd +- `expressjs/basic-auth-connect`: N/A +- `expressjs/body-parser`: @wesleytodd, @jonchurch +- `expressjs/compression`: N/A +- `expressjs/connect-multiparty`: N/A +- `expressjs/cookie-parser`: @wesleytodd, @UlisesGascon +- `expressjs/cookie-session`: N/A +- `expressjs/cors`: @jonchurch +- `expressjs/discussions`: @wesleytodd +- `expressjs/errorhandler`: N/A +- `expressjs/express-paginate`: N/A +- `expressjs/express`: @wesleytodd +- `expressjs/expressjs.com`: @crandmck, @jonchurch +- `expressjs/flash`: N/A +- `expressjs/generator`: @wesleytodd +- `expressjs/method-override`: N/A +- `expressjs/morgan`: @jonchurch +- `expressjs/multer`: @LinusU +- `expressjs/response-time`: @blakeembrey +- `expressjs/serve-favicon`: N/A +- `expressjs/serve-index`: N/A +- `expressjs/serve-static`: N/A +- `expressjs/session`: N/A +- `expressjs/statusboard`: @wesleytodd +- `expressjs/timeout`: N/A +- `expressjs/vhost`: N/A +- `jshttp/accepts`: @blakeembrey +- `jshttp/basic-auth`: @blakeembrey +- `jshttp/compressible`: @blakeembrey +- `jshttp/content-disposition`: @blakeembrey +- `jshttp/content-type`: @blakeembrey +- `jshttp/cookie`: @wesleytodd +- `jshttp/etag`: @blakeembrey +- `jshttp/forwarded`: @blakeembrey +- `jshttp/fresh`: @blakeembrey +- `jshttp/http-assert`: @wesleytodd, @jonchurch +- `jshttp/http-errors`: @wesleytodd, @jonchurch +- `jshttp/media-typer`: @blakeembrey +- `jshttp/methods`: @blakeembrey +- `jshttp/mime-db`: @blakeembrey, @UlisesGascon +- `jshttp/mime-types`: @blakeembrey, @UlisesGascon +- `jshttp/negotiator`: @blakeembrey +- `jshttp/on-finished`: @wesleytodd +- `jshttp/on-headers`: @blakeembrey +- `jshttp/proxy-addr`: @wesleytodd +- `jshttp/range-parser`: @blakeembrey +- `jshttp/statuses`: @blakeembrey +- `jshttp/type-is`: @blakeembrey +- `jshttp/vary`: @blakeembrey +- `pillarjs/cookies`: @blakeembrey +- `pillarjs/csrf`: N/A +- `pillarjs/encodeurl`: @blakeembrey +- `pillarjs/finalhandler`: @wesleytodd +- `pillarjs/hbs`: N/A +- `pillarjs/multiparty`: @blakeembrey +- `pillarjs/parseurl`: @blakeembrey +- `pillarjs/path-to-regexp`: @blakeembrey +- `pillarjs/request`: @wesleytodd +- `pillarjs/resolve-path`: @blakeembrey +- `pillarjs/router`: @blakeembrey +- `pillarjs/send`: @blakeembrey +- `pillarjs/understanding-csrf`: N/A + +### Current Initiative Captains + +- Triage team [ref](https://github.com/expressjs/discussions/issues/227): @UlisesGascon diff --git a/History.md b/History.md index e49870fed0b..178e718fc36 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,57 @@ +4.21.0 / 2024-09-11 +========== + + * Deprecate `res.location("back")` and `res.redirect("back")` magic string + * deps: serve-static@1.16.2 + * includes send@0.19.0 + * deps: finalhandler@1.3.1 + * deps: qs@6.13.0 + +4.20.0 / 2024-09-10 +========== + * deps: serve-static@0.16.0 + * Remove link renderization in html while redirecting + * deps: send@0.19.0 + * Remove link renderization in html while redirecting + * deps: body-parser@0.6.0 + * add `depth` option to customize the depth level in the parser + * IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`) + * Remove link renderization in html while using `res.redirect` + * deps: path-to-regexp@0.1.10 + - Adds support for named matching groups in the routes using a regex + - Adds backtracking protection to parameters without regexes defined + * deps: encodeurl@~2.0.0 + - Removes encoding of `\`, `|`, and `^` to align better with URL spec + * Deprecate passing `options.maxAge` and `options.expires` to `res.clearCookie` + - Will be ignored in v5, clearCookie will set a cookie with an expires in the past to instruct clients to delete the cookie + +4.19.2 / 2024-03-25 +========== + + * Improved fix for open redirect allow list bypass + +4.19.1 / 2024-03-20 +========== + + * Allow passing non-strings to res.location with new encoding handling checks + +4.19.0 / 2024-03-20 +========== + + * Prevent open redirect allow list bypass due to encodeurl + * deps: cookie@0.6.0 + +4.18.3 / 2024-02-29 +========== + + * Fix routing requests without method + * deps: body-parser@1.20.2 + - Fix strict json error message on Node.js 19+ + - deps: content-type@~1.0.5 + - deps: raw-body@2.5.2 + * deps: cookie@0.6.0 + - Add `partitioned` option + 4.18.2 / 2022-10-08 =================== @@ -2111,7 +2165,7 @@ * deps: connect@2.21.0 - deprecate `connect(middleware)` -- use `app.use(middleware)` instead - deprecate `connect.createServer()` -- use `connect()` instead - - fix `res.setHeader()` patch to work with with get -> append -> set pattern + - fix `res.setHeader()` patch to work with get -> append -> set pattern - deps: compression@~1.0.8 - deps: errorhandler@~1.1.1 - deps: express-session@~1.5.0 @@ -3322,8 +3376,8 @@ Shaw] * Added node v0.1.97 compatibility * Added support for deleting cookies via Request#cookie('key', null) * Updated haml submodule - * Fixed not-found page, now using using charset utf-8 - * Fixed show-exceptions page, now using using charset utf-8 + * Fixed not-found page, now using charset utf-8 + * Fixed show-exceptions page, now using charset utf-8 * Fixed view support due to fs.readFile Buffers * Changed; mime.type() no longer accepts ".type" due to node extname() changes @@ -3358,7 +3412,7 @@ Shaw] ================== * Added charset support via Request#charset (automatically assigned to 'UTF-8' when respond()'s - encoding is set to 'utf8' or 'utf-8'. + encoding is set to 'utf8' or 'utf-8'). * Added "encoding" option to Request#render(). Closes #299 * Added "dump exceptions" setting, which is enabled by default. * Added simple ejs template engine support @@ -3397,7 +3451,7 @@ Shaw] * Added [haml.js](http://github.com/visionmedia/haml.js) submodule; removed haml-js * Added callback function support to Request#halt() as 3rd/4th arg * Added preprocessing of route param wildcards using param(). Closes #251 - * Added view partial support (with collections etc) + * Added view partial support (with collections etc.) * Fixed bug preventing falsey params (such as ?page=0). Closes #286 * Fixed setting of multiple cookies. Closes #199 * Changed; view naming convention is now NAME.TYPE.ENGINE (for example page.html.haml) diff --git a/Readme.md b/Readme.md index 0936816bedb..bc108d55fc0 100644 --- a/Readme.md +++ b/Readme.md @@ -1,10 +1,29 @@ [![Express Logo](https://i.cloudup.com/zfY6lL7eFa-3000x3000.png)](http://expressjs.com/) - Fast, unopinionated, minimalist web framework for [Node.js](http://nodejs.org). +**Fast, unopinionated, minimalist web framework for [Node.js](http://nodejs.org).** + +**This project has a [Code of Conduct][].** + +## Table of contents + +* [Installation](#Installation) +* [Features](#Features) +* [Docs & Community](#docs--community) +* [Quick Start](#Quick-Start) +* [Running Tests](#Running-Tests) +* [Philosophy](#Philosophy) +* [Examples](#Examples) +* [Contributing to Express](#Contributing) +* [TC (Technical Committee)](#tc-technical-committee) +* [Triagers](#triagers) +* [License](#license) + + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Install Size][npm-install-size-image]][npm-install-size-url] +[![NPM Downloads][npm-downloads-image]][npm-downloads-url] +[![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer] - [![NPM Version][npm-version-image]][npm-url] - [![NPM Install Size][npm-install-size-image]][npm-install-size-url] - [![NPM Downloads][npm-downloads-image]][npm-downloads-url] ```js const express = require('express') @@ -104,7 +123,7 @@ $ npm start To view the examples, clone the Express repo and install the dependencies: ```console -$ git clone git://github.com/expressjs/express.git --depth 1 +$ git clone https://github.com/expressjs/express.git --depth 1 $ cd express $ npm install ``` @@ -144,10 +163,82 @@ $ npm test The original author of Express is [TJ Holowaychuk](https://github.com/tj) -The current lead maintainer is [Douglas Christopher Wilson](https://github.com/dougwilson) - [List of all contributors](https://github.com/expressjs/express/graphs/contributors) +### TC (Technical Committee) + +* [UlisesGascon](https://github.com/UlisesGascon) - **Ulises Gascón** (he/him) +* [jonchurch](https://github.com/jonchurch) - **Jon Church** +* [wesleytodd](https://github.com/wesleytodd) - **Wes Todd** +* [LinusU](https://github.com/LinusU) - **Linus Unnebäck** +* [blakeembrey](https://github.com/blakeembrey) - **Blake Embrey** +* [sheplu](https://github.com/sheplu) - **Jean Burellier** +* [crandmck](https://github.com/crandmck) - **Rand McKinney** +* [ctcpip](https://github.com/ctcpip) - **Chris de Almeida** + +
+TC emeriti members + +#### TC emeriti members + + * [dougwilson](https://github.com/dougwilson) - **Douglas Wilson** + * [hacksparrow](https://github.com/hacksparrow) - **Hage Yaapa** + * [jonathanong](https://github.com/jonathanong) - **jongleberry** + * [niftylettuce](https://github.com/niftylettuce) - **niftylettuce** + * [troygoode](https://github.com/troygoode) - **Troy Goode** +
+ + +### Triagers + +* [aravindvnair99](https://github.com/aravindvnair99) - **Aravind Nair** +* [carpasse](https://github.com/carpasse) - **Carlos Serrano** +* [CBID2](https://github.com/CBID2) - **Christine Belzie** +* [enyoghasim](https://github.com/enyoghasim) - **David Enyoghasim** +* [UlisesGascon](https://github.com/UlisesGascon) - **Ulises Gascón** (he/him) +* [mertcanaltin](https://github.com/mertcanaltin) - **Mert Can Altin** +* [0ss](https://github.com/0ss) - **Salah** +* [import-brain](https://github.com/import-brain) - **Eric Cheng** (he/him) +* [3imed-jaberi](https://github.com/3imed-jaberi) - **Imed Jaberi** +* [dakshkhetan](https://github.com/dakshkhetan) - **Daksh Khetan** (he/him) +* [lucasraziel](https://github.com/lucasraziel) - **Lucas Soares Do Rego** +* [IamLizu](https://github.com/IamLizu) - **S M Mahmudul Hasan** (he/him) +* [Sushmeet](https://github.com/Sushmeet) - **Sushmeet Sunger** + +
+Triagers emeriti members + +#### Emeritus Triagers + + * [AuggieH](https://github.com/AuggieH) - **Auggie Hudak** + * [G-Rath](https://github.com/G-Rath) - **Gareth Jones** + * [MohammadXroid](https://github.com/MohammadXroid) - **Mohammad Ayashi** + * [NawafSwe](https://github.com/NawafSwe) - **Nawaf Alsharqi** + * [NotMoni](https://github.com/NotMoni) - **Moni** + * [VigneshMurugan](https://github.com/VigneshMurugan) - **Vignesh Murugan** + * [davidmashe](https://github.com/davidmashe) - **David Ashe** + * [digitaIfabric](https://github.com/digitaIfabric) - **David** + * [e-l-i-s-e](https://github.com/e-l-i-s-e) - **Elise Bonner** + * [fed135](https://github.com/fed135) - **Frederic Charette** + * [firmanJS](https://github.com/firmanJS) - **Firman Abdul Hakim** + * [getspooky](https://github.com/getspooky) - **Yasser Ameur** + * [ghinks](https://github.com/ghinks) - **Glenn** + * [ghousemohamed](https://github.com/ghousemohamed) - **Ghouse Mohamed** + * [gireeshpunathil](https://github.com/gireeshpunathil) - **Gireesh Punathil** + * [jake32321](https://github.com/jake32321) - **Jake Reed** + * [jonchurch](https://github.com/jonchurch) - **Jon Church** + * [lekanikotun](https://github.com/lekanikotun) - **Troy Goode** + * [marsonya](https://github.com/marsonya) - **Lekan Ikotun** + * [mastermatt](https://github.com/mastermatt) - **Matt R. Wilson** + * [maxakuru](https://github.com/maxakuru) - **Max Edell** + * [mlrawlings](https://github.com/mlrawlings) - **Michael Rawlings** + * [rodion-arr](https://github.com/rodion-arr) - **Rodion Abdurakhimov** + * [sheplu](https://github.com/sheplu) - **Jean Burellier** + * [tarunyadav1](https://github.com/tarunyadav1) - **Tarun yadav** + * [tunniclm](https://github.com/tunniclm) - **Mike Tunnicliffe** +
+ + ## License [MIT](LICENSE) @@ -164,3 +255,6 @@ The current lead maintainer is [Douglas Christopher Wilson](https://github.com/d [npm-install-size-url]: https://packagephobia.com/result?p=express [npm-url]: https://npmjs.org/package/express [npm-version-image]: https://badgen.net/npm/v/express +[ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/express/badge +[ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/express +[Code of Conduct]: https://github.com/expressjs/express/blob/master/Code-Of-Conduct.md diff --git a/Release-Process.md b/Release-Process.md index ae740972f77..9ca0a15ab46 100644 --- a/Release-Process.md +++ b/Release-Process.md @@ -77,6 +77,13 @@ non-patch flow. - This branch contains the commits accepted so far that implement the proposal in the tracking pull request. +### Pre-release Versions + +Alpha and Beta releases are made from a proposal branch. The version number should be +incremented to the next minor version with a `-beta` or `-alpha` suffix. +For example, if the next beta release is `5.0.1`, the beta release would be `5.0.1-beta.0`. +The pre-releases are unstable and not suitable for production use. + ### Patch flow In the patch flow, simple changes are committed to the release branch which @@ -184,3 +191,9 @@ $ npm publish **NOTE:** The version number to publish will be picked up automatically from package.json. + +### Step 7. Update documentation website + +The documentation website https://expressjs.com/ documents the current release version in various places. For a new release: +1. Change the value of `current_version` in https://github.com/expressjs/expressjs.com/blob/gh-pages/_data/express.yml to match the latest version number. +2. Add a new section to the change log. For example, for a 4.x release, https://github.com/expressjs/expressjs.com/blob/gh-pages/en/changelog/4x.md, diff --git a/Security.md b/Security.md index cdcd7a6e0aa..dcfbe88abd4 100644 --- a/Security.md +++ b/Security.md @@ -29,6 +29,12 @@ announcement, and may ask for additional information or guidance. Report security bugs in third-party modules to the person or team maintaining the module. +## Pre-release Versions + +Alpha and Beta releases are unstable and **not suitable for production use**. +Vulnerabilities found in pre-releases should be reported according to the [Reporting a Bug](#reporting-a-bug) section. +Due to the unstable nature of the branch it is not guaranteed that any fixes will be released in the next pre-release. + ## Disclosure Policy When the security team receives a security bug report, they will assign it to a @@ -40,6 +46,10 @@ involving the following steps: * Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible to npm. +## The Express Threat Model + +We are currently working on a new version of the security model, the most updated version can be found [here](https://github.com/expressjs/security-wg/blob/main/docs/ThreatModel.md) + ## Comments on this Policy If you have suggestions on how this process could be improved please submit a diff --git a/Triager-Guide.md b/Triager-Guide.md index a2909ef30db..c15e6be5313 100644 --- a/Triager-Guide.md +++ b/Triager-Guide.md @@ -9,11 +9,18 @@ classification: * `needs triage`: This can be kept if the triager is unsure which next steps to take * `awaiting more info`: If more info has been requested from the author, apply this label. -* `question`: User questions that do not appear to be bugs or enhancements. -* `discuss`: Topics for discussion. Might end in an `enhancement` or `question` label. * `bug`: Issues that present a reasonable conviction there is a reproducible bug. * `enhancement`: Issues that are found to be a reasonable candidate feature additions. +If the issue is a question or discussion, it should be moved to GitHub Discussions. + +### Moving Discussions and Questions to GitHub Discussions + +For issues labeled with `question` or `discuss`, it is recommended to move them to GitHub Discussions instead: + +* **Questions**: User questions that do not appear to be bugs or enhancements should be moved to GitHub Discussions. +* **Discussions**: Topics for discussion should be moved to GitHub Discussions. If the discussion leads to a new feature or bug identification, it can be moved back to Issues. + In all cases, issues may be closed by maintainers if they don't receive a timely response when further information is sought, or when additional questions are asked. diff --git a/appveyor.yml b/appveyor.yml index 1fca21801e8..629499bf37c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,9 +17,13 @@ environment: - nodejs_version: "13.14" - nodejs_version: "14.20" - nodejs_version: "15.14" - - nodejs_version: "16.17" + - nodejs_version: "16.20" - nodejs_version: "17.9" - - nodejs_version: "18.10" + - nodejs_version: "18.19" + - nodejs_version: "19.9" + - nodejs_version: "20.11" + - nodejs_version: "21.6" + - nodejs_version: "22.0" cache: - node_modules install: @@ -30,7 +34,11 @@ install: # Configure npm - ps: | npm config set loglevel error - npm config set shrinkwrap false + if ((npm config get package-lock) -eq "true") { + npm config set package-lock false + } else { + npm config set shrinkwrap false + } # Remove all non-test dependencies - ps: | # Remove example dependencies @@ -65,12 +73,12 @@ install: # nyc for test coverage # - use 10.3.2 for Node.js < 4 # - use 11.9.0 for Node.js < 6 - # - use 14.1.1 for Node.js < 8 + # - use 14.1.1 for Node.js < 10 if ([int]$env:nodejs_version.split(".")[0] -lt 4) { npm install --silent --save-dev nyc@10.3.2 } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) { npm install --silent --save-dev nyc@11.9.0 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) { + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 10) { npm install --silent --save-dev nyc@14.1.1 } - ps: | diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 00000000000..1894c527d63 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,34 @@ +# Express Benchmarks + +## Installation + +You will need to install [wrk](https://github.com/wg/wrk/blob/master/INSTALL) in order to run the benchmarks. + +## Running + +To run the benchmarks, first install the dependencies `npm i`, then run `make` + +The output will look something like this: + +``` + 50 connections + 1 middleware + 7.15ms + 6784.01 + + [...redacted...] + + 1000 connections + 10 middleware + 139.21ms + 6155.19 + +``` + +### Tip: Include Node.js version in output + +You can use `make && node -v` to include the node.js version in the output. + +### Tip: Save the results to a file + +You can use `make > results.log` to save the results to a file `results.log`. diff --git a/examples/README.md b/examples/README.md index c19ed30a25c..bd1f1f6310f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,7 +13,6 @@ This page contains list of examples using Express. - [hello-world](./hello-world) - Simple request handler - [markdown](./markdown) - Markdown as template engine - [multi-router](./multi-router) - Working with multiple Express routers -- [multipart](./multipart) - Accepting multipart-encoded forms - [mvc](./mvc) - MVC-style controllers - [online](./online) - Tracking online user activity with `online` and `redis` packages - [params](./params) - Working with route parameters diff --git a/examples/cookie-sessions/index.js b/examples/cookie-sessions/index.js index 01c731c1c8f..83b6faee82b 100644 --- a/examples/cookie-sessions/index.js +++ b/examples/cookie-sessions/index.js @@ -13,13 +13,10 @@ var app = module.exports = express(); app.use(cookieSession({ secret: 'manny is cool' })); // do something with the session -app.use(count); - -// custom middleware -function count(req, res) { +app.get('/', function (req, res) { req.session.count = (req.session.count || 0) + 1 res.send('viewed ' + req.session.count + ' times\n') -} +}) /* istanbul ignore next */ if (!module.parent) { diff --git a/examples/error/index.js b/examples/error/index.js index d922de06cc4..d733a81172d 100644 --- a/examples/error/index.js +++ b/examples/error/index.js @@ -26,7 +26,7 @@ function error(err, req, res, next) { res.send('Internal Server Error'); } -app.get('/', function(req, res){ +app.get('/', function () { // Caught and passed down to the errorHandler middleware throw new Error('something broke!'); }); diff --git a/examples/markdown/index.js b/examples/markdown/index.js index 74ac05e77f3..23d645e66b2 100644 --- a/examples/markdown/index.js +++ b/examples/markdown/index.js @@ -26,7 +26,7 @@ app.engine('md', function(path, options, fn){ app.set('views', path.join(__dirname, 'views')); -// make it the default so we dont need .md +// make it the default, so we don't need .md app.set('view engine', 'md'); app.get('/', function(req, res){ diff --git a/examples/multipart/index.js b/examples/multipart/index.js deleted file mode 100644 index ea7b86e0c94..00000000000 --- a/examples/multipart/index.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict' - -/** - * Module dependencies. - */ - -var express = require('../..'); -var multiparty = require('multiparty'); -var format = require('util').format; - -var app = module.exports = express(); - -app.get('/', function(req, res){ - res.send('
' - + '

Title:

' - + '

Image:

' - + '

' - + '
'); -}); - -app.post('/', function(req, res, next){ - // create a form to begin parsing - var form = new multiparty.Form(); - var image; - var title; - - form.on('error', next); - form.on('close', function(){ - res.send(format('\nuploaded %s (%d Kb) as %s' - , image.filename - , image.size / 1024 | 0 - , title)); - }); - - // listen on field event for title - form.on('field', function(name, val){ - if (name !== 'title') return; - title = val; - }); - - // listen on part event for image file - form.on('part', function(part){ - if (!part.filename) return; - if (part.name !== 'image') return part.resume(); - image = {}; - image.filename = part.filename; - image.size = 0; - part.on('data', function(buf){ - image.size += buf.length; - }); - }); - - - // parse the form - form.parse(req); -}); - -/* istanbul ignore next */ -if (!module.parent) { - app.listen(4000); - console.log('Express started on port 4000'); -} diff --git a/examples/view-locals/index.js b/examples/view-locals/index.js index bf52d2a85ad..a2af24f3553 100644 --- a/examples/view-locals/index.js +++ b/examples/view-locals/index.js @@ -61,7 +61,7 @@ function users(req, res, next) { }) } -app.get('/middleware', count, users, function(req, res, next){ +app.get('/middleware', count, users, function (req, res) { res.render('index', { title: 'Users', count: req.count, @@ -99,7 +99,7 @@ function users2(req, res, next) { }) } -app.get('/middleware-locals', count2, users2, function(req, res, next){ +app.get('/middleware-locals', count2, users2, function (req, res) { // you can see now how we have much less // to pass to res.render(). If we have // several routes related to users this diff --git a/examples/web-service/index.js b/examples/web-service/index.js index a2cd2cb7f90..d1a90362153 100644 --- a/examples/web-service/index.js +++ b/examples/web-service/index.js @@ -72,12 +72,12 @@ var userRepos = { // and simply expose the data // example: http://localhost:3000/api/users/?api-key=foo -app.get('/api/users', function(req, res, next){ +app.get('/api/users', function (req, res) { res.send(users); }); // example: http://localhost:3000/api/repos/?api-key=foo -app.get('/api/repos', function(req, res, next){ +app.get('/api/repos', function (req, res) { res.send(repos); }); diff --git a/lib/response.js b/lib/response.js index fede486c06d..2b654f4c662 100644 --- a/lib/response.js +++ b/lib/response.js @@ -822,6 +822,14 @@ res.get = function(field){ */ res.clearCookie = function clearCookie(name, options) { + if (options) { + if (options.maxAge) { + deprecate('res.clearCookie: Passing "options.maxAge" is deprecated. In v5.0.0 of Express, this option will be ignored, as res.clearCookie will automatically set cookies to expire immediately. Please update your code to omit this option.'); + } + if (options.expires) { + deprecate('res.clearCookie: Passing "options.expires" is deprecated. In v5.0.0 of Express, this option will be ignored, as res.clearCookie will automatically set cookies to expire immediately. Please update your code to omit this option.'); + } + } var opts = merge({ expires: new Date(1), path: '/' }, options); return this.cookie(name, '', opts); @@ -904,14 +912,16 @@ res.cookie = function (name, value, options) { */ res.location = function location(url) { - var loc = url; + var loc; // "back" is an alias for the referrer if (url === 'back') { + deprecate('res.location("back"): use res.location(req.get("Referrer") || "/") and refer to https://dub.sh/security-redirect for best practices'); loc = this.req.get('Referrer') || '/'; + } else { + loc = String(url); } - // set location return this.set('Location', encodeUrl(loc)); }; @@ -960,7 +970,7 @@ res.redirect = function redirect(url) { html: function(){ var u = escapeHtml(address); - body = '

' + statuses.message[status] + '. Redirecting to ' + u + '

' + body = '

' + statuses.message[status] + '. Redirecting to ' + u + '

' }, default: function(){ diff --git a/lib/router/index.js b/lib/router/index.js index 5174c34f455..abb3a6f589e 100644 --- a/lib/router/index.js +++ b/lib/router/index.js @@ -36,7 +36,7 @@ var toString = Object.prototype.toString; * Initialize a new `Router` with the given `options`. * * @param {Object} [options] - * @return {Router} which is an callable function + * @return {Router} which is a callable function * @public */ diff --git a/lib/router/route.js b/lib/router/route.js index cc643ac8bdb..a65756d6de6 100644 --- a/lib/router/route.js +++ b/lib/router/route.js @@ -60,7 +60,10 @@ Route.prototype._handles_method = function _handles_method(method) { return true; } - var name = method.toLowerCase(); + // normalize name + var name = typeof method === 'string' + ? method.toLowerCase() + : method if (name === 'head' && !this.methods['head']) { name = 'get'; @@ -103,8 +106,10 @@ Route.prototype.dispatch = function dispatch(req, res, done) { if (stack.length === 0) { return done(); } + var method = typeof req.method === 'string' + ? req.method.toLowerCase() + : req.method - var method = req.method.toLowerCase(); if (method === 'head' && !this.methods['head']) { method = 'get'; } diff --git a/lib/utils.js b/lib/utils.js index 799a6a2b4ee..56e12b9b541 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -117,17 +117,15 @@ exports.contentDisposition = deprecate.function(contentDisposition, /** * Parse accept params `str` returning an * object with `.value`, `.quality` and `.params`. - * also includes `.originalIndex` for stable sorting * * @param {String} str - * @param {Number} index * @return {Object} * @api private */ -function acceptParams(str, index) { +function acceptParams (str) { var parts = str.split(/ *; */); - var ret = { value: parts[0], quality: 1, params: {}, originalIndex: index }; + var ret = { value: parts[0], quality: 1, params: {} } for (var i = 1; i < parts.length; ++i) { var pms = parts[i].split(/ *= */); @@ -282,6 +280,7 @@ function createETagGenerator (options) { /** * Parse an extended query string with qs. * + * @param {String} str * @return {Object} * @private */ diff --git a/package.json b/package.json index 0996637deaa..f9b43a69e5a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "express", "description": "Fast, unopinionated, minimalist web framework", - "version": "4.18.2", + "version": "4.21.0", "author": "TJ Holowaychuk ", "contributors": [ "Aaron Heckmann ", @@ -30,30 +30,30 @@ "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -65,15 +65,14 @@ "connect-redis": "3.4.2", "cookie-parser": "1.4.6", "cookie-session": "2.0.0", - "ejs": "3.1.8", - "eslint": "8.24.0", + "ejs": "3.1.9", + "eslint": "8.47.0", "express-session": "1.17.2", "hbs": "4.2.0", "marked": "0.7.0", "method-override": "3.0.0", - "mocha": "10.0.0", + "mocha": "10.2.0", "morgan": "1.10.0", - "multiparty": "4.2.3", "nyc": "15.1.0", "pbkdf2-password": "1.2.1", "supertest": "6.3.0", @@ -92,8 +91,8 @@ "scripts": { "lint": "eslint .", "test": "mocha --require test/support/env --reporter spec --bail --check-leaks test/ test/acceptance/", - "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", - "test-cov": "nyc --reporter=html --reporter=text npm test", + "test-ci": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=html --reporter=text npm test", "test-tap": "mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/" } } diff --git a/test/Route.js b/test/Route.js index 64dbad60ce9..2a37b9a4839 100644 --- a/test/Route.js +++ b/test/Route.js @@ -124,7 +124,7 @@ describe('Route', function(){ var req = { method: 'POST', url: '/' }; var route = new Route(''); - route.get(function(req, res, next) { + route.get(function () { throw new Error('not me!'); }) @@ -198,7 +198,7 @@ describe('Route', function(){ var req = { order: '', method: 'GET', url: '/' }; var route = new Route(''); - route.all(function(req, res, next){ + route.all(function () { throw new Error('foobar'); }); @@ -224,7 +224,7 @@ describe('Route', function(){ var req = { method: 'GET', url: '/' }; var route = new Route(''); - route.get(function(req, res, next){ + route.get(function () { throw new Error('boom!'); }); diff --git a/test/Router.js b/test/Router.js index 0d0502ab40d..b22001a9ff2 100644 --- a/test/Router.js +++ b/test/Router.js @@ -61,6 +61,33 @@ describe('Router', function(){ router.handle({ method: 'GET' }, {}, done) }) + it('handle missing method', function (done) { + var all = false + var router = new Router() + var route = router.route('/foo') + var use = false + + route.post(function (req, res, next) { next(new Error('should not run')) }) + route.all(function (req, res, next) { + all = true + next() + }) + route.get(function (req, res, next) { next(new Error('should not run')) }) + + router.get('/foo', function (req, res, next) { next(new Error('should not run')) }) + router.use(function (req, res, next) { + use = true + next() + }) + + router.handle({ url: '/foo' }, {}, function (err) { + if (err) return done(err) + assert.ok(all) + assert.ok(use) + done() + }) + }) + it('should not stack overflow with many registered routes', function(done){ this.timeout(5000) // long-running test @@ -201,7 +228,7 @@ describe('Router', function(){ it('should handle throwing inside routes with params', function(done) { var router = new Router(); - router.get('/foo/:id', function(req, res, next){ + router.get('/foo/:id', function () { throw new Error('foo'); }); @@ -579,8 +606,8 @@ describe('Router', function(){ var req2 = { url: '/foo/10/bar', method: 'get' }; var router = new Router(); var sub = new Router(); + var cb = after(2, done) - done = after(2, done); sub.get('/bar', function(req, res, next) { next(); @@ -599,14 +626,14 @@ describe('Router', function(){ assert.ifError(err); assert.equal(req1.ms, 50); assert.equal(req1.originalUrl, '/foo/50/bar'); - done(); + cb() }); router.handle(req2, {}, function(err) { assert.ifError(err); assert.equal(req2.ms, 10); assert.equal(req2.originalUrl, '/foo/10/bar'); - done(); + cb() }); }); }); diff --git a/test/app.listen.js b/test/app.listen.js index 08eeaaa63c2..5b150063b9e 100644 --- a/test/app.listen.js +++ b/test/app.listen.js @@ -6,9 +6,8 @@ describe('app.listen()', function(){ it('should wrap with an HTTP server', function(done){ var app = express(); - var server = app.listen(9999, function(){ - server.close(); - done(); + var server = app.listen(0, function () { + server.close(done) }); }) }) diff --git a/test/app.param.js b/test/app.param.js index 8893851f9d5..b4ccc8a2d12 100644 --- a/test/app.param.js +++ b/test/app.param.js @@ -166,7 +166,7 @@ describe('app', function(){ app.get('/:user', function(req, res, next) { next('route'); }); - app.get('/:user', function(req, res, next) { + app.get('/:user', function (req, res) { res.send(req.params.user); }); @@ -187,11 +187,11 @@ describe('app', function(){ next(new Error('invalid invocation')) }); - app.post('/:user', function(req, res, next) { + app.post('/:user', function (req, res) { res.send(req.params.user); }); - app.get('/:thing', function(req, res, next) { + app.get('/:thing', function (req, res) { res.send(req.thing); }); diff --git a/test/app.router.js b/test/app.router.js index 3069a22c772..8e427bd6dc7 100644 --- a/test/app.router.js +++ b/test/app.router.js @@ -6,6 +6,8 @@ var express = require('../') , assert = require('assert') , methods = require('methods'); +var shouldSkipQuery = require('./support/utils').shouldSkipQuery + describe('app.router', function(){ it('should restore req.params after leaving router', function(done){ var app = express(); @@ -39,6 +41,9 @@ describe('app.router', function(){ if (method === 'connect') return; it('should include ' + method.toUpperCase(), function(done){ + if (method === 'query' && shouldSkipQuery(process.versions.node)) { + this.skip() + } var app = express(); app[method]('/foo', function(req, res){ @@ -90,7 +95,7 @@ describe('app.router', function(){ it('should decode correct params', function(done){ var app = express(); - app.get('/:name', function(req, res, next){ + app.get('/:name', function (req, res) { res.send(req.params.name); }); @@ -102,7 +107,7 @@ describe('app.router', function(){ it('should not accept params in malformed paths', function(done) { var app = express(); - app.get('/:name', function(req, res, next){ + app.get('/:name', function (req, res) { res.send(req.params.name); }); @@ -114,7 +119,7 @@ describe('app.router', function(){ it('should not decode spaces', function(done) { var app = express(); - app.get('/:name', function(req, res, next){ + app.get('/:name', function (req, res) { res.send(req.params.name); }); @@ -126,7 +131,7 @@ describe('app.router', function(){ it('should work with unicode', function(done) { var app = express(); - app.get('/:name', function(req, res, next){ + app.get('/:name', function (req, res) { res.send(req.params.name); }); @@ -188,6 +193,23 @@ describe('app.router', function(){ .expect('editing user 10', done); }) + if (supportsRegexp('(?.*)')) { + it('should populate req.params with named captures', function(done){ + var app = express(); + var re = new RegExp('^/user/(?[0-9]+)/(view|edit)?$'); + + app.get(re, function(req, res){ + var id = req.params.userId + , op = req.params[0]; + res.end(op + 'ing user ' + id); + }); + + request(app) + .get('/user/10/edit') + .expect('editing user 10', done); + }) + } + it('should ensure regexp matches path prefix', function (done) { var app = express() var p = [] @@ -896,7 +918,7 @@ describe('app.router', function(){ request(app) .get('/foo.json') - .expect(200, 'foo as json', done) + .expect(200, 'foo as json', cb) }) }) @@ -910,7 +932,7 @@ describe('app.router', function(){ next(); }); - app.get('/bar', function(req, res){ + app.get('/bar', function () { assert(0); }); @@ -919,7 +941,7 @@ describe('app.router', function(){ next(); }); - app.get('/foo', function(req, res, next){ + app.get('/foo', function (req, res) { calls.push('/foo 2'); res.json(calls) }); @@ -939,7 +961,7 @@ describe('app.router', function(){ next('route') } - app.get('/foo', fn, function(req, res, next){ + app.get('/foo', fn, function (req, res) { res.end('failure') }); @@ -964,11 +986,11 @@ describe('app.router', function(){ next('router') } - router.get('/foo', fn, function (req, res, next) { + router.get('/foo', fn, function (req, res) { res.end('failure') }) - router.get('/foo', function (req, res, next) { + router.get('/foo', function (req, res) { res.end('failure') }) @@ -995,7 +1017,7 @@ describe('app.router', function(){ next(); }); - app.get('/bar', function(req, res){ + app.get('/bar', function () { assert(0); }); @@ -1004,7 +1026,7 @@ describe('app.router', function(){ next(new Error('fail')); }); - app.get('/foo', function(req, res, next){ + app.get('/foo', function () { assert(0); }); @@ -1109,3 +1131,12 @@ describe('app.router', function(){ assert.strictEqual(app.get('/', function () {}), app) }) }) + +function supportsRegexp(source) { + try { + new RegExp(source) + return true + } catch (e) { + return false + } +} diff --git a/test/app.use.js b/test/app.use.js index fd9b1751a31..1de3275c8e2 100644 --- a/test/app.use.js +++ b/test/app.use.js @@ -57,7 +57,7 @@ describe('app', function(){ request(app) .get('/forum') - .expect(200, 'forum', done) + .expect(200, 'forum', cb) }) it('should set the child\'s .parent', function(){ diff --git a/test/express.json.js b/test/express.json.js index a8cfebc41e2..f6f536b15e5 100644 --- a/test/express.json.js +++ b/test/express.json.js @@ -262,7 +262,7 @@ describe('express.json()', function () { .post('/') .set('Content-Type', 'application/json') .send('true') - .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace('#', 't'), done) + .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, 't'), done) }) }) @@ -290,7 +290,7 @@ describe('express.json()', function () { .post('/') .set('Content-Type', 'application/json') .send('true') - .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace('#', 't'), done) + .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, 't'), done) }) it('should not parse primitives with leading whitespaces', function (done) { @@ -298,7 +298,7 @@ describe('express.json()', function () { .post('/') .set('Content-Type', 'application/json') .send(' true') - .expect(400, '[entity.parse.failed] ' + parseError(' #rue').replace('#', 't'), done) + .expect(400, '[entity.parse.failed] ' + parseError(' #rue').replace(/#/g, 't'), done) }) it('should allow leading whitespaces in JSON', function (done) { @@ -316,7 +316,7 @@ describe('express.json()', function () { .set('X-Error-Property', 'stack') .send('true') .expect(400) - .expect(shouldContainInBody(parseError('#rue').replace('#', 't'))) + .expect(shouldContainInBody(parseError('#rue').replace(/#/g, 't'))) .end(done) }) }) diff --git a/test/express.static.js b/test/express.static.js index 245fd5929cc..23e607ed933 100644 --- a/test/express.static.js +++ b/test/express.static.js @@ -486,7 +486,7 @@ describe('express.static()', function () { request(this.app) .get('/users') .expect('Location', '/users/') - .expect(301, //, done) + .expect(301, /\/users\//, done) }) it('should redirect directories with query string', function (done) { @@ -508,7 +508,7 @@ describe('express.static()', function () { .get('/snow') .expect('Location', '/snow%20%E2%98%83/') .expect('Content-Type', /html/) - .expect(301, />Redirecting to \/snow%20%E2%98%83\/<\/a>Redirecting to \/snow%20%E2%98%83\/hey

') }, @@ -155,7 +155,7 @@ describe('res', function(){ var app = express(); var router = express.Router(); - router.get('/', function(req, res, next){ + router.get('/', function (req, res) { res.format({ text: function(){ res.send('hey') }, html: function(){ res.send('

hey

') }, diff --git a/test/res.location.js b/test/res.location.js index 158afac01e7..2e88002625f 100644 --- a/test/res.location.js +++ b/test/res.location.js @@ -1,7 +1,9 @@ 'use strict' var express = require('../') - , request = require('supertest'); + , request = require('supertest') + , assert = require('assert') + , url = require('url'); describe('res', function(){ describe('.location(url)', function(){ @@ -9,38 +11,38 @@ describe('res', function(){ var app = express(); app.use(function(req, res){ - res.location('http://google.com').end(); + res.location('http://google.com/').end(); }); request(app) .get('/') - .expect('Location', 'http://google.com') + .expect('Location', 'http://google.com/') .expect(200, done) }) - it('should encode "url"', function (done) { - var app = express() + it('should preserve trailing slashes when not present', function(done){ + var app = express(); - app.use(function (req, res) { - res.location('https://google.com?q=\u2603 §10').end() - }) + app.use(function(req, res){ + res.location('http://google.com').end(); + }); request(app) .get('/') - .expect('Location', 'https://google.com?q=%E2%98%83%20%C2%A710') + .expect('Location', 'http://google.com') .expect(200, done) }) - it('should not touch already-encoded sequences in "url"', function (done) { + it('should encode "url"', function (done) { var app = express() app.use(function (req, res) { - res.location('https://google.com?q=%A710').end() + res.location('https://google.com?q=\u2603 §10').end() }) request(app) .get('/') - .expect('Location', 'https://google.com?q=%A710') + .expect('Location', 'https://google.com?q=%E2%98%83%20%C2%A710') .expect(200, done) }) @@ -101,5 +103,260 @@ describe('res', function(){ .expect(200, done) }) }) + + it('should encode data uri', function (done) { + var app = express() + app.use(function (req, res) { + res.location('data:text/javascript,export default () => { }').end(); + }); + + request(app) + .get('/') + .expect('Location', 'data:text/javascript,export%20default%20()%20=%3E%20%7B%20%7D') + .expect(200, done) + }) + + it('should consistently handle non-string input: boolean', function (done) { + var app = express() + app.use(function (req, res) { + res.location(true).end(); + }); + + request(app) + .get('/') + .expect('Location', 'true') + .expect(200, done) + }); + + it('should consistently handle non-string inputs: object', function (done) { + var app = express() + app.use(function (req, res) { + res.location({}).end(); + }); + + request(app) + .get('/') + .expect('Location', '[object%20Object]') + .expect(200, done) + }); + + it('should consistently handle non-string inputs: array', function (done) { + var app = express() + app.use(function (req, res) { + res.location([]).end(); + }); + + request(app) + .get('/') + .expect('Location', '') + .expect(200, done) + }); + + it('should consistently handle empty string input', function (done) { + var app = express() + app.use(function (req, res) { + res.location('').end(); + }); + + request(app) + .get('/') + .expect('Location', '') + .expect(200, done) + }); + + + if (typeof URL !== 'undefined') { + it('should accept an instance of URL', function (done) { + var app = express(); + + app.use(function(req, res){ + res.location(new URL('http://google.com/')).end(); + }); + + request(app) + .get('/') + .expect('Location', 'http://google.com/') + .expect(200, done); + }); + } }) + + describe('location header encoding', function() { + function createRedirectServerForDomain (domain) { + var app = express(); + app.use(function (req, res) { + var host = url.parse(req.query.q, false, true).host; + // This is here to show a basic check one might do which + // would pass but then the location header would still be bad + if (host !== domain) { + res.status(400).end('Bad host: ' + host + ' !== ' + domain); + } + res.location(req.query.q).end(); + }); + return app; + } + + function testRequestedRedirect (app, inputUrl, expected, expectedHost, done) { + return request(app) + // Encode uri because old supertest does not and is required + // to test older node versions. New supertest doesn't re-encode + // so this works in both. + .get('/?q=' + encodeURIComponent(inputUrl)) + .expect('') // No body. + .expect(200) + .expect('Location', expected) + .end(function (err, res) { + if (err) { + console.log('headers:', res.headers) + console.error('error', res.error, err); + return done(err, res); + } + + // Parse the hosts from the input URL and the Location header + var inputHost = url.parse(inputUrl, false, true).host; + var locationHost = url.parse(res.headers['location'], false, true).host; + + assert.strictEqual(locationHost, expectedHost); + + // Assert that the hosts are the same + if (inputHost !== locationHost) { + return done(new Error('Hosts do not match: ' + inputHost + " !== " + locationHost)); + } + + return done(null, res); + }); + } + + it('should not touch already-encoded sequences in "url"', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'https://google.com?q=%A710', + 'https://google.com?q=%A710', + 'google.com', + done + ); + }); + + it('should consistently handle relative urls', function (done) { + var app = createRedirectServerForDomain(null); + testRequestedRedirect( + app, + '/foo/bar', + '/foo/bar', + null, + done + ); + }); + + it('should not encode urls in such a way that they can bypass redirect allow lists', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'http://google.com\\@apple.com', + 'http://google.com\\@apple.com', + 'google.com', + done + ); + }); + + it('should not be case sensitive', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'HTTP://google.com\\@apple.com', + 'HTTP://google.com\\@apple.com', + 'google.com', + done + ); + }); + + it('should work with https', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'https://google.com\\@apple.com', + 'https://google.com\\@apple.com', + 'google.com', + done + ); + }); + + it('should correctly encode schemaless paths', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + '//google.com\\@apple.com/', + '//google.com\\@apple.com/', + 'google.com', + done + ); + }); + + it('should keep backslashes in the path', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'https://google.com/foo\\bar\\baz', + 'https://google.com/foo\\bar\\baz', + 'google.com', + done + ); + }); + + it('should escape header splitting for old node versions', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'http://google.com\\@apple.com/%0d%0afoo:%20bar', + 'http://google.com\\@apple.com/%0d%0afoo:%20bar', + 'google.com', + done + ); + }); + + it('should encode unicode correctly', function (done) { + var app = createRedirectServerForDomain(null); + testRequestedRedirect( + app, + '/%e2%98%83', + '/%e2%98%83', + null, + done + ); + }); + + it('should encode unicode correctly even with a bad host', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'http://google.com\\@apple.com/%e2%98%83', + 'http://google.com\\@apple.com/%e2%98%83', + 'google.com', + done + ); + }); + + it('should work correctly despite using deprecated url.parse', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'https://google.com\'.bb.com/1.html', + 'https://google.com\'.bb.com/1.html', + 'google.com', + done + ); + }); + + it('should encode file uri path', function (done) { + var app = createRedirectServerForDomain(''); + testRequestedRedirect( + app, + 'file:///etc\\passwd', + 'file:///etc\\passwd', + '', + done + ); + }); + }); }) diff --git a/test/res.redirect.js b/test/res.redirect.js index 5ffc7e48f12..f7214d93312 100644 --- a/test/res.redirect.js +++ b/test/res.redirect.js @@ -106,7 +106,7 @@ describe('res', function(){ .set('Accept', 'text/html') .expect('Content-Type', /html/) .expect('Location', 'http://google.com') - .expect(302, '

Found. Redirecting to http://google.com

', done) + .expect(302, '

Found. Redirecting to http://google.com

', done) }) it('should escape the url', function(done){ @@ -122,9 +122,27 @@ describe('res', function(){ .set('Accept', 'text/html') .expect('Content-Type', /html/) .expect('Location', '%3Cla\'me%3E') - .expect(302, '

Found. Redirecting to %3Cla'me%3E

', done) + .expect(302, '

Found. Redirecting to %3Cla'me%3E

', done) }) + it('should not render evil javascript links in anchor href (prevent XSS)', function(done){ + var app = express(); + var xss = 'javascript:eval(document.body.innerHTML=`

XSS

`);'; + var encodedXss = 'javascript:eval(document.body.innerHTML=%60%3Cp%3EXSS%3C/p%3E%60);'; + + app.use(function(req, res){ + res.redirect(xss); + }); + + request(app) + .get('/') + .set('Host', 'http://example.com') + .set('Accept', 'text/html') + .expect('Content-Type', /html/) + .expect('Location', encodedXss) + .expect(302, '

Found. Redirecting to ' + encodedXss +'

', done); + }); + it('should include the redirect type', function(done){ var app = express(); @@ -137,7 +155,7 @@ describe('res', function(){ .set('Accept', 'text/html') .expect('Content-Type', /html/) .expect('Location', 'http://google.com') - .expect(301, '

Moved Permanently. Redirecting to http://google.com

', done); + .expect(301, '

Moved Permanently. Redirecting to http://google.com

', done); }) }) diff --git a/test/res.send.js b/test/res.send.js index c92568db6ad..b4cf68a7df6 100644 --- a/test/res.send.js +++ b/test/res.send.js @@ -7,6 +7,8 @@ var methods = require('methods'); var request = require('supertest'); var utils = require('./support/utils'); +var shouldSkipQuery = require('./support/utils').shouldSkipQuery + describe('res', function(){ describe('.send()', function(){ it('should set body to ""', function(done){ @@ -409,6 +411,9 @@ describe('res', function(){ if (method === 'connect') return; it('should send ETag in response to ' + method.toUpperCase() + ' request', function (done) { + if (method === 'query' && shouldSkipQuery(process.versions.node)) { + this.skip() + } var app = express(); app[method]('/', function (req, res) { diff --git a/test/support/utils.js b/test/support/utils.js index 5b4062e0b2a..5ad4ca98410 100644 --- a/test/support/utils.js +++ b/test/support/utils.js @@ -16,6 +16,7 @@ exports.shouldHaveBody = shouldHaveBody exports.shouldHaveHeader = shouldHaveHeader exports.shouldNotHaveBody = shouldNotHaveBody exports.shouldNotHaveHeader = shouldNotHaveHeader; +exports.shouldSkipQuery = shouldSkipQuery /** * Assert that a supertest response has a specific body. @@ -70,3 +71,16 @@ function shouldNotHaveHeader(header) { assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header); }; } + +function getMajorVersion(versionString) { + return versionString.split('.')[0]; +} + +function shouldSkipQuery(versionString) { + // Skipping HTTP QUERY tests on Node 21, it is reported in http.METHODS on 21.7.2 but not supported + // update this implementation to run on supported versions of 21 once they exist + // upstream tracking https://github.com/nodejs/node/issues/51562 + // express tracking issue: https://github.com/expressjs/express/issues/5615 + return Number(getMajorVersion(versionString)) === 21 +} +