mirror of
https://github.com/openwrt/packages.git
synced 2026-04-15 10:51:55 +00:00
uacme: use acme-common
remake uacme hook scripts to base on acme-common, and implements helper to able to use acme.sh DNS APIs Signed-off-by: Seo Suchan <tjtncks@gmail.com>
This commit is contained in:
committed by
Alexandru Ardelean
parent
fe3d05090b
commit
7f88cc5eb8
@@ -51,7 +51,7 @@ endef
|
||||
define Package/acme-acmesh-dnsapi
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
DEPENDS:=+acme-common +PACKAGE_uacme:curl
|
||||
DEPENDS:=+acme
|
||||
TITLE:=DNS API integration for ACME (Letsencrypt) client
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
@@ -19,7 +19,7 @@ PKG_MAINTAINER:=Lucian Cristian <lucian.cristian@gmail.com>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
PKG_LICENSE_FILES:=COPYING
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-upstream-$(PKG_VERSION)
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/uacme-upstream-$(PKG_VERSION)
|
||||
PKG_INSTALL:=1
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
|
||||
@@ -46,7 +46,7 @@ define Package/uacme
|
||||
$(call Package/uacme/Default)
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
DEPENDS:=+libcurl +LIBCURL_WOLFSSL:libmbedtls
|
||||
DEPENDS:=+libcurl +LIBCURL_WOLFSSL:libmbedtls +acme-common
|
||||
TITLE:=lightweight client for ACMEv2
|
||||
Menu:=1
|
||||
endef
|
||||
@@ -58,6 +58,12 @@ define Package/uacme-ualpn
|
||||
URL:=https://github.com/ndilieto/uacme
|
||||
endef
|
||||
|
||||
define Package/uacme-dnsapi-adapter
|
||||
$(call Package/uacme/Default)
|
||||
DEPENDS:= +uacme +acme-acmesh-dnsapi +curl
|
||||
TITLE:=adapter script for use acme.sh dnsapi with uacme
|
||||
endef
|
||||
|
||||
define Package/uacme/Default/description
|
||||
lightweight client for the RFC8555 ACMEv2 protocol, written in plain C code
|
||||
with minimal dependencies (libcurl and one of GnuTLS, OpenSSL or mbedTLS).
|
||||
@@ -100,30 +106,30 @@ define Package/uacme/install
|
||||
$(INSTALL_DIR) \
|
||||
$(1)/usr/sbin \
|
||||
$(1)/etc/acme \
|
||||
$(1)/etc/config \
|
||||
$(1)/etc/init.d \
|
||||
$(1)/usr/share/uacme
|
||||
$(1)/usr/share/uacme \
|
||||
$(1)/usr/lib/acme/client
|
||||
|
||||
$(INSTALL_BIN) ./files/hook.sh $(1)/usr/lib/acme/hook
|
||||
$(INSTALL_BIN) ./files/httpchalhook.sh $(1)/usr/lib/acme/client/httpchalhook.sh
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/uacme $(1)/usr/sbin/uacme
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/share/uacme/uacme.sh $(1)/usr/share/uacme/
|
||||
$(SED) '/^CHALLENGE_PATH=/d' $(1)/usr/share/uacme/uacme.sh
|
||||
$(INSTALL_CONF) ./files/acme.config $(1)/etc/config/acme
|
||||
$(INSTALL_BIN) ./files/run.sh $(1)/usr/share/uacme/run-uacme
|
||||
$(INSTALL_BIN) ./files/acme.init $(1)/etc/init.d/acme
|
||||
endef
|
||||
|
||||
define Package/uacme-ualpn/install
|
||||
$(INSTALL_DIR) \
|
||||
$(1)/usr/sbin \
|
||||
$(1)/usr/share/uacme
|
||||
$(1)/usr/share/uacme \
|
||||
$(1)/usr/lib/acme/client
|
||||
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/ualpn $(1)/usr/sbin/ualpn
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/ualpn.sh $(1)/usr/share/uacme/
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/ualpn.sh $(1)/usr/lib/acme/client/ualpn.sh
|
||||
endef
|
||||
|
||||
define Package/uacme/prerm
|
||||
#!/bin/sh
|
||||
sed -i '/\/etc\/init\.d\/acme start/d' /etc/crontabs/root
|
||||
define Package/uacme-dnsapi-adapter/install
|
||||
$(INSTALL_DIR) \
|
||||
$(1)/usr/lib/acme/client
|
||||
|
||||
$(INSTALL_BIN) ./files/dnschalhook.sh $(1)/usr/lib/acme/client/dnschalhook.sh
|
||||
$(INSTALL_BIN) ./files/dnsapi_helper.sh $(1)/usr/lib/acme/client/dnsapi_helper.sh
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,uacme))
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
config acme
|
||||
option state_dir '/etc/acme'
|
||||
option account_email 'email@example.org'
|
||||
option debug 0
|
||||
|
||||
config cert 'example'
|
||||
option enabled 0
|
||||
option use_staging 1
|
||||
option keylength 2048
|
||||
option update_uhttpd 1
|
||||
option update_nginx 1
|
||||
option update_haproxy 1
|
||||
option webroot "/www/.well-known/acme-challenge"
|
||||
# option user_setup "path-to-custom-setup.script"
|
||||
# option user_cleanup "path-to-custom-cleanup.script"
|
||||
list domains example.org
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
|
||||
START=50
|
||||
SCRIPT=/usr/share/uacme/run-uacme
|
||||
|
||||
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/uacme_boot"
|
||||
start
|
||||
}
|
||||
|
||||
service_triggers()
|
||||
{
|
||||
procd_add_reload_trigger acme
|
||||
}
|
||||
1260
net/uacme/files/dnsapi_helper.sh
Executable file
1260
net/uacme/files/dnsapi_helper.sh
Executable file
File diff suppressed because it is too large
Load Diff
79
net/uacme/files/dnschalhook.sh
Executable file
79
net/uacme/files/dnschalhook.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/bin/sh
|
||||
# Copyright (C) 2019-2024 Nicola Di Lieto <nicola.dilieto@gmail.com>
|
||||
#
|
||||
# This file is part of uacme.
|
||||
#
|
||||
# uacme 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.
|
||||
#
|
||||
# uacme is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# Part of this is copied from acme.sh
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
ARGS=5
|
||||
E_BADARGS=85
|
||||
LOG_TAG=acme-uacme-dnshook
|
||||
|
||||
if test $# -ne "$ARGS"
|
||||
then
|
||||
echo "Usage: $(basename "$0") method type ident token auth" 1>&2
|
||||
exit $E_BADARGS
|
||||
fi
|
||||
|
||||
METHOD=$1
|
||||
TYPE=$2
|
||||
IDENT=$3
|
||||
TOKEN=$4
|
||||
AUTH=$5
|
||||
|
||||
if [ "$TYPE" != "dns-01" ]; then
|
||||
echo "skipping $TYPE" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=net/acme/files/functions.sh
|
||||
. /usr/lib/acme/functions.sh
|
||||
. /usr/lib/acme/client/dnsapi_helper.sh
|
||||
ACCOUNT_CONF_PATH=$UACME_CONFDIR/accounts.conf
|
||||
DOMAIN_CONF_DIR=$UACME_CONFDIR/$IDENT
|
||||
DOMAIN_CONF=$DOMAIN_CONF_DIR/dnsapi.conf
|
||||
ACMESH_DNSSCIRPT_DIR=${ACMESH_DNSSCIRPT_DIR:-/usr/lib/acme/client/dnsapi}
|
||||
|
||||
#import dns hook script
|
||||
dns=${dns:-$(head -n 1 $DOMAIN_CONF_DIR/selected_api)} # use different file to not hurt acme.sh config file struct
|
||||
if [ ! -f "$ACMESH_DNSSCIRPT_DIR/$dns.sh" ]; then
|
||||
echo "dns file $dns doesn't exit" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
. /usr/lib/acme/client/dnsapi/$dns.sh
|
||||
echo $dns > "$DOMAIN_CONF_DIR/selected_api"
|
||||
case "$METHOD" in
|
||||
"begin")
|
||||
(umask 077 ; touch -a "$DOMAIN_CONF")
|
||||
log info logging $DOMAIN_CONF
|
||||
${dns}_add _acme-challenge.$IDENT $AUTH
|
||||
RESULT=$?
|
||||
if [ $RESULT -eq 0 ]; then
|
||||
sleep ${dns_wait:-"30s"}
|
||||
exit 0
|
||||
else
|
||||
exit $RESULT
|
||||
fi
|
||||
;;
|
||||
"done"|"failed")
|
||||
${dns}_rm _acme-challenge.$IDENT $AUTH
|
||||
exit $?
|
||||
;;
|
||||
*)
|
||||
echo "$0: invalid method" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
218
net/uacme/files/hook.sh
Executable file
218
net/uacme/files/hook.sh
Executable file
@@ -0,0 +1,218 @@
|
||||
#!/bin/sh
|
||||
# Wrapper for uacme to work on openwrt.
|
||||
|
||||
set -u
|
||||
ACME=/usr/sbin/uacme
|
||||
HPROGRAM=/usr/share/uacme/uacme.sh
|
||||
LOG_TAG=acme-uacme
|
||||
NOTIFY=/usr/lib/acme/notify
|
||||
HOOKDIR=/usr/lib/acme
|
||||
|
||||
# shellcheck source=net/acme/files/functions.sh
|
||||
. /usr/lib/acme/functions.sh
|
||||
|
||||
link_certs() {
|
||||
local main_domain
|
||||
local domain_dir
|
||||
domain_dir="$1"
|
||||
main_domain="$2"
|
||||
#uacme saves only fullchain as cert.pem
|
||||
(
|
||||
umask 077
|
||||
cat "$domain_dir/cert.pem" "$state_dir/private/$main_domain/key.pem" >"$domain_dir/combined.cer"
|
||||
sed -n '1,/-----END CERTIFICATE-----/p' "$domain_dir/cert.pem" >"$domain_dir/leaf_cert.pem"
|
||||
sed '1,/-----END CERTIFICATE-----/d' "$domain_dir/cert.pem" >"$domain_dir/chain.crt"
|
||||
)
|
||||
|
||||
if [ ! -e "$CERT_DIR/$main_domain.crt" ]; then
|
||||
ln -s "$domain_dir/leaf_cert.pem" "$CERT_DIR/$main_domain.crt"
|
||||
fi
|
||||
#uacme doesn't rotate key, and it saves ../private/$main_domain dir
|
||||
if [ ! -e "$CERT_DIR/$main_domain.key" ]; then
|
||||
ln -s "$state_dir/private/$main_domain/key.pem" "$CERT_DIR/$main_domain.key"
|
||||
fi
|
||||
if [ ! -e "$CERT_DIR/$main_domain.fullchain.crt" ]; then
|
||||
ln -s "$domain_dir/cert.pem" "$CERT_DIR/$main_domain.fullchain.crt"
|
||||
fi
|
||||
if [ ! -e "$CERT_DIR/$main_domain.combined.crt" ]; then
|
||||
ln -s "$domain_dir/combined.cer" "$CERT_DIR/$main_domain.combined.crt"
|
||||
fi
|
||||
if [ ! -e "$CERT_DIR/$main_domain.chain.crt" ]; then
|
||||
ln -s "$domain_dir/chain.crt" "$CERT_DIR/$main_domain.chain.crt"
|
||||
fi
|
||||
}
|
||||
|
||||
#expand acme server short alias
|
||||
case $acme_server in
|
||||
letsencrypt)
|
||||
unset acme_server
|
||||
;;
|
||||
letsencrypt_test)
|
||||
acme_server=https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
;;
|
||||
zerossl)
|
||||
acme_server=https://acme.zerossl.com/v2/DV90
|
||||
;;
|
||||
google)
|
||||
acme_server=https://dv.acme-v02.api.pki.goog/directory
|
||||
;;
|
||||
actalis)
|
||||
acme_server=https://acme-api.actalis.com/acme/directory
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
|
||||
case $1 in
|
||||
get)
|
||||
#uacme doesn't save account per ca nor it make new account when not registered
|
||||
#while server doesn't care, we record which CAs we have account to reduce noise
|
||||
#using staging var for default letsencrypts.
|
||||
if grep -q "^${acme_server:-$staging}$" $state_dir/accounts; then
|
||||
:
|
||||
else
|
||||
#not found
|
||||
if [ "$acme_server" ]; then
|
||||
$ACME new $account_email -t EC -y --confdir "$state_dir" -a $acme_server
|
||||
echo $acme_server >> $state_dir/accounts
|
||||
elif [ "$staging" = 1 ]; then
|
||||
$ACME new $account_email -t EC -y --confdir "$state_dir" -s
|
||||
echo $staging >> $state_dir/accounts
|
||||
else
|
||||
$ACME new $account_email -t EC -y --confdir "$state_dir"
|
||||
echo $staging >> $state_dir/accounts
|
||||
fi
|
||||
fi
|
||||
set --
|
||||
[ "$debug" = 1 ] && set -- "$@" -v
|
||||
#uacme doesn't rotate privkey
|
||||
case $key_type in
|
||||
ec*)
|
||||
keylength=${key_type#ec}
|
||||
domain_dir="$state_dir/$main_domain"
|
||||
set -- "$@" -t EC
|
||||
;;
|
||||
rsa*)
|
||||
keylength=${key_type#rsa}
|
||||
domain_dir="$state_dir/$main_domain"
|
||||
;;
|
||||
esac
|
||||
|
||||
set -- "$@" --bits "$keylength"
|
||||
|
||||
if [ "$acme_server" ]; then
|
||||
set -- "$@" --acme-url "$acme_server"
|
||||
elif [ "$staging" = 1 ]; then
|
||||
set -- "$@" --staging
|
||||
else
|
||||
set -- "$@"
|
||||
fi
|
||||
|
||||
log info "Running ACME for $main_domain with validation_method $validation_method"
|
||||
|
||||
staging_moved=0
|
||||
is_renew=0
|
||||
if [ -e "$domain_dir" ]; then
|
||||
if [ "$staging" = 0 ] && [ -e $domain_dir/this_is_staging ]; then
|
||||
mv "$domain_dir" "$domain_dir.staging"
|
||||
mv "$state_dir/private/$main_domain" "$state_dir/private/$main_domain.staging"
|
||||
log info "Certificates are previously issued from a staging server, but staging option is disabled, moved to $domain_dir.staging."
|
||||
staging_moved=1
|
||||
else
|
||||
#this is renewal
|
||||
is_renew=1
|
||||
fi
|
||||
else
|
||||
log info no prv certificate remembered
|
||||
fi
|
||||
|
||||
if [ "$days" ]; then
|
||||
set -- "$@" --days "$days"
|
||||
fi
|
||||
|
||||
# uacme handles challange select by hook script
|
||||
case "$validation_method" in
|
||||
"alpn")
|
||||
log info "using already running ualpn, it's user's duty to config ualpn server deamon"
|
||||
set -- "$@" -h "$HOOKDIR/client/ualpn.sh"
|
||||
;;
|
||||
"dns")
|
||||
export dns
|
||||
set -- "$@" -h "$HOOKDIR/client/dnschalhook.sh"
|
||||
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
|
||||
if [ "$dns_wait" ]; then
|
||||
export dns_wait
|
||||
fi
|
||||
;;
|
||||
"standalone")
|
||||
set -- "$@" --standalone --listen-v6
|
||||
log err "standalone server is not implmented for uacme"
|
||||
exit 1
|
||||
;;
|
||||
"webroot")
|
||||
mkdir -p "$CHALLENGE_DIR"
|
||||
export CHALLENGE_DIR
|
||||
set -- "$@" -h "$HOOKDIR/client/httpchalhook.sh"
|
||||
;;
|
||||
*)
|
||||
log err "Unsupported validation_method $validation_method"
|
||||
;;
|
||||
esac
|
||||
|
||||
set -- "$@" --confdir "$state_dir" issue
|
||||
for d in $domains; do
|
||||
set -- "$@" "$d"
|
||||
done
|
||||
|
||||
log info "$ACME $*"
|
||||
trap '$NOTIFY issue-failed;exit 1' INT
|
||||
"$ACME" "$@" 2>&1
|
||||
status=$?
|
||||
trap - INT
|
||||
|
||||
case $status in
|
||||
0)
|
||||
link_certs "$domain_dir" "$main_domain"
|
||||
if [ "$is_renew" = 1 ]; then
|
||||
$NOTIFY renewed
|
||||
else
|
||||
$NOTIFY issued
|
||||
if [ "$staging" = 1 ]; then
|
||||
touch $domain_dir/this_is_staging
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
1)
|
||||
#cert is not due to renewl so don't do anything
|
||||
if [ "$staging_moved" = 1 ]; then
|
||||
log err "Staging certificate '$domain_dir' restored"
|
||||
mv "$domain_dir.staging" "$domain_dir"
|
||||
fi
|
||||
log debug "not due to renewal"
|
||||
;;
|
||||
*)
|
||||
if [ "$is_renew" = 1 ]; then
|
||||
$NOTIFY renew-failed
|
||||
exit 1;
|
||||
fi
|
||||
if [ "$staging_moved" = 1 ]; then
|
||||
log err "Staging certificate '$domain_dir' restored"
|
||||
mv "$domain_dir.staging" "$domain_dir"
|
||||
mv "$state_dir/private/$main_domain.staging" "$state_dir/private/$main_domain"
|
||||
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
|
||||
$NOTIFY issue-failed
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
64
net/uacme/files/httpchalhook.sh
Executable file
64
net/uacme/files/httpchalhook.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/bin/sh
|
||||
# Copyright (C) 2019-2024 Nicola Di Lieto <nicola.dilieto@gmail.com>
|
||||
#
|
||||
# This file is part of uacme.
|
||||
#
|
||||
# uacme 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.
|
||||
#
|
||||
# uacme is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
CHALLENGE_PATH="${CHALLENGE_DIR:-/var/run/acme/challenge}"
|
||||
mkdir -p "${CHALLENGE_PATH}/.well-known/acme-challenge"
|
||||
ARGS=5
|
||||
E_BADARGS=85
|
||||
|
||||
if test $# -ne "$ARGS"
|
||||
then
|
||||
echo "Usage: $(basename "$0") method type ident token auth" 1>&2
|
||||
exit $E_BADARGS
|
||||
fi
|
||||
|
||||
METHOD=$1
|
||||
TYPE=$2
|
||||
IDENT=$3
|
||||
TOKEN=$4
|
||||
AUTH=$5
|
||||
|
||||
case "$METHOD" in
|
||||
"begin")
|
||||
case "$TYPE" in
|
||||
http-01)
|
||||
printf "%s" "${AUTH}" > "${CHALLENGE_PATH}/.well-known/acme-challenge/${TOKEN}"
|
||||
exit $?
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
"done"|"failed")
|
||||
case "$TYPE" in
|
||||
http-01)
|
||||
rm "${CHALLENGE_PATH}/.well-known/acme-challenge/${TOKEN}"
|
||||
exit $?
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "$0: invalid method" 1>&2
|
||||
exit 1
|
||||
esac
|
||||
@@ -1,540 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Wrapper for uacme 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.
|
||||
#
|
||||
# Initial Author: Toke Høiland-Jørgensen <toke@toke.dk>
|
||||
# Adapted for uacme: Lucian Cristian <lucian.cristian@gmail.com>
|
||||
# Adapted for custom CA and TLS-ALPN-01: Peter Putzer <openwrt@mundschenk.at>
|
||||
|
||||
CHECK_CRON=$1
|
||||
|
||||
#check for installed packages, for now, support only one
|
||||
if [ -e "/usr/lib/acme/acme.sh" ]; then
|
||||
ACME=/usr/lib/acme/acme.sh
|
||||
APP=acme
|
||||
elif [ -e "/usr/sbin/uacme" ]; then
|
||||
ACME=/usr/sbin/uacme
|
||||
HPROGRAM=/usr/share/uacme/uacme.sh
|
||||
APP=uacme
|
||||
else
|
||||
echo "Please install ACME or uACME package"
|
||||
return 1
|
||||
fi
|
||||
|
||||
export CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
export NO_TIMESTAMP=1
|
||||
|
||||
UHTTPD_LISTEN_HTTP=
|
||||
PRODUCTION_STATE_DIR='/etc/acme'
|
||||
STAGING_STATE_DIR='/etc/acme/staging'
|
||||
|
||||
ACCOUNT_EMAIL=
|
||||
DEBUG=0
|
||||
NGINX_WEBSERVER=0
|
||||
UPDATE_NGINX=0
|
||||
UPDATE_UHTTPD=0
|
||||
UPDATE_HAPROXY=0
|
||||
FW_RULE=
|
||||
USER_CLEANUP=
|
||||
ACME_URL=
|
||||
ACME_STAGING_URL=
|
||||
|
||||
. /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 $APP -s -p daemon.info "$@"
|
||||
}
|
||||
|
||||
err()
|
||||
{
|
||||
logger -t $APP -s -p daemon.err "$@"
|
||||
}
|
||||
|
||||
debug()
|
||||
{
|
||||
[ "$DEBUG" -eq "1" ] && logger -t $APP -s -p daemon.debug "$@"
|
||||
}
|
||||
|
||||
get_listeners() {
|
||||
local proto rq sq listen remote state program
|
||||
netstat -nptl 2>/dev/null | while read proto listen program; do
|
||||
case "$proto#$listen#$program" in
|
||||
tcp#*:80#[0-9]*/*) echo -n "${program%% *} " ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
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="${listener#*/}"
|
||||
|
||||
case "$cmd" in
|
||||
uhttpd)
|
||||
debug "Found uhttpd listening on port 80"
|
||||
if [ "$APP" = "acme" ]; then
|
||||
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
|
||||
fi
|
||||
;;
|
||||
nginx*)
|
||||
debug "Found nginx listening on port 80"
|
||||
NGINX_WEBSERVER=1
|
||||
if [ "$APP" = "acme" ]; then
|
||||
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
|
||||
fi
|
||||
;;
|
||||
"")
|
||||
err "Nothing listening on port 80."
|
||||
err "Standalone mode not supported, setup uhttpd or nginx"
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
err "$main_domain: unsupported (apache/haproxy?) daemon is listening on port 80."
|
||||
err "if webroot is set on your current webserver comment line 132 (return 1) from this script."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
FW_RULE=$(uci add firewall rule) || return 1
|
||||
uci set firewall."$FW_RULE".name='uacme: temporarily allow incoming http'
|
||||
uci set firewall."$FW_RULE".enabled='1'
|
||||
uci set firewall."$FW_RULE".target='ACCEPT'
|
||||
uci set firewall."$FW_RULE".src='wan'
|
||||
uci set firewall."$FW_RULE".proto='tcp'
|
||||
uci set firewall."$FW_RULE".dest_port='80'
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload
|
||||
|
||||
debug "added firewall rule: $FW_RULE"
|
||||
return 0
|
||||
}
|
||||
|
||||
post_checks()
|
||||
{
|
||||
log "Running post checks (cleanup)."
|
||||
# $FW_RULE contains the string to identify firewall rule created earlier
|
||||
if [ -n "$FW_RULE" ]; then
|
||||
uci delete firewall."$FW_RULE"
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload
|
||||
fi
|
||||
|
||||
if [ -e /etc/init.d/uhttpd ] && [ "$UPDATE_UHTTPD" -eq 1 ]; then
|
||||
uci commit uhttpd
|
||||
/etc/init.d/uhttpd reload
|
||||
log "Restarting uhttpd..."
|
||||
fi
|
||||
|
||||
if [ -e /etc/init.d/nginx ] && ( [ "$NGINX_WEBSERVER" -eq 1 ] || [ "$UPDATE_NGINX" -eq 1 ]; ); then
|
||||
NGINX_WEBSERVER=0
|
||||
/etc/init.d/nginx restart
|
||||
log "Restarting nginx..."
|
||||
fi
|
||||
|
||||
if [ -e /etc/init.d/haproxy ] && [ "$UPDATE_HAPROXY" -eq 1 ]; then
|
||||
/etc/init.d/haproxy restart
|
||||
log "Restarting haproxy..."
|
||||
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="$1"
|
||||
|
||||
grep -q "acme-staging" "$STATE_DIR/$main_domain/${main_domain}.conf"
|
||||
return $?
|
||||
}
|
||||
|
||||
issue_cert()
|
||||
{
|
||||
local section="$1"
|
||||
local acme_args=
|
||||
local debug=
|
||||
local enabled
|
||||
local use_staging
|
||||
local update_uhttpd
|
||||
local update_nginx
|
||||
local update_haproxy
|
||||
local keylength
|
||||
local domains
|
||||
local main_domain
|
||||
local failed_dir
|
||||
local webroot
|
||||
local dns
|
||||
local tls
|
||||
local user_setup
|
||||
local user_cleanup
|
||||
local ret
|
||||
local staging=
|
||||
local HOOK=
|
||||
|
||||
# reload uci values, as the value of use_staging may have changed
|
||||
config_load acme
|
||||
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 domains "$section" domains
|
||||
config_get keylength "$section" keylength
|
||||
config_get webroot "$section" webroot
|
||||
config_get dns "$section" dns
|
||||
config_get tls "$section" tls
|
||||
config_get user_setup "$section" user_setup
|
||||
config_get user_cleanup "$section" user_cleanup
|
||||
|
||||
UPDATE_NGINX=$update_nginx
|
||||
UPDATE_UHTTPD=$update_uhttpd
|
||||
UPDATE_HAPROXY=$update_haproxy
|
||||
USER_CLEANUP=$user_cleanup
|
||||
|
||||
[ "$enabled" -eq "1" ] || return 0
|
||||
|
||||
if [ "$APP" = "uacme" ]; then
|
||||
[ "$DEBUG" -eq "1" ] && debug="--verbose --verbose"
|
||||
[ "$tls" -eq "1" ] && HPROGRAM=/usr/share/uacme/ualpn.sh
|
||||
elif [ "$APP" = "acme" ]; then
|
||||
[ "$DEBUG" -eq "1" ] && acme_args="$acme_args --debug"
|
||||
fi
|
||||
if [ "$use_staging" -eq "1" ]; then
|
||||
STATE_DIR="$STAGING_STATE_DIR";
|
||||
|
||||
# Check if we should use a custom stagin URL
|
||||
if [ "$APP" = "uacme" -a -n "$ACME_STAGING_URL" ]; then
|
||||
ACME="$ACME --acme-url $ACME_STAGING_URL"
|
||||
else
|
||||
staging="--staging";
|
||||
fi
|
||||
else
|
||||
STATE_DIR="$PRODUCTION_STATE_DIR";
|
||||
staging="";
|
||||
|
||||
if [ "$APP" = "uacme" -a -n "$ACME_URL" ]; then
|
||||
ACME="$ACME --acme-url $ACME_URL"
|
||||
fi
|
||||
fi
|
||||
|
||||
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 2
|
||||
else
|
||||
[ -n "$webroot" ] || [ -n "$dns" ] || [ -n "$tls" ] || pre_checks "$main_domain" || return 2
|
||||
fi
|
||||
|
||||
log "Running $APP for $main_domain"
|
||||
|
||||
if [ "$APP" = "uacme" ]; then
|
||||
if [ ! -f "$STATE_DIR/private/key.pem" ]; then
|
||||
log "Create a new ACME account with email $ACCOUNT_EMAIL use staging=$use_staging"
|
||||
$ACME $debug --confdir "$STATE_DIR" $staging --yes new $ACCOUNT_EMAIL
|
||||
fi
|
||||
|
||||
if [ -f "$STATE_DIR/$main_domain/cert.pem" ]; then
|
||||
log "Found previous cert config, use staging=$use_staging. Issuing renew."
|
||||
export CHALLENGE_PATH="$webroot"
|
||||
$ACME $debug --confdir "$STATE_DIR" $staging --never-create issue $domains --hook=$HPROGRAM; ret=$?
|
||||
post_checks
|
||||
return $ret
|
||||
fi
|
||||
fi
|
||||
if [ "$APP" = "acme" ]; then
|
||||
handle_credentials() {
|
||||
local credential="$1"
|
||||
eval export "$credential"
|
||||
}
|
||||
config_list_foreach "$section" credentials handle_credentials
|
||||
|
||||
if [ -e "$STATE_DIR/$main_domain" ]; then
|
||||
if [ "$use_staging" -eq "0" ] && is_staging "$main_domain"; then
|
||||
log "Found previous cert issued using staging server. Moving it out of the way."
|
||||
mv "$STATE_DIR/$main_domain" "$STATE_DIR/$main_domain.staging"
|
||||
else
|
||||
log "Found previous cert config. Issuing renew."
|
||||
$ACME --home "$STATE_DIR" --renew -d "$main_domain" "$acme_args"; ret=$?
|
||||
post_checks
|
||||
return $ret
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
acme_args="$acme_args --bits $keylength"
|
||||
acme_args="$acme_args $(for d in $domains; do echo -n " $d "; done)"
|
||||
if [ "$APP" = "acme" ]; then
|
||||
[ -n "$ACCOUNT_EMAIL" ] && acme_args="$acme_args --accountemail $ACCOUNT_EMAIL"
|
||||
[ "$use_staging" -eq "1" ] && acme_args="$acme_args --staging"
|
||||
fi
|
||||
if [ -n "$dns" ]; then
|
||||
#TO-DO
|
||||
if [ "$APP" = "acme" ]; then
|
||||
log "Using dns mode"
|
||||
acme_args="$acme_args --dns $dns"
|
||||
else
|
||||
log "Using dns mode, dns-01 is not wrapped yet"
|
||||
return 2
|
||||
# uacme_args="$uacme_args --dns $dns"
|
||||
fi
|
||||
elif [ -n "$tls" ]; then
|
||||
if [ "$APP" = "uacme" ]; then
|
||||
log "Using TLS mode"
|
||||
else
|
||||
log "TLS not supported by $APP"
|
||||
return 2
|
||||
fi
|
||||
elif [ -z "$webroot" ]; then
|
||||
if [ "$APP" = "acme" ]; then
|
||||
log "Using standalone mode"
|
||||
acme_args="$acme_args --standalone --listen-v6"
|
||||
else
|
||||
log "Standalone not supported by $APP"
|
||||
return 2
|
||||
fi
|
||||
else
|
||||
if [ ! -d "$webroot" ]; then
|
||||
err "$main_domain: Webroot dir '$webroot' does not exist!"
|
||||
post_checks
|
||||
return 2
|
||||
fi
|
||||
log "Using webroot dir: $webroot"
|
||||
if [ "$APP" = "uacme" ]; then
|
||||
export CHALLENGE_PATH="$webroot"
|
||||
else
|
||||
acme_args="$acme_args --webroot $webroot"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$APP" = "uacme" ]; then
|
||||
workdir="--confdir"
|
||||
HOOK="--hook=$HPROGRAM"
|
||||
else
|
||||
workdir="--home"
|
||||
fi
|
||||
|
||||
$ACME $debug $workdir "$STATE_DIR" $staging issue $acme_args $HOOK; ret=$?
|
||||
if [ "$ret" -ne 0 ]; then
|
||||
failed_dir="$STATE_DIR/${main_domain}.failed-$(date +%s)"
|
||||
err "Issuing cert for $main_domain failed. Moving state to $failed_dir"
|
||||
[ -d "$STATE_DIR/$main_domain" ] && mv "$STATE_DIR/$main_domain" "$failed_dir"
|
||||
[ -d "$STATE_DIR/private/$main_domain" ] && mv "$STATE_DIR/private/$main_domain" "$failed_dir"
|
||||
post_checks
|
||||
return $ret
|
||||
fi
|
||||
|
||||
if [ -e /etc/init.d/uhttpd ] && [ "$update_uhttpd" -eq "1" ]; then
|
||||
if [ "$APP" = "uacme" ]; then
|
||||
uci set uhttpd.main.key="$STATE_DIR/private/${main_domain}/key.pem"
|
||||
uci set uhttpd.main.cert="$STATE_DIR/${main_domain}/cert.pem"
|
||||
else
|
||||
uci set uhttpd.main.key="$STATE_DIR/${main_domain}/${main_domain}.key"
|
||||
uci set uhttpd.main.cert="$STATE_DIR/${main_domain}/fullchain.cer"
|
||||
fi
|
||||
# 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
|
||||
if [ "$APP" = "uacme" ]; then
|
||||
nginx-util add_ssl "${domain}" uacme "$STATE_DIR/${main_domain}/cert.pem" \
|
||||
"$STATE_DIR/private/${main_domain}/key.pem" || nginx_updated=0
|
||||
else
|
||||
nginx-util add_ssl "${domain}" acme "$STATE_DIR/${main_domain}/fullchain.cer" \
|
||||
"$STATE_DIR/${main_domain}/${main_domain}.key" || nginx_updated=0
|
||||
fi
|
||||
done
|
||||
# reload is in post_checks
|
||||
fi
|
||||
|
||||
if [ "$nginx_updated" -eq "0" ] && [ -w /etc/nginx/nginx.conf ] && [ "$update_nginx" -eq "1" ]; then
|
||||
if [ "$APP" = "uacme" ]; then
|
||||
sed -i "s#ssl_certificate\ .*#ssl_certificate $STATE_DIR/${main_domain}/cert.pem;#g" /etc/nginx/nginx.conf
|
||||
sed -i "s#ssl_certificate_key\ .*#ssl_certificate_key $STATE_DIR/private/${main_domain}/key.pem;#g" /etc/nginx/nginx.conf
|
||||
else
|
||||
sed -i "s#ssl_certificate\ .*#ssl_certificate $STATE_DIR/${main_domain}/fullchain.cer;#g" /etc/nginx/nginx.conf
|
||||
sed -i "s#ssl_certificate_key\ .*#ssl_certificate_key $STATE_DIR/${main_domain}/${main_domain}.key;#g" /etc/nginx/nginx.conf
|
||||
fi
|
||||
# commit and reload is in post_checks
|
||||
fi
|
||||
|
||||
if [ -e /etc/init.d/haproxy ] && [ "$update_haproxy" -eq 1 ]; then
|
||||
if [ "$APP" = "uacme" ]; then
|
||||
cat $STATE_DIR/${main_domain}/cert.pem $STATE_DIR/private/${main_domain}/key.pem > $STATE_DIR/${main_domain}/full_haproxy.pem
|
||||
else
|
||||
cat $STATE_DIR/${main_domain}/fullchain.cer $STATE_DIR/${main_domain}/${main_domain}.key > $STATE_DIR/${main_domain}/full_haproxy.pem
|
||||
fi
|
||||
fi
|
||||
|
||||
post_checks
|
||||
}
|
||||
|
||||
issue_cert_with_retries() {
|
||||
local section="$1"
|
||||
local use_staging
|
||||
local retries
|
||||
local use_auto_staging
|
||||
local infinite_retries
|
||||
config_get_bool use_staging "$section" use_staging
|
||||
config_get_bool use_auto_staging "$section" use_auto_staging
|
||||
config_get_bool enabled "$section" enabled
|
||||
config_get retries "$section" retries
|
||||
|
||||
[ -z "$retries" ] && retries=1
|
||||
[ -z "$use_auto_staging" ] && use_auto_staging=0
|
||||
[ "$retries" -eq "0" ] && infinite_retries=1
|
||||
[ "$enabled" -eq "1" ] || return 0
|
||||
|
||||
while true; do
|
||||
issue_cert "$1"; ret=$?
|
||||
|
||||
if [ "$ret" -eq "2" ]; then
|
||||
# An error occurred while retrieving the certificate.
|
||||
retries="$((retries-1))"
|
||||
|
||||
if [ "$use_auto_staging" -eq "1" ] && [ "$use_staging" -eq "0" ]; then
|
||||
log "Production certificate could not be obtained. Switching to staging server."
|
||||
use_staging=1
|
||||
uci set "acme.$1.use_staging=1"
|
||||
uci commit acme
|
||||
fi
|
||||
|
||||
if [ -z "$infinite_retries" ] && [ "$retries" -lt "1" ]; then
|
||||
log "An error occurred while retrieving the certificate. Retries exceeded."
|
||||
return "$ret"
|
||||
fi
|
||||
|
||||
if [ "$use_staging" -eq "1" ]; then
|
||||
# The "Failed Validations" limit of LetsEncrypt is 60 per hour. This
|
||||
# means one failure every minute. Here we wait 2 minutes to be within
|
||||
# limits for sure.
|
||||
sleeptime=120
|
||||
else
|
||||
# There is a "Failed Validation" limit of LetsEncrypt is 5 failures per
|
||||
# account, per hostname, per hour. This means one failure every 12
|
||||
# minutes. Here we wait 25 minutes to be within limits for sure.
|
||||
sleeptime=1500
|
||||
fi
|
||||
|
||||
log "An error occurred while retrieving the certificate. Retrying in $sleeptime seconds."
|
||||
sleep "$sleeptime"
|
||||
continue
|
||||
else
|
||||
if [ "$use_auto_staging" -eq "1" ]; then
|
||||
if [ "$use_staging" -eq "0" ]; then
|
||||
log "Production certificate obtained. Exiting."
|
||||
else
|
||||
log "Staging certificate obtained. Continuing with production server."
|
||||
use_staging=0
|
||||
uci set "acme.$1.use_staging=0"
|
||||
uci commit acme
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
return "$ret"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
load_vars()
|
||||
{
|
||||
local section="$1"
|
||||
|
||||
PRODUCTION_STATE_DIR=$(config_get "$section" state_dir)
|
||||
STAGING_STATE_DIR=$PRODUCTION_STATE_DIR/staging
|
||||
ACCOUNT_EMAIL=$(config_get "$section" account_email)
|
||||
DEBUG=$(config_get "$section" debug)
|
||||
ACME_URL=$(config_get "$section" acme_url)
|
||||
ACME_STAGING_URL=$(config_get "$section" acme_staging_url)
|
||||
}
|
||||
|
||||
if [ -z "$INCLUDE_ONLY" ]; then
|
||||
check_cron
|
||||
[ -n "$CHECK_CRON" ] && exit 0
|
||||
[ -e "/var/run/acme_boot" ] && rm -f "/var/run/acme_boot" && exit 0
|
||||
fi
|
||||
|
||||
config_load acme
|
||||
config_foreach load_vars acme
|
||||
|
||||
if [ -z "$PRODUCTION_STATE_DIR" ] || [ -z "$ACCOUNT_EMAIL" ]; then
|
||||
err "state_dir and account_email must be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ -d "$PRODUCTION_STATE_DIR" ] || mkdir -p "$PRODUCTION_STATE_DIR"
|
||||
[ -d "$STAGING_STATE_DIR" ] || mkdir -p "$STAGING_STATE_DIR"
|
||||
|
||||
trap err_out HUP TERM
|
||||
trap int_out INT
|
||||
|
||||
if [ -z "$INCLUDE_ONLY" ]; then
|
||||
config_foreach issue_cert_with_retries cert
|
||||
|
||||
exit 0
|
||||
fi
|
||||
Reference in New Issue
Block a user