Created
November 23, 2016 14:25
-
-
Save mikeash/daf02b6cc3017560f930515c3c17af43 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
function HTMLEscape(s) | |
{ | |
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); | |
} | |
function CommentPreview() | |
{ | |
$("#commentPreviewLabelArea").html("<b>Loading comment preview....</b><p>"); | |
$.post("/commentsubmit.py", $("#commentForm").serialize() + "&preview=true", | |
function(data) { | |
$("#commentLoadingArea").html(""); | |
$("#commentPreviewLabelArea").html("<b>Comment preview:</b><p>"); | |
$("#commentPreviewArea").html(data); | |
var target = $("#commentPreviewLabelArea"); | |
var top = target.offset().top; | |
$("html,body").animate({scrollTop : top}, 100); | |
}); | |
return false; | |
} | |
$(document).ajaxError(function(event, request, settings) { | |
if(settings.url.indexOf("/commentsubmit.py?") === 0) | |
{ | |
$("#commentLoadingArea").html(""); | |
$("#commentPreviewLabelArea").html("<b>Error getting comment preview:</b><p>") | |
$("#commentPreviewArea").html( | |
"Server returned " + request.status + " " + HTMLEscape(request.statusText) + "<p>" + | |
HTMLEscape(request.responseText)); | |
} | |
}); | |
var gHashcashStarted = false; | |
function StartHashcashIfNecessary() | |
{ | |
if(gHashcashStarted) | |
return; | |
gHashcashStarted = true; | |
$("#commentLoadingArea").text("Fetching salt..."); | |
$.getJSON("/commentsubmit.py?action=getsalt", function(result) { | |
var salt = result["salt"]; | |
var zeroes = result["zeroes"]; | |
var workCount = result["work_count"]; | |
$("#commentLoadingArea").text("Computing hashcash...."); | |
HashcashBG(salt, zeroes, workCount, | |
function(progress) { | |
$("#commentLoadingArea").text("Computing hashcash (" + progress + " complete out of " + workCount + ")."); | |
}, | |
function(buf) { | |
var hash = Sha1.hash(salt + buf, 0); | |
$("#saltinput").val(salt); | |
$("#hashcash").val(buf); | |
$("#commentLoadingArea").text(""); | |
$("#submit").removeAttr("disabled"); | |
}); | |
}).error(function(request, status, error) { | |
$("#commentLoadingArea").text("Error loading hashcash salt: " + error); | |
gHashcashStarted = false; | |
setTimeout("StartHashcashIfNecessary()", 2000); | |
}); | |
} |
This file contains hidden or 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
#!/usr/bin/python | |
import cgi | |
import datetime | |
import hashlib | |
import json | |
import md5 | |
import os | |
import sys | |
import sqlite3 | |
import time | |
import Comments | |
import CommentsDB | |
blacklist = ['112.111.174.222', '58.22.64.84'] | |
HASHCASH_ZEROES = 13 | |
HASHCASH_WORK_COUNT = 128 | |
HASHCASH_SQLITE_PATH = 'hashcash-salts.sqlite3' | |
SALT_TIMEOUT = 7 * 24 * 60 * 60 # time out after a week, whatever | |
def getField(form, field): | |
if form.has_key(field): | |
return form[field].value | |
else: | |
return None | |
def main(): | |
form = cgi.FieldStorage() | |
action = getField(form, "action") | |
if action == "delete": | |
deleteComment(form) | |
elif action == "getsalt": | |
getSalt(form) | |
else: | |
addComment(form) | |
def deleteComment(form): | |
print "Content-Type: text/html; charset=utf-8\n\n" | |
md5 = getField(form, "md5") | |
db = CommentsDB.connect() | |
cursor = db.cursor() | |
query = "delete from comments where md5=?" | |
cursor.execute(query, (md5,)) | |
db.commit() | |
db.close() | |
print '<meta http-equiv="refresh" content="0;/recentcomments.html">' | |
print "The comment %s has been deleted" % md5 | |
def DBConnect(): | |
with sqlite3.connect(HASHCASH_SQLITE_PATH) as connection: | |
cursor = connection.cursor() | |
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='salts';") | |
if not cursor.fetchone(): | |
cursor.execute("CREATE TABLE salts(salt TEXT UNIQUE, zeroes INT, work_count INT, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP)") | |
return connection | |
def DBPrune(): | |
with DBConnect() as connection: | |
cursor = connection.cursor() | |
cursor.execute("DELETE FROM salts WHERE strftime('%s', 'now') - strftime('%s', created) > ?", (SALT_TIMEOUT, )) | |
def getSalt(form): | |
DBPrune() | |
print 'Content-Type: application/json' | |
print '' | |
salt = os.urandom(20).encode('hex') | |
zeroes = HASHCASH_ZEROES | |
workCount = HASHCASH_WORK_COUNT | |
with DBConnect() as connection: | |
connection.execute("INSERT INTO salts(salt, zeroes, work_count) VALUES(?, ?, ?);", (salt, zeroes, workCount)) | |
d = { | |
'salt' : salt, | |
'zeroes' : zeroes, | |
'work_count' : workCount | |
} | |
print json.dumps(d) | |
def CountLeadingZeroes(s): | |
if len(s) == 0: | |
return 0 | |
c = ord(s[0]) | |
if c == 0: | |
return 8 + CountLeadingZeroes(s[1:]) | |
zeroes = 0; | |
while (c & 0x80) == 0: | |
c <<= 1 | |
zeroes += 1 | |
return zeroes | |
def HashcashGood(salt, hashcashes): | |
with DBConnect() as connection: | |
cursor = connection.cursor() | |
cursor.execute('SELECT zeroes, work_count FROM salts WHERE salt=?', (salt,)) | |
row = cursor.fetchone() | |
cursor.execute('DELETE FROM salts WHERE salt=?', (salt,)) | |
connection.commit() | |
if not row: | |
return False | |
else: | |
zeroesTarget = row[0] | |
workCount = row[1] | |
hashcashList = set(hashcashes.split(',')) | |
if len(hashcashList) < workCount: | |
return False | |
for hashcash in hashcashList: | |
toHash = salt + hashcash | |
hash = hashlib.sha1(toHash).digest() | |
zeroes = CountLeadingZeroes(hash) | |
if zeroes < zeroesTarget: | |
return False | |
return True | |
def addComment(form): | |
print "Content-Type: text/html; charset=utf-8\n\n" | |
errors = [] | |
page = getField(form, "page") | |
name = getField(form, "name") | |
url = getField(form, "url") | |
comment = getField(form, "comment") | |
preview = getField(form, "preview") | |
salt = getField(form, "salt") | |
hashcash = getField(form, "hashcash") | |
if not page: | |
errors.append("Empty page given for comment.") | |
if not name: | |
errors.append("Please fill in the name field.") | |
name = "" | |
if not comment: | |
errors.append("Please fill in the comments field.") | |
comment = "" | |
if not url: | |
url = "" | |
comment = comment.decode('utf-8') | |
name = name.decode('utf-8') | |
page = page.decode('utf-8') | |
if page and len(page) > 250: | |
errors.append("Page name too long.") | |
if name and len(name) > 250: | |
errors.append("Name too long, please use 250 characters or less.") | |
if url and len(url) > 250: | |
errors.append("URL too long, please use 250 characters or less.") | |
if comment and len(comment) > 65000: | |
errors.append("Comment too long, please use 65000 characters or less.") | |
if url and url.find("http://") != 0 and url.find("mailto:") != 0 and url.find("https://") != 0: | |
errors.append("Invalid web site %s, use something that starts with http://, https://, or mailto:." % url) | |
# Make sure to check the hashcash at the very end. HashcashGood burns the hashcash when it checks it, | |
# so if it runs when there are errors, a failed comment will burn the hashcash, and then subsequent | |
# submissions from browsers that don't trigger a hashcash recomputation (i.e. Safari) will continue | |
# to fail when they should work. | |
if not preview and not errors and not HashcashGood(salt, hashcash): | |
errors.append("Bad hashcash value.") | |
if errors: | |
print "<ul><li>" | |
print "<li>".join(errors) | |
print "</ul>" | |
return | |
date = datetime.datetime.utcnow() | |
hash = md5.new(comment.encode('utf-8')).hexdigest() | |
ip = os.environ["REMOTE_ADDR"] | |
if preview: | |
print Comments.commentsHTMLForComment(page, time.mktime(date.utctimetuple()), name, url, comment, hash, ip, False, False).encode('utf-8') | |
return | |
if ip not in blacklist: | |
db = CommentsDB.connect() | |
cursor = db.cursor() | |
query = "select md5 from comments where name=? and md5=?" | |
cursor.execute(query, (name, hash)) | |
result = cursor.fetchall() | |
if not result: | |
tzoff = time.mktime(time.gmtime(0)) | |
query = "insert into comments (page, date, name, url, comment, md5, ip) values (?, ?, ?, ?, ?, ?, ?)" | |
tuple = (page, time.mktime(date.utctimetuple()) - tzoff, name, url, comment, hash, ip) | |
cursor.execute(query, tuple) | |
db.commit() | |
db.close() | |
redirURL = "/" + page + "#comment-" + hash | |
print "<meta http-equiv=\"Refresh\" content=\"0;URL=%s\">" % redirURL | |
print "Your comment has been accepted." | |
sys.stderr = sys.stdout | |
main() |
This file contains hidden or 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
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
/* SHA-1 implementation in JavaScript | (c) Chris Veness 2002-2010 | www.movable-type.co.uk */ | |
/* - see http://csrc.nist.gov/groups/ST/toolkit/secure_hashing.html */ | |
/* http://csrc.nist.gov/groups/ST/toolkit/examples.html */ | |
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
var Sha1 = {}; // Sha1 namespace | |
/** | |
* Generates SHA-1 hash of string | |
* | |
* @param {String} msg String to be hashed | |
* @param {Boolean} [utf8encode=true] Encode msg as UTF-8 before generating hash | |
* @returns {String} Hash of msg as hex character string | |
*/ | |
Sha1.hash = function(msg, utf8encode) { | |
utf8encode = (typeof utf8encode == 'undefined') ? true : utf8encode; | |
// convert string to UTF-8, as SHA only deals with byte-streams | |
if (utf8encode) msg = Utf8.encode(msg); | |
// constants [§4.2.1] | |
var K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6]; | |
// PREPROCESSING | |
msg += String.fromCharCode(0x80); // add trailing '1' bit (+ 0's padding) to string [§5.1.1] | |
// convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1] | |
var l = msg.length/4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length | |
var N = Math.ceil(l/16); // number of 16-integer-blocks required to hold 'l' ints | |
var M = new Array(N); | |
for (var i=0; i<N; i++) { | |
M[i] = new Array(16); | |
for (var j=0; j<16; j++) { // encode 4 chars per integer, big-endian encoding | |
M[i][j] = (msg.charCodeAt(i*64+j*4)<<24) | (msg.charCodeAt(i*64+j*4+1)<<16) | | |
(msg.charCodeAt(i*64+j*4+2)<<8) | (msg.charCodeAt(i*64+j*4+3)); | |
} // note running off the end of msg is ok 'cos bitwise ops on NaN return 0 | |
} | |
// add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1] | |
// note: most significant word would be (len-1)*8 >>> 32, but since JS converts | |
// bitwise-op args to 32 bits, we need to simulate this by arithmetic operators | |
M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32); M[N-1][14] = Math.floor(M[N-1][14]) | |
M[N-1][15] = ((msg.length-1)*8) & 0xffffffff; | |
// set initial hash value [§5.3.1] | |
var H0 = 0x67452301; | |
var H1 = 0xefcdab89; | |
var H2 = 0x98badcfe; | |
var H3 = 0x10325476; | |
var H4 = 0xc3d2e1f0; | |
// HASH COMPUTATION [§6.1.2] | |
var W = new Array(80); var a, b, c, d, e; | |
for (var i=0; i<N; i++) { | |
// 1 - prepare message schedule 'W' | |
for (var t=0; t<16; t++) W[t] = M[i][t]; | |
for (var t=16; t<80; t++) W[t] = Sha1.ROTL(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1); | |
// 2 - initialise five working variables a, b, c, d, e with previous hash value | |
a = H0; b = H1; c = H2; d = H3; e = H4; | |
// 3 - main loop | |
for (var t=0; t<80; t++) { | |
var s = Math.floor(t/20); // seq for blocks of 'f' functions and 'K' constants | |
var T = (Sha1.ROTL(a,5) + Sha1.f(s,b,c,d) + e + K[s] + W[t]) & 0xffffffff; | |
e = d; | |
d = c; | |
c = Sha1.ROTL(b, 30); | |
b = a; | |
a = T; | |
} | |
// 4 - compute the new intermediate hash value | |
H0 = (H0+a) & 0xffffffff; // note 'addition modulo 2^32' | |
H1 = (H1+b) & 0xffffffff; | |
H2 = (H2+c) & 0xffffffff; | |
H3 = (H3+d) & 0xffffffff; | |
H4 = (H4+e) & 0xffffffff; | |
} | |
return Sha1.toHexStr(H0) + Sha1.toHexStr(H1) + | |
Sha1.toHexStr(H2) + Sha1.toHexStr(H3) + Sha1.toHexStr(H4); | |
} | |
// | |
// function 'f' [§4.1.1] | |
// | |
Sha1.f = function(s, x, y, z) { | |
switch (s) { | |
case 0: return (x & y) ^ (~x & z); // Ch() | |
case 1: return x ^ y ^ z; // Parity() | |
case 2: return (x & y) ^ (x & z) ^ (y & z); // Maj() | |
case 3: return x ^ y ^ z; // Parity() | |
} | |
} | |
// | |
// rotate left (circular left shift) value x by n positions [§3.2.5] | |
// | |
Sha1.ROTL = function(x, n) { | |
return (x<<n) | (x>>>(32-n)); | |
} | |
// | |
// hexadecimal representation of a number | |
// (note toString(16) is implementation-dependant, and | |
// in IE returns signed numbers when used on full words) | |
// | |
Sha1.toHexStr = function(n) { | |
var s="", v; | |
for (var i=7; i>=0; i--) { v = (n>>>(i*4)) & 0xf; s += v.toString(16); } | |
return s; | |
} | |
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
/* Utf8 class: encode / decode between multi-byte Unicode characters and UTF-8 multiple */ | |
/* single-byte character encoding (c) Chris Veness 2002-2010 */ | |
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
var Utf8 = {}; // Utf8 namespace | |
/** | |
* Encode multi-byte Unicode string into utf-8 multiple single-byte characters | |
* (BMP / basic multilingual plane only) | |
* | |
* Chars in range U+0080 - U+07FF are encoded in 2 chars, U+0800 - U+FFFF in 3 chars | |
* | |
* @param {String} strUni Unicode string to be encoded as UTF-8 | |
* @returns {String} encoded string | |
*/ | |
Utf8.encode = function(strUni) { | |
// use regular expressions & String.replace callback function for better efficiency | |
// than procedural approaches | |
var strUtf = strUni.replace( | |
/[\u0080-\u07ff]/g, // U+0080 - U+07FF => 2 bytes 110yyyyy, 10zzzzzz | |
function(c) { | |
var cc = c.charCodeAt(0); | |
return String.fromCharCode(0xc0 | cc>>6, 0x80 | cc&0x3f); } | |
); | |
strUtf = strUtf.replace( | |
/[\u0800-\uffff]/g, // U+0800 - U+FFFF => 3 bytes 1110xxxx, 10yyyyyy, 10zzzzzz | |
function(c) { | |
var cc = c.charCodeAt(0); | |
return String.fromCharCode(0xe0 | cc>>12, 0x80 | cc>>6&0x3F, 0x80 | cc&0x3f); } | |
); | |
return strUtf; | |
} | |
/** | |
* Decode utf-8 encoded string back into multi-byte Unicode characters | |
* | |
* @param {String} strUtf UTF-8 string to be decoded back to Unicode | |
* @returns {String} decoded string | |
*/ | |
Utf8.decode = function(strUtf) { | |
// note: decode 3-byte chars first as decoded 2-byte strings could appear to be 3-byte char! | |
var strUni = strUtf.replace( | |
/[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g, // 3-byte chars | |
function(c) { // (note parentheses for precence) | |
var cc = ((c.charCodeAt(0)&0x0f)<<12) | ((c.charCodeAt(1)&0x3f)<<6) | ( c.charCodeAt(2)&0x3f); | |
return String.fromCharCode(cc); } | |
); | |
strUni = strUni.replace( | |
/[\u00c0-\u00df][\u0080-\u00bf]/g, // 2-byte chars | |
function(c) { // (note parentheses for precence) | |
var cc = (c.charCodeAt(0)&0x1f)<<6 | c.charCodeAt(1)&0x3f; | |
return String.fromCharCode(cc); } | |
); | |
return strUni; | |
} | |
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
var leadingZeroesTable = { | |
"0" : 4, | |
"1" : 3, | |
"2" : 2, | |
"3" : 2, | |
"4" : 1, | |
"5" : 1, | |
"6" : 1, | |
"7" : 1, | |
"8" : 0, | |
"9" : 0, | |
"a" : 0, | |
"b" : 0, | |
"c" : 0, | |
"d" : 0, | |
"e" : 0, | |
"f" : 0 | |
}; | |
var incrementTable = { | |
"0" : "1", | |
"1" : "2", | |
"2" : "3", | |
"3" : "4", | |
"4" : "5", | |
"5" : "6", | |
"6" : "7", | |
"7" : "8", | |
"8" : "9", | |
"9" : "a", | |
"a" : "b", | |
"b" : "c", | |
"c" : "d", | |
"d" : "e", | |
"e" : "f" | |
} | |
function CountLeadingZeroes(s) | |
{ | |
var zeroes = 0; | |
var i = 0; | |
while(i < s.length) | |
{ | |
var c = s.charAt(i); | |
zeroes += leadingZeroesTable[c]; | |
if(c != "0") | |
break; | |
i++; | |
} | |
return zeroes; | |
} | |
function Increment(s) | |
{ | |
if(s.length == 0) | |
return "0"; | |
var c = s.charAt(0); | |
var rest = s.slice(1); | |
if(c == "f") | |
return "0" + Increment(rest); | |
else | |
return incrementTable[c] + rest; | |
} | |
function Hashcash(salt, zeroes) { | |
var buf = ""; | |
return function(iterations) { | |
var hash; | |
while(iterations > 0) | |
{ | |
buf = Increment(buf); | |
hash = Sha1.hash(salt + buf, 0); | |
var lz = CountLeadingZeroes(hash); | |
if(lz >= zeroes) | |
return buf | |
iterations--; | |
} | |
return false; | |
}; | |
} | |
function HashcashBG(salt, zeroes, workCount, progresscb, donecb) { | |
var iterations = 100; | |
var delay = 1; | |
var processor = Hashcash(salt, zeroes); | |
var results = [] | |
var f = function() { | |
var buf = processor(iterations); | |
if(buf) | |
{ | |
results.push(buf); | |
if(results.length >= workCount) | |
{ | |
donecb(results.join(",")); | |
} | |
else | |
{ | |
progresscb(results.length); | |
setTimeout(f, delay); | |
} | |
} | |
else | |
{ | |
setTimeout(f, delay); | |
} | |
}; | |
f(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment