acme-tool/acme-tool

527 lines
16 KiB
Bash
Executable File

#!/bin/bash
# Internal Initialization
if [[ -d "${PWD}/.acme.sh" ]]; then
export LE_WORKING_DIR="${PWD}/.acme.sh"
export AWS_CONFIG_FILE="${PWD}/.acme.sh/aws/config"
export AWS_SHARED_CREDENTIALS_FILE="${PWD}/.acme.sh/aws/credentials"
elif [[ -d "${HOME}/.acme.sh" ]]; then
export LE_WORKING_DIR="${HOME}/.acme.sh"
export AWS_CONFIG_FILE="${HOME}/.acme.sh/aws/config"
export AWS_SHARED_CREDENTIALS_FILE="${HOME}/.acme.sh/aws/credentials"
elif [[ -d "/etc/acme" ]]; then
export LE_WORKING_DIR="/etc/acme"
export AWS_CONFIG_FILE="/etc/acme/aws/config"
export AWS_SHARED_CREDENTIALS_FILE="/etc/acme/aws/credentials"
else
if [[ "${1,,}" != "install" ]]; then
echo "ERROR: Cannot find acme.sh working directory in:"
echo " \"${PWD}/.acme.sh\""
echo " \"${HOME}/.acme.sh\""
echo " \"/etc/acme\""
echo "Either one of these directories need to exist to act on behalve of acme.sh's"
echo "home directory for configuration, hooks, and certificates."
exit 99
fi
fi
script_name=$(readlink -e $0)
script_dir=$(dirname "$script_name")
if [[ -r "${LE_WORKING_DIR}/acme-tool.conf" ]]; then
. "${LE_WORKING_DIR}/acme-tool.conf"
else
echo "ACME-Tool Configuration not found in '${LE_WORKING_DIR}/acme-tool.conf'"
exit 99
fi
if [[ ! -d "${LE_WORKING_DIR}/domains" ]]; then
mkdir "${LE_WORKING_DIR}/domains"
fi
if [[ -d "${PWD}/hooks/pre.d" || -d "${PWD}/hooks/post.d" ]]; then
hook_dir="${PWD}/hooks"
elif [[ -d "${LE_WORKING_DIR}/hooks/pre.d" || -d "${LE_WORKING_DIR}/hooks/post.d" ]]; then
hook_dir="${LE_WORKING_DIR}/hooks"
elif [[ -d "/etc/acme/hooks/pre.d" || -d "/etc/acme/hooks/post.d" ]]; then
hook_dir="/etc/acme/hooks"
else
if [[ "${1,,}" != "install" ]]; then
echo "Hook dir does not exist in at least one of the following paths:"
echo " \"${PWD}/hooks\""
echo " \"${LE_WORKING_DIR}/hooks\""
echo " \"/etc/acme/hooks\""
echo "Either of these directories need to contain one or both of pre.d and post.d"
echo "directories for hooks to run."
exit 99
fi
fi
# Cleanup (Just in case...)
if [[ -f "${LE_WORKING_DIR}/hooks/renew.flg" ]]; then
rm -f "${LE_WORKING_DIR}/hooks/renew.flg"
fi
# Functions
trim() {
local extglobWasOff=1
local var=$1
shopt extglob >/dev/null && extglobWasOff=0
(( extglobWasOff )) && shopt -s extglob
var=${var##+([[:space:]])}
var=${var%%+([[:space:]])}
(( extglobWasOff )) && shopt -u extglob
echo -n "$var"
}
run-parts() {
# Ignore *~ and *, scripts
for i in $(LC_ALL=C; echo ${1%/}/*[^~,]) ; do
[[ -d $i ]] && continue
# Don't run *.{rpmsave,rpmorig,rpmnew,swp,cfsaved} scripts
[[ "${i%.cfsaved}" != "${i}" ]] && continue
[[ "${i%.rpmsave}" != "${i}" ]] && continue
[[ "${i%.rpmorig}" != "${i}" ]] && continue
[[ "${i%.rpmnew}" != "${i}" ]] && continue
[[ "${i%.swp}" != "${i}" ]] && continue
[[ "${i%,v}" != "${i}" ]] && continue
# jobs.deny prevents specific files from being executed
# jobs.allow prohibits all non-named jobs from being run.
# can be used in conjunction but there's no reason to do so.
if [[ -r "${1}/jobs.deny" ]]; then
grep -q "^$(basename $i)$" "${1}/jobs.deny" && continue
fi
if [[ -r "${1}/jobs.allow" ]]; then
grep -q "^$(basename $i)$" "${1}/jobs.allow" || continue
fi
if [[ -e $i ]]; then
if [[ -r "${1}/whitelist" ]]; then
grep -q "^$(basename $i)$" "${1}/whitelist" && continue
fi
if [[ -x "$i" ]]; then
# run executable files
echo "$i"
fi
fi
done
}
run-hook() {
local hook=$1
local errors=0
shift
if [[ ! -d "${hook_dir}/${hook}" ]]; then
return 0
fi
while read s
do
echo "Running hooks in ${hook}:$(basename "$s")"
"$s" $*
err=$?
if [[ $err -ne 0 ]]; then
echo "Error running hooks for ${hook}:$(basename "$s")"
let errors++
fi
done < <(run-parts "${hook_dir}/${hook}")
return $errors
}
get_arg_domains() {
local domain=$1
if [[ ! -f "${LE_WORKING_DIR}/domains/${domain}" ]]; then
return 1
fi
while read d; do
d=$(trim "$d")
if [[ ${d:0:1} == '#' ]] || [[ -z "$d" ]]; then
continue
fi
echo -n " -d $d"
done < "${LE_WORKING_DIR}/domains/${domain}"
}
get_conf_domains() {
local di
local dn
while read di;
do
echo "$di"
done < <(find "${LE_WORKING_DIR}/domains/" -mindepth 1 -maxdepth 1 -type f -exec basename "{}" \; | sort)
}
get_acme_domains() {
local di
while read di;
do
echo "$di"
done < <(find "${LE_WORKING_DIR}/" -mindepth 1 -maxdepth 1 -type d -name '*.*' -exec basename "{}" \; | sed -e 's/_ecc//' | uniq | sort)
}
get_acme_domains_types() {
local domain=$1
if [[ -d "${LE_WORKING_DIR}/${domain}" ]]; then
echo -n "(rsa)"
fi
if [[ -d "${LE_WORKING_DIR}/${domain}_ecc" ]]; then
echo -n "(ecc)"
fi
}
check_domains_file() {
local domain=$1
if [[ -f "${LE_WORKING_DIR}/domains/${domain}" ]]; then
return 0
else
return 1
fi
}
is_certs_different() {
local domain=$1
local file_domains
local cert_domains
if check_domains_file "$domain"; then
if [[ ! -f "${LE_WORKING_DIR}/${domain}/${domain}.cer" ]] && \
[[ ! -f "${LE_WORKING_DIR}/${domain}_ecc/${domain}.cer" ]]; then
return 1
fi
else
echo "ERROR: '${LE_WORKING_DIR}/domains/${domain}' does not exist."
exit 3
fi
cert_domains=$(
openssl x509 -in certs/"${domain}"/"${domain}".cer -noout -text | awk '
/X509v3 Subject Alternative Name/ {
getline
gsub(/ /, "", $0)
gsub(/DNS:/, "", $0)
gsub(/IPAddress:/, "", $0)
gsub(",", "\n")
print
}' | sort | tr -d '\n')
file_domains=$(sort < "${LE_WORKING_DIR}/domains/${domain}" | tr -d '\n')
if [[ "$cert_domains" == "$file_domains" ]]; then
return 0
else
return 1
fi
}
issue_certs() {
local domain=$1
shift
local args=$*
if [[ -d "${LE_WORKING_DIR}/${domain}" ]]; then
echo "Running Lets Encrypt on $domain for RSA${keysize_rsa}"
"$LE_WORKING_DIR"/acme.sh \
--issue --dns dns_aws --keylength $keysize_rsa \
--post-hook "$script_name hook deploy.d" \
$(get_arg_domains "$domain") $args
fi
if [[ -d "${LE_WORKING_DIR}/${domain}_ecc" ]]; then
echo "Running Lets Encrypt on $domain for EC${keysize_ecc}"
"$LE_WORKING_DIR"/acme.sh \
--issue --dns dns_aws --keylength ec-$keysize_ecc \
--post-hook "$script_name hook deploy.d" \
$(get_arg_domains "$domain") $args
fi
}
cron_certs() {
if [[ "$cron_issue" == true ]]; then
"${LE_WORKING_DIR}"/acme.sh --cron --home "${LE_WORKING_DIR}" --renew-hook "${script_name} hook renew.d"
if [[ -r "${LE_WORKING_DIR}/hooks/renew.flg" ]]; then
rm -f "${LE_WORKING_DIR}/hooks/renew.flg"
if [[ "$cron_upload" == true ]]; then
s3_upload
else
run-hook deploy.d
fi
fi
elif [[ "$cron_download" == true ]]; then
if s3_check; then
s3_download
fi
fi
}
create_certs() {
local domain=$1
shift
local types=$*
for t in $types; do
case ${t,,} in
rsa)
if [[ ! -d "${LE_WORKING_DIR}/${domain}" ]]; then
mkdir "${LE_WORKING_DIR}/${domain}"
else
if [[ -f "${LE_WORKING_DIR}/${domain}/${domain}.cer" ]]; then
echo "ERROR: '$domain' already initialized"
fi
fi
;;
ec|ecc)
if [[ ! -d "${LE_WORKING_DIR}/${domain}_ecc" ]]; then
mkdir "${LE_WORKING_DIR}/${domain}_ecc"
else
if [[ -f "${LE_WORKING_DIR}/${domain}_ecc/${domain}.cer" ]]; then
echo "ERROR: '$domain' already initialized"
fi
fi
;;
esac
done
issue_certs $domain
}
s3_upload() {
local domain
local dompart
local errors=0
local totalerrors=0
for domain in $(get_acme_domains); do
errors=0
for dompart in "${domain}" "${domain}_ecc"; do
if [[ -d "${LE_WORKING_DIR}/${dompart}" ]]; then
echo "Uploading certs for ${domain}:${dompart}"
run-hook pre.d "$domain"
aws --exact-timestamps s3 sync "${LE_WORKING_DIR}/${dompart}/" "${s3_bucket}${s3_folder}${dompart}/"
if [[ $? -ne 0 ]]; then
echo "Error uploading ${domain}:${dompart}"
let errors++
fi
run-hook post.d "$domain"
fi
done
if [[ $errors -eq 0 ]]; then
run-hook deploy.d "$domain"
fi
totalerrors=$((totalerrors+errors))
done
return $totalerrors
}
s3_check() {
local domain
local dompart
local status=1
for domain in $(get_acme_domains); do
for dompart in "$domain" "${domain}_ecc"; do
if [[ -d "${LE_WORKING_DIR}/${dompart}" ]]; then
aws --dryrun --exact-timestamps s3 sync "${s3_bucket}${s3_folder}${dompart}/" "${LE_WORKING_DIR}/${dompart}/" | grep download &>/dev/null
if [[ $? -eq 0 ]]; then
status=0
fi
fi
done
done
return $status
}
s3_show() {
local domain
local dompart
for domain in $(get_acme_domains); do
for dompart in "$domain" "${domain}_ecc"; do
if [[ -d "${LE_WORKING_DIR}/${dompart}" ]]; then
aws --dryrun --exact-timestamps s3 sync "${s3_bucket}${s3_folder}${dompart}/" "${LE_WORKING_DIR}/${dompart}/" | sed -e "s|.* to .*\/\(${dompart}.*\)$|\1|"
fi
done
done
}
s3_download() {
local domain
local dompart
local errors=0
local totalerrors=0
for domain in $(get_acme_domains); do
errors=0
for dompart in "${domain}" "${domain}_ecc"; do
if [[ -d "${LE_WORKING_DIR}/${dompart}" ]]; then
echo "Downloading certs for ${domain}:${dompart}"
aws --exact-timestamps s3 sync "${s3_bucket}${s3_folder}${dompart}/" "${LE_WORKING_DIR}/${dompart}/"
if [[ $? -ne 0 ]]; then
echo "Error downloading certs in ${domain}:${dompart}"
let errors++
fi
fi
done
totalerrors=$((totalerrors+errors))
done
if [[ $totalerrors -eq 0 ]]; then
run-hook deploy.d "$domain"
fi
return $totalerrors
}
install_system() {
local acme_basedir
if [[ "$USER" == "root" ]]; then
acme_basedir="/etc/acme"
else
acme_basedir="${HOME}/.acme.sh"
fi
mkdir -p "${acme_basedir}/domains"
mkdir -p "${acme_basedir}/hooks/pre.d"
mkdir -p "${acme_basedir}/hooks/post.d"
mkdir -p "${acme_basedir}/hooks/sync.d"
mkdir -p "${acme_basedir}/hooks/deploy.d"
}
# Main
case ${1,,} in
install)
install_system
;;
init)
if [[ -n "$2" ]] && [[ -n "$3" ]]; then
do=${2,,}
shift 2
if [[ -n "$EDITOR" ]]; then
$EDITOR "${LE_WORKING_DIR}/domains/${do}"
fi
create_certs $do $*
else
echo "ERROR: init requires a domain and type"
exit 6
fi
;;
issue|update)
for d in $(get_conf_domains); do
if is_certs_different $d; then
echo "$d: Match"
else
echo "$d: Changes"
issue_certs "$d" --force
fi
done
;;
list)
echo "Configured Domains:"
get_conf_domains
echo
echo "ACME Domains:"
for d in $(get_acme_domains); do
echo "$d $(get_acme_domains_types "$d")"
done
;;
sync)
case "${2,,}" in
upload)
echo "Synchronizing Let's Encrypt Certificates (upload)"
s3_upload
err=$?
if [[ $err -ne 0 ]]; then
echo "Upload(s) failed: $err"
fi
echo "Done"
;;
check|"")
echo "Checking for new Let's Encrypt Certificates"
if s3_check; then
echo "There are new certificates available"
else
echo "No new certificates available"
fi
;;
show)
echo "Listing updated Let's Encrypt Certificates:"
s3_show
;;
down|download)
echo "Synchronizing Let's Encrypt Certificates (download)"
if s3_check; then
s3_download
err=$?
if [[ $err -ne 0 ]]; then
echo "Download(s) failed: $err"
fi
echo "Done"
fi
;;
*)
echo "ERROR: Unknown sync command. Available sync commands:"
echo " check, show, download, upload"
exit 9
;;
esac
;;
edit)
if [[ -z "$EDITOR" ]]; then
echo "ERROR: EDITOR environment not set"
exit 3
fi
for d in $(get_conf_domains); do
if [[ "$d" == "${2,,}" ]]; then
echo "Editing domain: $d"
$EDITOR "${LE_WORKING_DIR}/domains/${d}"
exit 0
fi
done
;;
hook)
if [[ -n "$2" ]]; then
#shift 2
case "${2,,}" in
pre.d) run-hook pre.d $*;;
post.d) run-hook post.d $*;;
sync.d) run-hook sync.d $*;;
deploy.d) run-hook deploy.d $*;;
renew.d) touch "${LE_WORKING_DIR}/hooks/renew.flg";;
*) echo "ERROR: Unknown hook \"${2,,}\". Available hooks:"
echo " pre.d Before running issue/renew/sync"
echo " post.d After running issue/renew/sync"
echo " sync.d After running issue/renew"
echo " deploy.d After successfully running issue/renew/sync"
exit 6
;;
esac
fi
;;
cron)
cron_certs
;;
*)
echo "Unknown command '$1'. Available commands are: init, issue, list, sync, cron"
;;
esac