# $Date: 2007-02-07 15:54:57 -0600 (Wed, 07 Feb 2007) $ # $Revision: 19 $ package Web::Passwd; use base 'CGI::Application'; use strict; use warnings; use CGI::Carp qw/fatalsToBrowser warningsToBrowser/; use Config::Tiny; # set up application framework, including mode parameter and dispatch table sub setup { my $self = shift; $self->start_mode('index'); $self->mode_param('mode'); $self->run_modes( 'index' => 'display_index', 'view' => 'display_htfile', 'adduser' => 'user_op', 'changepw' => 'user_op', 'deluser' => 'user_op', ); # return just to be tidy return; } # perform final actions after all other processing sub teardown { # trigger the printing of any warnings to browser as HTML comments warningsToBrowser(1); # return just to be tidy return; } # display index page with no actions to be performed sub display_index { my $self = shift; # load configuration as a hash ref $self->param('act_config', load_config( $self->param('config') ) ); # load template file with HTML::Template my $tmpl_obj; if(-e $self->param('act_config')->{'_'}->{'tmpl_path'} . 'index.tmpl' ) { $tmpl_obj = $self->load_tmpl( $self->param('act_config')->{'_'}->{'tmpl_path'} . 'index.tmpl' ); } else { $tmpl_obj = $self->load_tmpl( \$Web::Passwd::INDEX_TEMPLATE ); } # get list of htpasswd config blocks my @htfiles; for my $key (keys %{$self->param('act_config')}) { if($key ne '_') { push(@htfiles, {'TITLE' => $key}); } } # pass template parameters $tmpl_obj->param( 'HTFILES' => \@htfiles, 'IS_WARNINGS' => $#CGI::Carp::WARNINGS + 1, 'FORM_METHOD' => $self->param('act_config')->{'_'}->{'form_method'}, ); # return template-generated output return $tmpl_obj->output; } # display page to view/manage a specific htpasswd file sub display_htfile { my $self = shift; # get CGI query object my $query_obj = $self->query(); # load configuration as a hash ref $self->param('act_config', load_config( $self->param('config') ) ); # load template file with HTML::Template my $tmpl_obj; if(-e $self->param('act_config')->{'_'}->{'tmpl_path'} . 'view.tmpl' ) { $tmpl_obj = $self->load_tmpl( $self->param('act_config')->{'_'}->{'tmpl_path'} . 'view.tmpl' ); } else { $tmpl_obj = $self->load_tmpl( \$Web::Passwd::VIEW_TEMPLATE ); } # get user list and format for template processing my @users; for my $user ( htfile_listusers( $self->param('act_config')->{ $query_obj->param('htfile') }->{'path'} ) ) { push(@users, {'USERNAME' => $user}); } # pass template parameters $tmpl_obj->param( 'HTFILENAME' => $query_obj->param('htfile'), 'USER_LOOP' => \@users, 'IS_WARNINGS' => $#CGI::Carp::WARNINGS + 1, 'FORM_METHOD' => $self->param('act_config')->{'_'}->{'form_method'}, ); # return template-generated output return $tmpl_obj->output; } # display the status of an operation sub display_status { my($self, $mode, $htfile, @users) = @_; # load template file with HTML::Template my $tmpl_obj; if(-d $self->param('act_config')->{'_'}->{'tmpl_path'}.'status.tmpl' ) { $tmpl_obj = $self->load_tmpl( $self->param('act_config')->{'_'}->{'tmpl_path'}.'status.tmpl' ); } else { $tmpl_obj = $self->load_tmpl( \$Web::Passwd::STATUS_TEMPLATE ); } # build action status header my $act_stat = ($mode eq 'adduser') ? "Addition Successful" : ($mode eq 'changepw') ? "Modification Successful" : ($mode eq 'deluser') ? "Deletion Successful" : "Action Successful"; # build action message my $act_msg = ($mode eq 'adduser') ? sprintf("User '%s' added.", $users[0]) : ($mode eq 'changepw') ? sprintf("Password changed for user '%s'.", $users[0]) : ($mode eq 'deluser' && $#users == 0) ? sprintf("User '%s' deleted.", $users[0]) : ($mode eq 'deluser' && $#users > 0) ? sprintf("Users '%s' deleted.", join "','", @users) : 'Unknown operation...Check error logs.'; # pass template parameters $tmpl_obj->param( 'ACTION_STATUS' => $act_stat, 'ACTION_MESSAGE' => $act_msg, 'HTFILENAME' => $htfile, 'IS_WARNINGS' => $#CGI::Carp::WARNINGS + 1, 'FORM_METHOD' => $self->param('act_config')->{'_'}->{'form_method'}, ); # return template-generated output return $tmpl_obj->output; } # perform an operation sub user_op { my $self = shift; # get CGI query object my $query_obj = $self->query(); # create lexical copy of mode my $user_mode = lc $query_obj->param('mode'); # if adding or modifying user, check that passwords match if($user_mode eq 'adduser' || $user_mode eq 'changepw') { if($query_obj->param('pass') ne $query_obj->param('pass_confirm')) { die 'passwords did not match'; } } # load configuration as a hash ref $self->param('act_config', load_config( $self->param('config') ) ); # add new or change existing user/pass my @users = $query_obj->param('user'); if($user_mode eq 'adduser' || $user_mode eq 'changepw') { htfile_moduser( $self->param('act_config')->{'_'}->{'htpasswd_command'}, $self->param('act_config')->{ $query_obj->param('htfile') }->{'path'}, $users[0], $query_obj->param('pass'), $self->param('act_config')->{ $query_obj->param('htfile') }->{'algorithm'} ); } # or delete existing user(s) elsif($user_mode eq 'deluser') { for my $user (@users) { htfile_deluser( $self->param('act_config')->{'_'}->{'htpasswd_command'}, $self->param('act_config')->{ $query_obj->param('htfile') }->{'path'}, $user ); } } # generate operation status page from template my $tmpl_output = display_status( $self, $user_mode, $query_obj->param('htfile'), @users ); # return template-generated output return $tmpl_output; } # load the app configuration, returning a hash reference sub load_config { my $conf_file = shift; # if custom configuration not provided, search for a default config file if(!defined $conf_file) { # expected filename of the config my $CONFIG_FILENAME = 'webpasswd.conf'; # search for config file in current, parent, and /etc directories $conf_file = (-e "./$CONFIG_FILENAME") ? "./$CONFIG_FILENAME" : (-e "../$CONFIG_FILENAME") ? "../$CONFIG_FILENAME" : (-e "/etc/$CONFIG_FILENAME") ? "/etc/$CONFIG_FILENAME" : undef; # die if config file was not found if(! defined $conf_file) { die "configuration file not found" } } # load configuration, or die on error my $config_obj = Config::Tiny->read($conf_file) or die Config::Tiny::errstr(); # if no htpasswd command supplied, default to 'htpasswd' if(!exists $config_obj->{'_'}->{'htpasswd_command'}) { $config_obj->{'_'}->{'htpasswd_command'} = 'htpasswd'; warn "missing 'htpasswd_command' configuration option, using default of 'htpasswd'"; } # if template path doesnt exist, try root path if(!exists $config_obj->{'_'}->{'tmpl_path'}) { $config_obj->{'_'}->{'tmpl_path'} = '/'; } # if template path doesnt end with a fore-slash, append one if(substr($config_obj->{'_'}->{'tmpl_path'}, -1) ne '/') { $config_obj->{'_'}->{'tmpl_path'} .= '/'; } # if form method not provided or not GET, default to POST if(!exists $config_obj->{'_'}->{'form_method'} || uc($config_obj->{'_'}->{'form_method'}) ne 'GET') { $config_obj->{'_'}->{'form_method'} = 'POST'; } # ensure valid attributes for each configured section for my $section (keys %{$config_obj}) { # if not root section if($section ne '_') { # if missing path, remove from active config and issue warning my $file_path = $config_obj->{$section}->{'path'}; if(!defined $file_path || $file_path =~ m/\A\s*\z/ || ! -e $file_path) { delete $config_obj->{$section}; warn "invalid path for config block [$section]"; next; } # if missing or invalid algorithm, default to 'crypt' and issue warning my $pass_alg = lc $config_obj->{$section}->{'algorithm'}; if($pass_alg !~ m/\s*(?:crypt|md5|sha|plain)\s*/i) { warn "invalid password algorithm '$pass_alg' for config block [$section], using 'crypt' instead"; $pass_alg = 'crypt'; } $config_obj->{$section}->{'algorithm'} = $pass_alg; } } # return config return $config_obj; } # list the users in a given htfile sub htfile_listusers { my $htfile = shift; # declare array to hold usernames my @users; # read htfile in as text open(my $HTFILE, '<', $htfile) or die $!; my @file = <$HTFILE>; close($HTFILE); # parse off usernames, add to array for my $line (@file) { my($user) = split /:/, $line, 2; push @users, $user; } # return username array return @users; } # add/modify a user in a given htfile sub htfile_moduser { my($htcmd,$htfile,$user,$pass,$algorithm) = @_; # translate algorithm to appropriate flag $algorithm = ($algorithm eq 'plain') ? 'p' : ($algorithm eq 'md5') ? 'm' : ($algorithm eq 'sha') ? 's' : ($algorithm eq 'crypt') ? 'd' : ''; # assemble command my $command = sprintf "%s -b%s %s %s %s", $htcmd, $algorithm, $htfile, $user, $pass; # execute, or die on unsuccessful return value if(system($command) != 0) { die "htpasswd command failed: $?"; } # return just to be tidy return; } # delete a user in a given htfile sub htfile_deluser { my($htcmd,$htfile,$user) = @_; # assemble command my $command = sprintf "%s -D %s %s", $htcmd, $htfile, $user; # try to make htpasswd do the work with the -D flag (apache 2.x) if(system($command) == 0) { return 1; } # otherwise, do the damn thing by hand (apache 1.3.x) else { # read in htfile contents open(my $HTIN, '<', $htfile) or die $!; my @file = <$HTIN>; close($HTIN); # search for, and remove, offending user line my $deleted = 0; for my $ln (0..$#file) { no warnings; # bypass a puzzling warning of uninitialized value in m// if($file[$ln] =~ m/\A$user\:/) { splice @file, $ln, 1; $deleted++; } } # write changes back to htfile open(my $HTOUT, '>', $htfile) or die $!; print {$HTOUT} @file; close($HTOUT); # set error string $! = ($deleted) ? undef : "remove of line '$user' failed"; return $deleted; } } $Web::Passwd::INDEX_TEMPLATE = <<'HTML_CODE'; Web Htpasswd Management

Web Htpasswd Management


"> Select Htpasswd File:  


Warnings were encountered...Please check error log.
HTML_CODE $Web::Passwd::VIEW_TEMPLATE = <<'HTML_CODE'; Web Htpasswd Management

Managing Htpasswd File:


"> ">
Add User
Username:  
Password:  
Retype Password:  

"> ">
Modify User
Username:  
Password:  
Retype Password:  

"> ">
Delete Users
Usernames:  


Back to Main
Warnings were encountered...Please check error log.
HTML_CODE $Web::Passwd::STATUS_TEMPLATE = <<'HTML_CODE'; Web Htpasswd Management

">
 
">


Warnings were encountered...Please check error log.
HTML_CODE =head1 NAME Web::Passwd - Web-based htpasswd Management =head1 VERSION Version 0.03 =cut our $VERSION = "0.03"; =head1 SYNOPSIS 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: use Web::Passwd; my $webapp = Web::Passwd->new(); $webapp->run(); 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: my $webapp = Web::Passwd->new( PARAMS => { config => '/home/evan/custom_webpasswd.conf' } ); =head1 CONFIGURATION 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. ./webpasswd.conf (the current directory) ../webpasswd.conf (the parent directory) /etc/webpasswd.conf 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). tmpl_path = /var/www/cgi-bin/webpasswd/ 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. htpasswd_command = htpasswd 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. form_method = POST The configuration file should also contain a section for each htpasswd file it will be used to maintain, using the following format: [Descriptive Name] path = /system/path/to/passwdfile algorithm = {crypt|md5|sha|plain} B The default algorithm Apache uses is C under Linux, and C under Windows. 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. =head1 SECURITY It is *imperitive* that the Web::Passwd instance script itself be htpasswd protected, as it includes no access control mechanism. 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. Any htpasswd files to be managed with this utility MUST be owned by whatever user apache runs as. Usually, this is 'apache' or 'nobody'. =head1 COMPATABILITY 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. =head1 DEPENDENCIES A Perl version of 5.6.1 or higher is recommended, and the following modules are required: CGI::Application Config::Tiny HTML::Template =head1 AUTHOR Evan Kaufman, C<< >> =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc Web::Passwd =head1 ACKNOWLEDGEMENTS Written for BCD Music Group. =head1 COPYRIGHT & LICENSE Copyright 2007 Evan Kaufman, all rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut # we're a good little module 1;