Initial Commit
This commit is contained in:
parent
45f1874078
commit
1b94f880a6
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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)})}}
|
61
README.md
61
README.md
|
@ -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 >
|
||||
```
|
||||
|
|
|
@ -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>
|
|
@ -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()
|
Loading…
Reference in New Issue