Browse Source

sync (#2404)

* support jdcloud.com

* fix format

* ttl 3000

* Escape slashes (#2375)

* Change 1.1.1.1 to 1.0.0.1 to probe compatibility (#2330)

As we can see, 1.1.1.1 is not routed or routed to an Intranet devices due to historical reason. Change 1.1.1.1 to 1.0.0.1 will have a better compatibility. I found this problem on my Tencent Cloud server.

* check empty id

* fix error

* Add dnsapi for Vultr (#2370)

* Add Vultr dns api

* PushOver notifications (#2325)

* PushOver notifications, using AppToken, UserKey, and optional sounds

* fix errors

* added dns api support for hexonet (#1776)

* update

* minor

* support new Cloudflare Token format
fix https://github.com/Neilpang/acme.sh/issues/2398

* fix wildcard domain name

* add more info

* fix https://github.com/Neilpang/acme.sh/issues/2377

* fix format

* fix format
neil 5 years ago
parent
commit
55dea4ee9d
8 changed files with 443 additions and 49 deletions
  1. 4 2
      acme.sh
  2. 3 1
      deploy/docker.sh
  3. 45 42
      dnsapi/dns_cf.sh
  4. 156 0
      dnsapi/dns_hexonet.sh
  5. 1 1
      dnsapi/dns_namecheap.sh
  6. 8 3
      dnsapi/dns_namesilo.sh
  7. 163 0
      dnsapi/dns_vultr.sh
  8. 63 0
      notify/pushover.sh

+ 4 - 2
acme.sh

@@ -3035,11 +3035,13 @@ _clearupdns() {
     d=$(_getfield "$entry" 1)
     txtdomain=$(_getfield "$entry" 2)
     aliasDomain=$(_getfield "$entry" 3)
+    _currentRoot=$(_getfield "$entry" 4)
     txt=$(_getfield "$entry" 5)
     d_api=$(_getfield "$entry" 6)
     _debug "d" "$d"
     _debug "txtdomain" "$txtdomain"
     _debug "aliasDomain" "$aliasDomain"
+    _debug "_currentRoot" "$_currentRoot"
     _debug "txt" "$txt"
     _debug "d_api" "$d_api"
     if [ "$d_api" = "$txt" ]; then
@@ -3621,7 +3623,7 @@ _ns_purge_cf() {
   _cf_d="$1"
   _cf_d_type="$2"
   _debug "Cloudflare purge $_cf_d_type record for domain $_cf_d"
-  _cf_purl="https://1.1.1.1/api/v1/purge?domain=$_cf_d&type=$_cf_d_type"
+  _cf_purl="https://1.0.0.1/api/v1/purge?domain=$_cf_d&type=$_cf_d_type"
   response="$(_post "" "$_cf_purl")"
   _debug2 response "$response"
 }
@@ -6787,7 +6789,7 @@ _process() {
       _debug "Using server: $_server"
     fi
   fi
-
+  _debug "Running cmd: ${_CMD}"
   case "${_CMD}" in
     install) install "$_nocron" "$_confighome" "$_noprofile" ;;
     uninstall) uninstall "$_nocron" ;;

+ 3 - 1
deploy/docker.sh

@@ -126,6 +126,7 @@ docker_deploy() {
   fi
 
   if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then
+    _info "Reloading: $DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"
     if ! _docker_exec "$_cid" "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"; then
       return 1
     fi
@@ -223,7 +224,8 @@ _docker_cp() {
     _debug2 "_frompath" "$_frompath"
     _toname="$(basename "$_to")"
     _debug2 "_toname" "$_toname"
-    if ! tar --transform="s,$_frompath,$_toname," -cz "$_from" 2>/dev/null | _curl_unix_sock "$_DOCKER_SOCK" PUT "/containers/$_dcid/archive?noOverwriteDirNonDir=1&path=$(printf "%s" "$_dir" | _url_encode)" '@-' "Content-Type: application/octet-stream"; then
+    _debug2 "_from" "$_from"
+    if ! tar --transform="s,$(printf "%s" "$_frompath" | tr '*' .),$_toname," -cz "$_from" 2>/dev/null | _curl_unix_sock "$_DOCKER_SOCK" PUT "/containers/$_dcid/archive?noOverwriteDirNonDir=1&path=$(printf "%s" "$_dir" | _url_encode)" '@-' "Content-Type: application/octet-stream"; then
       _err "copy error"
       return 1
     fi

+ 45 - 42
dnsapi/dns_cf.sh

@@ -5,6 +5,9 @@
 #
 #CF_Email="xxxx@sss.com"
 
+#CF_Token="xxxx"
+#CF_Account_ID="xxxx"
+
 CF_Api="https://api.cloudflare.com/client/v4"
 
 ########  Public functions #####################
@@ -14,25 +17,32 @@ dns_cf_add() {
   fulldomain=$1
   txtvalue=$2
 
+  CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
+  CF_Account_ID="${CF_Account_ID:-$(_readaccountconf_mutable CF_Account_ID)}"
   CF_Key="${CF_Key:-$(_readaccountconf_mutable CF_Key)}"
   CF_Email="${CF_Email:-$(_readaccountconf_mutable CF_Email)}"
-  if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
-    CF_Key=""
-    CF_Email=""
-    _err "You didn't specify a Cloudflare api key and email yet."
-    _err "You can get yours from here https://dash.cloudflare.com/profile."
-    return 1
-  fi
 
-  if ! _contains "$CF_Email" "@"; then
-    _err "It seems that the CF_Email=$CF_Email is not a valid email address."
-    _err "Please check and retry."
-    return 1
-  fi
+  if [ "$CF_Token" ]; then
+    _saveaccountconf_mutable CF_Token "$CF_Token"
+    _saveaccountconf_mutable CF_Account_ID "$CF_Account_ID"
+  else
+    if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
+      CF_Key=""
+      CF_Email=""
+      _err "You didn't specify a Cloudflare api key and email yet."
+      _err "You can get yours from here https://dash.cloudflare.com/profile."
+      return 1
+    fi
 
-  #save the api key and email to the account conf file.
-  _saveaccountconf_mutable CF_Key "$CF_Key"
-  _saveaccountconf_mutable CF_Email "$CF_Email"
+    if ! _contains "$CF_Email" "@"; then
+      _err "It seems that the CF_Email=$CF_Email is not a valid email address."
+      _err "Please check and retry."
+      return 1
+    fi
+    #save the api key and email to the account conf file.
+    _saveaccountconf_mutable CF_Key "$CF_Key"
+    _saveaccountconf_mutable CF_Email "$CF_Email"
+  fi
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -71,19 +81,6 @@ dns_cf_add() {
   fi
   _err "Add txt record error."
   return 1
-  #  else
-  #    _info "Updating record"
-  #    record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1)
-  #    _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, OK"
-  #      return 0
-  #    fi
-  #    _err "Update error"
-  #    return 1
-  #  fi
 
 }
 
@@ -92,15 +89,10 @@ dns_cf_rm() {
   fulldomain=$1
   txtvalue=$2
 
+  CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
+  CF_Account_ID="${CF_Account_ID:-$(_readaccountconf_mutable CF_Account_ID)}"
   CF_Key="${CF_Key:-$(_readaccountconf_mutable CF_Key)}"
   CF_Email="${CF_Email:-$(_readaccountconf_mutable CF_Email)}"
-  if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
-    CF_Key=""
-    CF_Email=""
-    _err "You didn't specify a Cloudflare api key and email yet."
-    _err "You can get yours from here https://dash.cloudflare.com/profile."
-    return 1
-  fi
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -157,8 +149,14 @@ _get_root() {
       return 1
     fi
 
-    if ! _cf_rest GET "zones?name=$h"; then
-      return 1
+    if [ "$CF_Account_ID" ]; then
+      if ! _cf_rest GET "zones?name=$h&account.id=$CF_Account_ID"; then
+        return 1
+      fi
+    else
+      if ! _cf_rest GET "zones?name=$h"; then
+        return 1
+      fi
     fi
 
     if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_count":1'; then
@@ -182,12 +180,17 @@ _cf_rest() {
   data="$3"
   _debug "$ep"
 
-  email_trimmed=$(echo $CF_Email | tr -d '"')
-  key_trimmed=$(echo $CF_Key | tr -d '"')
+  email_trimmed=$(echo "$CF_Email" | tr -d '"')
+  key_trimmed=$(echo "$CF_Key" | tr -d '"')
+  token_trimmed=$(echo "$CF_Token" | tr -d '"')
 
-  export _H1="X-Auth-Email: $email_trimmed"
-  export _H2="X-Auth-Key: $key_trimmed"
-  export _H3="Content-Type: application/json"
+  export _H1="Content-Type: application/json"
+  if [ "$token_trimmed" ]; then
+    export _H2="Authorization: Bearer $token_trimmed"
+  else
+    export _H2="X-Auth-Email: $email_trimmed"
+    export _H3="X-Auth-Key: $key_trimmed"
+  fi
 
   if [ "$m" != "GET" ]; then
     _debug data "$data"

+ 156 - 0
dnsapi/dns_hexonet.sh

@@ -0,0 +1,156 @@
+#!/usr/bin/env sh
+
+#
+# Hexonet_Login="username!roleId"
+#
+# Hexonet_Password="rolePassword"
+
+Hexonet_Api="https://coreapi.1api.net/api/call.cgi"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_hexonet_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  Hexonet_Login="${Hexonet_Login:-$(_readaccountconf_mutable Hexonet_Login)}"
+  Hexonet_Password="${Hexonet_Password:-$(_readaccountconf_mutable Hexonet_Password)}"
+  if [ -z "$Hexonet_Login" ] || [ -z "$Hexonet_Password" ]; then
+    Hexonet_Login=""
+    Hexonet_Password=""
+    _err "You must export variables: Hexonet_Login and Hexonet_Password"
+    return 1
+  fi
+
+  if ! _contains "$Hexonet_Login" "!"; then
+    _err "It seems that the Hexonet_Login=$Hexonet_Login is not a restrivteed user."
+    _err "Please check and retry."
+    return 1
+  fi
+
+  #save the username and password to the account conf file.
+  _saveaccountconf_mutable Hexonet_Login "$Hexonet_Login"
+  _saveaccountconf_mutable Hexonet_Password "$Hexonet_Password"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug "Getting txt records"
+  _hexonet_rest "&command=QueryDNSZoneRRList&dnszone=${h}.&RRTYPE=TXT"
+
+  if ! _contains "$response" "CODE=200"; then
+    _err "Error"
+    return 1
+  fi
+
+  _info "Adding record"
+  if _hexonet_rest "command=UpdateDNSZone&dnszone=${_domain}.&addrr0=${_sub_domain}%20IN%20TXT%20${txtvalue}"; then
+    if _contains "$response" "CODE=200"; then
+      _info "Added, OK"
+      return 0
+    else
+      _err "Add txt record error."
+      return 1
+    fi
+  fi
+  _err "Add txt record error."
+  return 1
+
+}
+
+#fulldomain txtvalue
+dns_hexonet_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  Hexonet_Login="${Hexonet_Login:-$(_readaccountconf_mutable Hexonet_Login)}"
+  Hexonet_Password="${Hexonet_Password:-$(_readaccountconf_mutable Hexonet_Password)}"
+  if [ -z "$Hexonet_Login" ] || [ -z "$Hexonet_Password" ]; then
+    Hexonet_Login=""
+    Hexonet_Password=""
+    _err "You must export variables: Hexonet_Login and Hexonet_Password"
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug "Getting txt records"
+  _hexonet_rest "&command=QueryDNSZoneRRList&dnszone=${h}.&RRTYPE=TXT&RR=${txtvalue}"
+
+  if ! _contains "$response" "CODE=200"; then
+    _err "Error"
+    return 1
+  fi
+
+  count=$(printf "%s\n" "$response" | _egrep_o "PROPERTY[TOTAL][0]=" | cut -d = -f 2)
+  _debug count "$count"
+  if [ "$count" = "0" ]; then
+    _info "Don't need to remove."
+  else
+    if ! _hexonet_rest "&command=UpdateDNSZone&dnszone=${_domain}.&delrr0='${_sub_domain}%20IN%20TXT%20\"${txtvalue}\""; then
+      _err "Delete record error."
+      return 1
+    fi
+    _contains "$response" "CODE=200"
+  fi
+
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+_get_root() {
+  domain=$1
+  i=1
+  p=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if ! _hexonet_rest "&command=QueryDNSZoneRRList&dnszone=${h}."; then
+      return 1
+    fi
+
+    if _contains "$response" "CODE=200"; then
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _domain=$h
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+_hexonet_rest() {
+  query_params="$1"
+  _debug "$query_params"
+
+  response="$(_get "${Hexonet_Api}?s_login=${Hexonet_Login}&s_pw=${Hexonet_Password}&${query_params}")"
+
+  if [ "$?" != "0" ]; then
+    _err "error $query_params"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 1 - 1
dnsapi/dns_namecheap.sh

@@ -164,7 +164,7 @@ _namecheap_set_publicip() {
     _debug sourceip "$NAMECHEAP_SOURCEIP"
 
     ip=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
-    addr=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '(http|https)://.*')
+    addr=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '(http|https):\/\/.*')
 
     _debug2 ip "$ip"
     _debug2 addr "$addr"

+ 8 - 3
dnsapi/dns_namesilo.sh

@@ -59,9 +59,14 @@ dns_namesilo_rm() {
   if _namesilo_rest GET "dnsListRecords?version=1&type=xml&key=$Namesilo_Key&domain=$_domain"; then
     retcode=$(printf "%s\n" "$response" | _egrep_o "<code>300")
     if [ "$retcode" ]; then
-      _record_id=$(printf "%s\n" "$response" | _egrep_o "<record_id>([^<]*)</record_id><type>TXT</type><host>$fulldomain</host>" | _egrep_o "<record_id>([^<]*)</record_id>" | sed -r "s/<record_id>([^<]*)<\/record_id>/\1/" | tail -n 1)
-      _debug record_id "$_record_id"
-      _info "Successfully retrieved the record id for ACME challenge."
+      _record_id=$(echo "$response" | _egrep_o "<record_id>([^<]*)</record_id><type>TXT</type><host>$fulldomain</host>" | _egrep_o "<record_id>([^<]*)</record_id>" | sed -r "s/<record_id>([^<]*)<\/record_id>/\1/" | tail -n 1)
+      _debug _record_id "$_record_id"
+      if [ "$_record_id" ]; then
+        _info "Successfully retrieved the record id for ACME challenge."
+      else
+        _info "Empty record id, it seems no such record."
+        return 0
+      fi
     else
       _err "Unable to retrieve the record id."
       return 1

+ 163 - 0
dnsapi/dns_vultr.sh

@@ -0,0 +1,163 @@
+#!/usr/bin/env sh
+
+#
+#VULTR_API_KEY=000011112222333344445555666677778888
+
+VULTR_Api="https://api.vultr.com/v1"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_vultr_add() {
+  fulldomain=$1
+  txtvalue=$2
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  VULTR_API_KEY="${VULTR_API_KEY:-$(_readaccountconf_mutable VULTR_API_KEY)}"
+  if test -z "$VULTR_API_KEY"; then
+    VULTR_API_KEY=''
+    _err 'VULTR_API_KEY was not exported'
+    return 1
+  fi
+
+  _saveaccountconf_mutable VULTR_API_KEY "$VULTR_API_KEY"
+
+  _debug 'First detect the root zone'
+  if ! _get_root "$fulldomain"; then
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug 'Getting txt records'
+  _vultr_rest GET "dns/records?domain=$_domain"
+
+  if printf "%s\n" "$response" | grep "\"type\":\"TXT\",\"name\":\"$fulldomain\"" >/dev/null; then
+    _err 'Error'
+    return 1
+  fi
+
+  if ! _vultr_rest POST 'dns/create_record' "domain=$_domain&name=$_sub_domain&data=\"$txtvalue\"&type=TXT"; then
+    _err "$response"
+    return 1
+  fi
+
+  _debug2 _response "$response"
+  return 0
+}
+
+#fulldomain txtvalue
+dns_vultr_rm() {
+  fulldomain=$1
+  txtvalue=$2
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  VULTR_API_KEY="${VULTR_API_KEY:-$(_readaccountconf_mutable VULTR_API_KEY)}"
+  if test -z "$VULTR_API_KEY"; then
+    VULTR_API_KEY=""
+    _err 'VULTR_API_KEY was not exported'
+    return 1
+  fi
+
+  _saveaccountconf_mutable VULTR_API_KEY "$VULTR_API_KEY"
+
+  _debug 'First detect the root zone'
+  if ! _get_root "$fulldomain"; then
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug 'Getting txt records'
+  _vultr_rest GET "dns/records?domain=$_domain"
+
+  if printf "%s\n" "$response" | grep "\"type\":\"TXT\",\"name\":\"$fulldomain\"" >/dev/null; then
+    _err 'Error'
+    return 1
+  fi
+
+  _record_id="$(echo "$response" | tr '{}' '\n' | grep '"TXT"' | grep "$txtvalue" | tr ',' '\n' | grep -i 'RECORDID' | cut -d : -f 2)"
+  _debug _record_id "$_record_id"
+  if [ "$_record_id" ]; then
+    _info "Successfully retrieved the record id for ACME challenge."
+  else
+    _info "Empty record id, it seems no such record."
+    return 0
+  fi
+
+  if ! _vultr_rest POST 'dns/delete_record' "domain=$_domain&RECORDID=$_record_id"; then
+    _err "$response"
+    return 1
+  fi
+
+  _debug2 _response "$response"
+  return 0
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain=$1
+  i=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      return 1
+    fi
+
+    if ! _vultr_rest GET "dns/list"; then
+      return 1
+    fi
+
+    if printf "%s\n" "$response" | grep '^\[.*\]' >/dev/null; then
+      if _contains "$response" "\"domain\":\"$_domain\""; then
+        _sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")"
+        _domain=$_domain
+        return 0
+      else
+        _err 'Invalid domain'
+        return 1
+      fi
+    else
+      _err "$response"
+      return 1
+    fi
+    i=$(_math "$i" + 1)
+  done
+
+  return 1
+}
+
+_vultr_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  api_key_trimmed=$(echo $VULTR_API_KEY | tr -d '"')
+
+  export _H1="Api-Key: $api_key_trimmed"
+  export _H2='Content-Type: application/x-www-form-urlencoded'
+
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$VULTR_Api/$ep" "" "$m")"
+  else
+    response="$(_get "$VULTR_Api/$ep")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "Error $ep"
+    return 1
+  fi
+
+  _debug2 response "$response"
+  return 0
+}

+ 63 - 0
notify/pushover.sh

@@ -0,0 +1,63 @@
+#!/usr/bin/env sh
+
+#Support for pushover.net's api. Push notification platform for multiple platforms
+#PUSHOVER_TOKEN="" Required, pushover application token
+#PUSHOVER_USER="" Required, pushover userkey
+#PUSHOVER_DEVICE="" Optional, Specific device or devices by hostnames, joining multiples with a comma (such as device=iphone,nexus5)
+#PUSHOVER_PRIORITY="" Optional, Lowest Priority (-2), Low Priority (-1), Normal Priority (0), High Priority (1)
+
+PUSHOVER_URI="https://api.pushover.net/1/messages.json"
+
+pushover_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_statusCode" "$_statusCode"
+
+  PUSHOVER_TOKEN="${PUSHOVER_TOKEN:-$(_readaccountconf_mutable PUSHOVER_TOKEN)}"
+  if [ -z "$PUSHOVER_TOKEN" ]; then
+    PUSHOVER_TOKEN=""
+    _err "You didn't specify a PushOver application token yet."
+    return 1
+  fi
+  _saveaccountconf_mutable PUSHOVER_TOKEN "$PUSHOVER_TOKEN"
+
+  PUSHOVER_USER="${PUSHOVER_USER:-$(_readaccountconf_mutable PUSHOVER_USER)}"
+  if [ -z "$PUSHOVER_USER" ]; then
+    PUSHOVER_USER=""
+    _err "You didn't specify a PushOver UserKey yet."
+    return 1
+  fi
+  _saveaccountconf_mutable PUSHOVER_USER "$PUSHOVER_USER"
+
+  PUSHOVER_DEVICE="${PUSHOVER_DEVICE:-$(_readaccountconf_mutable PUSHOVER_DEVICE)}"
+  if [ "$PUSHOVER_DEVICE" ]; then
+    _saveaccountconf_mutable PUSHOVER_DEVICE "$PUSHOVER_DEVICE"
+  fi
+
+  PUSHOVER_PRIORITY="${PUSHOVER_PRIORITY:-$(_readaccountconf_mutable PUSHOVER_PRIORITY)}"
+  if [ "$PUSHOVER_PRIORITY" ]; then
+    _saveaccountconf_mutable PUSHOVER_PRIORITY "$PUSHOVER_PRIORITY"
+  fi
+
+  PUSHOVER_SOUND="${PUSHOVER_SOUND:-$(_readaccountconf_mutable PUSHOVER_SOUND)}"
+  if [ "$PUSHOVER_SOUND" ]; then
+    _saveaccountconf_mutable PUSHOVER_SOUND "$PUSHOVER_SOUND"
+  fi
+
+  export _H1="Content-Type: application/json"
+  _content="$(printf "*%s*\n" "$_content" | _json_encode)"
+  _subject="$(printf "*%s*\n" "$_subject" | _json_encode)"
+  _data="{\"token\": \"$PUSHOVER_TOKEN\",\"user\": \"$PUSHOVER_USER\",\"title\": \"$_subject\",\"message\": \"$_content\",\"sound\": \"$PUSHOVER_SOUND\", \"device\": \"$PUSHOVER_DEVICE\", \"priority\": \"$PUSHOVER_PRIORITY\"}"
+
+  response="" #just make shellcheck happy
+  if _post "$_data" "$PUSHOVER_URI"; then
+    if _contains "$response" "{\"status\":1"; then
+      _info "PUSHOVER send success."
+      return 0
+    fi
+  fi
+  _err "PUSHOVER send error."
+  _err "$response"
+  return 1
+}