-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit eae8956
Showing
36 changed files
with
1,654 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
deps/* | ||
ebin/* | ||
priv/log/* | ||
priv/mnesia.store/* | ||
.eunit/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Copyright (c) 2012 Moritz Windelen | ||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
ERL ?= erl | ||
APP := erli | ||
|
||
.PHONY: deps | ||
|
||
all: deps | ||
@./rebar compile | ||
|
||
deps: | ||
@./rebar get-deps | ||
|
||
clean: | ||
@./rebar clean | ||
|
||
distclean: clean | ||
@./rebar delete-deps | ||
|
||
docs: | ||
@erl -noshell -run edoc_run application '$(APP)' '"."' '[]' | ||
|
||
test: | ||
@./rebar skip_deps=true eunit | ||
|
||
compile: | ||
@./rebar compile |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# erli | ||
|
||
**erli** is an Erlang based URL-shortener, with statistics and 'nsfw' functionality. | ||
|
||
## statistics | ||
|
||
Every shortened URL keeps track of visitor counts (non-real time total and unique visit counts) and provides estimates for the geographic location of visitors. | ||
|
||
## 'nsfw' functionality | ||
|
||
Appending `?landing=true` to a shortened URL, prevents the automatic redirect, instead a landing page, showing the full target URL and an "are you sure you want to proceed" button, is displayed. | ||
|
||
Reporting inappropriate URLs is kept extremely simple, just add `/report` to the URL. URLs that reach a certain threshold - this mechanism will be expanded in the future (pull requests are most welcome ;)) - are permanetly banned. | ||
|
||
## demo | ||
|
||
Live demo will be up as soon as I get around to it. | ||
|
||
To play around with it on localhost, just clone, `make compile`, `./start.sh` and open [http://localhost:8000](http://localhost:8000). | ||
|
||
## todo | ||
|
||
security: | ||
|
||
+ implement throttling via 503 with `retry-after` (based on a simple request/interval scheme?) | ||
|
||
usability: | ||
|
||
+ timeline of visits by hour | ||
+ thumbnails on the landing pages? | ||
+ ensure that no matter the `STATS_COLLECT_INTERVAL`, statistics don't get corrupted | ||
|
||
code quality: | ||
|
||
+ eunit tests | ||
|
||
## the api | ||
|
||
### root_resource.erl | ||
|
||
GET / -> displays the index | ||
POST / -> create a new shortened URL with a generated path | ||
|
||
### path_resource.erl | ||
|
||
GET /path -> grab the redirect to the target URL | ||
DELETE /path -> report the target | ||
PUT /path -> create a new shortened url with a preferred path | ||
----------------- | ||
GET /path/report -> report the target URL | ||
----------------- | ||
GET /path/stats -> view stats for the path | ||
----------------- | ||
GET /path/check -> utility URL to facility 'low' overhead checking whether a path is already taken via ajax (@ W3C please give us an option to disable the automatic redirect on 30x!) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
%%-*- mode: erlang -*- | ||
{[], root_resource, []}. | ||
{["static", '*'], static_resource, ["static"]}. | ||
{[path, '*'], path_resource, []}. | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import datetime | ||
import os | ||
import subprocess | ||
|
||
from erlport import Atom, Port, Protocol, String | ||
import pygeoip | ||
|
||
|
||
class ParseEval(Protocol): | ||
"""Parses the webmachine access log for usage statistics of shortened URLs""" | ||
|
||
def __init__(self, log_dir=None): | ||
super(ParseEval, self).__init__() | ||
self.log_dir = log_dir or os.path.join(os.path.abspath(os.getcwd()), | ||
'priv', | ||
'log') | ||
|
||
def handle_path(self, path): | ||
# parse the log files from the last hour | ||
log_timestamp = datetime.datetime.utcnow() - datetime.timedelta(hours=1) | ||
log_fname = 'access.log.%d_%02d_%02d_%02d' % (log_timestamp.year, | ||
log_timestamp.month, | ||
log_timestamp.day, | ||
log_timestamp.hour) | ||
log_fpath = os.path.join(self.log_dir, log_fname) | ||
if not os.path.exists(log_fpath): | ||
return Atom('reschedule'), log_timestamp.minute + 1 | ||
|
||
parse_cmd = 'grep -e \'GET /%s \' %s | awk \'{ print $1 }\'' % (String(path), | ||
log_fpath) | ||
gi = pygeoip.GeoIP('/usr/local/GeoIP/GeoIP.dat') | ||
proc = subprocess.Popen(parse_cmd, shell=True, stdout=subprocess.PIPE) | ||
stdout, stderr = proc.communicate() | ||
total_visits = 0 | ||
countries = set() | ||
ips = set() | ||
for ip in stdout.splitlines(): | ||
countries.add(gi.country_code_by_addr(ip)) | ||
ips.add(ip) | ||
total_visits += 1 | ||
return list(countries), list(ips), total_visits | ||
|
||
|
||
if __name__ == "__main__": | ||
pe = ParseEval() | ||
pe.run(Port(use_stdio=True)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
h1 a:link { | ||
text-decoration: none; | ||
color: #404040; | ||
} | ||
h1 a:visited { | ||
text-decoration: none; | ||
color: #404040; | ||
} | ||
h1 a:hover { | ||
text-decoration: none; | ||
color: #404040; | ||
} | ||
body { | ||
text-align: center; | ||
padding-top: 40px; | ||
} | ||
label { | ||
float: none; | ||
} | ||
.container { | ||
width: 820px; | ||
} | ||
.alert-message { | ||
margin-bottom: 0px; | ||
} | ||
form .input { | ||
margin-left: 0px; | ||
} | ||
form .clearfix.error input, form .clearfix.error textarea { | ||
color: gray; | ||
} | ||
#about_popover, #url_structure_popup, #path_help_popup { | ||
color: #BFBFBF; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
/* global vars */ | ||
var re = /^(?:[a-zA-Z0-9]+:\/\/)(?:(?:(?:[a-zA-Z0-9]+\.)+(?:[a-zA-Z]+))|(?:(?:[0-9]+\.){3}(?:[0-9]+))|(?:(?:[a-f0-9]+\:)+(?:[a-f0-9]+)))(?:(?:\s*$)|(?:(?:[:\/?]).+$))/; | ||
|
||
/* utility functions */ | ||
function show_error_and_helptext(E) { | ||
E.parent().addClass("error"); | ||
E.siblings('.help-inline').show(); | ||
} | ||
|
||
function hide_error_and_helptext(E) { | ||
E.parent().removeClass("error"); | ||
E.siblings('.help-inline').hide(); | ||
} | ||
|
||
$(document).ready(function() { | ||
$(".alert-message.warning").remove(); // close the banner for non-js enabled visitors | ||
$('.alert-message.error').alert(); | ||
$('#url_structure_popup').popover(); | ||
$('#path_help_popup').popover(); | ||
|
||
/* Modal ToU button click handlers */ | ||
$(".modal-footer a.primary").click(function(e){ | ||
$("#tou_checkbox").attr("checked", "checked"); | ||
$("#modal-tou").modal("hide"); | ||
e.preventDefault(); | ||
}); | ||
$(".modal-footer a.secondary").click(function(e){ | ||
$("#modal-tou").modal("hide"); | ||
e.preventDefault(); | ||
}); | ||
|
||
/* validation functions for the URL submission form */ | ||
$('#tou_checkbox').change(function(e){ | ||
var parDiv = $(this).parents('div.clearfix.input'); | ||
parDiv.removeClass("error"); | ||
}); | ||
$('#url_input').keyup(function(){ | ||
hide_error_and_helptext($(this)); | ||
}); | ||
$('#pref_path_input').keyup(function() { | ||
$.ajax({ | ||
type: 'GET', | ||
url: $(this).val() + '/check', // need to append the check call to guarantee that the status code is either 200 or 404 | ||
statusCode: | ||
{ | ||
404: function() { | ||
// path available | ||
hide_error_and_helptext($('#pref_path_input')); | ||
}, | ||
200: function() { | ||
// path taken | ||
show_error_and_helptext($('#pref_path_input')); | ||
} | ||
} | ||
}); | ||
}); | ||
|
||
$('#url_input').blur(function(){ | ||
if (!$('#url_input').val().match(re)){ | ||
show_error_and_helptext($('#url_input')); | ||
} | ||
}); | ||
|
||
/* form submission */ | ||
$("#shorten_url_form").submit(function() { | ||
var url = $("#url_input").val(); | ||
var checked = $("#tou_checkbox").attr("checked") ? true : false; | ||
var pref_path = $('#pref_path_input').val(); | ||
if (!checked || !url) { | ||
if (!url) { | ||
show_error_and_helptext($('#url_input')); | ||
} | ||
if (!checked) { | ||
$('#tou_checkbox').parents('div.clearfix.input').addClass("error"); | ||
} | ||
return false; | ||
} | ||
|
||
if (!url.match(re)){ | ||
show_error_and_helptext($('#url_input')); | ||
return false; | ||
} | ||
if (pref_path) { | ||
$.ajax({ | ||
type: 'PUT', | ||
url: pref_path, | ||
contentType: 'application/json', | ||
data: JSON.stringify({ | ||
url: url, | ||
tou_checked: checked | ||
}), | ||
statusCode: { | ||
204: function(){ | ||
$('#path').attr('href', pref_path).text(pref_path); | ||
$('#path_stats').attr('href', pref_path + '/stats').text(pref_path + '/stats'); | ||
$('#success_banner').show(); | ||
}, | ||
400: function(){ | ||
// bad request - should never occur | ||
}, | ||
409: function(){ | ||
// conflict, pref_path is already taken | ||
show_error_and_helptext($('#pref_path_input')); | ||
}, | ||
410: function(){ | ||
// target url is permanently banned | ||
$('#banned_url').text($('#url_input').val()); | ||
$('.alert-message.error').show(); | ||
} | ||
} | ||
}); | ||
} | ||
else { | ||
$.ajax({ | ||
type:'POST', | ||
url:'/', | ||
data: JSON.stringify({ | ||
url: url, | ||
tou_checked: checked | ||
}), | ||
contentType: 'application/json', | ||
dataType: 'json', | ||
processData: false, // we want the raw json object to be transmitted | ||
statusCode: { | ||
201: function(data, textStatus, jqXHR) { | ||
var location = jqXHR.getResponseHeader('Location'); | ||
$('#path').attr('href', location).text(location); | ||
$('#path_stats').attr('href', location + '/stats').text(location + '/stats'); | ||
$('#success_banner').show(); | ||
}, | ||
400: function(){ | ||
}, | ||
410: function(){ | ||
$('#banned_url').text($('#url_input').val()); | ||
$('.alert-message.error').show(); | ||
}, | ||
500: function(){ | ||
} | ||
} | ||
}); | ||
} | ||
return false; | ||
}); | ||
}); |
Oops, something went wrong.