You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

304 lines
8.8 KiB

  1. #!/bin/sh
  2. # Wrapper for acme.sh to work on openwrt.
  3. #
  4. # This program is free software; you can redistribute it and/or modify it under
  5. # the terms of the GNU General Public License as published by the Free Software
  6. # Foundation; either version 3 of the License, or (at your option) any later
  7. # version.
  8. #
  9. # Author: Toke Høiland-Jørgensen <toke@toke.dk>
  10. CHECK_CRON=$1
  11. ACME=/usr/lib/acme/acme.sh
  12. export CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
  13. export NO_TIMESTAMP=1
  14. UHTTPD_LISTEN_HTTP=
  15. STATE_DIR='/etc/acme'
  16. ACCOUNT_EMAIL=
  17. DEBUG=0
  18. NGINX_WEBSERVER=0
  19. UPDATE_NGINX=0
  20. UPDATE_UHTTPD=0
  21. . /lib/functions.sh
  22. check_cron()
  23. {
  24. [ -f "/etc/crontabs/root" ] && grep -q '/etc/init.d/acme' /etc/crontabs/root && return
  25. echo "0 0 * * * /etc/init.d/acme start" >> /etc/crontabs/root
  26. /etc/init.d/cron start
  27. }
  28. log()
  29. {
  30. logger -t acme -s -p daemon.info "$@"
  31. }
  32. err()
  33. {
  34. logger -t acme -s -p daemon.err "$@"
  35. }
  36. debug()
  37. {
  38. [ "$DEBUG" -eq "1" ] && logger -t acme -s -p daemon.debug "$@"
  39. }
  40. get_listeners() {
  41. local proto rq sq listen remote state program
  42. netstat -nptl 2>/dev/null | while read proto rq sq listen remote state program; do
  43. case "$proto#$listen#$program" in
  44. tcp#*:80#[0-9]*/*) echo -n "${program%% *} " ;;
  45. esac
  46. done
  47. }
  48. pre_checks()
  49. {
  50. main_domain="$1"
  51. log "Running pre checks for $main_domain."
  52. listeners="$(get_listeners)"
  53. debug "port80 listens: $listeners"
  54. for listener in $(get_listeners); do
  55. pid="${listener%/*}"
  56. cmd="${listener#*/}"
  57. case "$cmd" in
  58. uhttpd)
  59. debug "Found uhttpd listening on port 80; trying to disable."
  60. UHTTPD_LISTEN_HTTP=$(uci get uhttpd.main.listen_http)
  61. if [ -z "$UHTTPD_LISTEN_HTTP" ]; then
  62. err "$main_domain: Unable to find uhttpd listen config."
  63. err "Manually disable uhttpd or set webroot to continue."
  64. return 1
  65. fi
  66. uci set uhttpd.main.listen_http=''
  67. uci commit uhttpd || return 1
  68. if ! /etc/init.d/uhttpd reload ; then
  69. uci set uhttpd.main.listen_http="$UHTTPD_LISTEN_HTTP"
  70. uci commit uhttpd
  71. return 1
  72. fi
  73. ;;
  74. nginx*)
  75. debug "Found nginx listening on port 80; trying to disable."
  76. NGINX_WEBSERVER=1
  77. local tries=0
  78. while grep -sq "$cmd" "/proc/$pid/cmdline" && kill -0 "$pid"; do
  79. /etc/init.d/nginx stop
  80. if [ $tries -gt 10 ]; then
  81. debug "Can't stop nginx. Terminating script."
  82. return 1
  83. fi
  84. debug "Waiting for nginx to stop..."
  85. tries=$((tries + 1))
  86. sleep 1
  87. done
  88. ;;
  89. "")
  90. debug "Nothing listening on port 80."
  91. ;;
  92. *)
  93. err "$main_domain: Cannot run in standalone mode; another daemon is listening on port 80."
  94. err "Disable other daemon or set webroot to continue."
  95. return 1
  96. ;;
  97. esac
  98. done
  99. iptables -I input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" || return 1
  100. ip6tables -I input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" || return 1
  101. debug "v4 input_rule: $(iptables -nvL input_rule)"
  102. debug "v6 input_rule: $(ip6tables -nvL input_rule)"
  103. return 0
  104. }
  105. post_checks()
  106. {
  107. log "Running post checks (cleanup)."
  108. # The comment ensures we only touch our own rules. If no rules exist, that
  109. # is fine, so hide any errors
  110. iptables -D input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" 2>/dev/null
  111. ip6tables -D input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" 2>/dev/null
  112. if [ -e /etc/init.d/uhttpd ] && ( [ -n "$UHTTPD_LISTEN_HTTP" ] || [ "$UPDATE_UHTTPD" -eq 1 ] ); then
  113. if [ -n "$UHTTPD_LISTEN_HTTP" ]; then
  114. uci set uhttpd.main.listen_http="$UHTTPD_LISTEN_HTTP"
  115. UHTTPD_LISTEN_HTTP=
  116. fi
  117. uci commit uhttpd
  118. /etc/init.d/uhttpd reload
  119. fi
  120. if [ -e /etc/init.d/nginx ] && ( [ "$NGINX_WEBSERVER" -eq 1 ] || [ "$UPDATE_NGINX" -eq 1 ] ); then
  121. NGINX_WEBSERVER=0
  122. /etc/init.d/nginx restart
  123. fi
  124. }
  125. err_out()
  126. {
  127. post_checks
  128. exit 1
  129. }
  130. int_out()
  131. {
  132. post_checks
  133. trap - INT
  134. kill -INT $$
  135. }
  136. is_staging()
  137. {
  138. local main_domain="$1"
  139. grep -q "acme-staging" "$STATE_DIR/$main_domain/${main_domain}.conf"
  140. return $?
  141. }
  142. issue_cert()
  143. {
  144. local section="$1"
  145. local acme_args=
  146. local enabled
  147. local use_staging
  148. local update_uhttpd
  149. local update_nginx
  150. local keylength
  151. local domains
  152. local main_domain
  153. local moved_staging=0
  154. local failed_dir
  155. local webroot
  156. local dns
  157. local ret
  158. config_get_bool enabled "$section" enabled 0
  159. config_get_bool use_staging "$section" use_staging
  160. config_get_bool update_uhttpd "$section" update_uhttpd
  161. config_get_bool update_nginx "$section" update_nginx
  162. config_get domains "$section" domains
  163. config_get keylength "$section" keylength
  164. config_get webroot "$section" webroot
  165. config_get dns "$section" dns
  166. UPDATE_NGINX=$update_nginx
  167. UPDATE_UHTTPD=$update_uhttpd
  168. [ "$enabled" -eq "1" ] || return
  169. [ "$DEBUG" -eq "1" ] && acme_args="$acme_args --debug"
  170. set -- $domains
  171. main_domain=$1
  172. [ -n "$webroot" ] || [ -n "$dns" ] || pre_checks "$main_domain" || return 1
  173. log "Running ACME for $main_domain"
  174. handle_credentials() {
  175. local credential="$1"
  176. eval export $credential
  177. }
  178. config_list_foreach "$section" credentials handle_credentials
  179. if [ -e "$STATE_DIR/$main_domain" ]; then
  180. if [ "$use_staging" -eq "0" ] && is_staging "$main_domain"; then
  181. log "Found previous cert issued using staging server. Moving it out of the way."
  182. mv "$STATE_DIR/$main_domain" "$STATE_DIR/$main_domain.staging"
  183. moved_staging=1
  184. else
  185. log "Found previous cert config. Issuing renew."
  186. $ACME --home "$STATE_DIR" --renew -d "$main_domain" $acme_args && ret=0 || ret=1
  187. post_checks
  188. return $ret
  189. fi
  190. fi
  191. acme_args="$acme_args $(for d in $domains; do echo -n "-d $d "; done)"
  192. acme_args="$acme_args --keylength $keylength"
  193. [ -n "$ACCOUNT_EMAIL" ] && acme_args="$acme_args --accountemail $ACCOUNT_EMAIL"
  194. [ "$use_staging" -eq "1" ] && acme_args="$acme_args --staging"
  195. if [ -n "$dns" ]; then
  196. log "Using dns mode"
  197. acme_args="$acme_args --dns $dns"
  198. elif [ -z "$webroot" ]; then
  199. log "Using standalone mode"
  200. acme_args="$acme_args --standalone --listen-v6"
  201. else
  202. if [ ! -d "$webroot" ]; then
  203. err "$main_domain: Webroot dir '$webroot' does not exist!"
  204. post_checks
  205. return 1
  206. fi
  207. log "Using webroot dir: $webroot"
  208. acme_args="$acme_args --webroot $webroot"
  209. fi
  210. if ! $ACME --home "$STATE_DIR" --issue $acme_args; then
  211. failed_dir="$STATE_DIR/${main_domain}.failed-$(date +%s)"
  212. err "Issuing cert for $main_domain failed. Moving state to $failed_dir"
  213. [ -d "$STATE_DIR/$main_domain" ] && mv "$STATE_DIR/$main_domain" "$failed_dir"
  214. if [ "$moved_staging" -eq "1" ]; then
  215. err "Restoring staging certificate"
  216. mv "$STATE_DIR/${main_domain}.staging" "$STATE_DIR/${main_domain}"
  217. fi
  218. post_checks
  219. return 1
  220. fi
  221. if [ -e /etc/init.d/uhttpd ] && [ "$update_uhttpd" -eq "1" ]; then
  222. uci set uhttpd.main.key="$STATE_DIR/${main_domain}/${main_domain}.key"
  223. uci set uhttpd.main.cert="$STATE_DIR/${main_domain}/fullchain.cer"
  224. # commit and reload is in post_checks
  225. fi
  226. if [ -e /etc/init.d/nginx ] && [ "$update_nginx" -eq "1" ]; then
  227. sed -i "s#ssl_certificate\ .*#ssl_certificate $STATE_DIR/${main_domain}/fullchain.cer;#g" /etc/nginx/nginx.conf
  228. sed -i "s#ssl_certificate_key\ .*#ssl_certificate_key $STATE_DIR/${main_domain}/${main_domain}.key;#g" /etc/nginx/nginx.conf
  229. # commit and reload is in post_checks
  230. fi
  231. post_checks
  232. }
  233. load_vars()
  234. {
  235. local section="$1"
  236. STATE_DIR=$(config_get "$section" state_dir)
  237. ACCOUNT_EMAIL=$(config_get "$section" account_email)
  238. DEBUG=$(config_get "$section" debug)
  239. }
  240. check_cron
  241. [ -n "$CHECK_CRON" ] && exit 0
  242. [ -e "/var/run/acme_boot" ] && rm -f "/var/run/acme_boot" && exit 0
  243. config_load acme
  244. config_foreach load_vars acme
  245. if [ -z "$STATE_DIR" ] || [ -z "$ACCOUNT_EMAIL" ]; then
  246. err "state_dir and account_email must be set"
  247. exit 1
  248. fi
  249. [ -d "$STATE_DIR" ] || mkdir -p "$STATE_DIR"
  250. trap err_out HUP TERM
  251. trap int_out INT
  252. config_foreach issue_cert cert
  253. exit 0