Created
November 17, 2020 13:41
-
-
Save jaapmarcus/db87d7d1086608b7e57e1ce57c752a1f to your computer and use it in GitHub Desktop.
v-add-lets-encrypt-domain
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# info: check letsencrypt domain | |
# options: USER DOMAIN [ALIASES] [MAIL] | |
# labels: web | |
# | |
# example: v-add-letsencrypt-domain admin wonderland.com www.wonderland.com | |
# | |
# The function check and validates domain with Let's Encrypt | |
#----------------------------------------------------------# | |
# Variable&Function # | |
#----------------------------------------------------------# | |
# Argument definition | |
user=$1 | |
domain=$2 | |
aliases=$3 | |
mail=${4// } | |
# Includes | |
source $HESTIA/func/main.sh | |
source $HESTIA/func/domain.sh | |
source $HESTIA/conf/hestia.conf | |
# LE API | |
LE_API='https://acme-v02.api.letsencrypt.org' | |
if [[ "$LE_STAGING" = 'yes' ]]; then | |
LE_API='https://acme-staging-v02.api.letsencrypt.org' | |
fi | |
# encode base64 | |
encode_base64() { | |
cat |base64 |tr '+/' '-_' |tr -d '\r\n=' | |
} | |
# Let's Encrypt v2 curl function | |
query_le_v2() { | |
protected='{"nonce": "'$3'",' | |
protected=''$protected' "url": "'$1'",' | |
protected=''$protected' "alg": "RS256", "kid": "'$KID'"}' | |
content="Content-Type: application/jose+json" | |
payload_=$(echo -n "$2" |encode_base64) | |
protected_=$(echo -n "$protected" |encode_base64) | |
signature_=$(printf "%s" "$protected_.$payload_" |\ | |
openssl dgst -sha256 -binary -sign $USER_DATA/ssl/user.key |\ | |
encode_base64) | |
post_data='{"protected":"'"$protected_"'",' | |
post_data=$post_data'"payload":"'"$payload_"'",' | |
post_data=$post_data'"signature":"'"$signature_"'"}' | |
# Save http response to file passed as "$4" arg or print to stdout if not provided | |
# http response headers are always sent to stdout | |
local save_to_file=${4:-"/dev/stdout"} | |
curl --silent --dump-header /dev/stdout --data "$post_data" "$1" --header "$content" --output "$save_to_file" | |
} | |
#----------------------------------------------------------# | |
# Verifications # | |
#----------------------------------------------------------# | |
check_args '2' "$#" 'USER DOMAIN [ALIASES] [MAIL]' | |
is_format_valid 'user' 'domain' 'aliases' | |
is_object_valid 'user' 'USER' "$user" | |
is_object_unsuspended 'user' 'USER' "$user" | |
if [ ! -z "$mail" ]; then | |
is_boolean_format_valid "$mail" 'mail' | |
fi | |
# Set DNS CAA record retrieval commands | |
if [ ! -z "$DNS_SYSTEM" ]; then | |
dns_domain=$($BIN/v-list-dns-domains $user | grep $domain | cut -d' ' -f1) | |
caa_record=$($BIN/v-list-dns-records $user $domain | grep -i "CAA" | cut -d' ' -f1) | |
fi | |
if [ -z "$mail" ] || [ "$mail" = 'no' ]; then | |
mail='' | |
is_system_enabled "$WEB_SYSTEM" 'WEB_SYSTEM' | |
is_object_valid 'web' 'DOMAIN' "$domain" | |
is_object_unsuspended 'web' 'DOMAIN' "$domain" | |
get_domain_values 'web' | |
# check if alias is the letsencrypt wildcard domain, if not, make the normal checks | |
if [[ "$aliases" != "*.$domain" ]]; then | |
for alias in $(echo "$aliases" |tr ',' '\n' |sort -u); do | |
check_alias="$(echo $ALIAS |tr ',' '\n' |grep ^$alias$)" | |
if [ -z "$check_alias" ]; then | |
check_result $E_NOTEXIST "domain alias $alias doesn't exist" | |
fi | |
done | |
fi; | |
else | |
is_system_enabled "$MAIL_SYSTEM" 'MAIL_SYSTEM' | |
is_object_valid 'mail' 'DOMAIN' "$domain" | |
is_object_unsuspended 'mail' 'DOMAIN' "$domain" | |
fi | |
#----------------------------------------------------------# | |
# Action # | |
#----------------------------------------------------------# | |
# Generate correct variables for mail domain SSL certificates | |
if [ ! -z "$mail" ]; then | |
root_domain=$domain | |
domain="mail.$root_domain" | |
aliases="$WEBMAIL_ALIAS.$root_domain" | |
fi | |
# Registering LetsEncrypt user account | |
$BIN/v-add-letsencrypt-user $user | |
if [ "$?" -ne 0 ]; then | |
touch $HESTIA/data/queue/letsencrypt.pipe | |
sed -i "/ $domain /d" $HESTIA/data/queue/letsencrypt.pipe | |
send_notice "LETSENCRYPT" "Account registration failed" | |
check_result $E_CONNECT "LE account registration" > /dev/null | |
fi | |
# Parsing LetsEncrypt account data | |
source $USER_DATA/ssl/le.conf | |
# Checking wildcard alias | |
if [ "$aliases" = "*.$domain" ]; then | |
wildcard='yes' | |
proto="dns-01" | |
if [ ! -e "$HESTIA/data/users/$user/dns/$domain.conf" ]; then | |
check_result $E_NOTEXIST "DNS domain $domain doesn't exist" | |
fi | |
else | |
proto="http-01" | |
fi | |
# Check if dns records exist for requested domain/aliases | |
if [ "$proto" = "http-01" ]; then | |
for identifier in $(echo $domain,$aliases |tr ',' '\n' |sort -u); do | |
if [[ "$identifier" = *[![:ascii:]]* ]]; then | |
identifier=$(idn -t --quiet -a $identifier) | |
fi | |
if ! nslookup "${identifier}" > /dev/null 2>&1 ; then | |
check_result $E_NOTEXIST "DNS record for $identifier doesn't exist" | |
fi | |
done | |
fi | |
# Ensure DNS CAA record exists for Let's Encrypt before requesting certificate | |
if [ ! -z "$DNS_SYSTEM" ]; then | |
# Check for DNS zone | |
if [ "$dns_domain" = "$domain" ]; then | |
# Replace DNS domain CAA records with Let's Encrypt values | |
if [ -z "$caa_record" ]; then | |
$BIN/v-add-dns-record $user $domain '@' 'CAA' '0 issue "letsencrypt.org"' | |
else | |
$BIN/v-delete-dns-record $user $domain $caa_record | |
$BIN/v-add-dns-record $user $domain '@' 'CAA' '0 issue "letsencrypt.org"' | |
fi | |
fi | |
fi | |
# Requesting nonce / STEP 1 | |
answer=$(curl -s -I "$LE_API/directory") | |
nonce=$(echo "$answer" |grep -i nonce |cut -f2 -d \ |tr -d '\r\n') | |
status=$(echo "$answer"|grep HTTP/ |tail -n1 |cut -f 2 -d ' ') | |
if [[ "$status" -ne 200 ]]; then | |
# Delete DNS CAA record | |
if [ ! -z "$DNS_SYSTEM" ]; then | |
if [ "$dns_domain" = "$domain" ]; then | |
if [ ! -z "$caa_record" ]; then | |
$BIN/v-delete-dns-record $user $domain $caa_record | |
fi | |
fi | |
fi | |
check_result $E_CONNECT "Let's Encrypt nonce request status $status" | |
fi | |
# Placing new order / STEP 2 | |
url="$LE_API/acme/new-order" | |
payload='{"identifiers":[' | |
for identifier in $(echo $domain,$aliases |tr ',' '\n' |sort -u); do | |
if [[ "$identifier" = *[![:ascii:]]* ]]; then | |
identifier=$(idn -t --quiet -a $identifier) | |
fi | |
payload=$payload'{"type":"dns","value":"'$identifier'"},' | |
done | |
payload=$(echo "$payload"|sed "s/,$//") | |
payload=$payload']}' | |
answer=$(query_le_v2 "$url" "$payload" "$nonce") | |
nonce=$(echo "$answer" |grep -i nonce |cut -f2 -d \ |tr -d '\r\n') | |
authz=$(echo "$answer" |grep "acme/authz" |cut -f2 -d '"') | |
finalize=$(echo "$answer" |grep 'finalize":' |cut -f4 -d '"') | |
status=$(echo "$answer" |grep HTTP/ |tail -n1 |cut -f2 -d ' ') | |
echo "$user - $domain - $answer" >> "/var/log/hestia/letsencrypt-debug.log" | |
if [[ "$status" -ne 201 ]]; then | |
# Delete DNS CAA record | |
if [ ! -z "$DNS_SYSTEM" ]; then | |
if [ "$dns_domain" = "$domain" ]; then | |
if [ ! -z "$caa_record" ]; then | |
$BIN/v-delete-dns-record $user $domain $caa_record | |
fi | |
fi | |
fi | |
check_result $E_CONNECT "Let's Encrypt new auth status $status" | |
fi | |
# Requesting authorization token / STEP 3 | |
for auth in $authz; do | |
payload='' | |
answer=$(query_le_v2 "$auth" "$payload" "$nonce") | |
url=$(echo "$answer" |grep -A3 $proto |grep url |cut -f 4 -d \") | |
token=$(echo "$answer" |grep -A3 $proto |grep token |cut -f 4 -d \") | |
nonce=$(echo "$answer" |grep -i nonce |cut -f2 -d \ |tr -d '\r\n') | |
status=$(echo "$answer"|grep HTTP/ |tail -n1 |cut -f 2 -d ' ') | |
if [[ "$status" -ne 200 ]]; then | |
# Delete DNS CAA record | |
if [ ! -z "$DNS_SYSTEM" ]; then | |
dns_domain=$($BIN/v-list-dns-domains $user | grep $domain | cut -d' ' -f1) | |
caa_record=$($BIN/v-list-dns-records $user $domain | grep -i "letsencrypt" | cut -d' ' -f1) | |
if [ "$dns_domain" = "$domain" ]; then | |
if [ ! -z "$caa_record" ]; then | |
$BIN/v-delete-dns-record $user $domain $caa_record | |
fi | |
fi | |
fi | |
check_result $E_CONNECT "Let's Encrypt acme/authz bad status $status" | |
fi | |
# Accepting challenge / STEP 4 | |
if [ "$wildcard" = 'yes' ]; then | |
record=$(printf "%s" "$token.$THUMB" |\ | |
openssl dgst -sha256 -binary |encode_base64) | |
old_records=$($BIN/v-list-dns-records $user $domain plain|grep 'TXT') | |
old_records=$(echo "$old_records" |grep _acme-challenge |cut -f 1) | |
for old_record in $old_records; do | |
$BIN/v-delete-dns-record $user $domain $old_record | |
done | |
$BIN/v-add-dns-record $user $domain "_acme-challenge" "TXT" $record | |
check_result $? "DNS _acme-challenge record wasn't created" | |
else | |
if [ -z "$mail" ]; then | |
if [ "$WEB_SYSTEM" = 'nginx' ] || [ "$PROXY_SYSTEM" = 'nginx' ]; then | |
conf="$HOMEDIR/$user/conf/web/$domain/nginx.conf_letsencrypt" | |
sconf="$HOMEDIR/$user/conf/web/$domain/nginx.ssl.conf_letsencrypt" | |
echo 'location ~ "^/\.well-known/acme-challenge/(.*)$" {' \ | |
> $conf | |
echo ' default_type text/plain;' >> $conf | |
echo ' return 200 "$1.'$THUMB'";' >> $conf | |
echo '}' >> $conf | |
if [ ! -e "$sconf" ]; then | |
ln -s "$conf" "$sconf" | |
fi | |
if [ ! -z "$PROXY_SYSTEM" ]; then | |
$BIN/v-restart-proxy | |
check_result $? "Proxy restart failed" > /dev/null | |
fi | |
else | |
# Get root directory from configuration | |
domain_config="$HOMEDIR/$user/conf/web/$domain" | |
if [ -f "$domain_config/apache2.conf" ]; then | |
well_known="$(cat $domain_config/apache2.conf | egrep \ | |
'^\s+DocumentRoot'| awk '{split($0, a, " "); \ | |
print a[2]}')/.well-known" | |
else | |
well_known="$(cat $domain_config/nginx.conf | egrep '^\s+root'| \ | |
awk '{split($0, a, " "); print a[2]}' | \ | |
sed 's/;$//')/.well-known" | |
fi | |
acme_challenge="$well_known/acme-challenge" | |
mkdir -p $acme_challenge | |
echo "$token.$THUMB" > $acme_challenge/$token | |
chown -R $user:$user $well_known | |
fi | |
else | |
well_known="/var/lib/roundcube/.well-known" | |
acme_challenge="$well_known/acme-challenge" | |
mkdir -p $acme_challenge | |
echo "$token.$THUMB" > $acme_challenge/$token | |
chown -R $user:$user $well_known | |
fi | |
if [ "$WEB_SYSTEM" = 'nginx' ]; then | |
$BIN/v-restart-web | |
check_result $? "Web restart failed" > /dev/null | |
fi | |
fi | |
# Requesting ACME validation / STEP 5 | |
validation_check=$(echo "$answer" |grep '"valid"') | |
if [[ ! -z "$validation_check" ]]; then | |
validation='valid' | |
else | |
validation='pending' | |
sleep 5 | |
fi | |
# Doing pol check on status | |
i=1 | |
while [ "$validation" = 'pending' ]; do | |
payload='{}' | |
answer=$(query_le_v2 "$url" "$payload" "$nonce") | |
validation=$(echo "$answer"|grep -A1 $proto |tail -n1|cut -f4 -d \") | |
nonce=$(echo "$answer" |grep -i nonce |cut -f2 -d \ |tr -d '\r\n') | |
status=$(echo "$answer"|grep HTTP/ |tail -n1 |cut -f 2 -d ' ') | |
details=$(echo "$answer"| grep detail | cut -f 1 -d ',' | cut -f 2-4 -d ':' | cut -f 2 -d '"') | |
if [[ "$status" -ne 200 ]]; then | |
# Delete DNS CAA record | |
if [ ! -z "$DNS_SYSTEM" ]; then | |
dns_domain=$($BIN/v-list-dns-domains $user | grep $domain | cut -d' ' -f1) | |
caa_record=$($BIN/v-list-dns-records $user $domain | grep -i "letsencrypt" | cut -d' ' -f1) | |
if [ "$dns_domain" = "$domain" ]; then | |
if [ ! -z "$caa_record" ]; then | |
$BIN/v-delete-dns-record $user $domain $caa_record | |
fi | |
fi | |
fi | |
check_result $E_CONNECT "Let's Encrypt validation status $status. Details: $details" | |
fi | |
i=$((i + 1)) | |
if [ "$i" -gt 10 ]; then | |
# Delete DNS CAA record | |
if [ ! -z "$DNS_SYSTEM" ]; then | |
dns_domain=$($BIN/v-list-dns-domains $user | grep $domain | cut -d' ' -f1) | |
caa_record=$($BIN/v-list-dns-records $user $domain | grep -i "letsencrypt" | cut -d' ' -f1) | |
if [ "$dns_domain" = "$domain" ]; then | |
if [ ! -z "$caa_record" ]; then | |
$BIN/v-delete-dns-record $user $domain $caa_record | |
fi | |
fi | |
fi | |
check_result $E_CONNECT "Let's Encrypt domain validation timeout" | |
fi | |
sleep $((i*2)) | |
done | |
if [ "$validation" = 'invalid' ]; then | |
# Delete DNS CAA record | |
if [ ! -z "$DNS_SYSTEM" ]; then | |
dns_domain=$($BIN/v-list-dns-domains $user | grep $domain | cut -d' ' -f1) | |
caa_record=$($BIN/v-list-dns-records $user $domain | grep -i "letsencrypt" | cut -d' ' -f1) | |
if [ "$dns_domain" = "$domain" ]; then | |
if [ ! -z "$caa_record" ]; then | |
$BIN/v-delete-dns-record $user $domain $caa_record | |
fi | |
fi | |
fi | |
check_result $E_CONNECT "Let's Encrypt domain verification failed" | |
fi | |
done | |
# Generating new ssl certificate | |
ssl_dir=$($BIN/v-generate-ssl-cert "$domain" "info@$domain" "US" "California"\ | |
"San Francisco" "Hestia" "IT" "$aliases" |tail -n1 |awk '{print $2}') | |
# Sending CSR to finalize order / STEP 6 | |
csr=$(openssl req -in $ssl_dir/$domain.csr -outform DER |encode_base64) | |
payload='{"csr":"'$csr'"}' | |
answer=$(query_le_v2 "$finalize" "$payload" "$nonce") | |
nonce=$(echo "$answer" |grep -i nonce |cut -f2 -d \ |tr -d '\r\n') | |
status=$(echo "$answer"|grep HTTP/ |tail -n1 |cut -f 2 -d ' ') | |
certificate=$(echo "$answer"|grep 'certificate":' |cut -f4 -d '"') | |
if [[ "$status" -ne 200 ]]; then | |
[ -d "$ssl_dir" ] && rm -rf "$ssl_dir" | |
check_result $E_CONNECT "Let's Encrypt finalize bad status $status" | |
fi | |
# Downloading signed certificate / STEP 7 | |
answer=$(query_le_v2 "$certificate" "" "$nonce" "$ssl_dir/$domain.pem") | |
status=$(echo "$answer"|grep HTTP/ |tail -n1 |cut -f 2 -d ' ') | |
if [[ "$status" -ne 200 ]]; then | |
[ -d "$ssl_dir" ] && rm -rf "$ssl_dir" | |
check_result $E_NOTEXIST "Let's Encrypt downloading signed cert failed status:$status" | |
fi | |
# Splitting up downloaded pem | |
crt_end=$(grep -n END $ssl_dir/$domain.pem |head -n1 |cut -f1 -d:) | |
head -n $crt_end $ssl_dir/$domain.pem > $ssl_dir/$domain.crt | |
pem_lines=$(wc -l $ssl_dir/$domain.pem |cut -f 1 -d ' ') | |
ca_end=$(grep -n "BEGIN" $ssl_dir/$domain.pem |tail -n1 |cut -f 1 -d :) | |
ca_end=$(( pem_lines - crt_end + 1 )) | |
tail -n $ca_end $ssl_dir/$domain.pem > $ssl_dir/$domain.ca | |
# Temporary fix for double "END CERTIFICATE" | |
if [[ $(head -n 1 $ssl_dir/$domain.ca) = "-----END CERTIFICATE-----" ]]; then | |
sed -i '1,2d' $ssl_dir/$domain.ca | |
fi | |
# Rename certs for mail | |
if [ ! -z "$mail" ]; then | |
mv $ssl_dir/$domain.ca $ssl_dir/$root_domain.ca | |
mv $ssl_dir/$domain.crt $ssl_dir/$root_domain.crt | |
mv $ssl_dir/$domain.csr $ssl_dir/$root_domain.csr | |
mv $ssl_dir/$domain.key $ssl_dir/$root_domain.key | |
mv $ssl_dir/$domain.pem $ssl_dir/$root_domain.pem | |
fi | |
# Adding SSL | |
if [ -z "$mail" ]; then | |
ssl_home=$(search_objects 'web' 'LETSENCRYPT' 'yes' 'SSL_HOME') | |
ssl_enabled="$(get_object_value 'web' 'DOMAIN' "$domain" '$SSL')" | |
ssl_force="$(get_object_value 'web' 'DOMAIN' "$domain" '$SSL_FORCE')" | |
[[ "$ssl_enabled" = "yes" ]] && $BIN/v-delete-web-domain-ssl $user $domain > /dev/null 2>&1 | |
$BIN/v-add-web-domain-ssl $user $domain $ssl_dir $ssl_home | |
[[ "$ssl_force" = "yes" ]] && $BIN/v-add-web-domain-ssl-force $user $domain > /dev/null 2>&1 | |
else | |
ssl_enabled="$(get_object_value 'mail' 'DOMAIN' "$root_domain" '$SSL')" | |
[[ "$ssl_enabled" = "yes" ]] && $BIN/v-delete-mail-domain-ssl $user $root_domain > /dev/null 2>&1 | |
$BIN/v-add-mail-domain-ssl $user $root_domain $ssl_dir | |
fi | |
if [ "$?" -ne '0' ]; then | |
[ -d "$ssl_dir" ] && rm -rf "$ssl_dir" | |
touch $HESTIA/data/queue/letsencrypt.pipe | |
sed -i "/ $domain /d" $HESTIA/data/queue/letsencrypt.pipe | |
send_notice 'LETSENCRYPT' "$domain certificate installation failed" | |
check_result $? "SSL install" > /dev/null | |
fi | |
# Adding LE autorenew cronjob | |
if [ -z "$(grep v-update-lets $HESTIA/data/users/admin/cron.conf)" ]; then | |
min=$(generate_password '012345' '2') | |
hour=$(generate_password '1234567' '1') | |
cmd="sudo $BIN/v-update-letsencrypt-ssl" | |
$BIN/v-add-cron-job admin "$min" "$hour" '*' '*' '*' "$cmd" > /dev/null | |
fi | |
# Updating letsencrypt key | |
if [ -z "$mail" ]; then | |
if [ -z "$LETSENCRYPT" ]; then | |
add_object_key "web" 'DOMAIN' "$domain" 'LETSENCRYPT' 'FTP_USER' | |
add_object_key "web" 'DOMAIN' "$domain" 'LETSENCRYPT_FAIL_COUNT' 'LETSENCRYPT' | |
fi | |
update_object_value 'web' 'DOMAIN' "$domain" '$LETSENCRYPT' 'yes' | |
update_object_value 'web' 'DOMAIN' "$domain" '$LETSENCRYPT_FAIL_COUNT' "0" | |
else | |
if [ -z "$LETSENCRYPT" ]; then | |
add_object_key "mail" 'DOMAIN' "$root_domain" 'LETSENCRYPT' | |
add_object_key "mail" 'DOMAIN' "$root_domain" 'LETSENCRYPT_FAIL_COUNT' 'LETSENCRYPT' | |
fi | |
update_object_value 'mail' 'DOMAIN' "$root_domain" '$LETSENCRYPT' 'yes' | |
update_object_value 'mail' 'DOMAIN' "$root_domain" '$LETSENCRYPT_FAIL_COUNT' "0" | |
fi | |
# Remove challenge folder if exist | |
if [ ! -z "$well_known" ]; then | |
rm -fr $well_known | |
fi | |
# Remove temporary SSL folder | |
[ -d "$ssl_dir" ] && rm -rf "$ssl_dir" | |
#----------------------------------------------------------# | |
# Hestia # | |
#----------------------------------------------------------# | |
# Deleting task from queue | |
touch $HESTIA/data/queue/letsencrypt.pipe | |
sed -i "/ $domain /d" $HESTIA/data/queue/letsencrypt.pipe | |
# Notifying user | |
send_notice 'LETSENCRYPT' "$domain SSL has been installed successfully" | |
# Logging | |
log_event "$OK" "$ARGUMENTS" | |
exit |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment