diff --git a/README.md b/README.md index 071680a7..d9447fea 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,27 @@ -# le +# le: means simp`Le` Simplest shell script for LetsEncrypt free Certificate client -This is a shell version from https://github.com/diafygi/acme-tiny +Simple and Powerful, you only need 3 minutes to learn. -Pure written in bash, no dependencies to python , acme-tiny or LetsEncrypt official client (https://github.com/letsencrypt/letsencrypt) - -Just one script, to issue, renew your certiricates automatically. +Pure written in bash, no dependencies to python , acme-tiny or LetsEncrypt official client. +Just one script, to issue, renew your certificates automatically. Probably it's the smallest&easiest&smartest shell script to automatically issue&renew the free certificates from LetsEncrypt. +Do NOT require to be `root/sudoer`. + +#Tested OS +1. Ubuntu/Debian. +2. CentOS +3. Windows (cygwin with curl, openssl and crontab included) +4. FreeBSD with bash -#Supported OS -1. Tested on Ubuntu/Debian. -2. CentOS is Not tested yet, It should work. +#Supported Mode +1. Webroot mode +2. Standalone mode +3. Apache mode +4. Dns mode #How to use @@ -23,64 +31,199 @@ Probably it's the smallest&easiest&smartest shell script to automatically issue ``` ./le.sh install ``` -Which does 2 things: -* create and copy le.sh to your home dir: `~/.le` +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: `/bin/le -> ~/.le/le.sh` +* 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. -3. Ok, you are ready to issue cert now. +Ok, you are ready to issue cert now. Show help message: ``` -root@xvm:~# le -Usage: issue|renew|renewAll|createAccountKey|createDomainKey|createCSR|install|uninstall +root@v1:~# le.sh +https://github.com/Neilpang/le +v1.1.1 +Usage: le.sh [command] ...[args].... +Available commands: + +install: + Install le.sh to your system. +issue: + Issue a cert. +installcert: + Install the issued cert to apache/nginx or any other server. +renew: + Renew a cert. +renewAll: + Renew all the certs. +uninstall: + Uninstall le.sh, and uninstall the cron job. +version: + Show version info. +installcronjob: + Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job. +uninstallcronjob: + Uninstall the cron job. The 'uninstall' command can do this automatically. +createAccountKey: + Create an account private key, professional use. +createDomainKey: + Create an domain private key, professional use. +createCSR: + Create CSR , professional use. + + +root@v1:~/le# le issue +Usage: le issue webroot|no|apache|dns a.com [www.a.com,b.com,c.com]|no [key-length]|no -root@xvm:~# le issue -Usage: /bin/le webroot a.com [www.a.com,b.com,c.com] [key-length] [cert-file-path] [key-file-path] [reloadCmd] ``` + +Set the param value to "no" means you want to ignore it. + +For example, if you give "no" to "key-length", it will use default length 2048. + +And if you give 'no' to 'cert-file-path', it will not copy the issued cert to the "cert-file-path". + +In all the cases, the issued cert will be placed in "~/.le/domain.com/" + # Just issue a cert: ``` le issue /home/wwwroot/aa.com aa.com www.aa.com,cp.aa.com ``` -First argument " /home/wwwroot/aa.com" is the web root folder +First argument `/home/wwwroot/aa.com` is the web root folder, You must have `write` access to this folder. -Second argument "aa.com" is the domain you want to issue cert for. +Second argument "aa.com" is the main domain you want to issue cert for. -Third argument is the additional domain list you want to use. Comma sperated list, Optional. +Third argument is the additional domain list you want to use. Comma separated list, which is Optional. -'You must point and bind all the domains to the same webroot dir:/home/wwwroot/aa.com' +You must point and bind all the domains to the same webroot dir:`/home/wwwroot/aa.com` The cert will be placed in `~/.le/aa.com/` +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/apache/nginx "service apache2|nginx reload" +``` + +Install the issued cert/key to the production apache or nginx path. -The issued cert will be renewed every 50 days automatically. +The cert will be renewed every 80 days by default (which is configurable), Once the cert is renewed, the apache/nginx will be automatically reloaded by the command: ` service apache2 reload` or `service nginx reload` -# Issue a cert, and install to apache +# Use Standalone server to issue cert( requires you be root/sudoer, or you have permission to listen tcp 80 port): +Same usage as all above, just give `no` as the webroot. +The tcp `80` port must be free to listen, otherwise you will be prompted to free the `80` port and try again. + ``` -le issue /home/wwwroot/aa.com aa.com www.aa.com,cp.aa.com 2048 /path/to/certfile/in/apache/nginx /path/to/keyfile/in/apache/nginx "service apache2 reload" +le issue no aa.com www.aa.com,cp.aa.com ``` -This can link the issued cert to the production apache or nginx path. -Once the cert is renewed, the apache/nginx will be automatically reloaded by the command: ` service apache2 reload` +# Use Apache mode(requires you be root/sudoer, since it is required to interact with apache server): +If you are running a web server, apache or nginx, it is recommended to use the Webroot mode. +Particularly, if you are running an apache server, you can use apache mode instead. Which doesn't write any file to your web root folder. + +Just set string "apache" to the first argument, it will use apache plugin automatically. + +``` +le issue apache aa.com www.aa.com,user.aa.com +``` +All the other arguments are the same with previous. + + +# Use DNS mode: +Support the latest dns-01 challenge. + +``` +le issue dns aa.com www.aa.com,user.aa.com +``` + +You will get the output like bellow: +``` +Add the following txt record: +Domain:_acme-challenge.aa.com +Txt value:9ihDbjYfTExAYeDs4DBUeuTo18KBzwvTEjUnSwd32-c + +Add the following txt record: +Domain:_acme-challenge.www.aa.com +Txt value:9ihDbjxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +Please add those txt records to the domains. Waiting for the dns to take effect. + +Then just retry with 'renew' command: + +``` +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 the `length` parameter 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", which is not supported by letsencrypt yet.) #Under the Hood -Use bash to say ACME language directly to Let's encrypt. +Speak ACME language with bash directly to Let's encrypt. TODO: +#Acknowledgment +1. Acme-tiny: https://github.com/diafygi/acme-tiny +2. ACME protocol: https://github.com/ietf-wg-acme/acme +3. letsencrypt: https://github.com/letsencrypt/letsencrypt + + + #License & Other 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..159969d2 --- /dev/null +++ b/dnsapi/dns-cf.sh @@ -0,0 +1,171 @@ +#!/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 _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _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\":\"[^\"]*\" | head -1 | 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 d523b60f..bb99dfd6 100755 --- a/le.sh +++ b/le.sh @@ -1,105 +1,163 @@ -#!/bin/bash +#!/usr/bin/env bash +VER=1.1.8 +PROJECT="https://github.com/Neilpang/le" - -WORKING_DIR=~/.le - -ACCOUNT_KEY_PATH=$WORKING_DIR/account.acc - -CERT_KEY_PATH=$WORKING_DIR/domain.key - -CSR_PATH=$WORKING_DIR/domain.csr - -CERT_PATH=$WORKING_DIR/domain.cer - -DOMAIN_CONF=$WORKING_DIR/domain.conf - -CURL_HEADER="" - -HEADER="" -HEADERPLACE="" - -ACCOUNT_EMAIL="" DEFAULT_CA="https://acme-v01.api.letsencrypt.org" +DEFAULT_AGREEMENT="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf" -API=$DEFAULT_CA +STAGE_CA="https://acme-staging.api.letsencrypt.org" -DEBUG= +VTYPE_HTTP="http-01" +VTYPE_DNS="dns-01" + +if [ -z "$AGREEMENT" ] ; then + AGREEMENT="$DEFAULT_AGREEMENT" +fi _debug() { - if ! [ "DEBUG" ] ; then + + if [ -z "$DEBUG" ] ; then return fi if [ -z "$2" ] ; then echo $1 else - echo $1:$2 + echo "$1"="$2" fi } _info() { if [ -z "$2" ] ; then - echo $1 + echo "$1" else - echo $1:$2 + echo "$1"="$2" fi } +_err() { + if [ -z "$2" ] ; then + echo "$1" >&2 + else + echo "$1"="$2" >&2 + fi + return 1 +} + +_h2b() { + hex=$(cat) + i=1 + j=2 + while [ '1' ] ; do + h=$(printf $hex | cut -c $i-$j) + if [ -z "$h" ] ; then + break; + fi + printf "\x$h" + let "i+=2" + let "j+=2" + done +} + +_base64() { + openssl base64 -e | tr -d '\n' +} + #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 [ -z "$2" ] ; then - echo Use default length 2048 + + if [[ "$length" == "ec-"* ]] ; then length=2048 fi - mkdir -p $WORKING_DIR - ACCOUNT_KEY_PATH=$WORKING_DIR/account.acc + if [ -z "$2" ] ; then + _info "Use default length 2048" + length=2048 + fi + _initpath if [ -f "$ACCOUNT_KEY_PATH" ] ; then - echo account key exists, skip + _info "Account key exists, skip" return else #generate account key - openssl genrsa $length > $ACCOUNT_KEY_PATH + openssl genrsa $length > "$ACCOUNT_KEY_PATH" fi } #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 - echo Use default length 2048 - length=2048 + isec="" + if [[ "$length" == "ec-"* ]] ; then + isec="1" + length=$(printf $length | cut -d '-' -f 2-100) + eccname="$length" fi - mkdir -p $WORKING_DIR/$domain - CERT_KEY_PATH=$WORKING_DIR/$domain/$domain.key + 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 - if [ -f "$CERT_KEY_PATH" ] ; then - echo domain key exists, skip - else + _initpath $domain + + if [ ! -f "$CERT_KEY_PATH" ] || ( [ "$FORCE" ] && ! [ "$IS_RENEW" ] ); then #generate account key - openssl genrsa $length > $CERT_KEY_PATH + 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 + else + _err "Domain key exists, do you want to overwrite the key?" + _err "Set FORCE=1, and try again." + return 1 + fi fi } # domain domainlist createCSR() { + _info "Creating csr" if [ -z "$1" ] ; then echo Usage: $0 domain [domainlist] return @@ -109,92 +167,99 @@ createCSR() { domainlist=$2 - if [ -f $CSR_PATH ] ; then - echo CSR exists, skip + if [ -f "$CSR_PATH" ] && [ "$IS_RENEW" ] && ! [ "$FORCE" ]; then + _info "CSR exists, skip" return fi if [ -z "$domainlist" ] ; then #single domain - echo single domain - openssl req -new -sha256 -key $CERT_KEY_PATH -subj "/CN=$domain" > $CSR_PATH + _info "Single domain" $domain + openssl req -new -sha256 -key "$CERT_KEY_PATH" -subj "/CN=$domain" > "$CSR_PATH" else - alt=DNS:$(echo $domainlist | sed "s/,/,DNS:/g") + alt="DNS:$(echo $domainlist | sed "s/,/,DNS:/g")" #multi - echo multi domain $alt - openssl req -new -sha256 -key $CERT_KEY_PATH -subj "/CN=$domain" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=$alt")) -out $CSR_PATH + _info "Multi domain" "$alt" + printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\n[SAN]\nsubjectAltName=$alt" > "$DOMAIN_SSL_CONF" + openssl req -new -sha256 -key "$CERT_KEY_PATH" -subj "/CN=$domain" -reqexts SAN -config "$DOMAIN_SSL_CONF" -out "$CSR_PATH" fi } _b64() { - while read __line; do - __n=$__n$__line - done; - __n=$(echo $__n | sed "s|/|_|g") - __n=$(echo $__n | sed "s| ||g") - __n=$(echo $__n | sed "s|+|-|g") - __n=$(echo $__n | sed "s|=||g") - echo $__n + __n=$(cat) + echo $__n | tr '/+' '_-' | tr -d '= ' +} + +_time2str() { + #BSD + if date -u -d@$1 2>/dev/null ; then + return + fi + + #Linux + if date -u -r $1 2>/dev/null ; then + return + fi + } _send_signed_request() { url=$1 payload=$2 + needbase64=$3 - needbas64="$3" + _debug url $url + _debug payload "$payload" - _info url $url - _info 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 + if [ "$DEBUG" ] ; then CURL="$CURL --trace-ascii $dp " fi - payload64=$(echo -n $payload | base64 | _b64) + payload64=$(echo -n $payload | _base64 | _b64) _debug payload64 $payload64 nonceurl="$API/directory" - nonce=$($CURL -I $nonceurl | grep "^Replay-Nonce:" | sed s/\\r//|sed s/\\n//| cut -d ' ' -f 2) + nonce="$($CURL -I $nonceurl | grep -o "^Replay-Nonce:.*$" | tr -d "\r\n" | cut -d ' ' -f 2)" - _debug nonce $nonce + _debug nonce "$nonce" - protected=$(echo -n "$HEADERPLACE" | sed "s/NONCE/$nonce/" ) + protected="$(printf "$HEADERPLACE" | sed "s/NONCE/$nonce/" )" _debug protected "$protected" - protected64=$( echo -n $protected | base64 | _b64) + protected64="$(printf "$protected" | _base64 | _b64)" _debug protected64 "$protected64" - sig=$(echo -n "$protected64.$payload64" | openssl dgst -sha256 -sign $ACCOUNT_KEY_PATH | base64| _b64) + sig=$(echo -n "$protected64.$payload64" | openssl dgst -sha256 -sign $ACCOUNT_KEY_PATH | _base64 | _b64) _debug sig "$sig" body="{\"header\": $HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" _debug body "$body" - - if [ "$needbas64" ] ; then - response=$($CURL -X POST --data "$body" $url | base64) + if [ "$needbase64" ] ; then + response="$($CURL -X POST --data "$body" $url | _base64)" else - response=$($CURL -X POST --data "$body" $url) + response="$($CURL -X POST --data "$body" $url)" fi + responseHeaders="$(cat $CURL_HEADER)" _debug responseHeaders "$responseHeaders" _debug response "$response" - code=$(grep ^HTTP $CURL_HEADER | tail -1 | cut -d " " -f 2) + code="$(grep ^HTTP $CURL_HEADER | tail -1 | cut -d " " -f 2 | tr -d "\r\n" )" _debug code $code } _get() { url="$1" - _info url $url - response=$(curl --silent $url) + _debug url $url + response="$(curl --silent $url)" ret=$? _debug response "$response" - code=$(echo $response | grep -o '"status":[0-9]\+' | cut -d : -f 2) + code="$(echo $response | grep -o '"status":[0-9]\+' | cut -d : -f 2)" _debug code $code return $ret } @@ -210,79 +275,337 @@ _setopt() { echo usage: $0 '"file" "opt" "=" "value" [";"]' return fi + if [ ! -f "$__conf" ] ; then + touch "$__conf" + fi + if grep -H -n "^$__opt$__sep" "$__conf" > /dev/null ; then + _debug OK + if [[ "$__val" == *"&"* ]] ; then + __val="$(echo $__val | sed 's/&/\\&/g')" + fi + text="$(cat $__conf)" + printf "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" > "$__conf" + else + _debug APP + echo "$__opt$__sep$__val$__end" >> "$__conf" + fi + _debug "$(grep -H -n "^$__opt$__sep" $__conf)" +} - if grep -H -n "^$__opt$__sep" $__conf ; then - echo OK - sed -i "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" $__conf +#_savedomainconf key value +#save to domain.conf +_savedomainconf() { + key="$1" + value="$2" + if [ "$DOMAIN_CONF" ] ; then + _setopt $DOMAIN_CONF "$key" "=" "$value" else - echo APP - echo "$__opt$__sep$__val$__end" >> $__conf + _err "DOMAIN_CONF is empty, can not save $key=$value" fi - grep -H -n "^$__opt$__sep" $__conf +} + +#_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" + if nc -h 2>&1 | grep "nmap.org/ncat" >/dev/null ; then + _NC="nc" + fi +# while true ; do + if [ "$DEBUG" ] ; then + 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 $Le_HTTPPort > /dev/null + fi +# done +} + +_stopserver() { + pid="$1" + } _initpath() { - WORKING_DIR=~/.le - domain=$1 - mkdir -p $WORKING_DIR - ACCOUNT_KEY_PATH=$WORKING_DIR/account.acc + + 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 + API="$DEFAULT_CA" + else + API="$STAGE_CA" + _info "Using stage api:$API" + fi + fi + + if [ -z "$ACME_DIR" ] ; then + ACME_DIR="/home/.acme" + fi + + if [ -z "$APACHE_CONF_BACKUP_DIR" ] ; then + APACHE_CONF_BACKUP_DIR="$LE_WORKING_DIR/" + fi + domain="$1" + mkdir -p "$LE_WORKING_DIR" + + if [ -z "$ACCOUNT_KEY_PATH" ] ; then + 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="$domainhome/$Le_Domain.conf" + fi - if [ -z "$CSR_PATH" ] ; then - CSR_PATH=$WORKING_DIR/$domain/$domain.csr + if [ -z "$DOMAIN_SSL_CONF" ] ; then + DOMAIN_SSL_CONF="$domainhome/$Le_Domain.ssl.conf" fi + if [ -z "$CSR_PATH" ] ; then + 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="$domainhome/ca.cer" + fi + +} + + +_apachePath() { + httpdroot="$(apachectl -V | grep HTTPD_ROOT= | cut -d = -f 2 | tr -d '"' )" + httpdconfname="$(apachectl -V | grep SERVER_CONFIG_FILE= | cut -d = -f 2 | tr -d '"' )" + httpdconf="$httpdroot/$httpdconfname" + if [ ! -f $httpdconf ] ; then + _err "Apache Config file not found" $httpdconf + return 1 + fi + return 0 +} + +_restoreApache() { + if [ -z "$usingApache" ] ; then + return 0 + fi + _initpath + if ! _apachePath ; then + return 1 fi + if [ ! -f "$APACHE_CONF_BACKUP_DIR/$httpdconfname" ] ; then + _debug "No config file to restore." + return 0 + fi + + cp -p "$APACHE_CONF_BACKUP_DIR/$httpdconfname" "$httpdconf" + if ! apachectl -t ; then + _err "Sorry, restore apache config error, please contact me." + return 1; + fi + rm -f "$APACHE_CONF_BACKUP_DIR/$httpdconfname" + return 0 } -#issue webroot a.com [www.a.com,b.com,c.com] [key-length] [cert-file-path] [key-file-path] [reloadCmd] -issue() { - if [ -z "$1" ] ; then - echo "Usage: $0 webroot a.com [www.a.com,b.com,c.com] [key-length] [cert-file-path] [key-file-path] [reloadCmd]" +_setApache() { + _initpath + if ! _apachePath ; then return 1 fi - Le_Webroot=$1 - Le_Domain=$2 - Le_Alt=$3 - Le_Keylength=$4 + + #backup the conf + _debug "Backup apache config file" $httpdconf + cp -p $httpdconf $APACHE_CONF_BACKUP_DIR/ + _info "JFYI, Config file $httpdconf is backuped to $APACHE_CONF_BACKUP_DIR/$httpdconfname" + _info "In case there is an error that can not be restored automatically, you may try restore it yourself." + _info "The backup file will be deleted on sucess, just forget it." + + #add alias + echo " +Alias /.well-known/acme-challenge $ACME_DIR + + +Require all granted + + " >> $httpdconf + + if ! apachectl -t ; then + _err "Sorry, apache config error, please contact me." + _restoreApache + return 1; + fi - if [ -z "$Le_Domain" ] ; then - Le_Domain="$1" + if [ ! -d "$ACME_DIR" ] ; then + mkdir -p "$ACME_DIR" + chmod 755 "$ACME_DIR" fi + if ! apachectl graceful ; then + _err "Sorry, apachectl graceful error, please contact me." + _restoreApache + return 1; + fi + usingApache="1" + return 0 +} + +_clearup () { + _stopserver $serverproc + serverproc="" + _restoreApache +} + +# webroot removelevel tokenfile +_clearupwebbroot() { + __webroot="$1" + if [ -z "$__webroot" ] ; then + _debug "no webroot specified, skip" + return 0 + fi + + if [ "$2" == '1' ] ; then + _debug "remove $__webroot/.well-known" + rm -rf "$__webroot/.well-known" + elif [ "$2" == '2' ] ; then + _debug "remove $__webroot/.well-known/acme-challenge" + rm -rf "$__webroot/.well-known/acme-challenge" + elif [ "$2" == '3' ] ; then + _debug "remove $__webroot/.well-known/acme-challenge/$3" + rm -rf "$__webroot/.well-known/acme-challenge/$3" + else + _info "Skip for removelevel:$2" + fi + + return 0 + +} + +issue() { + if [ -z "$2" ] ; then + _err "Usage: le issue webroot|no|apache|dns a.com [www.a.com,b.com,c.com]|no [key-length]|no" + return 1 + fi + Le_Webroot="$1" + Le_Domain="$2" + Le_Alt="$3" + Le_Keylength="$4" + Le_RealCertPath="$5" + Le_RealKeyPath="$6" + Le_RealCACertPath="$7" + Le_ReloadCmd="$8" + + _initpath $Le_Domain - DOMAIN_CONF=$WORKING_DIR/$Le_Domain/$Le_Domain.conf if [ -f "$DOMAIN_CONF" ] ; then - source "$DOMAIN_CONF" - if [ "$(date -u "+%s" )" -lt "$Le_NextRenewTime" ] ; then - _info "Skip, Next renwal time is: $Le_NextRenewTimeStr" + Le_NextRenewTime=$(grep "^Le_NextRenewTime=" "$DOMAIN_CONF" | cut -d '=' -f 2) + if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(date -u "+%s" )" -lt "$Le_NextRenewTime" ] ; then + _info "Skip, Next renewal time is: $(grep "^Le_NextRenewTimeStr" "$DOMAIN_CONF" | cut -d '=' -f 2)" return 2 fi fi - if [ -z "$Le_Webroot" ] ; then - echo Usage: $0 webroot a.com [b.com,c.com] [key-length] - return 1 + if [ "$Le_Alt" == "no" ] ; then + Le_Alt="" + fi + if [ "$Le_Keylength" == "no" ] ; then + Le_Keylength="" + fi + if [ "$Le_RealCertPath" == "no" ] ; then + Le_RealCertPath="" + fi + if [ "$Le_RealKeyPath" == "no" ] ; then + Le_RealKeyPath="" + fi + if [ "$Le_RealCACertPath" == "no" ] ; then + Le_RealCACertPath="" + fi + if [ "$Le_ReloadCmd" == "no" ] ; then + Le_ReloadCmd="" + fi + + _setopt "$DOMAIN_CONF" "Le_Domain" "=" "$Le_Domain" + _setopt "$DOMAIN_CONF" "Le_Alt" "=" "$Le_Alt" + _setopt "$DOMAIN_CONF" "Le_Webroot" "=" "$Le_Webroot" + _setopt "$DOMAIN_CONF" "Le_Keylength" "=" "$Le_Keylength" + _setopt "$DOMAIN_CONF" "Le_RealCertPath" "=" "\"$Le_RealCertPath\"" + _setopt "$DOMAIN_CONF" "Le_RealCACertPath" "=" "\"$Le_RealCACertPath\"" + _setopt "$DOMAIN_CONF" "Le_RealKeyPath" "=" "\"$Le_RealKeyPath\"" + _setopt "$DOMAIN_CONF" "Le_ReloadCmd" "=" "\"$Le_ReloadCmd\"" + + if [ "$Le_Webroot" == "no" ] ; then + _info "Standalone mode." + if ! command -v "nc" > /dev/null ; then + _err "Please install netcat(nc) tools first." + return 1 + fi + + 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 $Le_HTTPPort is already used by $(echo "$netprc" | cut -d : -f 4)" + _err "Please stop it first" + return 1 + fi + fi + + if [ "$Le_Webroot" == "apache" ] ; then + if ! _setApache ; then + _err "set up apache error. Report error to me." + return 1 + fi + wellknown_path="$ACME_DIR" + else + usingApache="" fi createAccountKey $Le_Domain $Le_Keylength - createDomainKey $Le_Domain $Le_Keylength + if ! createDomainKey $Le_Domain $Le_Keylength ; then + _err "Create domain key error." + return 1 + fi - createCSR $Le_Domain $Le_Alt + if ! createCSR $Le_Domain $Le_Alt ; then + _err "Create CSR error." + return 1 + fi 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) if [ "${#pub_exp}" == "5" ] ; then @@ -290,11 +613,11 @@ issue() { fi _debug pub_exp "$pub_exp" - e=$(echo $pub_exp | xxd -r -p | base64) + e=$(echo $pub_exp | _h2b | _base64) _debug e "$e" modulus=$(openssl rsa -in $ACCOUNT_KEY_PATH -modulus -noout | cut -d '=' -f 2 ) - n=$(echo $modulus| xxd -r -p | base64 | _b64 ) + n=$(echo $modulus| _h2b | _base64 | _b64 ) jwk='{"e": "'$e'", "kty": "RSA", "n": "'$n'"}' @@ -302,69 +625,197 @@ issue() { HEADERPLACE='{"nonce": "NONCE", "alg": "RS256", "jwk": '$jwk'}' _debug HEADER "$HEADER" - accountkey_json=$(echo -n "$jwk" | sed "s/ //g") - thumbprint=$(echo -n "$accountkey_json" | sha256sum | xxd -r -p | base64 | _b64) + accountkey_json=$(echo -n "$jwk" | tr -d ' ' ) + thumbprint=$(echo -n "$accountkey_json" | openssl dgst -sha256 -binary | _base64 | _b64) _info "Registering account" - regjson='{"resource": "new-reg", "agreement": "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"}' + regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}' if [ "$ACCOUNT_EMAIL" ] ; then - regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"}' + regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}' fi _send_signed_request "$API/acme/new-reg" "$regjson" 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 - _info "Register account Error." + _err "Register account Error." + _clearup return 1 fi - # verify each domain - _info "verify each domain" + vtype="$VTYPE_HTTP" + if [[ "$Le_Webroot" == "dns"* ]] ; then + vtype="$VTYPE_DNS" + fi - alldomains=$(echo "$Le_Domain,$Le_Alt" | sed "s/,/ /g") - for d in $alldomains - do - _info "Verifing domain $d" - - _send_signed_request "$API/acme/new-authz" "{\"resource\": \"new-authz\", \"identifier\": {\"type\": \"dns\", \"value\": \"$d\"}}" - - if [ ! -z "$code" ] && [ ! "$code" == '201' ] ; then - _info "new-authz error: $d" + vlist="$Le_Vlist" + # verify each domain + _info "Verify each domain" + sep='#' + if [ -z "$vlist" ] ; then + alldomains=$(echo "$Le_Domain,$Le_Alt" | tr ',' ' ' ) + for d in $alldomains + do + _info "Geting token for domain" $d + _send_signed_request "$API/acme/new-authz" "{\"resource\": \"new-authz\", \"identifier\": {\"type\": \"dns\", \"value\": \"$d\"}}" + if [ ! -z "$code" ] && [ ! "$code" == '201' ] ; then + _err "new-authz error: $response" + _clearup + return 1 + fi + + entry="$(printf $response | egrep -o '{[^{]*"type":"'$vtype'"[^}]*')" + _debug entry "$entry" + + token="$(printf "$entry" | egrep -o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" + _debug token $token + + uri="$(printf "$entry" | egrep -o '"uri":"[^"]*'| cut -d : -f 2,3 | tr -d '"' )" + _debug uri $uri + + keyauthorization="$token.$thumbprint" + _debug keyauthorization "$keyauthorization" + + dvlist="$d$sep$keyauthorization$sep$uri" + _debug dvlist "$dvlist" + + vlist="$vlist$dvlist," + + done + + #add entry + dnsadded="" + ventries=$(echo "$vlist" | tr ',' ' ' ) + for ventry in $ventries + do + d=$(echo $ventry | cut -d $sep -f 1) + keyauthorization=$(echo $ventry | cut -d $sep -f 2) + + if [ "$vtype" == "$VTYPE_DNS" ] ; then + dnsadded='0' + txtdomain="_acme-challenge.$d" + _debug txtdomain "$txtdomain" + txt="$(echo -e -n $keyauthorization | openssl dgst -sha256 -binary | _base64 | _b64)" + _debug txt "$txt" + #dns + #1. check use api + 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 + + if [ "$dnsadded" == '0' ] ; then + _setopt "$DOMAIN_CONF" "Le_Vlist" "=" "\"$vlist\"" + _debug "Dns record not added yet, so, save to $DOMAIN_CONF and exit." + _err "Please add the TXT records to the domains, and retry again." return 1 fi - http01=$(echo $response | egrep -o '{[^{]*"type":"http-01"[^}]*') - _debug http01 "$http01" - - token=$(echo "$http01" | sed 's/,/\n'/g| grep '"token":'| cut -d : -f 2|sed 's/"//g') - _info token $token - - uri=$(echo "$http01" | sed 's/,/\n'/g| grep '"uri":'| cut -d : -f 2,3|sed 's/"//g') - _info uri $uri - - keyauthorization="$token.$thumbprint" - _debug keyauthorization "$keyauthorization" - - wellknown_path="$Le_Webroot/.well-known/acme-challenge" - _debug wellknown_path "$wellknown_path" - - mkdir -p "$wellknown_path" - wellknown_path="$wellknown_path/$token" - echo -n "$keyauthorization" > $wellknown_path - - wellknown_url="http://$d/.well-known/acme-challenge/$token" - _debug wellknown_url "$wellknown_url" + 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" | tr ',' ' ' ) + for ventry in $ventries + do + d=$(echo $ventry | cut -d $sep -f 1) + keyauthorization=$(echo $ventry | cut -d $sep -f 2) + uri=$(echo $ventry | cut -d $sep -f 3) + _info "Verifying:$d" + _debug "d" "$d" + _debug "keyauthorization" "$keyauthorization" + _debug "uri" "$uri" + removelevel="" + token="" + if [ "$vtype" == "$VTYPE_HTTP" ] ; then + if [ "$Le_Webroot" == "no" ] ; then + _info "Standalone mode server" + _startserver "$keyauthorization" & + serverproc="$!" + sleep 2 + _debug serverproc $serverproc + else + if [ -z "$wellknown_path" ] ; then + wellknown_path="$Le_Webroot/.well-known/acme-challenge" + fi + _debug wellknown_path "$wellknown_path" + + if [ ! -d "$Le_Webroot/.well-known" ] ; then + removelevel='1' + elif [ ! -d "$Le_Webroot/.well-known/acme-challenge" ] ; then + removelevel='2' + else + removelevel='3' + fi + + token="$(echo -e -n "$keyauthorization" | cut -d '.' -f 1)" + _debug "writing token:$token to $wellknown_path/$token" + + mkdir -p "$wellknown_path" + echo -n "$keyauthorization" > "$wellknown_path/$token" + + webroot_owner=$(stat -c '%U:%G' $Le_Webroot) + _debug "Changing owner/group of .well-known to $webroot_owner" + chown -R $webroot_owner "$Le_Webroot/.well-known" + + fi + fi - _debug challenge "$challenge" _send_signed_request $uri "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}" if [ ! -z "$code" ] && [ ! "$code" == '202' ] ; then - _info "challenge error: $d" + _err "$d:Challenge error: $resource" + _clearupwebbroot "$Le_Webroot" "$removelevel" "$token" + _clearup return 1 fi @@ -374,193 +825,464 @@ issue() { _debug "checking" if ! _get $uri ; then - _info "verify error:$d" + _err "$d:Verify error:$resource" + _clearupwebbroot "$Le_Webroot" "$removelevel" "$token" + _clearup return 1 fi - status=$(echo $response | egrep -o '"status":"[^"]+"' | cut -d : -f 2 | sed 's/"//g') + status=$(echo $response | egrep -o '"status":"[^"]+"' | cut -d : -f 2 | tr -d '"') if [ "$status" == "valid" ] ; then - _info "verify success:$d" + _info "Success" + _stopserver $serverproc + serverproc="" + _clearupwebbroot "$Le_Webroot" "$removelevel" "$token" break; fi if [ "$status" == "invalid" ] ; then error=$(echo $response | egrep -o '"error":{[^}]*}' | grep -o '"detail":"[^"]*"' | cut -d '"' -f 4) - _info "verify error:$d" - _debug $error + _err "$d:Verify error:$error" + _clearupwebbroot "$Le_Webroot" "$removelevel" "$token" + _clearup return 1; fi if [ "$status" == "pending" ] ; then - _info "verify pending:$d" + _info "Pending" else - _info "verify error:$d" + _err "$d:Verify error:$response" + _clearupwebbroot "$Le_Webroot" "$removelevel" "$token" + _clearup return 1 fi - done - done - - _info "verify finished, start to sign." - der=$(openssl req -in $CSR_PATH -outform DER | base64 | _b64) - _send_signed_request "$API/acme/new-cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbas64" + done + + done + + _clearup + _info "Verify finished, start to sign." + der="$(openssl req -in $CSR_PATH -outform DER | _base64 | _b64)" + _send_signed_request "$API/acme/new-cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64" - echo -----BEGIN CERTIFICATE----- > $CERT_PATH - echo $response | sed "s/ /\n/g" >> $CERT_PATH - echo -----END CERTIFICATE----- >> $CERT_PATH - _info "Cert success." - cat $CERT_PATH - _setopt $DOMAIN_CONF "Le_Domain" "=" "$Le_Domain" - _setopt $DOMAIN_CONF "Le_Alt" "=" "$Le_Alt" - _setopt $DOMAIN_CONF "Le_Webroot" "=" "$Le_Webroot" - _setopt $DOMAIN_CONF "Le_Keylength" "=" "$Le_Keylength" + Le_LinkCert="$(grep -i -o '^Location.*$' $CURL_HEADER | tr -d "\r\n" | cut -d " " -f 2)" + _setopt "$DOMAIN_CONF" "Le_LinkCert" "=" "$Le_LinkCert" + + if [ "$Le_LinkCert" ] ; then + echo -----BEGIN CERTIFICATE----- > "$CERT_PATH" + curl --silent "$Le_LinkCert" | openssl base64 -e >> "$CERT_PATH" + echo -----END CERTIFICATE----- >> "$CERT_PATH" + _info "Cert success." + cat "$CERT_PATH" + + _info "Your cert is in $CERT_PATH" + fi + + if [ -z "$Le_LinkCert" ] ; then + response="$(echo $response | openssl base64 -d -A)" + _err "Sign failed: $(echo "$response" | grep -o '"detail":"[^"]*"')" + return 1 + fi + _setopt "$DOMAIN_CONF" 'Le_Vlist' '=' "\"\"" - Le_LinkIssuer=$(grep -i '^Link' $CURL_HEADER | cut -d " " -f 2| cut -d ';' -f 1 | sed 's///g') - _setopt $DOMAIN_CONF "Le_LinkIssuer" "=" "$Le_LinkIssuer" + Le_LinkIssuer=$(grep -i '^Link' $CURL_HEADER | cut -d " " -f 2| cut -d ';' -f 1 | tr -d '<>' ) + _setopt "$DOMAIN_CONF" "Le_LinkIssuer" "=" "$Le_LinkIssuer" - Le_LinkCert=$(grep -i '^Location' $CURL_HEADER | cut -d " " -f 2) - _setopt $DOMAIN_CONF "Le_LinkCert" "=" "$Le_LinkCert" + if [ "$Le_LinkIssuer" ] ; then + echo -----BEGIN CERTIFICATE----- > "$CA_CERT_PATH" + curl --silent "$Le_LinkIssuer" | openssl base64 -e >> "$CA_CERT_PATH" + echo -----END CERTIFICATE----- >> "$CA_CERT_PATH" + _info "The intermediate CA cert is in $CA_CERT_PATH" + fi Le_CertCreateTime=$(date -u "+%s") - _setopt $DOMAIN_CONF "Le_CertCreateTime" "=" "$Le_CertCreateTime" + _setopt "$DOMAIN_CONF" "Le_CertCreateTime" "=" "$Le_CertCreateTime" - Le_CertCreateTimeStr=$(date -u "+%Y-%m-%d %H:%M:%S UTC") - _setopt $DOMAIN_CONF "Le_CertCreateTimeStr" "=" "\"$Le_CertCreateTimeStr\"" + Le_CertCreateTimeStr=$(date -u ) + _setopt "$DOMAIN_CONF" "Le_CertCreateTimeStr" "=" "\"$Le_CertCreateTimeStr\"" if [ ! "$Le_RenewalDays" ] ; then - Le_RenewalDays=50 - fi - - _setopt $DOMAIN_CONF "Le_RenewalDays" "=" "$Le_RenewalDays" - - Le_NextRenewTime=$(date -u -d "+$Le_RenewalDays day" "+%s") - _setopt $DOMAIN_CONF "Le_NextRenewTime" "=" "$Le_NextRenewTime" - - Le_NextRenewTimeStr=$(date -u -d "+$Le_RenewalDays day" "+%Y-%m-%d %H:%M:%S UTC") - _setopt $DOMAIN_CONF "Le_NextRenewTimeStr" "=" "\"$Le_NextRenewTimeStr\"" - - _setopt $DOMAIN_CONF "Le_RealCertPath" "=" "\"$Le_RealCertPath\"" - if [ "$Le_RealCertPath" ] ; then - if [ -f "$Le_RealCertPath" ] ; then - rm -f $Le_RealCertPath - fi - ln -s $CERT_PATH $Le_RealCertPath - + Le_RenewalDays=80 fi - _setopt $DOMAIN_CONF "Le_RealKeyPath" "=" "\"$Le_RealKeyPath\"" - if [ "$Le_RealKeyPath" ] ; then - if [ -f "$Le_RealKeyPath" ] ; then - rm -f $Le_RealKeyPath - fi - ln -s $CERT_KEY_PATH $Le_RealKeyPath - - fi - _setopt $DOMAIN_CONF "Le_ReloadCmd" "=" "\"$Le_ReloadCmd\"" + _setopt "$DOMAIN_CONF" "Le_RenewalDays" "=" "$Le_RenewalDays" - if [ "Le_ReloadCmd" ] ; then - _info "Run Le_ReloadCmd: $Le_ReloadCmd" - $Le_ReloadCmd - fi + let "Le_NextRenewTime=Le_CertCreateTime+Le_RenewalDays*24*60*60" + _setopt "$DOMAIN_CONF" "Le_NextRenewTime" "=" "$Le_NextRenewTime" -} + Le_NextRenewTimeStr=$( _time2str $Le_NextRenewTime ) + _setopt "$DOMAIN_CONF" "Le_NextRenewTimeStr" "=" "\"$Le_NextRenewTimeStr\"" + + installcert $Le_Domain "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" +} renew() { Le_Domain="$1" if [ -z "$Le_Domain" ] ; then - echo Usage: $0 domain.com + _err "Usage: $0 domain.com" return 1 fi - - DOMAIN_CONF=$WORKING_DIR/$Le_Domain/$Le_Domain.conf - if [ -f "$DOMAIN_CONF" ] ; then - source "$DOMAIN_CONF" - if [ "$(date -u "+%s" )" -lt "$Le_NextRenewTime" ] ; then - _info "Skip, Next renwal time is: $Le_NextRenewTimeStr" - return 2 - fi + + _initpath $Le_Domain + + if [ ! -f "$DOMAIN_CONF" ] ; then + _info "$Le_Domain is not a issued domain, skip." + return 0; fi - if [ -z "$Le_Webroot" ] ; then - echo Le_Webroot can not found, please remove the conf file and issue a new cert - return 1 + 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 - issue $Le_Domain + 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" + + Le_LinkCert="" + Le_Domain="" + Le_Alt="" + Le_Webroot="" + Le_Keylength="" + Le_LinkIssuer="" + + Le_CertCreateTime="" + Le_CertCreateTimeStr="" + Le_RenewalDays="" + Le_NextRenewTime="" + Le_NextRenewTimeStr="" + + Le_RealCertPath="" + Le_RealKeyPath="" + + Le_RealCACertPath="" + + Le_ReloadCmd="" + + DOMAIN_CONF="" + DOMAIN_SSL_CONF="" + CSR_PATH="" + CERT_KEY_PATH="" + CERT_PATH="" + CA_CERT_PATH="" + ACCOUNT_KEY_PATH="" + + wellknown_path="" + renew "$d" done } +installcert() { + Le_Domain="$1" + if [ -z "$Le_Domain" ] ; then + _err "Usage: $0 domain.com [cert-file-path]|no [key-file-path]|no [ca-cert-file-path]|no [reloadCmd]|no" + return 1 + fi + + Le_RealCertPath="$2" + Le_RealKeyPath="$3" + Le_RealCACertPath="$4" + Le_ReloadCmd="$5" + + _initpath $Le_Domain + + _setopt "$DOMAIN_CONF" "Le_RealCertPath" "=" "\"$Le_RealCertPath\"" + _setopt "$DOMAIN_CONF" "Le_RealCACertPath" "=" "\"$Le_RealCACertPath\"" + _setopt "$DOMAIN_CONF" "Le_RealKeyPath" "=" "\"$Le_RealKeyPath\"" + _setopt "$DOMAIN_CONF" "Le_ReloadCmd" "=" "\"$Le_ReloadCmd\"" + + if [ "$Le_RealCertPath" ] ; then + if [ -f "$Le_RealCertPath" ] ; then + cp -p "$Le_RealCertPath" "$Le_RealCertPath".bak + fi + cat "$CERT_PATH" > "$Le_RealCertPath" + fi + + if [ "$Le_RealCACertPath" ] ; then + if [ -f "$Le_RealCACertPath" ] ; then + cp -p "$Le_RealCACertPath" "$Le_RealCACertPath".bak + fi + if [ "$Le_RealCACertPath" == "$Le_RealCertPath" ] ; then + echo "" >> "$Le_RealCACertPath" + cat "$CA_CERT_PATH" >> "$Le_RealCACertPath" + else + cat "$CA_CERT_PATH" > "$Le_RealCACertPath" + fi + fi + + + if [ "$Le_RealKeyPath" ] ; then + if [ -f "$Le_RealKeyPath" ] ; then + cp -p "$Le_RealKeyPath" "$Le_RealKeyPath".bak + fi + cat "$CERT_KEY_PATH" > "$Le_RealKeyPath" + fi + + if [ "$Le_ReloadCmd" ] ; then + _info "Run Le_ReloadCmd: $Le_ReloadCmd" + eval $Le_ReloadCmd + fi + +} + +installcronjob() { + _initpath + _info "Installing cron job" + if ! crontab -l | grep 'le.sh cron' ; then + 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 * * * LE_WORKING_DIR=\"$LE_WORKING_DIR\" $lesh cron > /dev/null"; } | crontab - + fi + return 0 +} + +uninstallcronjob() { + _info "Removing cron job" + cr="$(crontab -l | grep 'le.sh cron')" + if [ "$cr" ] ; then + crontab -l | sed "/le.sh cron/d" | crontab - + LE_WORKING_DIR="$(echo "$cr" | cut -d ' ' -f 6 | 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 - if ! command -v "curl" ; then - _info "Please install curl first." - _info "sudo apt-get install curl" + + #check if there is sudo installed, AND if the current user is a sudoer. + if command -v sudo > /dev/null ; then + if [ "$(sudo -n uptime 2>&1|grep "load"|wc -l)" != "0" ] ; then + SUDO=sudo + fi + fi + + if command -v yum > /dev/null ; then + YUM="1" + INSTALL="$SUDO yum install -y " + elif command -v apt-get > /dev/null ; then + INSTALL="$SUDO apt-get install -y " + fi + + if ! command -v "curl" > /dev/null ; then + _err "Please install curl first." + _err "$INSTALL curl" return 1 fi - _info "Installing to $WORKING_DIR" - mkdir -p $WORKING_DIR/ - cp le.sh $WORKING_DIR/ - chmod +x $WORKING_DIR/le.sh + if ! command -v "crontab" > /dev/null ; then + _err "Please install crontab first." + if [ "$YUM" ] ; then + _err "$INSTALL crontabs" + else + _err "$INSTALL crontab" + fi + return 1 + fi - if [ ! -f /bin/le.sh ] ; then - ln -s $WORKING_DIR/le.sh /bin/le.sh - ln -s $WORKING_DIR/le.sh /bin/le + if ! command -v "openssl" > /dev/null ; then + _err "Please install openssl first." + _err "$INSTALL openssl" + return 1 + fi + + _info "Installing to $LE_WORKING_DIR" + + _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 + + mkdir -p $LE_WORKING_DIR/dnsapi + cp dnsapi/* $LE_WORKING_DIR/dnsapi/ - _info "Installing cron job" - if ! crontab -l | grep 'le.sh renewAll' ; then - crontab -l | { cat; echo "0 0 * * * le.sh renewAll"; } | crontab - - service cron restart - fi + #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 } uninstall() { + uninstallcronjob _initpath - _info "Removing cron job" - crontab -l | sed "/le.sh renewAll/d" | crontab - - - _info "Removing /bin/le.sh" - rm -f /bin/le - rm -f /bin/le.sh - - _info "The keys and certs are in $WORKING_DIR, you can remove them by yourself." + + _profile="$(_detect_profile)" + if [ "$_profile" ] ; then + sed -i /le.env/d "$_profile" + fi + + rm -f $LE_WORKING_DIR/le.sh + _info "The keys and certs are in $LE_WORKING_DIR, you can remove them by yourself." } +cron() { + renewAll +} + +version() { + _info "$PROJECT" + _info "v$VER" +} showhelp() { - echo "Usage: issue|renew|renewAll|createAccountKey|createDomainKey|createCSR|install|uninstall" + version + echo "Usage: le.sh [command] ...[args].... +Avalible commands: +install: + Install le.sh to your system. +issue: + Issue a cert. +installcert: + Install the issued cert to apache/nginx or any other server. +renew: + Renew a cert. +renewAll: + Renew all the certs. +uninstall: + Uninstall le.sh, and uninstall the cron job. +version: + Show version info. +installcronjob: + Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job. +uninstallcronjob: + Uninstall the cron job. The 'uninstall' command can do this automatically. +createAccountKey: + Create an account private key, professional use. +createDomainKey: + Create an domain private key, professional use. +createCSR: + Create CSR , professional use. + " } if [ -z "$1" ] ; then showhelp +else + "$@" fi - - - -$1 $2 $3 $4 $5 $6 $7 $8 - - - -