acme: Support running in webroot mode, detect other daemons on port 80
authorToke Høiland-Jørgensen <redacted>
Sun, 9 Apr 2017 16:43:34 +0000 (18:43 +0200)
committerToke Høiland-Jørgensen <redacted>
Wed, 26 Apr 2017 14:32:24 +0000 (16:32 +0200)
For configurations where another web server is running on port 80, running
acme.sh in standalone mode fails. Try to detect this and refuse to run; and
allow the user to configure a webroot directory to use the running webserver for
certificate verification.

This also updates acme.sh to the latest version.

Signed-off-by: Toke Høiland-Jørgensen <redacted>
net/acme/Makefile
net/acme/files/acme-cbi.lua
net/acme/files/acme.config
net/acme/files/run.sh

index a7f0664508f0be6dd87dfc44480ac5e1f64d3ade..88148545bb1bba6eb48590d485c14e7cfdced932 100644 (file)
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
 
 PKG_NAME:=acme
 PKG_SOURCE_VERSION:=7b40cbe8c1a52041351524bcde4b37665a7cdf79
-PKG_VERSION:=1.5
+PKG_VERSION:=1.6
 PKG_RELEASE:=1
 PKG_LICENSE:=GPLv3
 
@@ -47,6 +47,7 @@ define Build/Compile
 endef
 
 define Package/acme/install
+       $(INSTALL_DIR) $(1)/etc/acme
        $(INSTALL_DIR) $(1)/etc/config
        $(INSTALL_CONF) ./files/acme.config $(1)/etc/config/acme
        $(INSTALL_DIR) $(1)/etc/init.d
index a4f7956de76624b5c4750a4cd439b6563465582b..c20cba203428e084521d58adc9280e8f0c8f17c6 100644 (file)
@@ -25,11 +25,12 @@ s.anonymous = true
 st = s:option(Value, "state_dir", translate("State directory"),
               translate("Where certs and other state files are kept."))
 st.rmempty = false
-st.datatype = "string"
+st.datatype = "directory"
 
 ae = s:option(Value, "account_email", translate("Account email"),
               translate("Email address to associate with account key."))
 ae.rmempty = false
+ae.datatype = "minlength(1)"
 
 d = s:option(Flag, "debug", translate("Enable debug logging"))
 d.rmempty = false
@@ -56,6 +57,12 @@ u = cs:option(Flag, "update_uhttpd", translate("Use for uhttpd"),
                         "(only select this for one certificate)."))
 u.rmempty = false
 
+wr = cs:option(Value, "webroot", translate("Webroot directory"),
+               translate("Webserver root directory. Set this to the webserver " ..
+                         "document root to run Acme in webroot mode. The web " ..
+                         "server must be accessible from the internet on port 80."))
+wr.rmempty = false
+
 dom = cs:option(DynamicList, "domains", translate("Domain names"),
                 translate("Domain names to include in the certificate. " ..
                           "The first name will be the subject name, subsequent names will be alt names. " ..
index c5cd7d3eab7c61cb78489c2aeddf9174a56dae42..af12ce1fb008e2f2f2f37e4dd1ae389569e7f3ab 100644 (file)
@@ -5,7 +5,8 @@ config acme
 
 config cert 'example'
        option enabled 0
-       option use_staging 0
+       option use_staging 1
        option keylength 2048
        option update_uhttpd 1
+       option webroot ""
        list domains example.org
index 0a4cad1c55771351d365fcf97368c707fcdc0aa3..6bedaca16770aaddd6ea85fb9fe0176aaa57fce2 100644 (file)
@@ -27,45 +27,85 @@ check_cron()
     /etc/init.d/cron start
 }
 
-debug()
+log()
 {
-    [ "$DEBUG" -eq "1" ] && echo "$@" >&2
+    logger -t acme -s -p daemon.info "$@"
 }
 
-pre_checks()
+err()
 {
-    echo "Running pre checks."
-    check_cron
-
-    [ -d "$STATE_DIR" ] || mkdir -p "$STATE_DIR"
-
-    if [ -e /etc/init.d/uhttpd ]; then
+    logger -t acme -s -p daemon.err "$@"
+}
 
-       UHTTPD_LISTEN_HTTP=$(uci get uhttpd.main.listen_http)
+debug()
+{
+    [ "$DEBUG" -eq "1" ] && logger -t acme -s -p daemon.debug "$@"
+}
 
-       uci set uhttpd.main.listen_http=''
-       uci commit uhttpd
-       /etc/init.d/uhttpd reload || return 1
-    fi
+get_listeners()
+{
+    netstat -nptl 2>/dev/null | awk 'match($4, /:80$/){split($7, parts, "/"); print parts[2];}' | uniq | tr "\n" " "
+}
 
-    iptables -I input_rule -p tcp --dport 80 -j ACCEPT || return 1
-    ip6tables -I input_rule -p tcp --dport 80 -j ACCEPT || return 1
+pre_checks()
+{
+    main_domain="$1"
+
+    log "Running pre checks for $main_domain."
+
+    listeners="$(get_listeners)"
+    debug "port80 listens: $listeners"
+
+    case "$listeners" in
+        "uhttpd")
+            debug "Found uhttpd listening on port 80; trying to disable."
+
+            UHTTPD_LISTEN_HTTP=$(uci get uhttpd.main.listen_http)
+
+            if [ -z "$UHTTPD_LISTEN_HTTP" ]; then
+                err "$main_domain: Unable to find uhttpd listen config."
+                err "Manually disable uhttpd or set webroot to continue."
+                return 1
+            fi
+
+            uci set uhttpd.main.listen_http=''
+            uci commit uhttpd || return 1
+            if ! /etc/init.d/uhttpd reload ; then
+                uci set uhttpd.main.listen_http="$UHTTPD_LISTEN_HTTP"
+                uci commit uhttpd
+                return 1
+            fi
+            ;;
+        "")
+            debug "Nothing listening on port 80."
+            ;;
+        *)
+            err "$main_domain: Cannot run in standalone mode; another daemon is listening on port 80."
+            err "Disable other daemon or set webroot to continue."
+            return 1
+            ;;
+    esac
+
+    iptables -I input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" || return 1
+    ip6tables -I input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" || return 1
     debug "v4 input_rule: $(iptables -nvL input_rule)"
     debug "v6 input_rule: $(ip6tables -nvL input_rule)"
-    debug "port80 listens: $(netstat -ntpl | grep :80)"
     return 0
 }
 
 post_checks()
 {
-    echo "Running post checks (cleanup)."
-    iptables -D input_rule -p tcp --dport 80 -j ACCEPT
-    ip6tables -D input_rule -p tcp --dport 80 -j ACCEPT
+    log "Running post checks (cleanup)."
+    # The comment ensures we only touch our own rules. If no rules exist, that
+    # is fine, so hide any errors
+    iptables -D input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" 2>/dev/null
+    ip6tables -D input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" 2>/dev/null
 
-    if [ -e /etc/init.d/uhttpd ]; then
+    if [ -e /etc/init.d/uhttpd ] && [ -n "$UHTTPD_LISTEN_HTTP" ]; then
         uci set uhttpd.main.listen_http="$UHTTPD_LISTEN_HTTP"
         uci commit uhttpd
         /etc/init.d/uhttpd reload
+        UHTTPD_LISTEN_HTTP=
     fi
 }
 
@@ -102,12 +142,14 @@ issue_cert()
     local main_domain
     local moved_staging=0
     local failed_dir
+    local webroot
 
     config_get_bool enabled "$section" enabled 0
     config_get_bool use_staging "$section" use_staging
     config_get_bool update_uhttpd "$section" update_uhttpd
     config_get domains "$section" domains
     config_get keylength "$section" keylength
+    config_get webroot "$section" webroot
 
     [ "$enabled" -eq "1" ] || return
 
@@ -116,13 +158,17 @@ issue_cert()
     set -- $domains
     main_domain=$1
 
+    [ -n "$webroot" ] || pre_checks "$main_domain" || return 1
+
+    log "Running ACME for $main_domain"
+
     if [ -e "$STATE_DIR/$main_domain" ]; then
         if [ "$use_staging" -eq "0" ] && is_staging "$main_domain"; then
-            echo "Found previous cert issued using staging server. Moving it out of the way."
+            log "Found previous cert issued using staging server. Moving it out of the way."
             mv "$STATE_DIR/$main_domain" "$STATE_DIR/$main_domain.staging"
             moved_staging=1
         else
-            echo "Found previous cert config. Issuing renew."
+            log "Found previous cert config. Issuing renew."
             $ACME --home "$STATE_DIR" --renew -d "$main_domain" $acme_args || return 1
             return 0
         fi
@@ -130,17 +176,28 @@ issue_cert()
 
 
     acme_args="$acme_args $(for d in $domains; do echo -n "-d $d "; done)"
-    acme_args="$acme_args --standalone"
     acme_args="$acme_args --keylength $keylength"
     [ -n "$ACCOUNT_EMAIL" ] && acme_args="$acme_args --accountemail $ACCOUNT_EMAIL"
     [ "$use_staging" -eq "1" ] && acme_args="$acme_args --staging"
 
+    if [ -z "$webroot" ]; then
+        log "Using standalone mode"
+        acme_args="$acme_args --standalone"
+    else
+        if [ ! -d "$webroot" ]; then
+            err "$main_domain: Webroot dir '$webroot' does not exist!"
+            return 1
+        fi
+        log "Using webroot dir: $webroot"
+        acme_args="$acme_args --webroot \"$webroot\""
+    fi
+
     if ! $ACME --home "$STATE_DIR" --issue $acme_args; then
         failed_dir="$STATE_DIR/${main_domain}.failed-$(date +%s)"
-        echo "Issuing cert for $main_domain failed. Moving state to $failed_dir" >&2
+        err "Issuing cert for $main_domain failed. Moving state to $failed_dir"
         [ -d "$STATE_DIR/$main_domain" ] && mv "$STATE_DIR/$main_domain" "$failed_dir"
         if [ "$moved_staging" -eq "1" ]; then
-            echo "Restoring staging certificate" >&2
+            err "Restoring staging certificate"
             mv "$STATE_DIR/${main_domain}.staging" "$STATE_DIR/${main_domain}"
         fi
         return 1
@@ -152,6 +209,7 @@ issue_cert()
         # commit and reload is in post_checks
     fi
 
+    post_checks
 }
 
 load_vars()
@@ -163,19 +221,22 @@ load_vars()
     DEBUG=$(config_get "$section" debug)
 }
 
-if [ -n "$CHECK_CRON" ]; then
-    check_cron
-    exit 0
-fi
+check_cron
+[ -n "$CHECK_CRON" ] && exit 0
 
 config_load acme
 config_foreach load_vars acme
 
-pre_checks || exit 1
+if [ -z "$STATE_DIR" ] || [ -z "$ACCOUNT_EMAIL" ]; then
+    err "state_dir and account_email must be set"
+    exit 1
+fi
+
+[ -d "$STATE_DIR" ] || mkdir -p "$STATE_DIR"
+
 trap err_out HUP TERM
 trap int_out INT
 
 config_foreach issue_cert cert
-post_checks
 
 exit 0
git clone https://git.99rst.org/PROJECT