File Coverage

blib/lib/Apache/Htpasswd.pm
Criterion Covered Total %
statement 225 259 86.8
branch 70 94 74.4
condition 9 21 42.8
subroutine 22 23 95.6
pod 11 12 91.6
total 337 409 82.4


line stmt bran cond sub pod time code
1             package Apache::Htpasswd;
2              
3 1     1   754 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  1         2  
  1         88  
4 1     1   4 use warnings;
  1         2  
  1         26  
5 1     1   4 use strict; # Restrict unsafe variables, references, barewords
  1         5  
  1         29  
6 1     1   4 use Carp;
  1         2  
  1         90  
7              
8 1     1   812 use POSIX qw ( SEEK_SET SEEK_END );
  1         8193  
  1         9  
9 1     1   1341 use Fcntl qw ( LOCK_EX LOCK_UN );
  1         3  
  1         3307  
10              
11             @ISA = qw(Exporter);
12              
13             @EXPORT = qw();
14              
15             @EXPORT_OK =
16             qw(htpasswd htDelete fetchPass fetchInfo writeInfo htCheckPassword error Version);
17              
18             %EXPORT_TAGS = ( all => [@EXPORT_OK] );
19              
20             $VERSION = '1.9';
21              
22             sub Version {
23 0     0 1 0 return $VERSION;
24             }
25              
26             #-----------------------------------------------------------#
27             # Public Methods
28             #-----------------------------------------------------------#
29              
30             sub new {
31 3     3 1 405 my $proto = shift;
32 3         6 my $args = shift;
33 3         3 my $passwdFile;
34              
35 3 100       10 if ( ref $args eq 'HASH' ) {
36 2         4 $passwdFile = $args->{'passwdFile'};
37             }
38             else {
39 1         2 $passwdFile = $args;
40             }
41              
42 3   33     17 my $class = ref($proto) || $proto;
43 3         5 my ($self) = {};
44 3         7 bless( $self, $class );
45              
46 3         11 $self->{'PASSWD'} = $passwdFile;
47 3         5 $self->{'ERROR'} = "";
48 3         6 $self->{'LOCK'} = 0;
49 3         5 $self->{'OPEN'} = 0;
50 3 100       10 $self->{'READONLY'} = $args->{'ReadOnly'} if ref $args eq 'HASH';
51 3 100       8 $self->{'USEMD5'} = $args->{'UseMD5'} if ref $args eq 'HASH';
52 3 100       7 $self->{'USEPLAIN'} = $args->{'UsePlain'} if ref $args eq 'HASH';
53              
54 3         13 return $self;
55             }
56              
57             #-----------------------------------------------------------#
58              
59             sub error {
60 2     2 1 3 my $self = shift;
61 2         6 return $self->{'ERROR'};
62             }
63              
64             #-----------------------------------------------------------#
65              
66             sub htCheckPassword {
67 7     7 1 17482 my $self = shift;
68 7         11 my $Id = shift;
69 7         8 my $pass = shift;
70 7         7 my $MD5Magic = '$apr1$';
71 7         7 my $SHA1Magic = '{SHA}';
72              
73 7         14 my $cryptPass = $self->fetchPass($Id);
74 7 100       14 if ( !$cryptPass ) { return undef; }
  1         2  
75              
76 6 100       23 if (index($cryptPass, $MD5Magic) == 0) {
    100          
77             # This is an MD5 password
78 1         5 require Crypt::PasswdMD5;
79 1         2 my $salt = $cryptPass;
80 1         13 $salt =~ s/^\Q$MD5Magic//; # Take care of the magic string if present
81 1         7 $salt =~ s/^(.*)\$/$1/; # Salt can have up to 8 chars...
82 1         4 $salt = substr( $salt, 0, 8 ); # That means no more than 8 chars too.
83 1 50       3 return 1 if Crypt::PasswdMD5::apache_md5_crypt( $pass, $salt ) eq $cryptPass;
84             }
85             elsif (index($cryptPass, $SHA1Magic) == 0) {
86             # This is an SHA1 password
87 1         6 require Digest::SHA;
88 1         4 require MIME::Base64;
89 1 50       30 return 1 if '{SHA}'.MIME::Base64::encode_base64( Digest::SHA::sha1( $pass ), '' ) eq $cryptPass;
90             }
91              
92             # See if it is encrypted using crypt
93 4 100       123 return 1 if crypt($pass, $cryptPass) eq $cryptPass;
94              
95             # See if it is a plain, unencrypted password
96 1 50 33     11 return 1 if $self->{USEPLAIN} && $pass eq $cryptPass;
97            
98 0         0 $self->{'ERROR'} =
99             __PACKAGE__ . "::htCheckPassword - Passwords do not match.";
100 0 0       0 carp $self->error() if caller eq $self;
101 0         0 return 0;
102             }
103              
104             #-----------------------------------------------------------#
105              
106             sub htpasswd {
107 5     5 1 9 my $self = shift;
108 5         7 my $Id = shift;
109 5         7 my $newPass = shift;
110 5         7 my $oldPass = shift;
111 5         5 my $noOld = 0;
112              
113 5 100       12 if ( $self->{READONLY} ) {
114 1         2 $self->{'ERROR'} =
115             __PACKAGE__ . "::htpasswd - Can't change passwords in ReadOnly mode";
116 1         2 carp $self->error();
117 1         4 return undef;
118             }
119              
120 4 100       7 if ( !defined($oldPass) ) {
121 1         2 $noOld = 1;
122             }
123              
124 4 100 100     22 if ( defined($oldPass) && ref $oldPass eq 'HASH' ) {
125 1 50       3 if ($oldPass->{'overwrite'}) {
126 1 50       3 $newPass = $Id unless $newPass;
127 1         2 my $newEncrypted = $self->CryptPasswd($newPass);
128 1         3 return $self->writePassword( $Id, $newEncrypted );
129             }
130             }
131              
132             # New Entry
133 3 100       5 if ($noOld) {
134 1         3 my $passwdFile = $self->{'PASSWD'};
135              
136             # Encrypt new password string
137              
138 1         5 my $passwordCrypted = $self->CryptPasswd($newPass);
139              
140 1         6 $self->_open();
141              
142 1 50       6 if ( $self->fetchPass($Id) ) {
143              
144             # User already has a password in the file.
145 0         0 $self->{'ERROR'} =
146             __PACKAGE__ . "::htpasswd - $Id already exists in $passwdFile";
147 0         0 carp $self->error();
148 0         0 $self->_close();
149 0         0 return undef;
150             }
151             else {
152              
153             # If we can add the user.
154 1         6 seek( FH, 0, SEEK_END );
155 1         6 print FH "$Id\:$passwordCrypted\n";
156              
157 1         3 $self->_close();
158 1         4 return 1;
159             }
160              
161 0         0 $self->_close();
162              
163             }
164             else {
165 2         14 $self->_open();
166              
167 2         7 my $exists = $self->htCheckPassword( $Id, $oldPass );
168              
169 2 100       4 if ($exists) {
170 1         3 my ($newCrypted) = $self->CryptPasswd($newPass);
171 1         4 return $self->writePassword( $Id, $newCrypted );
172             }
173             else {
174              
175             # ERROR returned from htCheckPass
176 1         2 $self->{'ERROR'} =
177             __PACKAGE__ . "::htpasswd - Password not changed.";
178 1         4 carp $self->error();
179 1         5 return undef;
180             }
181              
182 0         0 $self->_close();
183             }
184             } # end htpasswd
185              
186             #-----------------------------------------------------------#
187              
188             sub htDelete {
189 1     1 1 2 my $self = shift;
190 1         2 my $Id = shift;
191 1         27 my $passwdFile = $self->{'PASSWD'};
192 1         2 my @cache;
193             my $return;
194              
195             # Loop through the file, building a cache of exising records
196             # which don't match the Id.
197              
198 1         3 $self->_open();
199              
200 1         4 seek( FH, 0, SEEK_SET );
201 1         7 while () {
202              
203 2 100       22 if (/^$Id\:/) {
204 1         4 $return = 1;
205             }
206             else {
207 1         7 push ( @cache, $_ );
208             }
209             }
210              
211             # Write out the @cache if needed.
212              
213 1 50       3 if ($return) {
214              
215             # Return to beginning of file
216 1         4 seek( FH, 0, SEEK_SET );
217              
218 1         3 while (@cache) {
219 1         8 print FH shift (@cache);
220             }
221              
222             # Cut everything beyond current position
223 1         29 truncate( FH, tell(FH) );
224              
225             }
226             else {
227 0         0 $self->{'ERROR'} =
228             __PACKAGE__ . "::htDelete - User $Id not found in $passwdFile: $!";
229 0         0 carp $self->error();
230             }
231              
232 1         2 $self->_close();
233              
234 1         4 return $return;
235             }
236              
237             #-----------------------------------------------------------#
238              
239             sub fetchPass {
240 9     9 1 10 my $self = shift;
241 9         8 my $Id = shift;
242 9         12 my $passwdFile = $self->{'PASSWD'};
243              
244 9         10 my $passwd = 0;
245              
246 9         15 $self->_open();
247              
248 9         87 while () {
249 23         26 chop;
250 23         54 my @tmp = split ( /:/, $_, 3 );
251 23 100       72 if ( $tmp[0] eq $Id ) {
252 7         7 $passwd = $tmp[1];
253 7         10 last;
254             }
255             }
256              
257 9         19 $self->_close();
258              
259 9         29 return $passwd;
260             }
261              
262             #-----------------------------------------------------------#
263              
264             sub writePassword {
265 2     2 0 6 my $self = shift;
266 2         2 my $Id = shift;
267 2         2 my $newPass = shift;
268              
269 2         4 my $passwdFile = $self->{'PASSWD'};
270 2         2 my @cache;
271             my $return;
272              
273 2         4 $self->_open();
274 2         9 seek( FH, 0, SEEK_SET );
275              
276 2         14 while () {
277              
278 4         12 my @tmp = split ( /:/, $_, 3 );
279 4 100       7 if ( $tmp[0] eq $Id ) {
280 2 100       5 my $info = $tmp[2] ? $tmp[2] : "";
281 2         11 chomp $info;
282 2         4 push ( @cache, "$Id\:$newPass\:$info\n" );
283 2         13 $return = 1;
284              
285             }
286             else {
287 2         6 push ( @cache, $_ );
288             }
289             }
290              
291             # Write out the @cache, if needed.
292              
293 2 50       4 if ($return) {
294              
295             # Return to beginning of file
296 2         9 seek( FH, 0, SEEK_SET );
297              
298 2         4 while (@cache) {
299 4         9 print FH shift (@cache);
300             }
301              
302             # Cut everything beyond current position
303 2         64 truncate( FH, tell(FH) );
304              
305             }
306             else {
307 0         0 $self->{'ERROR'} = __PACKAGE__
308             . "::writePassword - User $Id not found in $passwdFile: $!";
309 0         0 carp $self->error() . "\n";
310             }
311              
312 2         4 $self->_close();
313              
314 2         7 return $return;
315             }
316              
317             #-----------------------------------------------------------#
318              
319             sub fetchInfo {
320 1     1 1 2 my $self = shift;
321 1         1 my $Id = shift;
322 1         2 my $passwdFile = $self->{'PASSWD'};
323              
324 1         1 my $info = 0;
325              
326 1         2 $self->_open();
327              
328 1         7 while () {
329 1         3 chop;
330 1         3 my @tmp = split ( /:/, $_, 3 );
331 1 50       9 if ( $tmp[0] eq $Id ) {
332 1         2 $info = $tmp[2];
333 1         2 last;
334             }
335             }
336              
337 1         3 $self->_close();
338              
339 1         4 return $info;
340             }
341              
342             #-----------------------------------------------------------#
343              
344             sub fetchUsers {
345 2     2 1 33 my $self = shift;
346 2         4 my $passwdFile = $self->{'PASSWD'};
347 2         2 my $count = 0;
348 2         2 my @users;
349              
350 2         9 $self->_open();
351              
352 2         15 while () {
353 2         3 chop;
354 2         5 my @tmp = split ( /:/, $_, 3 );
355 2 50       17 push ( @users, $tmp[0] ) unless !$tmp[0];
356             }
357              
358 2         5 $self->_close();
359              
360 2 100       8 return wantarray() ? @users : scalar @users;
361             }
362              
363             #-----------------------------------------------------------#
364              
365             sub writeInfo {
366 1     1 1 1 my $self = shift;
367 1         2 my $Id = shift;
368 1         2 my $newInfo = shift;
369              
370 1         2 my ($passwdFile) = $self->{'PASSWD'};
371 1         2 my (@cache);
372              
373             my ($return);
374              
375 1         2 $self->_open();
376 1         4 seek( FH, 0, SEEK_SET );
377              
378 1         7 while () {
379              
380 2         6 my @tmp = split ( /:/, $_, 3 );
381              
382 2 100       13 if ( $tmp[0] eq $Id ) {
383 1 50       4 chomp $tmp[1] if ( @tmp == 2 ); # Cut out EOL if there was no info
384 1         4 push ( @cache, "$Id\:$tmp[1]\:$newInfo\n" );
385 1         3 $return = 1;
386              
387             }
388             else {
389 1         6 push ( @cache, $_ );
390             }
391             }
392              
393             # Write out the @cache, if needed.
394              
395 1 50       3 if ($return) {
396              
397             # Return to beginning of file
398 1         5 seek( FH, 0, SEEK_SET );
399              
400 1         9 while (@cache) {
401 2         5 print FH shift (@cache);
402             }
403              
404             # Cut everything beyond current position
405 1         45 truncate( FH, tell(FH) );
406              
407             }
408             else {
409 0         0 $self->{'ERROR'} =
410             __PACKAGE__ . "::writeInfo - User $Id not found in $passwdFile: $!";
411 0         0 carp $self->error() . "\n";
412             }
413              
414 1         3 $self->_close();
415              
416 1         4 return $return;
417             }
418              
419             #-----------------------------------------------------------#
420              
421             sub CryptPasswd {
422 3     3 1 3 my $self = shift;
423 3         4 my $passwd = shift;
424 3         2 my $salt = shift;
425 3         50 my @chars = ( '.', '/', 0 .. 9, 'A' .. 'Z', 'a' .. 'z' );
426 3         3 my $Magic = '$apr1$'; # Apache specific Magic chars
427 3 50 33     22 my $cryptType = ( $^O =~ /^MSWin/i || $self->{'USEMD5'} ) ? "MD5" : "crypt";
428              
429 3 50 33     17 if ( $salt && $cryptType =~ /MD5/i && $salt =~ /^\Q$Magic/ ) {
    50 33        
      33        
430              
431             # Borrowed from Crypt::PasswdMD5
432 0         0 $salt =~ s/^\Q$Magic//; # Take care of the magic string if present
433 0         0 $salt =~ s/^(.*)\$.*$/$1/; # Salt can have up to 8 chars...
434 0         0 $salt = substr( $salt, 0, 8 ); # That means no more than 8 chars too.
435             # For old crypt only
436             }
437             elsif ( $salt && $cryptType =~ /crypt/i ) {
438 0 0       0 if ($salt =~ /\$2a\$\d+\$(.{23})/) {
439 0         0 $salt = $1;
440             } else {
441             # Make sure only use 2 chars
442 0         0 $salt = substr( $salt, 0, 2 );
443             }
444             }
445             else {
446              
447             # If we use MD5, create apache MD5 with 8 char salt: 3 randoms, 5 dots
448 3 50       7 if ( $cryptType =~ /MD5/i ) {
449 0         0 $salt =
450 0         0 join ( '', map { $chars[ int rand @chars ] } ( 0 .. 2 ) )
451             . "." x 5;
452              
453             # Otherwise fallback to standard archaic crypt
454             }
455             else {
456 3         6 $salt = join ( '', map { $chars[ int rand @chars ] } ( 0 .. 1 ) );
  6         59  
457             }
458             }
459              
460 3 50       10 if ( $cryptType =~ /MD5/i ) {
461 0         0 require Crypt::PasswdMD5;
462 0         0 return Crypt::PasswdMD5::apache_md5_crypt( $passwd, $salt );
463             }
464             else {
465 3         681 return crypt( $passwd, $salt );
466             }
467             }
468              
469             #-----------------------------------------------------------#
470              
471 2     2   58 sub DESTROY { close(FH); };
472              
473             #-----------------------------------------------------------#
474              
475             sub _lock {
476 15     15   23 my $self = shift;
477              
478             # Lock if we don't have the lock
479 15 100       78 flock( FH, LOCK_EX ) if ( $self->{'LOCK'} == 0 );
480              
481             # We have the lock
482 15         17 $self->{'LOCK'} = 1;
483              
484             # Seek to head
485 15         65 seek( FH, 0, SEEK_SET );
486             }
487              
488             #-----------------------------------------------------------#
489              
490             sub _unlock {
491 13     13   14 my $self = shift;
492              
493 13         71 flock( FH, LOCK_UN );
494              
495 13         20 $self->{'LOCK'} = 0;
496             }
497              
498             #-----------------------------------------------------------#
499              
500             sub _open {
501 19     19   21 my $self = shift;
502              
503 19 100       37 if ( $self->{'OPEN'} > 0 ) {
504 13         13 $self->{'OPEN'}++;
505 13         18 $self->_lock();
506 13         15 return;
507             }
508              
509 6         9 my $passwdFile = $self->{'PASSWD'};
510              
511 6 100       11 if ( $self->{READONLY} ) {
512 4 50       102 if ( !open( FH, $passwdFile ) ) {
513 0         0 $self->{'ERROR'} =
514             __PACKAGE__ . "::fetchPass - Cannot open $passwdFile: $!";
515 0         0 croak $self->error();
516             }
517             }
518             else {
519 2 50       78 if ( !open( FH, "+<$passwdFile" ) ) {
520 0         0 $self->{'ERROR'} =
521             __PACKAGE__ . "::fetchPass - Cannot open $passwdFile: $!";
522 0         0 croak $self->error();
523             }
524             }
525              
526 6         12 binmode(FH);
527 6         10 $self->{'OPEN'}++;
528 6 100       19 $self->_lock() unless $self->{READONLY}; # No lock on r/o
529             }
530              
531             #-----------------------------------------------------------#
532              
533             sub _close {
534 17     17   20 my $self = shift;
535 17 100       42 $self->_unlock() unless $self->{READONLY};
536              
537 17         18 $self->{'OPEN'}--;
538              
539 17 100       30 if ( $self->{'OPEN'} > 0 ) { return; }
  12         21  
540              
541 5 50       54 if ( !close(FH) ) {
542 0           my $passwdFile = $self->{'PASSWD'};
543 0           $self->{'ERROR'} =
544             __PACKAGE__ . "::htDelete - Cannot close $passwdFile: $!";
545 0           carp $self->error();
546 0           return undef;
547             }
548             }
549              
550             #-----------------------------------------------------------#
551              
552             1;
553              
554             __END__