File Coverage

blib/lib/WebService/CaptchasDotNet.pm
Criterion Covered Total %
statement 106 110 96.3
branch 35 48 72.9
condition 14 14 100.0
subroutine 18 18 100.0
pod 5 5 100.0
total 178 195 91.2


line stmt bran cond sub pod time code
1             package WebService::CaptchasDotNet;
2              
3 11     11   11993 use 5.006;
  11         66  
  11         636  
4              
5 11     11   69 use strict;
  11         23  
  11         426  
6 11     11   81 use warnings FATAL => qw(all);
  11         20  
  11         604  
7              
8 11     11   65 use Digest::MD5 qw(md5 md5_hex);
  11         18  
  11         940  
9 11     11   78 use File::Spec ();
  11         32  
  11         264  
10 11     11   79 use File::Path qw(mkpath);
  11         29  
  11         689  
11 11     11   59 use File::Find qw(find);
  11         20  
  11         846  
12 11     11   10037 use IO::File ();
  11         149237  
  11         431  
13 11     11   11089 use IO::Dir ();
  11         161129  
  11         19087  
14              
15             our $VERSION = 0.06;
16              
17             our $DEBUG = 0;
18              
19             #---------------------------------------------------------------------
20             # precompute some static variables to help persistent environments
21             # like mod_perl :)
22             #---------------------------------------------------------------------
23              
24             my @letters = 'a'..'z';
25              
26             my @characters;
27              
28             foreach my $char (33 .. 126) {
29             push @characters, chr $char;
30             }
31              
32              
33             #---------------------------------------------------------------------
34             # constructor
35             #---------------------------------------------------------------------
36             sub new {
37              
38 23     23 1 17264 my $class = shift;
39              
40 23         82 my %args = @_;
41              
42 23   100     200 my $self = { _secret => $args{secret},
43             _uid => $args{username},
44             _expire => $args{expire} || 3600,
45             };
46              
47 23         65 bless $self, $class;
48              
49 23         68 $self->_init;
50              
51 23         832 return $self;
52             }
53              
54              
55             #---------------------------------------------------------------------
56             # expire accessor
57             #---------------------------------------------------------------------
58             sub expire {
59              
60 3     3 1 1213 my $self = shift;
61              
62 3 100       12 $self->{_expire} = shift if @_;
63              
64 3         10 return $self->{_expire};
65             }
66              
67              
68             #---------------------------------------------------------------------
69             # verify routine
70             # make sure user input matches the captcha
71             #---------------------------------------------------------------------
72             sub verify {
73              
74 18     18 1 4186 my $self = shift;
75              
76 18         32 my ($input, $random) = @_;
77              
78 18         38 my $secret = $self->{_secret};
79              
80             # basic sanity checking
81              
82 18 100 100     153 unless ($secret && $random && $input && $input =~ m/^[a-z]{6}$/ ) {
      100        
      100        
83 12 50       80 print STDERR join ' ', 'WebService::CaptchasDotNet - ',
84             "insufficient data for verify()\n"
85             if $DEBUG;
86              
87 12         31 return;
88             }
89              
90             # make sure that the random string is sane
91 6         21 my $file = $self->_verify_random_string($random);
92              
93 6 100       18 return unless $file;
94              
95             # now for the computation - this is what
96             # the captcha image should really be
97 4         35 my $decode = substr(md5(join '', $secret, $random), 0, 6);
98              
99 4         7 my $captcha = '';
100              
101 4         16 foreach my $byte (split //, $decode) {
102 24         47 $captcha .= $letters[ord($byte) % 26];
103             }
104              
105 4 100       19 if ($input eq $captcha) {
106              
107             # a random string can only be used once - cleanup
108 2         207 unlink $file;
109              
110 2         10 return 1;
111             }
112              
113 2         8 return;
114             }
115              
116              
117             #---------------------------------------------------------------------
118             # random string generator
119             #---------------------------------------------------------------------
120             sub random {
121              
122 9     9 1 3775 my $self = shift;
123              
124 9         266 my $string = join '', @characters[rand 64, rand 64, rand 64, rand 64,
125             rand 64, rand 64, rand 64, rand 64,
126             rand 64, rand 64, rand 64, rand 64,
127             rand 64, rand 64, rand 64, rand 64,
128             rand 64, rand 64, rand 64, rand 64,
129             rand 64, rand 64, rand 64, rand 64,
130             rand 64, rand 64, rand 64, rand 64,
131             rand 64, rand 64, rand 64, rand 64,
132             rand 64, rand 64, rand 64, rand 64,
133             rand 64, rand 64, rand 64, rand 64,
134             ];
135              
136             # hmph, I can't seem to localize md5_hex() in my tests...
137 9         134 my $random = Digest::MD5->new->add($string)->hexdigest;
138              
139 9         57 my $tempdir = $self->{_tempdir};
140              
141 9         147 my $file = File::Spec->catfile($tempdir, $random);
142              
143 9 100       285 if (-e $file) {
144 1 50       5 print STDERR join ' ', 'WebService::CaptchasDotNet - ',
145             "collision found for '$random'\n"
146             if $DEBUG;
147              
148 1         4 return;
149             }
150              
151 8         67 my $fh = IO::File->new(">$file");
152              
153 8 50       1138 unless ($fh) {
154 0 0       0 print STDERR join ' ', 'WebService::CaptchasDotNet - ',
155             "could not create '$file': $!\n"
156             if $DEBUG;
157              
158 0         0 return;
159             }
160              
161 8         16 undef $fh;
162              
163 8         119 return $random;
164             }
165              
166              
167             #---------------------------------------------------------------------
168             # present a suitable url for html pages
169             #---------------------------------------------------------------------
170             sub url {
171              
172 1     1 1 521 my $self = shift;
173              
174 1         2 my $random = shift;
175              
176 1         2 my $user = $self->{_uid};
177              
178 1         4 return "http://image.captchas.net/?client=$user&random=$random";
179             }
180              
181              
182             #---------------------------------------------------------------------
183             # private initialization routine
184             #---------------------------------------------------------------------
185             sub _init {
186              
187 24     24   1597 my $self = shift;
188              
189             # create a temporary filesystem to store used random strings
190              
191 24         133 my $tmp = File::Spec->catfile(File::Spec->tmpdir,
192             'CaptchasDotNet');
193              
194 24 100       6817 mkpath $tmp unless -d $tmp;
195              
196 24         104 $self->{_tempdir} = $tmp;
197              
198 24         83 $self->_cleanup;
199             }
200              
201              
202             #---------------------------------------------------------------------
203             # check to make sure the random string passed to verify()
204             # is one we recently generated
205             #---------------------------------------------------------------------
206             sub _verify_random_string {
207              
208 13     13   368 my $self = shift;
209              
210 13         22 my $random = shift;
211              
212             # untaint
213 13 100       76 ($random) = $random =~ m!^([0-9a-z-A-Z]{32})$!
214             if $random;
215              
216 13 100       33 unless ($random) {
217              
218 4 50       35 print STDERR join ' ', 'WebService::CaptchasDotNet - ',
219             "unable to verify invalid random string\n"
220             if $DEBUG;
221              
222 4         15 return;
223             }
224              
225 9         117 my $file = File::Spec->catfile($self->{_tempdir}, $random);
226              
227 9 100       278 unless (-e $file) {
228              
229 1 50       3 print STDERR join ' ', 'WebService::CaptchasDotNet - ',
230             "sanity file $file not found\n"
231             if $DEBUG;
232              
233 1         6 return;
234             }
235              
236 8 100       30 if ($self->_time_to_cleanup($file)) {
237              
238 1 50       3 print STDERR join ' ', 'WebService::CaptchasDotNet - ',
239             "sanity file $file too old\n"
240             if $DEBUG;
241              
242 1         128 unlink $file;
243              
244 1         12 return;
245             }
246              
247 7         22 return $file;
248             }
249              
250             sub _cleanup {
251              
252 28     28   6003093 my $self = shift;
253              
254 28         68 my $dir = $self->{_tempdir};
255              
256 28         210 my $dh = IO::Dir->new($dir);
257              
258 28 50       3064 if ($dh) {
259 28 50       91 print STDERR join ' ', 'WebService::CaptchasDotNet - ',
260             "cleaning up stale entries in $dir\n"
261             if $DEBUG;
262              
263 28         113 foreach my $entry ($dh->read) {
264              
265             # untaint
266 68         954 ($entry) = $entry =~ m!^([0-9a-z-A-Z]{32})$!;
267              
268 68 100       220 next unless $entry;
269              
270 6         87 my $file = File::Spec->catfile($dir, $entry);
271              
272 6 100       26 unlink $file if $self->_time_to_cleanup($file);
273             }
274              
275 28         154 return 1;
276             }
277              
278 0 0       0 print STDERR join ' ', 'WebService::CaptchasDotNet - ',
279             "cannot open cache directory $dir - $!\n"
280             if $DEBUG;
281              
282 0         0 return;
283             }
284              
285             sub _time_to_cleanup {
286              
287 17     17   3000610 my $self = shift;
288              
289 17         35 my $file = shift;
290              
291 17         440 my $mtime = (stat $file)[9];
292              
293 17 100 100     174 if ($mtime && $mtime + $self->{_expire} < time) {
294              
295 3 50       20 print STDERR join ' ', 'WebService::CaptchasDotNet - ',
296             "$file created at $mtime ready for cleanup\n"
297             if $DEBUG;
298              
299 3         498 return 1;
300             }
301              
302 14         81 return;
303             }
304              
305             1;
306              
307             __END__