@angular/ssr 20.3.15 → 20.3.17

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.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * The set of headers that should be validated for host header injection attacks.
3
+ */
4
+ const HOST_HEADERS_TO_VALIDATE = new Set(['host', 'x-forwarded-host']);
5
+ /**
6
+ * Regular expression to validate that the port is a numeric value.
7
+ */
8
+ const VALID_PORT_REGEX = /^\d+$/;
9
+ /**
10
+ * Regular expression to validate that the protocol is either http or https (case-insensitive).
11
+ */
12
+ const VALID_PROTO_REGEX = /^https?$/i;
13
+ /**
14
+ * Regular expression to validate that the host is a valid hostname.
15
+ */
16
+ const VALID_HOST_REGEX = /^[a-z0-9.:-]+$/i;
17
+ /**
18
+ * Regular expression to validate that the prefix is valid.
19
+ */
20
+ const INVALID_PREFIX_REGEX = /^[/\\]{2}|(?:^|[/\\])\.\.?(?:[/\\]|$)/;
21
+ /**
22
+ * Extracts the first value from a multi-value header string.
23
+ *
24
+ * @param value - A string or an array of strings representing the header values.
25
+ * If it's a string, values are expected to be comma-separated.
26
+ * @returns The first trimmed value from the multi-value header, or `undefined` if the input is invalid or empty.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * getFirstHeaderValue("value1, value2, value3"); // "value1"
31
+ * getFirstHeaderValue(["value1", "value2"]); // "value1"
32
+ * getFirstHeaderValue(undefined); // undefined
33
+ * ```
34
+ */
35
+ function getFirstHeaderValue(value) {
36
+ return value?.toString().split(',', 1)[0]?.trim();
37
+ }
38
+ /**
39
+ * Validates a request.
40
+ *
41
+ * @param request - The incoming `Request` object to validate.
42
+ * @param allowedHosts - A set of allowed hostnames.
43
+ * @throws Error if any of the validated headers contain invalid values.
44
+ */
45
+ function validateRequest(request, allowedHosts) {
46
+ validateHeaders(request);
47
+ validateUrl(new URL(request.url), allowedHosts);
48
+ }
49
+ /**
50
+ * Validates that the hostname of a given URL is allowed.
51
+ *
52
+ * @param url - The URL object to validate.
53
+ * @param allowedHosts - A set of allowed hostnames.
54
+ * @throws Error if the hostname is not in the allowlist.
55
+ */
56
+ function validateUrl(url, allowedHosts) {
57
+ const { hostname } = url;
58
+ if (!isHostAllowed(hostname, allowedHosts)) {
59
+ throw new Error(`URL with hostname "${hostname}" is not allowed.`);
60
+ }
61
+ }
62
+ /**
63
+ * Clones a request and patches the `get` method of the request headers to validate the host headers.
64
+ * @param request - The request to validate.
65
+ * @param allowedHosts - A set of allowed hostnames.
66
+ * @returns An object containing the cloned request and a promise that resolves to an error
67
+ * if any of the validated headers contain invalid values.
68
+ */
69
+ function cloneRequestAndPatchHeaders(request, allowedHosts) {
70
+ let onError;
71
+ const onErrorPromise = new Promise((resolve) => {
72
+ onError = resolve;
73
+ });
74
+ const clonedReq = new Request(request.clone(), {
75
+ signal: request.signal,
76
+ });
77
+ const headers = clonedReq.headers;
78
+ const originalGet = headers.get;
79
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
80
+ headers.get = function (name) {
81
+ const value = originalGet.call(headers, name);
82
+ if (!value) {
83
+ return value;
84
+ }
85
+ validateHeader(name, value, allowedHosts, onError);
86
+ return value;
87
+ };
88
+ const originalValues = headers.values;
89
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
90
+ headers.values = function () {
91
+ for (const name of HOST_HEADERS_TO_VALIDATE) {
92
+ validateHeader(name, originalGet.call(headers, name), allowedHosts, onError);
93
+ }
94
+ return originalValues.call(headers);
95
+ };
96
+ const originalEntries = headers.entries;
97
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
98
+ headers.entries = function () {
99
+ const iterator = originalEntries.call(headers);
100
+ return {
101
+ next() {
102
+ const result = iterator.next();
103
+ if (!result.done) {
104
+ const [key, value] = result.value;
105
+ validateHeader(key, value, allowedHosts, onError);
106
+ }
107
+ return result;
108
+ },
109
+ [Symbol.iterator]() {
110
+ return this;
111
+ },
112
+ };
113
+ };
114
+ // Ensure for...of loops use the new patched entries
115
+ headers[Symbol.iterator] = headers.entries;
116
+ return { request: clonedReq, onError: onErrorPromise };
117
+ }
118
+ /**
119
+ * Validates a specific header value against the allowed hosts.
120
+ * @param name - The name of the header to validate.
121
+ * @param value - The value of the header to validate.
122
+ * @param allowedHosts - A set of allowed hostnames.
123
+ * @param onError - A callback function to call if the header value is invalid.
124
+ * @throws Error if the header value is invalid.
125
+ */
126
+ function validateHeader(name, value, allowedHosts, onError) {
127
+ if (!value) {
128
+ return;
129
+ }
130
+ if (!HOST_HEADERS_TO_VALIDATE.has(name.toLowerCase())) {
131
+ return;
132
+ }
133
+ try {
134
+ verifyHostAllowed(name, value, allowedHosts);
135
+ }
136
+ catch (error) {
137
+ onError(error);
138
+ throw error;
139
+ }
140
+ }
141
+ /**
142
+ * Validates a specific host header value against the allowed hosts.
143
+ *
144
+ * @param headerName - The name of the header to validate (e.g., 'host', 'x-forwarded-host').
145
+ * @param headerValue - The value of the header to validate.
146
+ * @param allowedHosts - A set of allowed hostnames.
147
+ * @throws Error if the header value is invalid or the hostname is not in the allowlist.
148
+ */
149
+ function verifyHostAllowed(headerName, headerValue, allowedHosts) {
150
+ const value = getFirstHeaderValue(headerValue);
151
+ if (!value) {
152
+ return;
153
+ }
154
+ const url = `http://${value}`;
155
+ if (!URL.canParse(url)) {
156
+ throw new Error(`Header "${headerName}" contains an invalid value and cannot be parsed.`);
157
+ }
158
+ const { hostname } = new URL(url);
159
+ if (!isHostAllowed(hostname, allowedHosts)) {
160
+ throw new Error(`Header "${headerName}" with value "${value}" is not allowed.`);
161
+ }
162
+ }
163
+ /**
164
+ * Checks if the hostname is allowed.
165
+ * @param hostname - The hostname to check.
166
+ * @param allowedHosts - A set of allowed hostnames.
167
+ * @returns `true` if the hostname is allowed, `false` otherwise.
168
+ */
169
+ function isHostAllowed(hostname, allowedHosts) {
170
+ if (allowedHosts.has(hostname)) {
171
+ return true;
172
+ }
173
+ for (const allowedHost of allowedHosts) {
174
+ if (!allowedHost.startsWith('*.')) {
175
+ continue;
176
+ }
177
+ const domain = allowedHost.slice(1);
178
+ if (hostname.endsWith(domain)) {
179
+ return true;
180
+ }
181
+ }
182
+ return false;
183
+ }
184
+ /**
185
+ * Validates the headers of an incoming request.
186
+ *
187
+ * @param request - The incoming `Request` object containing the headers to validate.
188
+ * @throws Error if any of the validated headers contain invalid values.
189
+ */
190
+ function validateHeaders(request) {
191
+ const headers = request.headers;
192
+ for (const headerName of HOST_HEADERS_TO_VALIDATE) {
193
+ const headerValue = getFirstHeaderValue(headers.get(headerName));
194
+ if (headerValue && !VALID_HOST_REGEX.test(headerValue)) {
195
+ throw new Error(`Header "${headerName}" contains characters that are not allowed.`);
196
+ }
197
+ }
198
+ const xForwardedPort = getFirstHeaderValue(headers.get('x-forwarded-port'));
199
+ if (xForwardedPort && !VALID_PORT_REGEX.test(xForwardedPort)) {
200
+ throw new Error('Header "x-forwarded-port" must be a numeric value.');
201
+ }
202
+ const xForwardedProto = getFirstHeaderValue(headers.get('x-forwarded-proto'));
203
+ if (xForwardedProto && !VALID_PROTO_REGEX.test(xForwardedProto)) {
204
+ throw new Error('Header "x-forwarded-proto" must be either "http" or "https".');
205
+ }
206
+ const xForwardedPrefix = getFirstHeaderValue(headers.get('x-forwarded-prefix'));
207
+ if (xForwardedPrefix && INVALID_PREFIX_REGEX.test(xForwardedPrefix)) {
208
+ throw new Error('Header "x-forwarded-prefix" must not start with multiple "/" or "\\" or contain ".", ".." path segments.');
209
+ }
210
+ }
211
+
212
+ export { cloneRequestAndPatchHeaders, getFirstHeaderValue, validateRequest, validateUrl };
213
+ //# sourceMappingURL=validation.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.mjs","sources":["../../../../../../k8-fastbuild-ST-199a4f3c4e20/bin/packages/angular/ssr/src/utils/validation.ts"],"sourcesContent":["/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\n/**\n * The set of headers that should be validated for host header injection attacks.\n */\nconst HOST_HEADERS_TO_VALIDATE: ReadonlySet<string> = new Set(['host', 'x-forwarded-host']);\n\n/**\n * Regular expression to validate that the port is a numeric value.\n */\nconst VALID_PORT_REGEX = /^\\d+$/;\n\n/**\n * Regular expression to validate that the protocol is either http or https (case-insensitive).\n */\nconst VALID_PROTO_REGEX = /^https?$/i;\n\n/**\n * Regular expression to validate that the host is a valid hostname.\n */\nconst VALID_HOST_REGEX = /^[a-z0-9.:-]+$/i;\n\n/**\n * Regular expression to validate that the prefix is valid.\n */\nconst INVALID_PREFIX_REGEX = /^[/\\\\]{2}|(?:^|[/\\\\])\\.\\.?(?:[/\\\\]|$)/;\n\n/**\n * Extracts the first value from a multi-value header string.\n *\n * @param value - A string or an array of strings representing the header values.\n * If it's a string, values are expected to be comma-separated.\n * @returns The first trimmed value from the multi-value header, or `undefined` if the input is invalid or empty.\n *\n * @example\n * ```typescript\n * getFirstHeaderValue(\"value1, value2, value3\"); // \"value1\"\n * getFirstHeaderValue([\"value1\", \"value2\"]); // \"value1\"\n * getFirstHeaderValue(undefined); // undefined\n * ```\n */\nexport function getFirstHeaderValue(\n value: string | string[] | undefined | null,\n): string | undefined {\n return value?.toString().split(',', 1)[0]?.trim();\n}\n\n/**\n * Validates a request.\n *\n * @param request - The incoming `Request` object to validate.\n * @param allowedHosts - A set of allowed hostnames.\n * @throws Error if any of the validated headers contain invalid values.\n */\nexport function validateRequest(request: Request, allowedHosts: ReadonlySet<string>): void {\n validateHeaders(request);\n validateUrl(new URL(request.url), allowedHosts);\n}\n\n/**\n * Validates that the hostname of a given URL is allowed.\n *\n * @param url - The URL object to validate.\n * @param allowedHosts - A set of allowed hostnames.\n * @throws Error if the hostname is not in the allowlist.\n */\nexport function validateUrl(url: URL, allowedHosts: ReadonlySet<string>): void {\n const { hostname } = url;\n if (!isHostAllowed(hostname, allowedHosts)) {\n throw new Error(`URL with hostname \"${hostname}\" is not allowed.`);\n }\n}\n\n/**\n * Clones a request and patches the `get` method of the request headers to validate the host headers.\n * @param request - The request to validate.\n * @param allowedHosts - A set of allowed hostnames.\n * @returns An object containing the cloned request and a promise that resolves to an error\n * if any of the validated headers contain invalid values.\n */\nexport function cloneRequestAndPatchHeaders(\n request: Request,\n allowedHosts: ReadonlySet<string>,\n): { request: Request; onError: Promise<Error> } {\n let onError: (value: Error) => void;\n const onErrorPromise = new Promise<Error>((resolve) => {\n onError = resolve;\n });\n\n const clonedReq = new Request(request.clone(), {\n signal: request.signal,\n });\n\n const headers = clonedReq.headers;\n\n const originalGet = headers.get;\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n (headers.get as typeof originalGet) = function (name) {\n const value = originalGet.call(headers, name);\n if (!value) {\n return value;\n }\n\n validateHeader(name, value, allowedHosts, onError);\n\n return value;\n };\n\n const originalValues = headers.values;\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n (headers.values as typeof originalValues) = function () {\n for (const name of HOST_HEADERS_TO_VALIDATE) {\n validateHeader(name, originalGet.call(headers, name), allowedHosts, onError);\n }\n\n return originalValues.call(headers);\n };\n\n const originalEntries = headers.entries;\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n (headers.entries as typeof originalEntries) = function () {\n const iterator = originalEntries.call(headers);\n\n return {\n next() {\n const result = iterator.next();\n if (!result.done) {\n const [key, value] = result.value;\n validateHeader(key, value, allowedHosts, onError);\n }\n\n return result;\n },\n [Symbol.iterator]() {\n return this;\n },\n };\n };\n\n // Ensure for...of loops use the new patched entries\n (headers[Symbol.iterator] as typeof originalEntries) = headers.entries;\n\n return { request: clonedReq, onError: onErrorPromise };\n}\n\n/**\n * Validates a specific header value against the allowed hosts.\n * @param name - The name of the header to validate.\n * @param value - The value of the header to validate.\n * @param allowedHosts - A set of allowed hostnames.\n * @param onError - A callback function to call if the header value is invalid.\n * @throws Error if the header value is invalid.\n */\nfunction validateHeader(\n name: string,\n value: string | null,\n allowedHosts: ReadonlySet<string>,\n onError: (value: Error) => void,\n): void {\n if (!value) {\n return;\n }\n\n if (!HOST_HEADERS_TO_VALIDATE.has(name.toLowerCase())) {\n return;\n }\n\n try {\n verifyHostAllowed(name, value, allowedHosts);\n } catch (error) {\n onError(error as Error);\n\n throw error;\n }\n}\n\n/**\n * Validates a specific host header value against the allowed hosts.\n *\n * @param headerName - The name of the header to validate (e.g., 'host', 'x-forwarded-host').\n * @param headerValue - The value of the header to validate.\n * @param allowedHosts - A set of allowed hostnames.\n * @throws Error if the header value is invalid or the hostname is not in the allowlist.\n */\nfunction verifyHostAllowed(\n headerName: string,\n headerValue: string,\n allowedHosts: ReadonlySet<string>,\n): void {\n const value = getFirstHeaderValue(headerValue);\n if (!value) {\n return;\n }\n\n const url = `http://${value}`;\n if (!URL.canParse(url)) {\n throw new Error(`Header \"${headerName}\" contains an invalid value and cannot be parsed.`);\n }\n\n const { hostname } = new URL(url);\n if (!isHostAllowed(hostname, allowedHosts)) {\n throw new Error(`Header \"${headerName}\" with value \"${value}\" is not allowed.`);\n }\n}\n\n/**\n * Checks if the hostname is allowed.\n * @param hostname - The hostname to check.\n * @param allowedHosts - A set of allowed hostnames.\n * @returns `true` if the hostname is allowed, `false` otherwise.\n */\nfunction isHostAllowed(hostname: string, allowedHosts: ReadonlySet<string>): boolean {\n if (allowedHosts.has(hostname)) {\n return true;\n }\n\n for (const allowedHost of allowedHosts) {\n if (!allowedHost.startsWith('*.')) {\n continue;\n }\n\n const domain = allowedHost.slice(1);\n if (hostname.endsWith(domain)) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Validates the headers of an incoming request.\n *\n * @param request - The incoming `Request` object containing the headers to validate.\n * @throws Error if any of the validated headers contain invalid values.\n */\nfunction validateHeaders(request: Request): void {\n const headers = request.headers;\n for (const headerName of HOST_HEADERS_TO_VALIDATE) {\n const headerValue = getFirstHeaderValue(headers.get(headerName));\n if (headerValue && !VALID_HOST_REGEX.test(headerValue)) {\n throw new Error(`Header \"${headerName}\" contains characters that are not allowed.`);\n }\n }\n\n const xForwardedPort = getFirstHeaderValue(headers.get('x-forwarded-port'));\n if (xForwardedPort && !VALID_PORT_REGEX.test(xForwardedPort)) {\n throw new Error('Header \"x-forwarded-port\" must be a numeric value.');\n }\n\n const xForwardedProto = getFirstHeaderValue(headers.get('x-forwarded-proto'));\n if (xForwardedProto && !VALID_PROTO_REGEX.test(xForwardedProto)) {\n throw new Error('Header \"x-forwarded-proto\" must be either \"http\" or \"https\".');\n }\n\n const xForwardedPrefix = getFirstHeaderValue(headers.get('x-forwarded-prefix'));\n if (xForwardedPrefix && INVALID_PREFIX_REGEX.test(xForwardedPrefix)) {\n throw new Error(\n 'Header \"x-forwarded-prefix\" must not start with multiple \"/\" or \"\\\\\" or contain \".\", \"..\" path segments.',\n );\n }\n}\n"],"names":[],"mappings":"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"}
package/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Type, EnvironmentProviders, Provider, ApplicationRef } from '@angular/core';
2
2
  import { DefaultExport } from '@angular/router';
3
3
  import { BootstrapContext } from '@angular/platform-browser';
4
+ import { Hooks } from './app-engine.js';
5
+ export { AngularAppEngine, AngularAppEngineOptions } from './app-engine.js';
4
6
  import Beasties from './third_party/beasties';
5
7
 
6
8
  /**
@@ -498,6 +500,10 @@ interface AngularAppEngineManifest {
498
500
  * - `value`: The url segment associated with that locale.
499
501
  */
500
502
  readonly supportedLocales: Readonly<Record<string, string>>;
503
+ /**
504
+ * A readonly array of allowed hostnames.
505
+ */
506
+ readonly allowedHosts: Readonly<string[]>;
501
507
  }
502
508
  /**
503
509
  * Manifest for a specific Angular server application, defining assets and bootstrap logic.
@@ -653,67 +659,6 @@ declare function extractRoutesAndCreateRouteTree(options: {
653
659
  errors: string[];
654
660
  }>;
655
661
 
656
- /**
657
- * Defines a handler function type for transforming HTML content.
658
- * This function receives an object with the HTML to be processed.
659
- *
660
- * @param ctx - An object containing the URL and HTML content to be transformed.
661
- * @returns The transformed HTML as a string or a promise that resolves to the transformed HTML.
662
- */
663
- type HtmlTransformHandler = (ctx: {
664
- url: URL;
665
- html: string;
666
- }) => string | Promise<string>;
667
- /**
668
- * Defines the names of available hooks for registering and triggering custom logic within the application.
669
- */
670
- type HookName = keyof HooksMapping;
671
- /**
672
- * Mapping of hook names to their corresponding handler types.
673
- */
674
- interface HooksMapping {
675
- 'html:transform:pre': HtmlTransformHandler;
676
- }
677
- /**
678
- * Manages a collection of hooks and provides methods to register and execute them.
679
- * Hooks are functions that can be invoked with specific arguments to allow modifications or enhancements.
680
- */
681
- declare class Hooks {
682
- /**
683
- * A map of hook names to arrays of hook functions.
684
- * Each hook name can have multiple associated functions, which are executed in sequence.
685
- */
686
- private readonly store;
687
- /**
688
- * Registers a new hook function under the specified hook name.
689
- * This function should be a function that takes an argument of type `T` and returns a `string` or `Promise<string>`.
690
- *
691
- * @template Hook - The type of the hook name. It should be one of the keys of `HooksMapping`.
692
- * @param name - The name of the hook under which the function will be registered.
693
- * @param handler - A function to be executed when the hook is triggered. The handler will be called with an argument
694
- * that may be modified by the hook functions.
695
- *
696
- * @remarks
697
- * - If there are existing handlers registered under the given hook name, the new handler will be added to the list.
698
- * - If no handlers are registered under the given hook name, a new list will be created with the handler as its first element.
699
- *
700
- * @example
701
- * ```typescript
702
- * hooks.on('html:transform:pre', async (ctx) => {
703
- * return ctx.html.replace(/foo/g, 'bar');
704
- * });
705
- * ```
706
- */
707
- on<Hook extends HookName>(name: Hook, handler: HooksMapping[Hook]): void;
708
- /**
709
- * Checks if there are any hooks registered under the specified name.
710
- *
711
- * @param name - The name of the hook to check.
712
- * @returns `true` if there are hooks registered under the specified name, otherwise `false`.
713
- */
714
- has(name: HookName): boolean;
715
- }
716
-
717
662
  /**
718
663
  * Options for configuring an `AngularServerApp`.
719
664
  */
@@ -931,98 +876,6 @@ declare class InlineCriticalCssProcessor extends BeastiesBase {
931
876
  private conditionallyInsertCspLoadingScript;
932
877
  }
933
878
 
934
- /**
935
- * Angular server application engine.
936
- * Manages Angular server applications (including localized ones), handles rendering requests,
937
- * and optionally transforms index HTML before rendering.
938
- *
939
- * @remarks This class should be instantiated once and used as a singleton across the server-side
940
- * application to ensure consistent handling of rendering requests and resource management.
941
- */
942
- declare class AngularAppEngine {
943
- /**
944
- * A flag to enable or disable the rendering of prerendered routes.
945
- *
946
- * Typically used during development to avoid prerendering all routes ahead of time,
947
- * allowing them to be rendered on the fly as requested.
948
- *
949
- * @private
950
- */
951
- static ɵallowStaticRouteRender: boolean;
952
- /**
953
- * Hooks for extending or modifying the behavior of the server application.
954
- * These hooks are used by the Angular CLI when running the development server and
955
- * provide extensibility points for the application lifecycle.
956
- *
957
- * @private
958
- */
959
- static ɵhooks: Hooks;
960
- /**
961
- * The manifest for the server application.
962
- */
963
- private readonly manifest;
964
- /**
965
- * A map of supported locales from the server application's manifest.
966
- */
967
- private readonly supportedLocales;
968
- /**
969
- * A cache that holds entry points, keyed by their potential locale string.
970
- */
971
- private readonly entryPointsCache;
972
- /**
973
- * Handles an incoming HTTP request by serving prerendered content, performing server-side rendering,
974
- * or delivering a static file for client-side rendered routes based on the `RenderMode` setting.
975
- *
976
- * @param request - The HTTP request to handle.
977
- * @param requestContext - Optional context for rendering, such as metadata associated with the request.
978
- * @returns A promise that resolves to the resulting HTTP response object, or `null` if no matching Angular route is found.
979
- *
980
- * @remarks A request to `https://www.example.com/page/index.html` will serve or render the Angular route
981
- * corresponding to `https://www.example.com/page`.
982
- */
983
- handle(request: Request, requestContext?: unknown): Promise<Response | null>;
984
- /**
985
- * Handles requests for the base path when i18n is enabled.
986
- * Redirects the user to a locale-specific path based on the `Accept-Language` header.
987
- *
988
- * @param request The incoming request.
989
- * @returns A `Response` object with a 302 redirect, or `null` if i18n is not enabled
990
- * or the request is not for the base path.
991
- */
992
- private redirectBasedOnAcceptLanguage;
993
- /**
994
- * Retrieves the Angular server application instance for a given request.
995
- *
996
- * This method checks if the request URL corresponds to an Angular application entry point.
997
- * If so, it initializes or retrieves an instance of the Angular server application for that entry point.
998
- * Requests that resemble file requests (except for `/index.html`) are skipped.
999
- *
1000
- * @param request - The incoming HTTP request object.
1001
- * @returns A promise that resolves to an `AngularServerApp` instance if a valid entry point is found,
1002
- * or `null` if no entry point matches the request URL.
1003
- */
1004
- private getAngularServerAppForRequest;
1005
- /**
1006
- * Retrieves the exports for a specific entry point, caching the result.
1007
- *
1008
- * @param potentialLocale - The locale string used to find the corresponding entry point.
1009
- * @returns A promise that resolves to the entry point exports or `undefined` if not found.
1010
- */
1011
- private getEntryPointExports;
1012
- /**
1013
- * Retrieves the entry point for a given URL by determining the locale and mapping it to
1014
- * the appropriate application bundle.
1015
- *
1016
- * This method determines the appropriate entry point and locale for rendering the application by examining the URL.
1017
- * If there is only one entry point available, it is returned regardless of the URL.
1018
- * Otherwise, the method extracts a potential locale identifier from the URL and looks up the corresponding entry point.
1019
- *
1020
- * @param url - The URL of the request.
1021
- * @returns A promise that resolves to the entry point exports or `undefined` if not found.
1022
- */
1023
- private getEntryPointExportsForUrl;
1024
- }
1025
-
1026
879
  /**
1027
880
  * Function for handling HTTP requests in a web environment.
1028
881
  *
@@ -1055,5 +908,5 @@ type RequestHandlerFunction = (request: Request) => Promise<Response | null> | n
1055
908
  */
1056
909
  declare function createRequestHandler(handler: RequestHandlerFunction): RequestHandlerFunction;
1057
910
 
1058
- export { AngularAppEngine, PrerenderFallback, RenderMode, createRequestHandler, provideServerRendering, withAppShell, withRoutes, InlineCriticalCssProcessor as ɵInlineCriticalCssProcessor, destroyAngularServerApp as ɵdestroyAngularServerApp, extractRoutesAndCreateRouteTree as ɵextractRoutesAndCreateRouteTree, getOrCreateAngularServerApp as ɵgetOrCreateAngularServerApp, getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig, setAngularAppEngineManifest as ɵsetAngularAppEngineManifest, setAngularAppManifest as ɵsetAngularAppManifest };
911
+ export { PrerenderFallback, RenderMode, createRequestHandler, provideServerRendering, withAppShell, withRoutes, InlineCriticalCssProcessor as ɵInlineCriticalCssProcessor, destroyAngularServerApp as ɵdestroyAngularServerApp, extractRoutesAndCreateRouteTree as ɵextractRoutesAndCreateRouteTree, getOrCreateAngularServerApp as ɵgetOrCreateAngularServerApp, getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig, setAngularAppEngineManifest as ɵsetAngularAppEngineManifest, setAngularAppManifest as ɵsetAngularAppManifest };
1059
912
  export type { RequestHandlerFunction, ServerRoute, ServerRouteClient, ServerRouteCommon, ServerRoutePrerender, ServerRoutePrerenderWithParams, ServerRouteServer };
package/node/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Type, ApplicationRef, StaticProvider } from '@angular/core';
2
2
  import { BootstrapContext } from '@angular/platform-browser';
3
3
  import { IncomingMessage, ServerResponse } from 'node:http';
4
4
  import { Http2ServerRequest, Http2ServerResponse } from 'node:http2';
5
+ import { AngularAppEngineOptions } from '../app-engine.js';
5
6
 
6
7
  interface CommonEngineOptions {
7
8
  /** A method that when invoked returns a promise that returns an `ApplicationRef` instance once resolved or an NgModule. */
@@ -10,6 +11,8 @@ interface CommonEngineOptions {
10
11
  providers?: StaticProvider[];
11
12
  /** Enable request performance profiling data collection and printing the results in the server console. */
12
13
  enablePerformanceProfiler?: boolean;
14
+ /** A set of hostnames that are allowed to access the server. */
15
+ allowedHosts?: readonly string[];
13
16
  }
14
17
  interface CommonEngineRenderOptions {
15
18
  /** A method that when invoked returns a promise that returns an `ApplicationRef` instance once resolved or an NgModule. */
@@ -38,6 +41,7 @@ declare class CommonEngine {
38
41
  private readonly templateCache;
39
42
  private readonly inlineCriticalCssProcessor;
40
43
  private readonly pageIsSSG;
44
+ private readonly allowedHosts;
41
45
  constructor(options?: CommonEngineOptions | undefined);
42
46
  /**
43
47
  * Render an HTML document for a specific URL with specified
@@ -51,6 +55,11 @@ declare class CommonEngine {
51
55
  private getDocument;
52
56
  }
53
57
 
58
+ /**
59
+ * Options for the Angular Node.js server application engine.
60
+ */
61
+ interface AngularNodeAppEngineOptions extends AngularAppEngineOptions {
62
+ }
54
63
  /**
55
64
  * Angular server application engine.
56
65
  * Manages Angular server applications (including localized ones), handles rendering requests,
@@ -61,22 +70,40 @@ declare class CommonEngine {
61
70
  */
62
71
  declare class AngularNodeAppEngine {
63
72
  private readonly angularAppEngine;
64
- constructor();
73
+ /**
74
+ * Creates a new instance of the Angular Node.js server application engine.
75
+ * @param options Options for the Angular Node.js server application engine.
76
+ */
77
+ constructor(options?: AngularNodeAppEngineOptions);
65
78
  /**
66
79
  * Handles an incoming HTTP request by serving prerendered content, performing server-side rendering,
67
80
  * or delivering a static file for client-side rendered routes based on the `RenderMode` setting.
68
81
  *
69
- * This method adapts Node.js's `IncomingMessage` or `Http2ServerRequest`
82
+ * This method adapts Node.js's `IncomingMessage`, `Http2ServerRequest` or `Request`
70
83
  * to a format compatible with the `AngularAppEngine` and delegates the handling logic to it.
71
84
  *
72
- * @param request - The incoming HTTP request (`IncomingMessage` or `Http2ServerRequest`).
85
+ * @param request - The incoming HTTP request (`IncomingMessage`, `Http2ServerRequest` or `Request`).
73
86
  * @param requestContext - Optional context for rendering, such as metadata associated with the request.
74
87
  * @returns A promise that resolves to the resulting HTTP response object, or `null` if no matching Angular route is found.
75
88
  *
76
89
  * @remarks A request to `https://www.example.com/page/index.html` will serve or render the Angular route
77
90
  * corresponding to `https://www.example.com/page`.
91
+ *
92
+ * @remarks
93
+ * To prevent potential Server-Side Request Forgery (SSRF), this function verifies the hostname
94
+ * of the `request.url` against a list of authorized hosts.
95
+ * If the hostname is not recognized and `allowedHosts` is not empty, a Client-Side Rendered (CSR) version of the
96
+ * page is returned otherwise a 400 Bad Request is returned.
97
+ *
98
+ * Resolution:
99
+ * Authorize your hostname by configuring `allowedHosts` in `angular.json` in:
100
+ * `projects.[project-name].architect.build.options.security.allowedHosts`.
101
+ * Alternatively, you can define the allowed hostname via the environment variable `process.env['NG_ALLOWED_HOSTS']`
102
+ * or pass it directly through the configuration options of `AngularNodeAppEngine`.
103
+ *
104
+ * For more information see: https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf
78
105
  */
79
- handle(request: IncomingMessage | Http2ServerRequest, requestContext?: unknown): Promise<Response | null>;
106
+ handle(request: IncomingMessage | Http2ServerRequest | Request, requestContext?: unknown): Promise<Response | null>;
80
107
  }
81
108
 
82
109
  /**
@@ -176,4 +203,4 @@ declare function createWebRequestFromNodeRequest(nodeRequest: IncomingMessage |
176
203
  declare function isMainModule(url: string): boolean;
177
204
 
178
205
  export { AngularNodeAppEngine, CommonEngine, createNodeRequestHandler, createWebRequestFromNodeRequest, isMainModule, writeResponseToNodeResponse };
179
- export type { CommonEngineOptions, CommonEngineRenderOptions, NodeRequestHandlerFunction };
206
+ export type { AngularNodeAppEngineOptions, CommonEngineOptions, CommonEngineRenderOptions, NodeRequestHandlerFunction };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular/ssr",
3
- "version": "20.3.15",
3
+ "version": "20.3.17",
4
4
  "description": "Angular server side rendering utilities",
5
5
  "type": "module",
6
6
  "license": "MIT",