@c9up/ream 0.2.28 → 0.2.30
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/README.md +5 -6
- package/dist/Codemods.d.ts +24 -0
- package/dist/Codemods.d.ts.map +1 -1
- package/dist/Codemods.js +211 -1
- package/dist/Codemods.js.map +1 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +17 -5
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +15 -2
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +33 -23
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +4 -4
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +41 -11
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +21 -15
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +40 -58
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/bodyparser/parseSize.d.ts +7 -4
- package/dist/bodyparser/parseSize.d.ts.map +1 -1
- package/dist/bodyparser/parseSize.js +12 -17
- package/dist/bodyparser/parseSize.js.map +1 -1
- package/dist/bodyparser/qsParse.d.ts +20 -1
- package/dist/bodyparser/qsParse.d.ts.map +1 -1
- package/dist/bodyparser/qsParse.js +78 -25
- 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/console/prompts.d.ts.map +1 -1
- package/dist/console/prompts.js +3 -1
- package/dist/console/prompts.js.map +1 -1
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +61 -71
- package/dist/container/Container.js.map +1 -1
- package/dist/events/Emitter.d.ts +12 -0
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +36 -9
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/helix/helpers.d.ts.map +1 -1
- package/dist/events/helix/helpers.js +19 -9
- package/dist/events/helix/helpers.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +8 -0
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +79 -12
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/MessageBuilder.d.ts +4 -2
- package/dist/helpers/MessageBuilder.d.ts.map +1 -1
- package/dist/helpers/MessageBuilder.js +4 -2
- package/dist/helpers/MessageBuilder.js.map +1 -1
- package/dist/helpers/errors.d.ts +11 -0
- package/dist/helpers/errors.d.ts.map +1 -0
- package/dist/helpers/errors.js +17 -0
- package/dist/helpers/errors.js.map +1 -0
- package/dist/helpers/napi-loader.d.ts.map +1 -1
- package/dist/helpers/napi-loader.js +3 -2
- package/dist/helpers/napi-loader.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 +31 -2
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +12 -5
- 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 +43 -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 +80 -58
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +7 -4
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +14 -6
- package/dist/http/Response.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 +25 -0
- package/dist/http/permissions.d.ts.map +1 -0
- package/dist/http/permissions.js +38 -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 +93 -36
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/profiler/Profiler.d.ts.map +1 -1
- package/dist/profiler/Profiler.js +4 -1
- package/dist/profiler/Profiler.js.map +1 -1
- package/dist/profiler/client.generated.d.ts +5 -0
- package/dist/profiler/client.generated.d.ts.map +1 -0
- package/dist/profiler/client.generated.js +6 -0
- package/dist/profiler/client.generated.js.map +1 -0
- package/dist/profiler/collectors.d.ts +92 -13
- package/dist/profiler/collectors.d.ts.map +1 -1
- package/dist/profiler/collectors.js +106 -19
- package/dist/profiler/collectors.js.map +1 -1
- package/dist/profiler/endpoint.d.ts +2 -1
- package/dist/profiler/endpoint.d.ts.map +1 -1
- package/dist/profiler/endpoint.js +25 -1
- package/dist/profiler/endpoint.js.map +1 -1
- package/dist/profiler/index.d.ts +8 -4
- package/dist/profiler/index.d.ts.map +1 -1
- package/dist/profiler/index.js +11 -4
- package/dist/profiler/index.js.map +1 -1
- package/dist/profiler/middleware.d.ts.map +1 -1
- package/dist/profiler/middleware.js +76 -10
- package/dist/profiler/middleware.js.map +1 -1
- package/dist/profiler/preview.d.ts +17 -0
- package/dist/profiler/preview.d.ts.map +1 -0
- package/dist/profiler/preview.js +100 -0
- package/dist/profiler/preview.js.map +1 -0
- package/dist/profiler/toolbar.d.ts +15 -23
- package/dist/profiler/toolbar.d.ts.map +1 -1
- package/dist/profiler/toolbar.js +25 -91
- package/dist/profiler/toolbar.js.map +1 -1
- package/dist/profiler/types.d.ts +6 -1
- package/dist/profiler/types.d.ts.map +1 -1
- package/dist/router/Router.d.ts +6 -0
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +115 -12
- package/dist/router/Router.js.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 +97 -40
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/Scheduler.js +1 -1
- package/dist/scheduler/Scheduler.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 +32 -8
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.d.ts.map +1 -1
- package/dist/security/SignedUrl.js +16 -19
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/server/Server.d.ts +44 -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 +57 -0
- package/dist/server/nativeLimits.d.ts.map +1 -0
- package/dist/server/nativeLimits.js +75 -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 +13 -4
- 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 +26 -7
- 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 +49 -27
- package/dist/storage/StaticMiddleware.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +7 -1
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +16 -4
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +1 -1
- package/dist/utils/Macroable.js +2 -2
- package/dist/utils/Macroable.js.map +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.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 -3
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Codemods.ts +314 -3
- package/src/HttpKernel.ts +18 -7
- package/src/Ignitor.ts +58 -26
- package/src/bodyparser/BodyParserMiddleware.ts +45 -16
- package/src/bodyparser/MultipartFile.ts +46 -67
- package/src/bodyparser/parseSize.ts +17 -17
- package/src/bodyparser/qsParse.ts +96 -26
- package/src/codemods/source.ts +303 -0
- package/src/console/prompts.ts +3 -1
- package/src/container/Container.ts +97 -81
- package/src/events/Emitter.ts +37 -10
- package/src/events/helix/helpers.ts +24 -19
- package/src/graphql/GraphQLEngine.ts +91 -14
- package/src/helpers/MessageBuilder.ts +4 -2
- package/src/helpers/errors.ts +16 -0
- package/src/helpers/napi-loader.ts +3 -2
- package/src/http/Exception.ts +17 -2
- package/src/http/HttpContext.ts +34 -5
- package/src/http/RedirectBuilder.ts +51 -37
- package/src/http/Request.ts +102 -58
- package/src/http/Response.ts +15 -7
- package/src/http/SseStream.ts +6 -7
- package/src/http/permissions.ts +47 -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 +6 -1
- package/src/middleware/Pipeline.ts +7 -3
- package/src/native/generated.ts +106 -48
- package/src/profiler/Profiler.ts +4 -1
- package/src/profiler/client/toolbar.css +595 -0
- package/src/profiler/client/toolbar.js +1299 -0
- package/src/profiler/client.generated.ts +7 -0
- package/src/profiler/collectors.ts +156 -19
- package/src/profiler/endpoint.ts +27 -1
- package/src/profiler/index.ts +17 -3
- package/src/profiler/middleware.ts +74 -9
- package/src/profiler/preview.ts +104 -0
- package/src/profiler/toolbar.ts +28 -91
- package/src/profiler/types.ts +6 -1
- package/src/router/Router.ts +136 -13
- package/src/rpc/RpcProvider.ts +10 -1
- package/src/rpc/RpcRouter.ts +129 -45
- package/src/scheduler/Scheduler.ts +1 -1
- package/src/scheduler/locks/RedisLockBackend.ts +12 -4
- package/src/security/CookieSigner.ts +30 -8
- package/src/security/SignedUrl.ts +18 -17
- package/src/server/Server.ts +60 -12
- package/src/server/nativeGrpc.ts +59 -0
- package/src/server/nativeLimits.ts +111 -0
- package/src/session/Session.ts +29 -4
- package/src/session/SessionMiddleware.ts +30 -10
- package/src/session/config.ts +7 -0
- package/src/session/drivers/MemoryDriver.ts +26 -7
- package/src/session/errors.ts +31 -0
- package/src/session/templateTags.ts +57 -0
- package/src/storage/StaticMiddleware.ts +46 -27
- package/src/testing/ApiResponse.ts +7 -1
- package/src/testing/TestClient.ts +14 -5
- package/src/utils/Macroable.ts +2 -2
- package/scripts/build-napi-types.mjs +0 -69
- package/scripts/copy-napi.mjs +0 -121
- package/scripts/generate-mime.mjs +0 -119
- package/scripts/generate-napi-types.mjs +0 -187
|
@@ -13,30 +13,39 @@ 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
|
+
// Only a web URL is a redirect target: `javascript://app.test/%0aalert(1)`
|
|
36
|
+
// has the right host and is a script, not a page.
|
|
37
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false
|
|
38
|
+
if (allowedHosts.includes(parsed.host)) return true
|
|
39
|
+
// Upstream accepts any absolute URL when it knows no host to compare with;
|
|
40
|
+
// refusing it is the safer reading of an attacker-controlled header.
|
|
41
|
+
return authority !== undefined && parsed.host === authority
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The query string of a path or URL, without the "?". */
|
|
45
|
+
function searchOf(url: string | undefined): string {
|
|
46
|
+
if (url === undefined) return ''
|
|
47
|
+
const index = url.indexOf('?')
|
|
48
|
+
return index === -1 ? '' : url.slice(index + 1)
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
/** The slice of the session the intended-URL methods use. */
|
|
@@ -60,6 +69,7 @@ export class RedirectBuilder {
|
|
|
60
69
|
#forwardQs = false
|
|
61
70
|
#requestUrl?: string
|
|
62
71
|
#requestReferer?: string
|
|
72
|
+
#requestAuthority?: string
|
|
63
73
|
#routeUrlResolver?: RouteUrlResolver
|
|
64
74
|
#session?: IntendedUrlStore
|
|
65
75
|
#isAjax = false
|
|
@@ -68,8 +78,14 @@ export class RedirectBuilder {
|
|
|
68
78
|
constructor(
|
|
69
79
|
response: Response,
|
|
70
80
|
options?: {
|
|
81
|
+
/** The request path with its query string. */
|
|
71
82
|
requestUrl?: string
|
|
72
83
|
requestReferer?: string
|
|
84
|
+
/**
|
|
85
|
+
* The request authority (`:authority` or `Host`, never X-Forwarded-Host)
|
|
86
|
+
* an absolute redirect target must match.
|
|
87
|
+
*/
|
|
88
|
+
requestAuthority?: string
|
|
73
89
|
routeUrlResolver?: RouteUrlResolver
|
|
74
90
|
/** The request session, for the intended-URL methods. */
|
|
75
91
|
session?: IntendedUrlStore
|
|
@@ -82,6 +98,7 @@ export class RedirectBuilder {
|
|
|
82
98
|
this.#response = response
|
|
83
99
|
this.#requestUrl = options?.requestUrl
|
|
84
100
|
this.#requestReferer = options?.requestReferer
|
|
101
|
+
this.#requestAuthority = options?.requestAuthority
|
|
85
102
|
this.#routeUrlResolver = options?.routeUrlResolver
|
|
86
103
|
this.#session = options?.session
|
|
87
104
|
this.#isAjax = options?.isAjax ?? false
|
|
@@ -135,13 +152,9 @@ export class RedirectBuilder {
|
|
|
135
152
|
|
|
136
153
|
#safeIntended<T extends string | null>(intended: string | null, fallback: T): string | T {
|
|
137
154
|
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
|
|
155
|
+
return isTrustedRedirect(intended, this.#requestAuthority, this.allowedHosts)
|
|
156
|
+
? intended
|
|
157
|
+
: fallback
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
/** Set redirect status code. */
|
|
@@ -191,7 +204,12 @@ export class RedirectBuilder {
|
|
|
191
204
|
|
|
192
205
|
/** Redirect to an absolute or relative path. */
|
|
193
206
|
toPath(path: string): void {
|
|
194
|
-
|
|
207
|
+
this.#send(path, searchOf(this.#requestUrl))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Redirect with `withQs()` forwarding `forwardedSearch` (a query string without "?"). */
|
|
211
|
+
#send(path: string, forwardedSearch: string): void {
|
|
212
|
+
const url = this.#appendQs(path, forwardedSearch)
|
|
195
213
|
this.#response.status(this.#status)
|
|
196
214
|
this.#response.header('location', url)
|
|
197
215
|
this.#response.setBody('')
|
|
@@ -207,12 +225,13 @@ export class RedirectBuilder {
|
|
|
207
225
|
}
|
|
208
226
|
|
|
209
227
|
/**
|
|
210
|
-
* Redirect back to the previous page (
|
|
211
|
-
*
|
|
212
|
-
*
|
|
228
|
+
* Redirect back to the previous page (AdonisJS `back`): the path of
|
|
229
|
+
* {@link getPreviousUrl}, so the redirect never leaves the site; `withQs()`
|
|
230
|
+
* forwards the previous page's query string, not the current request's.
|
|
213
231
|
*/
|
|
214
232
|
back(fallback = '/'): void {
|
|
215
|
-
|
|
233
|
+
const previous = new URL(this.getPreviousUrl(fallback), 'http://localhost')
|
|
234
|
+
this.#send(previous.pathname, previous.search.slice(1))
|
|
216
235
|
}
|
|
217
236
|
|
|
218
237
|
/**
|
|
@@ -225,7 +244,7 @@ export class RedirectBuilder {
|
|
|
225
244
|
*/
|
|
226
245
|
getPreviousUrl(fallback = '/'): string {
|
|
227
246
|
const referer = this.#requestReferer
|
|
228
|
-
if (referer &&
|
|
247
|
+
if (referer && isTrustedRedirect(referer, this.#requestAuthority, this.allowedHosts)) {
|
|
229
248
|
return referer
|
|
230
249
|
}
|
|
231
250
|
return fallback
|
|
@@ -236,15 +255,10 @@ export class RedirectBuilder {
|
|
|
236
255
|
* uses, so `withQs()` plus `withQs('page', 2)` overrides an inbound `page`
|
|
237
256
|
* rather than being overridden by it.
|
|
238
257
|
*/
|
|
239
|
-
#appendQs(path: string): string {
|
|
258
|
+
#appendQs(path: string, forwardedSearch: string): string {
|
|
240
259
|
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
|
-
}
|
|
260
|
+
if (this.#forwardQs) {
|
|
261
|
+
for (const [key, value] of new URLSearchParams(forwardedSearch)) params.append(key, value)
|
|
248
262
|
}
|
|
249
263
|
for (const [key, value] of Object.entries(this.#qs)) {
|
|
250
264
|
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
|
|
|
@@ -455,7 +478,9 @@ export class Request extends Macroable {
|
|
|
455
478
|
encryptedCookie(name: string, defaultValue?: string): string | null
|
|
456
479
|
encryptedCookie(name: string, defaultValue?: string): string | null {
|
|
457
480
|
const raw = this.plainCookie<string>(name, undefined, { encoded: false })
|
|
458
|
-
|
|
481
|
+
// Decrypted for the purpose it was written under — its own name — so a
|
|
482
|
+
// value issued as another encrypted cookie is refused here.
|
|
483
|
+
const value = raw === null || !this.#cookieSigner ? null : this.#cookieSigner.decrypt(raw, name)
|
|
459
484
|
return value ?? defaultValue ?? null
|
|
460
485
|
}
|
|
461
486
|
|
|
@@ -477,17 +502,26 @@ export class Request extends Macroable {
|
|
|
477
502
|
return this.#signedUrl.verify(this.url(true), purpose)
|
|
478
503
|
}
|
|
479
504
|
|
|
480
|
-
/**
|
|
505
|
+
/**
|
|
506
|
+
* Get the raw body as a string (decoded from base64 if binary). Throws
|
|
507
|
+
* `E_REQUEST_ENTITY_TOO_LARGE` (413) when the body was over its limit: what
|
|
508
|
+
* the server kept of it is nothing, and nothing is not the body.
|
|
509
|
+
*/
|
|
481
510
|
raw(): string {
|
|
482
511
|
if (this.#rawBodyOverride !== undefined) return this.#rawBodyOverride
|
|
512
|
+
this.#assertBodyKept()
|
|
483
513
|
if (this.#raw.bodyEncoding === 'base64') {
|
|
484
514
|
return Buffer.from(this.#raw.body, 'base64').toString('utf8')
|
|
485
515
|
}
|
|
486
516
|
return this.#raw.body
|
|
487
517
|
}
|
|
488
518
|
|
|
489
|
-
/**
|
|
519
|
+
/**
|
|
520
|
+
* Get the raw body as a Buffer (preserves binary integrity for file uploads).
|
|
521
|
+
* Throws `E_REQUEST_ENTITY_TOO_LARGE` (413) when the body was over its limit.
|
|
522
|
+
*/
|
|
490
523
|
rawBuffer(): Buffer {
|
|
524
|
+
this.#assertBodyKept()
|
|
491
525
|
if (this.#raw.bodyEncoding === 'base64') {
|
|
492
526
|
return Buffer.from(this.#raw.body, 'base64')
|
|
493
527
|
}
|
|
@@ -716,7 +750,8 @@ export class Request extends Macroable {
|
|
|
716
750
|
id: this.id(),
|
|
717
751
|
url: this.url(true),
|
|
718
752
|
query: this.parsedUrl().search?.slice(1) ?? '',
|
|
719
|
-
|
|
753
|
+
// A dump must not throw: an over-limit body is reported as absent.
|
|
754
|
+
body: this.#bodyWasDropped() ? undefined : this.body(),
|
|
720
755
|
params: this.params(),
|
|
721
756
|
headers: this.headers(),
|
|
722
757
|
method: this.method(),
|
|
@@ -724,7 +759,9 @@ export class Request extends Macroable {
|
|
|
724
759
|
cookies: this.cookies(),
|
|
725
760
|
hostname: this.hostname(),
|
|
726
761
|
ip: this.ip(),
|
|
727
|
-
|
|
762
|
+
// What the route's domain captured (`ctx.subdomains`), as upstream
|
|
763
|
+
// serialises it — not the host's labels, which `subdomains()` returns.
|
|
764
|
+
subdomains: (this.ctx === undefined ? undefined : Reflect.get(this.ctx, 'subdomains')) ?? {},
|
|
728
765
|
}
|
|
729
766
|
}
|
|
730
767
|
|
|
@@ -1062,38 +1099,45 @@ export class Request extends Macroable {
|
|
|
1062
1099
|
return this.#validated
|
|
1063
1100
|
}
|
|
1064
1101
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
this.#parsedBody
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1102
|
+
/** The body was over its limit and nothing has replaced it since. */
|
|
1103
|
+
#bodyWasDropped(): boolean {
|
|
1104
|
+
return (
|
|
1105
|
+
this.#raw.bodyTooLarge === true &&
|
|
1106
|
+
this.#parsedBody === undefined &&
|
|
1107
|
+
this.#rawBodyOverride === undefined
|
|
1108
|
+
)
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Refuse to read a body the server did not keep. The same 413 the body
|
|
1113
|
+
* parser raises, so a route without it still answers the client properly
|
|
1114
|
+
* the moment it reaches for the body — and a route that never does is not
|
|
1115
|
+
* charged for it.
|
|
1116
|
+
*/
|
|
1117
|
+
#assertBodyKept(): void {
|
|
1118
|
+
if (this.#raw.bodyTooLarge === true) {
|
|
1119
|
+
throw new Exception('request entity too large', {
|
|
1120
|
+
status: 413,
|
|
1121
|
+
code: 'E_REQUEST_ENTITY_TOO_LARGE',
|
|
1122
|
+
})
|
|
1077
1123
|
}
|
|
1078
1124
|
}
|
|
1079
|
-
}
|
|
1080
1125
|
|
|
1081
|
-
/**
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1126
|
+
/**
|
|
1127
|
+
* The body is whatever the body parser decided it is, and `{}` when no
|
|
1128
|
+
* parser claimed it. Nothing is parsed here: a JSON body is only JSON when
|
|
1129
|
+
* its Content-Type says so. Reading any body as JSON let a foreign page
|
|
1130
|
+
* drive a JSON route with a CORS simple request (no Content-Type, or
|
|
1131
|
+
* `text/plain`), which a browser sends cross-site with the user's cookies
|
|
1132
|
+
* and without a preflight.
|
|
1133
|
+
*/
|
|
1134
|
+
#ensureParsedBody(): void {
|
|
1135
|
+
if (this.#parsedBody !== undefined) return
|
|
1136
|
+
this.#assertBodyKept()
|
|
1137
|
+
this.#parsedBody = {}
|
|
1138
|
+
}
|
|
1094
1139
|
}
|
|
1095
1140
|
|
|
1096
|
-
/** True for an IPv4 dotted-quad or a (bracketed) IPv6 literal — hosts with no subdomains. */
|
|
1097
1141
|
/**
|
|
1098
1142
|
* Headers whose VALUE is a credential. Listing them is useful; printing them
|
|
1099
1143
|
* hands whoever reads the log a working key.
|
package/src/http/Response.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { readFile, stat } from 'node:fs/promises'
|
|
|
13
13
|
import { basename } from 'node:path'
|
|
14
14
|
import { ReamError } from '../errors/ReamError.js'
|
|
15
15
|
import { durationToSeconds } from '../helpers/duration.js'
|
|
16
|
+
import { toErrnoException } from '../helpers/errors.js'
|
|
16
17
|
import { etag, matchesIfNoneMatch } from './etag.js'
|
|
17
18
|
import { contentType } from './mime.js'
|
|
18
19
|
|
|
@@ -848,7 +849,7 @@ export class Response extends Macroable {
|
|
|
848
849
|
size = (await stat(filePath)).size
|
|
849
850
|
} catch (error) {
|
|
850
851
|
if (errorCallback) {
|
|
851
|
-
const [message, status] = errorCallback(error
|
|
852
|
+
const [message, status] = errorCallback(toErrnoException(error))
|
|
852
853
|
this.status(status ?? 404).send(message)
|
|
853
854
|
return
|
|
854
855
|
}
|
|
@@ -1002,7 +1003,7 @@ export class Response extends Macroable {
|
|
|
1002
1003
|
// The headers are already out, so there is no status left to change: end
|
|
1003
1004
|
// the body and let the client see a short read. `errorCallback` is still
|
|
1004
1005
|
// consulted for whatever logging it does.
|
|
1005
|
-
errorCallback?.(err
|
|
1006
|
+
errorCallback?.(toErrnoException(err))
|
|
1006
1007
|
} finally {
|
|
1007
1008
|
// Never rethrows: this runs detached, and an unhandled rejection here
|
|
1008
1009
|
// would take the process down over one client's download.
|
|
@@ -1022,7 +1023,7 @@ export class Response extends Macroable {
|
|
|
1022
1023
|
}
|
|
1023
1024
|
} catch (err) {
|
|
1024
1025
|
const [message, status] = errorCallback
|
|
1025
|
-
? errorCallback(err
|
|
1026
|
+
? errorCallback(toErrnoException(err))
|
|
1026
1027
|
: ['Cannot process the request', 500]
|
|
1027
1028
|
this.status(status ?? 500)
|
|
1028
1029
|
this.send(message)
|
|
@@ -1133,7 +1134,9 @@ export class Response extends Macroable {
|
|
|
1133
1134
|
'encryptedCookie() requires APP_KEY — set it so the encryption service is registered.',
|
|
1134
1135
|
)
|
|
1135
1136
|
}
|
|
1136
|
-
|
|
1137
|
+
// The cookie name is the purpose, as for a signed cookie: a value issued
|
|
1138
|
+
// as one encrypted cookie cannot be replayed under another one's name.
|
|
1139
|
+
return this.#writeCookie(name, this.#cookieSigner.encrypt(value, undefined, name), options)
|
|
1137
1140
|
}
|
|
1138
1141
|
|
|
1139
1142
|
#writeCookie(name: string, value: string, options?: CookieOptions): this {
|
|
@@ -1195,10 +1198,15 @@ export class Response extends Macroable {
|
|
|
1195
1198
|
|
|
1196
1199
|
/**
|
|
1197
1200
|
* Expire a cookie immediately (AdonisJS `clearCookie`) — re-sends it with
|
|
1198
|
-
* `Max-Age=0
|
|
1201
|
+
* `Max-Age=0` and an `Expires` in 1970, the RFC 6265 delete-now signals.
|
|
1202
|
+
*
|
|
1203
|
+
* A browser matches the cookie to replace on its name, domain and path, so
|
|
1204
|
+
* a cookie written with a `domain` (or a `path`) is only cleared when the
|
|
1205
|
+
* same ones are passed here; `secure` and `sameSite` are accepted so a
|
|
1206
|
+
* `SameSite=None` cookie can be cleared at all.
|
|
1199
1207
|
*/
|
|
1200
|
-
clearCookie(name: string, options?:
|
|
1201
|
-
return this.plainCookie(name, '', { ...options, maxAge: 0 })
|
|
1208
|
+
clearCookie(name: string, options?: Omit<CookieOptions, 'maxAge' | 'expires'>): this {
|
|
1209
|
+
return this.plainCookie(name, '', { ...options, maxAge: 0, expires: new Date(0) })
|
|
1202
1210
|
}
|
|
1203
1211
|
|
|
1204
1212
|
// ─── Internals (used by HttpKernel for NAPI serialization) ─
|
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,47 @@
|
|
|
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
|
+
* When the auth state exposes `hasAllPermissions` (the auth layer resolves
|
|
13
|
+
* permissions itself, as warden does through its rights resolver), its
|
|
14
|
+
* answer decides: a permission granted only by the resolver passes, one left
|
|
15
|
+
* only in the payload does not. Anything but `true` denies, and an error from
|
|
16
|
+
* the resolver propagates rather than grants. Only an auth state without that
|
|
17
|
+
* method falls back to the payload: top-level `auth.permissions`, or
|
|
18
|
+
* `auth.user.permissions`.
|
|
19
|
+
*
|
|
20
|
+
* When the request was authenticated with a token, each permission must ALSO
|
|
21
|
+
* be allowed by `auth.user.currentAccessToken`: an intersection, never a
|
|
22
|
+
* union. A token scope named like a permission grants nothing the user
|
|
23
|
+
* lacks, and a token limited to `profile` does not carry every permission of
|
|
24
|
+
* its user. A token whose `allows` cannot be called denies.
|
|
25
|
+
*/
|
|
26
|
+
export async function holdsPermissions(
|
|
27
|
+
auth: AuthState | undefined,
|
|
28
|
+
required: readonly string[],
|
|
29
|
+
): Promise<boolean> {
|
|
30
|
+
if (!(await userHolds(auth, required))) return false
|
|
31
|
+
const token = auth?.user?.currentAccessToken
|
|
32
|
+
if (token === undefined || token === null) return true
|
|
33
|
+
return required.every(
|
|
34
|
+
(permission) => typeof token.allows === 'function' && token.allows(permission) === true,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function userHolds(
|
|
39
|
+
auth: AuthState | undefined,
|
|
40
|
+
required: readonly string[],
|
|
41
|
+
): Promise<boolean> {
|
|
42
|
+
if (typeof auth?.hasAllPermissions === 'function') {
|
|
43
|
+
return (await auth.hasAllPermissions(required)) === true
|
|
44
|
+
}
|
|
45
|
+
const granted = auth?.permissions ?? auth?.user?.permissions ?? []
|
|
46
|
+
return required.every((permission) => granted.includes(permission))
|
|
47
|
+
}
|