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." }