@c9up/ream 0.2.28 → 0.2.29
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/dist/Codemods.d.ts +16 -0
- package/dist/Codemods.d.ts.map +1 -1
- package/dist/Codemods.js +182 -0
- package/dist/Codemods.js.map +1 -1
- package/dist/Ignitor.d.ts +10 -2
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +21 -22
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +3 -0
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/qsParse.d.ts +10 -1
- package/dist/bodyparser/qsParse.d.ts.map +1 -1
- package/dist/bodyparser/qsParse.js +53 -15
- package/dist/bodyparser/qsParse.js.map +1 -1
- package/dist/codemods/source.d.ts +57 -0
- package/dist/codemods/source.d.ts.map +1 -0
- package/dist/codemods/source.js +282 -0
- package/dist/codemods/source.js.map +1 -0
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +3 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/http/Exception.d.ts +14 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +10 -2
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +11 -0
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +2 -1
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts +9 -3
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +39 -37
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +42 -17
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +65 -29
- package/dist/http/Request.js.map +1 -1
- package/dist/http/SseStream.js +6 -7
- package/dist/http/SseStream.js.map +1 -1
- package/dist/http/permissions.d.ts +19 -0
- package/dist/http/permissions.d.ts.map +1 -0
- package/dist/http/permissions.js +28 -0
- package/dist/http/permissions.js.map +1 -0
- package/dist/http-client/HttpClient.d.ts +111 -0
- package/dist/http-client/HttpClient.d.ts.map +1 -0
- package/dist/http-client/HttpClient.js +386 -0
- package/dist/http-client/HttpClient.js.map +1 -0
- package/dist/http-client/addresses.d.ts +14 -0
- package/dist/http-client/addresses.d.ts.map +1 -0
- package/dist/http-client/addresses.js +57 -0
- package/dist/http-client/addresses.js.map +1 -0
- package/dist/http-client/fake.d.ts +50 -0
- package/dist/http-client/fake.d.ts.map +1 -0
- package/dist/http-client/fake.js +95 -0
- package/dist/http-client/fake.js.map +1 -0
- package/dist/http-client/index.d.ts +7 -0
- package/dist/http-client/index.d.ts.map +1 -0
- package/dist/http-client/index.js +7 -0
- package/dist/http-client/index.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +6 -3
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/native/generated.d.ts +78 -7
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.d.ts +6 -0
- package/dist/rpc/RpcProvider.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.js +4 -1
- package/dist/rpc/RpcProvider.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +27 -14
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +95 -38
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/locks/RedisLockBackend.d.ts.map +1 -1
- package/dist/scheduler/locks/RedisLockBackend.js +13 -2
- package/dist/scheduler/locks/RedisLockBackend.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +17 -3
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.d.ts.map +1 -1
- package/dist/security/SignedUrl.js +6 -1
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/server/Server.d.ts +41 -12
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +31 -10
- package/dist/server/Server.js.map +1 -1
- package/dist/server/nativeGrpc.d.ts +27 -0
- package/dist/server/nativeGrpc.d.ts.map +1 -0
- package/dist/server/nativeGrpc.js +47 -0
- package/dist/server/nativeGrpc.js.map +1 -0
- package/dist/server/nativeLimits.d.ts +49 -0
- package/dist/server/nativeLimits.d.ts.map +1 -0
- package/dist/server/nativeLimits.js +67 -0
- package/dist/server/nativeLimits.js.map +1 -0
- package/dist/session/Session.d.ts +12 -0
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +28 -9
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/config.d.ts +5 -0
- package/dist/session/config.d.ts.map +1 -1
- package/dist/session/config.js +8 -0
- package/dist/session/config.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +12 -5
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/errors.d.ts +17 -0
- package/dist/session/errors.d.ts.map +1 -1
- package/dist/session/errors.js +21 -0
- package/dist/session/errors.js.map +1 -1
- package/dist/session/templateTags.d.ts +14 -0
- package/dist/session/templateTags.d.ts.map +1 -1
- package/dist/session/templateTags.js +45 -17
- package/dist/session/templateTags.js.map +1 -1
- package/dist/storage/StaticMiddleware.d.ts.map +1 -1
- package/dist/storage/StaticMiddleware.js +29 -18
- package/dist/storage/StaticMiddleware.js.map +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +11 -2
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Codemods.ts +265 -0
- package/src/Ignitor.ts +37 -22
- package/src/bodyparser/BodyParserMiddleware.ts +3 -0
- package/src/bodyparser/qsParse.ts +69 -14
- package/src/codemods/source.ts +303 -0
- package/src/graphql/GraphQLEngine.ts +3 -2
- package/src/http/Exception.ts +17 -2
- package/src/http/HttpContext.ts +14 -1
- package/src/http/RedirectBuilder.ts +48 -37
- package/src/http/Request.ts +87 -29
- package/src/http/SseStream.ts +6 -7
- package/src/http/permissions.ts +31 -0
- package/src/http-client/HttpClient.ts +505 -0
- package/src/http-client/addresses.ts +58 -0
- package/src/http-client/fake.ts +141 -0
- package/src/http-client/index.ts +21 -0
- package/src/index.ts +5 -1
- package/src/middleware/Pipeline.ts +7 -3
- package/src/native/generated.ts +89 -7
- package/src/rpc/RpcProvider.ts +10 -1
- package/src/rpc/RpcRouter.ts +126 -42
- package/src/scheduler/locks/RedisLockBackend.ts +12 -4
- package/src/security/CookieSigner.ts +17 -3
- package/src/security/SignedUrl.ts +6 -1
- package/src/server/Server.ts +57 -12
- package/src/server/nativeGrpc.ts +59 -0
- package/src/server/nativeLimits.ts +94 -0
- package/src/session/Session.ts +16 -0
- package/src/session/SessionMiddleware.ts +30 -10
- package/src/session/config.ts +7 -0
- package/src/session/drivers/MemoryDriver.ts +11 -5
- package/src/session/errors.ts +31 -0
- package/src/session/templateTags.ts +57 -0
- package/src/storage/StaticMiddleware.ts +28 -18
|
@@ -13,30 +13,36 @@ import type { Response } from './Response.js'
|
|
|
13
13
|
|
|
14
14
|
export type RouteUrlResolver = (name: string, params?: Record<string, string>) => string
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* AdonisJS `isValidRedirectUrl`: a relative path, or an absolute URL whose host
|
|
18
|
+
* is the request authority or an allowed host.
|
|
19
|
+
*/
|
|
20
|
+
function isTrustedRedirect(
|
|
21
|
+
url: string,
|
|
22
|
+
authority: string | undefined,
|
|
23
|
+
allowedHosts: readonly string[],
|
|
20
24
|
): boolean {
|
|
21
25
|
// A relative path is trusted only when it starts with a single "/" and has no
|
|
22
26
|
// backslash: browsers normalise "\" to "/", so "/\evil.com" would become the
|
|
23
27
|
// protocol-relative "//evil.com" that a lone `!startsWith("//")` check misses.
|
|
24
|
-
if (
|
|
25
|
-
let
|
|
26
|
-
try {
|
|
27
|
-
ref = new URL(referer)
|
|
28
|
-
} catch {
|
|
29
|
-
return false
|
|
30
|
-
}
|
|
31
|
-
// An explicitly allowed host is trusted whatever the request's own origin is
|
|
32
|
-
// — that is what the list is for (AdonisJS `allowedHosts`).
|
|
33
|
-
if (allowedHosts.includes(ref.host)) return true
|
|
34
|
-
if (!requestUrl) return false
|
|
28
|
+
if (url.startsWith('/') && !url.startsWith('//') && !url.includes('\\')) return true
|
|
29
|
+
let parsed: URL
|
|
35
30
|
try {
|
|
36
|
-
|
|
31
|
+
parsed = new URL(url)
|
|
37
32
|
} catch {
|
|
38
33
|
return false
|
|
39
34
|
}
|
|
35
|
+
if (allowedHosts.includes(parsed.host)) return true
|
|
36
|
+
// Upstream accepts any absolute URL when it knows no host to compare with;
|
|
37
|
+
// refusing it is the safer reading of an attacker-controlled header.
|
|
38
|
+
return authority !== undefined && parsed.host === authority
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The query string of a path or URL, without the "?". */
|
|
42
|
+
function searchOf(url: string | undefined): string {
|
|
43
|
+
if (url === undefined) return ''
|
|
44
|
+
const index = url.indexOf('?')
|
|
45
|
+
return index === -1 ? '' : url.slice(index + 1)
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/** The slice of the session the intended-URL methods use. */
|
|
@@ -60,6 +66,7 @@ export class RedirectBuilder {
|
|
|
60
66
|
#forwardQs = false
|
|
61
67
|
#requestUrl?: string
|
|
62
68
|
#requestReferer?: string
|
|
69
|
+
#requestAuthority?: string
|
|
63
70
|
#routeUrlResolver?: RouteUrlResolver
|
|
64
71
|
#session?: IntendedUrlStore
|
|
65
72
|
#isAjax = false
|
|
@@ -68,8 +75,14 @@ export class RedirectBuilder {
|
|
|
68
75
|
constructor(
|
|
69
76
|
response: Response,
|
|
70
77
|
options?: {
|
|
78
|
+
/** The request path with its query string. */
|
|
71
79
|
requestUrl?: string
|
|
72
80
|
requestReferer?: string
|
|
81
|
+
/**
|
|
82
|
+
* The request authority (`:authority` or `Host`, never X-Forwarded-Host)
|
|
83
|
+
* an absolute redirect target must match.
|
|
84
|
+
*/
|
|
85
|
+
requestAuthority?: string
|
|
73
86
|
routeUrlResolver?: RouteUrlResolver
|
|
74
87
|
/** The request session, for the intended-URL methods. */
|
|
75
88
|
session?: IntendedUrlStore
|
|
@@ -82,6 +95,7 @@ export class RedirectBuilder {
|
|
|
82
95
|
this.#response = response
|
|
83
96
|
this.#requestUrl = options?.requestUrl
|
|
84
97
|
this.#requestReferer = options?.requestReferer
|
|
98
|
+
this.#requestAuthority = options?.requestAuthority
|
|
85
99
|
this.#routeUrlResolver = options?.routeUrlResolver
|
|
86
100
|
this.#session = options?.session
|
|
87
101
|
this.#isAjax = options?.isAjax ?? false
|
|
@@ -135,13 +149,9 @@ export class RedirectBuilder {
|
|
|
135
149
|
|
|
136
150
|
#safeIntended<T extends string | null>(intended: string | null, fallback: T): string | T {
|
|
137
151
|
if (!intended) return fallback
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// A protocol-relative `//evil.com` is absolute to a browser even though it
|
|
142
|
-
// looks relative — reject it whether or not a request URL is known.
|
|
143
|
-
if (intended.startsWith('//')) return fallback
|
|
144
|
-
return intended
|
|
152
|
+
return isTrustedRedirect(intended, this.#requestAuthority, this.allowedHosts)
|
|
153
|
+
? intended
|
|
154
|
+
: fallback
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
/** Set redirect status code. */
|
|
@@ -191,7 +201,12 @@ export class RedirectBuilder {
|
|
|
191
201
|
|
|
192
202
|
/** Redirect to an absolute or relative path. */
|
|
193
203
|
toPath(path: string): void {
|
|
194
|
-
|
|
204
|
+
this.#send(path, searchOf(this.#requestUrl))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Redirect with `withQs()` forwarding `forwardedSearch` (a query string without "?"). */
|
|
208
|
+
#send(path: string, forwardedSearch: string): void {
|
|
209
|
+
const url = this.#appendQs(path, forwardedSearch)
|
|
195
210
|
this.#response.status(this.#status)
|
|
196
211
|
this.#response.header('location', url)
|
|
197
212
|
this.#response.setBody('')
|
|
@@ -207,12 +222,13 @@ export class RedirectBuilder {
|
|
|
207
222
|
}
|
|
208
223
|
|
|
209
224
|
/**
|
|
210
|
-
* Redirect back to the previous page (
|
|
211
|
-
*
|
|
212
|
-
*
|
|
225
|
+
* Redirect back to the previous page (AdonisJS `back`): the path of
|
|
226
|
+
* {@link getPreviousUrl}, so the redirect never leaves the site; `withQs()`
|
|
227
|
+
* forwards the previous page's query string, not the current request's.
|
|
213
228
|
*/
|
|
214
229
|
back(fallback = '/'): void {
|
|
215
|
-
|
|
230
|
+
const previous = new URL(this.getPreviousUrl(fallback), 'http://localhost')
|
|
231
|
+
this.#send(previous.pathname, previous.search.slice(1))
|
|
216
232
|
}
|
|
217
233
|
|
|
218
234
|
/**
|
|
@@ -225,7 +241,7 @@ export class RedirectBuilder {
|
|
|
225
241
|
*/
|
|
226
242
|
getPreviousUrl(fallback = '/'): string {
|
|
227
243
|
const referer = this.#requestReferer
|
|
228
|
-
if (referer &&
|
|
244
|
+
if (referer && isTrustedRedirect(referer, this.#requestAuthority, this.allowedHosts)) {
|
|
229
245
|
return referer
|
|
230
246
|
}
|
|
231
247
|
return fallback
|
|
@@ -236,15 +252,10 @@ export class RedirectBuilder {
|
|
|
236
252
|
* uses, so `withQs()` plus `withQs('page', 2)` overrides an inbound `page`
|
|
237
253
|
* rather than being overridden by it.
|
|
238
254
|
*/
|
|
239
|
-
#appendQs(path: string): string {
|
|
255
|
+
#appendQs(path: string, forwardedSearch: string): string {
|
|
240
256
|
const params = new URLSearchParams()
|
|
241
|
-
if (this.#forwardQs
|
|
242
|
-
const
|
|
243
|
-
if (qsIdx !== -1) {
|
|
244
|
-
for (const [key, value] of new URLSearchParams(this.#requestUrl.slice(qsIdx + 1))) {
|
|
245
|
-
params.append(key, value)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
257
|
+
if (this.#forwardQs) {
|
|
258
|
+
for (const [key, value] of new URLSearchParams(forwardedSearch)) params.append(key, value)
|
|
248
259
|
}
|
|
249
260
|
for (const [key, value] of Object.entries(this.#qs)) {
|
|
250
261
|
if (value === undefined || value === null) continue
|
package/src/http/Request.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { SignedUrl } from '../security/SignedUrl.js'
|
|
|
13
13
|
import type { Dict } from '../types/helpers.js'
|
|
14
14
|
import { Macroable } from '../utils/Macroable.js'
|
|
15
15
|
import { getPath, omitPaths, pickPaths } from '../utils/objectPath.js'
|
|
16
|
+
import { Exception } from './Exception.js'
|
|
16
17
|
import type {
|
|
17
18
|
RequestAwareValidator,
|
|
18
19
|
RequestValidationOptions,
|
|
@@ -37,6 +38,26 @@ export interface RawRequest {
|
|
|
37
38
|
* `ip()` accessor falls back to legacy header parsing.
|
|
38
39
|
*/
|
|
39
40
|
ip?: string
|
|
41
|
+
/**
|
|
42
|
+
* The trusted address chain, closest hop first — the socket peer, then each
|
|
43
|
+
* `X-Forwarded-For` entry the Rust layer walked right to left while the hop
|
|
44
|
+
* before it was a trusted proxy. Its last entry is {@link ip}. Absent in
|
|
45
|
+
* hand-built fixtures, where the chain is the peer alone.
|
|
46
|
+
*/
|
|
47
|
+
ips?: string[]
|
|
48
|
+
/**
|
|
49
|
+
* Whether the socket peer is one of the trusted proxies configured with
|
|
50
|
+
* `server.trustedProxies()`, as the Rust layer decided against the same
|
|
51
|
+
* list that resolves {@link ip}. Absent in hand-built fixtures: untrusted.
|
|
52
|
+
*/
|
|
53
|
+
peerTrusted?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* The body was over its content type's limit (`config/bodyparser.ts`): the
|
|
56
|
+
* Rust layer did not keep it, and `body` is empty. Reading the body then
|
|
57
|
+
* throws `E_REQUEST_ENTITY_TOO_LARGE` instead of handing back nothing as if
|
|
58
|
+
* nothing had been sent.
|
|
59
|
+
*/
|
|
60
|
+
bodyTooLarge?: boolean
|
|
40
61
|
/**
|
|
41
62
|
* Connection scheme as seen by the server (`http`/`https`), when the Rust
|
|
42
63
|
* layer ships it. Defaults to `http` in `protocol()` when absent; the
|
|
@@ -72,7 +93,6 @@ export class Request extends Macroable {
|
|
|
72
93
|
#cookieSigner?: CookieSigner
|
|
73
94
|
#signedUrl?: SignedUrl
|
|
74
95
|
#allowMethodSpoofing = false
|
|
75
|
-
#trustProxy = false
|
|
76
96
|
#routeInfo?: { name?: string; pattern: string; reference?: string }
|
|
77
97
|
#response?: { fresh(): boolean }
|
|
78
98
|
#parsedBody: Dict<unknown> | undefined
|
|
@@ -245,13 +265,17 @@ export class Request extends Macroable {
|
|
|
245
265
|
}
|
|
246
266
|
|
|
247
267
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
268
|
+
* Whether the socket peer is one of the trusted proxies configured with
|
|
269
|
+
* `server.trustedProxies()` — the condition under which `X-Forwarded-Proto`
|
|
270
|
+
* and `X-Forwarded-Host` are believed by {@link protocol} and {@link host}.
|
|
271
|
+
*
|
|
272
|
+
* The decision is the Rust layer's (`peerTrusted`), made once per request
|
|
273
|
+
* against the same list that resolves {@link ip}, so a trusted proxy that
|
|
274
|
+
* forwards the scheme or host without an `X-Forwarded-For` is believed too.
|
|
275
|
+
* A request without the flag — a hand-built fixture — is untrusted.
|
|
252
276
|
*/
|
|
253
|
-
|
|
254
|
-
this.#
|
|
277
|
+
#peerIsTrustedProxy(): boolean {
|
|
278
|
+
return this.#raw.peerTrusted === true
|
|
255
279
|
}
|
|
256
280
|
|
|
257
281
|
/**
|
|
@@ -272,11 +296,12 @@ export class Request extends Macroable {
|
|
|
272
296
|
|
|
273
297
|
/**
|
|
274
298
|
* The request host including port (AdonisJS `host`). Honours
|
|
275
|
-
* `X-Forwarded-Host` ONLY when
|
|
276
|
-
* header. Returns null when
|
|
299
|
+
* `X-Forwarded-Host` ONLY when the request came through a trusted proxy (see
|
|
300
|
+
* `server.trustedProxies()`); otherwise the `Host` header. Returns null when
|
|
301
|
+
* neither is present.
|
|
277
302
|
*/
|
|
278
303
|
host(): string | null {
|
|
279
|
-
if (this.#
|
|
304
|
+
if (this.#peerIsTrustedProxy()) {
|
|
280
305
|
const forwarded = this.#raw.headers['x-forwarded-host']
|
|
281
306
|
// The first hop in the list, named rather than indexed: `split` always
|
|
282
307
|
// yields one element, and saying so beats being told it might not.
|
|
@@ -287,12 +312,15 @@ export class Request extends Macroable {
|
|
|
287
312
|
}
|
|
288
313
|
|
|
289
314
|
/**
|
|
290
|
-
* The request protocol — `http` or `https` (AdonisJS `protocol`).
|
|
291
|
-
* `
|
|
292
|
-
*
|
|
315
|
+
* The request protocol — `http` or `https` (AdonisJS `protocol`). A TLS
|
|
316
|
+
* connection is `https` whatever the headers say. Otherwise
|
|
317
|
+
* `X-Forwarded-Proto` is honoured only when the request came through a
|
|
318
|
+
* trusted proxy (see `server.trustedProxies()`), and the connection scheme
|
|
319
|
+
* the Rust layer reported (defaulting to `http`) is used when it is not.
|
|
293
320
|
*/
|
|
294
321
|
protocol(): string {
|
|
295
|
-
if (this.#
|
|
322
|
+
if (this.#raw.scheme === 'https') return 'https'
|
|
323
|
+
if (this.#peerIsTrustedProxy()) {
|
|
296
324
|
const forwarded = this.#raw.headers['x-forwarded-proto']
|
|
297
325
|
const [first] = forwarded?.split(',') ?? []
|
|
298
326
|
if (first !== undefined) return first.trim().toLowerCase()
|
|
@@ -306,20 +334,15 @@ export class Request extends Macroable {
|
|
|
306
334
|
}
|
|
307
335
|
|
|
308
336
|
/**
|
|
309
|
-
* The
|
|
310
|
-
* `X-Forwarded-For`
|
|
311
|
-
*
|
|
337
|
+
* The trusted address chain, closest hop first (AdonisJS `ips`): the socket
|
|
338
|
+
* peer, then each `X-Forwarded-For` hop a trusted proxy vouched for. The
|
|
339
|
+
* last entry is {@link ip}. Hops added by an untrusted peer are not in it —
|
|
340
|
+
* the Rust layer stops at the first address it does not trust, so the chain
|
|
341
|
+
* goes past the peer only when {@link peerTrusted} holds.
|
|
312
342
|
*/
|
|
313
343
|
ips(): string[] {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (forwarded) {
|
|
317
|
-
return forwarded
|
|
318
|
-
.split(',')
|
|
319
|
-
.map((value) => value.trim())
|
|
320
|
-
.filter((value) => value.length > 0)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
344
|
+
const chain = this.#raw.ips
|
|
345
|
+
if (chain && chain.length > 0 && this.#peerIsTrustedProxy()) return [...chain]
|
|
323
346
|
return [this.ip()]
|
|
324
347
|
}
|
|
325
348
|
|
|
@@ -477,17 +500,26 @@ export class Request extends Macroable {
|
|
|
477
500
|
return this.#signedUrl.verify(this.url(true), purpose)
|
|
478
501
|
}
|
|
479
502
|
|
|
480
|
-
/**
|
|
503
|
+
/**
|
|
504
|
+
* Get the raw body as a string (decoded from base64 if binary). Throws
|
|
505
|
+
* `E_REQUEST_ENTITY_TOO_LARGE` (413) when the body was over its limit: what
|
|
506
|
+
* the server kept of it is nothing, and nothing is not the body.
|
|
507
|
+
*/
|
|
481
508
|
raw(): string {
|
|
482
509
|
if (this.#rawBodyOverride !== undefined) return this.#rawBodyOverride
|
|
510
|
+
this.#assertBodyKept()
|
|
483
511
|
if (this.#raw.bodyEncoding === 'base64') {
|
|
484
512
|
return Buffer.from(this.#raw.body, 'base64').toString('utf8')
|
|
485
513
|
}
|
|
486
514
|
return this.#raw.body
|
|
487
515
|
}
|
|
488
516
|
|
|
489
|
-
/**
|
|
517
|
+
/**
|
|
518
|
+
* Get the raw body as a Buffer (preserves binary integrity for file uploads).
|
|
519
|
+
* Throws `E_REQUEST_ENTITY_TOO_LARGE` (413) when the body was over its limit.
|
|
520
|
+
*/
|
|
490
521
|
rawBuffer(): Buffer {
|
|
522
|
+
this.#assertBodyKept()
|
|
491
523
|
if (this.#raw.bodyEncoding === 'base64') {
|
|
492
524
|
return Buffer.from(this.#raw.body, 'base64')
|
|
493
525
|
}
|
|
@@ -716,7 +748,8 @@ export class Request extends Macroable {
|
|
|
716
748
|
id: this.id(),
|
|
717
749
|
url: this.url(true),
|
|
718
750
|
query: this.parsedUrl().search?.slice(1) ?? '',
|
|
719
|
-
|
|
751
|
+
// A dump must not throw: an over-limit body is reported as absent.
|
|
752
|
+
body: this.#bodyWasDropped() ? undefined : this.body(),
|
|
720
753
|
params: this.params(),
|
|
721
754
|
headers: this.headers(),
|
|
722
755
|
method: this.method(),
|
|
@@ -1062,8 +1095,33 @@ export class Request extends Macroable {
|
|
|
1062
1095
|
return this.#validated
|
|
1063
1096
|
}
|
|
1064
1097
|
|
|
1098
|
+
/** The body was over its limit and nothing has replaced it since. */
|
|
1099
|
+
#bodyWasDropped(): boolean {
|
|
1100
|
+
return (
|
|
1101
|
+
this.#raw.bodyTooLarge === true &&
|
|
1102
|
+
this.#parsedBody === undefined &&
|
|
1103
|
+
this.#rawBodyOverride === undefined
|
|
1104
|
+
)
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Refuse to read a body the server did not keep. The same 413 the body
|
|
1109
|
+
* parser raises, so a route without it still answers the client properly
|
|
1110
|
+
* the moment it reaches for the body — and a route that never does is not
|
|
1111
|
+
* charged for it.
|
|
1112
|
+
*/
|
|
1113
|
+
#assertBodyKept(): void {
|
|
1114
|
+
if (this.#raw.bodyTooLarge === true) {
|
|
1115
|
+
throw new Exception('request entity too large', {
|
|
1116
|
+
status: 413,
|
|
1117
|
+
code: 'E_REQUEST_ENTITY_TOO_LARGE',
|
|
1118
|
+
})
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1065
1122
|
#ensureParsedBody(): void {
|
|
1066
1123
|
if (this.#parsedBody !== undefined) return
|
|
1124
|
+
this.#assertBodyKept()
|
|
1067
1125
|
const raw = this.#raw.body
|
|
1068
1126
|
if (!raw || raw.length === 0) {
|
|
1069
1127
|
this.#parsedBody = {}
|
package/src/http/SseStream.ts
CHANGED
|
@@ -253,14 +253,13 @@ function encodeFrame(event: string, data: string, eventId?: string): string {
|
|
|
253
253
|
const lines: string[] = []
|
|
254
254
|
// `event:` and `id:` are single-line tokens — strip CR/LF so a
|
|
255
255
|
// caller-supplied event name or id can't splice extra SSE frames
|
|
256
|
-
// (the same injection `comment()` guards against).
|
|
257
|
-
// it's split on `\n` and each line re-prefixed, so newlines there
|
|
258
|
-
// stay inside one logical event.
|
|
256
|
+
// (the same injection `comment()` guards against).
|
|
259
257
|
if (event) lines.push(`event: ${event.replace(/[\r\n]+/g, ' ')}`)
|
|
260
|
-
// Multi-line data is allowed
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
|
|
258
|
+
// Multi-line data is allowed but each line must repeat the `data: `
|
|
259
|
+
// prefix. The event-stream parser ends a line on CRLF, LF AND a lone
|
|
260
|
+
// CR, so split on all three: a payload CR left in place would end the
|
|
261
|
+
// line and let the rest of the payload forge its own fields.
|
|
262
|
+
for (const line of data.split(/\r\n|\r|\n/)) {
|
|
264
263
|
lines.push(`data: ${line}`)
|
|
265
264
|
}
|
|
266
265
|
if (eventId) lines.push(`id: ${eventId.replace(/[\r\n]+/g, ' ')}`)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The permission check shared by the route, RPC and GraphQL gates, so the
|
|
3
|
+
* three cannot drift apart.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AuthState } from './HttpContext.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Whether the request's identity holds every permission in `required` — what
|
|
10
|
+
* the route, RPC and GraphQL `.permission()` gates ask.
|
|
11
|
+
*
|
|
12
|
+
* The permissions are the user's own (top-level `auth.permissions`, or
|
|
13
|
+
* `auth.user.permissions` where warden puts them). When the request was
|
|
14
|
+
* authenticated with a token, each permission must ALSO be allowed by
|
|
15
|
+
* `auth.user.currentAccessToken`: an intersection, never a union. A token
|
|
16
|
+
* scope named like a permission grants nothing the user lacks, and a token
|
|
17
|
+
* limited to `profile` does not carry every permission of its user. A token
|
|
18
|
+
* whose `allows` cannot be called denies.
|
|
19
|
+
*/
|
|
20
|
+
export function holdsPermissions(
|
|
21
|
+
auth: AuthState | undefined,
|
|
22
|
+
required: readonly string[],
|
|
23
|
+
): boolean {
|
|
24
|
+
const granted = auth?.permissions ?? auth?.user?.permissions ?? []
|
|
25
|
+
const token = auth?.user?.currentAccessToken
|
|
26
|
+
return required.every((permission) => {
|
|
27
|
+
if (!granted.includes(permission)) return false
|
|
28
|
+
if (token === undefined || token === null) return true
|
|
29
|
+
return typeof token.allows === 'function' && token.allows(permission) === true
|
|
30
|
+
})
|
|
31
|
+
}
|