@nifrajs/core 5.7.0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/server-types.d.ts +12 -0
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +15 -8
- package/dist/server/server.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/server/core-copies.ts +1 -1
- package/src/server/server-types.ts +12 -0
- package/src/server/server.ts +17 -9
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and
|
|
17
17
|
* `check:publish` asserts it equals `@nifrajs/core`'s package version.
|
|
18
18
|
*/
|
|
19
|
-
export const VERSION = "5.
|
|
19
|
+
export const VERSION = "5.8.0" as const
|
|
20
20
|
|
|
21
21
|
export type Version = typeof VERSION
|
|
22
22
|
|
|
@@ -26,7 +26,7 @@ if (!copies.includes(here) && copies.push(here) > 1) {
|
|
|
26
26
|
|
|
27
27
|
/** This copy's version: a literal like the package root's `VERSION` (no fs on the edge), which
|
|
28
28
|
* `scripts/version.ts` rewrites on every release and `check:publish` asserts. */
|
|
29
|
-
const COPY_VERSION = "5.
|
|
29
|
+
const COPY_VERSION = "5.8.0"
|
|
30
30
|
|
|
31
31
|
/** Record this copy's version beside its URL. Called by each `server()` behind the development guard,
|
|
32
32
|
* so a production bundle drops it with the version; a copy that built a server has recorded one. */
|
|
@@ -89,6 +89,18 @@ export interface ServerOptions {
|
|
|
89
89
|
* untrusted input.
|
|
90
90
|
*/
|
|
91
91
|
readonly trustBodyFraming?: boolean
|
|
92
|
+
/**
|
|
93
|
+
* Answer `400 invalid_host` on Bun to every Host that Node and Deno refuse: one sent twice, one
|
|
94
|
+
* no URL can be built on (a space, a byte past ASCII, a port past 65535, an IPv6 literal no parser
|
|
95
|
+
* takes), a percent-encoded or sub-delimiter one, and serve an HTTP/1.0 request with no Host as
|
|
96
|
+
* `localhost`. Bun serves those requests itself, building `c.req.url` on the first Host or leaving
|
|
97
|
+
* it relative, so `new URL(c.req.url)` can throw in a handler.
|
|
98
|
+
*
|
|
99
|
+
* Disabled by default: the check reads the Host on every request, which measured about 6% of a bare
|
|
100
|
+
* route on Bun. Left off, a target with `.` or `..` segments is still checked, so a dot segment is
|
|
101
|
+
* never routed raw. Node and Deno refuse these Hosts either way; the option changes nothing there.
|
|
102
|
+
*/
|
|
103
|
+
readonly validateHost?: boolean
|
|
92
104
|
/**
|
|
93
105
|
* Max inbound WebSocket message size (bytes). A larger message never reaches the handler and the
|
|
94
106
|
* socket closes with `1009`; when Bun's own frame limit refuses it, Bun up to 1.4.3 drops the
|
package/src/server/server.ts
CHANGED
|
@@ -713,7 +713,8 @@ function resolvedSource(req: Request, url: string): RequestSource {
|
|
|
713
713
|
const HOST_AUTHORITY = /^(?:\[[0-9a-f:.%]+\]|[\w.-]+)(?::(\d{1,5}))?$/i
|
|
714
714
|
|
|
715
715
|
// Clients repeat one Host on every request; comparing to the last valid one is ~7x cheaper than the regex.
|
|
716
|
-
|
|
716
|
+
// `null` until one passes, so an empty Host is never taken as the cached one.
|
|
717
|
+
let lastValidHost: string | null = null
|
|
717
718
|
|
|
718
719
|
// A port past 65535, or an IPv6 literal no URL parser takes (`[1:2]`), is refused as Node refuses it:
|
|
719
720
|
// no URL can be built on either, so resolving a dot segment against one threw out of `fetch`.
|
|
@@ -733,10 +734,11 @@ const invalidHost = (host: string): boolean => {
|
|
|
733
734
|
* its dot segments are resolved; so does Bun when HTTP/1.0 leaves the Host out, and its route table
|
|
734
735
|
* matched `/files/..` raw. Such a request is served as `localhost`, as Node and Deno serve it.
|
|
735
736
|
* `headerOf`, not `req.headers`: on Deno that reads the Host alone instead of building every
|
|
736
|
-
* header.
|
|
737
|
+
* header. On Bun only a target that needs resolving is checked unless `validateHost` is set.
|
|
737
738
|
*/
|
|
738
|
-
const requestTarget = (req: Request): string | boolean => {
|
|
739
|
-
if (!rawRequestTargets && !unparsedBunTargets
|
|
739
|
+
const requestTarget = (req: Request, validateHost: boolean): string | boolean => {
|
|
740
|
+
if (!rawRequestTargets && !(unparsedBunTargets && (validateHost || needsResolving(req.url))))
|
|
741
|
+
return false
|
|
740
742
|
const host = headerOf(req, "host")
|
|
741
743
|
if (host !== null && invalidHost(host)) {
|
|
742
744
|
if (rawRequestTargets) return true
|
|
@@ -1270,6 +1272,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1270
1272
|
private readonly formOrigins: FormOrigins | undefined
|
|
1271
1273
|
/** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
|
|
1272
1274
|
private readonly trustBodyFraming: boolean
|
|
1275
|
+
private readonly validateHost: boolean
|
|
1273
1276
|
private readonly wsMaxPayloadBytes: number
|
|
1274
1277
|
private readonly requestTimeoutMs: number
|
|
1275
1278
|
private readonly wsUpgradeTimeoutMs: number
|
|
@@ -1414,6 +1417,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1414
1417
|
this.protoPoisoning = options.protoPoisoning ?? "reject"
|
|
1415
1418
|
this.formOrigins = options.formOrigins
|
|
1416
1419
|
this.trustBodyFraming = options.trustBodyFraming ?? false
|
|
1420
|
+
this.validateHost = options.validateHost ?? false
|
|
1417
1421
|
const wsMaxPayloadBytes = options.wsMaxPayloadBytes ?? maxBodyBytes
|
|
1418
1422
|
assertByteLimit(wsMaxPayloadBytes, "wsMaxPayloadBytes")
|
|
1419
1423
|
this.maxBodyBytes = maxBodyBytes
|
|
@@ -3391,7 +3395,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3391
3395
|
fetch(req: Request, platform?: Platform<EnvOf<Ctx>>): MaybePromise<Response> {
|
|
3392
3396
|
// Deno's `Request.url` is the target as sent; Bun, workerd and every `new Request()` parse it.
|
|
3393
3397
|
// Route what the parsed URL says, so `/users/../admin` is `/admin` on every runtime.
|
|
3394
|
-
const target = requestTarget(req)
|
|
3398
|
+
const target = requestTarget(req, this.validateHost)
|
|
3395
3399
|
if (target === true) return jsonError(400, "invalid_host")
|
|
3396
3400
|
if (target !== false) {
|
|
3397
3401
|
const rebuilt = withResolvedTarget(req, target)
|
|
@@ -3895,7 +3899,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3895
3899
|
// The adapter answers a pass by handing this same object to `fetch`.
|
|
3896
3900
|
const received = req
|
|
3897
3901
|
// The handshake routes the path `fetch` would: Host checked, dot segments resolved (see `fetch`).
|
|
3898
|
-
const target = requestTarget(req)
|
|
3902
|
+
const target = requestTarget(req, this.validateHost)
|
|
3899
3903
|
if (target === true) {
|
|
3900
3904
|
return { kind: "reject", response: this.wrapWebResponse(jsonError(400, "invalid_host")) }
|
|
3901
3905
|
}
|
|
@@ -7048,9 +7052,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7048
7052
|
peer,
|
|
7049
7053
|
fallback,
|
|
7050
7054
|
)
|
|
7051
|
-
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it.
|
|
7052
|
-
|
|
7053
|
-
|
|
7055
|
+
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it. Left
|
|
7056
|
+
// off, `validateHost` costs nothing here: the table holds no wildcard, a `..` parameter falls
|
|
7057
|
+
// back, and reading the Host on every request measured about 6% of a bare route.
|
|
7058
|
+
tableAt(path, pattern.segments)[method as Method] = this.validateHost
|
|
7059
|
+
? (request, server) =>
|
|
7060
|
+
requestTarget(request, true) ? fallback(request, server) : handler(request, server)
|
|
7061
|
+
: handler
|
|
7054
7062
|
count += 1
|
|
7055
7063
|
if (paramNames.length > 0) {
|
|
7056
7064
|
below.add(pattern.segments.length + method)
|