diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts index 4132d30d635b2..d6b9dba730238 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts @@ -17,21 +17,29 @@ export interface BaseApplicationListenerRuleProps { readonly priority: number; /** - * Target groups to forward requests to. Only one of `targetGroups` or - * `fixedResponse` can be specified. + * Target groups to forward requests to. Only one of `fixedResponse`, `redirectResponse` or + * `targetGroups` can be specified. * * @default - No target groups. */ readonly targetGroups?: IApplicationTargetGroup[]; /** - * Fixed response to return. Only one of `fixedResponse` or + * Fixed response to return. Only one of `fixedResponse`, `redirectResponse` or * `targetGroups` can be specified. * * @default - No fixed response. */ readonly fixedResponse?: FixedResponse; + /** + * Redirect response to return. Only one of `fixedResponse`, `redirectResponse` or + * `targetGroups` can be specified. + * + * @default - No redirect response. + */ + readonly redirectResponse?: RedirectResponse; + /** * Rule applies if the requested host matches the indicated host * @@ -100,6 +108,50 @@ export interface FixedResponse { readonly messageBody?: string; } +/** + * A redirect response + */ +export interface RedirectResponse { + /** + * The hostname. This component is not percent-encoded. The hostname can contain #{host}. + * + * @default origin host of request + */ + readonly host?: string; + /** + * The absolute path, starting with the leading "/". This component is not percent-encoded. + * The path can contain #{host}, #{path}, and #{port}. + * + * @default origin path of request + */ + readonly path?: string; + /** + * The port. You can specify a value from 1 to 65535 or #{port}. + * + * @default origin port of request + */ + readonly port?: string; + /** + * The protocol. You can specify HTTP, HTTPS, or #{protocol}. You can redirect HTTP to HTTP, + * HTTP to HTTPS, and HTTPS to HTTPS. You cannot redirect HTTPS to HTTP. + * + * @default origin protocol of request + */ + readonly protocol?: string; + /** + * The query parameters, URL-encoded when necessary, but not percent-encoded. + * Do not include the leading "?", as it is automatically added. + * You can specify any of the reserved keywords. + * + * @default origin query string of request + */ + readonly query?: string; + /** + * The HTTP redirect code (HTTP_301 or HTTP_302) + */ + readonly statusCode: string; +} + /** * Define a new listener rule */ @@ -121,8 +173,10 @@ export class ApplicationListenerRule extends cdk.Construct { throw new Error(`At least one of 'hostHeader' or 'pathPattern' is required when defining a load balancing rule.`); } - if (props.targetGroups && props.fixedResponse) { - throw new Error('Cannot combine `targetGroups` with `fixedResponse`.'); + const possibleActions: Array = ['targetGroups', 'fixedResponse', 'redirectResponse']; + const providedActions = possibleActions.filter(action => props[action] !== undefined); + if (providedActions.length > 1) { + throw new Error(`'${providedActions}' specified together, specify only one`); } this.listener = props.listener; @@ -145,6 +199,8 @@ export class ApplicationListenerRule extends cdk.Construct { if (props.fixedResponse) { this.addFixedResponse(props.fixedResponse); + } else if (props.redirectResponse) { + this.addRedirectResponse(props.redirectResponse); } this.listenerRuleArn = resource.ref; @@ -180,6 +236,18 @@ export class ApplicationListenerRule extends cdk.Construct { }); } + /** + * Add a redirect response + */ + public addRedirectResponse(redirectResponse: RedirectResponse) { + validateRedirectResponse(redirectResponse); + + this.actions.push({ + redirectConfig: redirectResponse, + type: 'redirect' + }); + } + /** * Validate the rule */ @@ -218,3 +286,18 @@ export function validateFixedResponse(fixedResponse: FixedResponse) { throw new Error('`messageBody` cannot have more than 1024 characters.'); } } + +/** + * Validate the status code and message body of a redirect response + * + * @internal + */ +export function validateRedirectResponse(redirectResponse: RedirectResponse) { + if (redirectResponse.protocol && !/^(HTTPS?|#\{protocol\})$/i.test(redirectResponse.protocol)) { + throw new Error('`protocol` must be HTTP, HTTPS, or #{protocol}.'); + } + + if (!redirectResponse.statusCode || !/^HTTP_30[12]$/.test(redirectResponse.statusCode)) { + throw new Error('`statusCode` must be HTTP_301 or HTTP_302.'); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 06c22e4eb9c78..a3ae6e4086f47 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -5,7 +5,7 @@ import { HealthCheck } from '../shared/base-target-group'; import { ApplicationProtocol, SslPolicy } from '../shared/enums'; import { determineProtocolAndPort } from '../shared/util'; import { ApplicationListenerCertificate } from './application-listener-certificate'; -import { ApplicationListenerRule, FixedResponse, validateFixedResponse } from './application-listener-rule'; +import { ApplicationListenerRule, FixedResponse, RedirectResponse, validateFixedResponse, validateRedirectResponse } from './application-listener-rule'; import { IApplicationLoadBalancer } from './application-load-balancer'; import { ApplicationTargetGroup, IApplicationLoadBalancerTarget, IApplicationTargetGroup } from './application-target-group'; @@ -263,6 +263,37 @@ export class ApplicationListener extends BaseListener implements IApplicationLis } } + /** + * Add a redirect response + */ + public addRedirectResponse(id: string, props: AddRedirectResponseProps) { + checkAddRuleProps(props); + const redirectResponse = { + host: props.host, + path: props.path, + port: props.port, + protocol: props.protocol, + query: props.query, + statusCode: props.statusCode + }; + + validateRedirectResponse(redirectResponse); + + if (props.priority) { + new ApplicationListenerRule(this, id + 'Rule', { + listener: this, + priority: props.priority, + redirectResponse, + ...props + }); + } else { + this._addDefaultAction({ + redirectConfig: redirectResponse, + type: 'redirect' + }); + } + } + /** * Register that a connectable that has been added to this load balancer. * @@ -604,6 +635,12 @@ export interface AddApplicationTargetsProps extends AddRuleProps { export interface AddFixedResponseProps extends AddRuleProps, FixedResponse { } +/** + * Properties for adding a redirect response to a listener + */ +export interface AddRedirectResponseProps extends AddRuleProps, RedirectResponse { +} + function checkAddRuleProps(props: AddRuleProps) { if ((props.hostHeader !== undefined || props.pathPattern !== undefined) !== (props.priority !== undefined)) { throw new Error(`Setting 'pathPattern' or 'hostHeader' also requires 'priority', and vice versa`); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts index cc7c14dcfcb24..1c9d9578dc101 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts @@ -586,6 +586,59 @@ export = { test.done(); }, + 'Can add redirect responses'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', { + vpc + }); + const listener = lb.addListener('Listener', { + port: 80 + }); + + // WHEN + listener.addRedirectResponse('Default', { + statusCode: 'HTTP_301', + port: '443', + protocol: 'HTTPS' + }); + listener.addRedirectResponse('Hello', { + priority: 10, + pathPattern: '/hello', + path: '/new/#{path}', + statusCode: 'HTTP_302' + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + RedirectConfig: { + Port: '443', + Protocol: 'HTTPS', + StatusCode: 'HTTP_301' + }, + Type: 'redirect' + } + ] + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Actions: [ + { + RedirectConfig: { + Path: '/new/#{path}', + StatusCode: 'HTTP_302' + }, + Type: 'redirect' + } + ] + })); + + test.done(); + }, + 'Can configure deregistration_delay for targets'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -653,6 +706,48 @@ export = { } }, + 'Throws with bad redirect responses': { + + 'status code'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', { + vpc + }); + const listener = lb.addListener('Listener', { + port: 80 + }); + + // THEN + test.throws(() => listener.addRedirectResponse('Default', { + statusCode: '301' + }), /`statusCode`/); + + test.done(); + }, + + 'protocol'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', { + vpc + }); + const listener = lb.addListener('Listener', { + port: 80 + }); + + // THEN + test.throws(() => listener.addRedirectResponse('Default', { + protocol: 'tcp', + statusCode: 'HTTP_301' + }), /`protocol`/); + + test.done(); + } + }, + 'Throws when specifying both target groups and fixed reponse'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -678,6 +773,43 @@ export = { test.done(); }, + 'Throws when specifying both target groups and redirect reponse'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', { + vpc + }); + const listener = lb.addListener('Listener', { + port: 80 + }); + + // THEN + test.throws(() => new elbv2.ApplicationListenerRule(stack, 'Rule', { + listener, + priority: 10, + pathPattern: '/hello', + targetGroups: [new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { vpc, port: 80 })], + redirectResponse: { + statusCode: 'HTTP_301' + } + }), /`targetGroups`.*`redirectResponse`/); + + test.throws(() => new elbv2.ApplicationListenerRule(stack, 'Rule', { + listener, + priority: 10, + targetGroups: [new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { vpc, port: 80 })], + fixedResponse: { + statusCode: '500' + }, + redirectResponse: { + statusCode: 'HTTP_301' + } + }), /`targetGroups`.*`fixedResponse`.*`redirectResponse`/); + + test.done(); + }, + 'Imported listener with imported security group and allowAllOutbound set to false'(test: Test) { // GIVEN const stack = new cdk.Stack();