#! /bin/bash
#
#  Copyright 2010 Carlos Alberto Lopez Perez <clopez@igalia.com>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#
#
#
# The columns of openvpn index file are defined as:
#
#define DB_type         0 /* Status of the certificate */
#define DB_exp_date     1 /* Expiry date */
#define DB_rev_date     2 /* Revocation date */
#define DB_serial       3 /* Serial No., index - unique */
#define DB_file         4
#define DB_name         5  /* DN, index - unique when active and not disabled */
#
#DB_type is defined as
#define DB_TYPE_REV    'R' /* Revoked */
#define DB_TYPE_EXP    'E' /* Expired */
#define DB_TYPE_VAL    'V' /* Valid */
#
#'E' is currently not used by "openssl ca", I guess because it is
#redundant to DB_exp_date. So expired certificates still have status 'V'
#DB_file currently is always 'unknown' and not used by "openssl ca". I
#guess the original idea was to store the filename of the generated
#certificate file here.
#The dates are in ASN1_UTCTIME-format.
#
#A UTCTIME string must have one of the following formats:
#
# YYMMDDhhmmssZ
# YYMMDDhhmmss+hh'mm'
# YYMMDDhhmmss-hh'mm'
#
# where
#
# Z indicates that local time is GMT,
# + indicates that local time is later than GMT, and
# - indicates that local time is earlier than GMT,
# hh' is the absolute value of the offset from GMT in hours, and
# mm' is the absolute value of the offset from GMT in minutes.
#
#
#
#Define global variables:
set -e
ERROR=1
NOERROR=0

# Load configuration
: ${VPN_ADMIN_CONF:="/etc/admintools/vpn-admin.conf"}

# Note: We don't source the config file directly because we want environment
#       config variables to have precedence over the ones on the config file.
if [[ -r "${VPN_ADMIN_CONF}" ]]; then
    ALLOWEDCONFS=( "VPNDIR" "SSLDIR" "INDEX" "SSLCFG" "JAILDIR" "EXAMPLESDIR" "CERTEXPIREDAYS" )
    for CONF in ${ALLOWEDCONFS[@]}; do
        VALUE=$(grep ^${CONF}= ${VPN_ADMIN_CONF} 2>/dev/null|sed "s/^${CONF}=//")
        # eval value
        eval VALUE=$VALUE
        # eval and assign variable
        eval : \${$CONF:=\"\$VALUE\"}
    done
fi

# Default values
: ${VPNDIR:="/etc/openvpn"}
: ${SSLDIR:="${VPNDIR}/ssl/"}
: ${INDEX:="${VPNDIR}/index.txt"}
: ${SSLCFG:="${SSLDIR}/openssl.cnf"}
: ${JAILDIR:="${VPNDIR}/rwjail/"}
: ${CERTEXPIREDAYS:="365"}
: ${EXAMPLESDIR:="/usr/share/doc/valkyrie-vpn-admin/examples/"}

if ! echo ${CERTEXPIREDAYS} | grep -qE "^[0-9]+$"; then
    echo "ERROR: CERTEXPIREDAYS should be a positive integer" 1>&2
    exit $ERROR
fi

# prints the help and exits sending the message sintaxis error to stderr
function help()
{
        echo "Sintaxis ERROR" 1>&2
        echo "Usage: "
        echo "$myname create  <full user name>"
        echo "$myname list    <full user name>"
        echo "$myname revoke  <certificate ID>"
        echo "$myname check-expiration"
        exit $ERROR
}


# Its an internal funciton
# prints an error message and exits
# its called by the other functions when something goes wrong

function abort()
{
  echo "ERROR: $*" 1>&2
  # Remove temp directory
  if [ -n "$RANDIR" ] && [ -r "/tmp/$RANDIR" ]; then
    rm -fr "$RANDIR"
  fi

  exit $ERROR
}




# will get the serial number of an user by its common name
# will return the serial number or void
# this function is used to revoke certificates
# and also its used also to check that the user dont have any valid cert
# before creating a new one
# it parses the index file and matchs non-revoked certs with the common name

function get-serial()
{

    if [ -r $INDEX ]; then
            xcontent=$(cat "$INDEX")
    elif [ -f $INDEX ]; then
            abort "The index file $INDEX exists but is not readable to the script."

    else
        abort "The file $INDEX does not exist."

    fi
    found=0;
    oldIFS=$IFS
        IFS=$'\x0a'
        for xline in $xcontent; do
        linevpname=$(echo "$xline"|cut -f6|cut -d/ -f6|cut -d= -f1 --complement)
        lineserial=$(echo $xline|cut -f4)
        linestatus=$(echo $xline|cut -f1)
        linedate=$(echo $xline|cut -f2|cut -f1 -dZ)
    if  [[ "$linevpname" = "$1" ]] && [[ "$linestatus" = "V" ]]; then
        found=1;
        xyear=$(echo $linedate|cut -c1-2)
        xmonth=$(echo $linedate|cut -c3-4)
        xday=$(echo $linedate|cut -c5-6)
        xhour=$(echo $linedate|cut -c7-8)
        xminute=$(echo $linedate|cut -c9-10)
        xsecond=$(echo $linedate|cut -c11-12)
        #please keep in mind that this trick will only work in the 20XX century
        #but i think I will be death when it fails so dont mind
        xdate=$(echo 20"$xyear"-"$xmonth"-"$xday"T"$xhour":"$xminute":"$xsecond")
        echo  "$xdate $lineserial"
    fi
        done
        IFS=$oldIFS

    if [ "$found" -eq 0 ]; then
        abort "Cant find any valid certificate for the user $(echo "$1"|sed "s/VPN\ //")"
    else
        exit $NOERROR
    fi

}

#Outputs:
#    Messages to to stderr
#    Expiring certificate, user name and date if no error occurs to stdout

# it parses the index file and finds non-revoked certs that will expire in the next 2 weeks

function check-expiration ()
{

    if [ -r $INDEX ]; then
        content=$(cat "$INDEX")
    elif [ -f $INDEX ]; then
        abort "The index file $INDEX exists but is not readable to the script."

    else
        abort "The file $INDEX does not exist."

    fi

    now="$(date --utc +%s)"
    later="$(expr "$now" + 1209600)"
    #add 14days*24hours*3600seconds
    #printf "$now $later"
    in2weeks="$(date --utc --date "1970-01-01 $later sec" "+%y%m%d%H%M%S")"
    inthismoment="$(date --utc --date "1970-01-01 $now sec" "+%y%m%d%H%M%S")"
    #get the dates in the index file data format ASN1_UTCTIME without the Z at ending
    oldIFS=$IFS
    IFS=$'\x0a'
    for line in $content; do
        estado="$(echo $line|cut -f1)"
        exprdt="$(echo $line|cut -f2|cut -f1 -dZ)"
        revkdt="$(echo $line|cut -f3)"
        serial="$(echo $line|cut -f4)"
        filenm="$(echo $line|cut -f5)"
        namedn="$(echo $line|cut -f6)"

        if  [[ "$inthismoment" < "$exprdt" ]] && [[ "$in2weeks" > "$exprdt" ]] && [[ "$estado" = "V" ]]; then
            #we have found a cert that will expire.
                #we will convert the date to a friendly format
                user="$(echo $namedn|cut -d/ -f6|cut -d\  -f1 --complement)"
                year="$(echo $exprdt|cut -c1-2)"
                month="$(echo $exprdt|cut -c3-4)"
                day="$(echo $exprdt|cut -c5-6)"
                hour="$(echo $exprdt|cut -c7-8)"
                minute="$(echo $exprdt|cut -c9-10)"
                second="$(echo $exprdt|cut -c11-12)"
                #please keep in mind that this trick will only work in the 20XX century
                #but i think I will be death when it fails so dont mind
                date="$(echo 20"$year"-"$month"-"$day"T"$hour":"$minute":"$second")"
                echo "$date $serial $user"
        fi
    done
    IFS=$oldIFS
    exit $NOERROR

}


#Outputs:
#    Messages to stderr
#    A .tar.gz file with the certs if no error occurs to stdout
#
#this function creates a new user certificate.
#the parameter must be checked for non-alphanumeric and the magic string VPN must me added
#see the end of this script to see how it is done
function create-cert()
{

if [ -r $SSLCFG ]; then
       NAMEVPN="$1"
elif [ -f $INDEX ]; then
       abort "The openssl cnf file $SSLCFG is not readable to the script."
else
    abort "The openssl cnf file $SSLCFG does not exist."
fi


subj="
C=ES
ST=Corunha
L=Corunha
O=Igalia, S.L.
CN=$NAMEVPN
OU=Departamento de sistemas
emailAddress=admin@igalia.com
"
#certificate information for openvpn

RANDIR=$(mktemp -d /tmp/rand.XXXXXX)
#tempdir where to store the files

#first we copy openvpn public certs and touch magic file
VPNTOUCH="$NAMEVPN"

touch "$JAILDIR"/"$VPNTOUCH" 1>&2 \
    || abort "touch $JAILDIR/$VPNTOUCH"

if [ -d "$EXAMPLESDIR" ]; then
    cp -r "$EXAMPLESDIR" $RANDIR/examples 1>&2 \
           || abort "copying $EXAMPLESDIR"
else
    mkdir -p $RANDIR/examples
fi

cp "$SSLDIR"/certs/rwca.crt $RANDIR 1>&2 \
    || abort "copying $SSLDIR/certs/rwca.crt"

cp "$VPNDIR"/rwshared.key $RANDIR 1>&2 \
    || abort "copying $VPNDIR/rwshared.key"

cd $VPNDIR || abort "When changing dir to $VPNDIR"
#the openssl.cnf relies on ./ dir to get config paths

#then generate the client ssl cert
dd if=/dev/random of="$RANDIR"/random bs=512c count=10 1>&2 \
    || abort "dd reading random data"

openssl genrsa -rand "$RANDIR"/random \
    -out "$RANDIR"/rwclient.key 4096 1>&2 \
    || abort "when creating openssl genrsa"

openssl req -config "$SSLCFG" -new -sha512 -key "$RANDIR"/rwclient.key \
    -out "$RANDIR"/rwclient.req  -batch \
        -subj  "$(echo -n "$subj" | tr "\n" "/")" 1>&2 \
        || abort "when creating openssl req"

echo -e "y\ny" | openssl ca -config "$SSLDIR"/openssl.cnf -days "$CERTEXPIREDAYS" \
    -cert "$SSLDIR"/certs/rwca.crt -keyfile "$SSLDIR"/private/rwca.key \
    -in "$RANDIR"/rwclient.req -out "$RANDIR"/rwclient.crt 1>&2 \
        || abort "when creating openssl ca"

# now pack all files into a .tar.gz
OLDPWD=$(pwd)

# cd into the dir to remove directory information in the .tar.gz
cd $RANDIR

# generate password [12,14] chars with genpass and output hint mnemotechnic rule
genpass=$(apg -ql -Ml -a0 -n1 -m12 -x14) || true
password=$(echo $genpass|awk '{print $1}');
passhint=$(echo $genpass|awk '{print $2}');


# Sanity check. Check that password was generated as expected
if [[ "${#password}" -gt "11" ]] && [[ "${#password}" -lt "15" ]] && \
   [[ "${#passhint}" -gt "15" ]]; then
    # Overwrite the certificate with a new one with the password
    openssl rsa -in rwclient.key -out rwclient.key -aes256 \
    -passout "pass:${password}"

    cat > password.txt <<EOF
The password for the rwclient.key file is:
  ${password}

You can use the following mnemotechnic rule:
  ${passhint}

Or you can change it with:
  openssl rsa -in rwclient.key -out rwclient_newpass.key -aes256 -passin pass:${password}

You can also remove the password from the cert (avoid if possible) with:
  openssl rsa -in rwclient.key -out rwclient_nopassword.key -passin pass:${password}
EOF


    tar cf - rwca.crt rwshared.key rwclient.key rwclient.crt examples password.txt |\
    gzip -f -9 || abort "when creating tar.gz in $RANDIR"

# Password was not generated as expected so dont add password.txt to tarball
else


    tar cf - rwca.crt rwshared.key rwclient.key rwclient.crt examples | gzip -f -9 \
    || abort "when creating tar.gz in $RANDIR"


fi


cd $OLDPWD


# remove temporal data
rm -fr $RANDIR \
    || abort "when deleting temporaly files in $RANDIR"

exit $NOERROR

}

#this function revoked an already issued and valid certificate
#
function revoke-cert
{


certfile=$(echo "$SSLDIR"/newcerts/"$1".pem)

if [ -r $certfile ]; then

    openssl ca -config "$SSLDIR"/openssl.cnf -cert "$SSLDIR"/certs/rwca.crt \
        -keyfile "$SSLDIR"/private/rwca.key -revoke "$certfile" 1>&2 \
        || abort "revoking user in openssl ca"
        echo "Sucesfully revoked."

elif [ -f $certfile ]; then

    abort "The certificate file $certfile exists but is not readable to the script."

else
    abort "The certificate file $certfile does not exist."
fi

exit $NOERROR

}

#
#main program
#it parses arguments and call functions
#
myname=${0##*/}



if [ ! $1 ]; then
    help;
else
    param=$1
fi


function lockfile_error {
    echo >&2 "Warning: $LOCKFILE present, not running $myname"
    exit 1
}

LOCKDIR="/var/lock/$myname"
LOCKFILE="$LOCKDIR/lock"
LOCKTEMP="$LOCKDIR/tmp"
mkdir -p "$LOCKDIR" || abort "mkdir $LOCKDIR"
touch "$LOCKTEMP" || abort "touch $LOCKTEMP"

if [ -e "$LOCKFILE" ]; then
    lockfile_error
else
    # Fixes race conditions
    ln "$LOCKTEMP" "$LOCKFILE" || lockfile_error
fi

# The traps are set after lockfile checking to avoid clean the lockfile
# when a lockfile_error happens
#
# Set a trap to avoid unclean fisnish
trap "abort 'Signal received. Execution aborted'" \
SIGINT SIGKILL SIGQUIT SIGSEGV SIGPIPE SIGALRM SIGTERM
# Set another trap for exit
trap "rm -f $LOCKFILE" EXIT


cd $VPNDIR || abort "When changing dir to $VPNDIR"
#the openssl.cnf relies on relative paths

export CA_EXPIRE=3650 KEY_EXPIRE=3650 KEY_OU="" KEY_NAME="" \
KEY_CN="VPN Corunha" PKCS11_MODULE_PATH="pdummy" PKCS11_PIN="" \
KEY_DIR=$VPNDIR KEY_SIZE=4096 KEY_COUNTRY="ES" KEY_PROVINCE="Corunha" \
KEY_CITY="" KEY_ORG="VPN Corunha" KEY_EMAIL="admin@igalia.com"
#This are the default values
if [ $param = "check-expiration" ]; then
    check-expiration;
fi

if [ ! $2 ]; then
    help;
fi

if [ $param = "revoke" ]; then
    revoke-cert "$2"
fi

if [[ $param != "create" ]] && [[ $param != "list" ]]; then
    help;
fi



#parses the username string and convert to a friendly format

username="";
for i in $(seq "2" "$#"); do
        shift
        username=$(echo -n "$username" "$1")
        #store all parameters into one
done

#first will try to friendly replace the most common spanish accents
username=$(echo "$username" | sed -e 's/\xc3\xa1/a/g' \
-e 's/\xc3\xa9/e/g' \
-e 's/\xc3\xad/i/g' \
-e 's/\xc3\xb3/o/g' \
-e 's/\xc3\xba/u/g' \
-e 's/\xc3\xb1/n/g' \
-e 's/\xc3\x81/A/g' \
-e 's/\xc3\x89/E/g' \
-e 's/\xc3\x8d/I/g' \
-e 's/\xc3\x93/O/g' \
-e 's/\xc3\x9a/U/g' \
-e 's/\xc3\x91/N/g' )
#now will remove remaining non-alphanumeric characters
username=$(echo $username|tr -cd '[:alnum:]\ ')
#now will capitalize first letter of each word
cusername="";
for word in $username; do
    firstletter=$(echo -n "${word:0:1}" | tr "[:lower:]" "[:upper:]")
        restofword=$(echo -n "${word:1}" | tr "[:upper:]" "[:lower:]")
        cusername=$(echo -n $cusername "${firstletter}${restofword}")
done
#now will add VPN keyword and remove all non alphanumeric characters and also
#replace two or more blank spaces with only one and also cut to max 64chars
username=$(echo VPN $cusername|tr -cd '[:alnum:]\ '|sed "s/\ *\ /\ /g"|cut -c1-63)
#Its important to pass the parameter in this way or something wrong can happen
#now we are ready to call functions



if [ $param = "create" ]; then
    create-cert "$username"
fi

if [ $param = "list" ]; then
    get-serial "$username"
fi

#ALL FUNCTIONS EXIT WHEN CALLED. THE RUNFLOW WILL NEVER REACH THIS POINT
#END OF FILE

