Skip to content

Commit

Permalink
Merge pull request #116 from bestickley/fix-cross-env-ref
Browse files Browse the repository at this point in the history
fix: cross-region issue
  • Loading branch information
bestickley authored Jun 23, 2023
2 parents 4416343 + 03ca3b3 commit 2bf3c44
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 20 deletions.
16 changes: 10 additions & 6 deletions .projenrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { awscdk } = require('projen');
const { awscdk, JsonPatch } = require('projen');
const project = new awscdk.AwsCdkConstructLibrary({
author: 'JetBridge',
authorAddress: '[email protected]',
Expand Down Expand Up @@ -40,15 +40,19 @@ const project = new awscdk.AwsCdkConstructLibrary({
'@aws-crypto/sha256-js',
] /* Runtime dependencies of this module. */,
devDeps: ['open-next', 'aws-sdk', 'constructs'] /* Build dependencies for this module. */,
jestOptions: {
jestConfig: {
testMatch: ['<rootDir>/src/**/__tests__/**/*.ts?(x)', '<rootDir>/(test|src)/**/*(*.)@(spec|test|assets).ts?(x)'],
},
},

// do not generate sample test files
sampleCode: false,
});
const packageJson = project.tryFindObjectFile('package.json');
if (packageJson) {
packageJson.patch(
JsonPatch.replace('/jest/testMatch', [
'<rootDir>/src/**/__tests__/**/*.ts?(x)',
'<rootDir>/(test|src|assets)/**/*(*.)@(spec|test).ts?(x)',
])
);
}
// project.eslint.addOverride({
// rules: {},
// });
Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,49 @@ new Nextjs(this, 'Web', {

Available on [Construct Hub](https://constructs.dev/packages/cdk-nextjs-standalone/).

## Customization

### Increased Security
```ts
import { RemovalPolicy, Stack } from "aws-cdk-lib";
import { Construct } from "constructs";
import { CfnWebAcl } from "aws-cdk-lib/aws-wafv2";
import { SecurityPolicyProtocol, type DistributionProps } from "aws-cdk-lib/aws-cloudfront";
import { Nextjs, type NextjsDistributionProps } from "cdk-nextjs-standalone";
import { Bucket, BlockPublicAccess, BucketEncryption } from "aws-cdk-lib/aws-s3";

// Because of `WebAcl`, this stack must be deployed in us-east-1. If you want
// to deploy Nextjs in another region, add WAF in separate stack deployed in us-east-1
export class UiStack {
constructor(scope: Construct, id: string) {
const webAcl = new CfnWebAcl(this, "WebAcl", { ... });
new Nextjs(this, "NextSite", {
nextjsPath: "...",
defaults: {
assetDeployment: {
bucket: new Bucket(this, "NextjsAssetDeploymentBucket", {
autoDeleteObjects: true,
removalPolicy: RemovalPolicy.DESTROY,
encryption: BucketEncryption.S3_MANAGED,
enforceSSL: true,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
}),
},
distribution: {
functionUrlAuthType: FunctionUrlAuthType.AWS_IAM,
cdk: {
distribution: {
webAclId: webAcl.attrArn,
minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
} as DistributionProps,
},
} satisfies Partial<NextjsDistributionProps>,
},
});
}
}
```

### Discord Chat

We're in the #open-next channel on the [Serverless Stack Discord](https://discord.gg/sst).
Expand Down Expand Up @@ -76,6 +119,9 @@ Here is a short HowTo before you get started:
2. Link the bug in your pull request
3. Run `yarn build` after you made your changes and before you open a pull request

### Projen
Don't manually update package.json or use npm CLI. Update dependencies in .projenrc.js then run yarn projen.

## Breaking changes

- v3.0.0: Using open-next for building, ARM64 architecture for image handling, new build options.
Expand Down
22 changes: 18 additions & 4 deletions assets/lambda@edge/LambdaOriginRequestIamAuth.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
import type { CloudFrontRequestEvent } from 'aws-lambda';
import { signRequest } from './LambdaOriginRequestIamAuth';
import { getRegionFromLambdaUrl, isLambdaUrlRequest, signRequest } from './LambdaOriginRequestIamAuth';

describe('LambdaOriginRequestIamAuth', () => {
test('signRequest should add x-amz headers', async () => {
// dummy AWS credentials
process.env = { ...process.env, ...getFakeAwsCreds() };
const event = getFakeCloudFrontRequest();
const event = getFakeCloudFrontLambdaUrlRequest();
const request = event.Records[0].cf.request;
await signRequest(request);
const securityHeaders = ['x-amz-date', 'x-amz-security-token', 'x-amz-content-sha256', 'authorization'];
const hasSignedHeaders = securityHeaders.every((h) => h in request.headers);
expect(hasSignedHeaders).toBe(true);
});

test('isLambdaFunctionUrl should correctly identity Lambda URL', () => {
const event = getFakeCloudFrontLambdaUrlRequest();
const request = event.Records[0].cf.request;
const actual = isLambdaUrlRequest(request);
expect(actual).toBe(true);
});

test('getRegionFromLambdaUrl should correctly get region', () => {
const event = getFakeCloudFrontLambdaUrlRequest();
const request = event.Records[0].cf.request;
const actual = getRegionFromLambdaUrl(request.origin?.custom?.domainName || '');
expect(actual).toBe('us-east-1');
});
});

function getFakeCloudFrontRequest(): CloudFrontRequestEvent {
function getFakeCloudFrontLambdaUrlRequest(): CloudFrontRequestEvent {
return {
Records: [
{
Expand Down Expand Up @@ -43,7 +57,7 @@ function getFakeCloudFrontRequest(): CloudFrontRequestEvent {
referer: [
{
key: 'Referer',
value: 'https://d6b8brjqfujeb.cloudfront.net/batches/2/overview',
value: 'https://d6b8brjqfujeb.cloudfront.net/some/path',
},
],
'x-forwarded-for': [
Expand Down
26 changes: 20 additions & 6 deletions assets/lambda@edge/LambdaOriginRequestIamAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,32 @@ import { SignatureV4 } from '@aws-sdk/signature-v4';
import type { CloudFrontHeaders, CloudFrontRequest, CloudFrontRequestHandler } from 'aws-lambda';
import { fixHostHeader, handleS3Request } from './common';

const debug = false;

/**
* This Lambda@Edge handler fixes s3 requests, fixes the host header, and
* signs requests as they're destined for Lambda Function URL that requires
* IAM Auth.
*/
export const handler: CloudFrontRequestHandler = async (event) => {
const request = event.Records[0].cf.request;
// console.log('request', JSON.stringify(request, null, 2));
if (debug) console.log('request', JSON.stringify(request, null, 2));

handleS3Request(request);
fixHostHeader(request);
await signRequest(request);
// console.log(JSON.stringify(request), null, 2);
if (isLambdaUrlRequest(request)) {
await signRequest(request);
}
if (debug) console.log(JSON.stringify(request), null, 2);
return request;
};

let sigv4: SignatureV4;

export function isLambdaUrlRequest(request: CloudFrontRequest) {
return /[a-z0-9]+\.lambda-url\.[a-z0-9-]+\.on\.aws/.test(request.origin?.custom?.domainName || '');
}

/**
* When `NextjsDistributionProps.functionUrlAuthType` is set to
* `lambda.FunctionUrlAuthType.AWS_IAM` we need to sign the `CloudFrontRequest`s
Expand All @@ -33,7 +41,8 @@ let sigv4: SignatureV4;
*/
export async function signRequest(request: CloudFrontRequest) {
if (!sigv4) {
sigv4 = getSigV4();
const region = getRegionFromLambdaUrl(request.origin?.custom?.domainName || '');
sigv4 = getSigV4(region);
}
const headerBag = cfHeadersToHeaderBag(request);
let body: string | undefined;
Expand All @@ -53,8 +62,7 @@ export async function signRequest(request: CloudFrontRequest) {
request.headers = headerBagToCfHeaders(signed.headers);
}

function getSigV4(): SignatureV4 {
const region = process.env.AWS_REGION;
function getSigV4(region: string): SignatureV4 {
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
const sessionToken = process.env.AWS_SESSION_TOKEN;
Expand All @@ -74,6 +82,12 @@ function getSigV4(): SignatureV4 {
});
}

export function getRegionFromLambdaUrl(url: string): string {
const region = url.split('.').at(2);
if (!region) throw new Error("Region couldn't be extracted from Lambda Function URL");
return region;
}

type HeaderBag = Record<string, string>;
/**
* Converts CloudFront headers (can have array of header values) to simple
Expand Down
2 changes: 1 addition & 1 deletion package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/ImageOptimizationLambda.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable prettier/prettier */
import { Duration } from 'aws-cdk-lib';
import { Duration, PhysicalName, Stack } from 'aws-cdk-lib';
import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Architecture, Code, Function, FunctionOptions } from 'aws-cdk-lib/aws-lambda';
import { IBucket } from 'aws-cdk-lib/aws-s3';
Expand Down Expand Up @@ -52,6 +52,10 @@ export class ImageOptimizationLambda extends Function {
handler: 'index.handler',
runtime: LAMBDA_RUNTIME,
architecture: Architecture.ARM_64,
// prevents "Resolution error: Cannot use resource in a cross-environment
// fashion, the resource's physical name must be explicit set or use
// PhysicalName.GENERATE_IF_NEEDED."
functionName: Stack.of(scope).region !== 'us-east-1' ? PhysicalName.GENERATE_IF_NEEDED : undefined,
...lambdaOptions,
// defaults
memorySize: lambdaOptions?.memorySize || 1024,
Expand Down
7 changes: 5 additions & 2 deletions src/NextjsLambda.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as os from 'os';
import * as path from 'path';
import { Duration, RemovalPolicy, Token } from 'aws-cdk-lib';
import { Duration, PhysicalName, RemovalPolicy, Stack, Token } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Function, FunctionOptions } from 'aws-cdk-lib/aws-lambda';
import { Bucket } from 'aws-cdk-lib/aws-s3';
Expand Down Expand Up @@ -82,7 +82,10 @@ export class NextJsLambda extends Construct {
handler: path.join('index.handler'),
code,
environment,

// prevents "Resolution error: Cannot use resource in a cross-environment
// fashion, the resource's physical name must be explicit set or use
// PhysicalName.GENERATE_IF_NEEDED."
functionName: Stack.of(this).region !== 'us-east-1' ? PhysicalName.GENERATE_IF_NEEDED : undefined,
...functionOptions,
});
this.lambdaFunction = fn;
Expand Down

0 comments on commit 2bf3c44

Please sign in to comment.