File Coverage

blib/lib/Authen/Htpasswd.pm
Criterion Covered Total %
statement 111 130 85.3
branch 35 72 48.6
condition 9 21 42.8
subroutine 20 21 95.2
pod 7 7 100.0
total 182 251 72.5


line stmt bran cond sub pod time code
1             package Authen::Htpasswd;
2 3     3   60048 use 5.005;
  3         10  
  3         115  
3 3     3   17 use strict;
  3         6  
  3         103  
4 3     3   16 use base 'Class::Accessor::Fast';
  3         13  
  3         2877  
5 3     3   313454 use Carp;
  3         11  
  3         315  
6 3     3   3421 use IO::File;
  3         35498  
  3         420  
7 3     3   76922 use IO::LockedFile;
  3         8095  
  3         20  
8 3     3   5583 use Authen::Htpasswd::User;
  3         11  
  3         24  
9 3     3   116 use Scalar::Util qw(blessed);
  3         6  
  3         349  
10              
11 3     3   16 use vars qw{$VERSION $SUFFIX};
  3         5  
  3         5607  
12              
13             $VERSION = '0.171';
14             $VERSION = eval $VERSION;
15             $SUFFIX = '.new';
16              
17             __PACKAGE__->mk_accessors(qw/ file encrypt_hash check_hashes /);
18              
19             =head1 NAME
20              
21             Authen::Htpasswd - interface to read and modify Apache .htpasswd files
22              
23             =head1 SYNOPSIS
24            
25             my $pwfile = Authen::Htpasswd->new('user.txt', { encrypt_hash => 'md5' });
26            
27             # authenticate a user (checks all hash methods by default)
28             if ($pwfile->check_user_password('bob', 'foo')) { ... }
29            
30             # modify the file (writes immediately)
31             $pwfile->update_user('bob', $password, $info);
32             $pwfile->add_user('jim', $password);
33             $pwfile->delete_user('jim');
34            
35             # get user objects tied to a file
36             my $user = $pwfile->lookup_user('bob');
37             if ($user->check_password('vroom', [qw/ md5 sha1 /])) { ... } # only use secure hashes
38             $user->password('foo'); # writes to file
39             $user->set(password => 'bar', extra_info => 'editor'); # change more than one thing at once
40            
41             # or manage the file yourself
42             my $user = Authen::Htpasswd::User->new('bill', { hashed_password => 'iQ.IuWbUIhlPE' });
43             my $user = Authen::Htpasswd::User->new('bill', 'bar', 'staff', { encrypt_hash => 'crypt' });
44             print PASSWD $user->to_line, "\n";
45              
46             =head1 DESCRIPTION
47              
48             This module provides a convenient, object-oriented interface to Apache-style
49             F<.htpasswd> files.
50              
51             It supports passwords encrypted via MD5, SHA1, and crypt, as well as plain
52             (cleartext) passwords.
53              
54             Additional fields after username and password, if present, are accessible via
55             the C array.
56              
57             =head1 METHODS
58              
59             =head2 new
60              
61             my $pwfile = Authen::Htpasswd->new($filename, \%options);
62              
63             Creates an object for a given F<.htpasswd> file. Options:
64              
65             =over 4
66              
67             =item encrypt_hash
68              
69             How passwords should be encrypted if a user is added or changed. Valid values are C, C,
70             C, and C. Default is C.
71              
72             =item check_hashes
73              
74             An array of hash methods to try when checking a password. The methods will be tried in the order
75             given. Default is C, C, C, C.
76              
77             =back
78              
79             =cut
80              
81             sub new {
82 2     2 1 33448 my $class = shift;
83 2 50       16 my $self = ref $_[-1] eq 'HASH' ? pop @_ : {};
84 2 50       13 $self->{file} = $_[0] if $_[0];
85 2 50       9 croak "no file specified" unless $self->{file};
86 2 50       96 if (!-e $self->{file}) {
87 0 0       0 open my $file, '>', $self->{file} or die $!;
88 0 0       0 close $file or die $!;
89             }
90            
91 2   50     18 $self->{encrypt_hash} ||= 'crypt';
92 2   50     18 $self->{check_hashes} ||= [ Authen::Htpasswd::Util::supported_hashes() ];
93 2 50       13 unless ( defined $self->{write_locking} ) {
94 2 50 33     20 if ( $^O eq 'MSWin32' or $^O eq 'cygwin' ) {
95 0         0 $self->{write_locking} = 0;
96             } else {
97 2         6 $self->{write_locking} = 1;
98             }
99             }
100            
101 2         17 bless $self, $class;
102             }
103              
104             =head2 lookup_user
105            
106             my $userobj = $pwfile->lookup_user($username);
107              
108             Returns an L object for the given user in the password file.
109              
110             =cut
111              
112             sub lookup_user {
113 18     18 1 592 my ($self,$search_username) = @_;
114            
115 18 50       64 my $file = IO::LockedFile->new($self->file, 'r') or die $!;
116 18         4471 while (defined(my $line = <$file>)) {
117 47         64 chomp $line;
118 47         163 my ($username,$hashed_password,@extra_info) = split /:/, $line;
119 47 100       167 if ($username eq $search_username) {
120 16 50       52 $file->close or die $!;
121 16         951 return Authen::Htpasswd::User->new($username,undef,@extra_info, {
122             file => $self,
123             hashed_password => $hashed_password,
124             encrypt_hash => $self->encrypt_hash,
125             check_hashes => $self->check_hashes
126             });
127             }
128             }
129 2 50       6 $file->close or die $!;
130 2         101 return undef;
131             }
132              
133             =head2 all_users
134              
135             my @users = $pwfile->all_users;
136              
137             =cut
138              
139             sub all_users {
140 1     1 1 2 my $self = shift;
141              
142 1         2 my @users;
143 1 50       4 my $file = IO::LockedFile->new($self->file, 'r') or die $!;
144 1         192 while (defined(my $line = <$file>)) {
145 4         6 chomp $line;
146 4         11 my ($username,$hashed_password,@extra_info) = split /:/, $line;
147 4         14 push(@users, Authen::Htpasswd::User->new($username,undef,@extra_info, {
148             file => $self,
149             hashed_password => $hashed_password,
150             encrypt_hash => $self->encrypt_hash,
151             check_hashes => $self->check_hashes
152             }));
153             }
154 1 50       4 $file->close or die $!;
155 1         44 return @users;
156             }
157              
158             =head2 check_user_password
159              
160             $pwfile->check_user_password($username,$password);
161              
162             Returns whether the password is valid. Shortcut for
163             C<< $pwfile->lookup_user($username)->check_password($password) >>.
164              
165             =cut
166              
167             sub check_user_password {
168 13     13 1 2727 my ($self,$username,$password) = @_;
169 13         45 my $user = $self->lookup_user($username);
170 13 100       528 croak "could not find user $username" unless $user;
171 12         44 return $user->check_password($password);
172             }
173              
174             =head2 update_user
175            
176             $pwfile->update_user($userobj);
177             $pwfile->update_user($username, $password[, @extra_info], \%options);
178              
179             Modifies the entry for a user saves it to the file. If the user entry does not
180             exist, it is created. The options in the second form are passed to L.
181              
182             =cut
183              
184             sub update_user {
185 4     4 1 20 my $self = shift;
186 4         16 my $user = $self->_get_user(@_);
187 4         19 my $username = $user->username;
188              
189 4         13 my ($old,$new) = $self->_start_rewrite;
190 4         8 my $seen = 0;
191 4         82 while (defined(my $line = <$old>)) {
192 16 100       105 if ($line =~ /^\Q$username\E:/) {
193 4         11 chomp $line;
194 4         26 my (undef,undef,@extra_info) = split /:/, $line;
195 4 100 50     17 $user->{extra_info} ||= [ @extra_info ] if scalar @extra_info;
196 4         19 $self->_print( $new, $user->to_line . "\n" );
197 4         18 $seen++;
198             } else {
199 12         28 $self->_print( $new, $line );
200             }
201             }
202 4 50       11 $self->_print( $new, $user->to_line . "\n" ) unless $seen;
203 4         12 $self->_finish_rewrite($old,$new);
204             }
205              
206             =head2 add_user
207              
208             $pwfile->add_user($userobj);
209             $pwfile->add_user($username, $password[, @extra_info], \%options);
210              
211             Adds a user entry to the file. If the user entry already exists, an exception is raised.
212             The options in the second form are passed to L.
213              
214             =cut
215              
216             sub add_user {
217 1     1 1 422 my $self = shift;
218 1         6 my $user = $self->_get_user(@_);
219 1         6 my $username = $user->username;
220              
221 1         7 my ($old,$new) = $self->_start_rewrite;
222 1         54 while (defined(my $line = <$old>)) {
223 4 50       46 if ($line =~ /^\Q$username\E:/) {
224 0         0 $self->_abort_rewrite($old,$new);
225 0         0 croak "user $username already exists in " . $self->file . "!";
226             }
227 4         14 $self->_print( $new, $line );
228             }
229 1         11 $self->_print( $new, $user->to_line . "\n" );
230 1         6 $self->_finish_rewrite($old,$new);
231             }
232              
233             =head2 delete_user
234              
235             $pwfile->delete_user($userobj);
236             $pwfile->delete_user($username);
237              
238             Removes a user entry from the file.
239              
240             =cut
241              
242             sub delete_user {
243 2     2 1 8 my $self = shift;
244 2 50 33     14 my $username = blessed($_[0]) && $_[0]->isa('Authen::Htpasswd::User') ? $_[0]->username : $_[0];
245              
246 2         7 my ($old,$new) = $self->_start_rewrite;
247 2         44 while (defined(my $line = <$old>)) {
248 9 100       79 next if $line =~ /^\Q$username\E:/;
249 7         16 $self->_print( $new, $line );
250             }
251 2         6 $self->_finish_rewrite($old,$new);
252             }
253              
254             sub _print {
255 28     28   45 my ($self,$new,$string) = @_;
256 28 50       76 if ( $self->{write_locking} ) {
257 28         236 print $new $string;
258             } else {
259 0         0 $$new .= $string;
260             }
261             }
262              
263             sub _get_user {
264 5     5   8 my $self = shift;
265 5 100 66     58 return $_[0] if blessed($_[0]) && $_[0]->isa('Authen::Htpasswd::User');
266 2 50       10 my $attr = ref $_[-1] eq 'HASH' ? pop @_ : {};
267 2   33     21 $attr->{encrypt_hash} ||= $self->encrypt_hash;
268 2   33     30 $attr->{check_hashes} ||= $self->check_hashes;
269 2         28 return Authen::Htpasswd::User->new(@_, $attr);
270             }
271              
272             sub _start_rewrite {
273 7     7   12 my $self = shift;
274 7 50       19 if ( $self->{write_locking} ) {
275 7 50       25 my $old = IO::LockedFile->new($self->file, 'r+') or die $!;
276 7 50       1827 my $new = IO::File->new($self->file . $SUFFIX, 'w') or die $!;
277 7         11234 return ($old,$new);
278             } else {
279 0 0       0 my $old = IO::File->new( $self->file, 'r' ) or die $!;
280 0         0 my $new = "";
281 0         0 return ($old, \$new);
282             }
283             }
284              
285             sub _finish_rewrite {
286 7     7   12 my ($self,$old,$new) = @_;
287 7 50       18 if ( $self->{write_locking} ) {
288 7 50       39 $new->close or die $!;
289 7 50       402 rename $self->file . $SUFFIX, $self->file or die $!;
290 7 50       541 $old->close or die $!;
291             } else {
292 0 0         $old->close or die $!;
293 0 0         $old = IO::File->new( $self->file, 'w' ) or die $!;
294 0           print $old $$new;
295 0 0         $old->close or die $!;
296             }
297             }
298              
299             sub _abort_rewrite {
300 0     0     my ($self,$old,$new) = @_;
301 0 0         if ( $self->{write_locking} ) {
302 0           $new->close;
303 0           $old->close;
304 0           unlink $self->file . $SUFFIX;
305             } else {
306 0           $old->close;
307             }
308             }
309              
310             =head1 AUTHOR
311              
312             David Kamholz C
313              
314             Yuval Kogman
315              
316             =head1 SEE ALSO
317              
318             L.
319              
320             =head1 COPYRIGHT & LICENSE
321              
322             Copyright (c) 2005 - 2007 the aforementioned authors.
323            
324             This program is free software; you can redistribute
325             it and/or modify it under the same terms as Perl itself.
326              
327             =cut
328              
329             1;