Managing Apache Passwords using shell script

One terrific feature of the Apache web server is that it offers built-in support for password-protected directories, even on a shared public server. It's a great way to have private, secure, and limited-access information on your website, whether you have a pay subscription service or you just want to ensure that family pictures are viewed only by family.

Standard configurations require that in the password-protected directory you manage a data file called .htaccess, which specifies the security "zone" name and, most importantly, points to a separate data file, which in turn contains the account name and password pairs that are used to validate access to the directory. Managing this file is not a problem, except that the only tool included with Apache for doing so is the primitive htpasswd program, which is run on the command line. Instead, this script, apm, one of the most complex and sophisticated scripts in this book, offers a password management tool that runs as a CGI script and lets you easily add new accounts, change the passwords on existing accounts, and delete accounts from the access list.

To get started, you will need a properly formatted .htaccess file to control access to the directory it's located within. For demonstration purposes, this file might look like the following:

$ cat .htaccess
AuthUserFile /web/intuitive/wicked/examples/protected/.htpasswd
AuthGroupFile /dev/null
AuthName "Sample Protected Directory"
AuthType Basic


require valid-user


A separate file, .htpasswd, contains all the account and password pairs. If this file doesn't yet exist, you'll need to create one, but a blank one is fine: Use touch.htpasswd and ensure that it's writable by the user ID that runs Apache itself (probably user nobody). Then we're ready for the script.

The Code
#!/bin/sh

# apm - Apache Password Manager. Allows the administrator to easily
# manage the addition, update, or deletion of accounts and passwords
# for access to a subdirectory of a typical Apache configuration (where
# the config file is called .htaccess).

echo "Content-type: text/html"
echo ""
echo "Apache Password Manager Utility"

myname="$(basename $0)"
temppwfile="/tmp/apm.$$"; trap "/bin/rm -f $temppwfile" 0
footer="apm-footer.html"
htaccess=".htaccess" # if you use a /cgi-bin, make sure this points
# to the correct .htaccess file!

# Modern versions of 'htpasswd' include a -b flag that lets you specify
# the password on the command line. If yours can do that, specify it
# here, with the '-b' flag:
# htpasswd="/usr/local/bin/htpasswd -b"
# Otherwise, there's a simple Perl rewrite of this script that is a good
# substitute, at http://www.intuitive.com/shellhacks/examples/httpasswd-b.pl

htpasswd="/web/intuitive/wicked/examples/protected/htpasswd-b.pl"
if [ "$REMOTE_USER" != "admin" -a -s $htpasswd ] ; then
echo "Error: you must be user admin to use APM."
exit 0
fi

# Now get the password filename from the .htaccess file

if [ ! -r "$htaccess" ] ; then
echo "Error: cannot read $htaccess file in this directory."
exit 0
fi

passwdfile="$(grep "AuthUserFile" $htaccess | cut -d\ -f2)"

if [ ! -r $passwdfile ] ; then
echo "Error: can't read password file: can't make updates."
exit 0
elif [ ! -w $passwdfile ] ; then
echo "Error: can't write to password file: can't update."
exit 0
fi

echo "

Apache Password Manager

"

action="$(echo $QUERY_STRING | cut -c3)"
user="$(echo $QUERY_STRING|cut -d\& -f2|cut -d= -f2|tr '[:upper:]' '[:lower:]')"

case "$action" in
A ) echo "

Adding New User $user

"
if [ ! -z "$(grep -E "^${user}:" $passwdfile)" ] ; then
echo "Error: user $user already appears in the file."
else
pass="$(echo $QUERY_STRING|cut -d\& -f3|cut -d= -f2)"
if [ ! -z "$(echo $pass | tr -d '[[:upper:][:lower:][:digit:]]')" ]
then
echo "Error: passwords can only contain a-z A-Z 0-9 ($pass)"
else
$htpasswd $passwdfile $user $pass
echo "Added!
"
fi
fi
;;
U ) echo "

Updating Password for user $user

"
if [ -z "$(grep -E "^${user}:" $passwdfile)" ] ; then
echo "Error: user $user isn't in the password file?"
echo "
";cat $passwdfile;echo "
"
echo "searched for "^${user}:" in $passwdfile"
else
pass="$(echo $QUERY_STRING|cut -d\& -f3|cut -d= -f2)"
if [ ! -z "$(echo $pass | tr -d '[[:upper:][:lower:][:digit:]]')" ]
then
echo "Error: passwords can only contain a-z A-Z 0-9 ($pass)"
else
grep -vE "^${user}:" $passwdfile > $temppwfile
mv $temppwfile $passwdfile
$htpasswd $passwdfile $user $pass
echo "Updated!
"
fi
fi
;;
D ) echo "

Deleting User $user

"
if [ -z "$(grep -E "^${user}:" $passwdfile)" ] ; then
echo "Error: user $user isn't in the password file?"
elif [ "$user" = "admin" ] ; then
echo "Error: you can't delete the 'admin' account."
else
grep -vE "^${user}:" $passwdfile > $temppwfile
mv $temppwfile $passwdfile
echo "Deleted!
"
fi
;;
esac

# Always list the current users in the password file...

echo "

"
echo ""
oldIFS=$IFS ; IFS=":" # change word split delimiter
while read acct pw ; do
echo ""
done < $passwdfile echo "
List "
echo "of all current users
$acct"
echo "[delete]
"
IFS=$oldIFS # and restore it

# Build optionstring with all accounts included
optionstring="$(cut -d: -f1 $passwdfile | sed 's/^//'|tr '\n' ' ')"

# And output the footer
sed -e "s/--myname--/$myname/g" -e "s/--options--/$optionstring/g" < $footer exit 0 How It Works There's a lot working together for this script to function. Not only do you need to have your Apache configuration (or equivalent) correct, but you need to have the correct entries in the .htaccess file and you need an .htpasswd file with (ideally) at least an entry for the admin user. The script itself extracts the htpasswd filename from the .htaccess file and does a variety of tests to sidestep common htpasswd error situations, including an inability for the script to write to the file. It also checks to ensure that the user is logged in as admin if the password file exists and is nonzero in size. All of this occurs before the main block of the script, the case statement. Processing Changes to .htpasswd The case statement ascertains which of three possible actions is requested (A = add a user, U = update a user record, and D = delete a user) and invokes the correct segment of code accordingly. The action and the user account on which to perform the action are specified in the QUERY_STRING variable (sent by the web browser to the server) as a=X&u=Y, where X is the action letter code and Y is the specified username. When a password is being changed or a user is being added, a third argument, p, is needed and sent to the script. For example, let's say I was adding a new user called joe, with the password knife. This action would result in the following QUERY_STRING being given to the script from the web server: a=A&u=joe&p=knife The script would unwrap this so that action was A, user was joe, and pass was knife. Then it would ensure that the password contains only valid alphabetic characters in the following test: if [ ! -z "$(echo $pass | tr -d '[[:upper:][:lower:][:digit:]]')" ] ; then echo "Error: passwords can only contain a-z A-Z 0-9 ($pass)" Finally, if all was well, it would invoke the htpasswd program to encrypt the password and add the new entry to the .htpasswd file: $htpasswd $passwdfile $user $pass Listing All User Accounts In addition to processing requested changes to the .htpasswd file, directly after the case statement this script also produces an HTML table that lists each user in the .htpasswd file, along with a [delete] link. After producing three lines of HTML output for the heading of the table, the script continues with the interesting code: oldIFS=$IFS ; IFS=":" # change word split delimiter while read acct pw ; do echo "$acct"
echo "[delete]
"
done < $passwdfile echo "" IFS=$oldIFS # and restore it This while loop reads the name and password pairs from the .htpasswd file through the trick of changing the input field separator (IFS) to a colon (and changing it back when done). Adding a Footer of Actions to Take The script also relies on the presence of an HTML file called apm-footer.html that contains quite a bit of code itself, including occurrences of the strings "--myname--" and "--options--", which are replaced by the current name of the CGI script and the list of users, respectively, as the file is output to stdout. sed -e "s/--myname--/$myname/g" -e "s/--options--/$optionstring/g" < $footer The $myname variable is processed by the CGI engine, which replaces the variable with the actual name of the script. The script itself builds the $optionstring variable from the account name and password pairs in the .htpasswd file: optionstring="$(cut -d: -f1 $passwdfile | sed 's/^//'|tr '\n' ' ')" And here's the HTML footer file itself, which provides the ability to add a user, update a user's password, and delete a user:





Password Manager Actions





add user:


password:








update


password:




delete









Running the Script
You'll most likely want to have this script in the same directory you're endeavoring to protect with passwords, although you can also put it in your cgi-bin directory: Just tweak the htpasswd value at the beginning of the script as appropriate. You'll also need an .htaccess file defining access permissions and an .htpasswd file that's at least zero bytes and writable, if nothing else.

Very helpful tip When you use apm, make sure that the first account you create is admin, so you can use the script upon subsequent invocations! There's a special test in the code that allows you to create the admin account if .htpasswd is empty.


The Result
The result of running the apm script is shown in Figure 9-1. Notice in the screen shot that it not only lists all the accounts, with a delete link for each, but also, in the bottom section, offers options for adding another account, changing the password of an existing account, deleting an account, or listing all the accounts.