1package Authen::Captcha;
2
3# $Source: /usr/local/cvs/Captcha/pm/Captcha.pm,v $
4# $Revision: 1.23 $
5# $Date: 2003/12/18 04:44:34 $
6# $Author: jmiller $
7# License: GNU General Public License Version 2 (see license.txt)
8
9use 5.00503;
10use strict;
11use GD;
12use String::Random qw(random_regex);
13use Carp;
14# these are used to find default images dir
15use File::Basename;
16use File::Spec;
17
18use vars qw($VERSION);
19
20$VERSION = '1.024';
21
22# get our file name, used to find the default images
23my $default_images_folder;
24{
25	my $this_file = __FILE__;
26	my $this_dir = dirname($this_file);
27	my @this_dirs = File::Spec->splitdir( $this_dir );
28	$default_images_folder = File::Spec->catdir(@this_dirs,'Captcha','images');
29}
30
31my $num_of_soundfile_versions = 10;
32
33# Preloaded methods go here.
34
35sub new
36{
37	my ($this) = shift;
38	my $class = ref($this) || $this;
39	my $self = {};
40	bless( $self, $class );
41
42	my %opts = @_;
43
44	# default character source images
45	my $type = defined($opts{type}) ? $opts{type} : 'image';
46	$self->type($type);
47	my $src_images = (defined($opts{images_folder}) && (-d $opts{images_folder}))
48	                 ? $opts{images_folder} : $default_images_folder;
49	$self->images_folder($src_images);
50
51	my $debug = (defined($opts{debug}) && ($opts{debug} =~ /^\d+$/))
52	            ? $opts{debug} : 0;
53	$self->debug($debug);
54	$self->data_folder($opts{data_folder}) if($opts{data_folder});
55	$self->output_folder($opts{output_folder}) if($opts{output_folder});
56	my $expire = (defined($opts{expire}) && ($opts{expire} =~ /^\d+$/))
57	             ? $opts{expire} : 300;
58	$self->expire($expire);
59	my $width = (defined($opts{width}) && ($opts{width} =~ /^\d+$/))
60	             ? $opts{width} : 25;
61	$self->width($width);
62	my $height = (defined($opts{height}) && ($opts{height} =~ /^\d+$/))
63	             ? $opts{height} : 35;
64	$self->height($height);
65	my $keep_failures = (defined($opts{keep_failures}) && $opts{keep_failures})
66	                    ? 1 : 0;
67	$self->keep_failures($keep_failures);
68
69	# create a random seed if perl version less than 5.004
70	if ($] < 5.005)
71	{	# have to seed rand. using a fairly good seed
72		srand( time() ^ ($$ + ($$ << 15)) );
73	}	# else, we're just going to let perl do it's thing
74
75	return $self;
76}
77
78sub type
79{
80	ref(my $self = shift) or croak "instance variable needed";
81	if (@_)
82	{
83		if ($_[0] =~ /^(jpg|png|gif|image|picture)$/i)
84		{
85			$self->{_type} = 'image';
86		} elsif ($_[0] =~ /^(sound|snd|wav|mp3)$/i) {
87			$self->{_type} = 'sound';
88		}
89		return $self->{_type};
90	} else {
91		return $self->{_type};
92	}
93}
94
95sub debug
96{
97	ref(my $self = shift) or croak "instance variable needed";
98	if (@_)
99	{
100		$self->{_debug} = $_[0];
101		return $self->{_debug};
102	} else {
103		return $self->{_debug};
104	}
105}
106
107sub keep_failures
108{
109	ref(my $self = shift) or croak "instance variable needed";
110	if (@_)
111	{
112		croak "keep_failures must be a zero or one" unless ($_[0] =~ /^[01]$/);
113		$self->{_keep_failures} = $_[0];
114		return $self->{_keep_failures};
115	} else {
116		return $self->{_keep_failures};
117	}
118}
119
120sub expire
121{
122	ref(my $self = shift) or croak "instance variable needed";
123	if (@_)
124	{
125		croak "expire must be a possitive integer" unless ($_[0] =~ /^\d+$/);
126		$self->{_expire} = $_[0];
127		return $self->{_expire};
128	} else {
129		return $self->{_expire};
130	}
131}
132
133sub width
134{
135	ref(my $self = shift) or croak "instance variable needed";
136	if (@_)
137	{
138		croak "width must be a possitive integer" unless ($_[0] =~ /^\d+$/);
139		$self->{_width} = $_[0];
140		return $self->{_width};
141	} else {
142		return $self->{_width};
143	}
144}
145
146sub height
147{
148	ref(my $self = shift) or croak "instance variable needed";
149	if (@_)
150	{
151		croak "height must be a possitive integer" unless ($_[0] =~ /^\d+$/);
152		$self->{_height} = $_[0];
153		return $self->{_height};
154	} else {
155		return $self->{_height};
156	}
157}
158
159sub output_folder
160{
161
162	ref(my $self = shift) or croak "instance variable needed";
163	if (@_)
164	{   # it's a setter
165		$self->{_output_folder} = $_[0];
166		return $self->{_output_folder};
167	} else {
168		return $self->{_output_folder};
169	}
170}
171
172sub images_folder
173{
174   ref(my $self = shift) or croak "instance variable needed";
175   if (@_)
176   {   # it's a setter
177       $self->{_images_folder} = $_[0];
178       return $self->{_images_folder};
179   } else {
180       return $self->{_images_folder};
181   }
182}
183
184sub data_folder
185{
186   ref(my $self = shift) or croak "instance variable needed";
187   if (@_)
188   {   # it's a setter
189       $self->{_data_folder} = $_[0];
190       return $self->{_data_folder};
191   } else {
192       return $self->{_data_folder};
193   }
194}
195
196
197sub check_code
198{
199	ref(my $self = shift) or croak "instance variable needed";
200	my ($code, $token) = @_;
201
202	$code = lc($code);
203
204	warn "$code  $token\n" if($self->debug() >= 2);
205
206	my $current_time = time;
207	# solution was not found in database (well, yet :)
208	my $return_value = -2;
209	my $database_file = File::Spec->catfile($self->data_folder(),"codes.txt");
210
211	# create database file if it doesn't already exist
212	$self->_touch_file($database_file);
213
214	# zeros (0) and ones (1) are not part of the code
215	# they could be confused with (o) and (l), so we swap them in
216	$code =~ tr/01/ol/;
217
218	# pull in current database
219	warn "Open File: $database_file\n" if($self->debug() >= 2);
220	$self->_get_exclusive_lock();
221	open (DATA, "<$database_file")  or die "Can't open File: $database_file\n";
222		my @data=<DATA>;
223	close(DATA);
224	warn "Close File: $database_file\n" if($self->debug() >= 2);
225
226	my $passed=0;
227	# $new_data will hold the part of the database we want to keep and
228	# write back out
229	my $new_data = "";
230	foreach my $line (@data)
231	{
232		$line =~ s/\n//;
233		my ($data_time,$data_token,$data_code) = $line =~ m/(^\d+)::([a-f0-9]{32})::(.*)$/
234			or next;
235
236		my $png_file = File::Spec->catfile($self->output_folder(),$data_token . ".png");
237		if ($data_token eq $token)
238		{
239
240			# the token was found in the database
241			if (($current_time - $data_time) > $self->expire())
242			{
243				 warn "Crypt Found But Expired\n" if($self->debug() >= 2);
244				# the token was found but has expired
245				$return_value = -1;
246			} else {
247				warn "Match Crypt in File Crypt: $token\n" if($self->debug() >= 2);
248			}
249
250			if ( ($data_code eq $code) && ($return_value != -1) )
251			{
252				warn "Match: " . $data_token . " And " . $token . "\n" if($self->debug() >= 2);
253				# solution was correct and was found in database - passed
254				$return_value = 1;
255			}
256
257			if ( $return_value < ($self->keep_failures() ? -1 : -2) )
258			{
259				warn "No Match: " . $data_token . " And " . $token . "\n" if($self->debug() >= 2);
260				# solution was wrong, not expired, and we're keeping failures
261				$new_data .= $line."\n";
262			} else {
263				# remove the found token so it can't be used again
264				warn "Unlink File: " . $png_file . "\n" if($self->debug() >= 2);
265				unlink($png_file) or carp("Can't remove png file [$png_file]\n");
266			}
267
268			if ( $return_value == -2 ) {
269				# incorrect solution
270				$return_value = -3;
271			}
272
273		} elsif (($current_time - $data_time) > $self->expire()) {
274			# removed expired token
275			warn "Removing Expired Crypt File: " . $png_file ."\n" if($self->debug() >= 2);
276			unlink($png_file) or carp("Can't remove png file [$png_file]\n");
277		} else {
278			# token not found or expired, keep it
279			$new_data .= $line."\n";
280		}
281	}
282
283	# update database
284	open(DATA,">$database_file")  or die "Can't open File: $database_file\n";
285		print DATA $new_data;
286	close(DATA);
287	$self->_release_lock();
288
289	return $return_value;
290}
291
292
293sub _open_lock_file {
294	my $self = shift;
295	my $file_name = shift;
296	open(LOCK, ">>$file_name") or die "Error opening lockfile $file_name: $!\n";
297}
298
299sub _get_shared_lock {
300	my $self = shift;
301	my $lock_file_name = File::Spec->catfile($self->data_folder(),"codes.lock");
302	$self->_open_lock_file($lock_file_name);
303
304	# shared lock
305	flock(LOCK, 1) or die "Error locking lockfile in shared mode: $!\n";
306}
307
308sub _get_exclusive_lock {
309	my $self = shift;
310	my $lock_file_name = File::Spec->catfile($self->data_folder(),"codes.lock");
311	$self->_open_lock_file($lock_file_name);
312
313	# exclusive lock
314	flock(LOCK, 2) or die "Error locking lockfile exclusively: $!\n";
315}
316
317sub _release_lock {
318	my $self = shift;
319	flock(LOCK, 8) or die "Error unlocking lockfile: $!\n";
320	close(LOCK);
321}
322
323sub _touch_file
324{
325	ref(my $self = shift) or croak "instance variable needed";
326	my $file = shift;
327	# create database file if it doesn't already exist
328	if (! -e $file)
329	{
330		open (DATA, ">>$file") or die "Can't create File: $file\n";
331		close(DATA);
332	}
333}
334
335sub generate_random_string
336{
337	ref(my $self = shift) or croak "instance variable needed";
338	my $length = shift;
339
340	# generate a new code
341	my $code = "";
342	for(my $i=0; $i < $length; $i++)
343	{
344		my $char;
345		my $list = int(rand 4) +1;
346		if ($list == 1)
347		{ # choose a number 1/4 of the time
348			$char = int(rand 7)+50;
349		} else { # choose a letter 3/4 of the time
350			$char = int(rand 25)+97;
351		}
352		$char = chr($char);
353		$code .= $char;
354	}
355	return $code;
356}
357
358sub _save_code
359{
360	ref(my $self = shift) or croak "instance variable needed";
361	my $code = shift;
362	my $token = shift;
363
364	my $database_file = File::Spec->catfile($self->data_folder(),'codes.txt');
365
366	# set a variable with the current time
367	my $current_time = time;
368
369	# create database file if it doesn't already exist
370	$self->_touch_file($database_file);
371
372	# clean expired codes and images
373	$self->_get_exclusive_lock();
374
375	open (DATA, "<$database_file")  or die "Can't open File: $database_file\n";
376		my @data=<DATA>;
377	close(DATA);
378
379	my $new_data = "";
380	foreach my $line (@data)
381	{
382		$line =~ s/\n//;
383		my ($data_time,$data_token,$data_code) = $line =~ m/(^\d+)::([a-f0-9]{32})::(.*)$/
384			or next;
385		if ( (($current_time - $data_time) > ($self->expire())) ||
386		     ($data_token  eq $token) )
387		{	# remove expired captcha, or a dup
388			my $png_file = File::Spec->catfile($self->output_folder(),$data_token . ".png");
389			unlink($png_file) or carp("Can't remove png file [$png_file]\n");
390		} else {
391			$new_data .= $line."\n";
392		}
393	}
394
395	# save the code to database
396	warn "open File: $database_file\n" if($self->debug() >= 2);
397	open(DATA,">$database_file")  or die "Can't open File: $database_file\n";
398		warn "-->>" . $new_data . "\n" if($self->debug() >= 2);
399		warn "-->>" . $current_time . "::" . $token."::".$code."\n" if($self->debug() >= 2);
400		print DATA $new_data;
401		print DATA $current_time."::".$token."::".$code."\n";
402	close(DATA);
403	$self->_release_lock();
404	warn "Close File: $database_file\n" if($self->debug() >= 2);
405}
406
407sub create_image_file
408{
409	ref(my $self = shift) or croak "instance variable needed";
410	my $code = shift;
411
412	my $length = length($code);
413	my $im_width = $self->width();
414	# create a new image and color
415	my $im = new GD::Image(($im_width * $length),$self->height());
416	my $black = $im->colorAllocate(0,0,0);
417
418	# copy the character images into the code graphic
419	for(my $i=0; $i < $length; $i++)
420	{
421		my $letter = substr($code,$i,1);
422		my $letter_png = File::Spec->catfile($self->images_folder(),$letter . ".png");
423		my $source = new GD::Image($letter_png);
424		$im->copy($source,($i*($self->width()),0,0,0,$self->width(),$self->height()));
425		my $a = int(rand (int(($self->width())/14)))+0;
426		my $b = int(rand (int(($self->height())/12)))+0;
427		my $c = int(rand (int(($self->width())/3)))-(int(($self->width())/5));
428		my $d = int(rand (int(($self->height())/3)))-(int(($self->height())/5));
429		$im->copyResized($source,($i*($self->width()))+$a,$b,0,0,($self->width())+$c,($self->height())+$d,$self->width(),$self->height());
430	}
431
432	# distort the code graphic
433	for(my $i=0; $i<($length*($self->width())*($self->height())/14+200); $i++)
434	{
435		my $a = (int(rand ($length*($self->width())))+0);
436		my $b = (int(rand $self->height())+0);
437		my $c = (int(rand ($length*($self->width())))+0);
438		my $d = (int(rand $self->height())+0);
439		my $index = $im->getPixel($a,$b);
440		if ($i < (($length*($self->width())*($self->height())/14+200)/100))
441		{
442			$im->line($a,$b,$c,$d,$index);
443		} elsif ($i < (($length*($self->width())*($self->height())/14+200)/2)) {
444			$im->setPixel($c,$d,$index);
445		} else {
446			$im->setPixel($c,$d,$black);
447		}
448	}
449
450	# generate a background
451	my $a = int(rand 5)+1;
452	my $background_img = File::Spec->catfile($self->images_folder(),"background" . $a . ".png");
453	my $source = new GD::Image($background_img);
454	my ($background_width,$background_height) = $source->getBounds();
455	my $b = int(rand (int($background_width/13)))+0;
456	my $c = int(rand (int($background_height/7)))+0;
457	my $d = int(rand (int($background_width/13)))+0;
458	my $e = int(rand (int($background_height/7)))+0;
459	my $source2 = new GD::Image(($length*($self->width())),$self->height());
460	$source2->copyResized($source,0,0,$b,$c,($length*($self->width())),$self->height(),$background_width-$b-$d,$background_height-$c-$e);
461
462	# merge the background onto the image
463	$im->copyMerge($source2,0,0,0,0,($length*($self->width())),$self->height(),40);
464
465	# add a border
466	$im->rectangle(0,0,((($length)*($self->width()))-1),(($self->height())-1),$black);
467
468	# save the image to file
469	my $png_data = $im->png;
470
471	return \$png_data;
472}
473
474sub create_sound_file
475{
476	ref(my $self = shift) or croak "instance variable needed";
477	my $code = shift;
478	my $length = length($code);
479
480	my @chars = split('',$code);
481	my $snd_file;
482	local $/; # input record separator. So we can slurp the data.
483	# get a random voice speaking the code
484	foreach my $char (@chars)
485	{
486		my $voice = int(rand $num_of_soundfile_versions) + 1;
487		my $src_name = File::Spec->catfile($self->images_folder(),$voice, $char . ".wav");
488		warn "Open File: $src_name\n" if($self->debug() >= 2);
489		open (FILE,"< $src_name") or die "Can't open File: $src_name\n";
490			flock FILE, 1; # read lock
491			binmode FILE;
492			$snd_file .= <FILE>;
493		close FILE;
494		warn "Close File: $src_name\n" if($self->debug() >= 2);
495	}
496	return \$snd_file;
497}
498
499sub _save_file
500{
501	ref(my $self = shift) or croak "instance variable needed";
502	my $file_ref = shift;
503	my $file_name = shift;
504
505	warn "Open File: $file_name\n" if($self->debug() >= 2);
506	open (FILE,">$file_name") or die "Can't open File: $file_name \n";
507		flock FILE, 2; # write lock
508		binmode FILE;
509		print FILE $$file_ref;
510	close FILE;
511	warn "Close File: $file_name\n" if($self->debug() >= 2);
512}
513
514sub generate_code
515{
516	ref(my $self = shift) or croak "instance variable needed";
517	my $length = shift;
518
519	my $code = $self->generate_random_string($length);
520	my $token = random_regex('[a-f0-9]{32}');
521
522	my ($captcha_data_ref,$output_filename);
523	if ($self->type() eq 'image')
524	{
525		$captcha_data_ref = $self->create_image_file($code);
526		$output_filename = File::Spec->catfile($self->output_folder(),$token . ".png");
527	} elsif ($self->type() eq 'sound') {
528		$captcha_data_ref = $self->create_sound_file($code);
529		$output_filename = File::Spec->catfile($self->output_folder(),$token . ".wav");
530	} else {
531		croak "invalid captcha type [" . $self->type() . "]";
532	}
533
534	$self->_save_file($captcha_data_ref,$output_filename);
535	$self->_save_code($code,$token);
536
537	# return token (token)... or, if they want it, the code as well.
538	return wantarray ? ($token,$code) : $token;
539}
540
541sub version
542{
543   return $VERSION;
544}
545
5461;
547__END__
548# Below is stub documentation for your module. You'd better edit it!
549
550=head1 NAME
551
552Authen::Captcha - Perl extension for creating captcha's to verify the human element in transactions.
553
554=head1 SYNOPSIS
555
556  use Authen::Captcha;
557
558  # create a new object
559  my $captcha = Authen::Captcha->new();
560
561  # set the data_folder. contains flatfile db to maintain state
562  $captcha->data_folder('/some/folder');
563
564  # set directory to hold publicly accessible images
565  $captcha->output_folder('/some/http/folder');
566
567  # Alternitively, any of the methods to set variables may also be
568  # used directly in the constructor
569
570  my $captcha = Authen::Captcha->new(
571    data_folder => '/some/folder',
572    output_folder => '/some/http/folder',
573    );
574
575  # create a captcha. Image filename is "$token.png"
576  my $token = $captcha->generate_code($number_of_characters);
577
578  # check for a valid submitted captcha
579  #   $code is the submitted letter combination guess from the user
580  #   $token is the submitted token from the user (that we gave them)
581  my $results = $captcha->check_code($code,$token);
582  # $results will be one of:
583  #          1 : Passed
584  #          0 : Code not checked (file error)
585  #         -1 : Failed: code expired
586  #         -2 : Failed: invalid code (not in database)
587  #         -3 : Failed: invalid code (code does not match token)
588  ##############
589
590=head1 WARNING
591
592The captcha produced by this module is rather weak compared to other modules available. You are advised to update to L<GD::SecurityImage>, which provides API-compatible interface in L<GD::SecurityImage::AC> module.
593
594=head1 ABSTRACT
595
596Authen::Captcha provides an object oriented interface to captcha file creations.  Captcha stands for Completely Automated Public Turing test to tell Computers and Humans Apart. A Captcha is a program that can generate and grade tests that:
597
598    - most humans can pass.
599    - current computer programs can't pass
600
601The most common form is an image file containing distorted text, which humans are adept at reading, and computers (generally) do a poor job.
602This module currently implements that method. We plan to add other methods,
603such as distorted sound files, and plain text riddles.
604
605=head1 REQUIRES
606
607    GD          (see http://search.cpan.org/~lds/GD-2.11/)
608    Digest::MD5 (standard perl module)
609
610In most common situations, you'll also want to have:
611
612 A web server (untested on windows, but it should work)
613 cgi-bin or mod-perl access
614 Perl: Perl 5.00503 or later must be installed on the web server.
615 GD.pm (with PNG support)
616
617=head1 INSTALLATION
618
619Download the zipped tar file from:
620
621    http://search.cpan.org/search?dist=Authen-Captcha
622
623Unzip the module as follows or use winzip:
624
625    tar -zxvf Authen-Captcha-1.xxx.tar.gz
626
627The module can be installed using the standard Perl procedure:
628
629    perl Makefile.PL
630    make
631    make test
632    make install    # you need to be root
633
634Windows users without a working "make" can get nmake from:
635
636    ftp://ftp.microsoft.com/Softlib/MSLFILES/nmake15.exe
637
638=head1 METHODS
639
640=head2 MAIN METHODS
641
642=over
643
644=item C<$captcha = Authen::Captcha-E<gt>new();>
645
646This creates a new Captcha object.
647Optionally, you can pass in a hash with configuration information.
648See the method descriptions for more detail on what they mean.
649
650=over 2
651
652   data_folder => '/some/folder', # required
653   output_folder => '/some/http/folder', # required
654   expire => 300, # optional. default 300
655   width =>  25, # optional. default 25
656   height => 35, # optional. default 35
657   images_folder => '/some/folder', # optional. default to lib dir
658   keep_failures => 0, # optional, defaults to 0(false)
659   debug => 0, # optional. default 0
660
661=back
662
663=item C<$token = $captcha-E<gt>generate_code( $number_of_characters );>
664
665Creates a captcha. Image filename is "$token.png"
666
667It can also be called in array context to retrieve the string of characters used to generate the captcha (the string the user is expected to respond with). This is useful for debugging.
668ex.
669
670C<($token,$chars) = $captcha-E<gt>generate_code( $number_of_characters );>
671
672=item C<$results = $captcha-E<gt>check_code($code,$token);>
673
674check for a valid submitted captcha
675
676$code is the submitted letter combination guess from the user
677
678$token is the submitted token from the user (that we gave them)
679
680If the $code and $token are correct, the image file and database entry will be removed.
681
682If the $token matches one in the database, and "keep_failures" is false (the default), the image file and database entry will be removed to avoid repeated attempts on the same captcha.
683
684$results will be one of:
685
686    1 : Passed
687    0 : Code not checked (file error)
688   -1 : Failed: code expired
689   -2 : Failed: invalid code (not in database)
690   -3 : Failed: invalid code (code does not match token)
691
692=back
693
694=head2 ACCESSOR METHODS
695
696=over
697
698=item C<$captcha-E<gt>data_folder( '/some/folder' );>
699
700Required. Sets the directory to hold the flatfile database that will be used to store the current non-expired valid captcha tokens.
701Must be writable by the process running the script (usually the web server user, which is usually either "apache" or "http"), but should not be accessible to the end user.
702
703=item C<$captcha-E<gt>output_folder( '/some/folder' );>
704
705Required. Sets the directory to hold the generated Captcha image files. This is usually a web accessible directory so that the user can view the images in here, but it doesn't have to be web accessible (you could be attaching the images to an e-mail for some verification, or some other Captcha implementation).
706Must be writable by the process running the script (usually the web server user, which is usually either "apache" or "http").
707
708=item C<$captcha-E<gt>images_folder( '/some/folder' );>
709
710Optional, and may greatly affect the results... use with caution. Allows you to override the default character graphic png's and backgrounds with your own set of graphics. These are used in the generation of the final captcha image file. The defaults are held in:
711    [lib install dir]/Authen/Captcha/images
712
713=item C<$captcha-E<gt>expire( 300 );>
714
715Optional. Sets the number of seconds this captcha will remain valid. This means that the created captcha's will not remain valid forever, just as long as you want them to be active. Set to an appropriate value for your application. Defaults to 300.
716
717=item C<$captcha-E<gt>width( 25 );>
718
719Optional. Number of pixels high for the character graphics. Defaults to 25.
720
721=item C<$captcha-E<gt>height( 35 );>
722
723Optional. Number of pixels wide for the character graphics. Defaults to 35.
724
725=item C<$captcha-E<gt>keep_failures( [0|1] );>
726
727Optional. Defaults to zero. This option controls whether or not the captcha will remain valid after a failed attempt. By default, we only allow one attempt to solve it. This greatly reduces the possibility that a bot could brute force a correct answer. Change it at your own risk.
728
729=item C<$captcha-E<gt>debug( [0|1|2] );>
730
731Optional.
732Sets the debugging bit. 1 turns it on, 0 turns it off. 2 will print out verbose messages to STDERR.
733
734=back
735
736=head1 TODO
737
738sound file captcha: Incorporating distorted sound file creation.
739
740=head1 SEE ALSO
741
742The Captcha project:
743    http://www.captcha.net/
744
745The origonal perl script this came from:
746    http://www.firstproductions.com/cgi/
747
748=head1 AUTHORS
749
750Seth T. Jackson, E<lt>sjackson@purifieddata.netE<gt>
751
752Josh I. Miller, E<lt>jmiller@purifieddata.netE<gt>
753
754First Productions, Inc. created the cgi-script distributed under the GPL which was used as the basis for this module. Much work has gone into making this more robust, and suitable for other applications, but much of the origonal code remains.
755
756Fixes were reported and contributed by various people, see Changes file for a complete list.
757
758=head1 COPYRIGHT AND LICENSE
759
760Copyright 2003, First Productions, Inc. (FIRSTPRODUCTIONS HUMAN TEST 1.0)
761
762Copyright 2003 by Seth Jackson
763
764Copyright 2012 by Paolo Rossi, Lubomir Rintel, Chris Dunlop, Gert Schepens and Ernesto Hernández-Novich
765
766This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. (see license.txt).
767
768This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
769
770You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
771
772=cut
773