| 1 | #--------------------------------------------------------------------- |
|---|
| 2 | # Python module for easy utilization of http://captchas.net |
|---|
| 3 | # |
|---|
| 4 | # For documentation look at http://captchas.net/sample/python/ |
|---|
| 5 | # |
|---|
| 6 | # Written by Sebastian Wilhelmi <seppi@seppi.de> and |
|---|
| 7 | # Felix Holderied <felix@holderied.de> |
|---|
| 8 | # This file is in the public domain. |
|---|
| 9 | # |
|---|
| 10 | # ChangeLog: |
|---|
| 11 | # |
|---|
| 12 | # 2006-09-08: Add new optional parameters alphabet, letters |
|---|
| 13 | # height an width. Add audio_url. |
|---|
| 14 | # |
|---|
| 15 | # 2006-03-01: Only delete the random string from the repository in |
|---|
| 16 | # case of a successful verification. |
|---|
| 17 | # |
|---|
| 18 | # 2006-02-14: Add new image() method returning an HTML/JavaScript |
|---|
| 19 | # snippet providing a fault tolerant service. |
|---|
| 20 | # |
|---|
| 21 | # 2005-06-02: Initial version. |
|---|
| 22 | # |
|---|
| 23 | #--------------------------------------------------------------------- |
|---|
| 24 | |
|---|
| 25 | import os |
|---|
| 26 | import md5 |
|---|
| 27 | import random |
|---|
| 28 | import time |
|---|
| 29 | |
|---|
| 30 | class CaptchasDotNet: |
|---|
| 31 | def __init__ (self, client, secret, |
|---|
| 32 | alphabet = 'abcdefghkmnopqrstuvwxyz', |
|---|
| 33 | letters = 6, |
|---|
| 34 | width = 240, |
|---|
| 35 | height = 80, |
|---|
| 36 | random_repository = '/tmp/captchasnet-random-strings', |
|---|
| 37 | cleanup_time = 3600 |
|---|
| 38 | ): |
|---|
| 39 | self.__client = client |
|---|
| 40 | self.__secret = secret |
|---|
| 41 | self.__alphabet = alphabet |
|---|
| 42 | self.__letters = letters |
|---|
| 43 | self.__width = width |
|---|
| 44 | self.__height = height |
|---|
| 45 | self.__random_repository = random_repository |
|---|
| 46 | self.__cleanup_time = cleanup_time |
|---|
| 47 | self.__time_stamp_file = os.path.join (random_repository, |
|---|
| 48 | '__time_stamp__') |
|---|
| 49 | |
|---|
| 50 | # Return a random string |
|---|
| 51 | def __random_string (self): |
|---|
| 52 | # The random string shall consist of small letters, big letters |
|---|
| 53 | # and digits. |
|---|
| 54 | letters = "abcdefghijklmnopqrstuvwxyz" |
|---|
| 55 | letters += letters.upper () + "0123456789" |
|---|
| 56 | |
|---|
| 57 | # The random starts out empty, then 40 random possible characters |
|---|
| 58 | # are appended. |
|---|
| 59 | random_string = '' |
|---|
| 60 | for i in range (40): |
|---|
| 61 | random_string += random.choice (letters) |
|---|
| 62 | |
|---|
| 63 | # Return the random string. |
|---|
| 64 | return random_string |
|---|
| 65 | |
|---|
| 66 | # Create a new random string and register it. |
|---|
| 67 | def random (self): |
|---|
| 68 | # If the repository directory is does not yet exist, create it. |
|---|
| 69 | if not os.path.isdir (self.__random_repository): |
|---|
| 70 | os.makedirs (self.__random_repository) |
|---|
| 71 | |
|---|
| 72 | # If the time stamp file does not yet exist, create it. |
|---|
| 73 | if not os.path.isfile (self.__time_stamp_file): |
|---|
| 74 | os.close (os.open (self.__time_stamp_file, os.O_CREAT, 0700)) |
|---|
| 75 | |
|---|
| 76 | # Get the current time. |
|---|
| 77 | now = time.time () |
|---|
| 78 | |
|---|
| 79 | # Determine the time, before which to remove random strings. |
|---|
| 80 | cleanup_time = now - self.__cleanup_time |
|---|
| 81 | |
|---|
| 82 | # If the last cleanup is older than specified, cleanup the |
|---|
| 83 | # directory. |
|---|
| 84 | if os.stat (self.__time_stamp_file).st_mtime < cleanup_time: |
|---|
| 85 | os.utime (self.__time_stamp_file, (now, now)) |
|---|
| 86 | for file_name in os.listdir (self.__random_repository): |
|---|
| 87 | file_name = os.path.join (self.__random_repository, file_name) |
|---|
| 88 | if os.stat (file_name).st_mtime < cleanup_time: |
|---|
| 89 | os.unlink (file_name) |
|---|
| 90 | |
|---|
| 91 | # loop until a valid random string has been found and registered. |
|---|
| 92 | while True: |
|---|
| 93 | # generate a new random string. |
|---|
| 94 | random = self.__random_string () |
|---|
| 95 | |
|---|
| 96 | # open a file with the corresponding name in the repository |
|---|
| 97 | # directory in such a way, that the creation fails, when the |
|---|
| 98 | # file already exists. That should be near to impossible with |
|---|
| 99 | # good seeding of the random number generator, but it's better |
|---|
| 100 | # to play safe. |
|---|
| 101 | try: |
|---|
| 102 | os.close (os.open (os.path.join (self.__random_repository, |
|---|
| 103 | random), |
|---|
| 104 | os.O_EXCL | os.O_CREAT, 0700)) |
|---|
| 105 | except EnvironmentError, error: |
|---|
| 106 | # if the file already existed, rerun the loop to try the |
|---|
| 107 | # next string. |
|---|
| 108 | if error.errno == errno.EEXIST: |
|---|
| 109 | continue |
|---|
| 110 | else: |
|---|
| 111 | # other errors will certainly persist for other random |
|---|
| 112 | # strings, so raise the exception. |
|---|
| 113 | raise |
|---|
| 114 | |
|---|
| 115 | # return the successfully registered random string. |
|---|
| 116 | self.__random = random |
|---|
| 117 | return random |
|---|
| 118 | |
|---|
| 119 | def image_url (self, random = None, base = 'http://image.captchas.net/'): |
|---|
| 120 | if not random: |
|---|
| 121 | random = self.__random |
|---|
| 122 | url = base |
|---|
| 123 | url += '?client=%s&random=%s' % (self.__client, random) |
|---|
| 124 | if self.__alphabet != "abcdefghijklmnopqrstuvwxyz": |
|---|
| 125 | url += '&alphabet=%s' % self.__alphabet |
|---|
| 126 | if self.__letters != 6: |
|---|
| 127 | url += '&letters=%s' % self.__letters |
|---|
| 128 | if self.__width != 240: |
|---|
| 129 | url += '&width=%s' % self.__width |
|---|
| 130 | if self.__height != 80: |
|---|
| 131 | url += '&height=%s' % self.__height |
|---|
| 132 | return url |
|---|
| 133 | |
|---|
| 134 | def audio_url (self, random = None, base = 'http://audio.captchas.net/'): |
|---|
| 135 | if not random: |
|---|
| 136 | random = self.__random |
|---|
| 137 | url = base |
|---|
| 138 | url += '?client=%s&random=%s' % (self.__client, random) |
|---|
| 139 | if self.__alphabet != "abcdefghijklmnopqrstuvwxyz": |
|---|
| 140 | url += '&alphabet=%s' % self.__alphabet |
|---|
| 141 | if self.__letters != 6: |
|---|
| 142 | url += '&letters=%s' % self.__letters |
|---|
| 143 | return url |
|---|
| 144 | |
|---|
| 145 | def image (self, random = None, id = 'captchas.net'): |
|---|
| 146 | return ''' |
|---|
| 147 | <a href="http://captchas.net"><img |
|---|
| 148 | style="border: none; vertical-align: bottom" |
|---|
| 149 | id="%s" src="%s" width="%d" height="%d" |
|---|
| 150 | alt="The CAPTCHA image" /></a> |
|---|
| 151 | <script type="text/javascript"> |
|---|
| 152 | <!-- |
|---|
| 153 | function captchas_image_error (image) |
|---|
| 154 | { |
|---|
| 155 | if (!image.timeout) return true; |
|---|
| 156 | image.src = image.src.replace (/^http:\/\/image\.captchas\.net/, |
|---|
| 157 | 'http://image.backup.captchas.net'); |
|---|
| 158 | return captchas_image_loaded (image); |
|---|
| 159 | } |
|---|
| 160 | |
|---|
| 161 | function captchas_image_loaded (image) |
|---|
| 162 | { |
|---|
| 163 | if (!image.timeout) return true; |
|---|
| 164 | window.clearTimeout (image.timeout); |
|---|
| 165 | image.timeout = false; |
|---|
| 166 | return true; |
|---|
| 167 | } |
|---|
| 168 | |
|---|
| 169 | var image = document.getElementById ('%s'); |
|---|
| 170 | image.onerror = function() {return captchas_image_error (image);}; |
|---|
| 171 | image.onload = function() {return captchas_image_loaded (image);}; |
|---|
| 172 | image.timeout |
|---|
| 173 | = window.setTimeout( |
|---|
| 174 | "captchas_image_error (document.getElementById ('%s'))", |
|---|
| 175 | 10000); |
|---|
| 176 | image.src = image.src; |
|---|
| 177 | //--> |
|---|
| 178 | </script>''' % (id, self.image_url (random), self.__width, self.__height, id, id) |
|---|
| 179 | |
|---|
| 180 | def validate (self, random): |
|---|
| 181 | self.__random = random |
|---|
| 182 | |
|---|
| 183 | file_name = os.path.join (self.__random_repository, random) |
|---|
| 184 | |
|---|
| 185 | # Find out, whether the file exists |
|---|
| 186 | result = os.path.isfile (file_name) |
|---|
| 187 | |
|---|
| 188 | # if the file exists, remember it. |
|---|
| 189 | if result: |
|---|
| 190 | self.__random_file = file_name |
|---|
| 191 | |
|---|
| 192 | # the random string was valid, if and only if the |
|---|
| 193 | # corresponding file existed. |
|---|
| 194 | return result |
|---|
| 195 | |
|---|
| 196 | def verify (self, input, random = None): |
|---|
| 197 | if not random: |
|---|
| 198 | random = self.__random |
|---|
| 199 | |
|---|
| 200 | # The format of the password. |
|---|
| 201 | password_alphabet = self.__alphabet |
|---|
| 202 | password_length = self.__letters |
|---|
| 203 | |
|---|
| 204 | # If the user input has the wrong lenght, it can't be correct. |
|---|
| 205 | if len (input) != password_length: |
|---|
| 206 | return False |
|---|
| 207 | |
|---|
| 208 | # Calculate the MD5 digest of the concatenation of secret key and |
|---|
| 209 | # random string. |
|---|
| 210 | encryption_base = self.__secret + random |
|---|
| 211 | if (password_alphabet != "abcdefghijklmnopqrstuvwxyz") or (password_length != 6): |
|---|
| 212 | encryption_base += ":" + password_alphabet + ":" + str(password_length) |
|---|
| 213 | digest = md5.new (encryption_base).digest () |
|---|
| 214 | |
|---|
| 215 | # Compute password |
|---|
| 216 | correct_password = '' |
|---|
| 217 | for pos in range (password_length): |
|---|
| 218 | letter_num = ord (digest[pos]) % len (password_alphabet) |
|---|
| 219 | correct_password += password_alphabet[letter_num] |
|---|
| 220 | |
|---|
| 221 | # Check password |
|---|
| 222 | if input != correct_password: |
|---|
| 223 | return False |
|---|
| 224 | |
|---|
| 225 | # Remove the correspondig random file, if it exists. |
|---|
| 226 | try: |
|---|
| 227 | os.unlink (self.__random_file) |
|---|
| 228 | del self.__random_file |
|---|
| 229 | except: |
|---|
| 230 | pass |
|---|
| 231 | |
|---|
| 232 | # The user input was correct. |
|---|
| 233 | return True |
|---|
| 234 | |
|---|
| 235 | |
|---|