Browse Source

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

David Kerr 8 years ago
parent
commit
e02c94eb00
5 changed files with 464 additions and 20 deletions
  1. 2 3
      .travis.yml
  2. 2 0
      README.md
  3. 58 17
      acme.sh
  4. 27 0
      deploy/kong.sh
  5. 375 0
      dnsapi/dns_ad.sh

+ 2 - 3
.travis.yml

@@ -33,7 +33,6 @@ install:
     fi
     fi
   
   
 script:
 script:
-  - echo "TEST_LOCAL=$TEST_LOCAL"
   - echo "NGROK_TOKEN=$(echo "$NGROK_TOKEN" | wc -c)"
   - echo "NGROK_TOKEN=$(echo "$NGROK_TOKEN" | wc -c)"
   - command -V openssl && openssl version
   - command -V openssl && openssl version
   - if [ "$TRAVIS_OS_NAME" = "linux" ]; then curl -sSL $SHFMT_URL -o ~/shfmt ; fi
   - if [ "$TRAVIS_OS_NAME" = "linux" ]; then curl -sSL $SHFMT_URL -o ~/shfmt ; fi
@@ -44,8 +43,8 @@ script:
   - if [ "$TRAVIS_OS_NAME" = "linux" ]; then shellcheck **/*.sh && echo "shellcheck OK" ; fi
   - if [ "$TRAVIS_OS_NAME" = "linux" ]; then shellcheck **/*.sh && echo "shellcheck OK" ; fi
   - cd ..
   - cd ..
   - git clone https://github.com/Neilpang/acmetest.git && cp -r acme.sh acmetest/ && cd acmetest
   - git clone https://github.com/Neilpang/acmetest.git && cp -r acme.sh acmetest/ && cd acmetest
-  - if [ "$TRAVIS_OS_NAME" = "linux" -a "$NGROK_TOKEN" ]; then sudo NGROK_TOKEN="$NGROK_TOKEN" ./letest.sh ; fi
-  - if [ "$TRAVIS_OS_NAME" = "osx" -a "$NGROK_TOKEN" ]; then sudo NGROK_TOKEN="$NGROK_TOKEN" OPENSSL_BIN="$OPENSSL_BIN" ./letest.sh ; fi
+  - if [ "$TRAVIS_OS_NAME" = "linux" -a "$NGROK_TOKEN" ]; then sudo TEST_LOCAL="$TEST_LOCAL" NGROK_TOKEN="$NGROK_TOKEN" ./letest.sh ; fi
+  - if [ "$TRAVIS_OS_NAME" = "osx" -a "$NGROK_TOKEN" ]; then sudo TEST_LOCAL="$TEST_LOCAL" NGROK_TOKEN="$NGROK_TOKEN" OPENSSL_BIN="$OPENSSL_BIN" ./letest.sh ; fi
 
 
 
 
 matrix:
 matrix:

+ 2 - 0
README.md

@@ -55,6 +55,7 @@ https://github.com/Neilpang/acmetest
 - Standalone mode
 - Standalone mode
 - Apache mode
 - Apache mode
 - DNS mode
 - DNS mode
+- [Stateless mode](https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode)
 
 
 
 
 # 1. How to install
 # 1. How to install
@@ -270,6 +271,7 @@ You don't have to do anything manually!
 1. ISPConfig 3.1 API
 1. ISPConfig 3.1 API
 1. Alwaysdata.com API
 1. Alwaysdata.com API
 1. Linode.com API
 1. Linode.com API
+1. FreeDNS (https://freedns.afraid.org/)
 
 
 **More APIs coming soon...**
 **More APIs coming soon...**
 
 

+ 58 - 17
acme.sh

@@ -41,6 +41,8 @@ NO_VALUE="no"
 
 
 W_TLS="tls"
 W_TLS="tls"
 
 
+MODE_STATELESS="stateless"
+
 STATE_VERIFIED="verified_ok"
 STATE_VERIFIED="verified_ok"
 
 
 BEGIN_CSR="-----BEGIN CERTIFICATE REQUEST-----"
 BEGIN_CSR="-----BEGIN CERTIFICATE REQUEST-----"
@@ -61,6 +63,10 @@ DEFAULT_LOG_LEVEL="$LOG_LEVEL_1"
 
 
 _DEBUG_WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh"
 _DEBUG_WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh"
 
 
+_PREPARE_LINK="https://github.com/Neilpang/acme.sh/wiki/Install-preparations"
+
+_STATELESS_WIKI="https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode"
+
 __INTERACTIVE=""
 __INTERACTIVE=""
 if [ -t 1 ]; then
 if [ -t 1 ]; then
   __INTERACTIVE="1"
   __INTERACTIVE="1"
@@ -340,11 +346,29 @@ _is_solaris() {
   _contains "${__OS__:=$(uname -a)}" "solaris" || _contains "${__OS__:=$(uname -a)}" "SunOS"
   _contains "${__OS__:=$(uname -a)}" "solaris" || _contains "${__OS__:=$(uname -a)}" "SunOS"
 }
 }
 
 
+#_ascii_hex str
+#this can only process ascii chars, should only be used when od command is missing as a backup way.
+_ascii_hex() {
+  _debug2 "Using _ascii_hex"
+  _str="$1"
+  _str_len=${#_str}
+  _h_i=1
+  while [ "$_h_i" -le "$_str_len" ]; do
+    _str_c="$(printf "%s" "$_str" | cut -c "$_h_i")"
+    printf " %02x" "'$_str_c"
+    _h_i="$(_math "$_h_i" + 1)"
+  done
+}
+
 #stdin  output hexstr splited by one space
 #stdin  output hexstr splited by one space
 #input:"abc"
 #input:"abc"
 #output: " 61 62 63"
 #output: " 61 62 63"
 _hex_dump() {
 _hex_dump() {
-  od -A n -v -t x1 | tr -d "\r\t" | tr -s " " | sed "s/ $//" | tr -d "\n"
+  #in wired some system, the od command is missing.
+  if ! od -A n -v -t x1 | tr -d "\r\t" | tr -s " " | sed "s/ $//" | tr -d "\n" 2>/dev/null; then
+    str=$(cat)
+    _ascii_hex "$str"
+  fi
 }
 }
 
 
 #url encode, no-preserved chars
 #url encode, no-preserved chars
@@ -896,7 +920,7 @@ _readSubjectFromCSR() {
     _usage "_readSubjectFromCSR mycsr.csr"
     _usage "_readSubjectFromCSR mycsr.csr"
     return 1
     return 1
   fi
   fi
-  $OPENSSL_BIN req -noout -in "$_csrfile" -subject | _egrep_o "CN=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d '\n'
+  $OPENSSL_BIN req -noout -in "$_csrfile" -subject | _egrep_o "CN *=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d '\n'
 }
 }
 
 
 #_csrfile
 #_csrfile
@@ -1245,6 +1269,10 @@ _time() {
   date -u "+%s"
   date -u "+%s"
 }
 }
 
 
+_utc_date() {
+  date -u "+%Y-%m-%d %H:%M:%S"
+}
+
 _mktemp() {
 _mktemp() {
   if _exists mktemp; then
   if _exists mktemp; then
     if mktemp 2>/dev/null; then
     if mktemp 2>/dev/null; then
@@ -1666,6 +1694,14 @@ _startserver() {
     _NC="$_NC -6"
     _NC="$_NC -6"
   fi
   fi
 
 
+  if [ "$Le_Listen_V4$Le_Listen_V6$ncaddr" ]; then
+    if ! _contains "$nchelp" "-4"; then
+      _err "The nc doesn't support '-4', '-6' or local-address, please install 'netcat-openbsd' and try again."
+      _err "See $(__green $_PREPARE_LINK)"
+      return 1
+    fi
+  fi
+
   if echo "$nchelp" | grep "\-q[ ,]" >/dev/null; then
   if echo "$nchelp" | grep "\-q[ ,]" >/dev/null; then
     _NC="$_NC -q 1 -l $ncaddr"
     _NC="$_NC -q 1 -l $ncaddr"
   else
   else
@@ -2457,6 +2493,10 @@ __calcAccountKeyHash() {
   [ -f "$ACCOUNT_KEY_PATH" ] && _digest sha256 <"$ACCOUNT_KEY_PATH"
   [ -f "$ACCOUNT_KEY_PATH" ] && _digest sha256 <"$ACCOUNT_KEY_PATH"
 }
 }
 
 
+__calc_account_thumbprint() {
+  printf "%s" "$jwk" | tr -d ' ' | _digest "sha256" | _url_replace
+}
+
 #keylength
 #keylength
 _regAccount() {
 _regAccount() {
   _initpath
   _initpath
@@ -2547,6 +2587,8 @@ _regAccount() {
         return 1
         return 1
       fi
       fi
     fi
     fi
+    ACCOUNT_THUMBPRINT="$(__calc_account_thumbprint)"
+    _info "ACCOUNT_THUMBPRINT" "$ACCOUNT_THUMBPRINT"
     return 0
     return 0
   done
   done
 
 
@@ -2778,8 +2820,7 @@ issue() {
       fi
       fi
 
 
       if [ -z "$thumbprint" ]; then
       if [ -z "$thumbprint" ]; then
-        accountkey_json=$(printf "%s" "$jwk" | tr -d ' ')
-        thumbprint=$(printf "%s" "$accountkey_json" | _digest "sha256" | _url_replace)
+        thumbprint="$(__calc_account_thumbprint)"
       fi
       fi
 
 
       entry="$(printf "%s\n" "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
       entry="$(printf "%s\n" "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
@@ -2936,7 +2977,9 @@ issue() {
         serverproc="$!"
         serverproc="$!"
         sleep 1
         sleep 1
         _debug serverproc "$serverproc"
         _debug serverproc "$serverproc"
-
+      elif [ "$_currentRoot" = "$MODE_STATELESS" ]; then
+        _info "Stateless mode for domain:$d"
+        _sleep 1
       else
       else
         if [ "$_currentRoot" = "apache" ]; then
         if [ "$_currentRoot" = "apache" ]; then
           wellknown_path="$ACME_DIR"
           wellknown_path="$ACME_DIR"
@@ -3878,12 +3921,7 @@ _detect_profile() {
 _initconf() {
 _initconf() {
   _initpath
   _initpath
   if [ ! -f "$ACCOUNT_CONF_PATH" ]; then
   if [ ! -f "$ACCOUNT_CONF_PATH" ]; then
-    echo "#ACCOUNT_CONF_PATH=xxxx
-
-#ACCOUNT_EMAIL=aaa@example.com  # the account email used to register account.
-#ACCOUNT_KEY_PATH=\"/path/to/account.key\"
-#CERT_HOME=\"/path/to/cert/home\"
-
+    echo "
 
 
 #LOG_FILE=\"$DEFAULT_LOG_FILE\"
 #LOG_FILE=\"$DEFAULT_LOG_FILE\"
 #LOG_LEVEL=1
 #LOG_LEVEL=1
@@ -3891,12 +3929,6 @@ _initconf() {
 #AUTO_UPGRADE=\"1\"
 #AUTO_UPGRADE=\"1\"
 
 
 #NO_TIMESTAMP=1
 #NO_TIMESTAMP=1
-#OPENSSL_BIN=openssl
-
-#USER_AGENT=\"$USER_AGENT\"
-
-#USER_PATH=
-
 
 
     " >"$ACCOUNT_CONF_PATH"
     " >"$ACCOUNT_CONF_PATH"
   fi
   fi
@@ -4232,6 +4264,7 @@ Parameters:
     
     
   --webroot, -w  /path/to/webroot   Specifies the web root folder for web root mode.
   --webroot, -w  /path/to/webroot   Specifies the web root folder for web root mode.
   --standalone                      Use standalone mode.
   --standalone                      Use standalone mode.
+  --stateless                       Use stateless mode, see: $_STATELESS_WIKI
   --tls                             Use standalone tls mode.
   --tls                             Use standalone tls mode.
   --apache                          Use apache mode.
   --apache                          Use apache mode.
   --dns [dns_cf|dns_dp|dns_cx|/path/to/api/file]   Use dns mode or dns api.
   --dns [dns_cf|dns_dp|dns_cx|/path/to/api/file]   Use dns mode or dns api.
@@ -4537,6 +4570,14 @@ _process() {
           _webroot="$_webroot,$wvalue"
           _webroot="$_webroot,$wvalue"
         fi
         fi
         ;;
         ;;
+      --stateless)
+        wvalue="$MODE_STATELESS"
+        if [ -z "$_webroot" ]; then
+          _webroot="$wvalue"
+        else
+          _webroot="$_webroot,$wvalue"
+        fi
+        ;;
       --local-address)
       --local-address)
         lvalue="$2"
         lvalue="$2"
         _local_address="$_local_address$lvalue,"
         _local_address="$_local_address$lvalue,"

+ 27 - 0
deploy/kong.sh

@@ -278,6 +278,33 @@ acme.sh --issue --dns dns_linode --dnssleep 900 -d example.com -d www.example.co
 
 
 The `LINODE_API_KEY` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
 The `LINODE_API_KEY` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
 
 
+## 15. Use FreeDNS
+
+FreeDNS (https://freedns.afraid.org/) does not provide an API to update DNS records (other than IPv4 and IPv6
+dynamic DNS addresses).  The acme.sh plugin therefore retrieves and updates domain TXT records by logging
+into the FreeDNS website to read the HTML and posting updates as HTTP.  The plugin needs to know your
+userid and password for the FreeDNS website.
+
+```sh
+export FREEDNS_User="..."
+export FREEDNS_Password="..."
+```
+
+You need only provide this the first time you run the acme.sh client with FreeDNS validation and then again
+whenever you change your password at the FreeDNS site.  The acme.sh FreeDNS plugin does not store your userid
+or password but rather saves an authentication token returned by FreeDNS in `~/.acme.sh/account.conf` and
+reuses that when needed.
+
+Now you can issue a certificate.
+
+```sh
+acme.sh --issue --dns dns_freedns -d example.com -d www.example.com
+```
+
+Note that you cannot use acme.sh automatic DNS validation for FreeDNS public domains or for a subdomain that
+you create under a FreeDNS public domain.  You must own the top level domain in order to automaitcally
+validate with acme.sh at FreeDNS.
+
 # 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.

+ 375 - 0
dnsapi/dns_ad.sh

@@ -0,0 +1,375 @@
+#!/usr/bin/env sh
+
+#This file name is "dns_freedns.sh"
+#So, here must be a method dns_freedns_add()
+#Which will be called by acme.sh to add the txt record to your api system.
+#returns 0 means success, otherwise error.
+#
+#Author: David Kerr
+#Report Bugs here: https://github.com/dkerr64/acme.sh
+#
+########  Public functions #####################
+
+# Export FreeDNS userid and password in folowing variables...
+#  FREEDNS_User=username
+#  FREEDNS_Password=password
+# login cookie is saved in acme account config file so userid / pw
+# need to be set only when changed.
+
+#Usage: dns_freedns_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_freedns_add() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  _info "Add TXT record using FreeDNS"
+  _debug "fulldomain: $fulldomain"
+  _debug "txtvalue: $txtvalue"
+
+  if [ -z "$FREEDNS_User" ] || [ -z "$FREEDNS_Password" ]; then
+    FREEDNS_User=""
+    FREEDNS_Password=""
+    if [ -z "$FREEDNS_COOKIE" ]; then
+      _err "You did not specify the FreeDNS username and password yet."
+      _err "Please export as FREEDNS_User / FREEDNS_Password and try again."
+      return 1
+    fi
+    using_cached_cookies="true"
+  else
+    FREEDNS_COOKIE="$(_freedns_login "$FREEDNS_User" "$FREEDNS_Password")"
+    if [ -z "$FREEDNS_COOKIE" ]; then
+      return 1
+    fi
+    using_cached_cookies="false"
+  fi
+
+  _debug "FreeDNS login cookies: $FREEDNS_COOKIE (cached = $using_cached_cookies)"
+
+  _saveaccountconf FREEDNS_COOKIE "$FREEDNS_COOKIE"
+
+  # split our full domain name into two parts...
+  i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)"
+  i="$(_math "$i" - 1)"
+  top_domain="$(echo "$fulldomain" | cut -d. -f "$i"-100)"
+  i="$(_math "$i" - 1)"
+  sub_domain="$(echo "$fulldomain" | cut -d. -f -"$i")"
+
+  # Sometimes FreeDNS does not reurn the subdomain page but rather 
+  # returns a page regarding becoming a premium member.  This usually
+  # happens after a period of inactivity.  Immediately trying again
+  # returns the correct subdomain page.  So, we will try twice to
+  # load the page and obtain our domain ID
+  attempts=2
+  while [ "$attempts" -gt "0" ]; do
+    attempts="$(_math "$attempts" - 1)"
+
+    htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")"
+    if [ "$?" != "0" ]; then
+      if [ "$using_cached_cookies" = "true" ]; then
+        _err "Has your FreeDNS username and password channged?  If so..."
+        _err "Please export as FREEDNS_User / FREEDNS_Password and try again."
+      fi
+      return 1
+    fi
+
+    # Now convert the tables in the HTML to CSV.  This litte gem from
+    # http://stackoverflow.com/questions/1403087/how-can-i-convert-an-html-table-to-csv    
+    subdomain_csv="$(echo "$htmlpage" \
+      | grep -i -e '</\?TABLE\|</\?TD\|</\?TR\|</\?TH' \
+      | sed 's/^[\ \t]*//g' \
+      | tr -d '\n' \
+      | sed 's/<\/TR[^>]*>/\n/Ig' \
+      | sed 's/<\/\?\(TABLE\|TR\)[^>]*>//Ig' \
+      | sed 's/^<T[DH][^>]*>\|<\/\?T[DH][^>]*>$//Ig' \
+      | sed 's/<\/T[DH][^>]*><T[DH][^>]*>/,/Ig' \
+      | grep 'edit.php?' \
+      | grep "$top_domain")"
+    # The above beauty ends with striping out rows that do not have an
+    # href to edit.php and do not have the top domain we are looking for.
+    # So all we should be left with is CSV of table of subdomains we are
+    # interested in.
+
+    # Now we have to read through this table and extract the data we need
+    lines="$(echo "$subdomain_csv" | wc -l)"
+    nl='
+'
+    i=0
+    found=0
+    while [ "$i" -lt "$lines" ]; do
+      i="$(_math "$i" + 1)"
+      line="$(echo "$subdomain_csv" | cut -d "$nl" -f "$i")"
+      tmp="$(echo "$line" | cut -d ',' -f 1)"
+      if [ $found = 0 ] && _startswith "$tmp" "<td>$top_domain"; then
+        # this line will contain DNSdomainid for the top_domain
+        DNSdomainid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*domain_id=//;s/>.*//')"
+        found=1
+      else
+        # lines contain DNS records for all subdomains
+        DNSname="$(echo "$line" | cut -d ',' -f 2 | sed 's/^[^>]*>//;s/<\/a>.*//')"
+        DNStype="$(echo "$line" | cut -d ',' -f 3)"
+        if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then
+          DNSdataid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*data_id=//;s/>.*//')"
+          # Now get current value for the TXT record.  This method may
+          # not produce accurate results as the value field is truncated
+          # on this webpage. To get full value we would need to load
+          # another page. However we don't really need this so long as
+          # there is only one TXT record for the acme chalenge subdomain.
+          DNSvalue="$(echo "$line" | cut -d ',' -f 4 | sed 's/^[^&quot;]*&quot;//;s/&quot;.*//;s/<\/td>.*//')"
+          if [ $found != 0 ]; then
+            break
+            # we are breaking out of the loop at the first match of DNS name
+            # and DNS type (if we are past finding the domainid). This assumes
+            # that there is only ever one TXT record for the LetsEncrypt/acme
+            # challenge subdomain.  This seems to be a reasonable assumption
+            # as the acme client deletes the TXT record on successful validation.
+          fi
+        else
+          DNSname=""
+          DNStype=""
+        fi
+      fi
+    done
+
+    _debug "DNSname: $DNSname DNStype: $DNStype DNSdomainid: $DNSdomainid DNSdataid: $DNSdataid"
+    _debug "DNSvalue: $DNSvalue"
+
+    if [ -z "$DNSdomainid" ]; then
+      # If domain ID is empty then something went wrong (top level
+      # domain not found at FreeDNS).
+      if [ "$attempts" = "0" ]; then
+        # exhausted maximum retry attempts
+        _debug "$htmlpage"
+        _debug "$subdomain_csv"
+        _err "Domain $top_domain not found at FreeDNS"
+        return 1
+      fi
+    else
+      # break out of the 'retry' loop... we have found our domain ID
+      break
+    fi
+    _info "Domain $top_domain not found at FreeDNS"
+    _info "Retry loading subdomain page ($attempts attempts remaining)"
+  done
+
+  if [ -z "$DNSdataid" ]; then
+    # If data ID is empty then specific subdomain does not exist yet, need
+    # to create it this should always be the case as the acme client
+    # deletes the entry after domain is validated.
+    _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue"
+    return $?
+  else
+    if [ "$txtvalue" = "$DNSvalue" ]; then
+      # if value in TXT record matches value requested then DNS record
+      # does not need to be updated. But...
+      # Testing value match fails.  Website is truncating the value field.
+      # So for now we will always go down the else path.  Though in theory
+      # should never come here anyway as the acme client deletes
+      # the TXT record on successful validation, so we should not even
+      # have found a TXT record !!
+      _info "No update necessary for $fulldomain at FreeDNS"
+      return 0
+    else
+      # Delete the old TXT record (with the wrong value)
+      _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid"
+      if [ "$?" = "0" ]; then
+        # And add in new TXT record with the value provided
+        _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue"
+      fi
+      return $?
+    fi
+  fi
+  return 0
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_freedns_rm() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  _info "Delete TXT record using FreeDNS"
+  _debug "fulldomain: $fulldomain"
+  _debug "txtvalue: $txtvalue"
+
+  # Need to read cookie from conf file again in case new value set
+  # during login to FreeDNS when TXT record was created.
+  # acme.sh does not have a _readaccountconf() fuction
+  FREEDNS_COOKIE="$(_read_conf "$ACCOUNT_CONF_PATH" "FREEDNS_COOKIE")"
+  _debug "FreeDNS login cookies: $FREEDNS_COOKIE"
+
+  # Sometimes FreeDNS does not reurn the subdomain page but rather 
+  # returns a page regarding becoming a premium member.  This usually
+  # happens after a period of inactivity.  Immediately trying again
+  # returns the correct subdomain page.  So, we will try twice to
+  # load the page and obtain our TXT record.
+  attempts=2
+  while [ "$attempts" -gt "0" ]; do
+    attempts="$(_math "$attempts" - 1)"
+
+    htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")"
+    if [ "$?" != "0" ]; then
+      return 1
+    fi
+
+    # Now convert the tables in the HTML to CSV.  This litte gem from
+    # http://stackoverflow.com/questions/1403087/how-can-i-convert-an-html-table-to-csv
+    subdomain_csv="$(echo "$htmlpage" \
+      | grep -i -e '</\?TABLE\|</\?TD\|</\?TR\|</\?TH' \
+      | sed 's/^[\ \t]*//g' \
+      | tr -d '\n' \
+      | sed 's/<\/TR[^>]*>/\n/Ig' \
+      | sed 's/<\/\?\(TABLE\|TR\)[^>]*>//Ig' \
+      | sed 's/^<T[DH][^>]*>\|<\/\?T[DH][^>]*>$//Ig' \
+      | sed 's/<\/T[DH][^>]*><T[DH][^>]*>/,/Ig' \
+      | grep 'edit.php?' \
+      | grep "$fulldomain")"
+    # The above beauty ends with striping out rows that do not have an
+    # href to edit.php and do not have the domain name we are looking for.
+    # So all we should be left with is CSV of table of subdomains we are
+    # interested in.
+
+    # Now we have to read through this table and extract the data we need
+    lines="$(echo "$subdomain_csv" | wc -l)"
+    nl='
+'
+    i=0
+    found=0
+    while [ "$i" -lt "$lines" ]; do
+      i="$(_math "$i" + 1)"
+      line="$(echo "$subdomain_csv" | cut -d "$nl" -f "$i")"
+      DNSname="$(echo "$line" | cut -d ',' -f 2 | sed 's/^[^>]*>//;s/<\/a>.*//')"
+      DNStype="$(echo "$line" | cut -d ',' -f 3)"
+      if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then
+        DNSdataid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*data_id=//;s/>.*//')"
+        DNSvalue="$(echo "$line" | cut -d ',' -f 4 | sed 's/^[^&quot;]*&quot;//;s/&quot;.*//;s/<\/td>.*//')"
+        _debug "DNSvalue: $DNSvalue"
+        #     if [ "$DNSvalue" = "$txtvalue" ]; then
+        # Testing value match fails.  Website is truncating the value
+        # field. So for now we will assume that there is only one TXT
+        # field for the sub domain and just delete it. Currently this
+        # is a safe assumption.
+        _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid"
+        return $?
+        #     fi
+      fi
+    done
+  done
+
+  # If we get this far we did not find a match (after two attempts)
+  # Not necessarily an error, but log anyway.
+  _debug2 "$subdomain_csv"
+  _info "Cannot delete TXT record for $fulldomain/$txtvalue. Does not exist at FreeDNS"
+  return 0
+}
+
+####################  Private functions below ##################################
+
+# usage: _freedns_login username password
+# print string "cookie=value" etc.
+# returns 0 success
+_freedns_login() {
+  export _H1="Accept-Language:en-US"
+  username="$1"
+  password="$2"
+  url="https://freedns.afraid.org/zc.php?step=2"
+
+  _debug "Login to FreeDNS as user $username"
+
+  htmlpage="$(_post "username=$(printf '%s' "$username" | _url_encode)&password=$(printf '%s' "$password" | _url_encode)&submit=Login&action=auth" "$url")"
+
+  if [ "$?" != "0" ]; then
+    _err "FreeDNS login failed for user $username bad RC from _post"
+    return 1
+  fi
+
+  cookies="$(grep -i '^Set-Cookie.*dns_cookie.*$' "$HTTP_HEADER" | _head_n 1 | tr -d "\r\n" | cut -d " " -f 2)"
+
+  # if cookies is not empty then logon successful
+  if [ -z "$cookies" ]; then
+    _debug "$htmlpage"
+    _err "FreeDNS login failed for user $username. Check $HTTP_HEADER file"
+    return 1
+  fi
+
+  printf "%s" "$cookies"
+  return 0
+}
+
+# usage _freedns_retrieve_subdomain_page login_cookies
+# echo page retrieved (html)
+# returns 0 success
+_freedns_retrieve_subdomain_page() {
+  export _H1="Cookie:$1"
+  export _H2="Accept-Language:en-US"
+  url="https://freedns.afraid.org/subdomain/"
+
+  _debug "Retrieve subdmoain page from FreeDNS"
+
+  htmlpage="$(_get "$url")"
+
+  if [ "$?" != "0" ]; then
+    _err "FreeDNS retrieve subdomins failed bad RC from _get"
+    return 1
+  elif [ -z "$htmlpage" ]; then
+    _err "FreeDNS returned empty subdomain page"
+    return 1
+  fi
+
+  _debug2 "$htmlpage"
+
+  printf "%s" "$htmlpage"
+  return 0
+}
+
+# usage _freedns_add_txt_record login_cookies domain_id subdomain value
+# returns 0 success
+_freedns_add_txt_record() {
+  export _H1="Cookie:$1"
+  export _H2="Accept-Language:en-US"
+  domain_id="$2"
+  subdomain="$3"
+  value="$(printf '%s' "$4" | _url_encode)"
+  url="http://freedns.afraid.org/subdomain/save.php?step=2"
+
+  htmlpage="$(_post "type=TXT&domain_id=$domain_id&subdomain=$subdomain&address=%22$value%22&send=Save%21" "$url")"
+
+  if [ "$?" != "0" ]; then
+    _err "FreeDNS failed to add TXT record for $subdomain bad RC from _post"
+    return 1
+  elif ! grep "200 OK" "$HTTP_HEADER" >/dev/null; then
+    _debug "$htmlpage"
+    _err "FreeDNS failed to add TXT record for $subdomain. Check $HTTP_HEADER file"
+    return 1
+  elif _contains "$htmlpage" "security code was incorrect"; then
+    _debug "$htmlpage"
+    _err "FreeDNS failed to add TXT record for $subdomain as FreeDNS requested seurity code"
+    _err "Note that you cannot use automatic DNS validation for FreeDNS public domains"
+    return 1
+  fi
+
+  _debug2 "$htmlpage"
+  _info "Added acme challenge TXT record for $fulldomain at FreeDNS"
+  return 0
+}
+
+# usage _freedns_delete_txt_record login_cookies data_id
+# returns 0 success
+_freedns_delete_txt_record() {
+  export _H1="Cookie:$1"
+  export _H2="Accept-Language:en-US"
+  data_id="$2"
+  url="https://freedns.afraid.org/subdomain/delete2.php"
+
+  htmlheader="$(_get "$url?data_id%5B%5D=$data_id&submit=delete+selected" "onlyheader")"
+
+  if [ "$?" != "0" ]; then
+    _err "FreeDNS failed to delete TXT record for $data_id bad RC from _get"
+    return 1
+  elif ! _contains "$htmlheader" "200 OK"; then
+    _debug "$htmlheader"
+    _err "FreeDNS failed to delete TXT record $data_id"
+    return 1
+  fi
+
+  _info "Deleted acme challenge TXT record for $fulldomain at FreeDNS"
+  return 0
+}