File Coverage

blib/lib/Web/Passwd.pm
Criterion Covered Total %
statement 18 136 13.2
branch 0 78 0.0
condition 0 21 0.0
subroutine 6 16 37.5
pod 2 10 20.0
total 26 261 9.9


line stmt bran cond sub pod time code
1             # $Date: 2007-02-07 15:54:57 -0600 (Wed, 07 Feb 2007) $
2             # $Revision: 19 $
3            
4             package Web::Passwd;
5 1     1   24958 use base 'CGI::Application';
  1         3  
  1         1364  
6            
7 1     1   15475 use strict;
  1         3  
  1         37  
8 1     1   7 use warnings;
  1         8  
  1         36  
9 1     1   4035 use CGI::Carp qw/fatalsToBrowser warningsToBrowser/;
  1         3141  
  1         6  
10 1     1   2719 use Config::Tiny;
  1         1085  
  1         2101  
11            
12             # set up application framework, including mode parameter and dispatch table
13             sub setup {
14 0     0 1   my $self = shift;
15 0           $self->start_mode('index');
16 0           $self->mode_param('mode');
17 0           $self->run_modes(
18             'index' => 'display_index',
19             'view' => 'display_htfile',
20             'adduser' => 'user_op',
21             'changepw' => 'user_op',
22             'deluser' => 'user_op',
23             );
24            
25             # return just to be tidy
26 0           return;
27             }
28            
29             # perform final actions after all other processing
30             sub teardown {
31             # trigger the printing of any warnings to browser as HTML comments
32 0     0 1   warningsToBrowser(1);
33            
34             # return just to be tidy
35 0           return;
36             }
37            
38             # display index page with no actions to be performed
39             sub display_index {
40 0     0 0   my $self = shift;
41            
42             # load configuration as a hash ref
43 0           $self->param('act_config', load_config( $self->param('config') ) );
44            
45             # load template file with HTML::Template
46 0           my $tmpl_obj;
47 0 0         if(-e $self->param('act_config')->{'_'}->{'tmpl_path'} . 'index.tmpl' ) {
48 0           $tmpl_obj = $self->load_tmpl( $self->param('act_config')->{'_'}->{'tmpl_path'} . 'index.tmpl' );
49             }
50             else {
51 0           $tmpl_obj = $self->load_tmpl( \$Web::Passwd::INDEX_TEMPLATE );
52             }
53            
54             # get list of htpasswd config blocks
55 0           my @htfiles;
56 0           for my $key (keys %{$self->param('act_config')}) {
  0            
57 0 0         if($key ne '_') {
58 0           push(@htfiles, {'TITLE' => $key});
59             }
60             }
61            
62             # pass template parameters
63             $tmpl_obj->param(
64 0           'HTFILES' => \@htfiles,
65             'IS_WARNINGS' => $#CGI::Carp::WARNINGS + 1,
66             'FORM_METHOD' => $self->param('act_config')->{'_'}->{'form_method'},
67             );
68            
69             # return template-generated output
70 0           return $tmpl_obj->output;
71             }
72            
73             # display page to view/manage a specific htpasswd file
74             sub display_htfile {
75 0     0 0   my $self = shift;
76            
77             # get CGI query object
78 0           my $query_obj = $self->query();
79            
80             # load configuration as a hash ref
81 0           $self->param('act_config', load_config( $self->param('config') ) );
82            
83             # load template file with HTML::Template
84 0           my $tmpl_obj;
85 0 0         if(-e $self->param('act_config')->{'_'}->{'tmpl_path'} . 'view.tmpl' ) {
86 0           $tmpl_obj = $self->load_tmpl( $self->param('act_config')->{'_'}->{'tmpl_path'} . 'view.tmpl' );
87             }
88             else {
89 0           $tmpl_obj = $self->load_tmpl( \$Web::Passwd::VIEW_TEMPLATE );
90             }
91            
92             # get user list and format for template processing
93 0           my @users;
94 0           for my $user ( htfile_listusers( $self->param('act_config')->{ $query_obj->param('htfile') }->{'path'} ) ) {
95 0           push(@users, {'USERNAME' => $user});
96             }
97            
98             # pass template parameters
99             $tmpl_obj->param(
100 0           'HTFILENAME' => $query_obj->param('htfile'),
101             'USER_LOOP' => \@users,
102             'IS_WARNINGS' => $#CGI::Carp::WARNINGS + 1,
103             'FORM_METHOD' => $self->param('act_config')->{'_'}->{'form_method'},
104             );
105            
106             # return template-generated output
107 0           return $tmpl_obj->output;
108             }
109            
110             # display the status of an operation
111             sub display_status {
112 0     0 0   my($self, $mode, $htfile, @users) = @_;
113            
114             # load template file with HTML::Template
115 0           my $tmpl_obj;
116 0 0         if(-d $self->param('act_config')->{'_'}->{'tmpl_path'}.'status.tmpl' ) {
117 0           $tmpl_obj = $self->load_tmpl( $self->param('act_config')->{'_'}->{'tmpl_path'}.'status.tmpl' );
118             }
119             else {
120 0           $tmpl_obj = $self->load_tmpl( \$Web::Passwd::STATUS_TEMPLATE );
121             }
122            
123             # build action status header
124 0 0         my $act_stat = ($mode eq 'adduser') ? "Addition Successful"
    0          
    0          
125             : ($mode eq 'changepw') ? "Modification Successful"
126             : ($mode eq 'deluser') ? "Deletion Successful"
127             : "Action Successful";
128            
129             # build action message
130 0 0 0       my $act_msg = ($mode eq 'adduser') ? sprintf("User '%s' added.", $users[0])
    0 0        
    0          
    0          
131             : ($mode eq 'changepw') ? sprintf("Password changed for user '%s'.", $users[0])
132             : ($mode eq 'deluser' && $#users == 0) ? sprintf("User '%s' deleted.", $users[0])
133             : ($mode eq 'deluser' && $#users > 0) ? sprintf("Users '%s' deleted.", join "','", @users)
134             : 'Unknown operation...Check error logs.';
135            
136             # pass template parameters
137 0           $tmpl_obj->param(
138             'ACTION_STATUS' => $act_stat,
139             'ACTION_MESSAGE' => $act_msg,
140             'HTFILENAME' => $htfile,
141             'IS_WARNINGS' => $#CGI::Carp::WARNINGS + 1,
142             'FORM_METHOD' => $self->param('act_config')->{'_'}->{'form_method'},
143             );
144            
145             # return template-generated output
146 0           return $tmpl_obj->output;
147             }
148            
149             # perform an operation
150             sub user_op {
151 0     0 0   my $self = shift;
152            
153             # get CGI query object
154 0           my $query_obj = $self->query();
155            
156             # create lexical copy of mode
157 0           my $user_mode = lc $query_obj->param('mode');
158            
159             # if adding or modifying user, check that passwords match
160 0 0 0       if($user_mode eq 'adduser' || $user_mode eq 'changepw') {
161 0 0         if($query_obj->param('pass') ne $query_obj->param('pass_confirm')) {
162 0           die 'passwords did not match';
163             }
164             }
165            
166             # load configuration as a hash ref
167 0           $self->param('act_config', load_config( $self->param('config') ) );
168            
169             # add new or change existing user/pass
170 0           my @users = $query_obj->param('user');
171 0 0 0       if($user_mode eq 'adduser' || $user_mode eq 'changepw') {
    0          
172 0           htfile_moduser(
173             $self->param('act_config')->{'_'}->{'htpasswd_command'},
174             $self->param('act_config')->{ $query_obj->param('htfile') }->{'path'},
175             $users[0],
176             $query_obj->param('pass'),
177             $self->param('act_config')->{ $query_obj->param('htfile') }->{'algorithm'}
178             );
179             }
180             # or delete existing user(s)
181             elsif($user_mode eq 'deluser') {
182 0           for my $user (@users) {
183 0           htfile_deluser(
184             $self->param('act_config')->{'_'}->{'htpasswd_command'},
185             $self->param('act_config')->{ $query_obj->param('htfile') }->{'path'},
186             $user
187             );
188             }
189             }
190            
191             # generate operation status page from template
192 0           my $tmpl_output = display_status( $self, $user_mode, $query_obj->param('htfile'), @users );
193            
194             # return template-generated output
195 0           return $tmpl_output;
196             }
197            
198             # load the app configuration, returning a hash reference
199             sub load_config {
200 0     0 0   my $conf_file = shift;
201            
202             # if custom configuration not provided, search for a default config file
203 0 0         if(!defined $conf_file) {
204             # expected filename of the config
205 0           my $CONFIG_FILENAME = 'webpasswd.conf';
206            
207             # search for config file in current, parent, and /etc directories
208 0 0         $conf_file = (-e "./$CONFIG_FILENAME") ? "./$CONFIG_FILENAME"
    0          
    0          
209             : (-e "../$CONFIG_FILENAME") ? "../$CONFIG_FILENAME"
210             : (-e "/etc/$CONFIG_FILENAME") ? "/etc/$CONFIG_FILENAME"
211             : undef;
212            
213             # die if config file was not found
214 0 0         if(! defined $conf_file) {
215 0           die "configuration file not found"
216             }
217             }
218            
219             # load configuration, or die on error
220 0 0         my $config_obj = Config::Tiny->read($conf_file) or die Config::Tiny::errstr();
221            
222             # if no htpasswd command supplied, default to 'htpasswd'
223 0 0         if(!exists $config_obj->{'_'}->{'htpasswd_command'}) {
224 0           $config_obj->{'_'}->{'htpasswd_command'} = 'htpasswd';
225 0           warn "missing 'htpasswd_command' configuration option, using default of 'htpasswd'";
226             }
227            
228             # if template path doesnt exist, try root path
229 0 0         if(!exists $config_obj->{'_'}->{'tmpl_path'}) {
230 0           $config_obj->{'_'}->{'tmpl_path'} = '/';
231             }
232            
233             # if template path doesnt end with a fore-slash, append one
234 0 0         if(substr($config_obj->{'_'}->{'tmpl_path'}, -1) ne '/') {
235 0           $config_obj->{'_'}->{'tmpl_path'} .= '/';
236             }
237            
238             # if form method not provided or not GET, default to POST
239 0 0 0       if(!exists $config_obj->{'_'}->{'form_method'} || uc($config_obj->{'_'}->{'form_method'}) ne 'GET') {
240 0           $config_obj->{'_'}->{'form_method'} = 'POST';
241             }
242            
243             # ensure valid attributes for each configured section
244 0           for my $section (keys %{$config_obj}) {
  0            
245             # if not root section
246 0 0         if($section ne '_') {
247             # if missing path, remove from active config and issue warning
248 0           my $file_path = $config_obj->{$section}->{'path'};
249 0 0 0       if(!defined $file_path || $file_path =~ m/\A\s*\z/ || ! -e $file_path) {
      0        
250 0           delete $config_obj->{$section};
251 0           warn "invalid path for config block [$section]";
252 0           next;
253             }
254            
255             # if missing or invalid algorithm, default to 'crypt' and issue warning
256 0           my $pass_alg = lc $config_obj->{$section}->{'algorithm'};
257 0 0         if($pass_alg !~ m/\s*(?:crypt|md5|sha|plain)\s*/i) {
258 0           warn "invalid password algorithm '$pass_alg' for config block [$section], using 'crypt' instead";
259 0           $pass_alg = 'crypt';
260             }
261 0           $config_obj->{$section}->{'algorithm'} = $pass_alg;
262             }
263             }
264            
265             # return config
266 0           return $config_obj;
267             }
268            
269             # list the users in a given htfile
270             sub htfile_listusers {
271 0     0 0   my $htfile = shift;
272            
273             # declare array to hold usernames
274 0           my @users;
275            
276             # read htfile in as text
277 0 0         open(my $HTFILE, '<', $htfile) or die $!;
278 0           my @file = <$HTFILE>;
279 0           close($HTFILE);
280            
281             # parse off usernames, add to array
282 0           for my $line (@file) {
283 0           my($user) = split /:/, $line, 2;
284 0           push @users, $user;
285             }
286            
287             # return username array
288 0           return @users;
289             }
290            
291             # add/modify a user in a given htfile
292             sub htfile_moduser {
293 0     0 0   my($htcmd,$htfile,$user,$pass,$algorithm) = @_;
294            
295             # translate algorithm to appropriate flag
296 0 0         $algorithm = ($algorithm eq 'plain') ? 'p'
    0          
    0          
    0          
297             : ($algorithm eq 'md5') ? 'm'
298             : ($algorithm eq 'sha') ? 's'
299             : ($algorithm eq 'crypt') ? 'd'
300             : '';
301            
302             # assemble command
303 0           my $command = sprintf "%s -b%s %s %s %s",
304             $htcmd,
305             $algorithm,
306             $htfile,
307             $user,
308             $pass;
309            
310             # execute, or die on unsuccessful return value
311 0 0         if(system($command) != 0) {
312 0           die "htpasswd command failed: $?";
313             }
314            
315             # return just to be tidy
316 0           return;
317             }
318            
319             # delete a user in a given htfile
320             sub htfile_deluser {
321 0     0 0   my($htcmd,$htfile,$user) = @_;
322            
323             # assemble command
324 0           my $command = sprintf "%s -D %s %s",
325             $htcmd,
326             $htfile,
327             $user;
328            
329             # try to make htpasswd do the work with the -D flag (apache 2.x)
330 0 0         if(system($command) == 0) {
331 0           return 1;
332             }
333             # otherwise, do the damn thing by hand (apache 1.3.x)
334             else {
335             # read in htfile contents
336 0 0         open(my $HTIN, '<', $htfile) or die $!;
337 0           my @file = <$HTIN>;
338 0           close($HTIN);
339            
340             # search for, and remove, offending user line
341 0           my $deleted = 0;
342 0           for my $ln (0..$#file) {
343 1     1   9 no warnings; # bypass a puzzling warning of uninitialized value in m//
  1         2  
  1         364  
344 0 0         if($file[$ln] =~ m/\A$user\:/) {
345 0           splice @file, $ln, 1;
346 0           $deleted++;
347             }
348             }
349            
350             # write changes back to htfile
351 0 0         open(my $HTOUT, '>', $htfile) or die $!;
352 0           print {$HTOUT} @file;
  0            
353 0           close($HTOUT);
354            
355             # set error string
356 0 0         $! = ($deleted) ? undef : "remove of line '$user' failed";
357            
358 0           return $deleted;
359             }
360             }
361            
362            
363             $Web::Passwd::INDEX_TEMPLATE = <<'HTML_CODE';
364            
365            
366            
367             Web Htpasswd Management
368            
372            
373            
374            
375            
376            
377            
378            

Web Htpasswd Management

379            
380            
381            
382            

383            
">
384            
385            
386             Select Htpasswd File:  
387            
388            
389            
390            
391            
392            
393            
394            
395            
396            
397            

398            
399            
400            
401             Warnings were encountered...Please check error log.
402            
403            
404            
405            
406            
407            
408            
409            
412             HTML_CODE
413            
414             $Web::Passwd::VIEW_TEMPLATE = <<'HTML_CODE';
415            
416            
417            
418             Web Htpasswd Management
419            
423            
424            
425            
426            
427            
428            
429            

Managing Htpasswd File:

430            

431            
432            
433            
434            

435            
">
436             ">
437            
438            
439             Add User
440            
441            
442             Username:  
443            
444            
445            
446             Password:  
447            
448            
449            
450             Retype Password:  
451            
452            
453            
454            
455            
456            
457            
458            
459            
460            

461            
462            

463            
">
464             ">
465            
466            
467             Modify User
468            
469            
470             Username:  
471            
472            
473            
474            
475            
476            
477            
478            
479            
480             Password:  
481            
482            
483            
484             Retype Password:  
485            
486            
487            
488            
489            
490            
491            
492            
493            
494            

495            
496            

497            
">
498             ">
499            
500            
501             Delete Users
502            
503            
504             Usernames:  
505            
506            
507            
508            
509            
510            
511            
512            
513            
514            
515            
516            
517            
518            
519            
520            

521            
522            
523             Back to Main
524            
525            
526             Warnings were encountered...Please check error log.
527            
528            
529            
530            
531            
532            
533            
534            
537             HTML_CODE
538            
539             $Web::Passwd::STATUS_TEMPLATE = <<'HTML_CODE';
540            
541            
542            
543             Web Htpasswd Management
544            
548            
549            
550            
551            
552            
553            
554            

555            
556            

557            
">
558            
 
559             ">
560            
561            
562            
563            

564            
565            
566            
567             Warnings were encountered...Please check error log.
568            
569            
570            
571            
572            
573            
574            
575            
578             HTML_CODE
579            
580             =head1 NAME
581            
582             Web::Passwd - Web-based htpasswd Management
583            
584             =head1 VERSION
585            
586             Version 0.03
587            
588             =cut
589             our $VERSION = "0.03";
590            
591             =head1 SYNOPSIS
592            
593             Web::Passwd is a web-based utility for managing Apache C files. It uses the L framework, so functionality is encapsulated in the module and very little code is required to create an instance:
594            
595             use Web::Passwd;
596            
597             my $webapp = Web::Passwd->new();
598             $webapp->run();
599            
600             That's it. Drop that script in a web-accessible cgi directory and give it execute permissions, and (assuming a default config file is found), you're good to go. If you'd rather explicity define a configuration file to use, you can pass it through an extra parameter:
601            
602             my $webapp = Web::Passwd->new( PARAMS => { config => '/home/evan/custom_webpasswd.conf' } );
603            
604             =head1 CONFIGURATION
605            
606             If not explicitly provided, a configuration file will be searched for in the following locations (in order). If a valid configuration file is not found, the script will die with errors.
607            
608             ./webpasswd.conf (the current directory)
609             ../webpasswd.conf (the parent directory)
610             /etc/webpasswd.conf
611            
612             The configuration file can be used to specify a directory of templates in the L format. If no templates are found, default templates are used (see the C directory of the distribution).
613            
614             tmpl_path = /var/www/cgi-bin/webpasswd/
615            
616             The C command can also be specified. If no C command is provided, the default is used. Note that, on some systems, you must specify the I path to the C binary.
617            
618             htpasswd_command = htpasswd
619            
620             The configuration file can specify whether to use the C (data encoded into the URL) or C (data encoded into the message body) form request method. Defaults to using the generally more secure C.
621            
622             form_method = POST
623            
624             The configuration file should also contain a section for each htpasswd file it will be used to maintain, using the following format:
625            
626             [Descriptive Name]
627             path = /system/path/to/passwdfile
628             algorithm = {crypt|md5|sha|plain}
629            
630             B The default algorithm Apache uses is C under Linux, and C under Windows.
631            
632             B Enclosing values in quotes within the config file does not have the expected effect! It simply includes the literal quote characters in the config value.
633            
634             =head1 SECURITY
635            
636             It is *imperitive* that the Web::Passwd instance script itself be htpasswd protected, as it includes no access control mechanism.
637            
638             Understand that putting the ability to manage htpasswd files via a web-based utility carries an inherent security risk, in that anyone who gains access to the utility is potentially given access to any of the managed htpasswd-protected resources.
639            
640             Any htpasswd files to be managed with this utility MUST be owned by whatever user apache runs as. Usually, this is 'apache' or 'nobody'.
641            
642             =head1 COMPATABILITY
643            
644             This was written expressly for Apache webserver 1.3 or higher running under Linux. However, there is nothing as far as I am aware that would prevent execution on a higher version of Apache, or on Apache under Windows.
645            
646             =head1 DEPENDENCIES
647            
648             A Perl version of 5.6.1 or higher is recommended, and the following modules are required:
649            
650             CGI::Application
651             Config::Tiny
652             HTML::Template
653            
654             =head1 AUTHOR
655            
656             Evan Kaufman, C<< >>
657            
658             =head1 SUPPORT
659            
660             You can find documentation for this module with the perldoc command.
661            
662             perldoc Web::Passwd
663            
664             =head1 ACKNOWLEDGEMENTS
665            
666             Written for BCD Music Group.
667            
668             =head1 COPYRIGHT & LICENSE
669            
670             Copyright 2007 Evan Kaufman, all rights reserved.
671            
672             This program is free software; you can redistribute it and/or modify it
673             under the same terms as Perl itself.
674            
675             =cut
676            
677             # we're a good little module
678             1;