#!/bin/bash

# 13 May 2020 Charles for Task #10020 "net1.iciti.av: create as Buster DomU on
#     xen7 to replace apt-cache1, dhcp2, dns-master, netflow2, openvpn2,
#     syslog2 and web2" note 26
#   * Creation based on version 3.0.4 /usr/share/easy-rsa/easyrsa

# Function call tree
#    +
#    |
#    +-- initialise
#    |   |
#    |   +-- usage
#    |   |
#    |   +-- do_pid
#    |   |
#    |   +-- set_var
#    |
#    |-- build_client_full
#    |
#    |-- gen_crl
#    |
#    |-- revoke
#    |
#    +-- finalise
#
# Utility functions called from various places:
#    ck_file ck_uint fct msg

#--------------------------
# Name: build_client_full
# Purpose:
#   * Within Aurinoco's PKI and usage, same as running version 3.0.4 
#     /usr/share/easy-rsa/easyrsa build-client-full <fqdn> pass|nopass
#   * Specifically create
#         $EASYRSA_PKI/private/<fqdn>.key 
#         $EASYRSA_PKI/reqs/<fqdn>.req 
#         $EASYRSA_PKI/issued/<fqdn>.crt
#--------------------------
function build_client_full {
    fct "${FUNCNAME[0]}" started
    local buf cmd i msg rc
    local content opts serial
    local crt_out crt_out_tmp key_out key_out_tmp req_out req_out_tmp

    # Create key and certificate in temporary files
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    msg I 'Creating key and certificate in temporary files'
    key_out_tmp="$tmp_dir/$fqdn.key" 
    req_out_tmp="$tmp_dir/$fqdn.req"
    opts=
    [[ $pass_ctl = nopass ]] && opts+=' -nodes'
    [[ $EASYRSA_BATCH ]] && opts+=' -batch'
    cmd=(
        "$EASYRSA_OPENSSL" req -utf8 -new -newkey
            $EASYRSA_ALGO:"$EASYRSA_ALGO_PARAMS"
            -config "$EASYRSA_SSL_CONF"
            -keyout "$key_out_tmp" -out "$req_out_tmp" $opts
    )
    buf=$("${cmd[@]}" 2>&1)
    rc=$?
    if ((rc!=0)); then
        msg="Command returned $rc.  stderr not available."
        msg I "$msg  Set envars and run command interactively to investigate"
        msg E "Command: ${cmd[*]}"
    fi

    # Move temporary files to permanent
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    msg I 'Moving key and certificate to permanent files'
    key_out="$EASYRSA_PKI/private/$fqdn.key" 
    cmd=(mv "$key_out_tmp" "$key_out")
    buf=$("${cmd[@]}" 2>&1)
    rc=$?
    if ((rc!=0)); then
        msg="Command: ${cmd[*]}"
        msg+=$msg_lf"rc: $rc"
        msg+=$msg_lf"Output:"
        msg E "$msg"$'\n'"$buf"
    fi
    msg I "Created key file $key_out"
    req_out="$EASYRSA_PKI/reqs/$fqdn.req"
    cmd=(mv "$req_out_tmp" "$req_out")
    buf=$("${cmd[@]}" 2>&1)
    rc=$?
    if ((rc!=0)); then
        msg="Command: ${cmd[*]}"
        msg+=$msg_lf"rc: $rc"
        msg+=$msg_lf"Output:"
        msg E "$msg"$'\n'"$buf"
    fi
    msg I "Created request file $req_out"

    # Populate $EASYRSA_PKI/serial with a unique serial number
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    for i in 1 2 3 4 5
    do
        "$EASYRSA_OPENSSL" rand -hex -out "$EASYRSA_PKI/serial" 16
        serial="$(cat "$EASYRSA_PKI/serial")" 
        check_serial="$("$EASYRSA_OPENSSL" ca -config "$EASYRSA_SSL_CONF" -status "$serial" 2>&1)" 
        case "$check_serial" in
            *"not present in db"*) break ;;
            *) continue ;;
        esac
    done

    # Populate the extensions file
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    msg I "Populating the extensions file $EASYRSA_TEMP_EXT"
    content=
    content+=$(< "$EASYRSA_EXT_DIR/COMMON")
    content+=$(< "$EASYRSA_EXT_DIR/client")
    [[ ${EASYRSA_CP_EXT:-} ]] && content+=$'\ncopy_extensions = copy'
    cmd=(echo "$content")
    buf=$("${cmd[@]}" 2>&1 > "$EASYRSA_TEMP_EXT")
    rc=$?
    if [[ $buf != '' ]]; then
        msg="Command: ${cmd[*]} > $EASYRSA_TEMP_EXT"
        msg+=$msg_lf"rc: $rc"
        msg+=$msg_lf"Output:"
        msg E "$msg"$'\n'"$buf"
    fi

    # Sign the request in a temporary file
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    msg I 'Signing the request in a temporary file'
    crt_out_tmp="$tmp_dir/crt_out" 
    cmd=(
        "$EASYRSA_OPENSSL"
            ca
            -batch
            -config "$EASYRSA_SSL_CONF"
            -days $EASYRSA_CERT_EXPIRE
            -extfile "$EASYRSA_TEMP_EXT"
            -in "$req_out"
            -out "$crt_out_tmp"
            -utf8
    )
    buf=$("${cmd[@]}" 2>&1)
    rc=$?
    if ((rc!=0)); then
        if [[ $buf != '' ]]; then
            msg="Command: ${cmd[*]}"
            msg+=$msg_lf"rc: $rc"
            msg+=$msg_lf"Output:"
            msg E "$msg"$'\n'"$buf"
        else
            msg="Command returned $rc.  stderr not available."
            msg I "$msg  Set envars and run command interactively to investigate"
            msg E "Command: ${cmd[*]}"
        fi
    fi

    # Publish the signed request
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~
    msg='Publishing the signed request'
    msg I "$msg (moving the temporary file to a permanent file)"
    crt_out="$EASYRSA_PKI/issued/$fqdn.crt"
    cmd=(mv "$crt_out_tmp" "$crt_out")
    buf=$("${cmd[@]}" 2>&1)
    rc=$?
    if ((rc!=0)); then
        msg="Command: ${cmd[*]}"
        msg+=$msg_lf"rc: $rc"
        msg+=$msg_lf"Output:"
        msg E "$msg"$'\n'"$buf"
    fi
    msg I "Created certificate file $crt_out"

    fct "${FUNCNAME[0]}" returning
}  # end of function build_client_full

#--------------------------
# Name: ck_file
# Purpose: for each file listed in the argument list: checks that it is 
#   * reachable and exists
#   * is of the type specified (block special, ordinary file or directory)
#   * has the requested permission(s) for the user
#   * optionally, is absolute (begins with /)
# Usage: ck_file [ path <file_type>:<permissions>[:[a]] ] ...
#   where 
#     file  is a file name (path)
#     file_type  is b (block special file), f (file) or d (directory)
#     permissions  is none or more of r, w and x
#     a  requests an absoluteness test (that the path begins with /)
#   Example:
#     buf=$(ck_file foo f:rw 2>&1)
#     if [[ $buf != '' ]]; then
#          msg W "$buf"
#          fct "${FUNCNAME[0]}" 'returning 1'
#          return 1
#     fi
# Outputs:
#   * For the first requested property each file does not have, a message to
#     stderr
#   * For the first detected programminng error, a message to
#     stderr
# Returns: 
#   0 when all files have the requested properties
#   1 when at least one of the files have the requested properties
#   2 when a programming error is detected
#--------------------------
function ck_file {

    local absolute_flag buf file_name file_type perm perms retval

    # For each file ...
    # ~~~~~~~~~~~~~~~~~
    retval=0
    while [[ $# -gt 0 ]]
    do  
        file_name=$1
        file_type=${2%%:*}
        buf=${2#$file_type:}
        perms=${buf%%:*}
        absolute=${buf#$perms:}
        [[ $absolute = $buf ]] && absolute=
        case $absolute in 
            '' | a )
                ;;
            * )
                echo "ck_file: invalid absoluteness flag in '$2' specified for file '$file_name'" >&2
                return 2
        esac
        shift 2

        # Is the file reachable and does it exist?
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        case $file_type in
            b ) 
                if [[ ! -b $file_name ]]; then
                    echo "file '$file_name' is unreachable, does not exist or is not a block special file" >&2
                    retval=1
                    continue
                fi  
                ;;  
            f ) 
                if [[ ! -f $file_name ]]; then
                    echo "file '$file_name' is unreachable, does not exist or is not an ordinary file" >&2
                    retval=1
                    continue
                fi  
                ;;  
            d ) 
                if [[ ! -d $file_name ]]; then
                    echo "directory '$file_name' is unreachable, does not exist or is not a directory" >&2
                    retval=1
                    continue
                fi
                ;;
            * )
                echo "Programming error: ck_file: invalid file type '$file_type' specified for file '$file_name'" >&2
                return 2
        esac

        # Does the file have the requested permissions?
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        buf="$perms"
        while [[ $buf ]]
        do
            perm="${buf:0:1}"
            buf="${buf:1}"
            case $perm in
                r )
                    if [[ ! -r $file_name ]]; then
                        echo "$file_name: no read permission" >&2
                        retval=1
                        continue
                    fi
                    ;;
                w )
                    if [[ ! -w $file_name ]]; then
                        echo "$file_name: no write permission" >&2
                        retval=1
                        continue
                    fi
                    ;;
                x )
                    if [[ ! -x $file_name ]]; then
                        echo "$file_name: no execute permission" >&2
                        retval=1
                        continue
                    fi
                    ;;
                * )
                    echo "Programming error: ck_file: invalid permisssion '$perm' requested for file '$file_name'" >&2
                    return 2
            esac
        done

        # Does the file have the requested absoluteness?
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        if [[ $absolute = a && ${file_name:0:1} != / ]]; then
            echo "$file_name: does not begin with /" >&2
            retval=1
        fi

    done

    return $retval

}  #  end of function ck_file

#--------------------------
# Name: ck_uint
# Purpose: checks for a valid unsigned integer
# Usage: ck_uint <putative uint>
# Outputs: none
# Returns: 
#   0 when $1 is a valid unsigned integer
#   1 otherwise
#--------------------------
function ck_uint {
    local regex='^[[:digit:]]+$'
    [[ $1 =~ $regex ]] && return 0 || return 1
}  #  end of function ck_uint

#--------------------------
# Name: do_pid
# Purpose:
#   * If can take an exclusive lock on a PID file
#         Writes a record to it identifying the current process
#     Else
#         Exits via writing an error message via msg
# Arguments:
#    $1 - the script's argument list
# Outputs: none except via msg()
# Global variables
#    Read:
#        pid_dir: read
#        my_name: read
#    Set:
#        pid_file_locked_flag: set
#        pid_fn: set
# Returns: 
#   0 on success
#   Does not return otherwise
# Usage notes:
#   * Caller should ensure the PID directory exists and has rwx permissions
#--------------------------
function do_pid {
    fct "${FUNCNAME[0]}" 'started'
    local pid_contents
    
    pid_fn=$pid_dir/$my_name.pid

    [[ -r $pid_fn ]] && pid_contents=$(< "$pid_fn")
    exec 9>>"$pid_fn"
    if flock --exclusive --timeout 1 9; then
        pid_file_locked_flag=$true
        msg D "Taken lock on PID file $pid_fn"
        echo "$(date '+%b %e %X') $my_name [$$]: arguments: $1" >> "$pid_fn"
        [[ ${log_fn%/*} != /dev ]] \
            && msg I "Created PID file $pid_fn containing:$msg_lf$(< "$pid_fn")"
    else
        msg E "Another instance is running.  Contents of $pid_fn: $pid_contents"
    fi  
        
    fct "${FUNCNAME[0]}" 'returning'
}  #  end of function do_pid

#--------------------------
# Name: fct
# Purpose: function call trace (for debugging)
# $1 - name of calling function 
# $2 - message.  If it starts with "started" or "returning" then the output is prettily indented
#--------------------------
function fct {

    if [[ ! $debugging_flag ]]; then
        return 0
    fi  

    fct_indent="${fct_indent:=}"

    case $2 in
        'started'* )
            fct_indent="$fct_indent  "
            msg D "$fct_indent$1: $2"
            ;;  
        'returning'* )
            msg D "$fct_indent$1: $2"
            fct_indent="${fct_indent#  }"  
            ;;  
        * ) 
            msg D "$fct_indent$1: $2"
    esac

}  # end of function fct

#--------------------------
# Name: finalise
# Purpose: cleans up and exits
# Arguments:
#    $1  return value
# Return code (on exit): 
#   The sum of zero plus
#      1 if any warnings
#      2 if any errors
#      4,8,16 unused
#      32 if terminated by a signal
#--------------------------
function finalise {
    fct "${FUNCNAME[0]}" "started with args $*"

    finalising_flag=$true

    # Remove old logs
    # ~~~~~~~~~~~~~~~
    if [[ ${log_fn%/*} != /dev ]]; then
        msg I 'Removing old logs'
        buf=$(
            find "$log_dir" \
                -maxdepth 1 \
                -regextype posix-egrep \
                -regex ".*/$log_fn_pat" \
                -mtime +28 \
                -delete \
                2>&1
        )
        [[ $buf != '' ]] && msg W "Problem removing old logs: $buf"
    fi

    # Interrupted?  Message and exit return value
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    my_retval=0
    if ck_uint "${1:-}" && (($1>128)); then
        ((my_retval+=32))
        case $1 in
            129 )
                buf=SIGHUP
                ;;
            130 )
                buf=SIGINT
                ;;
            131 )
                buf=SIGQUIT
                ;;
            132 )
                buf=SIGILL
                ;;
            134 )
                buf=SIGABRT
                ;;
            135 )
                buf=SIGBUS
                ;;
            136 )
                buf=SIGFPE
                ;;
            138 )
                buf=SIGUSR1
                ;;
            139 )
                buf=SIGSEGV
                ;;
            140 )
                buf=SIGUSR2
                ;;
            141 )
                buf=SIGPIPE
                ;;
            142 )
                buf=SIGALRM
                ;;
            143 )
                buf=SIGTERM
                ;;
            146 )
                buf=SIGCONT
                ;;
            147 )
                buf=SIGSTOP
                ;;
            148 )
                buf=SIGTSTP
                ;;
            149 )
                buf=SIGTTIN
                ;;
            150 )
                buf=SIGTTOU
                ;;
            151 )
                buf=SIGURG
                ;;
            152 )
                buf=SIGCPU
                ;;
            153 )
                buf=SIGXFSZ
                ;;
            154 )
                buf=SIGVTALRM
                ;;
            155 )
                buf=SIGPROF
                ;;
            * )
                msg E "${FUNCNAME[0]}: programming error: \$1 ($1) not serviced"
                ;;
        esac
        interrupt_flag=$true
        msg="Finalising on $buf"
        msg E "$msg"    # Returns because finalising_flag is set
    fi

    # Exit return value adjustment
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    if [[ $warning_flag ]]; then
        msg I "There was at least one WARNING"
        ((my_retval+=1))
    fi
    if [[ $error_flag ]]; then
        msg I "There was at least one ERROR"
        ((my_retval+=2))
    fi
    [[ $interrupt_flag ]] && msg I 'There was at least one interrupt'
    if ((my_retval==0)) && ((${1:-0}!=0)); then
        msg E 'There was an error not reported in detail (probably by ... || finalise 1)'
        my_retval=2
    fi
    [[ ${log_fn%/*} != /dev ]] \
        && msg I "Exiting with return value $my_retval"

    # Remove PID file
    # ~~~~~~~~~~~~~~~
    [[ $pid_file_locked_flag ]] && rm "$pid_fn"

    # Temporary directory removal
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~
    tmp_dir_regex="^/tmp/$my_name\..{6}$"
    [[ $tmp_dir_created_flag \
        && ${tmp_dir:-} =~ $tmp_dir_regex \
    ]] && rm -fr "$tmp_dir"

    # Exit
    # ~~~~
    fct "${FUNCNAME[0]}" 'exiting'
    exit $my_retval
}  # end of function finalise

#--------------------------
# Name: gen_crl
# Purpose:
#--------------------------
function gen_crl {
    fct "${FUNCNAME[0]}" started
    local buf cmd msg rc
    local out_file out_file_tmp

    # Create CRL in temporary file
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    out_file_tmp=$tmp_dir/crl.pem
    msg I "Creating CRL in temporary file $out_file_tmp"
    cmd=(
        "$EASYRSA_OPENSSL"
            ca
            -config "$EASYRSA_SSL_CONF"
            -gencrl
            -out "$out_file_tmp"
            -utf8
    )
    buf=$("${cmd[@]}" 2>&1)
    rc=$?
    if ((rc!=0)); then
        msg="Command: ${cmd[*]}"
        msg+=$msg_lf"rc: $rc"
        msg+=$msg_lf"Output:"
        msg E "$msg"$'\n'"$buf"
    fi

    # Move CRL to temporary file
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~
    out_file="$EASYRSA_PKI/crl.pem"
    msg I "Moving CRL to permanent file $out_file"
    cmd=(mv "$out_file_tmp" "$out_file")
    buf=$("${cmd[@]}" 2>&1)
    rc=$?
    if ((rc!=0)); then
        msg="Command: ${cmd[*]}"
        msg+=$msg_lf"rc: $rc"
        msg+=$msg_lf"Output:"
        msg E "$msg"$'\n'"$buf"
    fi
    msg I "Moved CRL to permanent file $out_file"

    fct "${FUNCNAME[0]}" returning
}  # end of function gen_crl

#--------------------------
# Name: initialise
# Purpose: sets up environment, parses command line, reads config file
#--------------------------
function initialise {
    local args buf emsg opt opt_l_flag 
    local -r fqdn_regex='^[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$'
    local -r pass_ctl_regex='^(nopass|pass)$'

    # Configure shell environment
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~
    export LANG=en_GB.UTF-8
    export LANGUAGE=en_GB.UTF-8
    for var_name in LC_ADDRESS LC_ALL LC_COLLATE LC_CTYPE LC_IDENTIFICATION \
        LC_MEASUREMENT LC_MESSAGES LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER \
        LC_TELEPHONE LC_TIME 
    do
        unset $var_name
    done

    export PATH=/usr/sbin:/sbin:/usr/bin:/bin
    IFS=$' \n\t'
    set -o nounset
    shopt -s extglob            # Enable extended pattern matching operators
    unset CDPATH                # Ensure cd behaves as expected
    umask 022

    # Initialise some global logic variables
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    readonly false=
    readonly true=true
    
    debugging_flag=$false
    error_flag=$false
    finalising_flag=$false
    interrupt_flag=$false
    logging_flag=$false
    pid_file_locked_flag=$false
    tmp_dir_created_flag=$false
    warning_flag=$false

    # Initialise some global string variables
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    readonly my_name=${0##*/}

    readonly log_dir=/var/log/$my_name
    readonly log_file_name_date_format='%Y-%m-%d'
    readonly log_fn_pat="$my_name.[^.]+\.log\$"
    readonly log_msg_timestamp_format='%H:%M:%S'
    readonly msg_lf=$'\n    '              # Message linefeed and indent
    readonly pid_dir=/run

    # Parse command line
    # ~~~~~~~~~~~~~~~~~~
    args=("$@")
    args_org="$*"
    emsg=
    log_fn=/dev/stderr
    opt_l_flag=$false
    while getopts :dhl opt "$@"
    do
        case $opt in
            d )
                debugging_flag=$true
                ;;
            h )
                debugging_flag=$false
                usage verbose
                exit 0
                ;;
            l )
                opt_l_flag=$true
                log_fn=/dev/tty
                ;;
            : )
                emsg+=$msg_lf"Option $OPTARG must have an argument"
                ;;
            * )
                emsg+=$msg_lf"Invalid option '-$OPTARG'"
        esac
    done

    # Check for mandatory options missing
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # There are no mandatory options

    # Test for mutually exclusive options
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # There are no mutually exclusive options

    # Parse arguments
    # ~~~~~~~~~~~~~~~
    shift $(($OPTIND-1))
    case ${1:-} in
        build-client-full)
            command=build-client-full
            shift
            fqdn=${1:-}
            shift
            pass_ctl=${1:-}
            ;;
        gen-crl)
            command=gen-crl
            ;;
        revoke)
            command=revoke
            shift
            fqdn=${1:-}
            ;;
        *)
            [[ ${1:-} != '' ]] && echo "Invalid argument '$1'" >&2
            debugging_flag=$false
            usage
            exit 1
    esac
    [[ -v fqdn && ! $fqdn =~ $fqdn_regex ]] \
        && emsg+=$msg_lf"Invalid FQDN '$fqdn' (does not match $fqdn_regex)"
    [[ -v pass_ctl && ! $pass_ctl =~ $pass_ctl_regex ]] \
        && emsg+=$msg_lf"Invalid argument '$pass_ctl' (not nopass or pass)"

    # Test for incompatible argument and option combination
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    [[ ${command:-} != build-client-full && $opt_l_flag ]] \
        && emsg+=$msg_lf"Option -l is not valid with command $command"

    # Test for extra arguments
    # ~~~~~~~~~~~~~~~~~~~~~~~~
    shift
    if [[ $* != '' ]]; then
        emsg+=$msg_lf"Invalid extra argument(s) '$*'"
    fi

    # Test for running interactively
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    if [[ "$-#*i" = "$-" ]]; then
        [[ ${pass_ctl:-} = pass ]] \
            && emsg+=$msg_lf'argument pass requires running interactively'
        [[ ${command:-} != build-client-full ]] \
            && emsg+=$msg_lf"command $command requires running interactively"
    fi

    # Report any errors
    # ~~~~~~~~~~~~~~~~~
    if [[ $emsg != '' ]]; then
        msg E "$emsg"
    fi

    # Set traps
    # ~~~~~~~~~
    trap 'finalise 129' 'HUP'
    trap 'finalise 130' 'INT'
    trap 'finalise 131' 'QUIT'
    trap 'finalise 132' 'ILL'
    trap 'finalise 134' 'ABRT'
    trap 'finalise 135' 'BUS'
    trap 'finalise 136' 'FPE'
    trap 'finalise 138' 'USR1'
    trap 'finalise 139' 'SEGV'
    trap 'finalise 140' 'USR2'
    trap 'finalise 141' 'PIPE'
    trap 'finalise 142' 'ALRM'
    trap 'finalise 143' 'TERM'
    trap 'finalise 146' 'CONT'
    trap 'finalise 147' 'STOP'
    trap 'finalise 148' 'TSTP'
    trap 'finalise 149' 'TTIN'
    trap 'finalise 150' 'TTOU'
    trap 'finalise 151' 'URG'
    trap 'finalise 152' 'XCPU'
    trap 'finalise 153' 'XFSZ'
    trap 'finalise 154' 'VTALRM'
    trap 'finalise 155' 'PROF'

    # Create and lock the PID file
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    buf=$(ck_file "$pid_dir" d:rwx 2>&1)
    [[ $buf != '' ]] && msg E "$buf"
    do_pid "$(printf '%q ' "${args[@]}")"

    # Create temporary directory
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~
    # If the mktemplate is changed, tmp_dir_regex in the finalise function
    # may also need to be changed.
    buf=$(mktemp -d "/tmp/$my_name.XXXXXX" 2>&1)
    if (($?==0)); then 
        tmp_dir=$buf
        tmp_dir_created_flag=$true
        chmod 700 "$tmp_dir"
    else
        msg E "Unable to create temporary directory:$buf"
    fi

    # Export all EASYRSA* variables, defaulting any which are not set
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Note: copied from version 3.0.4 /usr/share/easy-rsa/easyrsa and customised
    set_var EASYRSA              "${0%/*}"
    set_var EASYRSA_OPENSSL      openssl
    set_var EASYRSA_PKI          "$PWD/pki"
    set_var EASYRSA_DN           cn_only
    set_var EASYRSA_REQ_COUNTRY  IN
    set_var EASYRSA_REQ_PROVINCE 'Tamil Nadu'
    set_var EASYRSA_REQ_CITY     'Auroville'
    set_var EASYRSA_REQ_ORG      'Aurinoco Systems'
    set_var EASYRSA_REQ_EMAIL    support@aurinoco.net
    set_var EASYRSA_REQ_OU       ''
    set_var EASYRSA_ALGO         rsa
    set_var EASYRSA_KEY_SIZE     2048
    set_var EASYRSA_CURVE        secp384r1
    set_var EASYRSA_EC_DIR       "$EASYRSA_PKI/ecparams"
    set_var EASYRSA_CA_EXPIRE    3650
    set_var EASYRSA_CERT_EXPIRE  3650
    set_var EASYRSA_CRL_DAYS     180
    set_var EASYRSA_NS_SUPPORT   no
    set_var EASYRSA_NS_COMMENT   'Easy-RSA Generated Certificate'
    set_var EASYRSA_TEMP_CONF    "$EASYRSA_PKI/openssl-easyrsa.temp"
    set_var EASYRSA_TEMP_EXT     "$EASYRSA_PKI/extensions.temp"
    set_var EASYRSA_TEMP_FILE_2  ''
    set_var EASYRSA_TEMP_FILE_3  ''
    set_var EASYRSA_REQ_CN       "${fqdn:-}"
    set_var EASYRSA_DIGEST       sha256

    # Detect openssl config, preferring EASYRSA_PKI over EASYRSA
    if [ -f "$EASYRSA_PKI/openssl-easyrsa.cnf" ]; then 
        set_var EASYRSA_SSL_CONF    "$EASYRSA_PKI/openssl-easyrsa.cnf"
    else    set_var EASYRSA_SSL_CONF    "$EASYRSA/openssl-easyrsa.cnf"
    fi   

    # Same as above for the x509-types extensions dir
    if [ -d "$EASYRSA_PKI/x509-types" ]; then 
        set_var EASYRSA_EXT_DIR     "$EASYRSA_PKI/x509-types"
    else    set_var EASYRSA_EXT_DIR     "$EASYRSA/x509-types"
    fi   

    # EASYRSA_ALGO_PARAMS must be set depending on selected algo
    if [ "ec" = "$EASYRSA_ALGO" ]; then 
        EASYRSA_ALGO_PARAMS="$EASYRSA_EC_DIR/${EASYRSA_CURVE}.pem"
    elif [ "rsa" = "$EASYRSA_ALGO" ]; then 
        EASYRSA_ALGO_PARAMS="${EASYRSA_KEY_SIZE}"
    else 
        die "Alg '$EASYRSA_ALGO' is invalid: must be 'rsa' or 'ec'"
    fi   

    # Setting OPENSSL_CONF prevents bogus warnings (especially useful on win32)
    export OPENSSL_CONF="$EASYRSA_SSL_CONF"

    # Set EASYRSA_BATCH
    # ~~~~~~~~~~~~~~~~~
    # 1 when not running interactively else empty
    # Note: extra to version 3.0.4 /usr/share/easy-rsa/easyrsa
    [[ "$-#*i" = "$-" ]] && export EASYRSA_BATCH= || export EASYRSA_BATCH=1 

    msg D "EASYRSA envars"$'\n'"$(env|grep EASYRSA|sort)" 

    fct "${FUNCNAME[0]}" 'returning'
}  # end of function initialise

#--------------------------
# Name: msg
# Purpose: generalised messaging interface
# Arguments:
#    $1 class: D, E, I or W indicating Debug, Error, Information or Warning
#    $2 message text
# Global variables read:
#     debugging_flag
# Global variables written:
#     error_flag
#     warning_flag
# Output: information messages to stdout; the rest to stderr
# Returns: 
#   Does not return (calls finalise) when class is E for error
#   Otherwise returns 0
#--------------------------
function msg {
    local buf class logger_flag message_text prefix

    # Process arguments
    # ~~~~~~~~~~~~~~~~~
    class="${1:-}"
    message_text="${2:-}"

    # Class-dependent set-up
    # ~~~~~~~~~~~~~~~~~~~~~~
    logger_flag=$false
    case "$class" in  
        D ) 
            [[ ! $debugging_flag ]] && return
            prefix='DEBUG: '
            ;;  
        E ) 
            error_flag=$true
            prefix='ERROR: '
            logger_flag=$true
            ;;  
        I ) 
            prefix=
            ;;  
        W ) 
            warning_flag=$true
            prefix='WARN: '
            ;;  
        * ) 
            msg E "msg: invalid class '$class': '$*'"
    esac
    message_text="$prefix$message_text"

    # Write to syslog
    # ~~~~~~~~~~~~~~~
    if [[ $logger_flag ]]; then
        buf=$(logger -t "$my_name[$$]" -- "$message_text" 2>&1)
        [[ $buf != '' ]] && msg W "${FUNCNAME[0]}: problem writing to syslog: $buf"
    fi

    # Write to log
    # ~~~~~~~~~~~~
    message_text="$(date "+$log_msg_timestamp_format") $message_text"
    [[ ${log_fn%/*} != /dev ]] \
        && log_fn="$log_dir/$my_name-$(date +$log_file_name_date_format).log"
    if [[ $class = I ]]; then
        echo "$message_text" >> "$log_fn"
    else
        echo "$message_text" >> "$log_fn"
        if [[ $class = E ]]; then
            [[ ! $finalising_flag ]] && finalise 1 
        fi
    fi  

    return 0
}  #  end of function msg

#--------------------------
# Name: revoke
# Purpose:
#--------------------------
function revoke {
    fct "${FUNCNAME[0]}" started
    local buf cmd msg rc
    local crt_in dn

    # Check certificate file to be revoked
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    crt_in="$EASYRSA_PKI/issued/$fqdn.crt"
    msg I "Checking certificate file $crt_in"
    buf=$(ck_file "$crt_in" f:r)
    [[ $buf != '' ]] && msg E "$buf"
    "$EASYRSA_OPENSSL" x509 -in "$crt_in" -noout
    rc=$?
    if ((rc!=0)); then
        msg="Command: ${cmd[*]}"
        msg+=$msg_lf"rc: $rc"
        msg+=$msg_lf"Output:"
        msg E "$msg"$'\n'"$buf"
    fi

    # Ask user to confirm
    # ~~~~~~~~~~~~~~~~~~~
    dn=$("$EASYRSA_OPENSSL" x509 -in "$crt_in" -noout -subject -nameopt \
        multiline \
        | grep commonName \
        | sed 's/.*= //'
    )
    while true
    do
        read -p "Revoke $dn? (y to continue, Ctrl+c to abort) > "
        [[ $REPLY = y ]] && break
        echo "Invalid answer '$REPLY'"
    done

    # Revoke
    # ~~~~~~
    cmd=(
        "$EASYRSA_OPENSSL"
            ca
            -config "$EASYRSA_SSL_CONF"
            -revoke "$crt_in"
            -utf8
    )
    buf=$("${cmd[@]}" 2>&1)
    rc=$?
    if ((rc!=0)); then
        msg="Command: ${cmd[*]}"
        msg+=$msg_lf"rc: $rc"
        msg+=$msg_lf"Output:"
        msg E "$msg"$'\n'"$buf"
    fi

    # Instruct user
    # ~~~~~~~~~~~~~
    msg=IMPORTANT!!!
    msg+=$msg_lf'Revocation was successful. You must run gen-crl and upload a'
    msg+=$msg_lf'CRL to your infrastructure to prevent the revoked certificate'
    msg I "$msg$msg_lf"'from being accepted'

    fct "${FUNCNAME[0]}" returning
}  # end of function revoke

#--------------------------
# Name: set_var
# Purpose: 
#     variable assignment by indirection when undefined; merely exports
#     the variable when it is already defined (even if currently null)
#     Sets $1 as the value contained in $2 and exports (may be blank)
# Note: commands copied verbatim from version 3.0.4 /usr/share/easy-rsa/easyrsa
#--------------------------
function set_var {
    var=$1
    shift
    value="$*"
    eval "export $var=\"\${$var-$value}\""
} #  end of function set_var

#--------------------------
# Name: usage
# Purpose: prints usage message
#--------------------------
function usage {
    local msg usage

    # Build the messages
    # ~~~~~~~~~~~~~~~~~~
    usage='usage:'
    usage+=$'\n  '"$my_name [-d] [-h] [-l] build-client-full <fqdn> pass|nopass"
    usage+=$'\n  '"$my_name [-d] [-h] gen-crl"
    usage+=$'\n  '"$my_name [-d] [-h] revoke <fqdn>"
    msg='  where:'
    msg+=$'\n    -d turns debugging on'
    msg+=$'\n    -h prints this help and exits'
    msg+=$'\n    -l logs to /dev/tty which is normally the terminal'
    msg+=$'\n       Default: file under'" $log_dir"
    msg+=$'\n    build-client-full'
    msg+=$'\n       Generate a keypair and sign locally for client <fqdn>'
    msg+=$'\n       nopass: with a password.  Prompts for password'
    msg+=$'\n       pass: without a password'
    msg+=$'\n    gen-crl'
    msg+=$'\n       Generate a certificate revocation list'
    msg+=$'\n    revoke'
    msg+=$'\n       Revoke the certificate for <fqdn>'

    # Display the message(s)
    # ~~~~~~~~~~~~~~~~~~~~~~
    echo "$usage" >&2
    if [[ ${1:-} != 'verbose' ]]; then
        echo "(use -h for help)" >&2
    else
        echo "$msg" >&2
    fi
}  # end of function usage

#--------------------------
# Name: main
# Purpose: the main sequence; execution starts here
#--------------------------
initialise "${@:-}"
case $command in
    build-client-full) build_client_full;;
    gen-crl) gen_crl;;
    revoke) revoke;;
esac
finalise 0
