AWS CDN + Lambda#Edge MimeType Issue - amazon-web-services

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.

Related

Cannot read properties of undefined (reading 'startsWith') error while deploying lambda edge function

In AWS I am configuring CloudFront with S3 Origin. The S3 is configured for Server-Side Encryption with Customer Master Keys (CMKs). Because of the coudfront issue with S3 with CMK encryption, we have to use Lambda#Edge function as suggested in this AWS Article
The Node JS code below is copied from the article linked
// Declare constants reqiured for the signature process
const crypto = require('crypto');
const emptyHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
const signedHeadersGeneric = 'host;x-amz-content-sha256;x-amz-date;x-amz-security-token';
// CloudFront includes the x-amz-cf-id header in the signature for custom origins
const signedHeadersCustomOrigin = 'host;x-amz-cf-id;x-amz-content-sha256;x-amz-date;x-amz-security-token';
// Retrieve the temporary IAM credentials of the function that were granted by
// the Lambda#Edge service based on the function permissions. In this solution, the function
// is given permissions to read from S3 and decrypt using the KMS key.
const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN } = process.env;
// Since the function is configured to be executed on origin request events, the handler
// is executed every time CloudFront needs to go back to the origin, which is S3 here.
exports.handler = async event => {
// Retrieve the original request that CloudFront was going to send to S3
const request = event.Records[0].cf.request;
// The request object has different properties depending on the type of
// origin that is being used. Account for that here.
let originType = '';
if (request.origin.hasOwnProperty('s3'))
originType = 's3';
else if (request.origin.hasOwnProperty('custom'))
originType = 'custom';
else
throw("Unexpected origin type. Expected 's3' or 'custom'. Got: " + JSON.stringify(request.origin));
// Create a JSON object with the fields that should be included in the Sigv4 request,
// including the X-Amz-Cf-Id header that CloudFront adds to every request forwarded
// upstream. This header is exposed to Lambda#Edge in the event object
const sigv4Options = {
method: request.method,
path: request.origin[originType].path + request.uri,
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
sessionToken: AWS_SESSION_TOKEN
},
host: request.headers['host'][0].value,
xAmzCfId: event.Records[0].cf.config.requestId,
originType: originType
};
// Compute the signature object that includes the following headers: X-Amz-Security-Token, Authorization,
// X-Amz-Date, X-Amz-Content-Sha256, and X-Amz-Security-Token
const signature = signV4(sigv4Options);
// Finally, add the signature headers to the request before it is sent to S3
for(var header in signature){
request.headers[header.toLowerCase()] = [{
key: header,
value: signature[header].toString()
}];
}
return request;
};
// Helper functions to sign the request using AWS Signature Version 4
// This helper only works for S3, using GET/HEAD requests, without query strings
// https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
function signV4(options) {
// Infer the region from the host header
const region = options.host.split('.')[2];
// Create the canonical request
const date = (new Date()).toISOString().replace(/[:-]|\.\d{3}/g, '');
let canonicalHeaders = '';
let signedHeaders = '';
if (options.originType == 's3') {
canonicalHeaders = ['host:'+options.host, 'x-amz-content-sha256:'+emptyHash, 'x-amz-date:'+date, 'x-amz-security-token:'+options.credentials.sessionToken].join('\n');
signedHeaders = signedHeadersGeneric;
} else {
canonicalHeaders = ['host:'+options.host, 'x-amz-cf-id:'+options.xAmzCfId, 'x-amz-content-sha256:'+emptyHash, 'x-amz-date:'+date, 'x-amz-security-token:'+options.credentials.sessionToken].join('\n');
signedHeaders = signedHeadersCustomOrigin;
}
const canonicalURI = encodeRfc3986(encodeURIComponent(decodeURIComponent(options.path).replace(/\+/g, ' ')).replace(/%2F/g, '/'));
const canonicalRequest = [options.method, canonicalURI, '', canonicalHeaders + '\n', signedHeaders,emptyHash].join('\n');
// Create string to sign
const credentialScope = [date.slice(0, 8), region, 's3/aws4_request'].join('/');
const stringToSign = ['AWS4-HMAC-SHA256', date, credentialScope, hash(canonicalRequest, 'hex')].join('\n');
// Calculate the signature
const signature = hmac(hmac(hmac(hmac(hmac('AWS4' + options.credentials.secretAccessKey, date.slice(0, 8)), region), "s3"), 'aws4_request'), stringToSign, 'hex');
// Form the authorization header
const authorizationHeader = ['AWS4-HMAC-SHA256 Credential=' + options.credentials.accessKeyId + '/' + credentialScope,'SignedHeaders=' + signedHeaders,'Signature=' + signature].join(', ');
// return required headers for Sigv4 to be added to the request to S3
return {
'Authorization': authorizationHeader,
'X-Amz-Content-Sha256' : emptyHash,
'X-Amz-Date': date,
'X-Amz-Security-Token': options.credentials.sessionToken
};
}
function encodeRfc3986(urlEncodedStr) {
return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())
}
function hash(string, encoding) {
return crypto.createHash('sha256').update(string, 'utf8').digest(encoding)
}
function hmac(key, string, encoding) {
return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding)
}
However when I try to deploy the lambda#edge function with the suggested Node JS code it throws error Cannot read properties of undefined (reading 'startsWith')
What could be the issue here
Select in dropdown "viewer response" or "origin response" and after that return to "origin request".

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)

How to remove .html extension using AWS Lambda & Cloudfront

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!!

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.