Compare commits
10 Commits
096682b315
...
e0ba8ef39d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0ba8ef39d | ||
|
|
2503a1fb14 | ||
|
|
d3ea8bf964 | ||
|
|
3b0508e8d0 | ||
|
|
fd111407e9 | ||
|
|
c5a52e056d | ||
|
|
55ab9bab04 | ||
|
|
fecd464ae7 | ||
|
|
d2066cf2a5 | ||
|
|
8694a06eb6 |
12
.github/workflows/npm-publish.yml
vendored
12
.github/workflows/npm-publish.yml
vendored
@@ -1,6 +1,8 @@
|
||||
name: NPM Publish
|
||||
|
||||
# Trigger only when tags matching semver format are pushed
|
||||
# Trigger when tags matching semver format are pushed, or manually via workflow_dispatch.
|
||||
# Manual triggers allow selecting a specific tag to publish (e.g. tags from the v2.x branch).
|
||||
#
|
||||
# Patterns match common semver formats:
|
||||
# - v1.0.0 (standard)
|
||||
# - v1.0.0-alpha (pre-release)
|
||||
@@ -16,6 +18,12 @@ name: NPM Publish
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-[a-zA-Z0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-[a-zA-Z0-9]+.[0-9]+'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Git tag to checkout and publish (e.g. v2.15.4)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
# Permissions for NPM trusted publishing with provenance
|
||||
permissions:
|
||||
@@ -27,6 +35,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
ref: ${{ inputs.tag || github.ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 #v6
|
||||
|
||||
@@ -189,14 +189,6 @@ See [AUTHORS](AUTHORS).
|
||||
- [中文文档 v2.x](https://github.com/demopark/koa-docs-Zh-CN)
|
||||
- __[#koajs]__ on freenode
|
||||
|
||||
## Job Board
|
||||
|
||||
Looking for a career upgrade?
|
||||
|
||||
<a href="https://astro.netlify.com/automattic"><img src="https://astro.netlify.com/static/automattic.png"></a>
|
||||
<a href="https://astro.netlify.com/segment"><img src="https://astro.netlify.com/static/segment.png"></a>
|
||||
<a href="https://astro.netlify.com/auth0"><img src="https://astro.netlify.com/static/auth0.png"/></a>
|
||||
|
||||
## Backers
|
||||
|
||||
Support us with a monthly donation and help us continue our activities.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('node:test')
|
||||
const { describe, it, beforeEach, afterEach } = require('node:test')
|
||||
const v8 = require('node:v8')
|
||||
const request = require('supertest')
|
||||
const assert = require('node:assert/strict')
|
||||
const Koa = require('../..')
|
||||
@@ -113,4 +114,85 @@ describe('app.currentContext', () => {
|
||||
await request(app.callback()).get('/').expect('ok')
|
||||
assert(app.currentContext === undefined)
|
||||
})
|
||||
|
||||
describe('v8 startup snapshot', () => {
|
||||
let originalStartupSnapshot
|
||||
|
||||
beforeEach(() => {
|
||||
originalStartupSnapshot = v8.startupSnapshot
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
v8.startupSnapshot = originalStartupSnapshot
|
||||
})
|
||||
|
||||
it('should defer AsyncLocalStorage creation when building snapshot', () => {
|
||||
let deserializeCallback
|
||||
v8.startupSnapshot = {
|
||||
isBuildingSnapshot: () => true,
|
||||
addDeserializeCallback: (cb, data) => {
|
||||
deserializeCallback = { cb, data }
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Koa({ asyncLocalStorage: true })
|
||||
assert.strictEqual(app.ctxStorage, null)
|
||||
assert(deserializeCallback, 'deserialize callback should be registered')
|
||||
|
||||
// simulate snapshot deserialization
|
||||
deserializeCallback.cb(deserializeCallback.data)
|
||||
assert(app.ctxStorage instanceof AsyncLocalStorage)
|
||||
})
|
||||
|
||||
it('should defer with custom AsyncLocalStorage when building snapshot', () => {
|
||||
const customStorage = new AsyncLocalStorage()
|
||||
let deserializeCallback
|
||||
v8.startupSnapshot = {
|
||||
isBuildingSnapshot: () => true,
|
||||
addDeserializeCallback: (cb, data) => {
|
||||
deserializeCallback = { cb, data }
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Koa({ asyncLocalStorage: customStorage })
|
||||
assert.strictEqual(app.ctxStorage, null)
|
||||
|
||||
// simulate snapshot deserialization
|
||||
deserializeCallback.cb(deserializeCallback.data)
|
||||
assert(app.ctxStorage instanceof AsyncLocalStorage)
|
||||
assert.strictEqual(app.ctxStorage, customStorage)
|
||||
})
|
||||
|
||||
it('should work normally after deserialization', async () => {
|
||||
let deserializeCallback
|
||||
v8.startupSnapshot = {
|
||||
isBuildingSnapshot: () => true,
|
||||
addDeserializeCallback: (cb, data) => {
|
||||
deserializeCallback = { cb, data }
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Koa({ asyncLocalStorage: true })
|
||||
|
||||
// simulate snapshot deserialization
|
||||
deserializeCallback.cb(deserializeCallback.data)
|
||||
|
||||
app.use(async ctx => {
|
||||
assert(ctx === app.currentContext)
|
||||
ctx.body = 'ok'
|
||||
})
|
||||
|
||||
await request(app.callback()).get('/').expect('ok')
|
||||
assert(app.currentContext === undefined)
|
||||
})
|
||||
|
||||
it('should not defer when not building snapshot', () => {
|
||||
v8.startupSnapshot = {
|
||||
isBuildingSnapshot: () => false
|
||||
}
|
||||
|
||||
const app = new Koa({ asyncLocalStorage: true })
|
||||
assert(app.ctxStorage instanceof AsyncLocalStorage)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,4 +94,48 @@ describe('req.host', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with Host header containing @', () => {
|
||||
it('should correctly parse host from userinfo@host format', () => {
|
||||
const req = request()
|
||||
req.header.host = 'evil.com:fake@legitimate.com'
|
||||
assert.strictEqual(req.host, 'legitimate.com')
|
||||
})
|
||||
|
||||
it('should correctly parse host from user@host format', () => {
|
||||
const req = request()
|
||||
req.header.host = 'user@example.com'
|
||||
assert.strictEqual(req.host, 'example.com')
|
||||
})
|
||||
|
||||
it('should correctly parse host with port from userinfo@host:port format', () => {
|
||||
const req = request()
|
||||
req.header.host = 'user:pass@example.com:8080'
|
||||
assert.strictEqual(req.host, 'example.com:8080')
|
||||
})
|
||||
|
||||
it('should correctly parse @ in X-Forwarded-Host when proxy is trusted', () => {
|
||||
const req = request()
|
||||
req.app.proxy = true
|
||||
req.header['x-forwarded-host'] = 'evil.com:fake@legitimate.com'
|
||||
req.header.host = 'foo.com'
|
||||
assert.strictEqual(req.host, 'legitimate.com')
|
||||
})
|
||||
|
||||
it('should correctly parse @ in :authority on HTTP/2', () => {
|
||||
const req = request({
|
||||
httpVersionMajor: 2,
|
||||
httpVersion: '2.0'
|
||||
})
|
||||
req.header[':authority'] = 'evil.com:fake@legitimate.com'
|
||||
req.header.host = 'foo.com'
|
||||
assert.strictEqual(req.host, 'legitimate.com')
|
||||
})
|
||||
|
||||
it('should return empty string for invalid host with @', () => {
|
||||
const req = request()
|
||||
req.header.host = 'user@'
|
||||
assert.strictEqual(req.host, '')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -70,4 +70,32 @@ describe('req.hostname', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with Host header containing @', () => {
|
||||
it('should correctly parse hostname from userinfo@host format', () => {
|
||||
const req = request()
|
||||
req.header.host = 'evil.com:fake@legitimate.com'
|
||||
assert.strictEqual(req.hostname, 'legitimate.com')
|
||||
})
|
||||
|
||||
it('should correctly parse hostname from user@host format', () => {
|
||||
const req = request()
|
||||
req.header.host = 'user@example.com'
|
||||
assert.strictEqual(req.hostname, 'example.com')
|
||||
})
|
||||
|
||||
it('should correctly parse hostname with port from userinfo@host:port format', () => {
|
||||
const req = request()
|
||||
req.header.host = 'user:pass@example.com:8080'
|
||||
assert.strictEqual(req.hostname, 'example.com')
|
||||
})
|
||||
|
||||
it('should correctly parse @ in X-Forwarded-Host when proxy is trusted', () => {
|
||||
const req = request()
|
||||
req.app.proxy = true
|
||||
req.header['x-forwarded-host'] = 'evil.com:fake@legitimate.com'
|
||||
req.header.host = 'foo.com'
|
||||
assert.strictEqual(req.hostname, 'legitimate.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
- [Async operations](#async-operations)
|
||||
- [Debugging Koa](#debugging-koa)
|
||||
- [HTTP2](#http2)
|
||||
- [Server-Side Events](#server-side-events)
|
||||
- [Server-Sent Events](#server-sent-events)
|
||||
|
||||
## Writing Middleware
|
||||
|
||||
@@ -272,9 +272,9 @@ const server = http2.createSecureServer(serverOptions, onRequestHandler);
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
## Server-Side Events
|
||||
## Server-Sent Events
|
||||
|
||||
An example of using server-side events with Koa:
|
||||
An example of using server-sent events with Koa:
|
||||
|
||||
```js
|
||||
import { PassThrough } from 'node:stream'
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
const util = require('node:util')
|
||||
const v8 = require('node:v8')
|
||||
const debug = util.debuglog('koa:application')
|
||||
const Emitter = require('node:events')
|
||||
const Stream = require('node:stream')
|
||||
@@ -40,6 +41,13 @@ const only = require('./only.js')
|
||||
* Inherits from `Emitter.prototype`.
|
||||
*/
|
||||
|
||||
function getAsyncLocalStorage (options) {
|
||||
if (options.asyncLocalStorage instanceof AsyncLocalStorage) {
|
||||
return options.asyncLocalStorage
|
||||
}
|
||||
return new AsyncLocalStorage()
|
||||
}
|
||||
|
||||
module.exports = class Application extends Emitter {
|
||||
/**
|
||||
* Initialize a new `Application`.
|
||||
@@ -81,10 +89,13 @@ module.exports = class Application extends Emitter {
|
||||
this[util.inspect.custom] = this.inspect
|
||||
}
|
||||
if (options.asyncLocalStorage) {
|
||||
if (options.asyncLocalStorage instanceof AsyncLocalStorage) {
|
||||
this.ctxStorage = options.asyncLocalStorage
|
||||
if (v8.startupSnapshot?.isBuildingSnapshot?.()) {
|
||||
this.ctxStorage = null
|
||||
v8.startupSnapshot.addDeserializeCallback(({ app, options }) => {
|
||||
app.ctxStorage = getAsyncLocalStorage(options)
|
||||
}, { app: this, options })
|
||||
} else {
|
||||
this.ctxStorage = new AsyncLocalStorage()
|
||||
this.ctxStorage = getAsyncLocalStorage(options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,17 @@ module.exports = {
|
||||
if (!host) host = this.get('Host')
|
||||
}
|
||||
if (!host) return ''
|
||||
return splitCommaSeparatedValues(host, 1)[0]
|
||||
host = splitCommaSeparatedValues(host, 1)[0]
|
||||
// Host header may contain userinfo (e.g., "user@host") which is invalid per RFC 7230.
|
||||
// Use URL parser to correctly extract the host portion.
|
||||
if (host.includes('@')) {
|
||||
try {
|
||||
host = new URL(`http://${host}`).host
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
return host
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
40
package-lock.json
generated
40
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "koa",
|
||||
"version": "3.1.1",
|
||||
"version": "3.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "koa",
|
||||
"version": "3.1.1",
|
||||
"version": "3.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^1.3.8",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-disposition": "~1.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
"cookies": "~0.9.1",
|
||||
"delegates": "^1.0.0",
|
||||
@@ -431,7 +431,6 @@
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -938,15 +937,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
@@ -1447,7 +1447,6 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -1655,7 +1654,6 @@
|
||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.8",
|
||||
@@ -1723,7 +1721,6 @@
|
||||
"integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"builtins": "^5.0.1",
|
||||
"eslint-plugin-es": "^4.1.0",
|
||||
@@ -1750,7 +1747,6 @@
|
||||
"integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
@@ -1767,7 +1763,6 @@
|
||||
"integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.8",
|
||||
"array.prototype.findlast": "^1.2.5",
|
||||
@@ -3136,9 +3131,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3909,9 +3904,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -4141,6 +4136,7 @@
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "koa",
|
||||
"version": "3.1.1",
|
||||
"version": "3.2.0",
|
||||
"description": "Koa web app framework",
|
||||
"main": "lib/application.js",
|
||||
"exports": {
|
||||
@@ -37,7 +37,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^1.3.8",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-disposition": "~1.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
"cookies": "~0.9.1",
|
||||
"delegates": "^1.0.0",
|
||||
|
||||
Reference in New Issue
Block a user