A pair of Lambda Functions:
- OriginRequest - Executed upon CloudFront's "Origin Request" behaviour, to modify requests for certain conditions, such as checking for authorization, adding a default document, restricting certain folders, rewrites and redirects;
- OriginResponse - Executed upon CloudFront's "Origin Response" behaviour, to modify responses for certain conditions, such as mapping errors to new content and/or adding headers.
Moving a complex server-side application into the cloud requires new solutions to things that were previously solved via web.config, web.xml or .htaccess. This suite of Lambda@Edge functionality is intended, in conjunction with the OriginResponse function, to meet those requirements, where the origin is an Amazon S3 Bucket.
-
Create two new Lambda Functions (OriginRequest and OriginResponse), using the latest version of Node.js (I used 10.x at the time of writing);
-
Ensure the Lambda Functions can execute with the required permissions. I use the following policy via IAM:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": [ "arn:aws:logs:*:*:*" ] } ] } -
Deploy new versions to Lambda@Edge;
-
Configure "Origin Request" and "Origin Response" Behaviours to call the ARN of Lambda Functions, with the current version;
-
Add a
/config.jsonfile on your S3 origin; -
Set Bucket Policy to allow access to your bucket and also to allow the HTTP Referer header with the name and Region of your Lambda Functions (below is an example):
{ "Version": "2008-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity EVKCL8WKOLIUT" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::ptylr-com-s3bucket-16w98h5h9thdp/*" }, { "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::ptylr-com-s3bucket-16w98h5h9thdp/*", "Condition": { "StringLike": { "aws:Referer": [ "us-east-1.ptylr-com-OriginRequestLambdaFunction-AD7023KLJFUT", "us-east-1.ptylr-com-OriginResponseLambdaFunction-133YYDV9TFHG" ] } } } ] } -
Browse to your resource(s).
There are a number of options available for the config.json file:
-
defaultDocumentcan be used to request a default document where the url ends with/:{ "defaultDocument": "index.html" } -
authorizecan be used to restrict access to the whole site, or to a selection of paths therein:{ "authorize": { "paths": [ "^/secure" ], "user1": "{Base64 Encoded Password}", "user2": "{Base64 Encoded Password}" } }If
pathsis not provided, or if therequest.uridoes not match any entries, no authorization is required. Otherwise if noAuthorizationheader is provided, or if the value does not match one of the supplied credentials, the user will be required to enter credentials using HTTP 401 Basic authorization. -
restrictedPathscan be used to forbid access to paths within the site, using regular expressions:{ "restrictedPaths": [ "/{Restricted File Path e.g., passwords.json}", "^/secret ] }If the
request.urimatches any of the entries,HTTP 403 Forbiddenwill be returned to the user. -
rewritescan be used to rewrite or redirect requests, using a syntax similar to a.htaccessfile:"rewrites": [ "^/original/$ /new/index.html [R=302,L]", ["%{REQUEST_PATH} !html?$ [NC]", "%{REQUEST_PATH} !-d", "%{REQUEST_PATH} !-f", "%{REQUEST_PATH}.html -f", "^(.*)$ $1.html [L]"], "^/oldpath/(\\d*)/(.*)$ /newpath/$2/$1 [L]", "^/topsecret.*$ [F,L]", "^/deadlink.*$ [G]", "^/foo$ /bar [H=^baz.com$]" ]Each entry is applied to the request, in order, and the result is applied to the request. The format is intended to closely match the
mod_rewriteinstruction set.If the entry is a single string, it must contain:
- a test expression, which is tested against the
request.uri; - a result expression which is applied to the
request.uri; - an optional set of additional instructions to be applied.
If the entry is an array, the final element in the array must be as for a string, above. The previous elements are conditions that are applied in order, and must all be successful for the final rule to be evaluated and applied. A condition contains:
- an item to test - either
%{REQUEST_PATH}or%{HTTP_HOST}; - a regular expression for the item to match
- an optional set of additional instructions to be applied.
- a test expression, which is tested against the
-
errorscan be used to show friendly content when an http error status is returned:{ "errors": { "403": { "url": "/404.html", "status": "404", "statusDescription": "Not Found", "headers": { "Content-Type": "text/html" } }, "404": "/404.html", "500": "/500.html" } }If
response.statusmatches a key in theerrorsobject, that uri will be returned. If the uri includes a domain, it will be fetched directly. If not, the file will be loaded from the S3 origin. Try not to have that resource return an error as well, otherwise bad things will happen recursively.If the value is an array, the
urlproperty will be applied as above. ThestatusandstatusDescriptionproperties can be used to change the status that will be returned to the client. Finally theheaderproperty allows for any additional headers to be set that will help the client to process the information that is sent. -
headerscan be used to add custom headers to a response:{ "headers": [{ "X-Custom": "Custom Value", "Access-Control-Allow-Origin": "*" },{ "path": "\\.(jpe?g|gif|png)$", "Cache-Control": "public, max-age=31536001", "Access-Control-Allow-Origin": "" }] }Entries are applied in order.
If the entry has no
pathvariable, the headers will be added to all responses. If a path is supplied, it is tested against therequest.uriand the headers applied if a match is found.All keys inside the
headersobject will be added to the response. If the value is""and the named response header is currently set, it will be removed from the response.
ReWrite capability inspired by marksteele/edge-rewrite.
Written by Richard Lund and Paul Taylor.
All yours. MIT License. Do what you will, at your own risk!
