source: CMESS/captcha/trunk/com/zms/captcha/CaptchasDotNet.py @ 318

Revision 318, 8.6 KB checked in by mike, 3 years ago (diff)
  • initial import of com.zms.captcha library
  • two captcha modes are available: captcha.net, static pics
Line 
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
25import os
26import md5
27import random
28import time
29
30class 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&amp;random=%s' % (self.__client, random)
124        if self.__alphabet != "abcdefghijklmnopqrstuvwxyz":
125            url += '&amp;alphabet=%s' % self.__alphabet
126        if self.__letters != 6:
127            url += '&amp;letters=%s' % self.__letters
128        if self.__width != 240:
129            url += '&amp;width=%s' % self.__width
130        if self.__height != 80:
131            url += '&amp;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&amp;random=%s' % (self.__client, random)
139        if self.__alphabet != "abcdefghijklmnopqrstuvwxyz":
140            url += '&amp;alphabet=%s' % self.__alphabet
141        if self.__letters != 6:
142            url += '&amp;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       
Note: See TracBrowser for help on using the repository browser.