Skip to content

Commit

Permalink
feat(elbv2): add redirect action of ALB's listener (#4606)
Browse files Browse the repository at this point in the history
Closes #4546.
  • Loading branch information
zxkane authored and mergify[bot] committed Oct 29, 2019
1 parent 35d82f0 commit c770d3c
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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
*/
Expand All @@ -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<keyof ApplicationListenerRuleProps> = ['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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -673,7 +768,45 @@ export = {
fixedResponse: {
statusCode: '500'
}
}), /`targetGroups`.*`fixedResponse`/);
}), /'targetGroups,fixedResponse'.*/);

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, 'Rule2', {
listener,
priority: 10,
pathPattern: '/hello',
targetGroups: [new elbv2.ApplicationTargetGroup(stack, 'TargetGroup2', { vpc, port: 80 })],
fixedResponse: {
statusCode: '500'
},
redirectResponse: {
statusCode: 'HTTP_301'
}
}), /'targetGroups,fixedResponse,redirectResponse'.*/);

test.done();
},
Expand Down

0 comments on commit c770d3c

Please sign in to comment.