Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"add-istanbul-instrumenter": {
"version": "22.0.0",
"factory": "./add-istanbul-instrumenter/migration",
"description": "Add istanbul-lib-instrument to devDependencies if Karma unit testing is used."
"description": "Add 'istanbul-lib-instrument' to 'devDependencies' if Karma unit testing is used."
},
"use-application-builder": {
"version": "22.0.0",
Expand All @@ -20,6 +20,11 @@
"description": "Migrate projects using legacy Karma unit-test builder to the new unit-test builder with Vitest.",
"optional": true
},
"trust-proxy-headers": {
"version": "22.0.0",
"factory": "./trust-proxy-headers/migration",
"description": "Add 'trustProxyHeaders' configuration to 'AngularNodeAppEngine' or 'AngularAppEngine'. For more information see: https://angular.dev/best-practices/security#configuring-trusted-proxy-headers"
},
"update-workspace-config": {
"version": "22.0.0",
"factory": "./update-workspace-config/migration",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { Rule } from '@angular-devkit/schematics';
import ts from 'typescript';
import { allTargetOptions, allWorkspaceTargets, getWorkspace } from '../../utility/workspace';

const TODO_COMMENT =
'// TODO: This is a security-sensitive option. Remove if not needed. ' +
'For more information, see https://angular.dev/best-practices/security#configuring-trusted-proxy-headers';

export default function (): Rule {
return async (tree) => {
const workspace = await getWorkspace(tree);
const serverFiles = new Set<string>();

for (const [targetName, target] of allWorkspaceTargets(workspace)) {
if (targetName !== 'build') {
continue;
}

for (const [, options] of allTargetOptions(target)) {
if (typeof options?.['server'] === 'string') {
serverFiles.add(options['server']);
}
}
}

for (const path of serverFiles) {
if (!tree.exists(path)) {
continue;
}

const content = tree.readText(path);
if (content.includes(TODO_COMMENT)) {
continue;
}

if (!content.includes('AngularAppEngine') && !content.includes('AngularNodeAppEngine')) {
continue;
}

const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
const recorder = tree.beginUpdate(path);

function visit(node: ts.Node) {
if (
ts.isNewExpression(node) &&
ts.isIdentifier(node.expression) &&
(node.expression.text === 'AngularNodeAppEngine' ||
node.expression.text === 'AngularAppEngine')
) {
// Check arguments
if (!node.arguments || node.arguments.length === 0) {
// Case 1: No arguments passed
const insertPos = node.end - 1; // right before )
recorder.insertRight(
insertPos,
`{\n ${TODO_COMMENT}\n ` +
`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],\n}`,
);
Comment thread
alan-agius4 marked this conversation as resolved.
} else if (node.arguments.length > 0) {
const firstArg = node.arguments[0];
if (ts.isObjectLiteralExpression(firstArg)) {
// Check if trustProxyHeaders is already present
const hasTrustProxyHeaders = firstArg.properties.some(
(prop: ts.ObjectLiteralElementLike) =>
ts.isPropertyAssignment(prop) &&
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
prop.name.text === 'trustProxyHeaders',
);
Comment thread
alan-agius4 marked this conversation as resolved.

if (!hasTrustProxyHeaders) {
// Insert right after the opening brace
const insertPos = firstArg.getStart() + 1;
recorder.insertRight(
insertPos,
`\n ${TODO_COMMENT}\n ` +
`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`,
);
}
}
}
}
ts.forEachChild(node, visit);
}

visit(sourceFile);
tree.commitUpdate(recorder);
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { EmptyTree } from '@angular-devkit/schematics';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';

function createWorkSpaceConfig(tree: UnitTestTree) {
const angularConfig = {
version: 1,
projects: {
app: {
root: '',
sourceRoot: 'src',
projectType: 'application',
architect: {
build: {
options: {
server: '/server.ts',
},
},
},
},
},
};

tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2));
}

describe(`Migration to add trustProxyHeaders to server.ts`, () => {
const schematicName = 'trust-proxy-headers';
const schematicRunner = new SchematicTestRunner(
'migrations',
require.resolve('../migration-collection.json'),
);
const TODO_COMMENT =
'// TODO: This is a security-sensitive option. Remove if not needed. ' +
'For more information, see https://angular.dev/best-practices/security#configuring-trusted-proxy-headers';

let tree: UnitTestTree;
beforeEach(() => {
tree = new UnitTestTree(new EmptyTree());
createWorkSpaceConfig(tree);
});

it(`should add trustProxyHeaders to AngularNodeAppEngine with no args`, async () => {
tree.create(
'/server.ts',
`import { AngularNodeAppEngine } from '@angular/ssr/node';\nconst angularApp = new AngularNodeAppEngine();`,
);

const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
const content = newTree.readText('/server.ts');
expect(content).toContain(`const angularApp = new AngularNodeAppEngine({`);
expect(content).toContain(TODO_COMMENT);
expect(content).toContain(`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`);
});

it(`should add trustProxyHeaders to AngularNodeAppEngine with existing args`, async () => {
tree.create(
'/server.ts',
`import { AngularNodeAppEngine } from '@angular/ssr/node';\n` +
`const angularApp = new AngularNodeAppEngine({\n allowedHosts: ['localhost']\n});`,
);

const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
const content = newTree.readText('/server.ts');
expect(content).toContain(`const angularApp = new AngularNodeAppEngine({`);
expect(content).toContain(TODO_COMMENT);
expect(content).toContain(`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`);
expect(content).toContain(`allowedHosts: ['localhost']`);
});

it(`should add trustProxyHeaders to AngularAppEngine`, async () => {
tree.create(
'/server.ts',
`import { AngularAppEngine } from '@angular/ssr';\nconst angularApp = new AngularAppEngine();`,
);

const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
const content = newTree.readText('/server.ts');
expect(content).toContain(`const angularApp = new AngularAppEngine({`);
expect(content).toContain(TODO_COMMENT);
expect(content).toContain(`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`);
});

it(`should not add trustProxyHeaders if it already exists`, async () => {
const originalContent =
`import { AngularAppEngine } from '@angular/ssr';\n` +
`const angularApp = new AngularAppEngine({\n trustProxyHeaders: true\n});`;
tree.create('/server.ts', originalContent);

const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
const content = newTree.readText('/server.ts');
expect(content).toBe(originalContent);
});
});
Loading