[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--
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.