@kensio/yulin 1.23.0 → 1.23.1

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.
@@ -29,6 +29,10 @@ export class SimCloudFrontControllerDependenciesFactory {
29
29
  new DefaultSimCloudFrontBehaviorResolver();
30
30
  const originFetcher = properties.originFetcher ?? new SimCloudFrontOriginFetcher();
31
31
  const edgeFunctions = new SimAwsCfEdgeFunctions(simAws);
32
+ // A request the web ACL blocks with WAF's default 403 gets the same custom
33
+ // error response an Origin's 403 would, so the one responder serves both.
34
+ const customErrorResponder = properties.customErrorResponder ??
35
+ new SimCfCustomErrorResponder({ behaviourResolver, originFetcher });
32
36
  return {
33
37
  distroRouter: properties.distroRouter ??
34
38
  new DefaultSimCloudFrontDistroRouter({
@@ -36,14 +40,13 @@ export class SimCloudFrontControllerDependenciesFactory {
36
40
  cloudFrontRegistry,
37
41
  }),
38
42
  webAclGuard: properties.webAclGuard ??
39
- new SimCfWebAclGuard(makeSimCfWebAclResolver(simAws)),
43
+ new SimCfWebAclGuard(makeSimCfWebAclResolver(simAws), customErrorResponder),
40
44
  behaviourResolver,
41
45
  cffApplicator: properties.cffApplicator ?? new SimCffApplicator(),
42
46
  edgeApplicator: properties.edgeApplicator ??
43
47
  new SimLambdaEdgeApplicator({ edgeFunctions }),
44
48
  contentStage: new SimCfContentStage(new SimCfOriginStage(originFetcher, properties.edgeOriginApplicator ??
45
- new SimLambdaEdgeOriginApplicator({ edgeFunctions })), properties.customErrorResponder ??
46
- new SimCfCustomErrorResponder({ behaviourResolver, originFetcher })),
49
+ new SimLambdaEdgeOriginApplicator({ edgeFunctions })), customErrorResponder),
47
50
  responseHeadersApplicator: properties.responseHeadersApplicator ??
48
51
  new SimCfResponseHeadersApplicator(),
49
52
  };
@@ -44,7 +44,8 @@ export class SimCloudFrontRequestPipeline {
44
44
  // Put the request through the Distribution's web ACL, if it has one.
45
45
  // CloudFront asks WAF before any content handling, so a blocked request is
46
46
  // answered at the edge without a Behavior, a CloudFront Function or the
47
- // Origin ever seeing it.
47
+ // Origin ever seeing it. Its 403 still goes through the Distribution's
48
+ // custom error responses, unless the block action named its own response.
48
49
  const webAclResult = await this.stages.webAclGuard.apply(requestReference, distro);
49
50
  if (webAclResult instanceof Response) {
50
51
  return webAclResult;
@@ -1,17 +1,26 @@
1
1
  import type { SimCloudFrontDistribution } from "../../distribution/sim-cloudfront-distribution.js";
2
2
  import type { SimCfWebAclResolver } from "../../web-acl/sim-cf-web-acl.js";
3
+ import type { SimCfCustomErrorResponder } from "../error/sim-cf-custom-error-responder.js";
3
4
  /**
4
5
  * Puts a request through the web ACL a Distribution names, before anything
5
6
  * else has a look at it.
6
7
  *
7
8
  * CloudFront asks WAF ahead of every content-handling stage, so a blocked
8
9
  * request never reaches a viewer-request CloudFront Function, a cache
9
- * Behavior, a response headers policy or the Origin. It is answered at the
10
- * edge with the 403 the web ACL's block action describes.
10
+ * Behavior, a response headers policy or the Origin.
11
+ *
12
+ * A blocked request is answered with the custom response its block action
13
+ * names, where it names one. Otherwise WAF hands CloudFront its default 403,
14
+ * and CloudFront treats that like a 403 from the Origin: a custom error
15
+ * response the Distribution configures for 403 replaces it. CloudFront cannot
16
+ * tell the two 403s apart, so one rule covers both.
17
+ *
18
+ * https://docs.aws.amazon.com/waf/latest/developerguide/cloudfront-waf-use-cases.html
11
19
  */
12
20
  export declare class SimCfWebAclGuard {
13
21
  private readonly resolveWebAcl;
14
- constructor(resolveWebAcl: SimCfWebAclResolver);
22
+ private readonly customErrorResponder;
23
+ constructor(resolveWebAcl: SimCfWebAclResolver, customErrorResponder: SimCfCustomErrorResponder);
15
24
  /**
16
25
  * Answer with the request to carry on with, or the response a blocked
17
26
  * request gets.
@@ -21,4 +30,17 @@ export declare class SimCfWebAclGuard {
21
30
  * request handling is for.
22
31
  */
23
32
  apply(request: Request, distribution: SimCloudFrontDistribution): Promise<Request | Response>;
33
+ /**
34
+ * The response a blocked request gets.
35
+ *
36
+ * A custom response from the block action takes precedence over anything
37
+ * the Distribution configures, whatever its status. WAF's default 403 goes
38
+ * through the Distribution's custom error responses instead.
39
+ *
40
+ * The answer is not stored in the Distribution's cache. WAF decides every
41
+ * request before the cache is consulted, so a blocked response stored under
42
+ * the request's cache key would only ever be served to a request WAF had
43
+ * already allowed.
44
+ */
45
+ private blockedResponse;
24
46
  }
@@ -6,13 +6,22 @@ import { SimCloudFrontInvalidWebAclId } from "../../error/sim-cloudfront.error.j
6
6
  *
7
7
  * CloudFront asks WAF ahead of every content-handling stage, so a blocked
8
8
  * request never reaches a viewer-request CloudFront Function, a cache
9
- * Behavior, a response headers policy or the Origin. It is answered at the
10
- * edge with the 403 the web ACL's block action describes.
9
+ * Behavior, a response headers policy or the Origin.
10
+ *
11
+ * A blocked request is answered with the custom response its block action
12
+ * names, where it names one. Otherwise WAF hands CloudFront its default 403,
13
+ * and CloudFront treats that like a 403 from the Origin: a custom error
14
+ * response the Distribution configures for 403 replaces it. CloudFront cannot
15
+ * tell the two 403s apart, so one rule covers both.
16
+ *
17
+ * https://docs.aws.amazon.com/waf/latest/developerguide/cloudfront-waf-use-cases.html
11
18
  */
12
19
  export class SimCfWebAclGuard {
13
20
  resolveWebAcl;
14
- constructor(resolveWebAcl) {
21
+ customErrorResponder;
22
+ constructor(resolveWebAcl, customErrorResponder) {
15
23
  this.resolveWebAcl = resolveWebAcl;
24
+ this.customErrorResponder = customErrorResponder;
16
25
  }
17
26
  /**
18
27
  * Answer with the request to carry on with, or the response a blocked
@@ -41,9 +50,29 @@ export class SimCfWebAclGuard {
41
50
  body: await requestBody(request),
42
51
  resourceType: "CLOUDFRONT",
43
52
  });
44
- return decision.action === "BLOCK"
45
- ? simWafBlockedHttpResponse(decision.blocked ?? simWafDefaultBlockedResponse())
46
- : allowedRequest(request, decision);
53
+ if (decision.action === "BLOCK") {
54
+ return await this.blockedResponse(request, distribution, decision.blocked ?? simWafDefaultBlockedResponse());
55
+ }
56
+ return allowedRequest(request, decision);
57
+ }
58
+ /**
59
+ * The response a blocked request gets.
60
+ *
61
+ * A custom response from the block action takes precedence over anything
62
+ * the Distribution configures, whatever its status. WAF's default 403 goes
63
+ * through the Distribution's custom error responses instead.
64
+ *
65
+ * The answer is not stored in the Distribution's cache. WAF decides every
66
+ * request before the cache is consulted, so a blocked response stored under
67
+ * the request's cache key would only ever be served to a request WAF had
68
+ * already allowed.
69
+ */
70
+ async blockedResponse(request, distribution, blocked) {
71
+ const response = simWafBlockedHttpResponse(blocked);
72
+ if (blocked.isCustom) {
73
+ return response;
74
+ }
75
+ return await this.customErrorResponder.apply(request, distribution, response);
47
76
  }
48
77
  }
49
78
  /**
@@ -7,6 +7,18 @@ export interface SimWafBlockedResponse {
7
7
  readonly contentType: string;
8
8
  readonly body: string;
9
9
  readonly headers: readonly SimWafHeader[];
10
+ /**
11
+ * Whether the block action named a `CustomResponse` of its own.
12
+ *
13
+ * A fronting service treats the two differently. A custom response goes to
14
+ * the client exactly as the rule describes it, and the service's own error
15
+ * handling has no say in it. WAF's default 403 is handed to the service as
16
+ * any other 403 would be, so a CloudFront custom error response for 403
17
+ * replaces it.
18
+ *
19
+ * https://docs.aws.amazon.com/waf/latest/developerguide/customizing-the-response-for-blocked-requests.html
20
+ */
21
+ readonly isCustom: boolean;
10
22
  }
11
23
  /**
12
24
  * The body a block action with no custom response of its own answers with.
@@ -15,6 +15,7 @@ export function simWafDefaultBlockedResponse() {
15
15
  body: "<html><head><title>403 Forbidden</title></head>" +
16
16
  "<body>Request blocked by AWS WAF.</body></html>",
17
17
  headers: [],
18
+ isCustom: false,
18
19
  };
19
20
  }
20
21
  /**
@@ -18,6 +18,7 @@ export function simWafBlockedResponse(customResponse, bodies) {
18
18
  contentType: body?.contentType ?? fallback.contentType,
19
19
  body: body?.content ?? fallback.body,
20
20
  headers: simWafResponseHeaders(customResponse.ResponseHeaders),
21
+ isCustom: true,
21
22
  };
22
23
  }
23
24
  /**
@@ -260,6 +260,11 @@ Origin again. A rule carrying it alone, with no `ResponsePagePath`, configures e
260
260
  status the Distribution serves no page for. See
261
261
  [What a Distribution stores](#what-a-distribution-stores).
262
262
 
263
+ A request the Distribution's [web ACL](#web-acls) blocks gets the custom error response for 403
264
+ as well. CloudFront cannot tell WAF's 403 from the Origin's, so a site mapping 403 to its not-found
265
+ page serves that page to a blocked request too. A block action with a `CustomResponse` of its own
266
+ is the exception (see [Web ACLs](#web-acls)).
267
+
263
268
  ## Serve simulated CloudFront on localhost
264
269
 
265
270
  Use `serveSimAws` to send HTTP requests through the simulated distribution on localhost.
@@ -1846,7 +1851,7 @@ the site serves from the Origin (see [Limitations](#limitations)).
1846
1851
 
1847
1852
  A Distribution can put a WAFv2 web ACL in front of everything it serves. Name the web ACL's ARN in
1848
1853
  `WebACLId` and the Distribution evaluates it against every request that arrives. A request the web
1849
- ACL blocks gets 403 from the edge. A request it allows carries on to the cache Behavior and the
1854
+ ACL blocks is answered at the edge. A request it allows carries on to the cache Behavior and the
1850
1855
  Origin.
1851
1856
 
1852
1857
  CloudFront takes its web ACL this way. WAFv2's `AssociateWebACL` covers the regional resource types.
@@ -1859,6 +1864,17 @@ simulation never created, is refused with `InvalidWebACLId` at `CreateDistributi
1859
1864
  The web ACL decides before any other stage sees the request. A blocked request never reaches a
1860
1865
  viewer-request CloudFront Function, a cache Behavior, a response headers policy or the Origin.
1861
1866
 
1867
+ What a blocked request gets depends on the block action. A `CustomResponse` on the action is sent
1868
+ as the rule describes it, whatever its status, and the Distribution's `CustomErrorResponses` have no
1869
+ say. With no `CustomResponse`, WAF answers 403 and the Distribution treats it as it treats a 403
1870
+ from the Origin. A custom error response for 403 with a `ResponsePagePath` replaces it with the
1871
+ response page and its `ResponseCode`. Without one, the viewer gets WAF's 403. The order is the one
1872
+ [AWS documents for blocked requests](https://docs.aws.amazon.com/waf/latest/developerguide/customizing-the-response-for-blocked-requests.html).
1873
+
1874
+ A blocked request's answer is never stored in the Distribution's cache, so `ErrorCachingMinTTL`
1875
+ has no effect on it. The next viewer to ask for the same URL is answered by WAF's decision about
1876
+ their own request.
1877
+
1862
1878
  A CloudFormation Distribution naming a web ACL this simulation does not hold deploys without one.
1863
1879
  The `WebACLId` lands on `stack.ignoredProperties` and every request is served, including the ones
1864
1880
  the web ACL would have decided. A template naming a web ACL from a real account is ordinary, and a
@@ -1096,9 +1096,15 @@ its assets are outside the simulation.
1096
1096
  ## Protecting a CloudFront distribution
1097
1097
 
1098
1098
  A simulated CloudFront distribution names its web ACL in `WebACLId` on its `DistributionConfig`,
1099
- and evaluates it against every request that reaches the distribution. A blocked request gets 403
1099
+ and evaluates it against every request that reaches the distribution. A blocked request is answered
1100
1100
  before a cache behaviour, a viewer-request CloudFront Function or the origin sees it.
1101
1101
 
1102
+ WAF's default 403 goes through the distribution's custom error responses, as a 403 from the origin
1103
+ would. A distribution mapping 403 to a not-found page serves that page, with its `ResponseCode`, to
1104
+ a request the web ACL blocked. A block action with a `CustomResponse` takes precedence. Its status
1105
+ and body reach the viewer unchanged, even when the distribution has a custom error response for the
1106
+ same status.
1107
+
1102
1108
  CloudFront is associated this way and not through `AssociateWebACL`, which real WAF keeps for the
1103
1109
  regional resource types. The ARN has to name a `CLOUDFRONT` [scope](#scopes) web ACL. See
1104
1110
  [web ACLs in the CloudFront docs](https://yulinsim.dev/services/cloudfront/#web-acls) for the whole example.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.23.0",
3
+ "version": "1.23.1",
4
4
  "description": "AWS system behaviour simulation for isolated unit testing",
5
5
  "repository": "https://github.com/KensioSoftware/yulin",
6
6
  "homepage": "https://yulinsim.dev/",