#!/bin/bash

# Copyright (C) 2020 Timothe Litt, litt _at__ acm ddot org
#
# Handle events triggered by Apache httpd's mod_md
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

MDE_VERSION="0.0.4"

MDE_SELF="$(basename $0)"

export MDE_INITIAL_UMASK="umask $(umask -S)"
export MDE_SAFE_UMASK="umask u=rw,g=r,o="
export MDE_WWW_UMASK="umask u=rw,g=r,o=r"

export E_OK=0 E_FAILED=1 E_RETRY=2

MDE_EVENT=
MDE_BINDIR="/usr/local/sbin"

# Under SeLinux, invoked in context system_u:system_r:httpd_t:s0
# User/Group are httpd's except for the 'installed' event, which
# runs as 'root'.
#
# Any output to stderr is logged at "info" level to the httpd error log

# Private log

MDE_LOG=
#MDE_LOG="/var/log/httpd/md_events.log"

function info() {
    local rv="$?"
    cat <<EOF
$*
EOF
    [ -n "$MDE_LOG" ] && cat >>$MDE_LOG <<EOF
$*
EOF
    return $rv
}
function debug() {
    local rv="$?"
    if [ -n "$MDE_DEBUG" ]; then
    cat <<EOF
$*
EOF
    [ -n "$MDE_LOG" ] && cat >>$MDE_LOG <<EOF
$*
EOF
    fi
    return $rv
}
function error() {
    local rv="$1"
    shift
    cat >&2 <<EOF
$*
EOF
    [ -n "$MDE_LOG" ] && cat >>$MDE_LOG <<EOF
$*
EOF
    exit $rv
}

# Try to find httpd

if ! which httpd >/dev/null 2>&1; then
    export PATH="/usr/local/sbin:/sbin:/usr/local/bin:/usr/bin:$PATH"
    if ! which httpd >/dev/null 2>&1; then
        error $E_FAILED "Can't find httpd - please ensure it is in PATH"
    fi
fi

# Default config file to md_events.conf in (default) ServerRoot
MDE_HTTPD_ROOT="$(httpd -V -f /dev/null 2>/dev/null | sed -ne 's/^ *-D HTTPD_ROOT="\([^\"]*\)"/\1/p;')"
if ! [ -n "$MDE_HTTPD_ROOT" ]; then
    error $E_FAILED "Can't find httpd - please ensure it is in PATH"
fi

MDE_CFG="$MDE_HTTPD_ROOT/md_events.conf"
MDE_CFGS=

# HTTPD's (default) config file
MDE_HTTPD_CFG="$MDE_HTTPD_ROOT/$(httpd -V -f /dev/null 2>/dev/null | sed -ne 's/^ *-D SERVER_CONFIG_FILE="\([^\"]*\)"/\1/p;')"

function usage() {
    cat <<EOF
$MDE_SELF - Handler for mod_md's MDMessageCmd

Usage: $MDE_SELF [-h -H: I -i: -c: -d -L: -l -m: -r: -u:] event domain [data...]
V$MDE_VERSION

Options:
 h            Usage
 H:string     A user-defined string that can identify an HTTPD instance
 I            Install, placing executable in $MDE_BINDIR
 i: path      Install, placing executable in path
 l            When installing, link to file (rather than copy) Apache currently rejects.
 L:file       Log all output to file
 m:event      Send mail when event occurs. (Can specify more than once or use "all".)
 c: path      Configuration file name (${MDE_CFG}
 d            Enable debugging messages
 r: path      Root directory for per-domain configurations
 u:user@dom   Mail selected notifications to user@dom (webmaster@\$domain)

 events are: all, renewed, installed, expiring, errored, ocsp-renewed, ocsp-errored,
             setup, teardown, setup-http-01 teardown-http-01

To install or update md_events, use
  $MDE_SELF -I or -i
  Applicable options: l c r u
  Creates configuration file (-c), sample domain configuration (example.net.conf in -r)
  -r is recorded in the configuration file.
  The installed version is smaller because it does not contain the installation code.

To configure httpd:
  In httpd configuration, add MDMessageCmd [-HLmc] $MDE_BINDIR/$MDE_SELF to the mod_md configuration.
    -m expiring is recommended.  Other events may depend on circumstance.
    -H is optional. It can be used to distinguish multiple instances of httpd without
       requiring separate configuration files.
    -c is required if md_events.conf is not in the DEFAULT ServerRoot
    -L -u are optional - if permanent, can be in md_events.conf.

To configure md_events:
 Mmodify md_events.conf if needed - see the comments.

To configure for a managed domain:
  The default action is to send mail (per -m).  Nothing more is required.

  If you want customized actions for a domain:
    Copy the sample domain configuration to yourdomain.conf.
    Enable one or more of the event handlers, and/or write your own.
    For the "installed" event, you can use the public functions to copy files for
    other applications, create separate or combined files, and/or restart applications.
    You can customize or suppress e-mail by modifying the MDE_MESSAGE variable.

    You can also customize for a validation target (e.g. for a dns-01 or remote http-01
    challenge to a specific host) by creating a <target-name>.conf file.

    If a hander or variable is defined at multiple levels, the most specific is used.

Bug reports/issues to https://github.com/tlhackque/mod_md
EOF

    exit $E_OK;
}

# Command arguments

MDE_EVENT=
MDE_DEBUG=
MDE_HTTPD=
MDE_LINK=
MDE_MAIL=
MDE_MAILTO=

while getopts "hH:Ii:lm:c:dr:u:" opt; do
    case "$opt" in
        c)
            MDE_CFG="$OPTARG"
            ;;
        d)
            MDE_DEBUG="true"
            ;;
        H)
            MDE_HTTPD="$OPTARG"
            ;;
        I)
            MDE_EVENT="_install"
            ;;
        i)
            MDE_BINDIR="$OPTARG"
            MDE_EVENT="_install"
            ;;
        l)
            MDE_LINK="true"
            ;;
        m)
            MDE_MAIL="$MDE_MAIL $OPTARG"
            ;;
        r)
            MDE_CFGS="$OPTARG"
            ;;
        u)
            MDE_MAILTO="$OPTARG"
            ;;
        h)
            usage
            ;;
        *)
            error $E_FAILED "$MDE_SELF -h for usage"
            ;;
    esac
done
shift $((OPTIND-1))

# Note: any "optional arguments" included in the MDMessageCmd directive that
# are not parsed by getopt will be before MDE_EVENT, MDE_DOMAIN, and MD_STORE

[ -z "$MDE_EVENT" ] && MDE_EVENT="$1"
MDE_DOMAIN="$2"
shift 2
export -a MDE_DATA=("$@")

# Environment variables passed from mod_md

[ -z "$MD_STORE" ] && MD_STORE="$MDE_HTTPD_ROOT/md"

[ -z "$MD_MDOMAIN" ] && MD_MDOMAIN="$MDE_DOMAIN"
export MD_STORE MD_VERSION MD_MDOMAIN


# Get existing configuration before applying defaults.

[ -f "$MDE_CFG" ] && . "$MDE_CFG" "$MDE_HTTPD"

if [ -z "$MDE_CFGS" ]; then
    # Domain configs are usually in <the config basename> .d/
    MDE_CFGS="$(dirname "$MDE_CFG")/$(basename "$MDE_CFG" ".conf").d"
fi

# Set MDE_NOTIFY if current event was included in -m

MDE_NOTIFY=
for MDE_ACT in $MDE_MAIL; do
    if [ "$MDE_ACT" == "$MDE_EVENT" -o "$MDE_ACT" == "all" ]; then
       MDE_NOTIFY="true"
       break
    fi
done
unset MDE_ACT

# MAILTO - if not set explicitly, perhaps from crontab.  Else default to webmaster.

[ -z "$MDE_MAILTO" ] && MDE_MAILTO="$MAILTO"
[ -z "$MDE_MAILTO" ] && MDE_MAILTO="webmaster@$MD_MDOMAIN"

# Handle install - bracketed installer code removes itself.

if [ "$MDE_EVENT" == "_install" ]; then
#-----BEGIN INSTALLER
    if ! [ -d "$MDE_BINDIR" ]; then
        error $E_FAILED "Install: $MDE_BINDIR does not exist"
    fi
    if [ -n "$MDE_LINK" ]; then
        # Useful in development - httpd refuses to follow (need to find out why)
        if ln -sf "$(readlink -e $0)" $MDE_BINDIR/ &&
            chown root.root $MDE_BINDIR/$MDE_SELF  &&
            chmod u+x,g+x,o= $MDE_BINDIR/$MDE_SELF; then
            info "Installed executable as $MDE_BINDIR/$MDE_SELF"
        else
            error $E_FAILED "Failed to install executable"
        fi
    else
        if sed -e'/^#-----BEGIN INSTALLER$/,/^#-----END INSTALLER$/d' "$0" >"$MDE_BINDIR/$MDE_SELF" &&
            touch --reference=$0 $MDE_BINDIR/$MDE_SELF &&
            chown root.root $MDE_BINDIR/$MDE_SELF &&
            chmod u+x,g+x,o= $MDE_BINDIR/$MDE_SELF; then
            info "Installed executable as $MDE_BINDIR/$MDE_SELF"
        else
            error $E_FAILED "Failed to install executable"
        fi
    fi
    fname="$MDE_CFG"
    [ -f "$fname" ] && fname="${fname}.new"
    cat >"$fname" <<EOF
# Sample configuration file for md_events
#
# Place the MDMessageCMD $MDE_BINDIR/$MDE_SELF directive in the httpd configuration
# (default: $MDE_HTTPD_CFG)
#
# An HTTPD instance string, if specified with -H, will be \$1
#
# Note that many of these variables can be set by options on the command
# line.  To ensure the command line has precedence, those are [ -z "\$foo" ] && export foo=
#
# Directory containing per-domain configuration (-r), default is from install
[ -z "\$MDE_CFGS" ] && export MDE_CFGS="$MDE_CFGS"
#
# PATH needed for commands
# export PATH="/usr/local/sbin:/usr/local/bin:\$PATH"
#
# Global Debugging (-d)
# [ -z "\$MDE_DEBUG" ] && export MDE_DEBUG=true
#
# Global MDE_MAILTO (-u)
# [ -z "\$MDE_MAILTO" ] && export MDE_MAILTO=me@example.net
#
# Logging (-L)
# [ -z "\$MDE_LOG" ] && export MDE_LOG="/var/log/httpd/md_events.log"
#
# Mail event notifications - space delimited list, or "all" (-m)
# [ -z "\$MDE_MAIL" ] && export MDE_MAIL="all"
#
# UMASK for sensitive files (e.g. keys, passwords)
# export MDE_SAFE_UMASK="umask u=rw,g=r,o="
#
# UMASK for www-served files (e.g. tokens)
# export MDE_WWW_UMASK="umask u=rw,g=r,o=r"
#
# SSH/SCP options - e.g. to select an identity (private key)
# export MDE_SSH_OPTIONS="-i ~certman/.ssh/id_ed25519"
#
# Handlers (as defined in $MDE_CFGS/example.net.conf) that are domain-
# independent.  These are inherited by all MDomains, and can be redefined
# in a MDomain's specific .conf file.  Note that these are functions, and
# execute in the request context (which is NOT available when they are
# defined.)
#
# The following are sample handlers that may be useful.  To keep them,
# see the instructions at the end of the block.
#
# **** Begin sample handlers ****

EOF
    rv="$?"
    cat >>"$fname" <<'EOFEOF'
# Simplest form of DNS updates
# This assumes DNS UPDATE (RFC 2136, etc) is supported, authorization is by IP address
#
# nsupdate_options="-k tsig_key.123+42123,private"
# You may need "server" and/or "local" commands if nsupdate doesn't default them.

function setup_handler() {
    local keyauth="${MDE_DATA[0]}"

    # Note: Blank line is "send" to nsupdate
    nsupdate $nsupdate_options <<EOF
update add _acme-challenge.${MDE_DOMAIN}. 300 in TXT "${keyauth}"

EOF
}

function teardown_handler() {

    nsupdate $nsupdate_options <<EOF
update delete _acme-challenge.${MDE_DOMAIN} TXT

EOF
}

# Wrapper functions for getssl ( https://github.com/srvrco/getssl ) DNS Update scripts
#
# Use these instead of the above if your provider is in the getssl library
#
# Depending on the provider, you may need to define variables for API credentials.
# Thy may also depend on other scripts.  See each script for its requirements.
#
# dns_scripts="/usr/local/sbin/getssl/dns_scripts"
# dns_provider="nsupdate" or "godaddy", "cloudflare", "dnspod", "linode", "ovh", ...
# At this writing, there are scripts for about a dozen providers there.

function xx_setup_handler() {
    local keyauth="${MDE_DATA[0]}"

    mkdir -p "$MDE_CFGS/${MD_MDOMAIN}-d"
    touch "$MDE_CFGS/${MD_MDOMAIN}_d/${keyauth}.token"
    $dns_scripts/dns_add_${provider} "$MD_MDOMAIN" "$keyauth"
    return $?
}

function xx_teardown_handler() {
    local keyauth="${MDE_DATA[0]}"

    # NOTE: These expect the token to be deleted - requires a mod_md change
    #       Work-around: track installed tokens in $MDE_CFGS/${MD_MDOMAIN}.d/*.token

    local ng="$(shopt -p nullglob)"
    shopt -s nullglob
    local T rv="0"
    for T in $MDE_CFGS/${MD_MDOMAIN}_d/*.token; do
        keyauth="$(basename $T .token)"
        $dns_scripts/dns_del_${provider} "$MD_MDOMAIN" "$keyauth"
        [ "$rv" == "0" ] && rv="$?"
        rm -f $T
    done
    $ng
    return $rv
}

export -f setup_handler teardown_handler

# Remove the following line to keep the sample handlers

unset -f setup_handler teardown_handler

# **** End sample handlers ****

# Do NOT exit - this is a sourced script
true
EOFEOF
    [ "$rv" == "0" ] && rv="$?"
    if [ "$rv" == "0" ]; then
        info "Created $fname with defaults, please review"
    fi

    if ! [ -d "$MDE_CFGS" ]; then
        if ! md="$(mkdir -pv "$MDE_CFGS")" ; then
            error $E_FAILED "mkdir $MDE_CFGS failed: $?"
        fi
        [ -n "$md" ] && info "$md"
    fi

    fname="$MDE_CFGS/example.net.conf"
    [ -f "$fname" ] && fname="${fname}.new"

    # May contain passwords (not recommended), but is out
    # of httpd content tree.
    $MDE_SAFE_UMASK
    cat >"$fname" <<EOFEOF
# Sample configuration file for example.net
#
# An HTTPD instance string, if specified with -H, will be \$1
#
# Customize a copy of this file for each managed domain, and
# place it in $MDE_CFGS named <domain_name>.conf
EOFEOF
    rv="$?"

    cat >>"$fname" <<'EOFEOF'
#
# A function is defined here for any event that is to be handled.
#
# The events are exactly the "reason" strings defined for MDMessageCmd,
# MDChallengeDns01, or (the hypothetical) MDChallengeHttp01, MDEvent.
# The corresponding function is "${reason}_handler", with any "-" in the
# reason string replaced with "_".
#
# On entry, the following variables are defined and (some) can be modified:
# MD_MDOMAIN        - the domain name that triggered the event
# MDE_EVENT         - the unmodified "reason" string
# MDE_DOMAIN        = the test or target domain identified by mod_md (e.g. DNS)
# MDE_DATA          - an array containg any additional data passed as command line arguments
# MD_STORE          - the path to the mod_md data store (directory).
# MD_VERSION        - the version of MOD_MD
#                     $MD_STORE/$MDE_WHERE./$MD_MDOMAIN/*.pem will be the certificate(s) and key(s).
# MDE_WHERE         - the MD_STORE location of the certificates affected by the event.
# MDE_VERSION       - the version of md_events
# MDE_MAILTO        - e-mail address used for mail (set to -u or webmaster@$MD_MDOMAIN)
# MDE_NOTIFY        - set to "true" if MDMessageCmd -m included this event.
# MDE_SUBJECT       - subject for an e-mail about this event.
# MDE_MESSAGE       - body for a generic description of this event
# MDE_TDIR          - a temporary directory
# MDE_INITIAL_UMASK - a umask command to restore umask
# MDE_SAFE_UMASK    - a umask command to set umask for sensitive files
# MDE_WWW_UMASK     - a umask command to set umask for httpd-served files
# -------           - The following are only present for corresponding events.
# MDE_CERTS         - Full path to each certificate installed/staged for $MD_MDOMAIN
# MDE_KEYS          - Full path to each private key installed/staged for $MD_MDOMAIN
# MDE_TYPES         - A list of the certificate types (rsa, p-256, ...)
#
EOFEOF
    [ "$rv" == "0" ] && rv="$?"

    cat >>"$fname" <<EOFEOF
# Additional variables from $MDE_CFG are also defined
#
# Function definitions here may be superseded by re-defining them in
# $MDE_CFGS/<mdomain-name>.conf or $MDE_CFGS/<target-domain-name>.conf
EOFEOF
    [ "$rv" == "0" ] && rv="$?"

    cat >>"$fname" <<'EOFEOF'
#
# MDE_MESSAGE is only set if MDMessageCmd -m included this event.
# If MDE_MESSAGE is set on return, an e-mail containing it will be sent.
#
# The function "info" will write a message to stdout.
# The function "debug" will write a message to stderr if -d was specified
# The function "error" will write a message to stderr and exit.
#
# If a handler returns non-zero (or exits), mod_md may retry.
#
# The following functions are available for common operations:
# copy_file_to $description $from $to [$owner]
#   $description is for messages.  $from is a local filename.
#   $to can be any of (including a list separated by ';':
#       scp:user@server.example.net:/where_to_put
#       sftp:user:password:ftp.example.net:/where_to_put
#       ftp:user:password:ftp.example.net:/where_to_put
#       /where_to_put (local filespec)
# delete_file_from $description $file
#   $file takes any of the forms noted above for copy_file_to
#
#   ftp is NOT recommended since it does not use encryption
#   sftp requires the `sshpass` utility
#   scp is a better choice than either ftp since with its public key
#   authentication, no password is required in this file.
# get_leaf_chain_certs - extracts all certificates from stdin
# create_leaf_chain_certs $dir file [file...] - creates PEM file(s) containing leaf & any
#                                               intermediate certificates in $dir
# get_leaf_cert - extracts the first (leaf) certificate from stdin
# create_leaf_pem $dir file [file...]
# get_chain_cert - extracts all BUT the first certificate from stdin
# create_chain_pem $dir file [file...]
# get_key_and_cert $certfile - combines private key with leaf from stdin
# create_key_and_cert_pem $dir file [file...]
# get_all_in_1 $certfile - combines private key with leaf and intermediate certificates() from stdin
# create_all_in_1_pem $dir file [file...]
#
# The 'create' versions create one file in $dir for each input file.
#
# write_http_token_to $description $token $keyauth $to
#  Stores a token value  inthe specified file.  $to as above
#
# reload_service $cmd
#  If $cmd is prefixed by "ssh:server:", it is executed remotely, otherwise locally
#  Use to activate a new certificate (or perform other housekeeping)

# Sample handlers for interesting events

#
# Sample handler for the "installed" event
#
www_owner="apache:apache"

function installed_handler() {
    # Create all the variant .PEM files

    create_leaf_pem          "$MDE_TDIR" "$MDE_CERTS"
    create_chain_pem         "$MDE_TDIR" "$MDE_CERTS"
    create_leaf_chain_pem    "$MDE_TDIR" "$MDE_CERTS"
    create_key_and_cert_pem  "$MDE_TDIR" "$MDE_CERTS"
    create_all_in_1_pem      "$MDE_TDIR" "$MDE_CERTS"

    # Copy them to your servers from $MDE_TDIR
    # Select whatever your clients need:
    #  leaf{.type}.pem
    #  leaf_chain{.type}.pem
    #  chain{.type}.pem
    #  key_chain{.type}.pem
    #  allin1{.type}.pem

    mkdir -v  -p /root/demo{1,2}/certs
    myserver="/root/demo1/certs;scp:localhost:/root/demo2/certs"

    for F in $MDE_TDIR/allin1*.pem; do
        copy_file_to "Combo file for server" "$F" "$myserver"
    done
    for F in $MDE_KEYS; do
        copy_file_to "Private keys for server" "$F" "$myserver"
    done

    # You can modify the standard message - here with debugging information

    MDE_MESSAGE="$(cat <<EOF
$MDE_MESSAGE
Types: $MDE_TYPES
Certs: $MDE_CERTS
Keys : $MDE_KEYS
Dir  : $MDE_TDIR
`ls -l $MDE_TDIR; find /root/demo* -ls`
EOF
)"
    
    MDE_MAILTO=root
    return $E_OK
}
export -f installed_handler

# "renewed" executes without privileges,but we can trigger an install in
# a couple of ways:
# For a local service, setup 'sudo':
#  sudo for the httpd user should be restricted (with sudoers) to just
#  the reload command - e.g. a script that ONLY does "apachectl graceful"
# For a remote service,=:
#  Create a user id with a restricted shell, again ONLY able to reload
# You can also treat a local server as "remote" and ssh to localhost...

function renewed_handler() {
    reload_service sudo /usr/sbin/httpd_reload
    reload_service ssh:reloader@myserver.example.net:/usr/sbin/httpd_reload
    return $E_OK
}
export -f renewed_handler

# http-01 challenges for remote hosts

mkdir -v -p /root/demo{1,2}/.wellknown/acme-challenge
chown -R $www_owner /root/demo{1,2}

function setup_http_01_handler() {
    local token="${MDE_DATA[0]}" keyauth="${MDE_DATA[1]}"

    write_http_token_to "http-01 token for $MDE_DOMAIN" "$token" "$keyauth" \
      "/root/demo1/.wellknown/acme-challenge/$token;scp:localhost:/root/demo2/.wellknown/acme-challenge/$token"
}
export -f setup_http_01_handler

function teardown_http_01_handler() {
    local token="${MDE_DATA[0]}"

    delete_file_from "http-01 token for $MDE_DOMAIN" \
      "/root/demo1/.wellknown/acme-challenge/$token;ssh:localhost:/root/demo2/.wellknown/acme-challenge/$token"

    return $E_OK
}
export -f teardown_http_01_handler

# Do NOT exit - this is a sourced script
true
EOFEOF
    [ "$rv" == "0" ] && rv="$?"
    $MDE_INITIAL_UMASK

    [ "$rv" == "0" ] && info "Installed sample domain configuration in $fname"

    info "Installation complete, update the httpd configuration (usually $MDE_HTTPD_CFG)"
    exit $E_OK
#-----END INSTALLER
    error $E_FAILED "Installation not supported in this version. Fresh copy required."
fi

if [ -z "$MDE_EVENT" -o -z "$MDE_DOMAIN" ]; then
    error $E_FAILED "Unless installing, event and domain parameters are required"
fi

export MDE_EVENT MDE_DOMAIN MDE_DEBUG MDE_LOG MDE_CFG MDE_CFGS MDE_NOTIFY MDE_VERSION MDE_HTTPD_ROOT MDE_HTTPD_CFG
export -f info debug error

MDE_SUBJECT=
MDE_MESSAGE=
export MDE_SUBJECT MDE_MESSAGE

export MDE_WHERE=
case "$MDE_EVENT" in
    "renewed")
        MDE_SUBJECT="renewed $MDE_DOMAIN certificate"
        MDE_MESSAGE="Your Apache renewed the certificate for '$MDE_DOMAIN'. It will become active after a server reload."
        MDE_WHERE="staging"
        ;;
    "installed")
        MDE_SUBJECT="installed $MDE_DOMAIN certificate"
        MDE_MESSAGE="Your Apache installed the certificate for '$MDE_DOMAIN'. It is now active."
        MDE_WHERE="domains"
        ;;
    "expiring")
        MDE_SUBJECT="expiring $MDE_DOMAIN certificate"
        MDE_MESSAGE="Your Apache reports that the certificate for '$MDE_DOMAIN' will soon expire."
        ;;
    "errored")
        MDE_SUBJECT="error renewing $MDE_DOMAIN certificate"
        MDE_MESSAGE="There was an error renewing the certificate for '$MDE_DOMAIN'. Apache will continue trying. Please check the md-status reso
urces or the server log for more information should this repeat."
        ;;
    "ocsp-renewed")
        MDE_SUBJECT="refreshed OCSP stapling for $MDE_DOMAIN"
        MDE_MESSAGE="The OCSP stapling information for '$MDE_DOMAIN' was successfully refreshed."
        ;;
    "ocsp-errored")
        MDE_SUBJECT="error refreshing OCSP stapling for $MDE_DOMAIN"
        MDE_MESSAGE="The was an error refreshing the OCSP stapling information for '$MDE_DOMAIN'. Apache will continue trying. Please check the
md-status resources or the server log for more information should this repeat."
        ;;
    "setup")
        ;;
    "teardown")
        ;;
    "setup-http-01")
        ;;
    "teardown-http-01")
        ;;
    *)
        MDE_SUBJECT="unknown event in MD message"
        MDE_MESSAGE="Your Apache reported event '$MDE_EVENT' for domain '$MDE_DOMAIN'."
        ;;
esac
[ -z "$MDE_NOTIFY" ] && MDE_MESSAGE=

# Public functions

# copies a file, using scp, sftp or ftp if required.
#
# SSH/sftp - use certificate (key file) authentication
# copy_file_to "thisfile" /foo/here/file scp:luser@foo.example.net:/there/dir;scp:...
#
# copy_file_to "thisfile" /foo/here/file {s,}ftp:luser:pass:ftp.example.net:/there/dir
#
# copy_file_to "thisfile" /foo/here/file /etc/pki/tls/certs/

function copy_file_to() {
  local desc="$1"   # descriptive name, used for display
  local from="$2"   # current file location
  local to="$3"     # location(s) to move file to, separated by ';'
  local owner="$4"  # remote file owner

  local -a dests
  IFS=\; read -r -a dests <<< "$to"
  for to in "${dests[@]}"; do
      info "copying $desc to $to"
      debug "copying from $from to $to"
      if [[ "${to:0:4}" == "scp:" ]] ; then
          debug "using scp scp $MDE_SSH_OPTIONS -q $from ${to:4}"
          if ! scp $MDE_SSH_OPTIONS -q -p "$from" "${to:4}" >/dev/null 2>&1 ; then
              error $E_RETRY "scp copy failed :$(scp $MDE_SSH_OPTIONS -p "$from" "${to:4}" 2>&1)"
          fi
          if [ -n "$owner" ]; then
              local servername="$(awk -F":" '{print $2}' <<< "$to")"
              local tofile="$(awk -F":" '{print $3}' <<< "$to")"
              debug "servername $servername"
              debug "file $tofile"
              ssh $MDE_SSH_OPTIONS "$servername" "chown $owner $tofile"
          fi
      elif [[ "${to:0:5}" == "sftp:" ]] ; then
          debug "using sftp to copy the file from $from"
          local ftpuser=$(awk -F: '{print $2}' <<< "$to")
          local ftppass=$(awk -F: '{print $3}' <<< "$to")
          local ftphost=$(awk -F: '{print $4}' <<< "$to")
          local ftplocn=$(awk -F: '{print $5}' <<< "$to")
          local ftpdirn=$(dirname "$ftplocn")
          local ftpfile=$(basename "$ftplocn")
          local fromdir=$(dirname "$from")
          local fromfile=$(basename "$from")
          debug "sftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile"
          debug "from dir=$fromdir  file=$fromfile"
          sshpass -p "$ftppass" sftp "$ftpuser@$ftphost" <<_EOF
cd $ftpdirn
lcd $fromdir
put $fromfile
_EOF
      elif [[ "${to:0:4}" == "ftp:" ]] ; then
          # FTP is a bad choice
          debug "using ftp to copy the file from $from"
          local ftpuser="$(awk -F: '{print $2}' <<< "$to")"
          local ftppass="$(awk -F: '{print $3}'<<< "$to")"
          local ftphost="$(awk -F: '{print $4}'<<< "$to")"
          local ftplocn="$(awk -F: '{print $5}'<<< "$to")"
          local ftpdirn="$(dirname "$ftplocn")"
          local ftpfile="$(basename "$ftplocn")"
          local fromdir="$(dirname "$from")"
          local fromfile="$(basename "$from")"
          debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile"
          debug "from dir=$fromdir  file=$fromfile"
          ftp -n <<_EOF
open $ftphost
user $ftpuser $ftppass
cd $ftpdirn
lcd $fromdir
put $fromfile
_EOF
      else # local file
          if ! mkdir -p "$(dirname "$to")" ; then
              error $E_RETRY "cannot create directory $(basename "$to")"
          fi
          [ -f "$to" ] && rm -f "$to"
          if ! cp -p "$from" "$to" ; then
              error $E_RETRY "cannot copy $from to $to"
          fi
          if [ -n "$owner" ]; then
              chown "$owner" "$to"
          fi
      fi
      debug "copied $from to $to"
  done
  return $E_OK
}
export -f copy_file_to

# Delete a file
# As copy_file to, except "ssh:" replaces "scp:"

function delete_file_from() {
  local desc="$1"   # descriptive name, used for display
  local file="$2"   # files to remove separated by ';'

  local -a files
  IFS=\; read -r -a files <<< "$file"
  for file in "${files[@]}"; do
      info "deleting $desc $file"
      if [[ "${file:0:4}" == "ssh:" ]] ; then
          debug "using ssh $MDE_SSH_OPTIONS rm ${file:4}"
          local servername="$(awk -F":" '{print $2}' <<< "$file")"
          local rfile="$(awk -F":" '{print $3}' <<< "$file")"
          [ "$rfile" = '/' -o "${#rfile}" -lt 5 ] && error $FAILED "Bad delete: $file"
          local cmd="rm -f $rfile"
          debug "$servername $cmd"
          ssh "$MDE_SSH_OPTIONS $servername" "$cmd" >/dev/null 2>&1
      elif [[ "${file:0:5}" == "sftp:" ]] ; then
          debug "using sftp to delete $file"
          local ftpuser=$(awk -F: '{print $2}' <<< "$file")
          local ftppass=$(awk -F: '{print $3}' <<< "$file")
          local ftphost=$(awk -F: '{print $4}' <<< "$file")
          local ftplocn=$(awk -F: '{print $5}' <<< "$file")
          local ftpdirn=$(dirname "$ftplocn")
          local ftpfile=$(basename "$ftplocn")
          debug "sftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile"
          sshpass -p "$ftppass" sftp "$ftpuser@$ftphost" <<_EOF
cd $ftpdirn
rm $ftpfile
_EOF
      elif [[ "${file:0:4}" == "ftp:" ]] ; then
          # FTP is a bad choice
          debug "using ftp to delete $file"
          local ftpuser="$(awk -F: '{print $2}' <<< "$file")"
          local ftppass="$(awk -F: '{print $3}'<<< "$file")"
          local ftphost="$(awk -F: '{print $4}'<<< "$file")"
          local ftplocn="$(awk -F: '{print $5}'<<< "$file")"
          local ftpdirn="$(dirname "$ftplocn")"
          local ftpfile="$(basename "$ftplocn")"
          debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile"
          ftp -n <<_EOF
open $ftphost
user $ftpuser $ftppass
cd $ftpdirn
delete $ftpfile
_EOF
      else # local file
          rm -f $file
      fi
      debug "deleted $file"
  done
}
export -f delete_file_from

# Write an http-01 token to one or more storage locations
# As copy_file_to

function write_http_token_to() {
    local desc="$1"
    local token="$2"
    local keyauth="$3"
    local to="$4"

    info "Saving $desc"
    $MDE_WWW_UMASK
    cat >$MDE_TDIR/$token <<< "$keyauth"
    $MDE_INITIAL_UMASK
    chmod $(cut -d' ' -f2 <<< "$MDE_WWW_UMASK") $MDE_TDIR/$token
    copy_file_to "$desc" "$MDE_TDIR/$token" "$to" "$www_owner"
}
export -f write_http_token_to

# Functions to create common variants of PEM files
#  Some consumers want leaf and intermediate certificates in separate files.
#  Others want key & certificate(s) in one file.  And so it goes.
# These also remove anything in the files that is not certificate data.
#
# The mod_md files contain (in order) the leaf certificate & the intermediate certificate(s)
#
# Stream functions (get_*):
#   Put certificate file on stdin, result on stdout
#   Functions needing keys get the certificate filespec as an argument
# File functions (create_*):
#
#   Pass output directory, list of certificate filenames

# Extract leaf and chain certificates

function get_leaf_chain_certs() {
    sed -ne'/^-----BEGIN.*CERTIFICATE-----$/,/^-----END.*CERTIFICATE-----$/p'
}

# Create .PEM files of leaf & chain certificates - leaf_chain.type.pem

function create_leaf_chain_pem() {
    local DIR="$1"
    shift
    local F
    for F in $@; do
        local OF="$(basename $F)"
        get_leaf_chain_certs <"$F" >"$DIR/leaf_chain${OF:7}"
    done
}

export -f get_leaf_chain_certs create_leaf_chain_pem

# Extract just a leaf certificate
#  Everything up to the first END

function get_leaf_cert() {
    get_leaf_chain_certs | sed -ne'1,/^-----END.* CERTIFICATE-----$/p'
}

# Create .PEM files of leaf certificates - leaf.type.pem

function create_leaf_pem() {
    local DIR="$1"
    shift
    local F
    for F in $@; do
        local OF="$(basename $F)"
        get_leaf_cert <"$F" >"$DIR/leaf${OF:7}"
    done
}
export -f get_leaf_cert create_leaf_pem

# Extract chain (non-leaf) certificate(s)
#  Everything after the first END

function get_chain_cert() {
    get_leaf_chain_certs | sed -e'1,/^-----END.* CERTIFICATE-----$/d'
}

# Create .PEM files of chain certificates - chain.type.pem

function create_chain_pem() {
    local DIR="$1"
    shift
    local F
    for F in $@; do
        local OF="$(basename $F)"
        get_chain_cert <"$F" >"$DIR/chain${OF:7}"
    done    
}
export -f create_chain_pem create_chain_pem

# Create leaf key and certificate file
# $1 full path of certificate file

function get_key_and_cert() {
    local certname="$(basename "$1")"

    local keyname="$(sed -e's/pubcert\./privkey./' <<< "$certname")"
    cat "$(dirname "$1")/$keyname"
    get_leaf_cert
}

# Create .PEM key and certificate files - key_chain.type.pem

function create_key_and_cert_pem() {
    local DIR="$1"
    shift
    local F
    $MDE_SAFE_UMASK
    for F in $@; do
        local OF="$(basename $F)"
        get_key_and_cert "$F" <"$F" >"$DIR/key_chain${OF:7}"
    done
    $MDE_INITIAL_UMASK
}
export -f get_key_and_cert create_key_and_cert_pem

# Create all-in-1 file (key, chain)
# $1 full path of certificate file

function get_all_in_1() {
    get_key_and_cert "$1"
    get_chain_cert <"$1"
}

# Create .PEM all-in-1 files - allin1.type.pem

function create_all_in_1_pem() {
    local DIR="$1"
    shift
    local F
    $MDE_SAFE_UMASK
    for F in $@; do
        local OF="$(basename $F)"
        get_all_in_1 "$F" <"$F" >"$DIR/allin1${OF:7}"
    done
    $MDE_INITIAL_UMASK
}
export -f get_all_in_1 create_all_in_1_pem

# Runs one or more commands to reload services ( via ssh if needed)
#  If the first command is prefixed with "ssh:", the commands will
#  be executed remotely, e.g.
#   reload_service ssh:myserver.example.net "apachectl graceful" "ps ax | grep httpd"
#  Otherwise it is executed locally
#   reload_service "apachectl graceful"
# If more than one command is specified, they are joined with '&&'
# Returns E_OK if the command succeeds, E_RETRY otheerwise.

function reload_service() {
    local cmd

    info "reloading SSL services"
    if [[ "${1:0:4}" == "ssh:" ]] ; then
        local sshhost="${1:4}"
        shift
        IFS='#' cmd="$*"
        cmd="${cmd//#/ && }"
        debug "reload command:"
        debug "ssh $MDE_SSH_OPTIONS $sshhost \"${cmd}\""
        if info "$(ssh $MDE_SSH_OPTIONS "$sshhost" "${cmd}" 2>&1)"; then
            debug "command returned success"
        else
            debug "Command exited with $?"
            return $E_RETRY
        fi
    else
        IFS='#' cmd="$*"
        cmd="${cmd//#/ && }"

        debug "running reload command \"$cmd\""
        if info "$(eval $cmd 2>&1)" ; then
            debug "command returned success"
        else
            debug "Command exited with $?"
            return $E_RETRY
        fi
    fi
    return $E_OK
}
export -f reload_service

# Read domain configuration file(s)

# - MDOMAIN (Certificate)

if [ -f "$MDE_CFGS/${MD_MDOMAIN}.conf" ]; then
    . "$MDE_CFGS/${MD_MDOMAIN}.conf" "$MDE_HTTPD"
    [ "$?" == "0" ] && debug "Read configuration file: $MDE_CFGS/${MD_MDOMAIN}.conf"
fi

# - Target domain (validation target - host or resource)

if [ "$MDE_DOMAIN" != "$MD_MDOMAIN" -a -f "$MDE_CFGS/${MDE_DOMAIN}.conf" ]; then
    . "$MDE_CFGS/${MDE_DOMAIN}.conf" "$MDE_HTTPD"
    [ "$?" == "0" ] && debug "Read configuration file: $MDE_CFGS/${MDE_DOMAIN}.conf"
fi

if [ -n "$MDE_WHERE" ]; then
    MDE_CERTS="$(find $MD_STORE/$MDE_WHERE/$MD_MDOMAIN -name 'pubcert*.pem' 2>/dev/null | tr '\n' ' ')"
    MDE_KEYS="$(find $MD_STORE/$MDE_WHERE/$MD_MDOMAIN  -name 'privkey*.pem' 2>/dev/null | tr '\n' ' ')"
    MDE_TYPES=
    if [ -n "$MDE_CERTS" ]; then
        for C in $MDE_CERTS; do
            C="$(sed -e's/cert\.pem/cert.rsa.pem/' <<< "$C")"
            C="$(basename "$C" ".pem" | sed -e's/pubcert\.//')"
            MDE_TYPES="$MDE_TYPES $C"
        done
        unset C
        MDE_TYPES="${MDE_TYPES:1}"
    fi
    export MDE_CERTS MDE_KEYS MDE_TYPES
fi

# Provide a temporary directory in case file variants required

if ! export MDE_TDIR="$(mktemp -d)"; then
    error $E_RETRY "Can't create temporary directory"
fi
function md_cleanup() {
    rm -rf $MDE_TDIR
}
trap md_cleanup INT TERM EXIT

# Take action

F="$(tr -- '-' '_' <<< "$MDE_EVENT")"

if  declare -pF ${F}_handler >/dev/null 2>&1 ; then
    if ! ${F}_handler; then # Tell mod_md to retry
        RV="$?"
        if [ -n "$MDE_MESSAGE" ]; then
            mail -s "$MDE_SUBJECT" $MDE_MAILTO <<EOF
$MDE_MESSAGE
EOF
        fi
        exit $RV
    fi
fi

if [ -n "$MDE_MESSAGE" ]; then
    mail -s "$MDE_SUBJECT" $MDE_MAILTO <<EOF
$MDE_MESSAGE
EOF
else
    exit $E_OK
fi
