From e84f6514538f0638507ce8066f801374db6f4150 Mon Sep 17 00:00:00 2001 From: Glen Huang Date: Sat, 29 Jan 2022 20:54:35 +0800 Subject: [PATCH] acme: use the hotplug system Signed-off-by: Glen Huang --- net/acme-acmesh/Makefile | 69 ++++ net/acme-acmesh/files/hook.sh | 125 +++++++ net/acme-common/Makefile | 67 ++++ net/{acme => acme-common}/files/acme.config | 13 - net/acme-common/files/acme.init | 9 + net/acme-common/files/acme.sh | 142 +++++++ net/acme-common/files/acme.uci-defaults | 4 + net/acme-common/files/functions.sh | 7 + net/acme/Makefile | 54 +-- net/acme/files/acme.init | 33 -- net/acme/files/run.sh | 392 -------------------- net/haproxy/Makefile | 2 + net/haproxy/files/acme.hotplug | 12 + net/nginx/Makefile | 5 +- net/nginx/files/acme.hotplug | 3 + 15 files changed, 450 insertions(+), 487 deletions(-) create mode 100644 net/acme-acmesh/Makefile create mode 100644 net/acme-acmesh/files/hook.sh create mode 100644 net/acme-common/Makefile rename net/{acme => acme-common}/files/acme.config (57%) create mode 100644 net/acme-common/files/acme.init create mode 100644 net/acme-common/files/acme.sh create mode 100644 net/acme-common/files/acme.uci-defaults create mode 100644 net/acme-common/files/functions.sh delete mode 100644 net/acme/files/acme.init delete mode 100644 net/acme/files/run.sh create mode 100644 net/haproxy/files/acme.hotplug create mode 100644 net/nginx/files/acme.hotplug diff --git a/net/acme-acmesh/Makefile b/net/acme-acmesh/Makefile new file mode 100644 index 0000000000..cd6d4f3331 --- /dev/null +++ b/net/acme-acmesh/Makefile @@ -0,0 +1,69 @@ +# +# Copyright (C) 2016 Toke Høiland-Jørgensen +# +# This is free software, licensed under the GNU General Public License v3 or +# later. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=acme-acmesh +PKG_VERSION:=3.0.1 +PKG_RELEASE:=$(AUTORELEASE) + +PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz +PKG_SOURCE_URL:=https://codeload.github.com/acmesh-official/acme.sh/tar.gz/$(PKG_VERSION)? +PKG_HASH:=6212cc0c2bca99a7dd6cbb4236b4c7dd5d1113dab0841e66dae4d307d902a8e6 +PKG_BUILD_DIR:=$(BUILD_DIR)/acme.sh-$(PKG_VERSION) + +PKG_MAINTAINER:=Toke Høiland-Jørgensen +PKG_LICENSE:=GPL-3.0-only +PKG_LICENSE_FILES:=LICENSE.md + +include $(INCLUDE_DIR)/package.mk + +define Package/acme-acmesh + SECTION:=net + CATEGORY:=Network + DEPENDS:=+acme-common +wget-ssl +ca-bundle +openssl-util +socat + TITLE:=ACME client acme.sh wrapper script + URL:=https://acme.sh + PKGARCH:=all + PROVIDES:=acme-client +endef + +define Package/acme-acmesh/description +A client for issuing ACME (e.g, Letsencrypt) certificates. +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/acme-acmesh/install + $(INSTALL_DIR) $(1)/usr/lib/acme/client + $(INSTALL_BIN) $(PKG_BUILD_DIR)/acme.sh $(1)/usr/lib/acme/client + $(INSTALL_BIN) ./files/hook.sh $(1)/usr/lib/acme/hook +endef + +define Package/acme-acmesh-dnsapi + SECTION:=net + CATEGORY:=Network + DEPENDS:=+acme + TITLE:=DNS API integration for ACME (Letsencrypt) client + PKGARCH:=all +endef + +define Package/acme-acmesh-dnsapi/description + This package provides DNS API integration for ACME (Letsencrypt) client. +endef + +define Package/acme-acmesh-dnsapi/install + $(INSTALL_DIR) $(1)/usr/lib/acme/client/dnsapi + $(INSTALL_DATA) $(PKG_BUILD_DIR)/dnsapi/*.sh $(1)/usr/lib/acme/client/dnsapi +endef + +$(eval $(call BuildPackage,acme-acmesh)) +$(eval $(call BuildPackage,acme-acmesh-dnsapi)) diff --git a/net/acme-acmesh/files/hook.sh b/net/acme-acmesh/files/hook.sh new file mode 100644 index 0000000000..bd3825b753 --- /dev/null +++ b/net/acme-acmesh/files/hook.sh @@ -0,0 +1,125 @@ +#!/bin/sh +set -u +ACME=/usr/lib/acme/acme.sh +LOG_TAG=acme-acmesh +# webroot option deprecated, use the hardcoded value directly in the next major version +WEBROOT=${webroot:-/var/run/acme/challenge} + +# shellcheck source=net/acme/files/functions.sh +. /usr/lib/acme/functions.sh + +# Needed by acme.sh +export CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +export NO_TIMESTAMP=1 + +cmd="$1" + +case $cmd in +get) + set -- + [ "$debug" = 1 ] && set -- "$@" --debug + + case $keylength in + ec-*) + domain_dir="$state_dir/${main_domain}_ecc" + set -- "$@" --ecc + ;; + *) + domain_dir="$state_dir/$main_domain" + ;; + esac + + log info "Running ACME for $main_domain" + + if [ -e "$domain_dir" ]; then + if [ "$staging" = 0 ] && grep -q "acme-staging" "$domain_dir/$main_domain.conf"; then + mv "$domain_dir" "$domain_dir.staging" + log info "Certificates are previously issued from a staging server, but staging option is diabled, moved to $domain_dir.staging." + staging_moved=1 + else + set -- "$@" --renew --home "$state_dir" -d "$main_domain" + log info "$*" + trap 'ACTION=renewed-failed hotplug-call acme;exit 1' INT + "$ACME" "$@" + status=$? + trap - INT + + case $status in + 0) ;; # renewed ok, handled by acme.sh hook, ignore. + 2) ;; # renew skipped, ignore. + *) + ACTION=renew-failed hotplug-call acme + ;; + esac + return 0 + fi + fi + + for d in $domains; do + set -- "$@" -d "$d" + done + set -- "$@" --keylength "$keylength" --accountemail "$account_email" + + if [ "$acme_server" ]; then + set -- "$@" --server "$acme_server" + # default to letsencrypt because the upstream default may change + elif [ "$staging" = 1 ]; then + set -- "$@" --server letsencrypt_test + else + set -- "$@" --server letsencrypt + fi + + if [ "$days" ]; then + set -- "$@" --days "$days" + fi + + if [ "$dns" ]; then + set -- "$@" --dns "$dns" + if [ "$dalias" ]; then + set -- "$@" --domain-alias "$dalias" + if [ "$calias" ]; then + log err "Both domain and challenge aliases are defined. Ignoring the challenge alias." + fi + elif [ "$calias" ]; then + set -- "$@" --challenge-alias "$calias" + fi + elif [ "$standalone" = 1 ]; then + set -- "$@" --standalone --listen-v6 + else + mkdir -p "$WEBROOT" + set -- "$@" --webroot "$WEBROOT" + fi + + set -- "$@" --issue --home "$state_dir" + + log info "$*" + trap 'ACTION=issue-failed hotplug-call acme;exit 1' INT + "$ACME" "$@" \ + --pre-hook 'ACTION=prepare hotplug-call acme' \ + --renew-hook 'ACTION=renewed hotplug-call acme' + status=$? + trap - INT + + case $status in + 0) + ln -s "$domain_dir/$main_domain.cer" /etc/ssl/acme + ln -s "$domain_dir/$main_domain.key" /etc/ssl/acme + ln -s "$domain_dir/fullchain.cer" "/etc/ssl/acme/$main_domain.fullchain.cer" + ln -s "$domain_dir/ca.cer" "/etc/ssl/acme/$main_domain.chain.cer" + ACTION=issued hotplug-call acme + ;; + *) + if [ "$staging_moved" = 1 ]; then + mv "$domain_dir.staging" "$domain_dir" + log err "Staging certificate restored" + elif [ -d "$domain_dir" ]; then + failed_dir="$domain_dir.failed-$(date +%s)" + mv "$domain_dir" "$failed_dir" + log err "State moved to $failed_dir" + fi + ACTION=issue-failed hotplug-call acme + return 0 + ;; + esac + ;; +esac diff --git a/net/acme-common/Makefile b/net/acme-common/Makefile new file mode 100644 index 0000000000..4e69702be7 --- /dev/null +++ b/net/acme-common/Makefile @@ -0,0 +1,67 @@ +# +# Copyright (C) 2016 Toke Høiland-Jørgensen +# +# This is free software, licensed under the GNU General Public License v3 or +# later. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=acme-common +PKG_VERSION:=1.0.0 + +PKG_MAINTAINER:=Toke Høiland-Jørgensen +PKG_LICENSE:=GPL-3.0-only +PKG_LICENSE_FILES:=LICENSE.md + +include $(INCLUDE_DIR)/package.mk + +define Package/acme-common + SECTION:=net + CATEGORY:=Network + TITLE:=ACME client wrapper common files + PKGARCH:=all +endef + +define Package/acme-common/description +ACME client wrapper common files. +endef + +define Package/acme-common/conffiles +/etc/config/acme +/etc/acme +/etc/ssl/acme +endef + +define Package/acme-common/install + $(INSTALL_DIR) $(1)/etc/acme + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/acme.config $(1)/etc/config/acme + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/acme.sh $(1)/usr/bin/acme + $(INSTALL_DIR) $(1)/usr/lib/acme + $(INSTALL_DATA) ./files/functions.sh $(1)/usr/lib/acme + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/acme.init $(1)/etc/init.d/acme + $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_DATA) ./files/acme.uci-defaults $(1)/etc/uci-defaults/acme +endef + +define Package/acme/postinst +#!/bin/sh +grep -q '/usr/bin/acme' /etc/crontabs/root 2>/dev/null && exit 0 +echo "0 0 * * * /usr/bin/acme get" >> /etc/crontabs/root +endef + +define Package/acme-common/prerm +#!/bin/sh +sed -i '\|/usr/bin/acme|d' /etc/crontabs/root +endef + +define Build/Configure +endef + +define Build/Compile +endef + +$(eval $(call BuildPackage,acme-common)) diff --git a/net/acme/files/acme.config b/net/acme-common/files/acme.config similarity index 57% rename from net/acme/files/acme.config rename to net/acme-common/files/acme.config index 4a0f9ec77b..12bffd60f7 100644 --- a/net/acme/files/acme.config +++ b/net/acme-common/files/acme.config @@ -6,15 +6,9 @@ config acme config cert 'example_wildcard' option enabled 0 option use_staging 1 - option keylength 2048 - option update_uhttpd 1 - option update_nginx 1 - option update_haproxy 0 list domains example.org list domains sub.example.org list domains *.sub.example.org - # option user_setup "path-to-custom-setup.script" - # option user_cleanup "path-to-custom-cleanup.script" option dns "dns_freedns" list credentials 'FREEDNS_User="ssladmin@example.org"' list credentials 'FREEDNS_Password="1234"' @@ -24,12 +18,5 @@ config cert 'example_wildcard' config cert 'example' option enabled 0 option use_staging 1 - option keylength 2048 - option update_uhttpd 1 - option update_nginx 1 - option update_haproxy 0 list domains example.org list domains sub.example.org - option webroot "" - # option user_setup "path-to-custom-setup.script" - # option user_cleanup "path-to-custom-cleanup.script" diff --git a/net/acme-common/files/acme.init b/net/acme-common/files/acme.init new file mode 100644 index 0000000000..e654054f4c --- /dev/null +++ b/net/acme-common/files/acme.init @@ -0,0 +1,9 @@ +#!/bin/sh /etc/rc.common + +START=80 +USE_PROCD=1 + +service_triggers() { + procd_add_config_trigger config.change acme \ + /usr/bin/acme get +} diff --git a/net/acme-common/files/acme.sh b/net/acme-common/files/acme.sh new file mode 100644 index 0000000000..3dff9f348e --- /dev/null +++ b/net/acme-common/files/acme.sh @@ -0,0 +1,142 @@ +#!/bin/sh +# Wrapper for acme.sh to work on openwrt. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Authors: Toke Høiland-Jørgensen + +export state_dir='/etc/acme' +export account_email= +export debug=0 +export challenge_dir='/var/run/acme/challenge' +NFT_HANDLE= +HOOK=/usr/lib/acme/hook +LOG_TAG=acme + +# shellcheck source=/dev/null +. /lib/functions.sh +# shellcheck source=net/acme/files/functions.sh +. /usr/lib/acme/functions.sh + +cleanup() { + log debug "cleaning up" + if [ "$NFT_HANDLE" ]; then + # $NFT_HANDLE contains the string 'handle XX' so pass it unquoted to nft + nft delete rule inet fw4 input $NFT_HANDLE + fi +} + +load_options() { + section=$1 + + # compatibility for old option name + config_get_bool use_staging "$section" staging + if [ -z "$staging" ]; then + config_get_bool staging "$section" staging 0 + fi + export staging + config_get calias "$section" calias + export calias + config_get dalias "$section" dalias + export dalias + config_get domains "$section" domains + export domains + export main_domain + main_domain="$(first_arg $domains)" + config_get keylength "$section" keylength ec-256 + export keylength + config_get dns "$section" dns + export dns + config_get acme_server "$section" acme_server + export acme_server + config_get days "$section" days + export days + config_get standalone "$section" standalone 0 + export standalone + + config_get webroot "$section" webroot + export webroot + if [ "$webroot" ]; then + log warn "Option \"webroot\" is deprecated, please remove it and change your web server's config so it serves ACME challenge requests from /var/run/acme/challenge." + fi +} + +first_arg() { + echo "$1" +} + +get_cert() { + section=$1 + + config_get_bool enabled "$section" enabled 1 + [ "$enabled" = 1 ] || return + + load_options "$section" + if [ -z "$dns" ] && [ "$standalone" = 0 ]; then + mkdir -p "$challenge_dir" + fi + + if [ "$standalone" = 1 ] && [ -z "$NFT_HANDLE" ]; then + if ! NFT_HANDLE=$(nft -a -e insert rule inet fw4 input tcp dport 80 counter accept comment ACME | grep -o 'handle [0-9]\+'); then + return 1 + fi + log debug "added nft rule: $NFT_HANDLE" + fi + + load_credentials() { + eval export "$1" + } + config_list_foreach "$section" credentials load_credentials + + "$HOOK" get +} + +load_globals() { + section=$1 + + config_get account_email "$section" account_email + if [ -z "$account_email" ]; then + log err "account_email option is required" + exit 1 + fi + + config_get state_dir "$section" state_dir "$state_dir" + mkdir -p "$state_dir" + + config_get debug "$section" debug "$debug" + + # only look for the first acme section + return 1 +} + +usage() { + cat < [arguments] +Commands: + get issue or renew certificates + cert show certificate matching domain +EOF + exit 1 +} + +if [ ! -x "$HOOK" ]; then + log err "An ACME client like acme-acmesh or acme-uacme is required, which is not installed." + exit 1 +fi + +case $1 in +get) + config_load acme + config_foreach load_globals acme + + mkdir -p /etc/ssl/acme + trap cleanup EXIT + config_foreach get_cert cert + ;; +*) + usage + ;; +esac diff --git a/net/acme-common/files/acme.uci-defaults b/net/acme-common/files/acme.uci-defaults new file mode 100644 index 0000000000..0b92c2e265 --- /dev/null +++ b/net/acme-common/files/acme.uci-defaults @@ -0,0 +1,4 @@ +#!/bin/sh + +grep -q '/usr/bin/acme' /etc/crontabs/root 2>/dev/null && exit 0 +echo "0 0 * * * /usr/bin/acme get" >> /etc/crontabs/root diff --git a/net/acme-common/files/functions.sh b/net/acme-common/files/functions.sh new file mode 100644 index 0000000000..3325a7ea2c --- /dev/null +++ b/net/acme-common/files/functions.sh @@ -0,0 +1,7 @@ +log() { + prio="$1" + shift + if [ "$prio" != debug ] || [ "$debug" = 0 ]; then + logger -t "$LOG_TAG" -s -p "daemon.$prio" -- "$@" + fi +} diff --git a/net/acme/Makefile b/net/acme/Makefile index f93e1d439a..5f260a9976 100644 --- a/net/acme/Makefile +++ b/net/acme/Makefile @@ -8,13 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=acme -PKG_VERSION:=3.0.1 -PKG_RELEASE:=$(AUTORELEASE) - -PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz -PKG_SOURCE_URL:=https://codeload.github.com/acmesh-official/acme.sh/tar.gz/$(PKG_VERSION)? -PKG_HASH:=6212cc0c2bca99a7dd6cbb4236b4c7dd5d1113dab0841e66dae4d307d902a8e6 -PKG_BUILD_DIR:=$(BUILD_DIR)/acme.sh-$(PKG_VERSION) +PKG_VERSION:=4.0.0 PKG_MAINTAINER:=Toke Høiland-Jørgensen PKG_LICENSE:=GPL-3.0-only @@ -25,19 +19,17 @@ include $(INCLUDE_DIR)/package.mk define Package/acme SECTION:=net CATEGORY:=Network - DEPENDS:=+wget-ssl +ca-bundle +openssl-util +socat - TITLE:=ACME (Letsencrypt) client - URL:=https://acme.sh + DEPENDS:=+acme-acmesh + TITLE:=Shorthand package for acme-acmesh PKGARCH:=all endef define Package/acme/description - A client for issuing Letsencrypt certificates. +Shorthand package for acme-acmesh. endef -define Package/acme/conffiles -/etc/config/acme -/etc/acme +define Package/acme/install + : endef define Build/Configure @@ -46,38 +38,4 @@ endef 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 - $(INSTALL_BIN) ./files/acme.init $(1)/etc/init.d/acme - $(INSTALL_DIR) $(1)/usr/lib/acme - $(INSTALL_BIN) ./files/run.sh $(1)/usr/lib/acme/run-acme - $(INSTALL_BIN) $(PKG_BUILD_DIR)/acme.sh $(1)/usr/lib/acme/acme.sh -endef - -define Package/acme/prerm -#!/bin/sh -sed -i '/\/etc\/init\.d\/acme start/d' /etc/crontabs/root -endef - -define Package/acme-dnsapi - SECTION:=net - CATEGORY:=Network - DEPENDS:=+acme - TITLE:=DNS API integration for ACME (Letsencrypt) client - PKGARCH:=all -endef - -define Package/acme-dnsapi/description - This package provides DNS API integration for ACME (Letsencrypt) client. -endef - -define Package/acme-dnsapi/install - $(INSTALL_DIR) $(1)/usr/lib/acme/dnsapi - $(INSTALL_DATA) $(PKG_BUILD_DIR)/dnsapi/*.sh $(1)/usr/lib/acme/dnsapi -endef - $(eval $(call BuildPackage,acme)) -$(eval $(call BuildPackage,acme-dnsapi)) diff --git a/net/acme/files/acme.init b/net/acme/files/acme.init deleted file mode 100644 index 6a20ccb91b..0000000000 --- a/net/acme/files/acme.init +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh /etc/rc.common - -USE_PROCD=1 - -START=50 -SCRIPT=/usr/lib/acme/run-acme - -start_service() { - procd_open_instance - procd_set_param command $SCRIPT - procd_set_param file /etc/config/acme - procd_set_param stdout 1 - procd_set_param stderr 1 - procd_close_instance -} - -reload_service() { - rc_procd start_service "$@" - return 0 -} - -stop_service() { - return 0 -} - -boot() { - touch "/var/run/acme_boot" - start -} - -service_triggers() { - procd_add_reload_trigger acme -} diff --git a/net/acme/files/run.sh b/net/acme/files/run.sh deleted file mode 100644 index 615cbf22ab..0000000000 --- a/net/acme/files/run.sh +++ /dev/null @@ -1,392 +0,0 @@ -#!/bin/sh -# Wrapper for acme.sh to work on openwrt. -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation; either version 3 of the License, or (at your option) any later -# version. -# -# Author: Toke Høiland-Jørgensen - -CHECK_CRON=$1 -ACME=/usr/lib/acme/acme.sh -export CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt -export NO_TIMESTAMP=1 - -UHTTPD_LISTEN_HTTP= -STATE_DIR='/etc/acme' -ACCOUNT_EMAIL= -DEBUG=0 -NGINX_WEBSERVER=0 -UPDATE_NGINX=0 -UPDATE_UHTTPD=0 -UPDATE_HAPROXY=0 -NFT_HANDLE= -USER_CLEANUP= - -. /lib/functions.sh - -check_cron() { - [ -f "/etc/crontabs/root" ] && grep -q '/etc/init.d/acme' /etc/crontabs/root && return - echo "0 0 * * * /etc/init.d/acme start" >> /etc/crontabs/root - /etc/init.d/cron start -} - -log() { - logger -t acme -s -p daemon.info -- "$@" -} - -err() { - logger -t acme -s -p daemon.err -- "$@" -} - -debug() { - [ "$DEBUG" -eq "1" ] && logger -t acme -s -p daemon.debug -- "$@" -} - -get_listeners() { - local proto rq sq listen remote state program - netstat -nptl 2> /dev/null | while read -r proto rq sq listen remote state program; do - case "$proto#$listen#$program" in - tcp#*:80#[0-9]*/*) echo -n "${program%% *} " ;; - esac - done -} - -run_acme() { - debug "Running acme.sh as '$ACME $*'" - $ACME "$@" -} - -pre_checks() { - main_domain="$1" - - log "Running pre checks for $main_domain." - - listeners="$(get_listeners)" - - debug "port80 listens: $listeners" - - for listener in $(get_listeners); do - pid="${listener%/*}" - cmd="$(basename $(readlink /proc/$pid/exe))" - - case "$cmd" in - uhttpd) - if [ -n "$UHTTPD_LISTEN_HTTP" ]; then - debug "Already handled uhttpd; skipping" - continue - fi - - 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 - ;; - nginx) - if [ "$NGINX_WEBSERVER" -eq "1" ]; then - debug "Already handled nginx; skipping" - continue - fi - - debug "Found nginx listening on port 80; trying to disable." - NGINX_WEBSERVER=1 - local tries=0 - while grep -sq "$cmd" "/proc/$pid/cmdline" && kill -0 "$pid"; do - /etc/init.d/nginx stop - if [ $tries -gt 10 ]; then - debug "Can't stop nginx. Terminating script." - return 1 - fi - debug "Waiting for nginx to stop..." - tries=$((tries + 1)) - sleep 1 - done - ;; - "") - 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 - done - - NFT_HANDLE=$(nft -a -e insert rule inet fw4 input tcp dport 80 counter accept comment ACME | grep -o 'handle [0-9]\+') - ret=$? - [ "$ret" -eq "0" ] || return 1 - debug "added nft rule: $NFT_HANDLE" - return 0 -} - -post_checks() { - log "Running post checks (cleanup)." - # $NFT_HANDLE contains the string 'handle XX' so pass it unquoted to nft - [ -n "$NFT_HANDLE" ] && nft delete rule inet fw4 input $NFT_HANDLE - - if [ -e /etc/init.d/uhttpd ] && { [ -n "$UHTTPD_LISTEN_HTTP" ] || [ "$UPDATE_UHTTPD" -eq 1 ]; }; then - if [ -n "$UHTTPD_LISTEN_HTTP" ]; then - uci set uhttpd.main.listen_http="$UHTTPD_LISTEN_HTTP" - UHTTPD_LISTEN_HTTP= - fi - uci commit uhttpd - /etc/init.d/uhttpd restart - fi - - if [ -e /etc/init.d/nginx ] && { [ "$NGINX_WEBSERVER" -eq 1 ] || [ "$UPDATE_NGINX" -eq 1 ]; }; then - NGINX_WEBSERVER=0 - /etc/init.d/nginx restart - fi - - if [ -e /etc/init.d/haproxy ] && [ "$UPDATE_HAPROXY" -eq 1 ] ; then - /etc/init.d/haproxy restart - fi - - if [ -n "$USER_CLEANUP" ] && [ -f "$USER_CLEANUP" ]; then - log "Running user-provided cleanup script from $USER_CLEANUP." - "$USER_CLEANUP" || return 1 - fi -} - -err_out() { - post_checks - exit 1 -} - -int_out() { - post_checks - trap - INT - kill -INT $$ -} - -is_staging() { - local main_domain - local domain_dir - main_domain="$1" - domain_dir="$2" - - grep -q "acme-staging" "${domain_dir}/${main_domain}.conf" - return $? -} - -issue_cert() { - local section="$1" - local acme_args= - local enabled - local use_staging - local update_uhttpd - local update_nginx - local update_haproxy - local keylength - local keylength_ecc=0 - local domains - local main_domain - local moved_staging=0 - local failed_dir - local webroot - local dns - local user_setup - local user_cleanup - local ret - local domain_dir - local acme_server - local days - - 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_bool update_nginx "$section" update_nginx - config_get_bool update_haproxy "$section" update_haproxy - config_get calias "$section" calias - config_get dalias "$section" dalias - config_get domains "$section" domains - config_get keylength "$section" keylength - config_get webroot "$section" webroot - config_get dns "$section" dns - config_get user_setup "$section" user_setup - config_get user_cleanup "$section" user_cleanup - config_get acme_server "$section" acme_server - config_get days "$section" days - - UPDATE_NGINX=$update_nginx - UPDATE_UHTTPD=$update_uhttpd - UPDATE_HAPROXY=$update_haproxy - USER_CLEANUP=$user_cleanup - - [ "$enabled" -eq "1" ] || return - - [ "$DEBUG" -eq "1" ] && acme_args="$acme_args --debug" - - set -- $domains - main_domain=$1 - - if [ -n "$user_setup" ] && [ -f "$user_setup" ]; then - log "Running user-provided setup script from $user_setup." - "$user_setup" "$main_domain" || return 1 - else - [ -n "$webroot" ] || [ -n "$dns" ] || pre_checks "$main_domain" || return 1 - fi - - if echo "$keylength" | grep -q "^ec-"; then - domain_dir="$STATE_DIR/${main_domain}_ecc" - keylength_ecc=1 - else - domain_dir="$STATE_DIR/${main_domain}" - fi - - log "Running ACME for $main_domain" - - handle_credentials() { - local credential="$1" - eval export $credential - } - config_list_foreach "$section" credentials handle_credentials - - if [ -e "$domain_dir" ]; then - if [ "$use_staging" -eq "0" ] && is_staging "$main_domain" "$domain_dir"; then - log "Found previous cert issued using staging server. Moving it out of the way." - mv "$domain_dir" "${domain_dir}.staging" - moved_staging=1 - else - log "Found previous cert config. Issuing renew." - [ "$keylength_ecc" -eq "1" ] && acme_args="$acme_args --ecc" - run_acme --home "$STATE_DIR" --renew -d "$main_domain" $acme_args && ret=0 || ret=1 - post_checks - return $ret - fi - fi - - acme_args="$acme_args $(for d in $domains; do echo -n "-d $d "; done)" - acme_args="$acme_args --keylength $keylength" - [ -n "$ACCOUNT_EMAIL" ] && acme_args="$acme_args --accountemail $ACCOUNT_EMAIL" - - if [ -n "$acme_server" ]; then - log "Using custom ACME server URL" - acme_args="$acme_args --server $acme_server" - else - # default to letsencrypt because the upstream default may change - if [ "$use_staging" -eq "1" ]; then - acme_args="$acme_args --server letsencrypt_test" - else - acme_args="$acme_args --server letsencrypt" - fi - fi - - if [ -n "$days" ]; then - log "Renewing after $days days" - acme_args="$acme_args --days $days" - fi - - if [ -n "$dns" ]; then - log "Using dns mode" - acme_args="$acme_args --dns $dns" - if [ -n "$dalias" ]; then - log "Using domain alias for dns mode" - acme_args="$acme_args --domain-alias $dalias" - if [ -n "$calias" ]; then - err "Both domain and challenge aliases are defined. Ignoring the challenge alias." - fi - elif [ -n "$calias" ]; then - log "Using challenge alias for dns mode" - acme_args="$acme_args --challenge-alias $calias" - fi - elif [ -z "$webroot" ]; then - log "Using standalone mode" - acme_args="$acme_args --standalone --listen-v6" - else - if [ ! -d "$webroot" ]; then - err "$main_domain: Webroot dir '$webroot' does not exist!" - post_checks - return 1 - fi - log "Using webroot dir: $webroot" - acme_args="$acme_args --webroot $webroot" - fi - - if ! run_acme --home "$STATE_DIR" --issue $acme_args; then - failed_dir="${domain_dir}.failed-$(date +%s)" - err "Issuing cert for $main_domain failed. Moving state to $failed_dir" - [ -d "$domain_dir" ] && mv "$domain_dir" "$failed_dir" - if [ "$moved_staging" -eq "1" ]; then - err "Restoring staging certificate" - mv "${domain_dir}.staging" "${domain_dir}" - fi - post_checks - return 1 - fi - - if [ -e /etc/init.d/uhttpd ] && [ "$update_uhttpd" -eq "1" ]; then - uci set uhttpd.main.key="${domain_dir}/${main_domain}.key" - uci set uhttpd.main.cert="${domain_dir}/fullchain.cer" - # commit and reload is in post_checks - fi - - local nginx_updated - nginx_updated=0 - if command -v nginx-util 2> /dev/null && [ "$update_nginx" -eq "1" ]; then - nginx_updated=1 - for domain in $domains; do - nginx-util add_ssl "${domain}" acme "${domain_dir}/fullchain.cer" \ - "${domain_dir}/${main_domain}.key" || nginx_updated=0 - done - # reload is in post_checks - fi - - if [ "$nginx_updated" -eq "0" ] && [ -w /etc/nginx/nginx.conf ] && [ "$update_nginx" -eq "1" ]; then - sed -i "s#ssl_certificate\ .*#ssl_certificate ${domain_dir}/fullchain.cer;#g" /etc/nginx/nginx.conf - sed -i "s#ssl_certificate_key\ .*#ssl_certificate_key ${domain_dir}/${main_domain}.key;#g" /etc/nginx/nginx.conf - # commit and reload is in post_checks - fi - - if [ -e /etc/init.d/haproxy ] && [ -w /etc/haproxy.cfg ] && [ "$update_haproxy" -eq "1" ]; then - cat "${domain_dir}/${main_domain}.key" "${domain_dir}/fullchain.cer" > "${domain_dir}/${main_domain}-haproxy.pem" - sed -i "s#bind :::443 v4v6 ssl crt .* alpn#bind :::443 v4v6 ssl crt ${domain_dir}/${main_domain}-haproxy.pem alpn#g" /etc/haproxy.cfg - # commit and reload is in post_checks - fi - - post_checks -} - -load_vars() { - local section="$1" - - STATE_DIR=$(config_get "$section" state_dir) - ACCOUNT_EMAIL=$(config_get "$section" account_email) - DEBUG=$(config_get "$section" debug) -} - -check_cron -[ -n "$CHECK_CRON" ] && exit 0 -[ -e "/var/run/acme_boot" ] && rm -f "/var/run/acme_boot" && exit 0 - -config_load acme -config_foreach load_vars acme - -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 - -exit 0 diff --git a/net/haproxy/Makefile b/net/haproxy/Makefile index 3b882d9543..bd47c15bbe 100644 --- a/net/haproxy/Makefile +++ b/net/haproxy/Makefile @@ -122,6 +122,8 @@ define Package/haproxy/install $(INSTALL_CONF) ./files/haproxy.cfg $(1)/etc/ $(INSTALL_DIR) $(1)/etc/init.d $(INSTALL_BIN) ./files/haproxy.init $(1)/etc/init.d/haproxy + $(INSTALL_DIR) $(1)/etc/hotplug.d/acme + $(INSTALL_DATA) ./files/acme.hotplug $(1)/etc/hotplug.d/acme/00-haproxy endef Package/haproxy-nossl/install = $(Package/haproxy/install) diff --git a/net/haproxy/files/acme.hotplug b/net/haproxy/files/acme.hotplug new file mode 100644 index 0000000000..5a4dc5cdd3 --- /dev/null +++ b/net/haproxy/files/acme.hotplug @@ -0,0 +1,12 @@ +case $ACTION in +issued|renewed) + cat \ + "/etc/ssl/acme/$main_domain.fullchain.cer" \ + "/etc/ssl/acme/$main_domain.key" \ + >"/etc/ssl/acme/$main_domain.combined.cer" + ;; +esac + +if [ "$ACTION" = renewed ]; then + /etc/init.d/haproxy reload +fi diff --git a/net/nginx/Makefile b/net/nginx/Makefile index 886c55db7c..340de5846f 100644 --- a/net/nginx/Makefile +++ b/net/nginx/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=nginx PKG_VERSION:=1.21.3 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE:=nginx-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://nginx.org/download/ @@ -372,6 +372,9 @@ ifeq ($(CONFIG_NGINX_NAXSI),y) endif $(if $(CONFIG_NGINX_NAXSI),$($(INSTALL_BIN) $(PKG_BUILD_DIR)/nginx-naxsi/naxsi_config/naxsi_core.rules $(1)/etc/nginx)) $(if $(CONFIG_NGINX_NAXSI),$(chmod 0640 $(1)/etc/nginx/naxsi_core.rules)) + + $(INSTALL_DIR) $(1)/etc/hotplug.d/acme + $(INSTALL_DATA) ./files/acme.hotplug $(1)/etc/hotplug.d/acme/00-nginx endef Package/nginx-all-module/install = $(Package/nginx-ssl/install) diff --git a/net/nginx/files/acme.hotplug b/net/nginx/files/acme.hotplug new file mode 100644 index 0000000000..74f1448d76 --- /dev/null +++ b/net/nginx/files/acme.hotplug @@ -0,0 +1,3 @@ +if [ "$ACTION" = renewed ]; then + /etc/init.d/nginx reload +fi