#!/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