]> git.proxmox.com Git - mirror_acme.sh.git/blob - le.sh
minor
[mirror_acme.sh.git] / le.sh
1 #!/usr/bin/env bash
2 VER=1.1.8
3 PROJECT="https://github.com/Neilpang/le"
4
5 DEFAULT_CA="https://acme-v01.api.letsencrypt.org"
6 DEFAULT_AGREEMENT="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
7
8 STAGE_CA="https://acme-staging.api.letsencrypt.org"
9
10 VTYPE_HTTP="http-01"
11 VTYPE_DNS="dns-01"
12
13 if [ -z "$AGREEMENT" ] ; then
14 AGREEMENT="$DEFAULT_AGREEMENT"
15 fi
16
17 _debug() {
18
19 if [ -z "$DEBUG" ] ; then
20 return
21 fi
22
23 if [ -z "$2" ] ; then
24 echo $1
25 else
26 echo "$1"="$2"
27 fi
28 }
29
30 _info() {
31 if [ -z "$2" ] ; then
32 echo "$1"
33 else
34 echo "$1"="$2"
35 fi
36 }
37
38 _err() {
39 if [ -z "$2" ] ; then
40 echo "$1" >&2
41 else
42 echo "$1"="$2" >&2
43 fi
44 return 1
45 }
46
47 _h2b() {
48 hex=$(cat)
49 i=1
50 j=2
51 while [ '1' ] ; do
52 h=$(printf $hex | cut -c $i-$j)
53 if [ -z "$h" ] ; then
54 break;
55 fi
56 printf "\x$h"
57 let "i+=2"
58 let "j+=2"
59 done
60 }
61
62 _base64() {
63 openssl base64 -e | tr -d '\n'
64 }
65
66 #domain [2048]
67 createAccountKey() {
68 _info "Creating account key"
69 if [ -z "$1" ] ; then
70 echo Usage: createAccountKey account-domain [2048]
71 return
72 fi
73
74 account=$1
75 length=$2
76
77 if [[ "$length" == "ec-"* ]] ; then
78 length=2048
79 fi
80
81 if [ -z "$2" ] ; then
82 _info "Use default length 2048"
83 length=2048
84 fi
85 _initpath
86
87 if [ -f "$ACCOUNT_KEY_PATH" ] ; then
88 _info "Account key exists, skip"
89 return
90 else
91 #generate account key
92 openssl genrsa $length > "$ACCOUNT_KEY_PATH"
93 fi
94
95 }
96
97 #domain length
98 createDomainKey() {
99 _info "Creating domain key"
100 if [ -z "$1" ] ; then
101 echo Usage: createDomainKey domain [2048]
102 return
103 fi
104
105 domain=$1
106 length=$2
107 isec=""
108 if [[ "$length" == "ec-"* ]] ; then
109 isec="1"
110 length=$(printf $length | cut -d '-' -f 2-100)
111 eccname="$length"
112 fi
113
114 if [ -z "$length" ] ; then
115 if [ "$isec" ] ; then
116 length=256
117 else
118 length=2048
119 fi
120 fi
121 _info "Use length $length"
122
123 if [ "$isec" ] ; then
124 if [ "$length" == "256" ] ; then
125 eccname="prime256v1"
126 fi
127 if [ "$length" == "384" ] ; then
128 eccname="secp384r1"
129 fi
130 if [ "$length" == "521" ] ; then
131 eccname="secp521r1"
132 fi
133 _info "Using ec name: $eccname"
134 fi
135
136 _initpath $domain
137
138 if [ ! -f "$CERT_KEY_PATH" ] || ( [ "$FORCE" ] && ! [ "$IS_RENEW" ] ); then
139 #generate account key
140 if [ "$isec" ] ; then
141 openssl ecparam -name $eccname -genkey 2>/dev/null > "$CERT_KEY_PATH"
142 else
143 openssl genrsa $length 2>/dev/null > "$CERT_KEY_PATH"
144 fi
145 else
146 if [ "$IS_RENEW" ] ; then
147 _info "Domain key exists, skip"
148 return 0
149 else
150 _err "Domain key exists, do you want to overwrite the key?"
151 _err "Set FORCE=1, and try again."
152 return 1
153 fi
154 fi
155
156 }
157
158 # domain domainlist
159 createCSR() {
160 _info "Creating csr"
161 if [ -z "$1" ] ; then
162 echo Usage: $0 domain [domainlist]
163 return
164 fi
165 domain=$1
166 _initpath $domain
167
168 domainlist=$2
169
170 if [ -f "$CSR_PATH" ] && [ "$IS_RENEW" ] && ! [ "$FORCE" ]; then
171 _info "CSR exists, skip"
172 return
173 fi
174
175 if [ -z "$domainlist" ] ; then
176 #single domain
177 _info "Single domain" $domain
178 openssl req -new -sha256 -key "$CERT_KEY_PATH" -subj "/CN=$domain" > "$CSR_PATH"
179 else
180 alt="DNS:$(echo $domainlist | sed "s/,/,DNS:/g")"
181 #multi
182 _info "Multi domain" "$alt"
183 printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\n[SAN]\nsubjectAltName=$alt" > "$DOMAIN_SSL_CONF"
184 openssl req -new -sha256 -key "$CERT_KEY_PATH" -subj "/CN=$domain" -reqexts SAN -config "$DOMAIN_SSL_CONF" -out "$CSR_PATH"
185 fi
186
187 }
188
189 _b64() {
190 __n=$(cat)
191 echo $__n | tr '/+' '_-' | tr -d '= '
192 }
193
194 _time2str() {
195 #BSD
196 if date -u -d@$1 2>/dev/null ; then
197 return
198 fi
199
200 #Linux
201 if date -u -r $1 2>/dev/null ; then
202 return
203 fi
204
205 }
206
207 _send_signed_request() {
208 url=$1
209 payload=$2
210 needbase64=$3
211
212 _debug url $url
213 _debug payload "$payload"
214
215 CURL_HEADER="$LE_WORKING_DIR/curl.header"
216 dp="$LE_WORKING_DIR/curl.dump"
217 CURL="curl --silent --dump-header $CURL_HEADER "
218 if [ "$DEBUG" ] ; then
219 CURL="$CURL --trace-ascii $dp "
220 fi
221 payload64=$(echo -n $payload | _base64 | _b64)
222 _debug payload64 $payload64
223
224 nonceurl="$API/directory"
225 nonce="$($CURL -I $nonceurl | grep -o "^Replay-Nonce:.*$" | tr -d "\r\n" | cut -d ' ' -f 2)"
226
227 _debug nonce "$nonce"
228
229 protected="$(printf "$HEADERPLACE" | sed "s/NONCE/$nonce/" )"
230 _debug protected "$protected"
231
232 protected64="$(printf "$protected" | _base64 | _b64)"
233 _debug protected64 "$protected64"
234
235 sig=$(echo -n "$protected64.$payload64" | openssl dgst -sha256 -sign $ACCOUNT_KEY_PATH | _base64 | _b64)
236 _debug sig "$sig"
237
238 body="{\"header\": $HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}"
239 _debug body "$body"
240
241 if [ "$needbase64" ] ; then
242 response="$($CURL -X POST --data "$body" $url | _base64)"
243 else
244 response="$($CURL -X POST --data "$body" $url)"
245 fi
246
247 responseHeaders="$(cat $CURL_HEADER)"
248
249 _debug responseHeaders "$responseHeaders"
250 _debug response "$response"
251 code="$(grep ^HTTP $CURL_HEADER | tail -1 | cut -d " " -f 2 | tr -d "\r\n" )"
252 _debug code $code
253
254 }
255
256 _get() {
257 url="$1"
258 _debug url $url
259 response="$(curl --silent $url)"
260 ret=$?
261 _debug response "$response"
262 code="$(echo $response | grep -o '"status":[0-9]\+' | cut -d : -f 2)"
263 _debug code $code
264 return $ret
265 }
266
267 #setopt "file" "opt" "=" "value" [";"]
268 _setopt() {
269 __conf="$1"
270 __opt="$2"
271 __sep="$3"
272 __val="$4"
273 __end="$5"
274 if [ -z "$__opt" ] ; then
275 echo usage: _setopt '"file" "opt" "=" "value" [";"]'
276 return
277 fi
278 if [ ! -f "$__conf" ] ; then
279 touch "$__conf"
280 fi
281
282 if grep -H -n "^$__opt$__sep" "$__conf" > /dev/null ; then
283 _debug OK
284 if [[ "$__val" == *"&"* ]] ; then
285 __val="$(echo $__val | sed 's/&/\\&/g')"
286 fi
287 text="$(cat $__conf)"
288 printf "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" > "$__conf"
289
290 elif grep -H -n "^#$__opt$__sep" "$__conf" > /dev/null ; then
291 if [[ "$__val" == *"&"* ]] ; then
292 __val="$(echo $__val | sed 's/&/\\&/g')"
293 fi
294 text="$(cat $__conf)"
295 printf "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|" > "$__conf"
296
297 else
298 _debug APP
299 echo "" >> "$__conf"
300 echo "$__opt$__sep$__val$__end" >> "$__conf"
301 fi
302 _debug "$(grep -H -n "^$__opt$__sep" $__conf)"
303 }
304
305 #_savedomainconf key value
306 #save to domain.conf
307 _savedomainconf() {
308 key="$1"
309 value="$2"
310 if [ "$DOMAIN_CONF" ] ; then
311 _setopt $DOMAIN_CONF "$key" "=" "$value"
312 else
313 _err "DOMAIN_CONF is empty, can not save $key=$value"
314 fi
315 }
316
317 #_saveaccountconf key value
318 _saveaccountconf() {
319 key="$1"
320 value="$2"
321 if [ "$ACCOUNT_CONF_PATH" ] ; then
322 _setopt $ACCOUNT_CONF_PATH "$key" "=" "$value"
323 else
324 _err "ACCOUNT_CONF_PATH is empty, can not save $key=$value"
325 fi
326 }
327
328 _startserver() {
329 content="$1"
330 _NC="nc -q 1"
331 if nc -h 2>&1 | grep "nmap.org/ncat" >/dev/null ; then
332 _NC="nc"
333 fi
334 # while true ; do
335 if [ "$DEBUG" ] ; then
336 echo -e -n "HTTP/1.1 200 OK\r\n\r\n$content" | $_NC -l -p $Le_HTTPPort -vv
337 else
338 echo -e -n "HTTP/1.1 200 OK\r\n\r\n$content" | $_NC -l -p $Le_HTTPPort > /dev/null
339 fi
340 # done
341 }
342
343 _stopserver() {
344 pid="$1"
345
346 }
347
348 _initpath() {
349
350 if [ -z "$LE_WORKING_DIR" ]; then
351 LE_WORKING_DIR=$HOME/.le
352 fi
353
354 if [ -z "$ACCOUNT_CONF_PATH" ] ; then
355 ACCOUNT_CONF_PATH="$LE_WORKING_DIR/account.conf"
356 fi
357
358 if [ -f "$ACCOUNT_CONF_PATH" ] ; then
359 source "$ACCOUNT_CONF_PATH"
360 fi
361
362 if [ -z "$API" ] ; then
363 if [ -z "$STAGE" ] ; then
364 API="$DEFAULT_CA"
365 else
366 API="$STAGE_CA"
367 _info "Using stage api:$API"
368 fi
369 fi
370
371 if [ -z "$ACME_DIR" ] ; then
372 ACME_DIR="/home/.acme"
373 fi
374
375 if [ -z "$APACHE_CONF_BACKUP_DIR" ] ; then
376 APACHE_CONF_BACKUP_DIR="$LE_WORKING_DIR/"
377 fi
378
379 domain="$1"
380 if ! mkdir -p "$LE_WORKING_DIR" ; then
381 _err "Can not craete working dir: $LE_WORKING_DIR"
382 return 1
383 fi
384
385 if [ -z "$ACCOUNT_KEY_PATH" ] ; then
386 ACCOUNT_KEY_PATH="$LE_WORKING_DIR/account.key"
387 fi
388
389 if [ -z "$domain" ] ; then
390 return 0
391 fi
392
393 domainhome="$LE_WORKING_DIR/$domain"
394 mkdir -p "$domainhome"
395
396 if [ -z "$DOMAIN_PATH" ] ; then
397 DOMAIN_PATH="$domainhome"
398 fi
399 if [ -z "$DOMAIN_CONF" ] ; then
400 DOMAIN_CONF="$domainhome/$Le_Domain.conf"
401 fi
402
403 if [ -z "$DOMAIN_SSL_CONF" ] ; then
404 DOMAIN_SSL_CONF="$domainhome/$Le_Domain.ssl.conf"
405 fi
406
407 if [ -z "$CSR_PATH" ] ; then
408 CSR_PATH="$domainhome/$domain.csr"
409 fi
410 if [ -z "$CERT_KEY_PATH" ] ; then
411 CERT_KEY_PATH="$domainhome/$domain.key"
412 fi
413 if [ -z "$CERT_PATH" ] ; then
414 CERT_PATH="$domainhome/$domain.cer"
415 fi
416 if [ -z "$CA_CERT_PATH" ] ; then
417 CA_CERT_PATH="$domainhome/ca.cer"
418 fi
419
420 }
421
422
423 _apachePath() {
424 httpdroot="$(apachectl -V | grep HTTPD_ROOT= | cut -d = -f 2 | tr -d '"' )"
425 httpdconfname="$(apachectl -V | grep SERVER_CONFIG_FILE= | cut -d = -f 2 | tr -d '"' )"
426 httpdconf="$httpdroot/$httpdconfname"
427 if [ ! -f $httpdconf ] ; then
428 _err "Apache Config file not found" $httpdconf
429 return 1
430 fi
431 return 0
432 }
433
434 _restoreApache() {
435 if [ -z "$usingApache" ] ; then
436 return 0
437 fi
438 _initpath
439 if ! _apachePath ; then
440 return 1
441 fi
442
443 if [ ! -f "$APACHE_CONF_BACKUP_DIR/$httpdconfname" ] ; then
444 _debug "No config file to restore."
445 return 0
446 fi
447
448 cp -p "$APACHE_CONF_BACKUP_DIR/$httpdconfname" "$httpdconf"
449 if ! apachectl -t ; then
450 _err "Sorry, restore apache config error, please contact me."
451 return 1;
452 fi
453 rm -f "$APACHE_CONF_BACKUP_DIR/$httpdconfname"
454 return 0
455 }
456
457 _setApache() {
458 _initpath
459 if ! _apachePath ; then
460 return 1
461 fi
462
463 #backup the conf
464 _debug "Backup apache config file" $httpdconf
465 cp -p $httpdconf $APACHE_CONF_BACKUP_DIR/
466 _info "JFYI, Config file $httpdconf is backuped to $APACHE_CONF_BACKUP_DIR/$httpdconfname"
467 _info "In case there is an error that can not be restored automatically, you may try restore it yourself."
468 _info "The backup file will be deleted on sucess, just forget it."
469
470 #add alias
471 echo "
472 Alias /.well-known/acme-challenge $ACME_DIR
473
474 <Directory $ACME_DIR >
475 Require all granted
476 </Directory>
477 " >> $httpdconf
478
479 if ! apachectl -t ; then
480 _err "Sorry, apache config error, please contact me."
481 _restoreApache
482 return 1;
483 fi
484
485 if [ ! -d "$ACME_DIR" ] ; then
486 mkdir -p "$ACME_DIR"
487 chmod 755 "$ACME_DIR"
488 fi
489
490 if ! apachectl graceful ; then
491 _err "Sorry, apachectl graceful error, please contact me."
492 _restoreApache
493 return 1;
494 fi
495 usingApache="1"
496 return 0
497 }
498
499 _clearup () {
500 _stopserver $serverproc
501 serverproc=""
502 _restoreApache
503 }
504
505 # webroot removelevel tokenfile
506 _clearupwebbroot() {
507 __webroot="$1"
508 if [ -z "$__webroot" ] ; then
509 _debug "no webroot specified, skip"
510 return 0
511 fi
512
513 if [ "$2" == '1' ] ; then
514 _debug "remove $__webroot/.well-known"
515 rm -rf "$__webroot/.well-known"
516 elif [ "$2" == '2' ] ; then
517 _debug "remove $__webroot/.well-known/acme-challenge"
518 rm -rf "$__webroot/.well-known/acme-challenge"
519 elif [ "$2" == '3' ] ; then
520 _debug "remove $__webroot/.well-known/acme-challenge/$3"
521 rm -rf "$__webroot/.well-known/acme-challenge/$3"
522 else
523 _info "Skip for removelevel:$2"
524 fi
525
526 return 0
527
528 }
529
530 issue() {
531 if [ -z "$2" ] ; then
532 _err "Usage: le issue webroot|no|apache|dns a.com [www.a.com,b.com,c.com]|no [key-length]|no"
533 return 1
534 fi
535 Le_Webroot="$1"
536 Le_Domain="$2"
537 Le_Alt="$3"
538 Le_Keylength="$4"
539 Le_RealCertPath="$5"
540 Le_RealKeyPath="$6"
541 Le_RealCACertPath="$7"
542 Le_ReloadCmd="$8"
543
544
545 _initpath $Le_Domain
546
547 if [ -f "$DOMAIN_CONF" ] ; then
548 Le_NextRenewTime=$(grep "^Le_NextRenewTime=" "$DOMAIN_CONF" | cut -d '=' -f 2)
549 if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(date -u "+%s" )" -lt "$Le_NextRenewTime" ] ; then
550 _info "Skip, Next renewal time is: $(grep "^Le_NextRenewTimeStr" "$DOMAIN_CONF" | cut -d '=' -f 2)"
551 return 2
552 fi
553 fi
554
555 if [ "$Le_Alt" == "no" ] ; then
556 Le_Alt=""
557 fi
558 if [ "$Le_Keylength" == "no" ] ; then
559 Le_Keylength=""
560 fi
561 if [ "$Le_RealCertPath" == "no" ] ; then
562 Le_RealCertPath=""
563 fi
564 if [ "$Le_RealKeyPath" == "no" ] ; then
565 Le_RealKeyPath=""
566 fi
567 if [ "$Le_RealCACertPath" == "no" ] ; then
568 Le_RealCACertPath=""
569 fi
570 if [ "$Le_ReloadCmd" == "no" ] ; then
571 Le_ReloadCmd=""
572 fi
573
574 _setopt "$DOMAIN_CONF" "Le_Domain" "=" "$Le_Domain"
575 _setopt "$DOMAIN_CONF" "Le_Alt" "=" "$Le_Alt"
576 _setopt "$DOMAIN_CONF" "Le_Webroot" "=" "$Le_Webroot"
577 _setopt "$DOMAIN_CONF" "Le_Keylength" "=" "$Le_Keylength"
578 _setopt "$DOMAIN_CONF" "Le_RealCertPath" "=" "\"$Le_RealCertPath\""
579 _setopt "$DOMAIN_CONF" "Le_RealCACertPath" "=" "\"$Le_RealCACertPath\""
580 _setopt "$DOMAIN_CONF" "Le_RealKeyPath" "=" "\"$Le_RealKeyPath\""
581 _setopt "$DOMAIN_CONF" "Le_ReloadCmd" "=" "\"$Le_ReloadCmd\""
582
583 if [ "$Le_Webroot" == "no" ] ; then
584 _info "Standalone mode."
585 if ! command -v "nc" > /dev/null ; then
586 _err "Please install netcat(nc) tools first."
587 return 1
588 fi
589
590 if [ -z "$Le_HTTPPort" ] ; then
591 Le_HTTPPort=80
592 fi
593 _setopt "$DOMAIN_CONF" "Le_HTTPPort" "=" "$Le_HTTPPort"
594
595 netprc="$(ss -ntpl | grep :$Le_HTTPPort" ")"
596 if [ "$netprc" ] ; then
597 _err "$netprc"
598 _err "tcp port $Le_HTTPPort is already used by $(echo "$netprc" | cut -d : -f 4)"
599 _err "Please stop it first"
600 return 1
601 fi
602 fi
603
604 if [ "$Le_Webroot" == "apache" ] ; then
605 if ! _setApache ; then
606 _err "set up apache error. Report error to me."
607 return 1
608 fi
609 wellknown_path="$ACME_DIR"
610 else
611 usingApache=""
612 fi
613
614 createAccountKey $Le_Domain $Le_Keylength
615
616 if ! createDomainKey $Le_Domain $Le_Keylength ; then
617 _err "Create domain key error."
618 return 1
619 fi
620
621 if ! createCSR $Le_Domain $Le_Alt ; then
622 _err "Create CSR error."
623 return 1
624 fi
625
626 pub_exp=$(openssl rsa -in $ACCOUNT_KEY_PATH -noout -text | grep "^publicExponent:"| cut -d '(' -f 2 | cut -d 'x' -f 2 | cut -d ')' -f 1)
627 if [ "${#pub_exp}" == "5" ] ; then
628 pub_exp=0$pub_exp
629 fi
630 _debug pub_exp "$pub_exp"
631
632 e=$(echo $pub_exp | _h2b | _base64)
633 _debug e "$e"
634
635 modulus=$(openssl rsa -in $ACCOUNT_KEY_PATH -modulus -noout | cut -d '=' -f 2 )
636 n=$(echo $modulus| _h2b | _base64 | _b64 )
637
638 jwk='{"e": "'$e'", "kty": "RSA", "n": "'$n'"}'
639
640 HEADER='{"alg": "RS256", "jwk": '$jwk'}'
641 HEADERPLACE='{"nonce": "NONCE", "alg": "RS256", "jwk": '$jwk'}'
642 _debug HEADER "$HEADER"
643
644 accountkey_json=$(echo -n "$jwk" | tr -d ' ' )
645 thumbprint=$(echo -n "$accountkey_json" | openssl dgst -sha256 -binary | _base64 | _b64)
646
647
648 _info "Registering account"
649 regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}'
650 if [ "$ACCOUNT_EMAIL" ] ; then
651 regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}'
652 fi
653 _send_signed_request "$API/acme/new-reg" "$regjson"
654
655 if [ "$code" == "" ] || [ "$code" == '201' ] ; then
656 _info "Registered"
657 echo $response > $LE_WORKING_DIR/account.json
658 elif [ "$code" == '409' ] ; then
659 _info "Already registered"
660 else
661 _err "Register account Error."
662 _clearup
663 return 1
664 fi
665
666 vtype="$VTYPE_HTTP"
667 if [[ "$Le_Webroot" == "dns"* ]] ; then
668 vtype="$VTYPE_DNS"
669 fi
670
671 vlist="$Le_Vlist"
672 # verify each domain
673 _info "Verify each domain"
674 sep='#'
675 if [ -z "$vlist" ] ; then
676 alldomains=$(echo "$Le_Domain,$Le_Alt" | tr ',' ' ' )
677 for d in $alldomains
678 do
679 _info "Getting token for domain" $d
680 _send_signed_request "$API/acme/new-authz" "{\"resource\": \"new-authz\", \"identifier\": {\"type\": \"dns\", \"value\": \"$d\"}}"
681 if [ ! -z "$code" ] && [ ! "$code" == '201' ] ; then
682 _err "new-authz error: $response"
683 _clearup
684 return 1
685 fi
686
687 entry="$(printf $response | egrep -o '{[^{]*"type":"'$vtype'"[^}]*')"
688 _debug entry "$entry"
689
690 token="$(printf "$entry" | egrep -o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')"
691 _debug token $token
692
693 uri="$(printf "$entry" | egrep -o '"uri":"[^"]*'| cut -d : -f 2,3 | tr -d '"' )"
694 _debug uri $uri
695
696 keyauthorization="$token.$thumbprint"
697 _debug keyauthorization "$keyauthorization"
698
699 dvlist="$d$sep$keyauthorization$sep$uri"
700 _debug dvlist "$dvlist"
701
702 vlist="$vlist$dvlist,"
703
704 done
705
706 #add entry
707 dnsadded=""
708 ventries=$(echo "$vlist" | tr ',' ' ' )
709 for ventry in $ventries
710 do
711 d=$(echo $ventry | cut -d $sep -f 1)
712 keyauthorization=$(echo $ventry | cut -d $sep -f 2)
713
714 if [ "$vtype" == "$VTYPE_DNS" ] ; then
715 dnsadded='0'
716 txtdomain="_acme-challenge.$d"
717 _debug txtdomain "$txtdomain"
718 txt="$(echo -e -n $keyauthorization | openssl dgst -sha256 -binary | _base64 | _b64)"
719 _debug txt "$txt"
720 #dns
721 #1. check use api
722 d_api=""
723 if [ -f "$LE_WORKING_DIR/$d/$Le_Webroot" ] ; then
724 d_api="$LE_WORKING_DIR/$d/$Le_Webroot"
725 elif [ -f "$LE_WORKING_DIR/$d/$Le_Webroot.sh" ] ; then
726 d_api="$LE_WORKING_DIR/$d/$Le_Webroot.sh"
727 elif [ -f "$LE_WORKING_DIR/$Le_Webroot" ] ; then
728 d_api="$LE_WORKING_DIR/$Le_Webroot"
729 elif [ -f "$LE_WORKING_DIR/$Le_Webroot.sh" ] ; then
730 d_api="$LE_WORKING_DIR/$Le_Webroot.sh"
731 elif [ -f "$LE_WORKING_DIR/dnsapi/$Le_Webroot" ] ; then
732 d_api="$LE_WORKING_DIR/dnsapi/$Le_Webroot"
733 elif [ -f "$LE_WORKING_DIR/dnsapi/$Le_Webroot.sh" ] ; then
734 d_api="$LE_WORKING_DIR/dnsapi/$Le_Webroot.sh"
735 fi
736 _debug d_api "$d_api"
737
738 if [ "$d_api" ]; then
739 _info "Found domain api file: $d_api"
740 else
741 _err "Add the following TXT record:"
742 _err "Domain: $txtdomain"
743 _err "TXT value: $txt"
744 _err "Please be aware that you prepend _acme-challenge. before your domain"
745 _err "so the resulting subdomain will be: $txtdomain"
746 continue
747 fi
748
749 if ! source $d_api ; then
750 _err "Load file $d_api error. Please check your api file and try again."
751 return 1
752 fi
753
754 addcommand="$Le_Webroot-add"
755 if ! command -v $addcommand ; then
756 _err "It seems that your api file is not correct, it must have a function named: $Le_Webroot"
757 return 1
758 fi
759
760 if ! $addcommand $txtdomain $txt ; then
761 _err "Error add txt for domain:$txtdomain"
762 return 1
763 fi
764 dnsadded='1'
765 fi
766 done
767
768 if [ "$dnsadded" == '0' ] ; then
769 _setopt "$DOMAIN_CONF" "Le_Vlist" "=" "\"$vlist\""
770 _debug "Dns record not added yet, so, save to $DOMAIN_CONF and exit."
771 _err "Please add the TXT records to the domains, and retry again."
772 return 1
773 fi
774
775 fi
776
777 if [ "$dnsadded" == '1' ] ; then
778 _info "Sleep 60 seconds for the txt records to take effect"
779 sleep 60
780 fi
781
782 _debug "ok, let's start to verify"
783 ventries=$(echo "$vlist" | tr ',' ' ' )
784 for ventry in $ventries
785 do
786 d=$(echo $ventry | cut -d $sep -f 1)
787 keyauthorization=$(echo $ventry | cut -d $sep -f 2)
788 uri=$(echo $ventry | cut -d $sep -f 3)
789 _info "Verifying:$d"
790 _debug "d" "$d"
791 _debug "keyauthorization" "$keyauthorization"
792 _debug "uri" "$uri"
793 removelevel=""
794 token=""
795 if [ "$vtype" == "$VTYPE_HTTP" ] ; then
796 if [ "$Le_Webroot" == "no" ] ; then
797 _info "Standalone mode server"
798 _startserver "$keyauthorization" &
799 serverproc="$!"
800 sleep 2
801 _debug serverproc $serverproc
802 else
803 if [ -z "$wellknown_path" ] ; then
804 wellknown_path="$Le_Webroot/.well-known/acme-challenge"
805 fi
806 _debug wellknown_path "$wellknown_path"
807
808 if [ ! -d "$Le_Webroot/.well-known" ] ; then
809 removelevel='1'
810 elif [ ! -d "$Le_Webroot/.well-known/acme-challenge" ] ; then
811 removelevel='2'
812 else
813 removelevel='3'
814 fi
815
816 token="$(echo -e -n "$keyauthorization" | cut -d '.' -f 1)"
817 _debug "writing token:$token to $wellknown_path/$token"
818
819 mkdir -p "$wellknown_path"
820 echo -n "$keyauthorization" > "$wellknown_path/$token"
821
822 webroot_owner=$(stat -c '%U:%G' $Le_Webroot)
823 _debug "Changing owner/group of .well-known to $webroot_owner"
824 chown -R $webroot_owner "$Le_Webroot/.well-known"
825
826 fi
827 fi
828
829 _send_signed_request $uri "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}"
830
831 if [ ! -z "$code" ] && [ ! "$code" == '202' ] ; then
832 _err "$d:Challenge error: $resource"
833 _clearupwebbroot "$Le_Webroot" "$removelevel" "$token"
834 _clearup
835 return 1
836 fi
837
838 while [ "1" ] ; do
839 _debug "sleep 5 secs to verify"
840 sleep 5
841 _debug "checking"
842
843 if ! _get $uri ; then
844 _err "$d:Verify error:$resource"
845 _clearupwebbroot "$Le_Webroot" "$removelevel" "$token"
846 _clearup
847 return 1
848 fi
849
850 status=$(echo $response | egrep -o '"status":"[^"]+"' | cut -d : -f 2 | tr -d '"')
851 if [ "$status" == "valid" ] ; then
852 _info "Success"
853 _stopserver $serverproc
854 serverproc=""
855 _clearupwebbroot "$Le_Webroot" "$removelevel" "$token"
856 break;
857 fi
858
859 if [ "$status" == "invalid" ] ; then
860 error=$(echo $response | egrep -o '"error":{[^}]*}' | grep -o '"detail":"[^"]*"' | cut -d '"' -f 4)
861 _err "$d:Verify error:$error"
862 _clearupwebbroot "$Le_Webroot" "$removelevel" "$token"
863 _clearup
864 return 1;
865 fi
866
867 if [ "$status" == "pending" ] ; then
868 _info "Pending"
869 else
870 _err "$d:Verify error:$response"
871 _clearupwebbroot "$Le_Webroot" "$removelevel" "$token"
872 _clearup
873 return 1
874 fi
875
876 done
877
878 done
879
880 _clearup
881 _info "Verify finished, start to sign."
882 der="$(openssl req -in $CSR_PATH -outform DER | _base64 | _b64)"
883 _send_signed_request "$API/acme/new-cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64"
884
885
886 Le_LinkCert="$(grep -i -o '^Location.*$' $CURL_HEADER | tr -d "\r\n" | cut -d " " -f 2)"
887 _setopt "$DOMAIN_CONF" "Le_LinkCert" "=" "$Le_LinkCert"
888
889 if [ "$Le_LinkCert" ] ; then
890 echo -----BEGIN CERTIFICATE----- > "$CERT_PATH"
891 curl --silent "$Le_LinkCert" | openssl base64 -e >> "$CERT_PATH"
892 echo -----END CERTIFICATE----- >> "$CERT_PATH"
893 _info "Cert success."
894 cat "$CERT_PATH"
895
896 _info "Your cert is in $CERT_PATH"
897 fi
898
899
900 if [ -z "$Le_LinkCert" ] ; then
901 response="$(echo $response | openssl base64 -d -A)"
902 _err "Sign failed: $(echo "$response" | grep -o '"detail":"[^"]*"')"
903 return 1
904 fi
905
906 _setopt "$DOMAIN_CONF" 'Le_Vlist' '=' "\"\""
907
908 Le_LinkIssuer=$(grep -i '^Link' $CURL_HEADER | cut -d " " -f 2| cut -d ';' -f 1 | tr -d '<>' )
909 _setopt "$DOMAIN_CONF" "Le_LinkIssuer" "=" "$Le_LinkIssuer"
910
911 if [ "$Le_LinkIssuer" ] ; then
912 echo -----BEGIN CERTIFICATE----- > "$CA_CERT_PATH"
913 curl --silent "$Le_LinkIssuer" | openssl base64 -e >> "$CA_CERT_PATH"
914 echo -----END CERTIFICATE----- >> "$CA_CERT_PATH"
915 _info "The intermediate CA cert is in $CA_CERT_PATH"
916 fi
917
918 Le_CertCreateTime=$(date -u "+%s")
919 _setopt "$DOMAIN_CONF" "Le_CertCreateTime" "=" "$Le_CertCreateTime"
920
921 Le_CertCreateTimeStr=$(date -u )
922 _setopt "$DOMAIN_CONF" "Le_CertCreateTimeStr" "=" "\"$Le_CertCreateTimeStr\""
923
924 if [ ! "$Le_RenewalDays" ] ; then
925 Le_RenewalDays=80
926 fi
927
928 _setopt "$DOMAIN_CONF" "Le_RenewalDays" "=" "$Le_RenewalDays"
929
930 let "Le_NextRenewTime=Le_CertCreateTime+Le_RenewalDays*24*60*60"
931 _setopt "$DOMAIN_CONF" "Le_NextRenewTime" "=" "$Le_NextRenewTime"
932
933 Le_NextRenewTimeStr=$( _time2str $Le_NextRenewTime )
934 _setopt "$DOMAIN_CONF" "Le_NextRenewTimeStr" "=" "\"$Le_NextRenewTimeStr\""
935
936
937 installcert $Le_Domain "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd"
938
939 }
940
941 renew() {
942 Le_Domain="$1"
943 if [ -z "$Le_Domain" ] ; then
944 _err "Usage: $0 domain.com"
945 return 1
946 fi
947
948 _initpath $Le_Domain
949
950 if [ ! -f "$DOMAIN_CONF" ] ; then
951 _info "$Le_Domain is not a issued domain, skip."
952 return 0;
953 fi
954
955 source "$DOMAIN_CONF"
956 if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(date -u "+%s" )" -lt "$Le_NextRenewTime" ] ; then
957 _info "Skip, Next renewal time is: $Le_NextRenewTimeStr"
958 return 2
959 fi
960
961 IS_RENEW="1"
962 issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd"
963 local res=$?
964 IS_RENEW=""
965
966 return $res
967 }
968
969 renewAll() {
970 _initpath
971 _info "renewAll"
972
973 for d in $(ls -F $LE_WORKING_DIR | grep [^.].*[.].*/$ ) ; do
974 d=$(echo $d | cut -d '/' -f 1)
975 _info "renew $d"
976
977 Le_LinkCert=""
978 Le_Domain=""
979 Le_Alt=""
980 Le_Webroot=""
981 Le_Keylength=""
982 Le_LinkIssuer=""
983
984 Le_CertCreateTime=""
985 Le_CertCreateTimeStr=""
986 Le_RenewalDays=""
987 Le_NextRenewTime=""
988 Le_NextRenewTimeStr=""
989
990 Le_RealCertPath=""
991 Le_RealKeyPath=""
992
993 Le_RealCACertPath=""
994
995 Le_ReloadCmd=""
996
997 DOMAIN_PATH=""
998 DOMAIN_CONF=""
999 DOMAIN_SSL_CONF=""
1000 CSR_PATH=""
1001 CERT_KEY_PATH=""
1002 CERT_PATH=""
1003 CA_CERT_PATH=""
1004 ACCOUNT_KEY_PATH=""
1005
1006 wellknown_path=""
1007
1008 renew "$d"
1009 done
1010
1011 }
1012
1013 installcert() {
1014 Le_Domain="$1"
1015 if [ -z "$Le_Domain" ] ; then
1016 _err "Usage: $0 domain.com [cert-file-path]|no [key-file-path]|no [ca-cert-file-path]|no [reloadCmd]|no"
1017 return 1
1018 fi
1019
1020 Le_RealCertPath="$2"
1021 Le_RealKeyPath="$3"
1022 Le_RealCACertPath="$4"
1023 Le_ReloadCmd="$5"
1024
1025 _initpath $Le_Domain
1026
1027 _setopt "$DOMAIN_CONF" "Le_RealCertPath" "=" "\"$Le_RealCertPath\""
1028 _setopt "$DOMAIN_CONF" "Le_RealCACertPath" "=" "\"$Le_RealCACertPath\""
1029 _setopt "$DOMAIN_CONF" "Le_RealKeyPath" "=" "\"$Le_RealKeyPath\""
1030 _setopt "$DOMAIN_CONF" "Le_ReloadCmd" "=" "\"$Le_ReloadCmd\""
1031
1032 if [ "$Le_RealCertPath" ] ; then
1033 if [ -f "$Le_RealCertPath" ] ; then
1034 cp -p "$Le_RealCertPath" "$Le_RealCertPath".bak
1035 fi
1036 cat "$CERT_PATH" > "$Le_RealCertPath"
1037 fi
1038
1039 if [ "$Le_RealCACertPath" ] ; then
1040 if [ -f "$Le_RealCACertPath" ] ; then
1041 cp -p "$Le_RealCACertPath" "$Le_RealCACertPath".bak
1042 fi
1043 if [ "$Le_RealCACertPath" == "$Le_RealCertPath" ] ; then
1044 echo "" >> "$Le_RealCACertPath"
1045 cat "$CA_CERT_PATH" >> "$Le_RealCACertPath"
1046 else
1047 cat "$CA_CERT_PATH" > "$Le_RealCACertPath"
1048 fi
1049 fi
1050
1051
1052 if [ "$Le_RealKeyPath" ] ; then
1053 if [ -f "$Le_RealKeyPath" ] ; then
1054 cp -p "$Le_RealKeyPath" "$Le_RealKeyPath".bak
1055 fi
1056 cat "$CERT_KEY_PATH" > "$Le_RealKeyPath"
1057 fi
1058
1059 if [ "$Le_ReloadCmd" ] ; then
1060 _info "Run Le_ReloadCmd: $Le_ReloadCmd"
1061 (cd "$DOMAIN_PATH" && eval "$Le_ReloadCmd")
1062 fi
1063
1064 }
1065
1066 installcronjob() {
1067 _initpath
1068 _info "Installing cron job"
1069 if ! crontab -l | grep 'le.sh cron' ; then
1070 if [ -f "$LE_WORKING_DIR/le.sh" ] ; then
1071 lesh="\"$LE_WORKING_DIR\"/le.sh"
1072 else
1073 _err "Can not install cronjob, le.sh not found."
1074 return 1
1075 fi
1076 crontab -l | { cat; echo "0 0 * * * LE_WORKING_DIR=\"$LE_WORKING_DIR\" $lesh cron > /dev/null"; } | crontab -
1077 fi
1078 if [ "$?" != "0" ] ; then
1079 _err "Install cron job failed. You need to manually renew your certs."
1080 _err "Or you can add cronjob by yourself:"
1081 _err "LE_WORKING_DIR=\"$LE_WORKING_DIR\" $lesh cron > /dev/null"
1082 return 1
1083 fi
1084 }
1085
1086 uninstallcronjob() {
1087 _info "Removing cron job"
1088 cr="$(crontab -l | grep 'le.sh cron')"
1089 if [ "$cr" ] ; then
1090 crontab -l | sed "/le.sh cron/d" | crontab -
1091 LE_WORKING_DIR="$(echo "$cr" | cut -d ' ' -f 6 | cut -d '=' -f 2 | tr -d '"')"
1092 _info LE_WORKING_DIR "$LE_WORKING_DIR"
1093 fi
1094 _initpath
1095
1096 }
1097
1098
1099 # Detect profile file if not specified as environment variable
1100 _detect_profile() {
1101 if [ -n "$PROFILE" -a -f "$PROFILE" ]; then
1102 echo "$PROFILE"
1103 return
1104 fi
1105
1106 local DETECTED_PROFILE
1107 DETECTED_PROFILE=''
1108 local SHELLTYPE
1109 SHELLTYPE="$(basename "/$SHELL")"
1110
1111 if [ "$SHELLTYPE" = "bash" ]; then
1112 if [ -f "$HOME/.bashrc" ]; then
1113 DETECTED_PROFILE="$HOME/.bashrc"
1114 elif [ -f "$HOME/.bash_profile" ]; then
1115 DETECTED_PROFILE="$HOME/.bash_profile"
1116 fi
1117 elif [ "$SHELLTYPE" = "zsh" ]; then
1118 DETECTED_PROFILE="$HOME/.zshrc"
1119 fi
1120
1121 if [ -z "$DETECTED_PROFILE" ]; then
1122 if [ -f "$HOME/.profile" ]; then
1123 DETECTED_PROFILE="$HOME/.profile"
1124 elif [ -f "$HOME/.bashrc" ]; then
1125 DETECTED_PROFILE="$HOME/.bashrc"
1126 elif [ -f "$HOME/.bash_profile" ]; then
1127 DETECTED_PROFILE="$HOME/.bash_profile"
1128 elif [ -f "$HOME/.zshrc" ]; then
1129 DETECTED_PROFILE="$HOME/.zshrc"
1130 fi
1131 fi
1132
1133 if [ ! -z "$DETECTED_PROFILE" ]; then
1134 echo "$DETECTED_PROFILE"
1135 fi
1136 }
1137
1138 _initconf() {
1139 _initpath
1140 if [ ! -f "$ACCOUNT_CONF_PATH" ] ; then
1141 echo "#Account configurations:
1142 #Here are the supported macros, uncomment them to make them take effect.
1143 #ACCOUNT_EMAIL=aaa@aaa.com # the account email used to register account.
1144
1145 #STAGE=1 # Use the staging api
1146 #FORCE=1 # Force to issue cert
1147 #DEBUG=1 # Debug mode
1148
1149 #dns api
1150 #######################
1151 #Cloudflare:
1152 #api key
1153 #CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
1154 #account email
1155 #CF_Email="xxxx@sss.com"
1156
1157 #######################
1158 #Dnspod.cn:
1159 #api key id
1160 #DP_Id="1234"
1161 #api key
1162 #DP_Key="sADDsdasdgdsf"
1163
1164 #######################
1165 #Cloudxns.com:
1166 #CX_Key="1234"
1167 #
1168 #CX_Secret="sADDsdasdgdsf"
1169
1170 " > $ACCOUNT_CONF_PATH
1171 fi
1172 }
1173
1174 install() {
1175 if ! _initpath ; then
1176 _err "Install failed."
1177 return 1
1178 fi
1179
1180 #check if there is sudo installed, AND if the current user is a sudoer.
1181 if command -v sudo > /dev/null ; then
1182 if [ "$(sudo -n uptime 2>&1|grep "load"|wc -l)" != "0" ] ; then
1183 SUDO=sudo
1184 fi
1185 fi
1186
1187 if command -v yum > /dev/null ; then
1188 YUM="1"
1189 INSTALL="$SUDO yum install -y "
1190 elif command -v apt-get > /dev/null ; then
1191 INSTALL="$SUDO apt-get install -y "
1192 fi
1193
1194 if ! command -v "curl" > /dev/null ; then
1195 _err "Please install curl first."
1196 _err "$INSTALL curl"
1197 return 1
1198 fi
1199
1200 if ! command -v "crontab" > /dev/null ; then
1201 _err "Please install crontab first."
1202 if [ "$YUM" ] ; then
1203 _err "$INSTALL crontabs"
1204 else
1205 _err "$INSTALL crontab"
1206 fi
1207 return 1
1208 fi
1209
1210 if ! command -v "openssl" > /dev/null ; then
1211 _err "Please install openssl first."
1212 _err "$INSTALL openssl"
1213 return 1
1214 fi
1215
1216 _info "Installing to $LE_WORKING_DIR"
1217
1218 cp le.sh "$LE_WORKING_DIR/" && chmod +x "$LE_WORKING_DIR/le.sh"
1219
1220 if [ "$?" != "0" ] ; then
1221 _err "Install failed, can not copy le.sh"
1222 return 1
1223 fi
1224
1225 _info "Installed to $LE_WORKING_DIR/le.sh"
1226
1227 _profile="$(_detect_profile)"
1228 if [ "$_profile" ] ; then
1229 _debug "Found profile: $_profile"
1230
1231 echo "LE_WORKING_DIR=$LE_WORKING_DIR
1232 alias le=\"$LE_WORKING_DIR/le.sh\"
1233 alias le.sh=\"$LE_WORKING_DIR/le.sh\"
1234 " > "$LE_WORKING_DIR/le.env"
1235
1236 _setopt "$_profile" "source \"$LE_WORKING_DIR/le.env\""
1237 _info "OK, Close and reopen your terminal to start using le"
1238 else
1239 _info "No profile is found, you will need to go into $LE_WORKING_DIR to use le.sh"
1240 fi
1241
1242 mkdir -p $LE_WORKING_DIR/dnsapi
1243 cp dnsapi/* $LE_WORKING_DIR/dnsapi/
1244
1245 #to keep compatible mv the .acc file to .key file
1246 if [ -f "$LE_WORKING_DIR/account.acc" ] ; then
1247 mv "$LE_WORKING_DIR/account.acc" "$LE_WORKING_DIR/account.key"
1248 fi
1249
1250 installcronjob
1251
1252 if [ ! -f "$ACCOUNT_CONF_PATH" ] ; then
1253 _initconf
1254 fi
1255 _info OK
1256 }
1257
1258 uninstall() {
1259 uninstallcronjob
1260 _initpath
1261
1262 _profile="$(_detect_profile)"
1263 if [ "$_profile" ] ; then
1264 sed -i /le.env/d "$_profile"
1265 fi
1266
1267 rm -f $LE_WORKING_DIR/le.sh
1268 _info "The keys and certs are in $LE_WORKING_DIR, you can remove them by yourself."
1269
1270 }
1271
1272 cron() {
1273 renewAll
1274 }
1275
1276 version() {
1277 _info "$PROJECT"
1278 _info "v$VER"
1279 }
1280
1281 showhelp() {
1282 version
1283 echo "Usage: le.sh [command] ...[args]....
1284 Avalible commands:
1285
1286 install:
1287 Install le.sh to your system.
1288 issue:
1289 Issue a cert.
1290 installcert:
1291 Install the issued cert to apache/nginx or any other server.
1292 renew:
1293 Renew a cert.
1294 renewAll:
1295 Renew all the certs.
1296 uninstall:
1297 Uninstall le.sh, and uninstall the cron job.
1298 version:
1299 Show version info.
1300 installcronjob:
1301 Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job.
1302 uninstallcronjob:
1303 Uninstall the cron job. The 'uninstall' command can do this automatically.
1304 createAccountKey:
1305 Create an account private key, professional use.
1306 createDomainKey:
1307 Create an domain private key, professional use.
1308 createCSR:
1309 Create CSR , professional use.
1310 "
1311 }
1312
1313
1314 if [ -z "$1" ] ; then
1315 showhelp
1316 else
1317 "$@"
1318 fi