Ticket #4034: spam-filter-captcha-fallback.diff
| File spam-filter-captcha-fallback.diff, 24.9 KB (added by athomas, 3 years ago) |
|---|
-
tracspamfilter/api.py
22 22 import textwrap 23 23 import time 24 24 25 from trac.config import BoolOption, IntOption 25 from trac.config import BoolOption, IntOption, ExtensionOption 26 26 from trac.core import * 27 27 from trac.db import DatabaseManager 28 28 from trac.env import IEnvironmentSetupParticipant … … 61 61 """ 62 62 63 63 64 class IRejectHandler(Interface): 65 """Handle content rejection.""" 66 67 def reject_content(req, reason): 68 """Reject content. `reason` is a human readable message describing why 69 the content was rejected. """ 70 71 64 72 class FilterSystem(Component): 65 73 strategies = ExtensionPoint(IFilterStrategy) 66 74 67 implements(IEnvironmentSetupParticipant, IPermissionRequestor) 75 implements(IEnvironmentSetupParticipant, IPermissionRequestor, 76 IRejectHandler) 68 77 69 78 min_karma = IntOption('spam-filter', 'min_karma', '0', 70 79 """The minimum score required for a submission to be allowed.""") … … 81 90 """Whether content submissions by authenticated users should be trusted 82 91 without checking for potential spam or other abuse.""") 83 92 93 reject_handler = ExtensionOption('spam-filter', 'reject_handler', 94 IRejectHandler, 'FilterSystem', 95 """The handler used to reject content.""") 96 97 # IRejectHandler methods 98 def reject_content(self, req, message): 99 raise RejectContent(message) 100 84 101 # Public methods 85 102 86 103 def test(self, req, author, changes): … … 140 157 msg = ', '.join([r[2] for r in reasons if r[1] < 0]) 141 158 if msg: 142 159 msg = ' (%s)' % msg 143 raise RejectContent('Submission rejected as potential spam%s' % msg) 160 self.reject_handler.reject_content(req, 'Submission rejected as ' 161 'potential spam %s' % msg) 144 162 145 163 def train(self, req, log_id, spam=True): 146 164 environ = {} -
tracspamfilter/captcha/api.py
1 import time 2 from pickle import loads, dumps 3 from trac.core import * 4 from trac.config import * 5 from trac.web.chrome import ITemplateProvider 6 from trac.web.api import IRequestFilter, IRequestHandler 7 from tracspamfilter.api import IRejectHandler 8 9 10 class ICaptchaMethod(Interface): 11 """ A captcha implementation. """ 12 def generate_captcha(req): 13 """ Return a tuple of `(result, html)`, where `result` is the expected 14 response and `html` is a HTML fragment for displaying the captcha 15 challenge. """ 16 17 18 class CaptchaVerification(Component): 19 implements(ITemplateProvider, IRequestHandler, IRejectHandler) 20 21 request_handlers = ExtensionPoint(IRequestHandler) 22 captcha = ExtensionOption('spam-filter', 'captcha', ICaptchaMethod, 23 'ExpressionCaptcha', 24 """ Captcha method to use for verifying humans. """) 25 26 # IRejectHandler methods 27 def reject_content(self, req, message): 28 if not int(req.session.get('captcha_verified', 0)): 29 req.session['reject_reason'] = message 30 req.session['captcha_redirect'] = req.href(req.path_info) 31 req.redirect(req.href.captcha()) 32 33 # IRequestHandler methods 34 def match_request(self, req): 35 return req.path_info == '/captcha' 36 37 def process_request(self, req): 38 if req.method == 'POST': 39 self.env.log.debug('Captcha response: %s (expected %s)' % 40 (req.args['captcha_response'], req.session['captcha_expected'])) 41 if req.args['captcha_response'] == req.session['captcha_expected']: 42 redirect = req.session.get('captcha_redirect', req.href()) 43 del req.session['captcha_redirect'] 44 del req.session['captcha_expected'] 45 req.session['captcha_verified'] = 1 46 req.redirect(redirect) 47 else: 48 req.hdf['error'] = 'Captcha verification failed' 49 else: 50 req.hdf['error'] = req.session.get('reject_reason') 51 result, html = self.captcha.generate_captcha(req) 52 req.hdf['captcha.challenge'] = html 53 req.hdf['captcha.href'] = req.href('captcha') 54 req.session['captcha_expected'] = result 55 req.session.save() 56 return 'verify_captcha.cs', None 57 58 # ITemplateProvider methods 59 def get_templates_dirs(self): 60 """ Return the absolute path of the directory containing the provided 61 ClearSilver templates. """ 62 from pkg_resources import resource_filename 63 return [resource_filename(__name__, 'templates')] 64 65 66 # IRequestFilter methods 67 def pre_process_request(self, req, handler): 68 # TODO Allow restrict_to to specify request paths to allow 69 if not self.trust_authenticated and req.method == 'POST' \ 70 and handler is not self: 71 return Intercept(handler) 72 return handler 73 74 def post_process_request(self, req, template, content_type): 75 return (template, content_type) -
tracspamfilter/captcha/__init__.py
Property changes on: tracspamfilter/captcha/api.py ___________________________________________________________________ Name: svn:eol-style + native
1 from tracspamfilter.captcha.api import * -
tracspamfilter/captcha/expression.py
Property changes on: tracspamfilter/captcha/__init__.py ___________________________________________________________________ Name: svn:eol-style + native
1 import random 2 from trac.core import * 3 from trac.config import * 4 from trac.util.html import html, Markup 5 from tracspamfilter.captcha import ICaptchaMethod 6 7 8 class ExpressionCaptcha(Component): 9 """ Implementation of a captcha in the form of a human readable numeric 10 expression. Initial implementation by sergeych@tancher.com. """ 11 12 implements(ICaptchaMethod) 13 14 terms = IntOption('numeric-captcha', 'terms', 3, 15 """ Number of terms in expression. """) 16 ceiling = IntOption('numeric-captcha', 'ceiling', 10, 17 """ Maximum value of individual terms in expression. """) 18 19 operations = {'*': 'multiplied by', '-': 'minus', '+': 'plus'} 20 numerals = ('zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 21 'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen', 22 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 23 'nineteen' ) 24 tens = ('twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 25 'eighty', 'ninety') 26 27 # ICaptchaMethod methods 28 def generate_captcha(self, req): 29 if self.ceiling > 100: 30 raise TracError('Numeric captcha can not represent numbers > 100') 31 terms = [str(random.randrange(0, self.ceiling)) for _ in xrange(self.terms)] 32 operations = [random.choice(self.operations.keys()) for _ in xrange(self.terms)] 33 expression = sum(zip(terms, operations), ())[:-1] 34 expression = eval(compile(' '.join(expression), 'captcha_eval', 'eval')) 35 human = sum(zip([self.humanise(int(t)) for t in terms], 36 [self.operations[o] for o in operations]), ())[:-1] 37 return (expression, html.blockquote(' '.join(map(str, human)))) 38 39 # Internal methods 40 def humanise(self, value): 41 if value < 20: 42 return self.numerals[value] 43 english = self.tens[value / 10 - 2] 44 if value % 10: 45 english += ' ' + self.numerals[value % 10] 46 return english -
tracspamfilter/captcha/image.py
Property changes on: tracspamfilter/captcha/expression.py ___________________________________________________________________ Name: svn:eol-style + native
1 import os 2 import random 3 import Image 4 import ImageFont 5 import ImageDraw 6 import ImageFilter 7 from StringIO import StringIO 8 from trac.core import * 9 from trac.util.html import html 10 from trac.config import * 11 from trac.web.api import IRequestHandler 12 from tracspamfilter.captcha import ICaptchaMethod 13 14 15 class ImageCaptcha(Component): 16 """ An image captcha courtesy of 17 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440588 """ 18 19 implements(ICaptchaMethod) 20 implements(IRequestHandler) 21 22 fonts = ListOption('image-captcha', 'fonts', 'vera.ttf', 23 doc=""" Set of fonts to choose from. """) 24 font_size = IntOption('image-captcha', 'font_size', 25, 25 "Font size") 26 alphabet = Option('image-captcha', 'alphabet', 'abcdefghkmnopqrstuvwxyz', 27 """ Alphabet to choose captcha challenge from. """) 28 letters = IntOption('image-captcha', 'letters', 6, 29 """ Number of letters to use in challenge. """) 30 31 # IRequestHandler methods 32 def match_request(self, req): 33 return req.path_info == '/captcha/image' 34 35 def process_request(self, req): 36 if 'captcha_expected' not in req.session: 37 # TODO Probably need to render an error image here 38 raise TracError('No Captcha response in session') 39 req.send_response(200) 40 req.send_header('Content-Type', 'image/jpeg') 41 req.end_headers() 42 43 image = StringIO() 44 from pkg_resources import resource_filename 45 font = os.path.join(resource_filename('tracspamfilter', 'fonts'), 46 random.choice(self.fonts)) 47 self.gen_captcha(image, req.session['captcha_expected'], font, 48 self.font_size) 49 req.write(image.getvalue()) 50 51 # ICaptchaMethod methods 52 def generate_captcha(self, req): 53 challenge = ''.join([random.choice(self.alphabet) for _ in xrange(self.letters)]) 54 return challenge, html.blockquote(html.img(src=req.href('/captcha/image'))) 55 56 # Internal methods 57 def gen_captcha(self, file, text, fnt, fnt_sz, fmt='JPEG'): 58 # randomly select the foreground color 59 fgcolor = random.randint(0,0xffff00) 60 # make the background color the opposite of fgcolor 61 bgcolor = fgcolor ^ 0xffffff 62 # create a font object 63 font = ImageFont.truetype(fnt,fnt_sz) 64 # determine dimensions of the text 65 dim = font.getsize(text) 66 # create a new image slightly larger that the text 67 im = Image.new('RGB', (dim[0]+5,dim[1]+5), bgcolor) 68 d = ImageDraw.Draw(im) 69 x, y = im.size 70 r = random.randint 71 # draw 100 random colored boxes on the background 72 for num in range(100): 73 d.rectangle((r(0,x),r(0,y),r(0,x),r(0,y)),fill=r(0,0xffffff)) 74 # add the text to the image 75 d.text((3,3), text, font=font, fill=fgcolor) 76 im = im.filter(ImageFilter.EDGE_ENHANCE_MORE) 77 # save the image to a file 78 im.save(file, format=fmt) -
tracspamfilter/captcha/captchasdotnet.py
Property changes on: tracspamfilter/captcha/image.py ___________________________________________________________________ Name: svn:eol-style + native
1 import os 2 import md5 3 import random 4 import time 5 from trac.core import * 6 from trac.config import * 7 from trac.util.html import html 8 from tracspamfilter.captcha import ICaptchaMethod 9 10 11 class CaptchasDotNetCaptcha(Component): 12 """ Trac adapter for the captcha.net Captcha community site. `client` and 13 `secret` '''must''' be configured in the `captchas.net` section of 14 `trac.ini`. """ 15 implements(ICaptchaMethod) 16 17 client = Option('captchas.net', 'client', doc= 18 """ [http://captchas.net] registered client name ('''required''') """) 19 secret = Option('captchas.net', 'secret', doc= 20 """ [http://captchas.net] registered client secret ('''required''') """) 21 alphabet = Option('captchas.net', 'alphabet', 'abcdefghkmnopqrstuvwxyz', 22 """ Alphabet to choose captcha challenge from. """) 23 letters = IntOption('captchas.net', 'letters', 6, 24 """ Number of letters to use in challenge. """) 25 width = IntOption('captchas.net', 'width', 240, 26 """ Width of captcha. """) 27 height = IntOption('captchas.net', 'height', 80, 28 """ Height of captcha. """) 29 random_repository = Option('captchas.net', 'random_repository', 30 '/tmp/captchasnet-random-strings', 31 """ Local captcha cache directory. """) 32 cleanup_time = IntOption('captchas.net', 'cleanup_time', 3600, 33 """ Delay, in seconds, before old captcha images are removed. """) 34 35 # ICaptchaMethod methods 36 def generate_captcha(self, req): 37 if not self.client or not self.secret: 38 raise TracError('captchas.net plugin not configured (`[captchas.net] client = xxx secret = yyy)') 39 captcha = CaptchasDotNet(self.client, self.secret, self.alphabet, 40 self.letters, self.width, self.height, 41 self.random_repository, self.cleanup_time) 42 challenge = captcha.random() 43 html = html.p('Please respond with the letters in the following image or sound:')( 44 Markup(captchas.image()), 45 html.a('Phonetic spelling', href=captchas.audio_url()) 46 ) 47 return (challenge, html) 48 49 50 #--------------------------------------------------------------------- 51 # Python module for easy utilization of http://captchas.net 52 # 53 # For documentation look at http://captchas.net/sample/python/ 54 # 55 # Written by Sebastian Wilhelmi <seppi@seppi.de> and 56 # Felix Holderied <felix@holderied.de> 57 # This file is in the public domain. 58 # 59 # ChangeLog: 60 # 61 # 2006-09-08: Add new optional parameters alphabet, letters 62 # height an width. Add audio_url. 63 # 64 # 2006-03-01: Only delete the random string from the repository in 65 # case of a successful verification. 66 # 67 # 2006-02-14: Add new image() method returning an HTML/JavaScript 68 # snippet providing a fault tolerant service. 69 # 70 # 2005-06-02: Initial version. 71 # 72 #--------------------------------------------------------------------- 73 74 class CaptchasDotNet: 75 def __init__ (self, client, secret, 76 alphabet = 'abcdefghkmnopqrstuvwxyz', 77 letters = 6, 78 width = 240, 79 height = 80, 80 random_repository = '/tmp/captchasnet-random-strings', 81 cleanup_time = 3600 82 ): 83 self.__client = client 84 self.__secret = secret 85 self.__alphabet = alphabet 86 self.__letters = letters 87 self.__width = width 88 self.__height = height 89 self.__random_repository = random_repository 90 self.__cleanup_time = cleanup_time 91 self.__time_stamp_file = os.path.join (random_repository, 92 '__time_stamp__') 93 94 # Return a random string 95 def __random_string (self): 96 # The random string shall consist of small letters, big letters 97 # and digits. 98 letters = "abcdefghijklmnopqrstuvwxyz" 99 letters += letters.upper () + "0123456789" 100 101 # The random starts out empty, then 40 random possible characters 102 # are appended. 103 random_string = '' 104 for i in range (40): 105 random_string += random.choice (letters) 106 107 # Return the random string. 108 return random_string 109 110 # Create a new random string and register it. 111 def random (self): 112 # If the repository directory is does not yet exist, create it. 113 if not os.path.isdir (self.__random_repository): 114 os.makedirs (self.__random_repository) 115 116 # If the time stamp file does not yet exist, create it. 117 if not os.path.isfile (self.__time_stamp_file): 118 os.close (os.open (self.__time_stamp_file, os.O_CREAT, 0700)) 119 120 # Get the current time. 121 now = time.time () 122 123 # Determine the time, before which to remove random strings. 124 cleanup_time = now - self.__cleanup_time 125 126 # If the last cleanup is older than specified, cleanup the 127 # directory. 128 if os.stat (self.__time_stamp_file).st_mtime < cleanup_time: 129 os.utime (self.__time_stamp_file, (now, now)) 130 for file_name in os.listdir (self.__random_repository): 131 file_name = os.path.join (self.__random_repository, file_name) 132 if os.stat (file_name).st_mtime < cleanup_time: 133 os.unlink (file_name) 134 135 # loop until a valid random string has been found and registered. 136 while True: 137 # generate a new random string. 138 random = self.__random_string () 139 140 # open a file with the corresponding name in the repository 141 # directory in such a way, that the creation fails, when the 142 # file already exists. That should be near to impossible with 143 # good seeding of the random number generator, but it's better 144 # to play safe. 145 try: 146 os.close (os.open (os.path.join (self.__random_repository, 147 random), 148 os.O_EXCL | os.O_CREAT, 0700)) 149 except EnvironmentError, error: 150 # if the file already existed, rerun the loop to try the 151 # next string. 152 if error.errno == errno.EEXIST: 153 continue 154 else: 155 # other errors will certainly persist for other random 156 # strings, so raise the exception. 157 raise 158 159 # return the successfully registered random string. 160 self.__random = random 161 return random 162 163 def image_url (self, random = None, base = 'http://image.captchas.net/'): 164 if not random: 165 random = self.__random 166 url = base 167 url += '?client=%s&random=%s' % (self.__client, random) 168 if self.__alphabet != "abcdefghijklmnopqrstuvwxyz": 169 url += '&alphabet=%s' % self.__alphabet 170 if self.__letters != 6: 171 url += '&letters=%s' % self.__letters 172 if self.__width != 240: 173 url += '&width=%s' % self.__width 174 if self.__height != 80: 175 url += '&height=%s' % self.__height 176 return url 177 178 def audio_url (self, random = None, base = 'http://audio.captchas.net/'): 179 if not random: 180 random = self.__random 181 url = base 182 url += '?client=%s&random=%s' % (self.__client, random) 183 if self.__alphabet != "abcdefghijklmnopqrstuvwxyz": 184 url += '&alphabet=%s' % self.__alphabet 185 if self.__letters != 6: 186 url += '&letters=%s' % self.__letters 187 return url 188 189 def image (self, random = None, id = 'captchas.net'): 190 return ''' 191 <a href="http://captchas.net"><img 192 style="border: none; vertical-align: bottom" 193 id="%s" src="%s" width="%d" height="%d" 194 alt="The CAPTCHA image" /></a> 195 <script type="text/javascript"> 196 <!-- 197 function captchas_image_error (image) 198 { 199 if (!image.timeout) return true; 200 image.src = image.src.replace (/^http:\/\/image\.captchas\.net/, 201 'http://image.backup.captchas.net'); 202 return captchas_image_loaded (image); 203 } 204 205 function captchas_image_loaded (image) 206 { 207 if (!image.timeout) return true; 208 window.clearTimeout (image.timeout); 209 image.timeout = false; 210 return true; 211 } 212 213 var image = document.getElementById ('%s'); 214 image.onerror = function() {return captchas_image_error (image);}; 215 image.onload = function() {return captchas_image_loaded (image);}; 216 image.timeout 217 = window.setTimeout( 218 "captchas_image_error (document.getElementById ('%s'))", 219 10000); 220 image.src = image.src; 221 //--> 222 </script>''' % (id, self.image_url (random), self.__width, self.__height, id, id) 223 224 def validate (self, random): 225 self.__random = random 226 227 file_name = os.path.join (self.__random_repository, random) 228 229 # Find out, whether the file exists 230 result = os.path.isfile (file_name) 231 232 # if the file exists, remember it. 233 if result: 234 self.__random_file = file_name 235 236 # the random string was valid, if and only if the 237 # corresponding file existed. 238 return result 239 240 def verify (self, input, random = None): 241 if not random: 242 random = self.__random 243 244 # The format of the password. 245 password_alphabet = self.__alphabet 246 password_length = self.__letters 247 248 # If the user input has the wrong lenght, it can't be correct. 249 if len (input) != password_length: 250 return False 251 252 # Calculate the MD5 digest of the concatenation of secret key and 253 # random string. 254 encryption_base = self.__secret + random 255 if (password_alphabet != "abcdefghijklmnopqrstuvwxyz") or (password_length != 6): 256 encryption_base += ":" + password_alphabet + ":" + str(password_length) 257 digest = md5.new (encryption_base).digest () 258 259 # Compute password 260 correct_password = '' 261 for pos in range (password_length): 262 letter_num = ord (digest[pos]) % len (password_alphabet) 263 correct_password += password_alphabet[letter_num] 264 265 # Check password 266 if input != correct_password: 267 return False 268 269 # Remove the correspondig random file, if it exists. 270 try: 271 os.unlink (self.__random_file) 272 del self.__random_file 273 except: 274 pass 275 276 # The user input was correct. 277 return True -
tracspamfilter/templates/verify_captcha.cs
Property changes on: tracspamfilter/captcha/captchasdotnet.py ___________________________________________________________________ Name: svn:eol-style + native Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: tracspamfilter/fonts/vera.ttf ___________________________________________________________________ Name: svn:mime-type + application/octet-stream
1 <?cs include "header.cs" ?> 2 <?cs include "macros.cs" ?> 3 4 <?cs if:error ?> 5 <div id="content" class="error"> 6 <h1>Captcha Error</h1> 7 <p class="message"><?cs var:error ?></p> 8 <?cs else ?> 9 <div id="content" class="traccaptcha"> 10 <?cs /if ?> 11 <form method="post" action="<?cs var:captcha.href ?>"> 12 <p> 13 Trac thinks your submission might be Spam. To prove otherwise please provide a response to the following. 14 </p> 15 <p> 16 <?cs var:captcha.challenge ?> 17 </p> 18 Response: <input type="text" name="captcha_response"/> 19 <input type="submit" value="Submit"/> 20 </form> 21 </div> 22 23 <?cs include "footer.cs" ?> -
setup.py
31 31 extras_require = { 32 32 'DNS': ['dnspython>=1.3.5'], 33 33 'SpamBayes': ['spambayes'], 34 'PIL': ['pil'], 34 35 }, 35 36 entry_points = """ 36 37 [trac.plugins] … … 44 45 spamfilter.ip_throttle = tracspamfilter.filters.ip_throttle 45 46 spamfilter.regex = tracspamfilter.filters.regex 46 47 spamfilter.session = tracspamfilter.filters.session 48 spamfilter.captcha = tracspamfilter.captcha.api 49 spamfilter.captcha.image = tracspamfilter.captcha.image[PIL] 50 spamfilter.captcha.expression = tracspamfilter.captcha.expression 51 spamfilter.captcha.captchasdotnet = tracspamfilter.captcha.captchasdotnet 47 52 """, 48 53 test_suite = 'tracspamfilter.tests.suite', 49 54 zip_safe = True
