Skip to content

Commit 3fbe995

Browse files
ryanoleeryan lee
andauthored
feat: add support for custom headers from origins (#261)
* Add support for custom headers from origins * Chore: Update documented node version * Fix oopsie with ts version * Also include minor fix for uri * feat: Overhaul how origins are injected in the request lifecycle * Chore: Typo Co-authored-by: ryan lee <rol@bluetel.co.uk>
1 parent f2c6b92 commit 3fbe995

File tree

10 files changed

+237
-15
lines changed

10 files changed

+237
-15
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ plugins:
2020

2121
provider:
2222
name: aws
23-
runtime: nodejs8.10
23+
runtime: nodejs12.x
2424

2525
functions:
2626
lambda:
@@ -65,3 +65,13 @@ custom:
6565
offlineEdgeLambda:
6666
path: '.build'
6767
```
68+
69+
For usage with `serverless-webpack` and `serverless-bundle` the config is similar but the build path changes.
70+
```yaml
71+
plugins:
72+
- serverless-webpack # or serverless-bundle
73+
74+
custom:
75+
offlineEdgeLambda:
76+
path: './.webpack/service/'
77+
```

examples/serverless.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins:
66

77
provider:
88
name: aws
9-
runtime: nodejs8.10
9+
runtime: nodejs12.x
1010

1111
functions:
1212
onViewerRequest:
@@ -16,8 +16,7 @@ functions:
1616
eventType: 'viewer-request'
1717
pathPattern: '/lambda'
1818
onOriginRequest:
19-
handler: src/handlers.onViewerRequest
20-
lambdaAtEdge:
19+
handler: src/handlers.onOriginRequest
2120
distribution: 'WebsiteDistribution'
2221
eventType: 'origin-request'
2322
pathPattern: '/lambda'

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/behavior-router.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import { HttpError, InternalServerError } from './errors/http';
1313
import { FunctionSet } from './function-set';
1414
import { asyncMiddleware, cloudfrontPost } from './middlewares';
1515
import { CloudFrontLifecycle, Origin, CacheService } from './services';
16-
import { ServerlessInstance, ServerlessOptions } from './types';
16+
import { CFDistribution, ServerlessInstance, ServerlessOptions } from './types';
1717
import {
1818
buildConfig, buildContext, CloudFrontHeadersHelper, ConfigBuilder,
19-
convertToCloudFrontEvent, IncomingMessageWithBodyAndCookies
19+
convertToCloudFrontEvent, getOriginFromCfDistribution, IncomingMessageWithBodyAndCookies
2020
} from './utils';
2121

2222

@@ -30,6 +30,7 @@ export class BehaviorRouter {
3030
private builder: ConfigBuilder;
3131
private context: Context;
3232
private behaviors = new Map<string, FunctionSet>();
33+
private cfResources: Record<string, CFDistribution>;
3334

3435
private cacheDir: string;
3536
private fileDir: string;
@@ -51,6 +52,7 @@ export class BehaviorRouter {
5152
this.builder = buildConfig(serverless);
5253
this.context = buildContext();
5354

55+
this.cfResources = serverless.service?.resources?.Resources || {};
5456
this.cacheDir = path.resolve(options.cacheDir || path.join(os.tmpdir(), 'edge-lambda'));
5557
this.fileDir = path.resolve(options.fileDir || path.join(os.tmpdir(), 'edge-lambda'));
5658
this.path = this.serverless.service.custom.offlineEdgeLambda.path || '';
@@ -143,16 +145,22 @@ export class BehaviorRouter {
143145
}
144146

145147
const handler = this.match(req);
146-
const cfEvent = convertToCloudFrontEvent(req, this.builder('viewer-request'));
147148

148149
if (!handler) {
149150
res.statusCode = StatusCodes.NOT_FOUND;
150151
res.end();
151152
return;
152153
}
153154

155+
const customOrigin = handler.distribution in this.cfResources ?
156+
getOriginFromCfDistribution(handler.pattern, this.cfResources[handler.distribution]) :
157+
null;
158+
159+
const cfEvent = convertToCloudFrontEvent(req, this.builder('viewer-request'));
160+
154161
try {
155-
const lifecycle = new CloudFrontLifecycle(this.serverless, this.options, cfEvent, this.context, this.cacheService, handler);
162+
const lifecycle = new CloudFrontLifecycle(this.serverless, this.options, cfEvent,
163+
this.context, this.cacheService, handler, customOrigin);
156164
const response = await lifecycle.run(req.url as string);
157165

158166
if (!response) {
@@ -230,20 +238,30 @@ export class BehaviorRouter {
230238
behaviors.clear();
231239

232240
for await (const [, def] of lambdaDefs) {
241+
233242
const pattern = def.lambdaAtEdge.pathPattern || '*';
243+
const distribution = def.lambdaAtEdge.distribution || '';
234244

235245
if (!behaviors.has(pattern)) {
236246
const origin = this.origins.get(pattern);
237-
behaviors.set(pattern, new FunctionSet(pattern, this.log, origin));
247+
behaviors.set(pattern, new FunctionSet(pattern, distribution, this.log, origin));
238248
}
239249

240250
const fnSet = behaviors.get(pattern) as FunctionSet;
241251

252+
// Don't try to register distributions that come from other sources
253+
if (fnSet.distribution !== distribution) {
254+
this.log(`Warning: pattern ${pattern} has registered handlers for cf distributions ${fnSet.distribution}` +
255+
` and ${distribution}. There is no way to tell which distribution should be used so only ${fnSet.distribution}` +
256+
` has been registered.` );
257+
continue;
258+
}
259+
242260
await fnSet.setHandler(def.lambdaAtEdge.eventType, path.join(this.path, def.handler));
243261
}
244262

245263
if (!behaviors.has('*')) {
246-
behaviors.set('*', new FunctionSet('*', this.log, this.origins.get('*')));
264+
behaviors.set('*', new FunctionSet('*', '', this.log, this.origins.get('*')));
247265
}
248266
}
249267

@@ -261,7 +279,10 @@ export class BehaviorRouter {
261279

262280
private logBehaviors() {
263281
this.behaviors.forEach((behavior, key) => {
264-
this.log(`Lambdas for path pattern ${key}: `);
282+
283+
this.log(`Lambdas for path pattern ${key}` +
284+
(behavior.distribution === '' ? ':' : ` on ${behavior.distribution}:`)
285+
);
265286

266287
behavior.viewerRequest && this.log(`viewer-request => ${behavior.viewerRequest.path || ''}`);
267288
behavior.originRequest && this.log(`origin-request => ${behavior.originRequest.path || ''}`);

src/function-set.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class FunctionSet {
2626

2727
constructor(
2828
public readonly pattern: string,
29+
public readonly distribution: string,
2930
private readonly log: (message: string) => void,
3031
public readonly origin: Origin = new Origin()
3132
) {

src/services/cloudfront.service.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
CloudFrontRequestEvent, CloudFrontResponseResult, CloudFrontResultResponse, Context
2+
CloudFrontRequestEvent, CloudFrontResponseResult, CloudFrontOrigin, Context
33
} from 'aws-lambda';
44

55
import { NoResult } from '../errors';
@@ -19,7 +19,8 @@ export class CloudFrontLifecycle {
1919
private event: CloudFrontRequestEvent,
2020
private context: Context,
2121
private fileService: CacheService,
22-
private fnSet: FunctionSet
22+
private fnSet: FunctionSet,
23+
private origin: CloudFrontOrigin | null,
2324
) {
2425
this.log = serverless.cli.log.bind(serverless.cli);
2526
}
@@ -85,6 +86,7 @@ export class CloudFrontLifecycle {
8586
throw new NoResult();
8687
} else {
8788
this.log('✓ Cache hit');
89+
throw new NoResult();
8890
}
8991

9092
const result = toResultResponse(cached);
@@ -99,6 +101,10 @@ export class CloudFrontLifecycle {
99101
async onOriginRequest() {
100102
this.log('→ origin-request');
101103

104+
// Inject origin into request once we reach the origin request step
105+
// as it is not available in viewer requests
106+
this.injectOriginIntoRequest();
107+
102108
const result = await this.fnSet.originRequest(this.event, this.context);
103109

104110
if (isResponseResult(result)) {
@@ -116,4 +122,10 @@ export class CloudFrontLifecycle {
116122
const event = combineResult(this.event, result);
117123
return this.fnSet.originResponse(event, this.context);
118124
}
125+
126+
protected injectOriginIntoRequest() {
127+
if (this?.event?.Records[0]?.cf?.request && this.origin !== null) {
128+
this.event.Records[0].cf.request.origin = this.origin;
129+
}
130+
}
119131
}

src/types/serverless.types.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,63 @@ export interface ServerlessInstance {
1313
}
1414
functions: { [key: string]: ServerlessFunction }
1515
package: ServerlessPackage
16+
resources?: {
17+
Resources?: Record<string, CFDistribution>
18+
}
1619
getAllFunctions: () => string[]
1720
};
1821
pluginManager: PluginManager;
1922
}
2023

24+
/**
25+
* A stub for the CF distributions we want details for in the context of this app
26+
*/
27+
export interface CFDistribution {
28+
Type: string;
29+
Properties: {
30+
DistributionConfig: {
31+
Origins: CFOrigin[]
32+
CacheBehaviors: CacheBehaviors[]
33+
}
34+
};
35+
}
36+
37+
export interface CacheBehaviors {
38+
PathPattern: string;
39+
TargetOriginId: string;
40+
}
41+
42+
export interface CFOrigin {
43+
DomainName: string;
44+
Id: string;
45+
ConnectionTimeout: number;
46+
OriginCustomHeaders: CFCustomHeaders[];
47+
S3OriginConfig?: {
48+
OriginAccessIdentity: string
49+
};
50+
CustomOriginConfig?: CFCustomOriginConfig;
51+
}
52+
53+
54+
export enum CFOriginProtocolPolicy {
55+
HTTP_ONLY = 'http-only',
56+
MATCH_VIEWER = 'match-viewer',
57+
HTTPS_ONLY = 'https-only'
58+
}
59+
60+
export interface CFCustomOriginConfig {
61+
HTTPPort?: number;
62+
HTTPSPort: number;
63+
OriginKeepaliveTimeout: string;
64+
OriginProtocolPolicy: CFOriginProtocolPolicy;
65+
OriginReadTimeout: number;
66+
OriginSSLProtocols: ('SSLv3' | 'TLSv1' | 'TLSv1.1' | 'TLSv1.2')[];
67+
}
68+
export interface CFCustomHeaders {
69+
HeaderName: string;
70+
HeaderValue: string;
71+
}
72+
2173
export interface ServerlessOptions {
2274
cacheDir: string;
2375
cloudfrontPort: number;

src/utils/convert-to-cf-event.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function convertToCloudFrontEvent(req: IncomingMessageWithBodyAndCookies,
1717
clientIp: req.socket.remoteAddress as string,
1818
method: req.method as string,
1919
headers: toCloudFrontHeaders(req.headers),
20-
uri: url.href as string,
20+
uri: url.pathname as string,
2121
querystring: url.query || '',
2222
body: req.body,
2323
cookies: req.cookies

0 commit comments

Comments
 (0)