How to remove .html extension using AWS Lambda & Cloudfront - amazon-web-services

I've my website's source code stored in AWS S3 and I'm using AWS Cloudfront to deliver my content.
I want to use AWS Lamda#Edge to remove .html extension from all the web links that's served through Cloudfront.
My required output should be www.example.com/foo instead of www.example.com/foo.html or example.com/foo1 instead of example.com/foo1.html.
Please help me to implement this as I can't find clear solution to use. I've referred the point 3 mentioned on this article: https://forums.aws.amazon.com/thread.jspa?messageID=796961&tstart=0. But it's not clear what I need to do.
PFB the lambda code, how can I modify it-
const config = {
suffix: '.html',
appendToDirs: 'index.html',
removeTrailingSlash: false,
};
const regexSuffixless = /\/[^/.]+$/; // e.g. "/some/page" but not "/", "/some/" or "/some.jpg"
const regexTrailingSlash = /.+\/$/; // e.g. "/some/" or "/some/page/" but not root "/"
exports.handler = function handler(event, context, callback) {
const { request } = event.Records[0].cf;
const { uri } = request;
const { suffix, appendToDirs, removeTrailingSlash } = config;
// Append ".html" to origin request
if (suffix && uri.match(regexSuffixless)) {
request.uri = uri + suffix;
callback(null, request);
return;
}
// Append "index.html" to origin request
if (appendToDirs && uri.match(regexTrailingSlash)) {
request.uri = uri + appendToDirs;
callback(null, request);
return;
}
// Redirect (301) non-root requests ending in "/" to URI without trailing slash
if (removeTrailingSlash && uri.match(/.+\/$/)) {
const response = {
// body: '',
// bodyEncoding: 'text',
headers: {
'location': [{
key: 'Location',
value: uri.slice(0, -1)
}]
},
status: '301',
statusDescription: 'Moved Permanently'
};
callback(null, response);
return;
}
// If nothing matches, return request unchanged
callback(null, request);
};
Please help me to remove .html extension from my website and what updated code do I need to paste in my AWS Lambda
Thanks in advance!!

Related

AWS CDN + Lambda#Edge MimeType Issue

I've been working lately with AWS CDN + S3 bucket as origin.
When no lambda#edge function attached into the Origin Request it works fine but I need the lambda#edge fn for rerouting. Some of the js file's mimetype are not text/javascript instead text/html. Any idea?
Lambda#edge function is just a simple rerouting no special feature.
Here is the function:
'use strict';
exports.handler = (evt, context, cb) => {
const { request } = evt.Records[0].cf;
const uriParts = request.uri.split("/")[1];
const locales = ['en-US', 'ja', 'ms'];
if (!uriParts || !locales.includes(uriParts)) {
request.uri = '/en-US/index.html';
return cb(null, request)
}
request.uri = `/${uriParts}/index.html`;
console.log(`Request Uri: ${request.uri}`);
cb(null, request);
}
Sample response image
Setup:
S3 Bucket
en-US/
index.html
<bunch of js files from angular>
ja/
index.html
<bunch of js files from angular>
ms/
index.html
<bunch of js files from angular>
CDN Distribution + Origin is the S3
Solved. First of all, my bad for not understanding well the triggers of Lambda#Edge function. Refer to this article.
First try, I used origin request as s trigger and somehow came to understand that it wasn't I need for my case.
After some debug and reading, I used viewer request this time. Also, alter my Lambda#Edge function to this.
'use strict';
const path = require("path");
exports.handler = async (evt,context,cb) => {
const { request } = evt.Records[0].cf;
console.log(`Original Uri: ${request.uri}`);
const uriParts = request.uri.split("/");
const locale = uriParts.length > 1 ? uriParts[1] : "";
const locales = ["en-US", "ja", "ms"];
if (locale === "" || locale === "index.html") {
request.uri = "/en-US/index.html";
return cb(null, request);
}
if (!locales.includes(locale)) return cb(null, request);
const lastPartUrl = uriParts[uriParts.length - 1];
const fileExt = path.extname(lastPartUrl);
if (!fileExt) request.uri = `/${locale}/index.html`;
console.log(`New Uri: ${request.uri}`);
return cb(null, request);
};
Now every locale in our application is now working fine.

How to redirect www to non-www static website on AWS

I've spent a few hours to solve redirection from www to non-www version of static site hosted on AWS.
You need to use two lamda#edge with cloudfront.
First lambda#edge - CloudFront Event is Viewer request
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request
request.headers['x-forwarded-host'] = [
{ key: 'X-Forwarded-Host', value: request.headers.host[0].value }
]
return callback(null, request)
}
Second lambda#edge - CloudFront Event is Origin request
exports.handler = function handler(event, context, callback) {
const { request } = event.Records[0].cf;
const { uri } = request;
const { suffix, appendToDirs, removeTrailingSlash } = config;
const requestHost = request.headers['x-forwarded-host'][0].value;
if (requestHost.startsWith("www.")) {
const domain_without_www = requestHost.replace("www.","");
var location = "https://" + domain_without_www + request.uri;
var responseWithRedirect = {
status: '301',
statusDescription: `Redirecting to apex domain`,
headers: {
location: [{
key: 'Location',
value: location
}]
}
};
callback(null, responseWithRedirect);
return;
}
// or return request unchanged
callback(null, request);
}
The last step is add "x-forwarded-host" header to whitelist headers in you distribution - behavior section.
I've found this solution in AWS forum, but it was difficult to find it. I believe this will help you.

When I use a x-amz-tagging in header it gives 403 forbidden error

Hi I have a working serverless function that uses s3 signedurl to put a file in an s3 bucket using on the Serverless framework that I am trying to migrate to a vercel serverless function using Next.
The function works via the serverless function and Postman, but when I try on Vercel although it generates the signedurl ok but when I try to use it with a "x-amz-tagging"="test" header I get a 403 error. Here is the relevant bit of my code:
//serverless function
const allowCors = fn => async (req, res) => {
res.setHeader('Access-Control-Allow-Credentials', true)
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT')
res.setHeader(
'Access-Control-Allow-Headers',
'X-CSRF-Token, X-Requested-With, x-amz-tagging, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
)
return await fn(req, res)
}
function requestUploadURL(req, res) {
...
}
module.exports = allowCors(requestUploadURL)
//code in app
try {
const config = {
onUploadProgress(progressEvent) {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total)
adduploadprogress({
file: fileSelectedArray[i].file,
fileType: fileSelectedArray[i].fileType,
myFsize: fileSelectedArray[i].myFsize,
percent,
})
},
headers: {
'Content-Type': fileSelectedArray[i].fileType,
'x-amz-tagging': 'test', // THIS CAUSES 403 ERROR
},
}
const resp1 = await axios.put(
uploadURL,
fileSelectedArray[i].file,
config
)
Any advice gratefully received
For some reason I also need to use Tagging:'' as part of the s3 params in the function being wrapped by the allowCors function. I didn't need to do this before
const { body } = req
const s3Params = {
Bucket: UNIQUEBUCKET,
Key: body.name,
ContentType: body.type,
ACL: 'public-read',
Tagging: '',
}
const uploadURL = s3.getSignedUrl('putObject', s3Params)

Validation error when I replace body in Lambda#Edge origin request function

I have Cloudfront in front of an s3 bucket that serves HLS videos. I'm trying to dynamically modify the manifest files to add an auth token to the segments inside of them.
What I would really like to do is modify the body I send back to the client in a viewer response function, but since that isn't possible, I'm attempting to use a origin request function to manually fetch the object from S3, modify it, and return a Cloudfront request with the new body. I get a 503 error of "The Lambda function result failed validation: The body is not a string, is not an object, or exceeds the maximum size"
My body is under 8kb (1MB is the limit in the docs). As far as I can tell the cloudfront request object I'm generating looks good and the base64 data decodes to what I want. I've also tried using text instead of base64. I have "include body" enabled in Cloudfront.
const fs = require('fs');
const querystring = require('querystring');
const AWS = require('aws-sdk');
const S3 = new AWS.S3();
exports.handler = async (event) => {
const cfrequest = event.Records[0].cf.request;
const queryString = querystring.parse(event.Records[0].cf.request.querystring);
const jwtToken = queryString.token;
if (cfrequest.uri.match(/\.m3u8?$/mi)) {
const s3Response = await (new Promise((resolve, reject) => {
S3.getObject({
Bucket: 'bucket',
Key: cfrequest.uri.substring(1)
}, (err, data) => {
if (err) {
reject(err)
} else {
resolve(data);
}
});
}));
const manifestFile = s3Response.Body.toString('utf8');
const newManifest = manifestFile.replace(/^((\S+)\.(m3u8|ts|vtt))$/gmi, (_, url) => `${url}?token=${jwtToken}`);
const base64NewManifest = Buffer.from(newManifest, 'utf8').toString('base64');
const tokenizedCfRequest = {
...cfrequest,
body: {
action: 'replace',
data: base64NewManifest,
encoding: 'base64'
}
};
return tokenizedCfRequest;
}
return cfrequest;
}
If you want to generate your own response you need to use a viewer request or origin request event and return a response like this:
exports.handler = async (event) => {
const cfRequest = event.Records[0].cf.request;
const queryString = querystring.parse(event.Records[0].cf.request.querystring);
const jwtToken = queryString.token;
if (cfrequest.uri.match(/\.m3u8?$/mi)) {
// ... your code here ...
const response = {
status: 200, // only mandatory field
body: base64NewManifest,
bodyEncoding: 'base64',
};
return response;
}
// Return original request if no uri match
return cfRequest;
}
See also Generating HTTP Responses in Request Triggers.

AWS Lambda#edge to set cookies to origin response

My objective is to protect an aws s3 bucket link and I'm trying to solve this by using cloudfront as the link via which the s3 buckets are accessible, hence when a user tries to access the cloudfront link, there is a basic auth if there's no cookie in their browser, but if there's a cookie, then auth values in this cookie is checked and user is granted access.
PS: This is not a website, my quest is to protect s3 bucket links.
Here is my attempt, using lambda#edge, on viewer request, there's the auth page if user is not logged in, otherwise, they're allowed access, it works but I can't set cookies, because somewhere in aws documentation, cloudfront deletes set-cookies in header files: CloudFront removes the Cookie header from requests that it forwards to your origin and removes the Set-Cookie header from responses that it returns to your viewers
Here is my code:
'use strict';
// returns a response error
const responseError = {
status: '401',
statusDescription: 'Unauthorized',
headers: {
'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}]
}
};
exports.handler = (event, context, callback) => {
// Get request and request headers
console.log(event.Records[0]);
const request = event.Records[0].cf.request;
const response = event.Records[0].cf.response;
const headers = request.headers;
// checks to see if headers exists with cookies
let hasTheHeader = (request, headerKey) => {
if (request.headers[headerKey]) {
return true;
}
else return false;
};
// Add set-cookie header to origin response
const setCookie = function(response, cookie) {
const cookieValue = `${cookie}`;
console.log(`Setting cookie ${cookieValue}`);
response.headers['set-cookie'] = [{ key: "Set-Cookie", value: cookieValue }];
}
// Configure authentication
const authUser = 'someuser';
const authPass = 'testpassword';
let authToken;
let authString;
// Construct the Auth string
const buff = new Buffer(authUser + ':' + authPass).toString('base64');
authString = 'Basic ' + buff;
const authCookie = 'testAuthToken';
//execute this on viewer request that is if request type is viewer request:
if(event.Records[0].cf.config.eventType == 'viewer-request'){
//check if cookies exists and assign authToken if it does not
if(hasTheHeader(request, 'cookie') ){
for (let i = 0; i < headers.cookie.length; i++)
{
if (headers.cookie[i].value.indexOf(authString) >= 0)
{
authToken = authString;
console.log(authToken);
break;
}
}
}
if (!authToken)
{
if (headers && headers.authorization && headers.authorization[0].value === authString)
{
// Set-Cookie: testAuthToken= new Buffer(authUser + ':' + authPass).toString('base64')
authToken = authString;
request.header.cookie = [];
//put cookie value to custom header - format is important
request.headers.cookie.push({'key': 'Cookie', 'value': authString});
}
else
{
callback(null, responseError);
}
// continue forwarding request
callback(null, request);
}
else{
//strip out "Basic " to extract Basic credential in base 64
var authInfo = authToken.slice(6);
var userCredentials = new Buffer(authInfo, 'base64');
var userLoginNamePass = userCredentials.toString();
var baseCredentials = userLoginNamePass.split(":");
var username = baseCredentials[0];
var userPass = baseCredentials[1];
if (username != authUser && userPass != authPass) {
//user auth failed
callback(null, responseError);
} else {
request.header.cookie = [];
//put cookie value to custom header - format is important
request.headers.cookie.push({'key': 'Cookie', 'value': authString});
}
// continue forwarding request
callback(null, request);
}
}
else if(event.Records[0].cf.config.eventType == 'origin-response')
{
if(hasTheHeader(request, 'cookie')){
for (let i = 0; i < headers.cookie.length; i++)
{
if (headers.cookie[i].value.indexOf(authString) >= 0)
{
setCookie(response, authString);
break;
}
}
}
// console.log(res_headers);
console.log("response: " + JSON.stringify(response));
callback(null, response);
}
};
Your suggestions will be most welcome. Thanks in advance.