|
| 1 | +#!/bin/bash |
| 2 | +# info: check letsencrypt domain |
| 3 | +# options: USER DOMAIN [ALIASES] [RESTART] [NOTIFY] |
| 4 | +# |
| 5 | +# The function check and validates domain with Let's Encrypt |
| 6 | + |
| 7 | + |
| 8 | +#----------------------------------------------------------# |
| 9 | +# Variable&Function # |
| 10 | +#----------------------------------------------------------# |
| 11 | + |
| 12 | +# Argument definition |
| 13 | +user=$1 |
| 14 | +domain=$2 |
| 15 | +aliases=$3 |
| 16 | +restart=$4 |
| 17 | +notify=$5 |
| 18 | + |
| 19 | +# LE API |
| 20 | +LE_API='https://acme-v02.api.letsencrypt.org' |
| 21 | + |
| 22 | +# Includes |
| 23 | +source $HESTIA/func/main.sh |
| 24 | +source $HESTIA/func/domain.sh |
| 25 | +source $HESTIA/conf/hestia.conf |
| 26 | + |
| 27 | +# encode base64 |
| 28 | +encode_base64() { |
| 29 | + cat |base64 |tr '+/' '-_' |tr -d '\r\n=' |
| 30 | +} |
| 31 | + |
| 32 | +# Let's Encrypt v2 curl function |
| 33 | +query_le_v2() { |
| 34 | + |
| 35 | + protected='{"nonce": "'$3'",' |
| 36 | + protected=''$protected' "url": "'$1'",' |
| 37 | + protected=''$protected' "alg": "RS256", "kid": "'$KID'"}' |
| 38 | + content="Content-Type: application/jose+json" |
| 39 | + |
| 40 | + payload_=$(echo -n "$2" |encode_base64) |
| 41 | + protected_=$(echo -n "$protected" |encode_base64) |
| 42 | + signature_=$(printf "%s" "$protected_.$payload_" |\ |
| 43 | + openssl dgst -sha256 -binary -sign $USER_DATA/ssl/user.key |\ |
| 44 | + encode_base64) |
| 45 | + |
| 46 | + post_data='{"protected":"'"$protected_"'",' |
| 47 | + post_data=$post_data'"payload":"'"$payload_"'",' |
| 48 | + post_data=$post_data'"signature":"'"$signature_"'"}' |
| 49 | + |
| 50 | + curl -s -i -d "$post_data" "$1" -H "$content" |
| 51 | +} |
| 52 | + |
| 53 | + |
| 54 | + |
| 55 | +#----------------------------------------------------------# |
| 56 | +# Verifications # |
| 57 | +#----------------------------------------------------------# |
| 58 | + |
| 59 | +check_args '2' "$#" 'USER DOMAIN [ALIASES] [RESTART] [NOTIFY]' |
| 60 | +is_format_valid 'user' 'domain' |
| 61 | +is_system_enabled "$MAIL_SYSTEM" 'MAIL_SYSTEM' |
| 62 | +is_object_valid 'user' 'USER' "$user" |
| 63 | +is_object_unsuspended 'user' 'USER' "$user" |
| 64 | +is_object_valid 'mail' 'DOMAIN' "$domain" |
| 65 | +is_object_unsuspended 'mail' 'DOMAIN' "$domain" |
| 66 | +is_object_value_empty 'mail' 'DOMAIN' "$domain" '$SSL' |
| 67 | + |
| 68 | + |
| 69 | +#----------------------------------------------------------# |
| 70 | +# Action # |
| 71 | +#----------------------------------------------------------# |
| 72 | +# Parsing domain data |
| 73 | +get_domain_values 'web' |
| 74 | + |
| 75 | +# Registering LetsEncrypt user account |
| 76 | +$BIN/v-add-letsencrypt-user $user |
| 77 | +if [ "$?" -ne 0 ]; then |
| 78 | + touch $HESTIA/data/queue/letsencrypt.pipe |
| 79 | + sed -i "/ $domain /d" $HESTIA/data/queue/letsencrypt.pipe |
| 80 | + send_notice "LETSENCRYPT" "Account registration failed" |
| 81 | + check_result $E_CONNECT "LE account registration" > /dev/null |
| 82 | +fi |
| 83 | + |
| 84 | +# Parsing LetsEncrypt account data |
| 85 | +source $USER_DATA/ssl/le.conf |
| 86 | + |
| 87 | +# Checking wildcard alias |
| 88 | +if [ "$aliases" = "*.$domain" ]; then |
| 89 | + wildcard='yes' |
| 90 | + proto="dns-01" |
| 91 | + if [ ! -e "$HESTIA/data/users/$user/dns/$domain.conf" ]; then |
| 92 | + check_result $E_NOTEXIST "DNS domain $domain doesn't exist" |
| 93 | + fi |
| 94 | +else |
| 95 | + proto="http-01" |
| 96 | +fi |
| 97 | + |
| 98 | +# Requesting nonce / STEP 1 |
| 99 | +answer=$(curl -s -I "$LE_API/directory") |
| 100 | +nonce=$(echo "$answer" |grep Nonce |cut -f2 -d \ |tr -d '\r\n') |
| 101 | +status=$(echo "$answer"|grep HTTP/1.1 |tail -n1 |cut -f 2 -d ' ') |
| 102 | +if [[ "$status" -ne 200 ]]; then |
| 103 | + check_result $E_CONNECT "Let's Encrypt nonce request status $status" |
| 104 | +fi |
| 105 | + |
| 106 | +# Placing new order / STEP 2 |
| 107 | +url="$LE_API/acme/new-order" |
| 108 | +payload='{"identifiers":[' |
| 109 | +for identifier in $(echo $domain,$aliases |tr ',' '\n' |sort -u); do |
| 110 | + payload=$payload'{"type":"dns","value":"'$identifier'"},' |
| 111 | +done |
| 112 | +payload=$(echo "$payload"|sed "s/,$//") |
| 113 | +payload=$payload']}' |
| 114 | +answer=$(query_le_v2 "$url" "$payload" "$nonce") |
| 115 | +nonce=$(echo "$answer" |grep Nonce |cut -f2 -d \ |tr -d '\r\n') |
| 116 | +authz=$(echo "$answer" |grep "acme/authz" |cut -f2 -d '"') |
| 117 | +finalize=$(echo "$answer" |grep 'finalize":' |cut -f4 -d '"') |
| 118 | +status=$(echo "$answer" |grep HTTP/1.1 |tail -n1 |cut -f2 -d ' ') |
| 119 | +if [[ "$status" -ne 201 ]]; then |
| 120 | + check_result $E_CONNECT "Let's Encrypt new auth status $status" |
| 121 | +fi |
| 122 | + |
| 123 | +# Requesting authorization token / STEP 3 |
| 124 | +for auth in $authz; do |
| 125 | + payload='' |
| 126 | + answer=$(query_le_v2 "$auth" "$payload" "$nonce") |
| 127 | + url=$(echo "$answer" |grep -A3 $proto |grep url |cut -f 4 -d \") |
| 128 | + token=$(echo "$answer" |grep -A3 $proto |grep token |cut -f 4 -d \") |
| 129 | + nonce=$(echo "$answer" |grep Nonce |cut -f2 -d \ |tr -d '\r\n') |
| 130 | + status=$(echo "$answer"|grep HTTP/1.1 |tail -n1 |cut -f 2 -d ' ') |
| 131 | + if [[ "$status" -ne 200 ]]; then |
| 132 | + check_result $E_CONNECT "Let's Encrypt acme/authz bad status $status" |
| 133 | + fi |
| 134 | + |
| 135 | + # Accepting challenge / STEP 4 |
| 136 | + if [ "$wildcard" = 'yes' ]; then |
| 137 | + record=$(printf "%s" "$token.$THUMB" |\ |
| 138 | + openssl dgst -sha256 -binary |encode_base64) |
| 139 | + old_records=$($BIN/v-list-dns-records $user $domain plain|grep 'TXT') |
| 140 | + old_records=$(echo "$old_records" |grep _acme-challenge |cut -f 1) |
| 141 | + for old_record in $old_records; do |
| 142 | + $BIN/v-delete-dns-record $user $domain $old_record |
| 143 | + done |
| 144 | + $BIN/v-add-dns-record $user $domain "_acme-challenge" "TXT" $record |
| 145 | + check_result $? "DNS _acme-challenge record wasn't created" |
| 146 | + else |
| 147 | + if [ "$WEB_SYSTEM" = 'nginx' ] || [ ! -z "$PROXY_SYSTEM" ]; then |
| 148 | + conf="$HOMEDIR/$user/conf/web/$domain/nginx.conf_letsencrypt" |
| 149 | + sconf="$HOMEDIR/$user/conf/web/$domain/nginx.ssl.conf_letsencrypt" |
| 150 | + if [ ! -e "$conf" ]; then |
| 151 | + echo 'location ~ "^/\.well-known/acme-challenge/(.*)$" {' \ |
| 152 | + > $conf |
| 153 | + echo ' default_type text/plain;' >> $conf |
| 154 | + echo ' return 200 "$1.'$THUMB'";' >> $conf |
| 155 | + echo '}' >> $conf |
| 156 | + fi |
| 157 | + if [ ! -e "$sconf" ]; then |
| 158 | + ln -s "$conf" "$sconf" |
| 159 | + fi |
| 160 | + $BIN/v-restart-proxy |
| 161 | + check_result $? "Proxy restart failed" > /dev/null |
| 162 | + |
| 163 | + else |
| 164 | + well_known="$HOMEDIR/$user/web/$rdomain/public_html/.well-known" |
| 165 | + acme_challenge="$well_known/acme-challenge" |
| 166 | + mkdir -p $acme_challenge |
| 167 | + echo "$token.$THUMB" > $acme_challenge/$token |
| 168 | + chown -R $user:$user $well_known |
| 169 | + fi |
| 170 | + $BIN/v-restart-web |
| 171 | + check_result $? "Web restart failed" > /dev/null |
| 172 | + fi |
| 173 | + |
| 174 | + # Requesting ACME validation / STEP 5 |
| 175 | + validation_check=$(echo "$answer" |grep '"valid"') |
| 176 | + if [[ ! -z "$validation_check" ]]; then |
| 177 | + validation='valid' |
| 178 | + else |
| 179 | + validation='pending' |
| 180 | + fi |
| 181 | + |
| 182 | + # Doing pol check on status |
| 183 | + i=1 |
| 184 | + while [ "$validation" = 'pending' ]; do |
| 185 | + payload='{}' |
| 186 | + answer=$(query_le_v2 "$url" "$payload" "$nonce") |
| 187 | + validation=$(echo "$answer"|grep -A1 $proto |tail -n1|cut -f4 -d \") |
| 188 | + nonce=$(echo "$answer" |grep Nonce |cut -f2 -d \ |tr -d '\r\n') |
| 189 | + status=$(echo "$answer"|grep HTTP/1.1 |tail -n1 |cut -f 2 -d ' ') |
| 190 | + if [[ "$status" -ne 200 ]]; then |
| 191 | + check_result $E_CONNECT "Let's Encrypt validation status $status" |
| 192 | + fi |
| 193 | + |
| 194 | + i=$((i + 1)) |
| 195 | + if [ "$i" -gt 10 ]; then |
| 196 | + check_result $E_CONNECT "Let's Encrypt domain validation timeout" |
| 197 | + fi |
| 198 | + sleep 1 |
| 199 | + done |
| 200 | + if [ "$validation" = 'invalid' ]; then |
| 201 | + check_result $E_CONNECT "Let's Encrypt domain verification failed" |
| 202 | + fi |
| 203 | +done |
| 204 | + |
| 205 | +# Generating new ssl certificate |
| 206 | +ssl_dir=$($BIN/v-generate-ssl-cert "$domain" "info@$domain" "US" "California"\ |
| 207 | + "San Francisco" "Hestia" "IT" "$aliases" |tail -n1 |awk '{print $2}') |
| 208 | + |
| 209 | +# Sending CSR to finalize order / STEP 6 |
| 210 | +csr=$(openssl req -in $ssl_dir/$domain.csr -outform DER |encode_base64) |
| 211 | +payload='{"csr":"'$csr'"}' |
| 212 | +answer=$(query_le_v2 "$finalize" "$payload" "$nonce") |
| 213 | +nonce=$(echo "$answer" |grep Nonce |cut -f2 -d \ |tr -d '\r\n') |
| 214 | +status=$(echo "$answer"|grep HTTP/1.1 |tail -n1 |cut -f 2 -d ' ') |
| 215 | +certificate=$(echo "$answer"|grep 'certificate":' |cut -f4 -d '"') |
| 216 | +if [[ "$status" -ne 200 ]]; then |
| 217 | + check_result $E_CONNECT "Let's Encrypt finalize bad status $status" |
| 218 | +fi |
| 219 | + |
| 220 | +# Downloading signed certificate / STEP 7 |
| 221 | +curl -s "$certificate" -o $ssl_dir/$domain.pem |
| 222 | + |
| 223 | +# Splitting up downloaded pem |
| 224 | +crt_end=$(grep -n END $ssl_dir/$domain.pem |head -n1 |cut -f1 -d:) |
| 225 | +head -n $crt_end $ssl_dir/$domain.pem > $ssl_dir/$domain.crt |
| 226 | + |
| 227 | +pem_lines=$(wc -l $ssl_dir/$domain.pem |cut -f 1 -d ' ') |
| 228 | +ca_end=$(grep -n "BEGIN" $ssl_dir/$domain.pem |tail -n1 |cut -f 1 -d :) |
| 229 | +ca_end=$(( pem_lines - crt_end + 1 )) |
| 230 | +tail -n $ca_end $ssl_dir/$domain.pem > $ssl_dir/$domain.ca |
| 231 | + |
| 232 | +# Temporary fix for double "END CERTIFICATE" |
| 233 | +if [[ $(head -n 1 $ssl_dir/$domain.ca) = "-----END CERTIFICATE-----" ]]; then |
| 234 | + sed -i '1,2d' $ssl_dir/$domain.ca |
| 235 | +fi |
| 236 | + |
| 237 | +# Adding SSL |
| 238 | +$BIN/v-delete-mail-domain-ssl $user $domain >/dev/null 2>&1 |
| 239 | +$BIN/v-add-mail-domain-ssl $user $domain $ssl_dir |
| 240 | + |
| 241 | +if [ "$?" -ne '0' ]; then |
| 242 | + touch $HESTIA/data/queue/letsencrypt.pipe |
| 243 | + sed -i "/ $domain /d" $HESTIA/data/queue/letsencrypt.pipe |
| 244 | + send_notice 'LETSENCRYPT' "$domain certificate installation failed" |
| 245 | + check_result $? "SSL install" > /dev/null |
| 246 | +fi |
| 247 | + |
| 248 | +# Adding LE autorenew cronjob |
| 249 | +if [ -z "$(grep v-update-lets $HESTIA/data/users/admin/cron.conf)" ]; then |
| 250 | + min=$(generate_password '012345' '2') |
| 251 | + hour=$(generate_password '1234567' '1') |
| 252 | + cmd="sudo $BIN/v-update-letsencrypt-ssl" |
| 253 | + $BIN/v-add-cron-job admin "$min" "$hour" '*' '*' '*' "$cmd" > /dev/null |
| 254 | +fi |
| 255 | + |
| 256 | +# Updating letsencrypt key |
| 257 | +if [ -z "$LETSENCRYPT" ]; then |
| 258 | + add_object_key "mail" 'DOMAIN' "$domain" 'LETSENCRYPT' 'SUSPENDED' |
| 259 | +fi |
| 260 | + |
| 261 | +update_object_value 'mail' 'DOMAIN' "$domain" 'LETSENCRYPT' 'yes' |
| 262 | + |
| 263 | +#----------------------------------------------------------# |
| 264 | +# Hestia # |
| 265 | +#----------------------------------------------------------# |
| 266 | + |
| 267 | +# Deleting task from queue |
| 268 | +touch $HESTIA/data/queue/letsencrypt.pipe |
| 269 | +sed -i "/ $domain /d" $HESTIA/data/queue/letsencrypt.pipe |
| 270 | + |
| 271 | +# Notifying user |
| 272 | +send_notice 'LETSENCRYPT' "$domain SSL has been installed successfully" |
| 273 | + |
| 274 | +# Logging |
| 275 | +log_event "$OK" "$ARGUMENTS" |
| 276 | + |
| 277 | +exit |
0 commit comments