#!/usr/bin/env bash
# This file contains the default hook functions for dehydrated - these functions will be used when there is no overriding certificate
# specific hooks file.  All but startup_hook and ext_hook can be overridden by a hooks script on a per certificate basis.
#
# shellcheck disable=SC2034,SC2317

# Configuration.
# Where the copies of the current certificates/keys should be placed.  Comment for no copying.
CERTSDIR="/etc/certificates"
# The syslog facility and tag to use.  Comment for no sysloging.
SYSLOG_FACILITY="local1"
SYSLOG_TAG="dehydrated-hooks"
# Where from/to to send emails.  Comment for no emailing.
EMAIL_FROM="\"Server: ${HOSTNAME%%.*}\" <noreply@slackware.uk.net>"
EMAIL_TO=("Systems' Administrator <sysadmin@slackware.uk>")

# Get the system ID.
# shellcheck disable=SC2046
declare SYSTEM_$(grep '^ID=' /etc/os-release 2>/dev/null)

# Write a message to syslog and/or send via email.
notify() {
  # Parameters:
  #   $1		Log entry type (currently one of: error, warning or info).
  #   $2...		The text of the log entry.

  local PREFIX PRIORITY

  [[ -z "$1" ]] && return 1

  # Select the syslog priority level.
  case "$1" in
    'error')
      PREFIX="${LOG_PREFIX:-Certificate renewal} $1"
      PRIORITY="err"
      shift
      ;;
    'info')
      PREFIX="${LOG_PREFIX:-Certificate renewal} $1"
      PRIORITY="info"
      shift
      ;;
    'warning')
      PREFIX="${LOG_PREFIX:-Certificate renewal} $1"
      PRIORITY="warn"
      shift
      ;;
    *)
      LOG_PREFIX="Dehydrated hooks' coding"
      notify "error" "Invalid or no log entry severity specified - using 'error'"
      PREFIX="${LOG_PREFIX:-Certificate renewal} error"
      PRIORITY="err"
      ;;
  esac

  # Log the message to syslog.
  [[ -n "$SYSLOG_FACILITY" ]] && [[ -n "$SYSLOG_TAG" ]] && {
    if [[ "$SYSTEM_ID" == "alpine" ]]; then
      # BusyBox logger on Alpine's is missing the --id option.
      printf "%s\\n" "$PREFIX:" "$@" "EOL" | logger -p "$SYSLOG_FACILITY.$PRIORITY" -t "$SYSLOG_TAG" >/dev/null 2>&1
    else
      printf "%s\\n" "$PREFIX:" "$@" "EOL" | logger --id="$$" -p "$SYSLOG_FACILITY.$PRIORITY" -t "$SYSLOG_TAG" >/dev/null 2>&1
    fi
  }

  # Email the notification.
  [[ -n "$EMAIL_FROM" ]] && [[ -n "${EMAIL_TO[*]}" ]] && {
    printf "%s\\n" "$@" | mail -r "$EMAIL_FROM" -s "$PREFIX" "${EMAIL_TO[@]}" >/dev/null 2>&1
  }

  return 0
}

# Service configurations (used at startup/shutdown).
services() {
  # Parameters:
  #   $1		Whether to sanity check configuration.  '1' == sanity check.

  local LOG_PREFIX="Dehydrated hooks' configuration" SERVICE
  declare -g -A SERVICE_CTL SERVICE_DAEMON SERVICE_PIDFILE

  # Select the service configurations based on the distribution.
  # SERVICE_CTL['<service>'] is required, and either SERVICE_DAEMON['<service>'] or SERVICE_PIDFILE['<service>'], or both is required for any service.
  # An 'httpd' service must be defined for certificate renewal to work.
  if [[ "$SYSTEM_ID" == "alpine" ]]; then
    # HTTP daemon.
    if [[ -x "/etc/init.d/apache2" ]]; then
      SERVICE_CTL['httpd']="/etc/init.d/apache2"
      SERVICE_DAEMON['httpd']="httpd"
      SERVICE_PIDFILE['httpd']="/run/apache2/httpd.pid"
    elif [[ -x "/etc/init.d/thttpd" ]]; then
      SERVICE_CTL['httpd']="/etc/init.d/thttpd"
      SERVICE_DAEMON['httpd']="thttpd"
      SERVICE_PIDFILE['httpd']="/run/thttpd.pid"
    fi
    # Samba daemon selection.
    if [[ -x "/etc/init.d/samba" ]]; then
      SERVICE_CTL['samba']="/etc/init.d/samba"
      SERVICE_DAEMON['samba']="samba"
      SERVICE_PIDFILE['samba']="/run/samba.pid"
    fi
  elif [[ "$SYSTEM_ID" =~ ^(debian|devuan)$ ]]; then
    # HTTP daemon.
    if [[ -x "/usr/sbin/apachectl" ]]; then
      SERVICE_CTL['httpd']="/usr/sbin/apachectl"
      SERVICE_DAEMON['httpd']="apache2"
      SERVICE_PIDFILE['httpd']="/run/apache2/apache2.pid"
    fi
    # Samba daemon.
    if [[ -x "/etc/init.d/samba" ]]; then
      SERVICE_CTL['samba']="/etc/init.d/samba"
      SERVICE_DAEMON['samba']="samba"
      SERVICE_PIDFILE['samba']="/run/samba/samba.pid"
    fi
  elif [[ "$SYSTEM_ID" == "slackware" ]]; then
    # HTTP daemon.
    if [[ -x "/etc/rc.d/rc.httpd" ]]; then
      SERVICE_CTL['httpd']="/etc/rc.d/rc.httpd"
      SERVICE_DAEMON['httpd']="httpd"
      SERVICE_PIDFILE['httpd']="/run/httpd.pid"
    elif [[ -x "/etc/rc.d/rc.thttpd" ]]; then
      SERVICE_CTL['httpd']="/etc/rc.d/rc.thttpd"
      SERVICE_DAEMON['httpd']="thttpd"
      SERVICE_PIDFILE['httpd']="/run/thttpd.pid"
    fi
    # FTP daemon.
    if [[ -x "/etc/rc.d/rc.proftpd" ]]; then
      SERVICE_CTL['ftpd']="/etc/rc.d/rc.proftpd"
      SERVICE_DAEMON['ftpd']="proftpd"
      SERVICE_PIDFILE['ftpd']="/run/proftpd.pid"
    fi
    # SMTP daemon.
    if [[ -x "/etc/rc.d/rc.exim" ]]; then
      SERVICE_CTL['smtpd']="/etc/rc.d/rc.exim"
      SERVICE_DAEMON['smtpd']="exim"
      SERVICE_PIDFILE['smtpd']="/run/exim.pid"
    fi
  elif [[ "$SYSTEM_ID" == "void" ]]; then
    # HTTP daemon selection.
    # Note: thttpd on Void doesn't have a directly callable rc script, so can't be supported.
    if [[ -x "/usr/sbin/apachectl" ]]; then
      SERVICE_CTL['httpd']="/usr/sbin/apachectl"
      SERVICE_DAEMON['httpd']="httpd"
      SERVICE_PIDFILE['httpd']="/run/httpd/httpd.pid"
    fi
  else
    notify "error" "'$SYSTEM_ID' is not a supported platform for hooks"
    return 1
  fi

  # Sanity check settings.
  (( $1 == 1 )) && {
    local IFS=$'\n'
    for SERVICE in $(printf "%s\\n" "${!SERVICE_CTL[@]}" "${!SERVICE_DAEMON[@]}" "${!SERVICE_PIDFILE[@]}" | sort | uniq); do
      if [[ -z "${SERVICE_CTL[\"$SERVICE\"]}" ]]; then
        if [[ "$SERVICE" == "httpd" ]]; then
          notify "warning" "'SERVICE_CTL' is not set for service 'httpd' - HTTP based validation, if used, not possible" "Comment all 'httpd' SERVICE_* settings to silence this warning"
        else
          notify "warning" "'SERVICE_CTL' is not set for service '$SERVICE'"
        fi
        unset "SERVICE_CTL['$SERVICE']" "SERVICE_DAEMON['$SERVICE']" "SERVICE_PIDFILE['$SERVICE']"
        continue
      else
        [[ -z "${SERVICE_DAEMON[\"$SERVICE\"]}" ]] && [[ -z "${SERVICE_PIDFILE[\"$SERVICE\"]}" ]] && {
          notify "warning" "neither 'SERVICE_DAEMON' nor 'SERVICE_PIDFILE' is set for service '$SERVICE' - at least one setting is required"
          unset "SERVICE_CTL['$SERVICE']" "SERVICE_DAEMON['$SERVICE']" "SERVICE_PIDFILE['$SERVICE']"
          continue
        }
      fi
    done
  }

  return 0
}

# Called once for every domain that needs to be validated, including any alternative names listed.
deploy_challenge() {
  # Parameters:
  #   $1 (DOMAIN)	The domain name (CN or subject alternative name) being validated.
  #   $2 (TOKEN_FILENAME) The name of the file containing the token to be served for HTTP validation
  #			Should be served by your web server as /.well-known/acme-challenge/${TOKEN_FILENAME}.
  #   $3 (TOKEN_VALUE)	The token value that needs to be served for validation.
  #			For HTTP validation it is the value that is expected be found in the $TOKEN_FILENAME file.
  #			For DNS validation, this is what you want to put in the _acme-challenge TXT record.

  local DOMAIN="$1" TOKEN_FILENAME="$2" TOKEN_VALUE="$3"

  # Simple example: Use nsupdate with local named
  # printf 'server 127.0.0.1\nupdate add _acme-challenge.%s 300 IN TXT "%s"\nsend\n' "$DOMAIN" "$TOKEN_VALUE" | nsupdate -k /var/run/named/session.key

  return 0
}


# Called after attempting to validate each domain, whether or not validation was successful.
# Can be used to delete files or DNS records that are no longer needed.
clean_challenge() {
  # Parameters:
  #   $1 (DOMAIN)	The domain name (CN or subject alternative name) being validated.
  #   $2 (TOKEN_FILENAME) The name of the file containing the token to be served for HTTP validation
  #			Should be served by your web server as /.well-known/acme-challenge/${TOKEN_FILENAME}.
  #   $3 (TOKEN_VALUE)	The token value that needs to be served for validation.
  #			For HTTP validation it is the value that is expected be found in the $TOKEN_FILENAME file.
  #			For DNS validation, this is what you want to put in the _acme-challenge TXT record.

  local DOMAIN="$1" TOKEN_FILENAME="$2" TOKEN_VALUE="$3"

  # Simple example: Use nsupdate with local named
  # printf 'server 127.0.0.1\nupdate delete _acme-challenge.%s TXT "%s"\nsend\n' "$DOMAIN" "$TOKEN_VALUE" | nsupdate -k /var/run/named/session.key

  return 0
}


# Called after the certificates have been created but before they are symlinked.
# This allows the sync the files to disk to prevent creating a symlink to empty files on unexpected system crashes.
# This hook is not intended to be used for further processing of certificate files; see deploy_cert for that.
sync_cert() {
  # Parameters:
  #   $1 (KEYFILE)	The path of the file containing the private key.
  #   $2 (CERTFILE)	The path of the file containing the signed certificate.
  #   $3 (FULLCHAINFILE) The path of the file containing the full certificate chain.
  #   $4 (CHAINFILE)	The path of the file containing the intermediate certificate(s).
  #   $5 (REQUESTFILE)	The path of the file containing the certificate signing request.

  local KEYFILE="$1" CERTFILE="$2" FULLCHAINFILE="$3" CHAINFILE="$4" REQUESTFILE="$5"

  sync "$KEYFILE" "$CERTFILE" "$FULLCHAINFILE" "$CHAINFILE" "$REQUESTFILE"

  return 0
}


# Called once for each certificate that has been produced.
# Here you can copy your new certificates to service-specific locations and reload the service.
deploy_cert() {
  # Parameters:
  #   $1 (DOMAIN)	The primary domain name, i.e. the certificate common name (CN).
  #   $2 (KEYFILE)	The path of the file containing the private key.
  #   $3 (CERTFILE)	The path of the file containing the signed certificate.
  #   $4 (FULLCHAINFILE) The path of the file containing the full certificate chain.
  #   $5 (CHAINFILE)	The path of the file containing the intermediate certificate(s).
  #   $6 (TIMESTAMP)	Timestamp when the specified certificate was created.

  local DOMAIN="$1" KEYFILE="$2" CERTFILE="$3" FULLCHAINFILE="$4" CHAINFILE="$5" TIMESTAMP="$6"

  local FILE LOG_PREFIX="Certificate deployment"

  # Only copy the certificate if there's a CERTSDIR setting.
  [[ -n "$CERTSDIR" ]] && {
    # If any of the destination files are symlinks, bail out - we don't want to clobber something.
    for FILE in "$CERTSDIR/${DOMAIN}_"{cert,key,chain,fullchain}.pem; do
      [[ -e "$FILE" ]] && [[ -L "$FILE" ]] && {
        notify "error" "Will not copy to symlink '$FILE' during '$DOMAIN' certificate deployment"
        # Return 0 so that dehydrated doesn't stop - there may be some more certificates to renew.
        return 0
      }
    done

    # Only copy the certificate if it differs from the new one.
    cmp "$CERTFILE" "$CERTSDIR/${DOMAIN}_cert.pem" >/dev/null 2>&1 || {
      # The first time through this will create the files readable by root only, but better to err on the side of caution.
      # Subsequent runs will retain whatever permissions were set by the admin after the first run.
      umask 066
      # shellcheck disable=SC2015
      cat "$CERTFILE" >"$CERTSDIR/${DOMAIN}_cert.pem" && cat "$KEYFILE" >"$CERTSDIR/${DOMAIN}_key.pem" && cat "$CHAINFILE" >"$CERTSDIR/${DOMAIN}_chain.pem" && cat "$FULLCHAINFILE" >"$CERTSDIR/${DOMAIN}_fullchain.pem" || {
        notify "error" "Failed to copy certificates/key to '$CERTSDIR' during '$DOMAIN' certificate deployment"
        # Return 0 so that dehydrated doesn't stop - there may be some more certificates to renew.
        return 0
      }
    }

    # Set a marker (used in the exit_hook function) to signal that services should be reloaded at the end of deployments.
    touch "/run/dehydrated-reload-marker" || {
      notify "warning" "Failed to create reload marker during '$DOMAIN' certificate deployment - reloading services manually may be required -- check server"
      # Return 0 so that dehydrated doesn't stop - there may be some more certificates to renew.
      return 0
    }
  }

  # Notify the sysadmin of the sucessful renewal.
  notify "information" "Sucessful renewal and deployment of certificate/key for '$DOMAIN'"

  return 0
}


# Called once for each updated OCSP stapling file that has been produced.
# Here you can copy your new OCSP stapling files to service-specific locations and reload the service.
deploy_ocsp() {
  # Parameters:
  #   $1 (DOMAIN)	The primary domain name, i.e. the certificate common name (CN).
  #   $2 (OCSPFILE)	The path of the OCSP stapling file.
  #   $3 (TIMESTAMP)	Timestamp when the specified OCSP stapling file was created.

  local DOMAIN="$1" OCSPFILE="$2" TIMESTAMP="$3"

  # Simple example: Copy file to nginx config
  # cp "$OCSPFILE" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
  # systemctl reload nginx

  return 0
}


# Called once for each certificate that is still valid and therefore wasn't reissued.
unchanged_cert() {
  # Parameters:
  #   $1 (DOMAIN)	The primary domain name, i.e. the certificate common name (CN).
  #   $2 (KEYFILE)	The path of the file containing the private key.
  #   $3 (CERTFILE)	The path of the file containing the signed certificate.
  #   $4 (FULLCHAINFILE) The path of the file containing the full certificate chain.
  #   $5 (CHAINFILE)	The path of the file containing the intermediate certificate(s).

  local DOMAIN="$1" KEYFILE="$2" CERTFILE="$3" FULLCHAINFILE="$4" CHAINFILE="$5"

  return 0
}


# Called if the challenge response has failed, so domain owners can be aware and act accordingly.
invalid_challenge() {
  # Parameters:
  #   $1 (DOMAIN)	The primary domain name, i.e. the certificate common name (CN).
  #   $2 (RESPONSE)	The response that the verification server returned

  local DOMAIN="$1" RESPONSE="$2"

  # Notify the sysadmin.
  notify "error" "Validation of '$DOMAIN' failed:" "$RESPONSE"

  return 0
}


# Called when an HTTP request fails (e.g., when the ACME server is busy, returns an error, etc).
# It will be called upon any response code that does not start with '2'.  Useful to alert admins about problems with requests.
request_failure() {
  # Parameters:
  #   $1 (STATUSCODE)	The HTML status code that originated the error.
  #   $2 (REASON)	The specified reason for the error.
  #   $3 (REQTYPE)	The kind of request that was made (GET, POST...)

  local STATUSCODE="$1" REASON="$2" REQTYPE="$3" HEADERS="$4"

  # Notify the sysadmin.
  notify "error" "HTTP '$REQTYPE' request failed for '$DOMAIN' with code '$STATUSCODE'" "Reason: $REASON" "Headers:" "$HEADERS"

  return 0
}


# Called before any certificate signing operation takes place.
# It can be used to generate or fetch a certificate signing request with external tools.
# The output should be just the cerificate signing request formatted as PEM.
generate_csr() {
  # Parameters:
  #   $1 (DOMAIN)	The primary domain as specified in domains.txt.
  #			This does not need to match with the domains in the CSR, it's basically just the directory name.
  #   $2 (CERTDIR)	Certificate output directory for this particular certificate.
  #			Can be used for storing additional files.
  #   $3 (ALTNAMES)	All domain names for the current certificate as specified in domains.txt.
  #			Again, this doesn't need to match with the CSR, it's just there for convenience.

  local DOMAIN="$1" CERTDIR="$2" ALTNAMES="$3"

  # Simple example: Look for pre-generated CSRs
  # [[ -e "$CERTDIR/pre-generated.csr" ]] && cat "$CERTDIR/pre-generated.csr"

  return 0
}


# Called before the cron command to do some initial tasks (e.g. starting a webserver).
startup_hook() {
  local LOG_PREFIX="Dehydrated startup"

  # The 'root' user is required.
  (( $(id -u) != 0 )) && {
    notify "error" "Must be running as 'root' -- aborting"
    return 1
  }

  # Read services configuration (with sanity checks).
  services "1"

  # Make sure the certificates directory exists.
  [[ -n "$CERTSDIR" ]] && {
    umask 022
    # shellcheck disable=SC2174
    mkdir -p -m 0755 "$CERTSDIR" 2>/dev/null || {
      notify "error" "Failed to create certificate storage directory '$CERTSDIR' -- aborting"
      return 1
    }
  }

  # If an HTTP daemon is not already running, start it.
  [[ -n "${SERVICE_CTL['httpd']}" ]] && {
    pgrep -c ${SERVICE_PIDFILE['httpd']:+-F "${SERVICE_PIDFILE['httpd']}"} "${SERVICE_DAEMON['httpd']}" >/dev/null 2>&1 || {
      "${SERVICE_CTL['httpd']}" "start" >/dev/null 2>&1
      sleep 5
      if pgrep -c ${SERVICE_PIDFILE['httpd']:+-F "${SERVICE_PIDFILE['httpd']}"} "${SERVICE_DAEMON['httpd']}" >/dev/null 2>&1; then
        # Set a marker (used in exit_hook()) to signal that the HTTP daemon should be stopped at the end of deployments.
        touch "/run/dehydrated-http-daemon-stop-marker" 2>/dev/null || notify "warning" "Failed to create HTTP daemon stop marker - HTTP daemon will be left running -- check server"
      else
        notify "error" "Failed to start HTTP daemon for nonce validation -- aborting"
        return 1
      fi
    }

    # Add firewall rules to allow HTTP traffic so the nonce can be validated.
    { iptables -N dehydrated && ip6tables -N dehydrated && \
      iptables -I INPUT 1 -j dehydrated && ip6tables -I INPUT 1 -j dehydrated && \
      iptables -I dehydrated 1 -p tcp --dport 80 -j ACCEPT && ip6tables -I dehydrated 1 -p tcp --dport 80 -j ACCEPT; } >/dev/null 2>&1 || {
      notify "warning" "Failed to insert firewall rules to allow nonce validation"
      return 1
    }
  }

  return 0
}


# Called at the end of the cron command and can be used to do some final (cleanup or other) tasks.
exit_hook() {
  # Parameters:
  #   $1 (ERROR)	Contains error message if dehydrated exits with error.

  local ERROR="$1"
  local ERR=0 LOG_PREFIX="Dehydrated shutdown" SERVICE TIMEOUT=30

  # Read services configuration (without sanity check - this was already done at startup)
  services 0

  # If the reload marker was set, restart services.
  [[ -e "/run/dehydrated-reload-marker" ]] && {
    local IFS=$'\n'
    for SERVICE in $(printf "%s\\n" "${!SERVICE_CTL[@]}"); do
      # If the HTTP daemon is going to be shut down, there's no need to restart it.
      [[ "$SERVICE" == "httpd" ]] && [[ -e "/run/dehydrated-http-daemon-stop-marker" ]] && continue

      # Restart the services.
      "${SERVICE_CTL[\"$SERVICE\"]}" "restart" >/dev/null 2>&1 || {
        notify "error" "Controlled restart of service '$SERVICE' failed -- check server"
        ERR=1
        continue
      }
      sleep "$TIMEOUT"
      pgrep -c ${SERVICE_PIDFILE["$SERVICE"]:+-F "${SERVICE_PIDFILE[\"$SERVICE\"]}"} "${SERVICE_DAEMON[\"$SERVICE\"]}" >/dev/null 2>&1 || {
        notify "warning" "Service '$SERVICE' exited unexpectedly - trying to start again"
        "${SERVICE_CTL[\"$SERVICE\"]}" "start" >/dev/null 2>&1 || {
          notify "error" "Controlled start of service '$SERVICE' failed -- check server"
          ERR=1
          continue
        }
        sleep "$TIMEOUT"
      pgrep -c ${SERVICE_PIDFILE["$SERVICE"]:+-F "${SERVICE_PIDFILE[\"$SERVICE\"]}"} "${SERVICE_DAEMON[\"$SERVICE\"]}" >/dev/null 2>&1 || {
          notify "error" "Multiple failures (re)starting service '$SERVICE' -- check server"
          ERR=1
          continue
        }
      }
    done
  }

  # Remove the reload marker if all services restarted without issue.  Keep the marker if any failed.
  (( ERR == 0 )) && {
    rm -f "/run/dehydrated-reload-marker" 2>/dev/null || notify "warning" "Failed to remove services reload marker -- check server"
  }

  # If an HTTP daemon is configured, process the shutdown of it.
  [[ -n "${SERVICE_CTL['httpd']}" ]] && {
    # Delete firewall rules that were added to allow HTTP traffic.
    { iptables -C INPUT -j dehydrated && iptables -D INPUT -j dehydrated; } >/dev/null 2>&1
    { ip6tables -C INPUT -j dehydrated && ip6tables -D INPUT -j dehydrated; } >/dev/null 2>&1
    iptables -F dehydrated >/dev/null 2>&1
    ip6tables -F dehydrated >/dev/null 2>&1
    iptables -X dehydrated >/dev/null 2>&1
    ip6tables -X dehydrated >/dev/null 2>&1

    # If an HTTP daemon was started in startup_hook(), stop it now.
    ERR=0
    [[ -e "/run/dehydrated-http-daemon-stop-marker" ]] && {
      pgrep -c ${SERVICE_PIDFILE['httpd']:+-F "${SERVICE_PIDFILE['httpd']}"} "${SERVICE_DAEMON['httpd']}" >/dev/null 2>&1 && {
        "${SERVICE_CTL['httpd']}" "stop" >/dev/null 2>&1 || notify "error" "Controlled stop service of 'httpd' failed -- check server"
        sleep "$TIMEOUT"
        pgrep -c ${SERVICE_PIDFILE['httpd']:+-F "${SERVICE_PIDFILE['httpd']}"} "${SERVICE_DAEMON['httpd']}" >/dev/null 2>&1 && {
          pkill -TERM ${SERVICE_PIDFILE['httpd']:+-F "${SERVICE_PIDFILE['httpd']}"} "${SERVICE_DAEMON['httpd']}" >/dev/null 2>&1 || notify "warning" "Failed to SIGTERM service 'httpd' -- check server"
          sleep "$TIMEOUT"
          pgrep -c ${SERVICE_PIDFILE['httpd']:+-F "${SERVICE_PIDFILE['httpd']}"} "${SERVICE_DAEMON['httpd']}" >/dev/null 2>&1 && {
            pkill -KILL ${SERVICE_PIDFILE['httpd']:+-F "${SERVICE_PIDFILE['httpd']}"} "${SERVICE_DAEMON['httpd']}" >/dev/null 2>&1 || notify "warning" "Failed to SIGKILL service 'httpd' -- check server"
            sleep 5
          }
        }
        pgrep -c ${SERVICE_PIDFILE['httpd']:+-F "${SERVICE_PIDFILE['httpd']}"} "${SERVICE_DAEMON['httpd']}" >/dev/null 2>&1 && notify "error" "Failed to stop HTTP daemon that dehydrated started" && ERR=1
      }
    }

    # If the HTTP daemon was stopped correctly, remove the stop marker.
    (( ERR == 0 )) && {
      rm -f "/run/dehydrated-http-daemon-stop-marker" 2>/dev/null || notify "warning" "Failed to remove HTTP daemon stop marker -- check server"
    }
  }

  return 0
}

# Run the correct function.
HANDLER="$1"
shift
if declare -pF "$HANDLER" >/dev/null 2>&1; then
  "$HANDLER" "$@"
  exit "$?"
else
  exit 0
fi
