[Fwd: nospam.py]
Steven Armstrong <[email protected]>
| Newsgroups | gmane.comp.web.pyblosxom.user |
|---|---|
| Message-ID | <[email protected]> |
Hi all Attached is a mail that Benjamin Mako Hill sent me with some enhancements for the nospam.py plugin. It makes the captcha much more difficult to interpret for spambots (or whatever those beasts are called). Thought that might be interesting for some of you. cheers Steven ------------------------------------------------------------------------- Take Surveys. Earn Cash. Influence the Future of IT Join SourceForge.net's Techsay panel and you'll get the chance to share your opinions on IT & business topics through brief surveys -- and earn cash http://www.techsay.com/default.php?page=join.php&p=sourceforge&CID=DEVDEV _______________________________________________ pyblosxom-users mailing list pyblosxom-users-5NWGOfrQmneRv+LV9MX5uipxlwaOVQ5f@public.gmane.org https://lists.sourceforge.net/lists/listinfo/pyblosxom-users
nospam.py
(message/rfc822, 22.6 KB)
Return-Path: <mako-YBxt/[email protected]> X-Original-To: [email protected] Delivered-To: [email protected] Received: from volo.yukidoke.org (volo.yukidoke.org [207.210.245.136]) by eurus.c-area.ch (Urban Mail Machine) with ESMTP id EB43522EFC for <[email protected]>; Sat, 23 Sep 2006 19:21:53 +0200 (CEST) Received: from nozomi (localhost [127.0.0.1]) by volo.yukidoke.org (Postfix) with ESMTP id A0ED9405CF for <[email protected]>; Sat, 23 Sep 2006 17:21:49 +0000 (UTC) Received: from mako by nozomi with local (Exim 3.36 #1 (Debian)) id 1GRBCF-00073y-00 for <[email protected]>; Sat, 23 Sep 2006 13:21:47 -0400 Date: Sat, 23 Sep 2006 13:21:47 -0400 From: "Benj. Mako Hill" <[email protected]> To: Steven Armstrong <[email protected]> Subject: nospam.py Message-ID: <20060923172147.GD25606-YBxt/[email protected]> Mime-Version: 1.0 Content-Type: multipart/signed; micalg=pgp-sha1; protocol="application/pgp-signature"; boundary="5G06lTa6Jq83wMTw" Content-Disposition: inline Sender: Mako Hill <mako-YBxt/[email protected]> --5G06lTa6Jq83wMTw Content-Type: multipart/mixed; boundary="Bn2rw/3z4jIqBvZU" Content-Disposition: inline --Bn2rw/3z4jIqBvZU Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Content-Transfer-Encoding: quoted-printable Thanks for your work on the nospam plugin for Pyblosxom! I've been a happy user for a quite a while now. However, my blog has been recieving several *thousand* successful spams a day over the last few days -- all of which have been based on a defeat of nospam.py. It seems a bit broken. :) This morning, I wrote some string generation code and incorporated some different PIL logic from Mediawiki's ConfirmEdit/FancyCaptcha extension which makes a much tougher CAPTCHA. It seems to have fixed the problem. The patch and the patched version is attached to this email. Regards, Mako --=20 Benjamin Mako Hill [email protected] http://mako.cc/ Creativity can be a social contribution, but only in so far as society is free to use the results. --RMS --Bn2rw/3z4jIqBvZU Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="nospam_tougher_captcha.diff" Content-Transfer-Encoding: quoted-printable --- nospam.py.orig 2006-04-14 14:12:17.000000000 -0400 +++ nospam.py 2006-09-23 13:01:05.691442716 -0400 @@ -3,8 +3,8 @@ Based on a idea and ref impl of Jesus Roncero Franco <jesus at roncero.org= >. Implemented as a pyblosxom plugin by Steven Armstrong <sa at c-area.ch>. =20 -Creates a random number, generates an image of it, and stores the number -in the session. Then compares the number the user entered in the comment +Creates a random string, generates an image of it, and stores the string= =20 +in the session. Then compares the string the user entered in the comment form with the one stored in the session. Rejects the comment if they=20 don't match. =20 @@ -38,8 +38,8 @@ =20 Add something like this to your comment-form.html template: <label for=3D"nospam">Secret Number:</label> -<img src=3D"$base_url/nospam.png" alt=3D"Secret Number Image" title=3D"Typ= e this number into the field on the right" /> -<input name=3D"nospam" id=3D"nospam" type=3D"text" value=3D"" maxlength=3D= "5" style=3D"width:5em" /> +<img src=3D"$base_url/nospam.png" alt=3D"Secret Number Image" title=3D"Typ= e this code into the field on the right" /> +<input name=3D"nospam" id=3D"nospam" type=3D"text" value=3D"" maxlength=3D= "10" style=3D"width:10em" /> =20 =20 Dependecies: @@ -61,6 +61,9 @@ import sys import os import random +import string +import math + from Pyblosxom import tools =20 # PIL imports http://www.pythonware.com/products/pil/ @@ -68,6 +71,7 @@ import Image import ImageDraw import ImageOps + import ImageEnhance try: import ImageFont except ImportError: @@ -82,7 +86,7 @@ _bgColor =3D (255,255,255) # White _gridInk =3D (200,200,200) _fontInk =3D (130,130,130) -_fontSize =3D 14 +_fontSize =3D 32=20 # set in cb_start callback _fontPath =3D None =20 @@ -146,10 +150,63 @@ =20 return retval =20 +# this function is (c) Benjamin Mako Hill <[email protected]> +# generate the unqiue string +def _generateString(): + import string + import random + + chars =3D string.ascii_lowercase + string.digits + secret_string =3D "" + + # generate a string that is 5-7 characters long=20 + string_len =3D random.randint(5,7) + + while (len(secret_string) < string_len ): + char =3D chars[random.randint(0,35)] +=09 + # skip a number of potentially confusable characters + if char in ['l', '1', 'i', 'j', 'o', '0', 'u', 'v', 'd', '5', 's',= 'f', 't' ]: + continue + secret_string +=3D char + return secret_string + +# This function is (c) Neil Harris +# Taken from Mediawiki's ConfirmEdit extension's captcha.py +# Modified and incorporated by Benjamin Mako Hill <[email protected]> +# +# Does X-axis wobbly copy, sandwiched between two rotates +def _wobbly_copy(src, wob, col, scale, ang): + x, y =3D src.size + f =3D random.uniform(4*scale, 5*scale) + p =3D random.uniform(0, math.pi*2) + rr =3D ang+random.uniform(-30, 30) # vary, but not too much + int_d =3D Image.new('RGB', src.size, 0) # a black rectangle + rot =3D src.rotate(rr, Image.BILINEAR) + # Do a cheap bounding-box op here to try to limit work below + bbx =3D rot.getbbox() + if bbx =3D=3D None: + print "whoops" + return src + else: + l, t, r, b=3D bbx =20 -# This function is (c) Jesus Roncero Franco <jesus at roncero.org>. -# Modified to support old and new PIL versions by Steven Armstrong. -def _generateImage(number): + # and only do lines with content on + for i in range(t, b+1): + # Drop a scan line in + xoff =3D int(math.sin(p+(i*f/y))*wob) + xoff +=3D int(random.uniform(-wob*0.5, wob*0.5)) + int_d.paste(rot.crop((0, i, x, i+1)), (xoff, i)) + + # try to stop blurring from building up + int_d =3D int_d.rotate(-rr, Image.BILINEAR) + enh =3D ImageEnhance.Sharpness(int_d) + return enh.enhance(2) + +# This function is (c) Neil Harris +# Taken from Mediawiki's ConfirmEdit extension's captcha.py +# Modified and incorporated by Benjamin Mako Hill <[email protected]> +def _generateImage(text): try: # recent PIL version with support for truetype fonts font =3D ImageFont.truetype(_fontPath, _fontSize) @@ -157,48 +214,52 @@ # old PIL version, fallback to pil fonts font =3D ImageFont.load(_fontPath) =20 - img =3D Image.new("RGB", _imageSize, _bgColor) - draw =3D ImageDraw.Draw(img) - =20 - xsize, ysize =3D img.size - - # Do we want the grid start at 0,0 or want some offset? - x, y =3D 0,0 - =20 - while x <=3D xsize: - try: - # recent PIL version - draw.line(((x, 0), (x, ysize)), fill=3D_gridInk) - except TypeError: - # old PIL version - draw.setink(_gridInk) - draw.line(((x, 0), (x, ysize))) - x =3D x + _xstep=20 - while y <=3D ysize: - try: - draw.line(((0, y), (xsize, y)), fill=3D_gridInk) - except TypeError: - draw.setink(_gridInk) - draw.line(((0, y), (xsize, y))) - y =3D y + _ystep=20 - =20 - try: - draw.text((10, 2), number, font=3Dfont, fill=3D_fontInk) - except TypeError: - draw.setink(_fontInk) - draw.text((10, 2), number, font=3Dfont) + """Generate a captcha image""" + # white text on a black background + bgcolor =3D 0x0 + fgcolor =3D 0xffffff + + # determine dimensions of the text + dim =3D font.getsize(text) + + # create a new image significantly larger that the text + edge =3D max(dim[0], dim[1]) + 2*min(dim[0], dim[1]) + + im =3D Image.new('RGB', (edge, edge), bgcolor) + d =3D ImageDraw.Draw(im) + x, y =3D im.size + # add the text to the image + d.text((x/2-dim[0]/2, y/2-dim[1]/2), text, font=3Dfont, fill=3Dfgcolor) + k =3D 3 + wob =3D 0.20*dim[1]/k + rot =3D 45 + # Apply lots of small stirring operations, rather than a few large ones + # in order to get some uniformity of treatment, whilst + # maintaining randomness + for i in range(k): + im =3D _wobbly_copy(im, wob, bgcolor, i*2+3, rot+0) + im =3D _wobbly_copy(im, wob, bgcolor, i*2+1, rot+45) + im =3D _wobbly_copy(im, wob, bgcolor, i*2+2, rot+90) + rot +=3D 30 + + # now get the bounding box of the nonzero parts of the image + bbox =3D im.getbbox() + bord =3D min(dim[0], dim[1])/4 # a bit of a border + im =3D im.crop((bbox[0]-bord, bbox[1]-bord, bbox[2]+bord, bbox[3]+bord= )) + # and turn into black on white + im =3D ImageOps.invert(im) =20 - return img + return(im) =20 =20 def _writeImage(request): - number =3D str(random.randrange(1,99999,1)) + secret_string =3D _generateString() =20 session =3D request.getSession() - session["nospam"] =3D number + session["nospam"] =3D secret_string session.save() =20 - image =3D _generateImage(number) + image =3D _generateImage(secret_string) =20 response =3D request.getResponse() response.addHeader('Content-Type', 'image/png') @@ -259,7 +320,7 @@ =20 def cb_comment_reject(args): """ - Checks if the the nospam number of the incomming request=20 + Checks if the the nospam code of the incoming request=20 matches the one stored in the session. Creates a template variable $cmt_nospam_error with a=20 error message if it didn't. @@ -285,11 +346,11 @@ allow_trackback =3D config.get('nospam_allow_trackback', 0) =20 try: - nospam =3D int(form["nospam"].value) - sess_nospam =3D int(session["nospam"]) + nospam =3D form["nospam"].value + sess_nospam =3D session["nospam"] except: - nospam =3D 0 - sess_nospam =3D 1 + nospam =3D "0" + sess_nospam =3D "1" =20 if allow_trackback: comment =3D args['comment'] @@ -300,7 +361,7 @@ =20 if nospam !=3D sess_nospam: _remember_comment(request) - data["cmt_nospam_error"] =3D "Secret number did not match." + data["cmt_nospam_error"] =3D "Secret code did not match." return True else: _forget_comment(request) --Bn2rw/3z4jIqBvZU Content-Type: text/x-python; charset=us-ascii Content-Disposition: attachment; filename="nospam.py" Content-Transfer-Encoding: quoted-printable """ Human verification system for the comments plugin. Based on a idea and ref impl of Jesus Roncero Franco <jesus at roncero.org>. Implemented as a pyblosxom plugin by Steven Armstrong <sa at c-area.ch>. Creates a random string, generates an image of it, and stores the string=20 in the session. Then compares the string the user entered in the comment form with the one stored in the session. Rejects the comment if they=20 don't match. If you make any changes to this plugin, please send a patch to=20 <sa+pyblosxom at c-area dot ch> so I can incorporate them. Thanks! Thanks to Ludger Humbert for pointing out issues with different PIL version= s. Thanks to Lance Levsen for adding the allow_trackback bypass and fixing som= e bugs. To install: 1) Put nospam.py in your plugin directory. 2) In config.py add nospam to py['load_plugins'] 3) Add the following variables to config.py: py['nospam_font'] =3D '/path/to/truetype/font.ttf' # required, no defau= lt py['nospam_extension'] =3D '/nospam.png' # optional, this is the default py['nospam_allow_trackback'] =3D 1 # optional to allow trackbacks to pa= ss # through when using the trackback # plugin from Ted Leung =20 Note: If you get an error about problems with TrueType fonts, it's likely that you are using an older PIL version that can't handle them. In this case you'll have to use a pil font from [1] instead and configure c= onfig.py as: py['nospam_font'] =3D '/path/to/pilfonts/lubB10.pil' [1] http://effbot.org/pil/pilfonts.zip Add something like this to your comment-form.html template: <label for=3D"nospam">Secret Number:</label> <img src=3D"$base_url/nospam.png" alt=3D"Secret Number Image" title=3D"Type= this code into the field on the right" /> <input name=3D"nospam" id=3D"nospam" type=3D"text" value=3D"" maxlength=3D"= 10" style=3D"width:10em" /> Dependecies: - My compatibility plugin if you're not using pyblosxom 1.2+. - My session plugin. - Python imaging library from http://www.pythonware.com/products/pil/ $Id: nospam.py,v 1.6 2006/04/14 18:12:17 sar Exp $ """ __author__ =3D "Steven Armstrong <sa at c-area dot ch>" __version__ =3D "$Revision: 1.6 $ $Date: 2006/04/14 18:12:17 $" __url__ =3D "http://www.c-area.ch/code/" __description__ =3D "Human verification system for the comments plugin" __license__ =3D "GPL 2+" # Python imports import sys import os import random import string import math =66rom Pyblosxom import tools # PIL imports http://www.pythonware.com/products/pil/ try: # use try/except so verify_installation can guide the user how to setu= p PIL import Image import ImageDraw import ImageOps import ImageEnhance try: import ImageFont except ImportError: import PIL.ImageFont as ImageFont except ImportError: pass # parameters _xstep =3D 5=20 _ystep =3D 5=20 _imageSize =3D (61,21) _bgColor =3D (255,255,255) # White _gridInk =3D (200,200,200) _fontInk =3D (130,130,130) _fontSize =3D 32=20 # set in cb_start callback _fontPath =3D None # the names of the fields used in the comment form _form_fields =3D ["title", "author", "email", "url", "body"] def verify_installation(request): config =3D request.getConfiguration() retval =3D 1 from Pyblosxom import pyblosxom version =3D pyblosxom.VERSION if version < 1.2: try: import compatibility except ImportError: print "You're running Pyblosxom %.1f and will need " % version print "the 'compatibility.py' plugin to use this plugin." retval =3D 0 try: import session except ImportError: print "Missing required plugin 'session.py'." retval =3D 0 old_pil =3D False no_pil =3D False try: import ImageFont except ImportError: old_pil =3D True try: import PIL.ImageFont except ImportError: no_pil =3D True if not config.has_key('nospam_font'): print "Missing required property: 'nospam_font'" print "This must be the absolute path to a truetype font." retval =3D 0 if no_pil: print "Python imaging library not found." print "Get and install it from http://www.pythonware.com/products/p= il/" retval =3D 0 if old_pil: print "You seem to be using an old PIL (Python imaging library) ver= sion." print "You must get a pil font from http://effbot.org/pil/pilfonts.= zip" print "and point the 'nospam_font' config property to one of them." if not config.has_key('nospam_extension'): print "Missing optional property: 'nospam_extension'" print "Using the default of '/nospam.png'" if not config.has_key('nospam_allow_trackback'): print "Missing optional property: 'nospam_allow_trackback'" print "Using the default of 0" =20 return retval # this function is (c) Benjamin Mako Hill <[email protected]> # generate the unqiue string def _generateString(): import string import random chars =3D string.ascii_lowercase + string.digits secret_string =3D "" # generate a string that is 5-7 characters long=20 string_len =3D random.randint(5,7) while (len(secret_string) < string_len ): char =3D chars[random.randint(0,35)] =09 # skip a number of potentially confusable characters if char in ['l', '1', 'i', 'j', 'o', '0', 'u', 'v', 'd', '5', 's', = 'f', 't' ]: continue secret_string +=3D char return secret_string # This function is (c) Neil Harris # Taken from Mediawiki's ConfirmEdit extension's captcha.py # Modified and incorporated by Benjamin Mako Hill <[email protected]> # # Does X-axis wobbly copy, sandwiched between two rotates def _wobbly_copy(src, wob, col, scale, ang): x, y =3D src.size f =3D random.uniform(4*scale, 5*scale) p =3D random.uniform(0, math.pi*2) rr =3D ang+random.uniform(-30, 30) # vary, but not too much int_d =3D Image.new('RGB', src.size, 0) # a black rectangle rot =3D src.rotate(rr, Image.BILINEAR) # Do a cheap bounding-box op here to try to limit work below bbx =3D rot.getbbox() if bbx =3D=3D None: print "whoops" return src else: l, t, r, b=3D bbx # and only do lines with content on for i in range(t, b+1): # Drop a scan line in xoff =3D int(math.sin(p+(i*f/y))*wob) xoff +=3D int(random.uniform(-wob*0.5, wob*0.5)) int_d.paste(rot.crop((0, i, x, i+1)), (xoff, i)) # try to stop blurring from building up int_d =3D int_d.rotate(-rr, Image.BILINEAR) enh =3D ImageEnhance.Sharpness(int_d) return enh.enhance(2) # This function is (c) Neil Harris # Taken from Mediawiki's ConfirmEdit extension's captcha.py # Modified and incorporated by Benjamin Mako Hill <[email protected]> def _generateImage(text): try: # recent PIL version with support for truetype fonts font =3D ImageFont.truetype(_fontPath, _fontSize) except AttributeError: # old PIL version, fallback to pil fonts font =3D ImageFont.load(_fontPath) """Generate a captcha image""" # white text on a black background bgcolor =3D 0x0 fgcolor =3D 0xffffff # determine dimensions of the text dim =3D font.getsize(text) # create a new image significantly larger that the text edge =3D max(dim[0], dim[1]) + 2*min(dim[0], dim[1]) im =3D Image.new('RGB', (edge, edge), bgcolor) d =3D ImageDraw.Draw(im) x, y =3D im.size # add the text to the image d.text((x/2-dim[0]/2, y/2-dim[1]/2), text, font=3Dfont, fill=3Dfgcolor) k =3D 3 wob =3D 0.20*dim[1]/k rot =3D 45 # Apply lots of small stirring operations, rather than a few large ones # in order to get some uniformity of treatment, whilst # maintaining randomness for i in range(k): im =3D _wobbly_copy(im, wob, bgcolor, i*2+3, rot+0) im =3D _wobbly_copy(im, wob, bgcolor, i*2+1, rot+45) im =3D _wobbly_copy(im, wob, bgcolor, i*2+2, rot+90) rot +=3D 30 # now get the bounding box of the nonzero parts of the image bbox =3D im.getbbox() bord =3D min(dim[0], dim[1])/4 # a bit of a border im =3D im.crop((bbox[0]-bord, bbox[1]-bord, bbox[2]+bord, bbox[3]+bord)) # and turn into black on white im =3D ImageOps.invert(im) return(im) def _writeImage(request): secret_string =3D _generateString() session =3D request.getSession() session["nospam"] =3D secret_string session.save() image =3D _generateImage(secret_string) response =3D request.getResponse() response.addHeader('Content-Type', 'image/png') image.save(response, "PNG") def _remember_comment(request): """ Stores form fields in the data dict so they can be used to=20 refill the form in the template. =20 @param request: pyblosxom request object @type request: L{Pyblosxom.pyblosxom.Request} """ data =3D request.getData() form =3D request.getForm() for key in _form_fields: data["cmt_%s" % key] =3D (form.has_key(key) and [form[key].value] o= r [''])[0] def _forget_comment(request): """ Resets/forgets any stored form field values. =20 @param request: pyblosxom request object @type request: L{Pyblosxom.pyblosxom.Request} """ data =3D request.getData() for key in _form_fields: key =3D "cmt_%s" % key if key in data: del data[key] #****************************** # Callbacks #****************************** def cb_start(args): request =3D args['request'] config =3D request.getConfiguration() global _fontPath _fontPath =3D config.get('nospam_font') def cb_handle(args): request =3D args['request'] http =3D request.getHttp() config =3D request.getConfiguration() ext =3D config.get("nospam_extension", "/nospam.png") if http['PATH_INFO'].endswith( ext ): # write the image to the output stream _writeImage(request) # return True to tell pyblosxom that the request has been taken car= e of return 1 def cb_comment_reject(args): """ Checks if the the nospam code of the incoming request=20 matches the one stored in the session. Creates a template variable $cmt_nospam_error with a=20 error message if it didn't. =20 Also creates the following template variables: $cmt_title, $cmt_author, $cmt_email, $cmt_url, $cmt_body which can be used to populate the form with the values provided by the user. If the config setting nospam_allow_trackback is True,=20 and the comment came in via the trackback plugin the comment is accepte= d. =20 @param args: a dict containing: pyblosxom request, comment dict=20 @type config: C{dict} @return: True if the comment should be rejected, False otherwise @rtype: C{bool} """ request =3D args['request'] session =3D request.getSession() form =3D request.getForm() data =3D request.getData() config =3D request.getConfiguration() allow_trackback =3D config.get('nospam_allow_trackback', 0) =20 try: nospam =3D form["nospam"].value sess_nospam =3D session["nospam"] except: nospam =3D "0" sess_nospam =3D "1" if allow_trackback: comment =3D args['comment'] #log =3D tools.getLogger() #log.info(comment['author']) if comment['author'].count("Trackback") > 0: return False =20 if nospam !=3D sess_nospam: _remember_comment(request) data["cmt_nospam_error"] =3D "Secret code did not match." return True else: _forget_comment(request) if "cmt_nospam_error" in data: del data["cmt_nospam_error"] return False --Bn2rw/3z4jIqBvZU-- --5G06lTa6Jq83wMTw Content-Type: application/pgp-signature; name="signature.asc" Content-Description: Digital signature Content-Disposition: inline -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.3 (GNU/Linux) iD8DBQFFFW0ric1LIWB1WeYRArcOAKDLLnZl+pmyDTc8TAdtsPrSH7ztygCgj/Yj HD/o0DDLgnq+BmzxUN+Jums= =c9DL -----END PGP SIGNATURE----- --5G06lTa6Jq83wMTw--