Files
dokku/plugins/letsencrypt/internal-functions
apple 89e359ae7d
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
更新 letsencrypt
2026-04-25 14:14:39 +08:00

640 lines
19 KiB
Bash
Executable File

#!/usr/bin/env bash
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_CORE_AVAILABLE_PATH/certs/functions"
source "$PLUGIN_CORE_AVAILABLE_PATH/domains/functions"
source "$PLUGIN_CORE_AVAILABLE_PATH/nginx-vhosts/functions"
if [[ -f "$PLUGIN_CORE_AVAILABLE_PATH/nginx-vhosts/internal-functions" ]]; then
source "$PLUGIN_CORE_AVAILABLE_PATH/nginx-vhosts/internal-functions"
fi
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/config"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
fn-letsencrypt-acme-execute-challenge() {
declare desc="perform actual ACME validation procedure"
declare APP="$1"
local FAKE_NGINX_CONF=false
local challenge_mode config_dir
if [[ ! -f "$DOKKU_ROOT/$APP/nginx.conf" ]]; then
FAKE_NGINX_CONF=true
fi
fn-letsencrypt-create-root "$APP"
challenge_mode="HTTP-01"
dns_provider="$(fn-letsencrypt-computed-dns-provider "$APP")"
if [[ -n "$dns_provider" ]]; then
challenge_mode="DNS-01"
fi
dokku_log_info1 "Getting letsencrypt certificate for ${APP} via ${challenge_mode}"
# read arguments from appropriate config file into the config array
config_dirs="$(fn-letsencrypt-configure-and-get-dir "$APP")"
host_config_dir="$(echo "$config_dirs" | cut -d: -f1)"
container_config_dir="$(echo "$config_dirs" | cut -d: -f2)"
read -r -a config <"$container_config_dir/config"
# run letsencrypt as a docker container using "certonly" mode
# port 80 of the standalone webserver will be forwarded by the proxy
set +e
export DOKKU_UID=$(id -u)
export DOKKU_GID=$(id -g)
mkdir -p "$DOKKU_LIB_ROOT/data/letsencrypt/$APP"
docker run --rm \
--env-file "$host_config_dir/docker.env" \
--user $DOKKU_UID:$DOKKU_GID \
-v "$host_config_dir:/certs" \
-v "$DOKKU_LIB_ROOT/data/letsencrypt/$APP:/webroot" \
"${PLUGIN_IMAGE}:${PLUGIN_IMAGE_VERSION}" \
"${config[@]}" run | sed "s/^/ /"
local exit_code=$?
set -e
if [[ "$FAKE_NGINX_CONF" == "true" ]]; then
rm "$DOKKU_ROOT/$APP/nginx.conf"
fi
if [[ $exit_code != 0 ]]; then
dokku_log_info1 "Certificate retrieval failed!"
return $exit_code
fi
# got certificate
dokku_log_info1 "Certificate retrieved successfully."
fn-letsencrypt-symlink-certs "$APP" "$container_config_dir"
plugn trigger proxy-build-config "$APP"
}
fn-letsencrypt-acme-revoke() {
declare desc="perform actual certificate revocation"
local APP="$1"
fn-letsencrypt-create-root "$APP"
challenge_mode="HTTP-01"
dns_provider="$(fn-letsencrypt-computed-dns-provider "$APP")"
if [[ -n "$dns_provider" ]]; then
challenge_mode="DNS-01"
fi
dokku_log_info1 "Revoking letsencrypt certificate for ${APP} via ${challenge_mode}"
local acme_port="$(plugn trigger ports-get-available)"
if [[ -z "$acme_port" ]]; then
acme_port="$(get_available_port)"
fi
# read arguments from appropriate config file into the config array
config_dirs="$(fn-letsencrypt-configure-and-get-dir "$APP")"
host_config_dir="$(echo "$config_dirs" | cut -d: -f1)"
container_config_dir="$(echo "$config_dirs" | cut -d: -f2)"
read -r -a config <"$container_config_dir/config"
# run letsencrypt as a docker container using "certonly" mode
# port 80 of the standalone webserver will be forwarded by the proxy
set +e
export DOKKU_UID=$(id -u)
export DOKKU_GID=$(id -g)
mkdir -p "$DOKKU_LIB_ROOT/data/letsencrypt/$APP"
docker run --rm \
--env-file "$host_config_dir/docker.env" \
--user $DOKKU_UID:$DOKKU_GID \
-p "$acme_port:$acme_port" \
-v "$host_config_dir:/certs" \
-v "$DOKKU_LIB_ROOT/data/letsencrypt/$APP:/webroot" \
"${PLUGIN_IMAGE}:${PLUGIN_IMAGE_VERSION}" \
"${config[@]}" revoke | sed "s/^/ /"
local exit_code=$?
set -e
# handle return codes
if [[ $exit_code == 0 ]]; then
# certificate revoked
dokku_log_info1 "Certificate revoked successfully."
else
# error - don't try to link certificates
dokku_log_info1 "Certificate revocation failed (code $simple_result)!"
return
fi
local domain="$(get_app_domains "$APP" | xargs | awk '{print $1}')"
# removing the certificate will automatically reconfigure nginx
if [[ -z $DOKKU_APP_NAME ]]; then
dokku certs:remove "$APP"
else
dokku certs:remove
fi
}
fn-letsencrypt-acme-proxy-disable() {
declare desc="disable ACME proxy for an app"
local APP="$1"
local app_root="$DOKKU_ROOT/$APP"
local app_config_dir="$app_root/nginx.conf.d"
dokku_log_info1 "Disabling ACME proxy for $APP..."
[[ -f "$app_config_dir/letsencrypt.conf" ]] && rm "$app_config_dir/letsencrypt.conf"
restart_nginx | sed "s/^/ /"
}
fn-letsencrypt-acme-proxy-enable() {
declare desc="enable ACME proxy for an app"
local APP="$1"
local app_root="$DOKKU_ROOT/$APP"
local app_config_dir="$app_root/nginx.conf.d"
dokku_log_info1 "Enabling ACME proxy for ${APP}..."
# ensure the nginx.conf.d directory exists
[[ -d "$app_config_dir" ]] || mkdir "$app_config_dir"
# generate letsencrypt config
sigil -f "$PLUGIN_AVAILABLE_PATH/letsencrypt/templates/letsencrypt.conf.sigil" \
APP="$APP" \
DOKKU_LIB_ROOT="$DOKKU_LIB_ROOT" \
>"$app_config_dir/letsencrypt.conf"
restart_nginx | sed "s/^/ /"
}
fn-letsencrypt-computed-dns-provider() {
declare desc="get configured dns provider"
declare APP="$1"
value="$(fn-letsencrypt-dns-provider "$APP")"
if [[ -z "$value" ]]; then
value="$(fn-letsencrypt-global-dns-provider)"
fi
echo "$value"
}
fn-letsencrypt-global-dns-provider() {
declare desc="get configured dns provider"
fn-plugin-property-get-default "letsencrypt" "--global" "dns-provider" ""
}
fn-letsencrypt-dns-provider() {
declare desc="get configured dns provider"
declare APP="$1"
fn-plugin-property-get-default "letsencrypt" "$APP" "dns-provider" ""
}
fn-letsencrypt-computed-lego-docker-args() {
declare desc="get configured lego docker args"
declare APP="$1"
value="$(fn-letsencrypt-lego-docker-args "$APP")"
if [[ -z "$value" ]]; then
value="$(fn-letsencrypt-global-lego-docker-args)"
fi
echo "$value"
}
fn-letsencrypt-global-lego-docker-args() {
declare desc="get configured lego docker args"
fn-plugin-property-get-default "letsencrypt" "--global" "lego-docker-args" ""
}
fn-letsencrypt-lego-docker-args() {
declare desc="get configured lego docker args"
declare APP="$1"
fn-plugin-property-get-default "letsencrypt" "$APP" "lego-docker-args" ""
}
fn-letsencrypt-check-email() {
declare desc="Check if an e-mail address is provided globally or for the app"
declare APP="$1"
# check we have a valid e-mail address
if [[ -z "$(fn-letsencrypt-computed-email "$APP")" ]]; then
dokku_log_warn "ERROR: Cannot request a certificate without an e-mail address!"
dokku_log_warn " please provide your e-mail address using"
dokku_log_warn " dokku letsencrypt:set $APP email <e-mail>"
return 1
fi
}
fn-letsencrypt-configure-and-get-dir() {
declare desc="assemble lego command line arguments and create a config hash directory for them"
declare APP="$1"
local config config_dir config_hash dns_provider domain_args domains email extra_args cert_timeout key server value
local app_root="$DOKKU_ROOT/$APP"
local le_root="$app_root/letsencrypt"
mkdir -p "$DOKKU_ROOT/$APP/letsencrypt/account"
# build up a string of all certificate-controlling configuration settings.
# this will be used to determine the folder name for the account key and certificates
# get the selected ACME server
server="$(fn-letsencrypt-computed-server "$APP")"
# construct domain arguments
domains="$(get_app_domains "$APP")"
domain_args=''
for domain in $domains; do
dokku_log_verbose " - Domain '$domain'" >&2
domain_args="$domain_args --domains $domain"
done
# lego --cert.timeout is in seconds and defaults to 30
cert_timeout=30
email="$(fn-letsencrypt-computed-email "$APP")"
extra_args="$(fn-letsencrypt-computed-lego-docker-args "$APP")"
config="--pem --accept-tos --cert.timeout $cert_timeout --path /certs --server $server --email $email $extra_args $domain_args"
dns_provider="$(fn-letsencrypt-computed-dns-provider "$APP")"
if [[ -n "$dns_provider" ]]; then
config="--dns $dns_provider $config"
else
config="--http --http.webroot /webroot $config"
fi
config_hash=$(echo "$config" | sha1sum | awk '{print $1}')
config_dir="$le_root/certs/$config_hash"
mkdir -p "$config_dir"
rm -f "$config_dir/docker.env"
touch "$config_dir/docker.env"
if [[ -n "$dns_provider" ]]; then
fn-plugin-property-get-all "letsencrypt" "--global" | while read -r line; do
[[ -n "$line" ]] || continue
key="$(cut -d" " -f1 <<<"$line")"
if [[ "$key" == dns-provider-* ]]; then
value="$(cut -d" " -f2 <<<"$line")"
echo "${key#"dns-provider-"}=$value" >>"$config_dir/docker.env"
fi
done
fn-plugin-property-get-all "letsencrypt" "$APP" | while read -r line; do
[[ -n "$line" ]] || continue
key="$(cut -d" " -f1 <<<"$line")"
if [[ "$key" == dns-provider-* ]]; then
value="$(cut -d" " -f2 <<<"$line")"
echo "${key#"dns-provider-"}=$value" >>"$config_dir/docker.env"
fi
done
fi
# ensure the permissions are set correctly on anything that may expose api keys
chmod 0755 "$config_dir/docker.env"
# store config settings
echo "$config" >"$config_dir/config"
# send both host and container path
# to respect mapped DOKKU_ROOT when running in a container
echo "$DOKKU_HOST_ROOT/$APP/letsencrypt/certs/$config_hash:$config_dir"
}
fn-letsencrypt-cron-job-enabled() {
declare desc="Check if the cron plugin is available"
if [[ ! -f "$PLUGIN_AVAILABLE_PATH/cron/cron-write" ]]; then
return 1
fi
}
fn-letsencrypt-cron-job-add() {
declare desc="Add auto-renew cronjob to dokku user's crontab"
touch "${DOKKU_LIB_ROOT}/data/letsencrypt/autorenew"
if fn-letsencrypt-cron-job-enabled; then
plugn trigger cron-write
else
crons="$(crontab -l || true)"
crons="$(grep -v -F "$LETSENCRYPT_CRON_CMD" <<<"$crons")"
printf "%s\n%s\n" "$crons" "$LETSENCRYPT_CRON_JOB" | crontab -
fi
dokku_log_info1 "Added cron job to dokku's crontab."
}
fn-letsencrypt-cron-job-remove() {
declare desc="Remove auto-renew cronjob from dokku user's crontab"
rm -f "${DOKKU_LIB_ROOT}/data/letsencrypt/autorenew"
if fn-letsencrypt-cron-job-enabled; then
plugn trigger cron-write
else
crons="$(crontab -l || true)"
crons="$(grep -v -F "$LETSENCRYPT_CRON_CMD" <<<"$crons")"
printf "%s" "$crons" | crontab -
fi
dokku_log_info1 "Removed cron job from dokku's crontab."
}
fn-letsencrypt-create-root() {
declare desc="Ensure the let's encrypt root directory exists"
declare APP="$1"
local NGINX_ACCESS_LOG_FORMAT NGINX_ACCESS_LOG_PATH NGINX_ERROR_LOG_PATH
local app_root="$DOKKU_ROOT/$APP"
local le_root="$app_root/letsencrypt"
mkdir -p "$le_root"
if [[ ! -f "$DOKKU_ROOT/$APP/nginx.conf" ]]; then
dokku_log_info1 "Setting temporary site"
NGINX_ACCESS_LOG_FORMAT="$(fn-nginx-access-log-format "$APP")"
NGINX_ACCESS_LOG_PATH="$(fn-nginx-access-log-path "$APP")"
NGINX_ERROR_LOG_PATH="$(fn-nginx-error-log-path "$APP")"
if [[ -z "$NGINX_ACCESS_LOG_FORMAT" ]]; then
NGINX_ACCESS_LOG_FORMAT="$(fn-nginx-computed-access-log-format "$APP")"
fi
if [[ -z "$NGINX_ACCESS_LOG_PATH" ]]; then
NGINX_ACCESS_LOG_PATH="$(fn-nginx-computed-access-log-path "$APP")"
fi
if [[ -z "$NGINX_ERROR_LOG_PATH" ]]; then
NGINX_ERROR_LOG_PATH="$(fn-nginx-computed-error-log-path "$APP")"
fi
sigil -f "$PLUGIN_AVAILABLE_PATH/letsencrypt/templates/default-nginx.conf.sigil" \
DOMAINS="$(get_app_domains "$APP" | xargs)" DOKKU_ROOT="$DOKKU_ROOT" APP="$APP" \
NGINX_ACCESS_LOG_FORMAT="$NGINX_ACCESS_LOG_FORMAT" NGINX_ACCESS_LOG_PATH="$NGINX_ACCESS_LOG_PATH" \
NGINX_ERROR_LOG_PATH="$NGINX_ERROR_LOG_PATH" \
>"$DOKKU_ROOT/$APP/nginx.conf"
restart_nginx | sed "s/^/ /"
fi
}
fn-letsencrypt-is-autorenew-enabled() {
declare desc="check if autorenew is enabled"
local enabled=false
if [[ -f "${DOKKU_LIB_ROOT}/data/letsencrypt/autorenew" ]]; then
enabled=true
fi
echo "$enabled"
}
fn-letsencrypt-computed-email() {
declare desc="get configured email address"
declare APP="$1"
value="$(fn-letsencrypt-email "$APP")"
if [[ -z "$value" ]]; then
value="$(fn-letsencrypt-global-email)"
fi
echo "$value"
}
fn-letsencrypt-global-email() {
declare desc="get configured email address"
fn-plugin-property-get-default "letsencrypt" "--global" "email" ""
}
fn-letsencrypt-email() {
declare desc="get configured email address"
declare APP="$1"
fn-plugin-property-get-default "letsencrypt" "$APP" "email" ""
}
fn-letsencrypt-enable() {
declare APP="$1"
local EXIT_CODE=0
local domain
verify_app_name "$APP"
domain="$(get_app_domains "$APP" | xargs | awk '{print $1}')"
if [[ -z "$domain" ]]; then
dokku_log_warn "No domains detected for $APP"
return 1
fi
dokku_log_info2 "Enabling letsencrypt for $APP"
fn-letsencrypt-check-email "$APP"
fn-letsencrypt-acme-proxy-enable "$APP"
fn-letsencrypt-acme-execute-challenge "$APP" || EXIT_CODE=$? # remove ACME proxy even if this fails
fn-letsencrypt-acme-proxy-disable "$APP"
if [[ "$EXIT_CODE" == 0 ]]; then
dokku_log_info1 "Done"
return
fi
dokku_log_warn "Failed to setup letsencrypt"
DOKKU_FAIL_EXIT_CODE="$EXIT_CODE" dokku_log_fail "Check log output for further information on failure"
}
fn-letsencrypt-expiration() {
declare desc="prints expiration time"
declare APP="$1"
if [[ -f "$DOKKU_ROOT/$APP/tls/server.letsencrypt.crt" ]]; then
date -u -d "$(openssl x509 -in "$DOKKU_ROOT/$APP/tls/server.letsencrypt.crt" -enddate -noout | sed -e "s/^notAfter=//")" "+%s"
else
date -u -d "$(openssl x509 -in "$DOKKU_ROOT/$APP/tls/server.crt" -enddate -noout | sed -e "s/^notAfter=//")" "+%s"
fi
}
fn-letsencrypt-computed-graceperiod() {
declare desc="get configured graceperiod"
declare APP="$1"
local value
value="$(fn-letsencrypt-graceperiod "$APP")"
if [[ -z "$value" ]]; then
value="$(fn-letsencrypt-global-graceperiod)"
fi
if [[ -z "$value" ]]; then
value="$((60 * 60 * 24 * 30))"
fi
echo "$value"
}
fn-letsencrypt-global-graceperiod() {
declare desc="get configured graceperiod"
fn-plugin-property-get-default "letsencrypt" "--global" "graceperiod" ""
}
fn-letsencrypt-graceperiod() {
declare desc="get configured graceperiod"
declare APP="$1"
fn-plugin-property-get-default "letsencrypt" "$APP" "graceperiod" ""
}
fn-letsencrypt-format-timediff() {
declare desc="format a time difference in seconds into a human-readable string"
local td="$1"
local negative_td=0
if [ "$td" -lt 0 ]; then
negative_td=1
td=$((-td))
fi
local days=$((td / (24 * 60 * 60)))
td=$((td % (24 * 60 * 60)))
local hours=$((td / (60 * 60)))
td=$((td % (60 * 60)))
local minutes=$((td / 60))
local secs=$((td % 60))
local res=""
if [ $days -gt 0 ]; then
res="${days}d, "
fi
if [ $hours -gt 0 ]; then
res="${res}${hours}h, "
fi
if [ $minutes -gt 0 ]; then
res="${res}${minutes}m, "
fi
if [ $secs -gt 0 ]; then
res="${res}${secs}s, "
fi
# remove trailing comma
res="$(echo "$res" | sed -re 's/, ?$//g')"
if [[ $negative_td == 1 ]]; then
res="${res} ago"
fi
echo "$res"
}
fn-letsencrypt-is-active() {
declare desc="outputs true if active, false otherwise"
declare APP=$1
local domain
# check if SSL is enabled on per-app level
if ! is_ssl_enabled "$APP"; then
echo "false"
return
fi
domain="$(get_app_domains "$APP" | xargs | awk '{print $1}')"
# check if certificate is identical to the current let's encrypt certificate by comparing SHA1 hashes
local cert_sha1
if [[ -f "$DOKKU_ROOT/$APP/tls/server.letsencrypt.crt" ]]; then
cert_sha1=$( (cat "$DOKKU_ROOT/$APP/tls/server.letsencrypt.crt" 2>/dev/null) | sha1sum || echo "not_found")
else
cert_sha1=$( (cat "$DOKKU_ROOT/$APP/tls/server.crt" 2>/dev/null) | sha1sum || echo "not_found")
fi
local le_sha1="not_found"
local fileSafeDomain
fileSafeDomain="${domain/\*/_}" # wildcards are using *.example.com which have certificates named _.example.com
if [[ -f "$DOKKU_ROOT/$APP/letsencrypt/certs/current/certificates/$fileSafeDomain.pem" ]]; then
le_sha1=$( (cat "$DOKKU_ROOT/$APP/letsencrypt/certs/current/certificates/$fileSafeDomain.crt" 2>/dev/null) | sha1sum || echo "not_found")
elif [[ -f "$DOKKU_ROOT/$APP/letsencrypt/certs/current/fullchain.pem" ]]; then
le_sha1=$( (cat "$DOKKU_ROOT/$APP/letsencrypt/certs/current/fullchain.pem" 2>/dev/null) | sha1sum || echo "not_found")
fi
if [[ "$cert_sha1" != "$le_sha1" ]]; then
echo "false"
return
fi
echo "true"
}
fn-letsencrypt-list-apps-with-expiry() {
declare desc="list all letsencrypt-secured apps together with their expiry date"
# prints a tab-separated list of
# * app name
# * expiry dates as UNIX timestamp (seconds since epoch)
# * selected renewal grace period (in seconds)
# * time left on certificate (in seconds)
# * time until renewal (in seconds)
for APP in $(dokku_apps); do
if [[ "$APP" == "=====>" ]] || [[ "$APP" == "My" ]] || [[ "$APP" == "Apps" ]]; then continue; fi
if [[ "$(fn-letsencrypt-is-active "$APP")" == "true" ]]; then
local expiry=$(fn-letsencrypt-expiration "$APP")
local grace_period="$(fn-letsencrypt-computed-graceperiod "$APP")"
local time_to_expiry=$((expiry - $(date +%s)))
local time_to_renewal=$((expiry - grace_period - $(date +%s)))
echo -e "$APP\t$expiry\t$grace_period\t$time_to_expiry\t$time_to_renewal"
fi
done
}
fn-letsencrypt-computed-server() {
declare desc="get configured server"
declare APP="$1"
value="$(fn-letsencrypt-server "$APP")"
if [[ -z "$value" ]]; then
value="$(fn-letsencrypt-global-server)"
fi
if [[ -z "$value" ]] || [[ "$value" == "default" ]]; then
value="https://acme-v02.api.letsencrypt.org/directory"
elif [[ "$value" == "staging" ]]; then
value="https://acme-staging-v02.api.letsencrypt.org/directory"
fi
echo "$value"
}
fn-letsencrypt-global-server() {
declare desc="get configured server"
fn-plugin-property-get-default "letsencrypt" "--global" "server" ""
}
fn-letsencrypt-server() {
declare desc="get configured server"
declare APP="$1"
fn-plugin-property-get-default "letsencrypt" "$APP" "server" ""
}
fn-letsencrypt-symlink-certs() {
declare desc="symlink let's encrypt certificates so they can be found by dokku"
declare APP="$1" config_dir="$2"
local app_root="$DOKKU_ROOT/$APP"
local le_root="$app_root/letsencrypt"
local domain
dokku_log_info1 "Installing let's encrypt certificates"
# link the current config directory to 'current'
ln -nsf "$config_dir" "$le_root/certs/current"
# install the let's encrypt certificate for the app
unset DOKKU_APP_NAME
domain="$(get_app_domains "$APP" | xargs | awk '{print $1}')"
local fileSafeDomain
fileSafeDomain="${domain/\*/_}" # wildcards are using *.example.com which have certificates named _.example.com
dokku certs:add "$APP" "$config_dir/certificates/$fileSafeDomain.pem" "$config_dir/certificates/$fileSafeDomain.key"
rm -f "$app_root/tls/server.letsencrypt.crt" "$app_root/tls/server.crt"
cp "$config_dir/certificates/$fileSafeDomain.crt" "$app_root/tls/server.letsencrypt.crt"
cp "$config_dir/certificates/$fileSafeDomain.crt" "$app_root/tls/server.crt"
}