Initial Commit

This commit is contained in:
iqbal rifai 2018-05-28 08:27:10 +02:00
parent 45f1874078
commit 1b94f880a6
5 changed files with 282 additions and 2 deletions

41
LinkShim.dev.js Normal file
View File

@ -0,0 +1,41 @@
var LinkShim = {
//set options
redirect_url: "http://yoururl.com:8888/r?",
tracker_attr: "data-track",
params: {},
//called on mousedown event
changeHref: function(a) {
var $a = $(a);
var params = this.params;
var url = this.redirect_url + $a.attr(this.tracker_attr);
//build params
params.href = $a.attr('href');
for (var f in params) {
url += "&" + f + "=" + encodeURIComponent(params[f]);
}
//change href of click
$a.attr('href', url);
},
//manually call to add PageParams
addPageParams: function(new_params) {
if (typeof new_params == 'object') {
for (var f in new_params) {
this.params[f] = new_params[f];
}
}
},
//call to listen to clicks on objects that contain the tracker_attr par
init: function() {
var obj = this; //added b/c 'this' will end up refering to a tag
$("a[" + obj.tracker_attr + "]").bind('mousedown', function(e) {
e.preventDefault();
obj.changeHref(this);
});
}
};

1
LinkShim.min.js vendored Normal file
View File

@ -0,0 +1 @@
var LinkShim={redirect_url:"http://yoururl.com:8888/r?",tracker_attr:"data-track",params:{},changeHref:function(a){var b=$(a);var c=this.params;var d=this.redirect_url+b.attr(this.tracker_attr);c.href=b.attr("href");for(var e in c){d+="&"+e+"="+encodeURIComponent(c[e])}b.attr("href",d)},addPageParams:function(a){if(typeof a=="object"){for(var b in a){this.params[b]=a[b]}}},init:function(){var a=this;$("a["+a.tracker_attr+"]").bind("mousedown",function(b){b.preventDefault();a.changeHref(this)})}}

View File

@ -1,3 +1,60 @@
# LinkShim
Notice:
======
This is a proof of concept, and not a production-ready script. Use it as a reference.
l.facebook.com clone
LinkShim
========
Replicates Facebook Functionality of their LinkShim
When you click on a link on Facebook to an external url, they take you to a script on Facebook that redirects you to link you requested. This is an important security feature, for the following reasons:
### Protects People
Creates opportunity to stop malicious and spammy sites in real-time.
### Protect Privacy
Websites know where you came from by the referrer attribute in the header. On most pages, this might not be an issue. But if I clicked on a link that was on my profile, the website could glean the fact that my facebook user
name is "iqbalrifaii" because my referrer would be "http://www.facebook.com/iqbalrifaii". But when we use a redirect script, the referrer is simply "http://www.facebook.com/l.php"
### Gather Analytics
A successful web company should know what's be linked to, shared by who, clicked by who, trends, etc. A redirect script creates that opportunity.
### Learn More About Facebook's (& thus this) LinkShim
Matt Jones, an engineer at Facebook, wrote an excellent explanation of their LinkShim
https://www.facebook.com/note.php?note_id=10150492832835766
How to Setup
========
This project is meant to be a framework you can use to quickly set a LinkShim. Thus, it is not comprehensive (for example, there is no user specific logging, which would be necessary in production). Follow these steps to setup:
### 1. Install Redis & Tornado
LinkShim uses [Redis](http://redis.io), a [NoSQL](http://en.wikipedia.org/wiki/NoSQL) technology, to maintain a spam watchlist, an analytics container, and a set of valid hashes to prevent becoming an [OpenRedirector](https://www.owasp.org/index.php/Open_redirect)
It also uses the python (Tornado Framework)[http://www.tornadoweb.org/], a scalable, non-blocking web server. I implemented this in python rather than PHP so we can keep settings and database connections open between calls. In redirect engines, speed is of utmost importantance (behind security, of course.)
### 2. Change Settings
Download these files, place them where you want, and open server.py. Change the `admin_token` to something random/secure, and `listen_on_port` to the port you want to listen to, and templates_dir to the absolute path of your templates. (duh.)
### 3. Start er' Up.
`python server.py` will work for testing. In production, you'll want to use a daemon.
### 4. Create some Hashes
Have your frontend guys/gals hit `/hash?admin_token=YOUR_TOKEN&num=10` to create some hashes when they need them. By Default, tokens are valid for 6 hours. This endpoint should really be only avaiable internally for security reasons. It's on the same port for now just for the demo.
### 5. Include the JS and place the Hashes
Include the JS Script on your page. Place one of the hashes in JS like so:
```javascript
<script type="text/javascript">
LinkShim.init();
LinkShim.addPageParams({
pageVersion: 'a',//great for A/B testing!
hash: 'CREATED_HASH',
anyRandomPageVar: '3000'
});
</ script >
```

30
example.html Normal file
View File

@ -0,0 +1,30 @@
<doctype html>
<head>
<title>Example Content Page</title>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="../LinkShim.min.js"></script>
<script type="text/javascript">
LinkShim.init();
LinkShim.addPageParams({
pageVersion: 'a',//great for A/B testing!
hash: '89rc3289cr3r789qx3qc48tjafhp938fhdkfjg398256g',
anyRandomPageVar: '3000'
});
</script>
</head>
<body>
<a data-track="" href="http://www.download.com">This link is outbound, and will be tracked!</a>
<br/><Br/>
<a data-track="placement=2" href="http://www.download.com">This link will also save placement=2 within redis</a>
<br/>
<a data-track="placement=2&version=2" href="http://www.download.com">This will save placement=2&version=2 as well!</a>
<br/><br/>
<a href="http://www.download.com">This link is outbound, and will not be tracked!</a>
</body>

151
server.py Normal file
View File

@ -0,0 +1,151 @@
import tornado.ioloop
import tornado.web
import tornado.template
from urlparse import urlparse
import redis
import datetime
import string
import random
import time
#Admin Handler
class HashHandler(tornado.web.RedirectHandler):
def initialize(self, cache, admin_token):
self.cache = cache
self.admin_token = admin_token
self.life_of_hash = 60*60*6
def get(self):
#simple admin check. Best to move this endpoint to an internal IP for security on production
if self.get_argument("admin_token") != self.admin_token:
self.write({"error":1,"errorMsg":"Are you the admin?"})
return 1
#get the number of hashes they want. Default to 10.
num = int(self.get_argument("num",10))
if num > 1000000000000:
self.write({"error":1,"errorMsg":"No more than 100 at a time, please."})
return 1
expiration = int(time.mktime(time.gmtime())) + self.life_of_hash
tokens = []
for i in range(num):
#create token and add it to the database
token = self.generateRandomToken()
self.cache.zadd("linkshim:hashes", expiration, token)
tokens.append(token)
self.write({"error":0,"tokens": tokens,"expiration":expiration})
return 1
def generateRandomToken(self, size=25, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits):
return "".join(random.choice(chars) for x in range(size))
#Handle requests to redirect
class RedirectHandler(tornado.web.RequestHandler):
def initialize(self, cache, templates_dir):
self.cache = cache
self.loader = tornado.template.Loader(templates_dir)
def get(self):
href = self.get_argument("href")
current_timestamp = int(time.mktime(time.gmtime()))
#add the url to today's set
today = datetime.date.today().strftime("%Y-%m-%d")
self.cache.zincrby("linkshim:outbound:" + today , href, 1)
#check domain for watch_list
domain = self.getDomain(href)
ismember = self.cache.sismember("linkshim:watchlist",domain)
#if it is in the watchlist, lets totally block that domain by displaying watchlist.html
if ismember:
self.writeConfirmationMessage("watchlist.html",href)
return 1
#get expiration of the hash provided
h = str(self.get_argument("h"))
expiration = self.cache.zscore("linkshim:hashes", h)
#if hash doesn't exist, or its expired, display the warning message
if expiration == None or expiration < time.mktime(time.gmtime()):
self.writeConfirmationMessage("warning.html", href)
return 1
#we're all good! [domain not marked as spam, and hash is valid]
self.smartRedirect(href)
return 1
#write a simple HTML page to stop user from being linked, or to warning them (you edit the HTML in watchlist.html or warning.html)
def writeConfirmationMessage(self, file, href):
html = self.loader.load(file).generate(href=href)
self.write(html)
return 1
#using this rather than a simple header redirect makes the referrer from this page, to protect privacy of the user
def smartRedirect(self, href):
#we set a refresh header just to be safe, but hopefully we won't use it.
self.set_header("Refresh","1")
self.set_header("URL", href)
#IE requires a special solution.
if self.isIE():
self.write("<a hef='" + href + "' id='a'></a><script>document.getElementById('a').click();</script>")
else:
self.write("<script>document.location.replace('" + href + "');</script>")
return 1
#IE sucks. Let's use a helper function to detect it
def isIE(self):
if "User-Agent" not in self.request.headers:
return False
user_agent = self.request.headers["User-Agent"]
if user_agent.find("MSIE") != -1:
return True
return False
#get raw domain name, stripped of subdomains and ports
def getDomain(self, href):
url_parts = urlparse(href)
domain = '.'.join(url_parts.netloc.split('.')[-2:])
index = domain.find(":")
if index != -1:
domain = domain[:index]
return domain
if __name__ == "__main__":
#connect to redis!
redis_cache = redis.StrictRedis(host='localhost', port=6379, db=0)
#change to random string. Strong authentication with IP checks, etc. should be required on production scripts.
admin_token = "JTCyFFO7OMWRxlnLCp6gp4fcJaLj2234tv3U0AabE7iQ"
listen_on_port = 8888
#absolute path that our templates are located in
templates_dir = "/var/www/linkshim/templates"
application = tornado.web.Application([
(r"/r", RedirectHandler, dict(cache=redis_cache, templates_dir=templates_dir)),
(r"/hash", HashHandler, dict(cache=redis_cache,admin_token=admin_token)),
])
application.listen(listen_on_port)
tornado.ioloop.IOLoop.instance().start()