AWS static website - how to connect subdomains with subfolders - amazon-s3

I want to setup S3 static website and connect with my domain (for example domain: example.com).
In this S3 bucket I want to create one particular folder (name content) and many different subfolders with in, then I want to connect these subfolders with appropriate subdomains, so for example
folder content/foo should be available from subdomain foo.example.com,
fodler content/bar should be available from subdomain bar.example.com.
Any content subfolder should be automatically available from subdomain with that same prefix name like folder name.
I will be grateful for any possible solutions for this problem. Should I use redirection option or there is any better solution? Thanks in advance for help.

My solution base on this video:
https://www.youtube.com/watch?v=mls8tiiI3uc
Because above video don’t explain subdomain problem, here is few additional things to do:
to AWS Route53 hostage zone we should add records A with “*.domainname” as record name and edge address as Value
to certificate domains we should add also “*.domainname”- to have certificate for wildcard domain
when setting up Cloudfront distribution we should add to “Alternate domain name (CNAME)“ section “www.domainname” and also “*.domainname”
redirection/forwarding from subdomain to subfolder is realizing via Lambda#Edge function (function should be improve a bit):
'use strict';
exports.handler = (event, context, callback) => {
const path = require("path");
const remove_suffix = ".domain.com";
const host_with_www = "www.domain.com"
const origin_hostname = "www.domain.com.s3-website.eu-west-1.amazonaws.com";
const request = event.Records[0].cf.request;
const headers = request.headers;
const host_header = headers.host[0].value;
if (host_header == host_with_www) {
return callback(null, request);
}
if (host_header.startsWith('www')) {
var new_host_header = host_header.substring(3,host_header.length)
}
if (typeof new_host_header === 'undefined') {
var new_host_header = host_header
}
if (new_host_header.endsWith(remove_suffix)) {
// to support SPA | redirect all(non-file) requests to index.html
const parsedPath = path.parse(request.uri);
if (parsedPath.ext === "") {
request.uri = "/index.html";
}
request.uri =
"/" +
new_host_header.substring(0, new_host_header.length - remove_suffix.length) +
request.uri;
}
headers.host[0].value = origin_hostname;
return callback(null, request);
};
Lambda#Edge is just Lambda function connected with particular Cloudfront distribution
need to add to Cloudfront distribution additional setting for Lambda execution (this setting is needed if we want to have different redirection for different subdomian, instead all redirection will point to main directory or probably to first directory which will be cached - first request to our Cloudfront domain):

Related

Cloudfront with lambda edge not working with new cache behavior

I had a CloudFront distribution using the legacy cache Behavior and Aws Lambda Edge to change the origin path to serve multiple websites using the same bucket.
This was my lambda edge that was working with the legacy cache behavior:
|
'use strict';
const env = '${Environment}';
const origin_hostname = 'yourwebsite-${Environment}.s3.amazonaws.com';
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const host_header = headers.host[0].value;
var remove_suffix = '.yourwebsite.com';
if(env == "dev"){
remove_suffix = '-dev.yourwebsite.com';
}
if(host_header.endsWith(remove_suffix))
{
request.uri = '/' + host_header.substring(0,host_header.length - remove_suffix.length) + request.uri;
}
// fix the host header so that S3 understands the request
headers.host[0].value = origin_hostname;
// return control to CloudFront with the modified request
return callback(null,request);
};
This was my CloudFormation Lambda function association and cache policies:
LambdaFunctionAssociations:
- EventType: origin-request
LambdaFunctionARN: !Ref HotSitesEdgeFunctionVersion
CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6"
ResponseHeadersPolicyId: "67f7725c-6f97-4210-82d7-5512b31e9d03"
After some hours working to understand, I realize that the host value was ..s3.amazonaws.com and not my subdomain. :(
The solution was
Create a new OriginRequestPolicy and attach the id to OriginRequestPolicyId in your distribution.
HotSiteCustomOriginRequestPolicy:
Type: AWS::CloudFront::OriginRequestPolicy
Properties:
OriginRequestPolicyConfig:
Comment: Custom policy to redirect Host header
CookiesConfig:
CookieBehavior: none
HeadersConfig:
HeaderBehavior: whitelist
Headers:
- Host
- Origin
Name: HotSiteCustomOriginRequestPolicy
QueryStringsConfig:
QueryStringBehavior: none
And in your distribution
OriginRequestPolicyId: !Ref HotSiteCustomOriginRequestPolicy
Documentation for all managed policy if you need:
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html
Basically, you have to forward the CloudFront Host and Origin to your lambda edge.
I hope this can help you guys.

Use Cloudfront URL for iFrame

I have a Nodejs - Express middleware that alters incoming file requests to point to Cloudfront
function cloudfrontController({url}, res, next) {
const ext = url.substring(url.lastIndexOf('.') + 1);
const name = url.substring(url.lastIndexOf('/') + 1);
const location = url.substring(0, url.lastIndexOf('/'));
const newurl = `${config.aws.cloudfront}${location}/${encodeURIComponent(name)}`;
request(newurl).pipe(res);
}
I display the pdf in an iframe on the front end.
This seems to work fine when I use the S3 URL, however, the PDF does not load when using the the cloudfront URL. When I update the src of my frame with a new Cloudfront URL, the pdf downloads (but still doesn't display). In essence, this works but defeats the purpose of using cloudfront in the first place:
const newurl = `${ext === 'pdf' ? config.aws.s3 : config.aws.cloudfront}${location}/${encodeURIComponent(name)}`;
Examples:
S3 URL Works for iFrame
https://npg-cloud.s3-us-west-2.amazonaws.com/companies/ipadmin/users/57a8b211f77b5b3801255034/57a8b211f77b5b3801255034_1564598795092.pdf
CloudFront URL doesn't work for iFrame
https://d2eva5limbx0hv.cloudfront.net/companies/ipadmin/users/57a8b211f77b5b3801255034/57a8b211f77b5b3801255034_1564598795092.pdf

Lambda#edge redirect gets in a redirect loop

I have a static website on aws s3. Have setup route 53 and cloud front and everything works smoothly. s3 Bucket is setup to serve index.html as index document.
Now I have added another file called index-en.html that should be served when the request country is any other country and not my home country.
For this I have added a lambda#edge function with the following code:
'use strict';
/* This is an origin request function */
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
/*
* Based on the value of the CloudFront-Viewer-Country header, generate an
* HTTP status code 302 (Redirect) response, and return a country-specific
* URL in the Location header.
* NOTE: 1. You must configure your distribution to cache based on the
* CloudFront-Viewer-Country header. For more information, see
* http://docs.aws.amazon.com/console/cloudfront/cache-on-selected-headers
* 2. CloudFront adds the CloudFront-Viewer-Country header after the viewer
* request event. To use this example, you must create a trigger for the
* origin request event.
*/
let url = 'prochoice.com.tr';
if (headers['cloudfront-viewer-country']) {
const countryCode = headers['cloudfront-viewer-country'][0].value;
if (countryCode === 'TR') {
url = 'prochoice.com.tr';
} else {
url = 'prochoice.com.tr/index-en.html';
}
}
const response = {
status: '302',
statusDescription: 'Found',
headers: {
location: [{
key: 'Location',
value: url,
}],
},
};
callback(null, response);
};
I have also edited cloud front behavior to whitelist Origin and Viewer-country headers and setup the cloudfront Viewer-Request event and lambda Function ARN relation.
However I get a "too many redirect error".
I have 2 questions:
How to correct the "too many redirects error"?
For viewers outside "TR" the default landing page should be index-en.html, from which 2 more pages in english are accessible via navigation menu. So when users request a specific page from page navigation they should be able to access those pages, when no page is requested the default landing page should be served.
Appreciate help.
Thanks.
You are creating a redirect loop because you are sending the viewer back to the same site, same page, no matter what the results of your test.
if (countryCode === 'TR') {
return callback(null, request);
} else {
...
callback(null,request) tells CloudFront to continue processing the request -- not generate a response. Using return before the callback causes the rest of the trigger code not to run.

Express-Gateway, How to pick a service end point based on URL pattern?

I am trying to get a bunch of individual servers on the same domain behind the gateway. Currently, each of these servers can be reached from outside world via multiple names. Our sales team wanted to provide customers with a unique url, so if a server serves 10 customers, we have 10 CNAME records pointing to it.
As you can see, with 5 or 6 servers, the number of apiEndpoints is pretty large. On top of that, new CNAMEs can be created at any given time making hardcoded apiEndpoints a pain to manage.
Is it possible to have a dynamic serviceEndpoint url. What I'm thinking is something like this:
apiEndpoints:
legacy:
host: '*.mydomain.com'
paths: '/v1/*'
serviceEndpoints:
legacyEndPoint:
url: '${someVarWithValueofStar}.internal.com'
pipelines:
default:
apiEndpoints:
- legacy:
policies:
- proxy:
- action:
serviceEndpoint: legacyEndPoint
Basically, what I want to achieve is to redirect the all the x.mydomain.com to x.internal.com where x can be anything.
Can I use variables in the url strings? Is there a way to get the string that matched the wild card in the host? Are there other options to deal with this problem?
I ended up hacking a proxy plugin together for my needs. Very basic and requires more work and testing, but this what I started with:
The proxy plugin (my-proxy)
const httpProxy = require("http-proxy");
/**
* This is a very rudimentary proxy plugin for the express gateway framework.
* Basically it will redirect requests xxx.external.com to xxx.internal.com
* Where xxx can be any name and the destination comes from providing a
* service endpoint with a http://*.destination.com url
* #param {*} params
* #param {*} config
*/
module.exports = function (params, config) {
const serviceEndpointKey = params.serviceEndpoint;
const changeOrigin = params.changeOrigin;
const endpoint = config.gatewayConfig.serviceEndpoints[serviceEndpointKey];
const url = endpoint.url;
const reg = /(\/\/\*\.)(\S+)/;
const match = reg.exec(url);
const domain = match[2];
const proxy = httpProxy.createProxyServer({changeOrigin : changeOrigin});
proxy.on("error", (err, req, res) => {
console.error(err);
if (!res.headersSent) {
res.status(502).send('Bad gateway.');
} else {
res.end();
}
});
return (req, res, next) => {
const hostname = req.hostname;
const regex = /^(.*?)\./
const tokens = regex.exec(hostname)
const serverName = tokens[1];
const destination = req.protocol + "://" + serverName + "." + domain;
proxy.web(req, res, {target : destination});
};
};
gateway.config.xml
http:
port: 8080
apiEndpoints:
legacy:
host: '*.external.com'
paths: '/v1/*'
serviceEndpoints:
legacy_end_point:
url: 'https://*.internal.com'
policies:
- my-proxy
pipelines:
default:
apiEndpoints:
- legacy
policies:
- my-proxy:
- action:
serviceEndpoint: legacy_end_point
changeOrigin: true
It all boils down to regex parsing the wild cards in the apiEndpoints and serviceEndpoints host and urls, nothing fancy so far. I looked at the source code of the built in proxy plugin and I don't think my naive approach will fit in very well, but it works for what I need it.
thanks for the question, I think this is going to be asked a lot over the following months.
Express Gateway has support for environment variables; unfortunately right now the apiEndpoint can only be a single and well defined endpoint without any replacement capabilities.
This is something we'll probably change in the near term future — with a Proxy Table API that will let you insert some more difficult templates.
In case this is pressing for you, I'd invite you to open an issue so that everybody in the team is aware of such feature and we can prioritize it effectively.
In meantime, unfortunately, you'll have to deal with numerous numbers of ApiEndpoints
V.

Mixing Wildcards and Parameters in Express

I have web content generated by Minecraft Overviewer in:
/home/mc/backups/servername/latest/overviewer
I have a very simple server with express/nodejs. Here is my app.js:
var express = require('express');
var app = express();
//----------------------------------------------------------------------------+
// Each server's root points to the latest overviewer page |
//----------------------------------------------------------------------------+
app.get('/minecraft/:server/*', function(req, res) {
console.log('HELLO?');
res.send('Finally some luck!');
/*
var
server = req.params.server,
file = req.params[0] ? req.params[0] : 'index.html',
dir = '../backups/' + server + '/latest/overviewer';
res.sendFile(file, { root: dir });
*/
});
app.use(express.static('www'));
app.listen(80, function () {
console.log('Web server listening on port 80');
});
I included a little more code than what is running so you can see my intent in case this is an xy kind of problem. I'm wanting to route static files, but I don't think I can use express.static because I want the URL to be mapped a little differently than my file structure (and based on a server name).
So what's the problem with my simple server? When I try and navigate to mysite.com/minecraft/isopre I see a white page saying Cannot GET /minecraft/isopre. If I remove the * from the end of the string I'm routing on line 7 I see Finally some luck!. But I want the star there so I can map mysite.com/minecraft/isopre to index.html or mysite.com/minecraft/isopre/overviewer.js.
So what's the right way to go about doing this?
In order to perform desirable operation i suggest you to use ? symbol for regular expression:
app.get('/minecraft/:server/:file?', function(req, res, next) {
if('undefined' != typeof req.params.file && req.params.file) {
var file = req.params.file;
}
if('undefined' != typeof req.params.server && req.params.server) {
var server = req.params.server;
}
});
In this case :file become optional and node.js won't fail your entire application if wildcard is absent.
Hereby, if req.params.file variable is undefined you could serve index.html file.