Skip to content

Commit 5db91f9

Browse files
committed
Let's Encrypt v2 with wildcard support - f8b4d42b740752e17944370440f37d3105f4b21f
1 parent 4220605 commit 5db91f9

File tree

5 files changed

+267
-486
lines changed

5 files changed

+267
-486
lines changed

bin/v-add-letsencrypt-domain

Lines changed: 184 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
#!/bin/bash
2-
# info: adding letsencrypt ssl cetificate for domain
3-
# options: USER DOMAIN [ALIASES] [RESTART] [NOTIFY]
2+
# info: check letsencrypt domain
3+
# options: USER DOMAIN [ALIASES]
44
#
5-
# The function turns on SSL support for a domain. Parameter ssl_dir is a path
6-
# to directory where 2 or 3 ssl files can be found. Certificate file
7-
# domain.tld.crt and its key domain.tld.key are mandatory. Certificate
8-
# authority domain.tld.ca file is optional. If home directory parameter
9-
# (ssl_home) is not set, https domain uses public_shtml as separate
10-
# documentroot directory.
5+
# The function check and validates domain with Let's Encrypt
116

127

138
#----------------------------------------------------------#
@@ -18,39 +13,67 @@
1813
user=$1
1914
domain=$2
2015
aliases=$3
21-
restart=$4
22-
notify=$5
16+
17+
# LE API
18+
API='https://acme-v02.api.letsencrypt.org'
2319

2420
# Includes
2521
source $HESTIA/func/main.sh
2622
source $HESTIA/func/domain.sh
2723
source $HESTIA/conf/hestia.conf
2824

29-
# Additional argument formatting
30-
format_domain_idn
25+
# encode base64
26+
encode_base64() {
27+
cat |base64 |tr '+/' '-_' |tr -d '\r\n='
28+
}
29+
30+
# Let's Encrypt v2 curl function
31+
query_le_v2() {
32+
33+
protected='{"nonce": "'$3'",'
34+
protected=''$protected' "url": "'$1'",'
35+
protected=''$protected' "alg": "RS256", "kid": "'$KID'"}'
36+
content="Content-Type: application/jose+json"
37+
38+
payload_=$(echo -n "$2" |encode_base64)
39+
protected_=$(echo -n "$protected" |encode_base64)
40+
signature_=$(printf "%s" "$protected_.$payload_" |\
41+
openssl dgst -sha256 -binary -sign $USER_DATA/ssl/user.key |\
42+
encode_base64)
43+
44+
post_data='{"protected":"'"$protected_"'",'
45+
post_data=$post_data'"payload":"'"$payload_"'",'
46+
post_data=$post_data'"signature":"'"$signature_"'"}'
47+
48+
curl -s -i -d "$post_data" "$1" -H "$content"
49+
}
50+
3151

3252

3353
#----------------------------------------------------------#
3454
# Verifications #
3555
#----------------------------------------------------------#
3656

37-
check_args '2' "$#" 'USER DOMAIN [ALIASES] [RESTART] [NOTIFY]'
38-
is_format_valid 'user' 'domain'
57+
check_args '2' "$#" 'USER DOMAIN [ALIASES]'
58+
is_format_valid 'user' 'domain' 'aliases'
3959
is_system_enabled "$WEB_SYSTEM" 'WEB_SYSTEM'
40-
is_system_enabled "$WEB_SSL" 'SSL_SUPPORT'
4160
is_object_valid 'user' 'USER' "$user"
4261
is_object_unsuspended 'user' 'USER' "$user"
4362
is_object_valid 'web' 'DOMAIN' "$domain"
4463
is_object_unsuspended 'web' 'DOMAIN' "$domain"
64+
get_domain_values 'web'
65+
for alias in $(echo "$aliases" |tr ',' '\n' |sort -u); do
66+
check_alias="$(echo $ALIAS |tr ',' '\n' |grep ^$alias$)"
67+
if [ -z "$check_alias" ]; then
68+
check_result $E_NOTEXIST "domain alias $alias doesn't exist"
69+
fi
70+
done
4571

4672

4773
#----------------------------------------------------------#
4874
# Action #
4975
#----------------------------------------------------------#
5076

51-
# Parsing domain data
52-
get_domain_values 'web'
53-
5477
# Registering LetsEncrypt user account
5578
$BIN/v-add-letsencrypt-user $user
5679
if [ "$?" -ne 0 ]; then
@@ -62,54 +85,153 @@ fi
6285

6386
# Parsing LetsEncrypt account data
6487
source $USER_DATA/ssl/le.conf
65-
email=$EMAIL
66-
67-
# Validating domain and aliases
68-
i=1
69-
for alias in $(echo $domain,$aliases |tr ',' '\n' |sort -u); do
70-
$BIN/v-check-letsencrypt-domain $user $alias
71-
if [ "$?" -ne 0 ]; then
72-
touch $HESTIA/data/queue/letsencrypt.pipe
73-
sed -i "/ $domain /d" $HESTIA/data/queue/letsencrypt.pipe
74-
send_notice "LETSENCRYPT" "$alias validation failed"
75-
check_result $E_INVALID "LE domain validation" >/dev/null
88+
89+
# Checking wildcard alias
90+
if [ "$aliases" = "*.$domain" ]; then
91+
wildcard='yes'
92+
proto="dns-01"
93+
if [ ! -e "$HESTIA/data/users/$user/dns/$domain.conf" ]; then
94+
check_result $E_NOTEXIST "DNS domain $domain doesn't exist"
95+
fi
96+
else
97+
proto="http-01"
98+
fi
99+
100+
# Requesting nonce / STEP 1
101+
answer=$(curl -s -I "$API/directory")
102+
nonce=$(echo "$answer" |grep Nonce |cut -f2 -d \ |tr -d '\r\n')
103+
status=$(echo "$answer"|grep HTTP/1.1 |tail -n1 |cut -f 2 -d ' ')
104+
if [[ "$status" -ne 200 ]]; then
105+
check_result $E_CONNECT "Let's Encrypt nonce request status $status"
106+
fi
107+
108+
# Placing new order / STEP 2
109+
url="$API/acme/new-order"
110+
payload='{"identifiers":['
111+
for identifier in $(echo $domain,$aliases |tr ',' '\n' |sort -u); do
112+
payload=$payload'{"type":"dns","value":"'$identifier'"},'
113+
done
114+
payload=$(echo "$payload"|sed "s/,$//")
115+
payload=$payload']}'
116+
answer=$(query_le_v2 "$url" "$payload" "$nonce")
117+
nonce=$(echo "$answer" |grep Nonce |cut -f2 -d \ |tr -d '\r\n')
118+
authz=$(echo "$answer" |grep "acme/authz" |cut -f2 -d '"')
119+
finalize=$(echo "$answer" |grep 'finalize":' |cut -f4 -d '"')
120+
status=$(echo "$answer" |grep HTTP/1.1 |tail -n1 |cut -f2 -d ' ')
121+
if [[ "$status" -ne 201 ]]; then
122+
check_result $E_CONNECT "Let's Encrypt new auth status $status"
123+
fi
124+
125+
# Requesting authorization token / STEP 3
126+
for auth in $authz; do
127+
payload=''
128+
answer=$(query_le_v2 "$auth" "$payload" "$nonce")
129+
url=$(echo "$answer" |grep -A3 $proto |grep url |cut -f 4 -d \")
130+
token=$(echo "$answer" |grep -A3 $proto |grep token |cut -f 4 -d \")
131+
nonce=$(echo "$answer" |grep Nonce |cut -f2 -d \ |tr -d '\r\n')
132+
status=$(echo "$answer"|grep HTTP/1.1 |tail -n1 |cut -f 2 -d ' ')
133+
if [[ "$status" -ne 200 ]]; then
134+
check_result $E_CONNECT "Let's Encrypt acme/authz bad status $status"
76135
fi
77136

78-
# Checking LE limits per account
79-
if [ "$i" -gt 100 ]; then
80-
touch $HESTIA/data/queue/letsencrypt.pipe
81-
sed -i "/ $domain /d" $HESTIA/data/queue/letsencrypt.pipe
82-
send_notice 'LETSENCRYPT' 'Limit of domains per account is reached'
83-
check_result $E_LIMIT "LE can't sign more than 100 domains"
137+
# Accepting challenge / STEP 4
138+
if [ "$wildcard" = 'yes' ]; then
139+
record=$(printf "%s" "$token.$THUMB" |\
140+
openssl dgst -sha256 -binary |encode_base64)
141+
old_records=$($BIN/v-list-dns-records $user $domain plain|grep 'TXT')
142+
old_records=$(echo "$old_records" |grep _acme-challenge |cut -f 1)
143+
for old_record in $old_records; do
144+
$BIN/v-delete-dns-record $user $domain $old_record
145+
done
146+
$BIN/v-add-dns-record $user $domain "_acme-challenge" "TXT" $record
147+
check_result $? "DNS _acme-challenge record wasn't created"
148+
else
149+
if [ "$WEB_SYSTEM" = 'nginx' ] || [ ! -z "$PROXY_SYSTEM" ]; then
150+
conf="$HOMEDIR/$user/conf/web/nginx.$domain.conf_letsencrypt"
151+
sconf="$HOMEDIR/$user/conf/web/snginx.$domain.conf_letsencrypt"
152+
if [ ! -e "$conf" ]; then
153+
echo 'location ~ "^/\.well-known/acme-challenge/(.*)$" {' \
154+
> $conf
155+
echo ' default_type text/plain;' >> $conf
156+
echo ' return 200 "$1.'$THUMB'";' >> $conf
157+
echo '}' >> $conf
158+
fi
159+
if [ ! -e "$sconf" ]; then
160+
ln -s "$conf" "$sconf"
161+
fi
162+
$BIN/v-restart-proxy
163+
check_result $? "Proxy restart failed" >/dev/null
164+
165+
else
166+
well_known="$HOMEDIR/$user/web/$rdomain/public_html/.well-known"
167+
acme_challenge="$well_known/acme-challenge"
168+
mkdir -p $acme_challenge
169+
echo "$token.$THUMB" > $acme_challenge/$token
170+
chown -R $user:$user $well_known
171+
fi
172+
$BIN/v-restart-web
173+
check_result $? "Web restart failed" >/dev/null
174+
fi
175+
176+
# Requesting ACME validation / STEP 5
177+
validation_check=$(echo "$answer" |grep '"valid"')
178+
if [[ ! -z "$validation_check" ]]; then
179+
validation='valid'
180+
else
181+
validation='pending'
182+
fi
183+
184+
# Doing pol check on status
185+
i=1
186+
while [ "$validation" = 'pending' ]; do
187+
payload='{}'
188+
answer=$(query_le_v2 "$url" "$payload" "$nonce")
189+
validation=$(echo "$answer"|grep -A1 $proto |tail -n1|cut -f4 -d \")
190+
nonce=$(echo "$answer" |grep Nonce |cut -f2 -d \ |tr -d '\r\n')
191+
status=$(echo "$answer"|grep HTTP/1.1 |tail -n1 |cut -f 2 -d ' ')
192+
if [[ "$status" -ne 200 ]]; then
193+
check_result $E_CONNECT "Let's Encrypt vvalidation status $status"
194+
fi
195+
196+
i=$((i + 1))
197+
if [ "$i" -gt 10 ]; then
198+
check_result $E_CONNECT "Let's Encrypt domain validation timeout"
199+
fi
200+
sleep 1
201+
done
202+
if [ "$validation" = 'invalid' ]; then
203+
check_result $E_CONNECT "Let's Encrypt domain verification failed"
84204
fi
85-
i=$((i++))
86205
done
87206

88-
# Generating CSR
89-
ssl_dir=$($BIN/v-generate-ssl-cert "$domain" "$email" "US" "California" \
207+
208+
# Generating new ssl certificate
209+
ssl_dir=$($BIN/v-generate-ssl-cert "$domain" "info@$domain" "US" "California"\
90210
"San Francisco" "Hestia" "IT" "$aliases" |tail -n1 |awk '{print $2}')
91211

92-
# Signing CSR
93-
crt=$($BIN/v-sign-letsencrypt-csr $user $domain $ssl_dir)
94-
if [ "$?" -ne 0 ]; then
95-
touch $HESTIA/data/queue/letsencrypt.pipe
96-
sed -i "/ $domain /d" $HESTIA/data/queue/letsencrypt.pipe
97-
send_notice "LETSENCRYPT" "$alias validation failed"
98-
check_result "$E_INVALID" "LE $domain validation"
99-
fi
100-
echo "$crt" > $ssl_dir/$domain.crt
101-
102-
# Dowloading CA certificate
103-
le_certs='https://letsencrypt.org/certs'
104-
x1='lets-encrypt-x1-cross-signed.pem.txt'
105-
x3='lets-encrypt-x3-cross-signed.pem.txt'
106-
issuer=$(openssl x509 -text -in $ssl_dir/$domain.crt |grep "Issuer:")
107-
if [ -z "$(echo $issuer|grep X3)" ]; then
108-
curl -s $le_certs/$x1 > $ssl_dir/$domain.ca
109-
else
110-
curl -s $le_certs/$x3 > $ssl_dir/$domain.ca
212+
# Sedning CSR to finalize order / STEP 6
213+
csr=$(openssl req -in $ssl_dir/$domain.csr -outform DER |encode_base64)
214+
payload='{"csr":"'$csr'"}'
215+
answer=$(query_le_v2 "$finalize" "$payload" "$nonce")
216+
nonce=$(echo "$answer" |grep Nonce |cut -f2 -d \ |tr -d '\r\n')
217+
status=$(echo "$answer"|grep HTTP/1.1 |tail -n1 |cut -f 2 -d ' ')
218+
certificate=$(echo "$answer"|grep 'certificate":' |cut -f4 -d '"')
219+
if [[ "$status" -ne 200 ]]; then
220+
check_result $E_CONNECT "Let's Encrypt finalize bad status $status"
111221
fi
112222

223+
# Downloading signed certificate / STEP 7
224+
curl -s "$certificate" -o $ssl_dir/$domain.pem
225+
226+
# Splitting up downloaded pem
227+
crt_end=$(grep -n END $ssl_dir/$domain.pem |head -n1 |cut -f1 -d:)
228+
head -n $crt_end $ssl_dir/$domain.pem > $ssl_dir/$domain.crt
229+
230+
pem_lines=$(wc -l $ssl_dir/$domain.pem |cut -f 1 -d ' ')
231+
ca_end=$(grep -n "BEGIN" $ssl_dir/$domain.pem |tail -n1 |cut -f 1 -d :)
232+
ca_end=$(( pem_lines - crt_end + 1 ))
233+
tail -n $ca_end $ssl_dir/$domain.pem > $ssl_dir/$domain.ca
234+
113235
# Adding SSL
114236
ssl_home=$(search_objects 'web' 'LETSENCRYPT' 'yes' 'SSL_HOME')
115237
$BIN/v-delete-web-domain-ssl $user $domain >/dev/null 2>&1
@@ -137,23 +259,18 @@ update_object_value 'web' 'DOMAIN' "$domain" '$LETSENCRYPT' 'yes'
137259

138260

139261
#----------------------------------------------------------#
140-
# Hestia #
262+
# Hestia #
141263
#----------------------------------------------------------#
142264

143-
# Restarting web
144-
$BIN/v-restart-web $restart
145-
if [ "$?" -ne 0 ]; then
146-
send_notice 'LETSENCRYPT' "web server needs to be restarted manually"
147-
fi
265+
# Deleteing task from queue
266+
touch $HESTIA/data/queue/letsencrypt.pipe
267+
sed -i "/ $domain /d" $HESTIA/data/queue/letsencrypt.pipe
148268

149269
# Notifying user
150270
send_notice 'LETSENCRYPT' "$domain SSL has been installed successfully"
151271

152-
# Deleteing task from queue
153-
touch $HESTIA/data/queue/letsencrypt.pipe
154-
sed -i "/ $domain /d" $HESTIA/data/queue/letsencrypt.pipe
155272

156273
# Logging
157274
log_event "$OK" "$ARGUMENTS"
158275

159-
exit
276+
exit

0 commit comments

Comments
 (0)