File Coverage

blib/lib/GD/SecurityImage/AC.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package GD::SecurityImage::AC;
2             # drop-in replacement for Authen::Captcha
3 2     2   12490 use strict;
  2         5  
  2         82  
4 2     2   11 use vars qw($VERSION);
  2         3  
  2         105  
5 2     2   9482 use GD::SecurityImage;
  0            
  0            
6             use Digest::MD5 qw(md5_hex);
7             use File::Spec;
8             use Fcntl qw(:flock); # imports LOCK_NB, LOCK_EX, LOCK_SH, LOCK_UN (among other things)
9             use Symbol; # imports 'gensym'
10            
11             BEGIN {
12             $VERSION = '1.11';
13             @Authen::Captcha::ISA = ('GD::SecurityImage::AC');
14             }
15            
16             sub new {
17             my $class = shift;
18             my %opts = scalar(@_) % 2 ? () : (@_);
19             my $self = {
20             gdsi => {
21             map {$_ => ''} qw[new create particle]
22             },
23             GDSI_CALLED => 0,
24             };
25             bless $self, $class;
26             foreach my $name (qw[keep_failures data_folder output_folder]) {
27             $self->{'_'.$name} = $opts{$name} if $opts{$name};
28             }
29             $self->{_debug} = $opts{debug} if defined $opts{debug};
30             foreach my $p ([expire => 300], [width => 100], [height => 32]) {
31             $self->{"_".$p->[0]} = $opts{$p->[0]} && ($opts{$p->[0]} !~ /[^0-9]/) ? $opts{$p->[0]} : $p->[1];
32             }
33             $self->{_keep_failures} = $opts{keep_failures} ? 1 : 0;
34             srand( time() ^ ($$ + ($$ << 15)) ) if $] < 5.005; # create a random seed if perl version less than 5.005
35             return $self;
36             }
37            
38             sub _lock_ex { shift->_lock(&LOCK_EX); }
39             sub _lock_sh { shift->_lock(&LOCK_SH); }
40             sub _lock_un { shift->_lock(&LOCK_UN); }
41            
42             sub _lock { # Non-blocking locking with a timeout
43             my $self = shift;
44            
45             my ($lock_mode) = @_;
46            
47             my $lock_handle = $self->_lock_handle;
48             my $timeout = 10; # seconds
49             my $count_timer = 10 * $timeout;
50             my $lock_result;
51             while (! ($lock_result = flock ($lock_handle, $lock_mode | &LOCK_NB))) {
52             if (! $count_timer--) {
53             my $package = __PACKAGE__;
54             die("${package}::_lock() - Failed to obtain lock in $timeout seconds: $!");
55             }
56             # sleep for 1/10th of a second before trying again
57             select (undef,undef,undef,0.1);
58             }
59             return;
60             }
61            
62             sub _lock_handle { # returns an open filehandle to use for locking
63             my $self = shift;
64            
65             my $lock_handle = $self->{'_lock_handle'};
66             return $lock_handle if defined ($lock_handle);
67             my $lock_file = $self->_lock_file;
68             $lock_handle = gensym;
69             if (! open ($lock_handle,"+>$lock_file")) {
70             my $package = __PACKAGE__;
71             die("${package}::_lock_handle() - Unable to open '$lock_file' for locking: $!");
72             }
73            
74             $self->{'_lock_handle'} = $lock_handle;
75             return $lock_handle;
76             }
77            
78             sub _lock_file { # Returns the lock file path
79             my $self = shift;
80            
81             my $package = __PACKAGE__;
82             my $lock_file = $self->{_lock_file};
83             return $lock_file if (defined $lock_file);
84             my $data_folder = $self->{_data_folder};
85             unless (defined ($data_folder)) {
86             die("${package}::_lock_file() - 'data_folder' is not set")
87             }
88             unless (-e $data_folder && -d _) {
89             die("${package}::_lock_file() - '$data_folder' either does not exist or is not a directory")
90             }
91             $lock_file = File::Spec->catfile($data_folder,'codes.lck');
92             $self->{_lock_file} = $lock_file;
93             return $lock_file;
94             }
95            
96             sub _untaint { # This doesn't make things safe. It just removes the taint flag. Use wisely.
97             my ($value) = @_;
98             my ($untainted_value) = $value =~ m/^(.*)$/s;
99             return $untainted_value;
100             }
101            
102             sub gdsi {
103             my $self = shift;
104             my %opt = scalar(@_) % 2 ? () : (@_);
105             $self->{gdsi}{'new'} = delete $opt{'new'} if ($opt{'new'} && ref $opt{'new'} && ref $opt{'new'} eq 'HASH' );
106             $self->{gdsi}{create} = delete $opt{create} if ($opt{create} && ref $opt{create} && ref $opt{create} eq 'ARRAY');
107             $self->{gdsi}{particle} = delete $opt{particle} if ($opt{particle} && ref $opt{particle} && ref $opt{particle} eq 'ARRAY');
108             $self->{GDSI_CALLED} = 1;
109             $self;
110             }
111            
112             sub create_image_file {
113             my $self = shift;
114             my $code = shift;
115             my $md5 = shift; # junk
116             my $i = GD::SecurityImage->new($self->{gdsi}{'new'} ? %{$self->{gdsi}{'new'}} : (
117             # defaults
118             width => $self->{_width} < 60 ? 60 : $self->{_width},
119             height => $self->{_height},
120             gd_font => 'giant',
121             lines => 2,
122             send_ctobg => 0,
123             ), rndmax => 1);
124             $i->random($code);
125             $i->create($self->{gdsi}{create}
126             ? @{ $self->{gdsi}{create} }
127             : (normal => 'default', '#6C7186', '#917862')
128             );
129             die "Error loading ttf font for GD: $@" if $i->gdbox_empty;
130             $i->particle(@{ $self->{gdsi}{particle} }) if $self->{gdsi}{particle};
131            
132             my @data = $i->out(force => 'png');
133             return $data[0];
134             }
135            
136             sub database_file {
137             my $self = shift;
138             my $file = File::Spec->catfile($self->{_data_folder},'codes.txt');
139             unless(-e $file) { # create database file if it doesn't already exist
140             local *DATA;
141             open DATA, '>>'.$file or die "Can't create File: $file\n";
142             close DATA;
143             }
144             return $file;
145             }
146            
147             sub database_data {
148             my $self = shift;
149             my $db = $self->database_file;
150             local *DATA;
151             open DATA, '<'.$db or die "Can't open $db for reading: $!\n";
152             my @data = ;
153             close DATA;
154             return @data;
155             }
156            
157             sub _unlink {
158             my $file = shift or return;
159             if (-e $file && !-d _) {
160             return unlink($file);
161             }
162             return 1; # resume on unexistent file
163             }
164            
165             sub check_code {
166             my $self = shift;
167             my $code = shift;
168             my $crypt = shift;
169             my $db = $self->database_file;
170             ($code = lc $code) =~ tr/01/ol/;
171             my $md5 = _untaint(md5_hex($code)); # remove 0-1
172             my $now = time;
173             my $rvalue = 0;
174             my $passed = 0;
175             my $new = ''; # saved entries
176             my $found;
177            
178             # make taint happy
179             local $ENV{'PATH'} = '';
180             local $ENV{'ENV'} = '';
181             local $ENV{'IFS'} = '';
182             local $ENV{'CDPATH'} = '';
183             local $ENV{'BASH_ENV'} = '';
184            
185             $self->_lock_ex;
186            
187             foreach my $line ($self->database_data) {
188             chomp $line;
189             my ($data_time, $data_code) = split /::/, $line;
190             my $png_file = File::Spec->catfile($self->{_output_folder}, _untaint($data_code) . '.png');
191             if ($data_code eq $crypt) { # the crypt was found in the database
192             if (($now - $data_time) > $self->{_expire}) {
193             $rvalue = -1; # the crypt was found but has expired
194             } else {
195             $found = 1;
196             }
197             if ( ($md5 ne $crypt) && ($rvalue != -1) && $self->{_keep_failures}) { # solution was wrong, not expired, and we're keeping failures
198             $new .= $line."\n";
199             } else {
200             _unlink($png_file) or die "Can't remove [$png_file]: $!\n"; # remove the found crypt so it can't be used again
201             }
202             } elsif (($now - $data_time) > $self->{_expire}) {
203             _unlink($png_file) or die "Can't remove [$png_file]: $!\n"; # removed expired crypt
204             } else {
205             $new .= $line."\n"; # crypt not found or expired, keep it
206             }
207             }
208            
209             # update database
210             local *DATA;
211             open DATA, '>'.$db or die "Can't open $db for writing: $!\n";
212             # Turn on autoflush for our output handle. I have seen rare cases where locking fails because of perl buffers without this.
213             my $temp_fh = select(DATA); $| = 1; select($temp_fh);
214             print DATA $new;
215             close DATA;
216            
217             $self->_lock_un;
218            
219             if ($md5 eq $crypt) { # solution was correct
220             if ($found) {
221             $rvalue = 1; # solution was correct and was found in database - passed
222             } elsif (!$rvalue) {
223             $rvalue = -2; # solution was not found in database
224             }
225             } else {
226             $rvalue = -3; # incorrect solution
227             }
228             return $rvalue;
229             }
230            
231             sub generate_code {
232             my $self = shift;
233             my $len = shift;
234             my $code = '';
235             $code .= chr( int(rand 4) == 0 ? (int(rand 7)+50) : (int(rand 25)+97)) for 1..$len;
236             my $md5 = _untaint(md5_hex($code));
237             my $now = time;
238             my $new = "";
239             my $db = $self->database_file;
240            
241             # make taint happy
242             local $ENV{'PATH'} = '';
243             local $ENV{'ENV'} = '';
244             local $ENV{'IFS'} = '';
245             local $ENV{'CDPATH'} = '';
246             local $ENV{'BASH_ENV'} = '';
247            
248             $self->_lock_ex;
249            
250             foreach my $line ($self->database_data) { # clean expired codes and images
251             chomp $line;
252             my ($data_time, $data_code) = split /::/, $line;
253             $data_code =~ m/^([a-fA-F0-9]+)$/;
254             $data_code = $1 or die "Bad session key!";
255             $data_time =~ m/^([0-9]+)$/s;
256             $data_time = $1 or die "Bad timeout data!";
257             if (($now - $data_time) > $self->{_expire} || $data_code eq $md5) { # remove expired captcha, or a dup
258             my $png_file = File::Spec->catfile($self->{_output_folder}, _untaint($data_code) . ".png");
259             _unlink($png_file) or die "Can't remove png file [$png_file]\n";
260             } else {
261             $new .= $line."\n";
262             }
263             }
264            
265             # first, test if we can open all files
266             my $file = File::Spec->catfile($self->{_output_folder},$md5 . '.png');
267             local *DATA;
268             local *FILE;
269             open FILE, '>'.$file or die "Can't open $file for writing: $!\n";
270             open DATA, '>'.$db or die "Can't open $db for writing: $!\n";
271            
272             # Turn on autoflush for our output handles. I have seen rare cases where locking fails because of perl buffers without this.
273             my $temp_fh = select(DATA); $| = 1; select(FILE); $| = 1; select($temp_fh);
274            
275             # save image data
276             binmode FILE;
277             print FILE $self->create_image_file($code, $md5);
278             close FILE;
279            
280             # save the code to database
281             print DATA $new, $now,"::",$md5,"\n";
282             close DATA;
283            
284             $self->_lock_un;
285            
286             return wantarray ? ($md5, $code) : $md5;
287             }
288            
289             sub output_folder { my ($self, $val) = @_; $self->{"_output_folder"} = $val if defined $val; return $self->{"_output_folder"}; }
290             sub images_folder { my ($self, $val) = @_; $self->{"_images_folder"} = $val if defined $val; return $self->{"_images_folder"}; }
291             sub data_folder { my ($self, $val) = @_; $self->{"_data_folder"} = $val if defined $val; return $self->{"_data_folder"}; }
292             sub debug { my ($self, $val) = @_; $self->{"_debug"} = $val if defined $val; return $self->{"_debug"}; }
293             sub expire { my ($self, $val) = @_; $self->{"_expire"} = $val if $val and $val >= 0; return $self->{"_expire"}; }
294             sub width { my ($self, $val) = @_; $self->{"_width"} = $val if $val and $val >= 0; return $self->{"_width"}; }
295             sub height { my ($self, $val) = @_; $self->{"_height"} = $val if $val and $val >= 0; return $self->{"_height"}; }
296             sub version { return $VERSION; }
297             sub keep_failures { my ($self, $val) = @_; $self->{"_keep_failures"} = $val ? 1 : 0 if defined $val; return $self->{"_keep_failures"}; }
298             sub create_sound_file { return 'there is no such thing!'; }
299             sub type { return 'image' }
300            
301             1;