Serve different origin as DNS CNAME value, depending on the IP Geolocation - amazon-cloudfront

I have Next.js Application that is deployed as Lambda Edge and Cloudfront that has S3 static files as origin. AWS Cloudfront URL is the value of example-domain.com CNAME.
Now I have a lot of changes to the Application and I want to deploy those changes.
I don't want these changes to be override current deploy, instead I would like to create new environment and that environment should be available in few countries.
So in this case I would have 2 different Cloudfront URLS:
oldfeatures.cloudfront.net
newfeatures.cloudfront.net
Different origin url should be served depending on the geoloaction.
I am not sure that this is the right approach, but I am open for suggestions.
I am managing the domain settings in Cloudflare, all the rest is AWS environment.
How can I achieve this, without making changes to the Applications code.

The easiest (and cost-effective) option would be to create a Cloudflare (CF) Worker and attach it to your subdomains. Probably better to attach it to both subdomains to ensure they can't access the newfeatures.cloudfront.net URL manually if they are 'not allowed' to. The second option would be to use CF Traffic Steering that comes with the CF Load Balancers feature. This option comes at a small cost and might not really fit your use case - it redirects based on regions rather than countries - I believe you'd need to fork more money and get an enterprise account for country based steering.
That said, the following option using a CF Worker JavaScript, will require you to have CF Proxy (the orange cloud) turned on.
Here are 3 options to satisfy your requirements - Recommending option 1 based on my understanding of your question.
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
return setRedirect(request) //Change to appropriate function
}
// Option 1
// Return same newfeatures URL for specific countries based on a country list
// Useful if single URL applies for many countries - I guess that's your case
async function setRedirect(request) {
const country = request.cf.country
var cSet = new Set(["US",
"ES",
"FR",
"CA",
"CZ"]);
if (cSet.has(country)) {
return Response.redirect('https://newfeatures.cloudfront.net', 302);
}
// Else return oldfeatures URL for all other countries
else {
return await fetch(request)
}
}
// Option 2
const countryMap = {
US: "https://newfeatures1.cloudfront.net", //URL1
CA: "https://newfeatures2.cloudfront.net", //URL2
FR: "https://newfeatures3.cloudfront.net" //URL3
}
// Return different newfeatures URL for specific countries based on a map
// Useful if more than two URLs
async function mapRedirect(request) {
const country = request.cf.country
if (country != null && country in countryMap) {
const url = countryMap[country]
return Response.redirect(url, 302)
}
// Else return old features for all other countries
else {
return await fetch(request)
}
}
// Option 3
// Return single newfeatures URL for a single country.
// Much simpler one if you only have one specific country to redirect.
async function singleRedirect(request) {
const country = request.cf.country
if (country == 'US'){
return Response.redirect('https://newfeatures.cloudfront.net', 302)
}
// Else return oldfeatures URL for all other countries
else {
return await fetch(request)
}
}

Related

How to check if two URL's lead to the same path?

I'm building a URL Shortener, and I've decided to recyle the short ID's if possible to save space in my database. How can i check if 2 URL's lead to the same path?
For example, let's say a user generates a short URL for https://google.com/.
My app generates the following short id: jkU3
So if this user visits https://tiny.url/jkU3 my express server will redirect the visitor to https://google.com/.
This works like a charm, but know let's imagine another person visits https://tiny.url/ and generates a short URL for https://google.com. And another one comes and generates a short URL for https://www.google.com/, and another one comes and generates one for https://www.google.com. You get the point..
So far my app would have wasted 4 short ID's.
How can i prevent this from happening? Is there a regex for this?
This is the current code I have for generating short URL's:
app.post("/", (req: Request, res: Response) => {
const shortUrl: string = nanoid(4);
const destination: string = req.body.destination;
UrlSchema.create({
_id: mongoose.Types.ObjectId(),
origin: shortUrl,
destination: destination,
}).then(() => {
// Unique Id
res.json(shortUrl);
});
});
Before creating a new entry you can check work destination with
const existing = await UrlSchema.findOne({destination:req.body.destination});
if(!existing){
// create new
} else{
// return same
}
This way you will be creating destination if it does not exist already. You can remove tariling slash(/) if it exists to match URLs better,
You've listed four slightly different URLs:
https://www.google.com
https://google.com
https://www.google.com/
https://google.com/
None of these are technically the same https request, though it sounds like you want to assume that the / at the end is optional and thus does not make it a different target URL.
The last two are not guaranteed to be the same host as the first two. For google.com and www.google.com, they are the same host, but this is not guaranteed to be the case for all possible hosts.
If you want to assume that these four are all to the same host no matter what the domain is, then you just have to normalize the URL before you put it in your database and then before assigning a new shortened ID, you search the database for the normalized version of the URL.
In this case, you would remove the www. and remove any trailing slash to create the normalized version of the URL.
function normalizeUrl(url) {
// remove "www." if at first part of hostname
// remove trailing slash
return url.replace(/\/\/www\./, "//").replace(/\/$/, "");
}
Once you've normalized the URL, you search for the normalized URL in your database. If you find it, you use the existing shortener for it. If you don't find it, you add the normalized version to your database with a newly generated shortId.
Here's a demo:
function normalizeUrl(url) {
// remove "www." if at first part of hostname
// remove trailing slash
return url.replace(/\/\/www\./i, "//").replace(/\/$/, "");
}
const testUrls = [
"https://www.google.com",
"https://www.google.com/",
"https://google.com",
"https://google.com/",
];
for (const url of testUrls) {
console.log(normalizeUrl(url));
}
FYI, since hostnames in DNS are not case sensitive, you may also want to force the hostname to lower case to normalize it. Path names or query parameters could be case sensitive (sometimes they are and sometime they are not).
To include the host case sensitivity normalization, you could use this:
function normalizeUrl(url) {
// remove "www." if at first part of hostname
// remove trailing slash
// lowercase host name
return newUrl = url.replace(/\/\/www\./i, "//").replace(/\/$/, "").replace(/\/\/([^/]+)/, function(match, p1) {
// console.log(match, p1);
return "//" + p1.toLowerCase();
});
}
const testUrls = [
"https://www.google.com",
"https://www.google.com/",
"https://google.com",
"https://google.com/",
"https://WWW.google.com",
"https://www.Google.com/",
"https://GOOGLE.com",
"https://google.COM/",
"https://www.Google.com/xxx", // this should be unique
"https://google.COM/XXX", // this should be unique
];
for (const url of testUrls) {
console.log(normalizeUrl(url));
}

Best approach for loading Kentico 10 web nodes including caching and permission checking

I have a custom product type that gets displayed in a custom listing web part. I was trying to cache the items for performance reasons, but it's also important to check user permissions as not all products are visible to all users.
private static IList<TreeNode> GetUniqueProducts(string clientId, string path)
{
var pages = CacheHelper.Cache(cs => GetProducts(cs, path), new CacheSettings(10, "cus|" + clientId));
return GetUniqueProductNamesItems(pages);
}
private static IList<TreeNode> GetProducts(CacheSettings cacheSettings, string rootPath)
{
var pages = DocumentHelper.GetDocuments().Types("CUS.Product")
.Path(rootPath, PathTypeEnum.Children)
.Published().CheckPermissions().ToList();
if (cacheSettings.Cached)
{
cacheSettings.CacheDependency = CacheHelper.GetCacheDependency("nodes|custom|cus.product|all");
}
return pages;
}
However I realise that this is caching the first user's list of products. When in fact I want to store the full list of products in cache - but then check permissions before they get displayed.
The only way of checking permission seems to be as part of a DocumentQuery as per above - but I don't know how to apply that to a cached list of products - or on an individual node.
So is there a good way to achieve what I want? Without having to loop through each node and individually check user is authorised to access the node ?
You are missing the caching part in your code and I am not quire sure about your cache dependencies.
var pages = CacheHelper.Cache(cs =>
{
var result = CMS.DocumentEngine.DocumentHelper.GetDocuments().Types("CUS.Product").Path(rooPath, CMS.DocumentEngine.PathTypeEnum.Children).Published().ToList();
if (cs.Cached) { cs.CacheDependency = CacheHelper.GetCacheDependency("cus.product|all"); }
return result;
},
new CacheSettings(CacheHelper.CacheMinutes(CurrentSite.SiteName), "custom_products"));
If you checking the user read permissions it means you kinda caching per user. Then your cache should be done per user i.e. cachename should be "custom_products"+ UserID.ToString() or something like this.

How to implement unique key constraints in CouchDB

I use CouchDB and I would like all my users to have unique emails. I would expect that the database return a status 400 (bad request) when I try to duplicate an email.
But since there is no way to define constraints in CouchDB, I should implement it myself, my question is:
In which layer of my application should this rule stand?
(1) Domain objects layer
I don't really know how to implement it in this layer
(2) Interactor layer
This constraint could be implemented in interactors here since there is the place where business rules stand. But if there is multiple rules for single document it could add unnecessary complexity...
function createUser(userData) {
let email = userData.email;
let exist = await userDB.userExist(email);
if(exist) {
// return status 400
} else {
// create user
}
}
(3) Database gateway layer
The constraint can also be implemented in the database gateway layer. Usually we'll have a gateway for each specific entity. But does that means that external services adapters contains a bit of business logic?
class userDB() {
constructor(opts) {
this.db = opts.db.connect();
}
async userExist(email) {
return await this.db.fetchByView('email', email);
}
async create(email) {
let exist = await this.userExist(data.email);
if(exist) {
// throw error
} else {
// create the user
}
}
}
Unique email address is a very old DDD topic. It relates to the set validation.
The simplest (and from my point of view is also the best) is to place a constraint at the database level.
From what I know, the only whay to create an unique constraint in CouchDB is to use the _id field so you can use this solution. This idea is to put the email in the _id field.
let exist = await this.userExist(data.email);
if(exist) { // throw
error } else { // create the user }
This method is not safe for concurrent updates. Two users can be created at the same time. Imagine that for both the requests the this.userExist(data.email) is returning false.
I am not a CouchDB expert but from pure Clean Architecture perspective ensuring unique email addresses is a business rule which should be implemented in an interactor.
With this approach ur business rule would remain intact even if u would once decide to replace the detail CouchDB with some other detail - another storage system.

Grails domain objects - limit access to specific user

Is there a way to limit access to Grails domain objects across the board to the owner of the object?
For example, I can make an assert easily, but I don't want to duplicate that everywhere, or risk missing a spot.
This isn't exactly the same as multi-tenancy because it's not just a tenant ID - it may be specific business logic for different domain objects.
class MyDomain {
String name
String user
}
class MyController {
def show(Long id) {
def obj = MyDomain.get(id)
// *** How do I not do copy-paste this line in each individual controller
// that touches MyDomain?? ***
assert obj.user == CURRENT_USER
return obj
}
}
There are so many ways to handle such scenarios, as other answers are suggesting, however, I think one proper way to approach it is to use Spring Security Plugin and spring-security-acl plugin. ACL plugin will drill down into object level and helps you to control
According to doc
"The ACL plugin adds Domain Object Security support to a Grails
application that uses Spring Security."
The combination of both security core and ACL can help you accomplish what you need.
Filter might be one way, another way is to adjust your queries if possible. You would probably need to be able to search for some other criteria other than the ID. Without understanding your use case a little more, it is hard to give a better answer.
def show(String someValue) {
def currentUser = howeverYouGetYourUser
def obj = MyDomain.findByUserAndSomeValue(currentUser, someValue)
if (obj) {
// yeah!!!
} else {
// boo!!
}
}
Ideally, if you are looking for specific data for a specific user, ID isn't really the way to go.
Iam not sure if you can do it in domain level but one way is using filters. There is a filters plugin available for Grails. Keep the User in session and verify in filter for each request..
Sample filter code:
class SecurityFilters {
def filters = {
loginCheck(controller: '*', action: '*') {
before = {
if (!session.user && !actionName.equals('login')) {
redirect(action: 'login')
return false
}
}
}
}
}
and specify the filter attributes..
Here is the documentation grails.org/doc/2.2.1/ref/Plug-ins/filters.html

ServiceStack - Redirecting at root based on query params [duplicate]

This question already has answers here:
Create route for root path, '/', with ServiceStack
(3 answers)
Closed 3 years ago.
I've got a Fallback DTO that looks like the following:
[FallbackRoute("/{Path*}")]
public class Fallback
{
public string Path { get; set; }
}
Now, in my Service I would like to redirect to an HTML5 compliant URL, and this is what I've tried:
public object Get(Fallback fallback)
{
return this.Redirect("/#!/" + fallback.Path);
}
It is working all fine and dandy, except for the fact that query parameters are not passed along with the path. Using Request.QueryString does not work as no matter what I do it is empty. Here's what my current (non-working) solution looks like:
public object Get(Fallback fallback)
{
StringBuilder sb = new StringBuilder("?");
foreach (KeyValuePair<string, string> item in Request.QueryString)
{
sb.Append(item.Key).Append("=").Append(item.Value).Append("&");
}
var s = "/#!/" + fallback.Path + sb.ToString();
return this.Redirect(s);
}
TL;DR: I want to pass on query strings along with fallback path.
EDIT: It turns out I had two problems; now going to mysite.com/url/that/does/not/exist?random=param correctly redirects the request to mysite.com/#!/url/that/does/not/exist?random=param& after I changed the above loop to:
foreach (string key in Request.QueryString)
{
sb.Append(key).Append("=").Append(Request.QueryString[key]).Append("&");
}
But the fallback is still not being called at root, meaning mysite.com/?random=param won't trigger anything.
In essence, what I want to do is to have ServiceStack look for query strings at root, e.g., mysite.com/?key=value, apply some logic and then fire off a redirect. The purpose of this is in order for crawler bots to be able to query the site with a _escaped_fragment_ parameter and then be presented with an HTML snapshot prepared by a server. This is in order for the bots to be able to index single-page applications (more on this).
I'm thinking perhaps the FallbackRoute function won't cover this and I need to resort to overriding the CatchAllHandler.
I managed to find a solution thanks to this answer.
First create an EndpointHostConfig object in your AppHost:
var config = new EndpointHostConfig
{
...
};
Then, add a RawHttpHandler:
config.RawHttpHandlers.Add(r =>
{
var crawl = r.QueryString["_escaped_fragment_"];
if (crawl != null)
{
HttpContext.Current.RewritePath("/location_of_snapshots/" + crawl);
}
return null;
});
Going to mysite.com/?_escaped_fragment_=home?key=value will fire off a redirection to mysite.com/location_of_snapshots/home?key=value, which should satisfy the AJAX crawling bots.
N.B. It's possible some logic needs to be applied to the redirection to ensure that there won't be double forward slashes. I have yet to test that.

Resources