diff --git a/README.md b/README.md
index 7069f040..5851e179 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ Do NOT require to be `root/sudoer`.
#Tested OS
1. Ubuntu/Debian.
2. CentOS
+3. Windows (cygwin with curl, openssl and crontab included)
#Supported Mode
@@ -27,14 +28,15 @@ Do NOT require to be `root/sudoer`.
```
./le.sh install
```
-You don't have to be root then, altough it is recommended.
+You don't have to be root then, although it is recommended.
Which does 3 jobs:
* create and copy `le.sh` to your home dir: `~/.le`
All the certs will be placed in this folder.
-* create symbol link: `/usr/local/bin/le -> ~/.le/le.sh` . (You must be root to do so.)
+* create alias : `le.sh=~/.le/le.sh` and `le=~/.le/le.sh`.
* create everyday cron job to check and renew the cert if needed.
+After install, you must close current terminal and reopen again to make the alias take effect.
Ok, you are ready to issue cert now.
Show help message:
@@ -43,7 +45,7 @@ root@v1:~# le.sh
https://github.com/Neilpang/le
v1.1.1
Usage: le.sh [command] ...[args]....
-Avalible commands:
+Available commands:
install:
Install le.sh to your system.
@@ -104,7 +106,7 @@ The issued cert will be renewed every 80 days automatically.
# Install issued cert to apache/nginx etc.
```
-le installcert aa.com /path/to/certfile/in/apache/nginx /path/to/keyfile/in/apache/nginx /path/to/ca/certfile/apahce/nginx "service apache2|nginx reload"
+le installcert aa.com /path/to/certfile/in/apache/nginx /path/to/keyfile/in/apache/nginx /path/to/ca/certfile/apache/nginx "service apache2|nginx reload"
```
Install the issued cert/key to the production apache or nginx path.
@@ -139,9 +141,6 @@ Support the latest dns-01 challenge.
le issue dns aa.com www.aa.com,user.aa.com
```
-Use domain api to automatically add dns record is not finished yet.
-So, you must manually add the txt record to finish verifying.
-
You will get the output like bellow:
```
Add the following txt record:
@@ -164,6 +163,42 @@ le renew aa.com
Ok, it's finished.
+#Automatic dns api integeration
+
+If your dns provider supports api access, we can use api to automatically issue certs.
+You don't have do anything manually.
+
+###Currently we support:
+
+1. Cloudflare.com api
+2. Dnspod.cn api
+3. Cloudxns.com api
+
+More apis are comming soon....
+
+If your dns provider is not in the supported list above, you can write your own script api easily.
+
+For more details: [How to use dns api](dnsapi)
+
+
+# Issue ECC certificate:
+LetsEncrypt now can issue ECDSA certificate.
+And we also support it.
+
+Just set key length to the `length` paramiter with a prefix "ec-".
+For example:
+```
+le issue /home/wwwroot/aa.com aa.com www.aa.com ec-256
+```
+Please look at the last parameter above.
+
+Valid values are:
+
+1. ec-256 (prime256v1, "ECDSA P-256")
+2. ec-384 (secp384r1, "ECDSA P-384")
+3. ec-521 (secp521r1, "ECDSA P-521", not supported by letsencrypt yet.)
+
+
#Under the Hood
@@ -185,7 +220,7 @@ License is GPLv3
Please Star and Fork me.
-Issues and pullrequests are welcomed.
+Issues and pull requests are welcomed.
diff --git a/dnsapi/README.md b/dnsapi/README.md
new file mode 100644
index 00000000..3588dd44
--- /dev/null
+++ b/dnsapi/README.md
@@ -0,0 +1,86 @@
+# How to use dns api
+
+## Use CloudFlare domain api to automatically issue cert
+
+For now, we support clourflare integeration.
+
+First you need to login to your clourflare account to get your api key.
+
+```
+export CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+
+export CF_Email="xxxx@sss.com"
+
+```
+
+Ok, let's issue cert now:
+```
+le.sh issue dns-cf aa.com www.aa.com
+```
+
+The `CF_Key` and `CF_Email` will be saved in `~/.le/account.conf`, when next time you use cloudflare api, it will reuse this key.
+
+
+
+## Use Dnspod.cn domain api to automatically issue cert
+
+For now, we support dnspod.cn integeration.
+
+First you need to login to your dnspod.cn account to get your api key and key id.
+
+```
+export DP_Id="1234"
+
+export DP_Key="sADDsdasdgdsf"
+
+```
+
+Ok, let's issue cert now:
+```
+le.sh issue dns-dp aa.com www.aa.com
+```
+
+The `DP_Id` and `DP_Key` will be saved in `~/.le/account.conf`, when next time you use dnspod.cn api, it will reuse this key.
+
+
+## Use Cloudxns.com domain api to automatically issue cert
+
+For now, we support Cloudxns.com integeration.
+
+First you need to login to your Cloudxns.com account to get your api key and key secret.
+
+```
+export CX_Key="1234"
+
+export CX_Secret="sADDsdasdgdsf"
+
+```
+
+Ok, let's issue cert now:
+```
+le.sh issue dns-cx aa.com www.aa.com
+```
+
+The `CX_Key` and `CX_Secret` will be saved in `~/.le/account.conf`, when next time you use Cloudxns.com api, it will reuse this key.
+
+
+
+# Use custom api
+
+If your api is not supported yet, you can write your own dns api.
+
+Let's assume you want to name it 'myapi',
+
+1. Create a bash script named `~/.le/dns-myapi.sh`,
+2. In the scrypt, you must have a function named `dns-myapi-add()`. Which will be called by le.sh to add dns records.
+3. Then you can use your api to issue cert like:
+
+```
+le.sh issue dns-myapi aa.com www.aa.com
+```
+
+For more details, please check our sample script: [dns-myapi.sh](dns-myapi.sh)
+
+
+
+
diff --git a/dnsapi/dns-cf.sh b/dnsapi/dns-cf.sh
new file mode 100755
index 00000000..b1e4b47d
--- /dev/null
+++ b/dnsapi/dns-cf.sh
@@ -0,0 +1,168 @@
+#!/bin/bash
+
+
+#
+#CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+#
+#CF_Email="xxxx@sss.com"
+
+
+CF_Api="https://api.cloudflare.com/client/v4/"
+
+######## Public functions #####################
+
+#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns-cf-add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ if [ -z "$CF_Key" ] || [ -z "$CF_Email" ] ; then
+ _err "You don't specify cloudflare api key and email yet."
+ _err "Please create you key and try again."
+ return 1
+ fi
+
+ #save the api key and email to the account conf file.
+ _saveaccountconf CF_Key "$CF_Key"
+ _saveaccountconf CF_Email "$CF_Email"
+
+ _debug "First detect the root zone"
+ if ! _get_root $fulldomain ; then
+ _err "invalid domain"
+ return 1
+ fi
+
+ _debug "Getting txt records"
+ _cf_rest GET "/zones/$_domain_id/dns_records?type=TXT&name=$fulldomain"
+
+ if [ "$?" != "0" ] || ! printf $response | grep \"success\":true > /dev/null ; then
+ _err "Error"
+ return 1
+ fi
+
+ count=$(printf $response | grep -o \"count\":[^,]* | cut -d : -f 2)
+
+ if [ "$count" == "0" ] ; then
+ _info "Adding record"
+ if _cf_rest POST "/zones/$_domain_id/dns_records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then
+ if printf $response | grep $fulldomain > /dev/null ; then
+ _info "Added, sleeping 10 seconds"
+ sleep 10
+ #todo: check if the record takes effect
+ return 0
+ else
+ _err "Add txt record error."
+ return 1
+ fi
+ fi
+ _err "Add txt record error."
+ else
+ _info "Updating record"
+ record_id=$(printf $response | grep -o \"id\":\"[^\"]*\" | cut -d : -f 2 | tr -d \")
+ _debug "record_id" $record_id
+
+ _cf_rest PUT "/zones/$_domain_id/dns_records/$record_id" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"zone_id\":\"$_domain_id\",\"zone_name\":\"$_domain\"}"
+ if [ "$?" == "0" ]; then
+ _info "Updated, sleeping 10 seconds"
+ sleep 10
+ #todo: check if the record takes effect
+ return 0;
+ fi
+ _err "Update error"
+ return 1
+ fi
+
+}
+
+
+
+
+
+#################### Private functions bellow ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+ domain=$1
+ i=2
+ p=1
+ while [ '1' ] ; do
+ h=$(printf $domain | cut -d . -f $i-100)
+ if [ -z "$h" ] ; then
+ #not valid
+ return 1;
+ fi
+
+ if ! _cf_rest GET "zones?name=$h" ; then
+ return 1
+ fi
+
+ if printf $response | grep \"name\":\"$h\" ; then
+ _domain_id=$(printf $response | grep -o \"id\":\"[^\"]*\" | cut -d : -f 2 | tr -d \")
+ if [ "$_domain_id" ] ; then
+ _sub_domain=$(printf $domain | cut -d . -f 1-$p)
+ _domain=$h
+ return 0
+ fi
+ return 1
+ fi
+ p=$i
+ let "i+=1"
+ done
+ return 1
+}
+
+
+_cf_rest() {
+ m=$1
+ ep="$2"
+ _debug $ep
+ if [ "$3" ] ; then
+ data="$3"
+ _debug data "$data"
+ response="$(curl --silent -X $m "$CF_Api/$ep" -H "X-Auth-Email: $CF_Email" -H "X-Auth-Key: $CF_Key" -H "Content-Type: application/json" --data $data)"
+ else
+ response="$(curl --silent -X $m "$CF_Api/$ep" -H "X-Auth-Email: $CF_Email" -H "X-Auth-Key: $CF_Key" -H "Content-Type: application/json")"
+ fi
+
+ if [ "$?" != "0" ] ; then
+ _err "error $ep"
+ return 1
+ fi
+ _debug response "$response"
+ return 0
+}
+
+
+_debug() {
+
+ if [ -z "$DEBUG" ] ; then
+ return
+ fi
+
+ if [ -z "$2" ] ; then
+ echo $1
+ else
+ echo "$1"="$2"
+ fi
+}
+
+_info() {
+ if [ -z "$2" ] ; then
+ echo "$1"
+ else
+ echo "$1"="$2"
+ fi
+}
+
+_err() {
+ if [ -z "$2" ] ; then
+ echo "$1" >&2
+ else
+ echo "$1"="$2" >&2
+ fi
+}
+
+
diff --git a/dnsapi/dns-cx.sh b/dnsapi/dns-cx.sh
new file mode 100644
index 00000000..07c9cf08
--- /dev/null
+++ b/dnsapi/dns-cx.sh
@@ -0,0 +1,234 @@
+#!/bin/bash
+
+# Cloudxns.com Domain api
+#
+#CX_Key="1234"
+#
+#CX_Secret="sADDsdasdgdsf"
+
+
+CX_Api="https://www.cloudxns.net/api2"
+
+
+#REST_API
+######## Public functions #####################
+
+#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns-cx-add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ if [ -z "$CX_Key" ] || [ -z "$CX_Secret" ] ; then
+ _err "You don't specify cloudxns.com api key or secret yet."
+ _err "Please create you key and try again."
+ return 1
+ fi
+
+ REST_API=$CX_Api
+
+ #save the api key and email to the account conf file.
+ _saveaccountconf CX_Key "$CX_Key"
+ _saveaccountconf CX_Secret "$CX_Secret"
+
+
+ _debug "First detect the root zone"
+ if ! _get_root $fulldomain ; then
+ _err "invalid domain"
+ return 1
+ fi
+
+ existing_records $_domain $_sub_domain
+ _debug count "$count"
+ if [ "$?" != "0" ] ; then
+ _err "Error get existing records."
+ return 1
+ fi
+
+ if [ "$count" == "0" ] ; then
+ add_record $_domain $_sub_domain $txtvalue
+ else
+ update_record $_domain $_sub_domain $txtvalue
+ fi
+
+ if [ "$?" == "0" ] ; then
+ return 0
+ fi
+ return 1
+}
+
+#usage: root sub
+#return if the sub record already exists.
+#echos the existing records count.
+# '0' means doesn't exist
+existing_records() {
+ _debug "Getting txt records"
+ root=$1
+ sub=$2
+
+ if ! _rest GET "record/$_domain_id?:domain_id?host_id=0&offset=0&row_num=100" ; then
+ return 1
+ fi
+ count=0
+ seg=$(printf "$response" | grep -o "{[^{]*host\":\"$_sub_domain[^}]*}")
+ _debug seg "$seg"
+ if [ -z "$seg" ] ; then
+ return 0
+ fi
+
+ if printf "$response" | grep '"type":"TXT"' > /dev/null ; then
+ count=1
+ record_id=$(printf "$seg" | grep -o \"record_id\":\"[^\"]*\" | cut -d : -f 2 | tr -d \")
+ _debug record_id "$record_id"
+ return 0
+ fi
+
+}
+
+#add the txt record.
+#usage: root sub txtvalue
+add_record() {
+ root=$1
+ sub=$2
+ txtvalue=$3
+ fulldomain=$sub.$root
+
+ _info "Adding record"
+
+ if ! _rest POST "record" "{\"domain_id\": $_domain_id, \"host\":\"$_sub_domain\", \"value\":\"$txtvalue\", \"type\":\"TXT\",\"ttl\":600, \"line_id\":1}"; then
+ return 1
+ fi
+
+ return 0
+}
+
+#update the txt record
+#Usage: root sub txtvalue
+update_record() {
+ root=$1
+ sub=$2
+ txtvalue=$3
+ fulldomain=$sub.$root
+
+ _info "Updating record"
+
+ if _rest PUT "record/$record_id" "{\"domain_id\": $_domain_id, \"host\":\"$_sub_domain\", \"value\":\"$txtvalue\", \"type\":\"TXT\",\"ttl\":600, \"line_id\":1}" ; then
+ return 0
+ fi
+
+ return 1
+}
+
+
+
+
+#################### Private functions bellow ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+ domain=$1
+ i=2
+ p=1
+
+ if ! _rest GET "domain" ; then
+ return 1
+ fi
+
+ while [ '1' ] ; do
+ h=$(printf $domain | cut -d . -f $i-100)
+ _debug h "$h"
+ if [ -z "$h" ] ; then
+ #not valid
+ return 1;
+ fi
+
+ if printf "$response" | grep "$h." ; then
+ seg=$(printf "$response" | grep -o "{[^{]*$h\.[^}]*\}" )
+ _debug seg "$seg"
+ _domain_id=$(printf "$seg" | grep -o \"id\":\"[^\"]*\" | cut -d : -f 2 | tr -d \")
+ _debug _domain_id "$_domain_id"
+ if [ "$_domain_id" ] ; then
+ _sub_domain=$(printf $domain | cut -d . -f 1-$p)
+ _debug _sub_domain $_sub_domain
+ _domain=$h
+ _debug _domain $_domain
+ return 0
+ fi
+ return 1
+ fi
+ p=$i
+ let "i+=1"
+ done
+ return 1
+}
+
+
+#Usage: method URI data
+_rest() {
+ m=$1
+ ep="$2"
+ _debug $ep
+ url="$REST_API/$ep"
+ _debug url "$url"
+
+ cdate=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
+ _debug cdate "$cdate"
+
+ data="$3"
+ _debug data "$data"
+
+ sec="$CX_Key$url$data$cdate$CX_Secret"
+ _debug sec "$sec"
+ hmac=$(printf "$sec"| openssl md5 |cut -d " " -f 2)
+ _debug hmac "$hmac"
+
+ if [ "$3" ] ; then
+ response="$(curl --silent -X $m "$url" -H "API-KEY: $CX_Key" -H "API-REQUEST-DATE: $cdate" -H "API-HMAC: $hmac" -H 'Content-Type: application/json' -d "$data")"
+ else
+ response="$(curl --silent -X $m "$url" -H "API-KEY: $CX_Key" -H "API-REQUEST-DATE: $cdate" -H "API-HMAC: $hmac" -H 'Content-Type: application/json')"
+ fi
+
+ if [ "$?" != "0" ] ; then
+ _err "error $ep"
+ return 1
+ fi
+ _debug response "$response"
+ if ! printf "$response" | grep '"message":"success"' > /dev/null ; then
+ return 1
+ fi
+ return 0
+}
+
+
+_debug() {
+
+ if [ -z "$DEBUG" ] ; then
+ return
+ fi
+
+ if [ -z "$2" ] ; then
+ echo $1
+ else
+ echo "$1"="$2"
+ fi
+}
+
+_info() {
+ if [ -z "$2" ] ; then
+ echo "$1"
+ else
+ echo "$1"="$2"
+ fi
+}
+
+_err() {
+ if [ -z "$2" ] ; then
+ echo "$1" >&2
+ else
+ echo "$1"="$2" >&2
+ fi
+}
+
+
diff --git a/dnsapi/dns-dp.sh b/dnsapi/dns-dp.sh
new file mode 100644
index 00000000..b39e3c40
--- /dev/null
+++ b/dnsapi/dns-dp.sh
@@ -0,0 +1,229 @@
+#!/bin/bash
+
+# Dnspod.cn Domain api
+#
+#DP_Id="1234"
+#
+#DP_Key="sADDsdasdgdsf"
+
+
+DP_Api="https://dnsapi.cn"
+
+
+#REST_API
+######## Public functions #####################
+
+#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns-dp-add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ if [ -z "$DP_Id" ] || [ -z "$DP_Key" ] ; then
+ _err "You don't specify dnspod api key and key id yet."
+ _err "Please create you key and try again."
+ return 1
+ fi
+
+ REST_API=$DP_Api
+
+ #save the api key and email to the account conf file.
+ _saveaccountconf DP_Id "$DP_Id"
+ _saveaccountconf DP_Key "$DP_Key"
+
+
+ _debug "First detect the root zone"
+ if ! _get_root $fulldomain ; then
+ _err "invalid domain"
+ return 1
+ fi
+
+ existing_records $_domain $_sub_domain
+ _debug count "$count"
+ if [ "$?" != "0" ] ; then
+ _err "Error get existing records."
+ return 1
+ fi
+
+ if [ "$count" == "0" ] ; then
+ add_record $_domain $_sub_domain $txtvalue
+ else
+ update_record $_domain $_sub_domain $txtvalue
+ fi
+}
+
+#usage: root sub
+#return if the sub record already exists.
+#echos the existing records count.
+# '0' means doesn't exist
+existing_records() {
+ _debug "Getting txt records"
+ root=$1
+ sub=$2
+
+ if ! _rest POST "Record.List" "login_token=$DP_Id,$DP_Key&domain_id=$_domain_id&sub_domain=$_sub_domain"; then
+ return 1
+ fi
+
+ if printf "$response" | grep 'No records' ; then
+ count=0;
+ return 0
+ fi
+
+ if printf "$response" | grep "Action completed successful" >/dev/null ; then
+ count=$(printf "$response" | grep 'TXT' | wc -l)
+
+ record_id=$(printf "$response" | grep '^' | tail -1 | cut -d '>' -f 2 | cut -d '<' -f 1)
+ return 0
+ else
+ _err "get existing records error."
+ return 1
+ fi
+
+
+ count=0
+}
+
+#add the txt record.
+#usage: root sub txtvalue
+add_record() {
+ root=$1
+ sub=$2
+ txtvalue=$3
+ fulldomain=$sub.$root
+
+ _info "Adding record"
+
+ if ! _rest POST "Record.Create" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=默认"; then
+ return 1
+ fi
+
+ if printf "$response" | grep "Action completed successful" ; then
+
+ return 0
+ fi
+
+
+ return 1 #error
+}
+
+#update the txt record
+#Usage: root sub txtvalue
+update_record() {
+ root=$1
+ sub=$2
+ txtvalue=$3
+ fulldomain=$sub.$root
+
+ _info "Updating record"
+
+ if ! _rest POST "Record.Modify" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=默认&record_id=$record_id"; then
+ return 1
+ fi
+
+ if printf "$response" | grep "Action completed successful" ; then
+
+ return 0
+ fi
+
+ return 1 #error
+}
+
+
+
+
+#################### Private functions bellow ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+ domain=$1
+ i=2
+ p=1
+ while [ '1' ] ; do
+ h=$(printf $domain | cut -d . -f $i-100)
+ if [ -z "$h" ] ; then
+ #not valid
+ return 1;
+ fi
+
+ if ! _rest POST "Domain.Info" "login_token=$DP_Id,$DP_Key&format=json&domain=$h"; then
+ return 1
+ fi
+
+ if printf "$response" | grep "Action completed successful" ; then
+ _domain_id=$(printf "$response" | grep -o \"id\":\"[^\"]*\" | cut -d : -f 2 | tr -d \")
+ _debug _domain_id "$_domain_id"
+ if [ "$_domain_id" ] ; then
+ _sub_domain=$(printf $domain | cut -d . -f 1-$p)
+ _debug _sub_domain $_sub_domain
+ _domain=$h
+ _debug _domain $_domain
+ return 0
+ fi
+ return 1
+ fi
+ p=$i
+ let "i+=1"
+ done
+ return 1
+}
+
+
+#Usage: method URI data
+_rest() {
+ m=$1
+ ep="$2"
+ _debug $ep
+ url="$REST_API/$ep"
+
+ _debug url "$url"
+
+ if [ "$3" ] ; then
+ data="$3"
+ _debug data "$data"
+ response="$(curl --silent -X $m "$url" -d $data)"
+ else
+ response="$(curl --silent -X $m "$url" )"
+ fi
+
+ if [ "$?" != "0" ] ; then
+ _err "error $ep"
+ return 1
+ fi
+ _debug response "$response"
+ return 0
+}
+
+
+_debug() {
+
+ if [ -z "$DEBUG" ] ; then
+ return
+ fi
+
+ if [ -z "$2" ] ; then
+ echo $1
+ else
+ echo "$1"="$2"
+ fi
+}
+
+_info() {
+ if [ -z "$2" ] ; then
+ echo "$1"
+ else
+ echo "$1"="$2"
+ fi
+}
+
+_err() {
+ if [ -z "$2" ] ; then
+ echo "$1" >&2
+ else
+ echo "$1"="$2" >&2
+ fi
+}
+
+
diff --git a/dnsapi/dns-myapi.sh b/dnsapi/dns-myapi.sh
new file mode 100644
index 00000000..af7dda7a
--- /dev/null
+++ b/dnsapi/dns-myapi.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+#Here is a sample custom api script.
+#This file name is "dns-myapi.sh"
+#So, here must be a method dns-myapi-add()
+#Which will be called by le.sh to add the txt record to your api system.
+#returns 0 meanst success, otherwise error.
+
+
+
+######## Public functions #####################
+
+#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns-myapi-add() {
+ fulldomain=$1
+ txtvalue=$2
+ _err "Not implemented!"
+ return 1;
+}
+
+
+
+
+
+
+
+
+
+#################### Private functions bellow ##################################
+
+
+_debug() {
+
+ if [ -z "$DEBUG" ] ; then
+ return
+ fi
+
+ if [ -z "$2" ] ; then
+ echo $1
+ else
+ echo "$1"="$2"
+ fi
+}
+
+_info() {
+ if [ -z "$2" ] ; then
+ echo "$1"
+ else
+ echo "$1"="$2"
+ fi
+}
+
+_err() {
+ if [ -z "$2" ] ; then
+ echo "$1" >&2
+ else
+ echo "$1"="$2" >&2
+ fi
+}
+
+
diff --git a/le.sh b/le.sh
index 493e4438..7bfd2c29 100755
--- a/le.sh
+++ b/le.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-VER=1.1.1
+VER=1.1.6
PROJECT="https://github.com/Neilpang/le"
DEFAULT_CA="https://acme-v01.api.letsencrypt.org"
@@ -41,6 +41,7 @@ _err() {
else
echo "$1"="$2" >&2
fi
+ return 1
}
_h2b() {
@@ -64,13 +65,19 @@ _base64() {
#domain [2048]
createAccountKey() {
+ _info "Creating account key"
if [ -z "$1" ] ; then
- echo Usage: $0 account-domain [2048]
+ echo Usage: createAccountKey account-domain [2048]
return
fi
account=$1
length=$2
+
+ if [[ "$length" == "ec-"* ]] ; then
+ length=2048
+ fi
+
if [ -z "$2" ] ; then
_info "Use default length 2048"
length=2048
@@ -89,20 +96,53 @@ createAccountKey() {
#domain length
createDomainKey() {
+ _info "Creating domain key"
if [ -z "$1" ] ; then
- echo Usage: $0 domain [2048]
+ echo Usage: createDomainKey domain [2048]
return
fi
domain=$1
length=$2
- if [ -z "$2" ] ; then
- _info "Use default length 2048"
- length=2048
+ isec=""
+ if [[ "$length" == "ec-"* ]] ; then
+ isec="1"
+ length=$(printf $length | cut -d '-' -f 2-100)
+ eccname="$length"
+ fi
+
+ if [ -z "$length" ] ; then
+ if [ "$isec" ] ; then
+ length=256
+ else
+ length=2048
+ fi
+ fi
+ _info "Use length $length"
+
+ if [ "$isec" ] ; then
+ if [ "$length" == "256" ] ; then
+ eccname="prime256v1"
+ fi
+ if [ "$length" == "384" ] ; then
+ eccname="secp384r1"
+ fi
+ if [ "$length" == "521" ] ; then
+ eccname="secp521r1"
+ fi
+ _info "Using ec name: $eccname"
fi
+
_initpath $domain
- if [ -f "$CERT_KEY_PATH" ] && ! [ "$FORCE" ] ; then
+ if [ ! -f "$CERT_KEY_PATH" ] || ( [ "$FORCE" ] && ! [ "$IS_RENEW" ] ); then
+ #generate account key
+ if [ "$isec" ] ; then
+ openssl ecparam -name $eccname -genkey 2>/dev/null > "$CERT_KEY_PATH"
+ else
+ openssl genrsa $length 2>/dev/null > "$CERT_KEY_PATH"
+ fi
+ else
if [ "$IS_RENEW" ] ; then
_info "Domain key exists, skip"
return 0
@@ -111,15 +151,13 @@ createDomainKey() {
_err "Set FORCE=1, and try again."
return 1
fi
- else
- #generate account key
- openssl genrsa $length > "$CERT_KEY_PATH"
fi
}
# domain domainlist
createCSR() {
+ _info "Creating csr"
if [ -z "$1" ] ; then
echo Usage: $0 domain [domainlist]
return
@@ -160,8 +198,8 @@ _send_signed_request() {
_debug url $url
_debug payload "$payload"
- CURL_HEADER="$WORKING_DIR/curl.header"
- dp="$WORKING_DIR/curl.dump"
+ CURL_HEADER="$LE_WORKING_DIR/curl.header"
+ dp="$LE_WORKING_DIR/curl.dump"
CURL="curl --silent --dump-header $CURL_HEADER "
if [ "$DEBUG" ] ; then
CURL="$CURL --trace-ascii $dp "
@@ -239,6 +277,29 @@ _setopt() {
_debug "$(grep -H -n "^$__opt$__sep" $__conf)"
}
+#_savedomainconf key value
+#save to domain.conf
+_savedomainconf() {
+ key="$1"
+ value="$2"
+ if [ "$DOMAIN_CONF" ] ; then
+ _setopt $DOMAIN_CONF "$key" "=" "$value"
+ else
+ _err "DOMAIN_CONF is empty, can not save $key=$value"
+ fi
+}
+
+#_saveaccountconf key value
+_saveaccountconf() {
+ key="$1"
+ value="$2"
+ if [ "$ACCOUNT_CONF_PATH" ] ; then
+ _setopt $ACCOUNT_CONF_PATH "$key" "=" "$value"
+ else
+ _err "ACCOUNT_CONF_PATH is empty, can not save $key=$value"
+ fi
+}
+
_startserver() {
content="$1"
_NC="nc -q 1"
@@ -247,9 +308,9 @@ _startserver() {
fi
# while true ; do
if [ "$DEBUG" ] ; then
- echo -e -n "HTTP/1.1 200 OK\r\n\r\n$content" | $_NC -l -p 80 -vv
+ echo -e -n "HTTP/1.1 200 OK\r\n\r\n$content" | $_NC -l -p $Le_HTTPPort -vv
else
- echo -e -n "HTTP/1.1 200 OK\r\n\r\n$content" | $_NC -l -p 80 > /dev/null
+ echo -e -n "HTTP/1.1 200 OK\r\n\r\n$content" | $_NC -l -p $Le_HTTPPort > /dev/null
fi
# done
}
@@ -267,6 +328,18 @@ _initpath() {
SUDO=sudo
fi
fi
+
+ if [ -z "$LE_WORKING_DIR" ]; then
+ LE_WORKING_DIR=$HOME/.le
+ fi
+
+ if [ -z "$ACCOUNT_CONF_PATH" ] ; then
+ ACCOUNT_CONF_PATH="$LE_WORKING_DIR/account.conf"
+ fi
+
+ if [ -f "$ACCOUNT_CONF_PATH" ] ; then
+ source "$ACCOUNT_CONF_PATH"
+ fi
if [ -z "$API" ] ; then
if [ -z "$STAGE" ] ; then
@@ -277,48 +350,45 @@ _initpath() {
fi
fi
- if [ -z "$WORKING_DIR" ]; then
- WORKING_DIR=$HOME/.le
- fi
-
if [ -z "$ACME_DIR" ] ; then
ACME_DIR="/home/.acme"
fi
if [ -z "$APACHE_CONF_BACKUP_DIR" ] ; then
- APACHE_CONF_BACKUP_DIR="$WORKING_DIR/"
+ APACHE_CONF_BACKUP_DIR="$LE_WORKING_DIR/"
fi
domain="$1"
- mkdir -p "$WORKING_DIR"
+ mkdir -p "$LE_WORKING_DIR"
if [ -z "$ACCOUNT_KEY_PATH" ] ; then
- ACCOUNT_KEY_PATH="$WORKING_DIR/account.acc"
+ ACCOUNT_KEY_PATH="$LE_WORKING_DIR/account.key"
fi
-
+
if [ -z "$domain" ] ; then
return 0
fi
- mkdir -p "$WORKING_DIR/$domain"
+ domainhome="$LE_WORKING_DIR/$domain"
+ mkdir -p "$domainhome"
if [ -z "$DOMAIN_CONF" ] ; then
- DOMAIN_CONF="$WORKING_DIR/$domain/$Le_Domain.conf"
+ DOMAIN_CONF="$domainhome/$Le_Domain.conf"
fi
+
if [ -z "$CSR_PATH" ] ; then
- CSR_PATH="$WORKING_DIR/$domain/$domain.csr"
+ CSR_PATH="$domainhome/$domain.csr"
fi
if [ -z "$CERT_KEY_PATH" ] ; then
- CERT_KEY_PATH="$WORKING_DIR/$domain/$domain.key"
+ CERT_KEY_PATH="$domainhome/$domain.key"
fi
if [ -z "$CERT_PATH" ] ; then
- CERT_PATH="$WORKING_DIR/$domain/$domain.cer"
+ CERT_PATH="$domainhome/$domain.cer"
fi
if [ -z "$CA_CERT_PATH" ] ; then
- CA_CERT_PATH="$WORKING_DIR/$domain/ca.cer"
+ CA_CERT_PATH="$domainhome/ca.cer"
fi
-
}
@@ -422,7 +492,7 @@ _clearupwebbroot() {
_debug "remove $__webroot/.well-known/acme-challenge/$3"
rm -rf "$__webroot/.well-known/acme-challenge/$3"
else
- _info "skip for removelevel:$2"
+ _info "Skip for removelevel:$2"
fi
return 0
@@ -488,11 +558,16 @@ issue() {
_err "Please install netcat(nc) tools first."
return 1
fi
-
- netprc="$(ss -ntpl | grep ':80 ')"
+
+ if [ -z "$Le_HTTPPort" ] ; then
+ Le_HTTPPort=80
+ fi
+ _setopt "$DOMAIN_CONF" "Le_HTTPPort" "=" "$Le_HTTPPort"
+
+ netprc="$(ss -ntpl | grep :$Le_HTTPPort" ")"
if [ "$netprc" ] ; then
_err "$netprc"
- _err "tcp port 80 is already used by $(echo "$netprc" | cut -d : -f 4)"
+ _err "tcp port $Le_HTTPPort is already used by $(echo "$netprc" | cut -d : -f 4)"
_err "Please stop it first"
return 1
fi
@@ -539,7 +614,7 @@ issue() {
_debug HEADER "$HEADER"
accountkey_json=$(echo -n "$jwk" | sed "s/ //g")
- thumbprint=$(echo -n "$accountkey_json" | openssl sha -sha256 -binary | _base64 | _b64)
+ thumbprint=$(echo -n "$accountkey_json" | openssl dgst -sha256 -binary | _base64 | _b64)
_info "Registering account"
@@ -551,7 +626,7 @@ issue() {
if [ "$code" == "" ] || [ "$code" == '201' ] ; then
_info "Registered"
- echo $response > $WORKING_DIR/account.json
+ echo $response > $LE_WORKING_DIR/account.json
elif [ "$code" == '409' ] ; then
_info "Already registered"
else
@@ -616,12 +691,49 @@ issue() {
_debug txt "$txt"
#dns
#1. check use api
- _err "Add the following TXT record:"
- _err "Domain: $txtdomain"
- _err "TXT value: $txt"
- _err "Please be aware that you prepend _acme-challenge. before your domain"
- _err "so the resulting subdomain will be: $txtdomain"
- #dnsadded='1'
+ d_api=""
+ if [ -f "$LE_WORKING_DIR/$d/$Le_Webroot" ] ; then
+ d_api="$LE_WORKING_DIR/$d/$Le_Webroot"
+ elif [ -f "$LE_WORKING_DIR/$d/$Le_Webroot.sh" ] ; then
+ d_api="$LE_WORKING_DIR/$d/$Le_Webroot.sh"
+ elif [ -f "$LE_WORKING_DIR/$Le_Webroot" ] ; then
+ d_api="$LE_WORKING_DIR/$Le_Webroot"
+ elif [ -f "$LE_WORKING_DIR/$Le_Webroot.sh" ] ; then
+ d_api="$LE_WORKING_DIR/$Le_Webroot.sh"
+ elif [ -f "$LE_WORKING_DIR/dnsapi/$Le_Webroot" ] ; then
+ d_api="$LE_WORKING_DIR/dnsapi/$Le_Webroot"
+ elif [ -f "$LE_WORKING_DIR/dnsapi/$Le_Webroot.sh" ] ; then
+ d_api="$LE_WORKING_DIR/dnsapi/$Le_Webroot.sh"
+ fi
+ _debug d_api "$d_api"
+
+ if [ "$d_api" ]; then
+ _info "Found domain api file: $d_api"
+ else
+ _err "Add the following TXT record:"
+ _err "Domain: $txtdomain"
+ _err "TXT value: $txt"
+ _err "Please be aware that you prepend _acme-challenge. before your domain"
+ _err "so the resulting subdomain will be: $txtdomain"
+ continue
+ fi
+
+ if ! source $d_api ; then
+ _err "Load file $d_api error. Please check your api file and try again."
+ return 1
+ fi
+
+ addcommand="$Le_Webroot-add"
+ if ! command -v $addcommand ; then
+ _err "It seems that your api file is not correct, it must have a function named: $Le_Webroot"
+ return 1
+ fi
+
+ if ! $addcommand $txtdomain $txt ; then
+ _err "Error add txt for domain:$txtdomain"
+ return 1
+ fi
+ dnsadded='1'
fi
done
@@ -634,6 +746,10 @@ issue() {
fi
+ if [ "$dnsadded" == '1' ] ; then
+ _info "Sleep 60 seconds for the txt records to take effect"
+ sleep 60
+ fi
_debug "ok, let's start to verify"
ventries=$(echo "$vlist" | sed "s/,/ /g")
@@ -754,7 +870,7 @@ issue() {
if [ -z "$Le_LinkCert" ] ; then
- response="$(echo $response | openssl base64 -d)"
+ response="$(echo $response | openssl base64 -d -A)"
_err "Sign failed: $(echo "$response" | grep -o '"detail":"[^"]*"')"
return 1
fi
@@ -803,23 +919,30 @@ renew() {
_initpath $Le_Domain
- if [ -f "$DOMAIN_CONF" ] ; then
- source "$DOMAIN_CONF"
- if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(date -u "+%s" )" -lt "$Le_NextRenewTime" ] ; then
- _info "Skip, Next renewal time is: $Le_NextRenewTimeStr"
- return 2
- fi
+ if [ ! -f "$DOMAIN_CONF" ] ; then
+ _info "$Le_Domain is not a issued domain, skip."
+ return 0;
fi
+
+ source "$DOMAIN_CONF"
+ if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(date -u "+%s" )" -lt "$Le_NextRenewTime" ] ; then
+ _info "Skip, Next renewal time is: $Le_NextRenewTimeStr"
+ return 2
+ fi
+
IS_RENEW="1"
issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd"
+ local res=$?
IS_RENEW=""
+
+ return $res
}
renewAll() {
_initpath
_info "renewAll"
- for d in $(ls -F $WORKING_DIR | grep '/$') ; do
+ for d in $(ls -F $LE_WORKING_DIR | grep [^.].*[.].*/$ ) ; do
d=$(echo $d | cut -d '/' -f 1)
_info "renew $d"
@@ -914,13 +1037,13 @@ installcronjob() {
_initpath
_info "Installing cron job"
if ! crontab -l | grep 'le.sh cron' ; then
- if [ -f "$WORKING_DIR/le.sh" ] ; then
- lesh="\"$WORKING_DIR\"/le.sh"
+ if [ -f "$LE_WORKING_DIR/le.sh" ] ; then
+ lesh="\"$LE_WORKING_DIR\"/le.sh"
else
_err "Can not install cronjob, le.sh not found."
return 1
fi
- crontab -l | { cat; echo "0 0 * * * $SUDO WORKING_DIR=\"$WORKING_DIR\" $lesh cron > /dev/null"; } | crontab -
+ crontab -l | { cat; echo "0 0 * * * $SUDO LE_WORKING_DIR=\"$LE_WORKING_DIR\" $lesh cron > /dev/null"; } | crontab -
fi
return 0
}
@@ -930,13 +1053,89 @@ uninstallcronjob() {
cr="$(crontab -l | grep 'le.sh cron')"
if [ "$cr" ] ; then
crontab -l | sed "/le.sh cron/d" | crontab -
- WORKING_DIR="$(echo "$cr" | cut -d ' ' -f 7 | cut -d '=' -f 2 | tr -d '"')"
- _info WORKING_DIR "$WORKING_DIR"
+ LE_WORKING_DIR="$(echo "$cr" | cut -d ' ' -f 7 | cut -d '=' -f 2 | tr -d '"')"
+ _info LE_WORKING_DIR "$LE_WORKING_DIR"
fi
_initpath
}
+
+# Detect profile file if not specified as environment variable
+_detect_profile() {
+ if [ -n "$PROFILE" -a -f "$PROFILE" ]; then
+ echo "$PROFILE"
+ return
+ fi
+
+ local DETECTED_PROFILE
+ DETECTED_PROFILE=''
+ local SHELLTYPE
+ SHELLTYPE="$(basename "/$SHELL")"
+
+ if [ "$SHELLTYPE" = "bash" ]; then
+ if [ -f "$HOME/.bashrc" ]; then
+ DETECTED_PROFILE="$HOME/.bashrc"
+ elif [ -f "$HOME/.bash_profile" ]; then
+ DETECTED_PROFILE="$HOME/.bash_profile"
+ fi
+ elif [ "$SHELLTYPE" = "zsh" ]; then
+ DETECTED_PROFILE="$HOME/.zshrc"
+ fi
+
+ if [ -z "$DETECTED_PROFILE" ]; then
+ if [ -f "$HOME/.profile" ]; then
+ DETECTED_PROFILE="$HOME/.profile"
+ elif [ -f "$HOME/.bashrc" ]; then
+ DETECTED_PROFILE="$HOME/.bashrc"
+ elif [ -f "$HOME/.bash_profile" ]; then
+ DETECTED_PROFILE="$HOME/.bash_profile"
+ elif [ -f "$HOME/.zshrc" ]; then
+ DETECTED_PROFILE="$HOME/.zshrc"
+ fi
+ fi
+
+ if [ ! -z "$DETECTED_PROFILE" ]; then
+ echo "$DETECTED_PROFILE"
+ fi
+}
+
+_initconf() {
+ _initpath
+ if [ ! -f "$ACCOUNT_CONF_PATH" ] ; then
+ echo "#Account configurations:
+#Here are the supported macros, uncomment them to make them take effect.
+#ACCOUNT_EMAIL=aaa@aaa.com # the account email used to register account.
+
+#STAGE=1 # Use the staging api
+#FORCE=1 # Force to issue cert
+#DEBUG=1 # Debug mode
+
+#dns api
+#######################
+#Cloudflare:
+#api key
+#CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+#account email
+#CF_Email="xxxx@sss.com"
+
+#######################
+#Dnspod.cn:
+#api key id
+#DP_Id="1234"
+#api key
+#DP_Key="sADDsdasdgdsf"
+
+#######################
+#Cloudxns.com:
+#CX_Key="1234"
+#
+#CX_Secret="sADDsdasdgdsf"
+
+ " > $ACCOUNT_CONF_PATH
+ fi
+}
+
install() {
_initpath
@@ -969,29 +1168,40 @@ install() {
return 1
fi
- _info "Installing to $WORKING_DIR"
+ _info "Installing to $LE_WORKING_DIR"
- #try install to /bin if is root
- if [ ! -f /usr/local/bin/le.sh ] ; then
- #if root
- if $SUDO cp le.sh /usr/local/bin/le.sh > /dev/null 2>&1; then
- $SUDO chmod 755 /usr/local/bin/le.sh
- $SUDO ln -s "/usr/local/bin/le.sh" /usr/local/bin/le
- rm -f $WORKING_DIR/le.sh
- $SUDO ln -s /usr/local/bin/le.sh $WORKING_DIR/le.sh
- _info "Installed to /usr/local/bin/le"
- else
- #install to home, for non root user
- cp le.sh $WORKING_DIR/
- chmod +x $WORKING_DIR/le.sh
- _info "Installed to $WORKING_DIR/le.sh"
- fi
+ _info "Installed to $LE_WORKING_DIR/le.sh"
+ cp le.sh $LE_WORKING_DIR/
+ chmod +x $LE_WORKING_DIR/le.sh
+
+ _profile="$(_detect_profile)"
+ if [ "$_profile" ] ; then
+ _debug "Found profile: $_profile"
+
+ echo "LE_WORKING_DIR=$LE_WORKING_DIR
+alias le=\"$LE_WORKING_DIR/le.sh\"
+alias le.sh=\"$LE_WORKING_DIR/le.sh\"
+ " > "$LE_WORKING_DIR/le.env"
+
+ _setopt "$_profile" "source \"$LE_WORKING_DIR/le.env\""
+ _info "OK, Close and reopen your terminal to start using le"
+ else
+ _info "No profile is found, you will need to go into $LE_WORKING_DIR to use le.sh"
fi
- rm -f $WORKING_DIR/le
- ln -s $WORKING_DIR/le.sh $WORKING_DIR/le
+ mkdir -p $LE_WORKING_DIR/dnsapi
+ cp dnsapi/* $LE_WORKING_DIR/dnsapi/
+
+ #to keep compatible mv the .acc file to .key file
+ if [ -f "$LE_WORKING_DIR/account.acc" ] ; then
+ mv "$LE_WORKING_DIR/account.acc" "$LE_WORKING_DIR/account.key"
+ fi
+
installcronjob
+ if [ ! -f "$ACCOUNT_CONF_PATH" ] ; then
+ _initconf
+ fi
_info OK
}
@@ -999,15 +1209,13 @@ uninstall() {
uninstallcronjob
_initpath
- if [ -f "/usr/local/bin/le.sh" ] ; then
- _info "Removing /usr/local/bin/le.sh"
- if $SUDO rm -f /usr/local/bin/le.sh ; then
- $SUDO rm -f /usr/local/bin/le
- fi
+ _profile="$(_detect_profile)"
+ if [ "$_profile" ] ; then
+ sed -i /le.env/d "$_profile"
fi
- rm -f $WORKING_DIR/le
- rm -f $WORKING_DIR/le.sh
- _info "The keys and certs are in $WORKING_DIR, you can remove them by yourself."
+
+ rm -f $LE_WORKING_DIR/le.sh
+ _info "The keys and certs are in $LE_WORKING_DIR, you can remove them by yourself."
}