acme-cpanel.sh 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. #!/usr/bin/env bash
  2. # This script uses acme.sh to issue and deploy SSL certificates from Let's Encrypt for a list of domains
  3. # using the webroot or dns methods
  4. #
  5. # See README.md for more details
  6. #
  7. # Copyright 2020 Bryan Roessler <bryanroessler@gmail.com>
  8. #
  9. # USAGE
  10. # ./acme-cpanel.sh [OPTIONS] [FILES...]
  11. #
  12. # EXAMPLES
  13. # TESTING: ./acme-cpanel-webroot.sh --debug -e me@gmail.com multisites/flatwhitedesign.pw multisites/greengingermultisite.website
  14. # PRODUCTION: ./acme-cpanel-webroot.sh --force -e me@gmail.com multisites/flatwhitedesign.pw multisites/greengingermultisite.website
  15. #
  16. # TESTING: ./acme-cpanel-webroot.sh --debug -s multisites
  17. # PRODUCTION: ./acme-cpanel-webroot.sh --force -s multisites
  18. #
  19. # FILES is a list of files containing first-level DOMAIN names (see domains.txt) on newlines
  20. # Certificates will automatically be issued and deployed for DOMAIN and www.DOMAIN using the webroot method
  21. #
  22. # NOTE: The webroot method does NOT support wildcard domains, Let's Encrypt requires wildcard domains to
  23. # use DNS challenges, which the cPanel uapi does not support (use dns method instead)
  24. unset SITES_DIR USEREMAIL DOMAIN_FILES DOMAIN_GROUPS DEPLOY_CMD_PREFIX ISSUE_CMD_PREFIX DEBUG GROUP
  25. DEBUG="true" # quote this line to stop DEBUG mode and issue certificates for real, or use --force in user options
  26. METHOD="dns" # set the default method
  27. CONF="$HOME/.acme.sh/account.conf"
  28. ACME_SH="$HOME/.acme.sh/acme.sh"
  29. parse_input() {
  30. local input
  31. declare -g USEREMAIL
  32. declare -ag DOMAIN_FILES
  33. if input=$(getopt -o +m:e:fgs:d -l method:,email:,force,group-by-file,sites-dir:,debug -- "$@"); then
  34. eval set -- "$input"
  35. while true; do
  36. case "$1" in
  37. --method|-m)
  38. shift
  39. METHOD="${1,,}"
  40. ;;
  41. --force|-f)
  42. unset DEBUG
  43. ;;
  44. --group-by-file|-g)
  45. GROUP="true"
  46. ;;
  47. --sites-dir|-s)
  48. shift
  49. SITES_DIR="$1"
  50. ;;
  51. --debug|-d)
  52. DEBUG="true"
  53. ;;
  54. --)
  55. shift
  56. break
  57. ;;
  58. esac
  59. shift
  60. done
  61. else
  62. echo "Incorrect options provided"
  63. exit 1
  64. fi
  65. # Load domain files from remaining arguments
  66. if [[ $# -lt 1 ]]; then
  67. [[ -v SITES_DIR && -d "$SITES_DIR" ]] && return 0
  68. if [[ -f "domains.txt" ]]; then
  69. echo "You have not supplied any domain files, using domains.txt by default"
  70. DOMAIN_FILES=("domains.txt")
  71. else
  72. echo "You must specify a domain list or use domains.txt by default"
  73. exit 1
  74. fi
  75. else
  76. DOMAIN_FILES=("$@")
  77. fi
  78. }
  79. interactive_dns() {
  80. if [[ -f "$CONF" ]] && grep -q "CPANELDNS_AUTH_PASSWORD" "$CONF"; then
  81. echo "cPanel credentials already present, skipping configuration..."
  82. echo "To rerun the configuration, first run 'rm $CONF'"
  83. else
  84. read -rp 'Enter your cPanel username: ' CPANELDNS_AUTH_ID
  85. echo
  86. export CPANELDNS_AUTH_ID
  87. read -rp 'Enter your cPanel password: ' CPANELDNS_AUTH_PASSWORD
  88. echo
  89. export CPANELDNS_AUTH_PASSWORD
  90. read -rp 'Enter your cPanel address and port number (example: "https://www.example.com:2083/"): ' CPANELDNS_API
  91. echo
  92. export CPANELDNS_API
  93. fi
  94. }
  95. get_acme() {
  96. curl https://get.acme.sh | sh
  97. # shellcheck disable=SC1090
  98. source "$HOME/.bashrc"
  99. "$ACME_SH" --upgrade --auto-upgrade
  100. [[ "$METHOD" == "dns" ]] && \
  101. curl -o "$HOME/.acme.sh/dnsapi/dns_cpaneldns.sh" https://raw.githubusercontent.com/cryobry/dns_cpaneldns/master/dns_cpaneldns.sh
  102. }
  103. update_email() {
  104. if [[ ! -v USEREMAIL ]]; then
  105. if [[ -f "$CONF" ]] && line=$(grep -q "ACCOUNT_EMAIL" "$CONF"); then
  106. echo "Reusing existing contact e-mail: ${line#ACCOUNT_EMAIL=}"
  107. return 0
  108. fi
  109. read -rp 'Enter your contact e-mail (in case of renewal failures): ' USEREMAIL
  110. fi
  111. "$ACME_SH" --update-account --accountemail "${USEREMAIL}"
  112. }
  113. command_prefixes() {
  114. declare -ag ISSUE_CMD_PREFIX DEPLOY_CMD_PREFIX
  115. ISSUE_CMD_PREFIX=("$ACME_SH" "--issue")
  116. [[ "$METHOD" == "dns" ]] && ISSUE_CMD_PREFIX=("${ISSUE_CMD_PREFIX[@]}" "--dns" "dns_cpaneldns")
  117. [[ -v DEBUG ]] && ISSUE_CMD_PREFIX=("${ISSUE_CMD_PREFIX[@]}" "--staging") || ISSUE_CMD_PREFIX=("${ISSUE_CMD_PREFIX[@]}" "--force")
  118. DEPLOY_CMD_PREFIX=("$ACME_SH" "--deploy" "--deploy-hook" "cpanel_uapi")
  119. }
  120. get_webroot() {
  121. local webroot
  122. if ! webroot=$(uapi DomainInfo single_domain_data domain="$1" | grep documentroot); then
  123. echo "UAPI call failed" >&2
  124. fi
  125. if [[ ! -v webroot || "$webroot" == "" ]]; then
  126. if [[ -v DEBUG ]]; then
  127. webroot="/tmp" # set missing webroot in DEBUG mode for testing
  128. else
  129. echo "Could not find $1's webroot" >&2
  130. exit 1
  131. fi
  132. fi
  133. echo "$webroot"
  134. }
  135. # Either create a single array of all domains (DOMAINS) to issue one-by-one or create an array of array names to issue for a single webroot
  136. load_domains() {
  137. local domain_file
  138. declare -ag DOMAIN_GROUPS=()
  139. if [[ -v SITES_DIR ]]; then
  140. for domain_file in "$SITES_DIR"/*; do
  141. DOMAIN_GROUPS+=("$(<"$domain_file")")
  142. done
  143. fi
  144. for domain_file in "${DOMAIN_FILES[@]}"; do
  145. # Load list of domains as space-delimited strings in elements of the DOMAINS array
  146. # We can keep these separate or combine them later
  147. DOMAIN_GROUPS+=("$(<"$domain_file")")
  148. done
  149. }
  150. issue_and_deploy_certs() {
  151. local domain_root domain domain_group
  152. local -a issue_cmd=()
  153. local -a deploy_cmd=()
  154. if [[ -v GROUP ]]; then
  155. for domain_group in "${DOMAIN_GROUPS[@]}"; do
  156. unset i
  157. for domain in $domain_group; do # we want to split on whitespace
  158. [[ "$domain" == "" ]] && continue
  159. # Get the webroot from the first domain
  160. if [[ ! -v i ]]; then
  161. local i="set"
  162. domain_root=$(get_webroot "$domain")
  163. issue_cmd=("${ISSUE_CMD_PREFIX[@]}" "-w" "$domain_root")
  164. fi
  165. issue_cmd+=("-d" "$domain" "-d" "www.$domain")
  166. done
  167. # Issue certificate for entire domain group
  168. echo "Running:" "${issue_cmd[@]}"
  169. if ! "${issue_cmd[@]}"; then
  170. echo "Failed to issue certificate"
  171. fi
  172. # Deploy certificates one by one
  173. for domain in $domain_group; do
  174. deploy_cmd=("${DEPLOY_CMD_PREFIX[@]}" "-w" "$domain_root" "-d" "$domain")
  175. echo "Running:" "${deploy_cmd[@]}"
  176. "${deploy_cmd[@]}"
  177. done
  178. done
  179. else
  180. for domain_group in "${DOMAIN_GROUPS[@]}"; do
  181. # Issue and deploy certificates one by one
  182. for domain in $domain_group; do # we want to split on whitespace
  183. issue_cmd=("${ISSUE_CMD_PREFIX[@]}" "-d" "$domain" "-d" "www.$domain")
  184. [[ "$METHOD" == "webroot" ]] && domain_root=$(get_webroot "$domain") && issue_cmd=("${issue_cmd[@]}" "-w" "$domain_root")
  185. deploy_cmd=("${DEPLOY_CMD_PREFIX[@]}" "-d" "$domain") # I think we only need to deploy to the domain, not subdomains
  186. [[ "$METHOD" == "webroot" ]] && deploy_cmd=("${deploy_cmd[@]}" "-w" "$domain_root")
  187. echo "Running:" "${issue_cmd[@]}"
  188. if ! "${issue_cmd[@]}"; then
  189. echo "Failed to issue certificate for $domain"
  190. err=1
  191. fi
  192. echo "Running:" "${deploy_cmd[@]}"
  193. if ! "${deploy_cmd[@]}"; then
  194. echo "Failed to deploy certificate for $domain"
  195. err=1
  196. fi
  197. done
  198. done
  199. fi
  200. }
  201. main() {
  202. parse_input "$@"
  203. get_acme
  204. update_email
  205. command_prefixes
  206. load_domains
  207. [[ "$METHOD" == "dns" ]] && interactive_dns
  208. sanity_check
  209. issue_and_deploy_certs
  210. }
  211. main "$@"
  212. exit "${err:-0}"