Browse Source

Merge remote-tracking branch 'upstream/master' into ssh-deploy

David Kerr 8 years ago
parent
commit
070a141601
9 changed files with 430 additions and 12 deletions
  1. 14 0
      README.md
  2. 15 5
      acme.sh
  3. 6 0
      deploy/README.md
  4. 31 0
      deploy/keychain.sh
  5. 58 0
      dnsapi/README.md
  6. 3 3
      dnsapi/dns_aws.sh
  7. 3 4
      dnsapi/dns_cx.sh
  8. 205 0
      dnsapi/dns_dgon.sh
  9. 95 0
      dnsapi/dns_knot.sh

+ 14 - 0
README.md

@@ -18,6 +18,18 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
 
 
 # [中文说明](https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
 # [中文说明](https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
 
 
+# Who are using **acme.sh**
+- [FreeBSD.org](https://blog.crashed.org/letsencrypt-in-freebsd-org/)
+- [ruby-china.org](https://ruby-china.org/topics/31983)
+- [Proxmox](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x_and_newer))
+- [pfsense](https://github.com/pfsense/FreeBSD-ports/pull/89)
+- [webfaction](https://community.webfaction.com/questions/19988/using-letsencrypt)
+- [Loadbalancer.org](https://www.loadbalancer.org/blog/loadbalancer-org-with-lets-encrypt-quick-and-dirty)
+- [discourse.org](https://meta.discourse.org/t/setting-up-lets-encrypt/40709)
+- [Centminmod](http://centminmod.com/letsencrypt-acmetool-https.html)
+- [splynx](https://forum.splynx.com/t/free-ssl-cert-for-splynx-lets-encrypt/297)
+- [archlinux](https://aur.archlinux.org/packages/acme.sh-git/)
+- [more...](https://github.com/Neilpang/acme.sh/wiki/Blogs-and-tutorials)
 
 
 # Tested OS
 # Tested OS
 
 
@@ -295,6 +307,8 @@ You don't have to do anything manually!
 1. cyon.ch
 1. cyon.ch
 1. Domain-Offensive/Resellerinterface/Domainrobot API
 1. Domain-Offensive/Resellerinterface/Domainrobot API
 1. Gandi LiveDNS API
 1. Gandi LiveDNS API
+1. Knot DNS API
+1. DigitalOcean API (native)
 
 
 **More APIs coming soon...**
 **More APIs coming soon...**
 
 

+ 15 - 5
acme.sh

@@ -299,6 +299,16 @@ _secure_debug3() {
   fi
   fi
 }
 }
 
 
+_upper_case() {
+  # shellcheck disable=SC2018,SC2019
+  tr 'a-z' 'A-Z'
+}
+
+_lower_case() {
+  # shellcheck disable=SC2018,SC2019
+  tr 'A-Z' 'a-z'
+}
+
 _startswith() {
 _startswith() {
   _str="$1"
   _str="$1"
   _sub="$2"
   _sub="$2"
@@ -2462,7 +2472,7 @@ _setNginx() {
   fi
   fi
   _debug "Start detect nginx conf for $_d from:$_start_f"
   _debug "Start detect nginx conf for $_d from:$_start_f"
   if ! _checkConf "$_d" "$_start_f"; then
   if ! _checkConf "$_d" "$_start_f"; then
-    "Can not find conf file for domain $d"
+    _err "Can not find conf file for domain $d"
     return 1
     return 1
   fi
   fi
   _info "Found conf file: $FOUND_REAL_NGINX_CONF"
   _info "Found conf file: $FOUND_REAL_NGINX_CONF"
@@ -2546,7 +2556,7 @@ _checkConf() {
   if [ ! -f "$2" ] && ! echo "$2" | grep '*$' >/dev/null && echo "$2" | grep '*' >/dev/null; then
   if [ ! -f "$2" ] && ! echo "$2" | grep '*$' >/dev/null && echo "$2" | grep '*' >/dev/null; then
     _debug "wildcard"
     _debug "wildcard"
     for _w_f in $2; do
     for _w_f in $2; do
-      if _checkConf "$1" "$_w_f"; then
+      if [ -f "$_w_f"] && _checkConf "$1" "$_w_f"; then
         return 0
         return 0
       fi
       fi
     done
     done
@@ -2559,9 +2569,9 @@ _checkConf() {
       FOUND_REAL_NGINX_CONF="$2"
       FOUND_REAL_NGINX_CONF="$2"
       return 0
       return 0
     fi
     fi
-    if grep "^ *include *.*;" "$2" >/dev/null; then
+    if cat "$2" | tr "\t" " " | grep "^ *include *.*;" >/dev/null; then
       _debug "Try include files"
       _debug "Try include files"
-      for included in $(grep "^ *include *.*;" "$2" | sed "s/include //" | tr -d " ;"); do
+      for included in $(cat "$2" | tr "\t" " " | grep "^ *include *.*;" | sed "s/include //" | tr -d " ;"); do
         _debug "check included $included"
         _debug "check included $included"
         if _checkConf "$1" "$included"; then
         if _checkConf "$1" "$included"; then
           return 0
           return 0
@@ -4618,7 +4628,7 @@ install() {
     #Modify shebang
     #Modify shebang
     if _exists bash; then
     if _exists bash; then
       _info "Good, bash is found, so change the shebang to use bash as preferred."
       _info "Good, bash is found, so change the shebang to use bash as preferred."
-      _shebang='#!/usr/bin/env bash'
+      _shebang='#!'"$(env bash -c "command -v bash")"
       _setShebang "$LE_WORKING_DIR/$PROJECT_ENTRY" "$_shebang"
       _setShebang "$LE_WORKING_DIR/$PROJECT_ENTRY" "$_shebang"
       for subf in $_SUB_FOLDERS; do
       for subf in $_SUB_FOLDERS; do
         if [ -d "$LE_WORKING_DIR/$subf" ]; then
         if [ -d "$LE_WORKING_DIR/$subf" ]; then

+ 6 - 0
deploy/README.md

@@ -210,3 +210,9 @@ export DEPLOY_EXIM4_RELOAD="/etc/init.d/exim4 restart"
 
 
 acme.sh --deploy -d ftp.example.com --deploy-hook exim4
 acme.sh --deploy -d ftp.example.com --deploy-hook exim4
 ```
 ```
+
+## 6. Deploy the cert to OSX Keychain
+
+```sh
+acme.sh --deploy -d ftp.example.com --deploy-hook keychain
+```

+ 31 - 0
deploy/keychain.sh

@@ -0,0 +1,31 @@
+#!/usr/bin/env sh
+
+#Here is a sample custom api script.
+#This file name is "myapi.sh"
+#So, here must be a method   myapi_deploy()
+#Which will be called by acme.sh to deploy the cert
+#returns 0 means success, otherwise error.
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+keychain_deploy() {
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+
+  _debug _cdomain "$_cdomain"
+  _debug _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  /usr/bin/security import "$_ckey" -k "/Library/Keychains/System.keychain"
+  /usr/bin/security import "$_ccert" -k "/Library/Keychains/System.keychain"
+  /usr/bin/security import "$_cca" -k "/Library/Keychains/System.keychain"
+  /usr/bin/security import "$_cfullchain" -k "/Library/Keychains/System.keychain"
+
+  return 0
+}

+ 58 - 0
dnsapi/README.md

@@ -349,6 +349,64 @@ Ok, let's issue a cert now:
 acme.sh --issue --dns dns_gandi_livedns -d example.com -d www.example.com
 acme.sh --issue --dns dns_gandi_livedns -d example.com -d www.example.com
 ```
 ```
 
 
+## 19. Use Knot (knsupdate) DNS API to automatically issue cert
+
+First, generate a TSIG key for updating the zone.
+
+```
+keymgr tsig generate acme_key algorithm hmac-sha512 > /etc/knot/acme.key
+```
+
+Include this key in your knot configuration file.
+
+```
+include: /etc/knot/acme.key
+```
+
+Next, configure your zone to allow dynamic updates.
+
+Dynamic updates for the zone are allowed via proper ACL rule with the `update` action. For in-depth instructions, please see [Knot DNS's documentation](https://www.knot-dns.cz/documentation/).
+
+```
+acl:
+  - id: acme_acl
+    address: 192.168.1.0/24
+    key: acme_key
+    action: update
+
+zone:
+  - domain: example.com
+    file: example.com.zone
+    acl: acme_acl
+```
+
+Finally, make the DNS server and TSIG Key available to `acme.sh`
+
+```
+export KNOT_SERVER="dns.example.com"
+export KNOT_KEY=`grep \# /etc/knot/acme.key | cut -d' ' -f2`
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_knot -d example.com -d www.example.com
+```
+
+The `KNOT_SERVER` and `KNOT_KEY` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 20. Use DigitalOcean API (native)
+
+You need to obtain a read and write capable API key from your DigitalOcean account. See: https://www.digitalocean.com/help/api/
+
+```
+export DO_API_KEY="75310dc4ca779ac39a19f6355db573b49ce92ae126553ebd61ac3a3ae34834cc"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_dgon -d example.com -d www.example.com
+```
+
 # Use custom API
 # Use custom API
 
 
 If your API is not supported yet, you can write your own DNS API.
 If your API is not supported yet, you can write your own DNS API.

+ 3 - 3
dnsapi/dns_aws.sh

@@ -143,7 +143,7 @@ aws_rest() {
   CanonicalHeaders="host:$aws_host\nx-amz-date:$RequestDate\n"
   CanonicalHeaders="host:$aws_host\nx-amz-date:$RequestDate\n"
   SignedHeaders="host;x-amz-date"
   SignedHeaders="host;x-amz-date"
   if [ -n "$AWS_SESSION_TOKEN" ]; then
   if [ -n "$AWS_SESSION_TOKEN" ]; then
-    export _H2="x-amz-security-token: $AWS_SESSION_TOKEN"
+    export _H3="x-amz-security-token: $AWS_SESSION_TOKEN"
     CanonicalHeaders="${CanonicalHeaders}x-amz-security-token:$AWS_SESSION_TOKEN\n"
     CanonicalHeaders="${CanonicalHeaders}x-amz-security-token:$AWS_SESSION_TOKEN\n"
     SignedHeaders="${SignedHeaders};x-amz-security-token"
     SignedHeaders="${SignedHeaders};x-amz-security-token"
   fi
   fi
@@ -204,8 +204,8 @@ aws_rest() {
   Authorization="$Algorithm Credential=$AWS_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature"
   Authorization="$Algorithm Credential=$AWS_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature"
   _debug2 Authorization "$Authorization"
   _debug2 Authorization "$Authorization"
 
 
-  _H3="Authorization: $Authorization"
-  _debug _H3 "$_H3"
+  _H2="Authorization: $Authorization"
+  _debug _H2 "$_H2"
 
 
   url="$AWS_URL/$ep"
   url="$AWS_URL/$ep"
 
 

+ 3 - 4
dnsapi/dns_cx.sh

@@ -209,8 +209,7 @@ _rest() {
     return 1
     return 1
   fi
   fi
   _debug2 response "$response"
   _debug2 response "$response"
-  if ! _contains "$response" '"message":"success"'; then
-    return 1
-  fi
-  return 0
+
+  _contains "$response" '"code":1'
+
 }
 }

+ 205 - 0
dnsapi/dns_dgon.sh

@@ -0,0 +1,205 @@
+#!/usr/bin/env sh
+
+## Will be called by acme.sh to add the txt record to your api system.
+## returns 0 means success, otherwise error.
+
+## Author: thewer <github at thewer.com>
+## GitHub: https://github.com/gitwer/acme.sh
+
+##
+## Environment Variables Required:
+##
+## DO_API_KEY="75310dc4ca779ac39a19f6355db573b49ce92ae126553ebd61ac3a3ae34834cc"
+##
+
+#####################  Public functions  #####################
+
+## Create the text record for validation.
+## Usage: fulldomain txtvalue
+## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs"
+dns_dgon_add() {
+  fulldomain="$(echo "$1" | _lower_case)"
+  txtvalue=$2
+  _info "Using digitalocean dns validation - add record"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  ## save the env vars (key and domain split location) for later automated use
+  _saveaccountconf DO_API_KEY "$DO_API_KEY"
+
+  ## split the domain for DO API
+  if ! _get_base_domain "$fulldomain"; then
+    _err "domain not found in your account for addition"
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  ## Set the header with our post type and key auth key
+  export _H1="Content-Type: application/json"
+  export _H2="Authorization: Bearer $DO_API_KEY"
+  PURL='https://api.digitalocean.com/v2/domains/'$_domain'/records'
+  PBODY='{"type":"TXT","name":"'$_sub_domain'","data":"'$txtvalue'"}'
+
+  _debug PURL "$PURL"
+  _debug PBODY "$PBODY"
+
+  ## the create request - post
+  ## args: BODY, URL, [need64, httpmethod]
+  response="$(_post "$PBODY" "$PURL")"
+
+  ## check response
+  if [ "$?" != "0" ]; then
+    _err "error in response: $response"
+    return 1
+  fi
+  _debug2 response "$response"
+
+  ## finished correctly
+  return 0
+}
+
+## Remove the txt record after validation.
+## Usage: fulldomain txtvalue
+## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs"
+dns_dgon_rm() {
+  fulldomain="$(echo "$1" | _lower_case)"
+  txtvalue=$2
+  _info "Using digitalocean dns validation - remove record"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  ## split the domain for DO API
+  if ! _get_base_domain "$fulldomain"; then
+    _err "domain not found in your account for removal"
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  ## Set the header with our post type and key auth key
+  export _H1="Content-Type: application/json"
+  export _H2="Authorization: Bearer $DO_API_KEY"
+  ## get URL for the list of domains
+  ## may get: "links":{"pages":{"last":".../v2/domains/DOM/records?page=2","next":".../v2/domains/DOM/records?page=2"}}
+  GURL="https://api.digitalocean.com/v2/domains/$_domain/records"
+
+  ## while we dont have a record ID we keep going
+  while [ -z "$record" ]; do
+    ## 1) get the URL
+    ## the create request - get
+    ## args: URL, [onlyheader, timeout]
+    domain_list="$(_get "$GURL")"
+    ## 2) find record
+    ## check for what we are looing for: "type":"A","name":"$_sub_domain"
+    record="$(echo "$domain_list" | _egrep_o "\"id\"\s*\:\s*\"*\d+\"*[^}]*\"name\"\s*\:\s*\"$_sub_domain\"[^}]*\"data\"\s*\:\s*\"$txtvalue\"")"
+    ## 3) check record and get next page
+    if [ -z "$record" ]; then
+      ## find the next page if we dont have a match
+      nextpage="$(echo "$domain_list" | _egrep_o "\"links\".*" | _egrep_o "\"next\".*" | _egrep_o "http.*page\=\d+")"
+      if [ -z "$nextpage" ]; then
+        _err "no record and no nextpage in digital ocean DNS removal"
+        return 1
+      fi
+      _debug2 nextpage "$nextpage"
+      GURL="$nextpage"
+    fi
+    ## we break out of the loop when we have a record
+  done
+
+  ## we found the record
+  rec_id="$(echo "$record" | _egrep_o "id\"\s*\:\s*\"*\d+" | _egrep_o "\d+")"
+  _debug rec_id "$rec_id"
+
+  ## delete the record
+  ## delete URL for removing the one we dont want
+  DURL="https://api.digitalocean.com/v2/domains/$_domain/records/$rec_id"
+
+  ## the create request - delete
+  ## args: BODY, URL, [need64, httpmethod]
+  response="$(_post "" "$DURL" "" "DELETE")"
+
+  ## check response (sort of)
+  if [ "$?" != "0" ]; then
+    _err "error in remove response: $response"
+    return 1
+  fi
+  _debug2 response "$response"
+
+  ## finished correctly
+  return 0
+}
+
+#####################  Private functions below  #####################
+
+## Split the domain provided into the "bade domain" and the "start prefix".
+## This function searches for the longest subdomain in your account
+## for the full domain given and splits it into the base domain (zone)
+## and the prefix/record to be added/removed
+## USAGE: fulldomain
+## EG: "_acme-challenge.two.three.four.domain.com"
+## returns
+## _sub_domain="_acme-challenge.two"
+## _domain="three.four.domain.com" *IF* zone "three.four.domain.com" exists
+## if only "domain.com" exists it will return
+## _sub_domain="_acme-challenge.two.three.four"
+## _domain="domain.com"
+_get_base_domain() {
+  # args
+  fulldomain="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
+  _debug fulldomain "$fulldomain"
+
+  # domain max legal length = 253
+  MAX_DOM=255
+
+  ## get a list of domains for the account to check thru
+  ## Set the headers
+  export _H1="Content-Type: application/json"
+  export _H2="Authorization: Bearer $DO_API_KEY"
+  _debug DO_API_KEY "$DO_API_KEY"
+  ## get URL for the list of domains
+  ## havent seen this request paginated, tested with 18 domains (more requres manual requests with DO)
+  DOMURL="https://api.digitalocean.com/v2/domains"
+
+  ## get the domain list (DO gives basically a full XFER!)
+  domain_list="$(_get "$DOMURL")"
+
+  ## check response
+  if [ "$?" != "0" ]; then
+    _err "error in domain_list response: $domain_list"
+    return 1
+  fi
+  _debug2 domain_list "$domain_list"
+
+  ## for each shortening of our $fulldomain, check if it exists in the $domain_list
+  ## can never start on 1 (aka whole $fulldomain) as $fulldomain starts with "_acme-challenge"
+  i=2
+  while [ $i -gt 0 ]; do
+    ## get next longest domain
+    _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM")
+    ## check we got something back from our cut (or are we at the end)
+    if [ -z "$_domain" ]; then
+      ## we got to the end of the domain - invalid domain
+      _err "domain not found in DigitalOcean account"
+      return 1
+    fi
+    ## we got part of a domain back - grep it out
+    found="$(echo "$domain_list" | _egrep_o "\"name\"\s*\:\s*\"$_domain\"")"
+    ## check if it exists
+    if [ ! -z "$found" ]; then
+      ## exists - exit loop returning the parts
+      sub_point=$(_math $i - 1)
+      _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point")
+      _debug _domain "$_domain"
+      _debug _sub_domain "$_sub_domain"
+      return 0
+    fi
+    ## increment cut point $i
+    i=$(_math $i + 1)
+  done
+
+  ## we went through the entire domain zone list and dint find one that matched
+  ## doesnt look like we can add in the record
+  _err "domain not found in DigitalOcean account, but we should never get here"
+  return 1
+}

+ 95 - 0
dnsapi/dns_knot.sh

@@ -0,0 +1,95 @@
+#!/usr/bin/env sh
+
+########  Public functions #####################
+
+#Usage: dns_knot_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_knot_add() {
+  fulldomain=$1
+  txtvalue=$2
+  _checkKey || return 1
+  [ -n "${KNOT_SERVER}" ] || KNOT_SERVER="localhost"
+  # save the dns server and key to the account.conf file.
+  _saveaccountconf KNOT_SERVER "${KNOT_SERVER}"
+  _saveaccountconf KNOT_KEY "${KNOT_KEY}"
+
+  if ! _get_root "$fulldomain"; then
+    _err "Domain does not exist."
+    return 1
+  fi
+
+  _info "Adding ${fulldomain}. 60 TXT \"${txtvalue}\""
+
+  knsupdate -y "${KNOT_KEY}" <<EOF
+server ${KNOT_SERVER}
+zone ${_domain}.
+update add ${fulldomain}. 60 TXT "${txtvalue}"
+send
+quit
+EOF
+
+  if [ $? -ne 0 ]; then
+    _err "Error updating domain."
+    return 1
+  fi
+
+  _info "Domain TXT record successfully added."
+  return 0
+}
+
+#Usage: dns_knot_rm   _acme-challenge.www.domain.com
+dns_knot_rm() {
+  fulldomain=$1
+  _checkKey || return 1
+  [ -n "${KNOT_SERVER}" ] || KNOT_SERVER="localhost"
+
+  if ! _get_root "$fulldomain"; then
+    _err "Domain does not exist."
+    return 1
+  fi
+
+  _info "Removing ${fulldomain}. TXT"
+
+  knsupdate -y "${KNOT_KEY}" <<EOF
+server ${KNOT_SERVER}
+zone ${_domain}.
+update del ${fulldomain}. TXT
+send
+quit
+EOF
+
+  if [ $? -ne 0 ]; then
+    _err "error updating domain"
+    return 1
+  fi
+
+  _info "Domain TXT record successfully deleted."
+  return 0
+}
+
+####################  Private functions below ##################################
+# _acme-challenge.www.domain.com
+# returns
+# _domain=domain.com
+_get_root() {
+  domain=$1
+  i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)"
+  i=$(_math "$i" - 1)
+
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+    if [ -z "$h" ]; then
+      return 1
+    fi
+    _domain="$h"
+    return 0
+  done
+  _debug "$domain not found"
+  return 1
+}
+
+_checkKey() {
+  if [ -z "${KNOT_KEY}" ]; then
+    _err "You must specify a TSIG key to authenticate the request."
+    return 1
+  fi
+}