浏览代码

Merge branch 'dev' into master

keryfan 4 月之前
父节点
当前提交
289d895423

+ 8 - 6
.github/workflows/pr_dns.yml

@@ -20,12 +20,14 @@ jobs:
               owner: context.repo.owner,
               repo: context.repo.repo,
               body: `**Welcome**
-                First thing: don't send PR to the master branch, please send to the dev branch instead.
-                Please make sure you've read our [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide) and [DNS-API-Test](../wiki/DNS-API-Test).
-                Then reply on this message, otherwise, your code will not be reviewed or merged.
-                Please also make sure to add/update the usage here: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2
-                We look forward to reviewing your Pull request shortly ✨
-                注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试.
+                    READ ME !!!!!
+                    Read me !!!!!!
+                    First thing: don't send PR to the master branch, please send to the dev branch instead.
+                    Please read the [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide).
+                    You MUST pass the [DNS-API-Test](../wiki/DNS-API-Test).
+                    Then reply on this message, otherwise, your code will not be reviewed or merged.
+                    Please also make sure to add/update the usage here: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2
+                    注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试.
                 `
             })
 

+ 8 - 1
acme.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-VER=3.1.1
+VER=3.1.2
 
 PROJECT_NAME="acme.sh"
 
@@ -5504,6 +5504,13 @@ renew() {
   if [ -z "$Le_Keylength" ]; then
     Le_Keylength=2048
   fi
+  if [ "$CA_LETSENCRYPT_V2" = "$Le_API" ]; then
+    #letsencrypt doesn't support ocsp anymore
+    if [ "$Le_OCSP_Staple" ]; then
+      export Le_OCSP_Staple=""
+      _cleardomainconf Le_OCSP_Staple
+    fi
+  fi
   issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To"
   res="$?"
   if [ "$res" != "0" ]; then

+ 98 - 0
deploy/kemplm.sh

@@ -0,0 +1,98 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to a Kemp Loadmaster.
+
+#returns 0 means success, otherwise error.
+
+#DEPLOY_KEMP_TOKEN="token"
+#DEPLOY_KEMP_URL="https://kemplm.example.com"
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+kemplm_deploy() {
+  _domain="$1"
+  _key_file="$2"
+  _cert_file="$3"
+  _ca_file="$4"
+  _fullchain_file="$5"
+
+  _debug _domain "$_domain"
+  _debug _key_file "$_key_file"
+  _debug _cert_file "$_cert_file"
+  _debug _ca_file "$_ca_file"
+  _debug _fullchain_file "$_fullchain_file"
+
+  if ! _exists jq; then
+    _err "jq not found"
+    return 1
+  fi
+
+  # Rename wildcard certs, kemp accepts only alphanumeric names so we delete '*.' from filename
+  _kemp_domain=$(echo "${_domain}" | sed 's/\*\.//')
+  _debug _kemp_domain "$_kemp_domain"
+
+  # Read config from saved values or env
+  _getdeployconf DEPLOY_KEMP_TOKEN
+  _getdeployconf DEPLOY_KEMP_URL
+
+  _debug DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL"
+  _secure_debug DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN"
+
+  if [ -z "$DEPLOY_KEMP_TOKEN" ]; then
+    _err "Kemp Loadmaster token is not found, please define DEPLOY_KEMP_TOKEN."
+    return 1
+  fi
+  if [ -z "$DEPLOY_KEMP_URL" ]; then
+    _err "Kemp Loadmaster URL is not found, please define DEPLOY_KEMP_URL."
+    return 1
+  fi
+
+  # Save current values
+  _savedeployconf DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN"
+  _savedeployconf DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL"
+
+  # Check if certificate is already installed
+  _info "Check if certificate is already present"
+  _list_request="{\"cmd\": \"listcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\"}"
+  _debug3 _list_request "${_list_request}"
+  _kemp_cert_count=$(HTTPS_INSECURE=1 _post "${_list_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "${_kemp_domain}")
+  _debug2 _kemp_cert_count "${_kemp_cert_count}"
+
+  _kemp_replace_cert=1
+  if [ "${_kemp_cert_count}" -eq 0 ]; then
+    _kemp_replace_cert=0
+    _info "Certificate does not exist on Kemp Loadmaster"
+  else
+    _info "Certificate already exists on Kemp Loadmaster"
+  fi
+  _debug _kemp_replace_cert "${_kemp_replace_cert}"
+
+  # Upload new certificate to Kemp Loadmaster
+  _kemp_upload_cert=$(_mktemp)
+  cat "${_fullchain_file}" "${_key_file}" | base64 | tr -d '\n' >"${_kemp_upload_cert}"
+
+  _info "Uploading certificate to Kemp Loadmaster"
+  _add_data=$(cat "${_kemp_upload_cert}")
+  _add_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"${_add_data}\"}"
+  _debug3 _add_request "${_add_request}"
+  _kemp_post_result=$(HTTPS_INSECURE=1 _post "${_add_request}" "${DEPLOY_KEMP_URL}/accessv2")
+  _retval=$?
+  _debug2 _kemp_post_result "${_kemp_post_result}"
+  if [ "${_retval}" -eq 0 ]; then
+    _kemp_post_status=$(echo "${_kemp_post_result}" | jq -r '.status')
+    _kemp_post_message=$(echo "${_kemp_post_result}" | jq -r '.message')
+    if [ "${_kemp_post_status}" = "ok" ]; then
+      _info "Upload successful"
+    else
+      _err "Upload failed: ${_kemp_post_message}"
+    fi
+  else
+    _err "Upload failed"
+    _retval=1
+  fi
+
+  rm "${_kemp_upload_cert}"
+
+  return $_retval
+}

+ 78 - 47
deploy/truenas_ws.sh

@@ -52,6 +52,39 @@ _ws_call() {
   return 0
 }
 
+# Upload certificate with webclient api
+_ws_upload_cert() {
+
+  /usr/bin/env python - <<EOF
+
+import sys
+
+from truenas_api_client import Client
+with Client() as c:
+
+  ### Login with API key
+  print("I:Trying to upload new certificate...")
+  ret = c.call("auth.login_with_api_key", "${DEPLOY_TRUENAS_APIKEY}")
+  if ret:
+    ### upload certificate
+    with open('$1', 'r') as file:
+      fullchain = file.read()
+    with open('$2', 'r') as file:
+      privatekey = file.read()
+    ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey, "passphrase": ""}, job=True)
+    print("R:" + str(ret["id"]))
+    sys.exit(0)
+  else:
+    print("R:0")
+    print("E:_ws_upload_cert error!")
+    sys.exit(7)
+
+EOF
+
+  return $?
+
+}
+
 # Check argument is a number
 # Usage:
 #
@@ -129,7 +162,6 @@ _ws_get_job_result() {
 #  5: WebUI cert error
 #  6: Job error
 #  7: WS call error
-# 10: No CORE or SCALE detected
 #
 truenas_ws_deploy() {
   _domain="$1"
@@ -179,14 +211,8 @@ truenas_ws_deploy() {
 
   _info "Gather system info..."
   _ws_response=$(_ws_call "system.info")
-  _truenas_system=$(printf "%s" "$_ws_response" | jq -r '."version"' | cut -d '-' -f 2 | tr '[:lower:]' '[:upper:]')
-  _truenas_version=$(printf "%s" "$_ws_response" | jq -r '."version"' | cut -d '-' -f 3)
-  _info "TrueNAS system: $_truenas_system"
+  _truenas_version=$(printf "%s" "$_ws_response" | jq -r '."version"')
   _info "TrueNAS version: $_truenas_version"
-  if [ "$_truenas_system" != "SCALE" ] && [ "$_truenas_system" != "CORE" ]; then
-    _err "Cannot gather TrueNAS system. Nor CORE oder SCALE detected."
-    return 10
-  fi
 
   ########## Gather current certificate
 
@@ -203,19 +229,26 @@ truenas_ws_deploy() {
   _certname="acme_$(_utc_date | tr -d '\-\:' | tr ' ' '_')"
   _info "New WebUI certificate name: $_certname"
   _debug _certname "$_certname"
-  _ws_jobid=$(_ws_call "certificate.create" "{\"name\": \"${_certname}\", \"create_type\": \"CERTIFICATE_CREATE_IMPORTED\", \"certificate\": \"$(_json_encode <"$_file_fullchain")\", \"privatekey\": \"$(_json_encode <"$_file_key")\", \"passphrase\": \"\"}")
-  _debug "_ws_jobid" "$_ws_jobid"
-  if ! _ws_check_jobid "$_ws_jobid"; then
-    _err "No JobID returned from websocket method."
-    return 3
-  fi
-  _ws_result=$(_ws_get_job_result "$_ws_jobid")
-  _ws_ret=$?
-  if [ $_ws_ret -gt 0 ]; then
-    return $_ws_ret
-  fi
-  _debug "_ws_result" "$_ws_result"
-  _new_certid=$(printf "%s" "$_ws_result" | jq -r '."id"')
+  _ws_out=$(_ws_upload_cert "$_file_fullchain" "$_file_key" "$_certname")
+
+  echo "$_ws_out" | while IFS= read -r LINE; do
+    case "$LINE" in
+    I:*)
+      _info "${LINE#I:}"
+      ;;
+    D:*)
+      _debug "${LINE#D:}"
+      ;;
+    E*)
+      _err "${LINE#E:}"
+      ;;
+    *) ;;
+
+    esac
+  done
+
+  _new_certid=$(echo "$_ws_out" | grep 'R:' | cut -d ':' -f 2)
+
   _info "New certificate ID: $_new_certid"
 
   ########## FTP
@@ -231,33 +264,31 @@ truenas_ws_deploy() {
 
   ########## ix Apps (SCALE only)
 
-  if [ "$_truenas_system" = "SCALE" ]; then
-    _info "Replace app certificates..."
-    _ws_response=$(_ws_call "app.query")
-    for _app_name in $(printf "%s" "$_ws_response" | jq -r '.[]."name"'); do
-      _info "Checking app $_app_name..."
-      _ws_response=$(_ws_call "app.config" "$_app_name")
-      if [ "$(printf "%s" "$_ws_response" | jq -r '."network" | has("certificate_id")')" = "true" ]; then
-        _info "App has certificate option, setup new certificate..."
-        _info "App will be redeployed after updating the certificate."
-        _ws_jobid=$(_ws_call "app.update" "$_app_name" "{\"values\": {\"network\": {\"certificate_id\": $_new_certid}}}")
-        _debug "_ws_jobid" "$_ws_jobid"
-        if ! _ws_check_jobid "$_ws_jobid"; then
-          _err "No JobID returned from websocket method."
-          return 3
-        fi
-        _ws_result=$(_ws_get_job_result "$_ws_jobid")
-        _ws_ret=$?
-        if [ $_ws_ret -gt 0 ]; then
-          return $_ws_ret
-        fi
-        _debug "_ws_result" "$_ws_result"
-        _info "App certificate replaced."
-      else
-        _info "App has no certificate option, skipping..."
+  _info "Replace app certificates..."
+  _ws_response=$(_ws_call "app.query")
+  for _app_name in $(printf "%s" "$_ws_response" | jq -r '.[]."name"'); do
+    _info "Checking app $_app_name..."
+    _ws_response=$(_ws_call "app.config" "$_app_name")
+    if [ "$(printf "%s" "$_ws_response" | jq -r '."network" | has("certificate_id")')" = "true" ]; then
+      _info "App has certificate option, setup new certificate..."
+      _info "App will be redeployed after updating the certificate."
+      _ws_jobid=$(_ws_call "app.update" "$_app_name" "{\"values\": {\"network\": {\"certificate_id\": $_new_certid}}}")
+      _debug "_ws_jobid" "$_ws_jobid"
+      if ! _ws_check_jobid "$_ws_jobid"; then
+        _err "No JobID returned from websocket method."
+        return 3
       fi
-    done
-  fi
+      _ws_result=$(_ws_get_job_result "$_ws_jobid")
+      _ws_ret=$?
+      if [ $_ws_ret -gt 0 ]; then
+        return $_ws_ret
+      fi
+      _debug "_ws_result" "$_ws_result"
+      _info "App certificate replaced."
+    else
+      _info "App has no certificate option, skipping..."
+    fi
+  done
 
   ########## WebUI
 

+ 500 - 0
deploy/zyxel_gs1900.sh

@@ -0,0 +1,500 @@
+#!/usr/bin/env sh
+
+# Deploy certificates to Zyxel GS1900 series switches
+#
+# This script uses the https web administration interface in order
+# to upload updated certificates to Zyxel GS1900 series switches.
+# Only a few models have been tested but untested switches from the
+# same model line may work as well. If you test and confirm a switch
+# as working please submit a pull request updating this compatibility
+# list!
+#
+# Known Issues:
+#   1. This is a consumer grade switch and is a bit underpowered
+#      the longer the RSA key size the slower your switch web UI
+#      will be. RSA 2048 will work, RSA 4096 will work but you may
+#      experience performance problems.
+#   2. You must use RSA certificates. The switch will reject EC-256
+#      and EC-384 certificates in firmware 2.80
+#      See: https://community.zyxel.com/en/discussion/21506/bug-cannot-import-ssl-cert-on-gs1900-8-and-gs1900-24e-firmware-v2-80/
+#
+# Current GS1900 Switch Compatibility:
+#   GS1900-8    - Working as of firmware V2.80
+#   GS1900-8HP  - Untested
+#   GS1900-10HP - Untested
+#   GS1900-16   - Untested
+#   GS1900-24   - Untested
+#   GS1900-24E  - Working as of firmware V2.80
+#   GS1900-24EP - Untested
+#   GS1900-24HP - Untested
+#   GS1900-48   - Untested
+#   GS1900-48HP - Untested
+#
+# Prerequisite Setup Steps:
+#   1. Install at least firmware V2.80 on your switch
+#   2. Enable HTTPS web management on your switch
+#
+# Usage:
+#   1. Ensure the switch has firmware V2.80 or later.
+#   2. Ensure the switch has HTTPS management enabled.
+#   3. Set the appropriate environment variables for your environment.
+#
+#      DEPLOY_ZYXEL_SWITCH          - The switch hostname. (Default: _cdomain)
+#      DEPLOY_ZYXEL_SWITCH_USER     - The webadmin user. (Default: admin)
+#      DEPLOY_ZYXEL_SWITCH_PASSWORD - The webadmin password for the switch.
+#      DEPLOY_ZYXEL_SWITCH_REBOOT   - If "1" reboot after update. (Default: "0")
+#
+#   4. Run the deployment plugin:
+#      acme.sh --deploy --deploy-hook zyxel_gs1900 -d example.com
+#
+# returns 0 means success, otherwise error.
+
+#domain keyfile certfile cafile fullchain
+zyxel_gs1900_deploy() {
+  _zyxel_gs1900_minimum_firmware_version="v2.80"
+
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+
+  _debug _cdomain "$_cdomain"
+  _debug2 _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  _getdeployconf DEPLOY_ZYXEL_SWITCH
+  _getdeployconf DEPLOY_ZYXEL_SWITCH_USER
+  _getdeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD
+  _getdeployconf DEPLOY_ZYXEL_SWITCH_REBOOT
+
+  if [ -z "$DEPLOY_ZYXEL_SWITCH" ]; then
+    DEPLOY_ZYXEL_SWITCH="$_cdomain"
+  fi
+
+  if [ -z "$DEPLOY_ZYXEL_SWITCH_USER" ]; then
+    DEPLOY_ZYXEL_SWITCH_USER="admin"
+  fi
+
+  if [ -z "$DEPLOY_ZYXEL_SWITCH_PASSWORD" ]; then
+    DEPLOY_ZYXEL_SWITCH_PASSWORD="1234"
+  fi
+
+  if [ -z "$DEPLOY_ZYXEL_SWITCH_REBOOT" ]; then
+    DEPLOY_ZYXEL_SWITCH_REBOOT="0"
+  fi
+
+  _savedeployconf DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH"
+  _savedeployconf DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER"
+  _savedeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD"
+  _savedeployconf DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT"
+
+  _debug DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH"
+  _debug DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER"
+  _secure_debug DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD"
+  _debug DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT"
+
+  _zyxel_switch_base_uri="https://${DEPLOY_ZYXEL_SWITCH}"
+
+  _info "Beginning to deploy to a Zyxel GS1900 series switch at ${_zyxel_switch_base_uri}."
+  _zyxel_gs1900_deployment_precheck || return $?
+
+  _zyxel_gs1900_should_update
+  if [ "$?" != "0" ]; then
+    _info "The switch already has our certificate installed. No update required."
+    return 0
+  else
+    _info "The switch does not yet have our certificate installed."
+  fi
+
+  _info "Logging into the switch web interface."
+  _zyxel_gs1900_login || return $?
+
+  _info "Validating the switch is compatible with this deployment process."
+  _zyxel_gs1900_validate_device_compatibility || return $?
+
+  _info "Uploading the certificate."
+  _zyxel_gs1900_upload_certificate || return $?
+
+  if [ "$DEPLOY_ZYXEL_SWITCH_REBOOT" = "1" ]; then
+    _info "Rebooting the switch."
+    _zyxel_gs1900_trigger_reboot || return $?
+  fi
+
+  return 0
+}
+
+_zyxel_gs1900_deployment_precheck() {
+  # Initialize the keylength if it isn't already
+  if [ -z "$Le_Keylength" ]; then
+    Le_Keylength=""
+  fi
+
+  if _isEccKey "$Le_Keylength"; then
+    _info "Warning: Zyxel GS1900 switches are not currently known to work with ECC keys!"
+    _info "You can continue, but your switch may reject your key."
+  elif [ -n "$Le_Keylength" ] && [ "$Le_Keylength" -gt "2048" ]; then
+    _info "Warning: Your RSA key length is greater than 2048!"
+    _info "You can continue, but you may experience performance issues in the web administration interface."
+  fi
+
+  # Check the server for some common failure modes prior to authentication and certificate upload in order to avoid
+  # sending a certificate when we may not want to.
+  test_login_response=$(_post "username=test&password=test&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" 2>&1)
+  test_login_page_exitcode="$?"
+  _debug3 "Test Login Response: ${test_login_response}"
+  if [ "$test_login_page_exitcode" -ne "0" ]; then
+    if { [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "60" ]; } || { [ "${ACME_USE_WGET:-0}" = "1" ] && [ "$test_login_page_exitcode" = "5" ]; }; then
+      _err "The SSL certificate at $_zyxel_switch_base_uri could not be validated."
+      _err "Please double check your hostname, port, and that you are actually connecting to your switch."
+      _err "If the problem persists then please ensure that the certificate is not self-signed, has not"
+      _err "expired, and matches the switch hostname. If you expect validation to fail then you can disable"
+      _err "certificate validation by running with --insecure."
+      return 1
+    elif [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "56" ]; then
+      _debug3 "Intentionally ignore curl exit code 56 in our precheck"
+    else
+      _err "Failed to submit the initial login attempt to $_zyxel_switch_base_uri."
+      return 1
+    fi
+  fi
+}
+
+_zyxel_gs1900_login() {
+  # Login to the switch and set the appropriate auth cookie in _H1
+  username_encoded=$(printf "%s" "$DEPLOY_ZYXEL_SWITCH_USER" | _url_encode)
+  password_encoded=$(_zyxel_gs1900_password_obfuscate "$DEPLOY_ZYXEL_SWITCH_PASSWORD" | _url_encode)
+
+  login_response=$(_post "username=${username_encoded}&password=${password_encoded}&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n')
+  auth_response=$(_post "authId=${login_response}&login_chk=true" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n')
+  if [ "$auth_response" != "OK" ]; then
+    _err "Login failed due to invalid credentials."
+    _err "Please double check the configured username and password and try again."
+    return 1
+  fi
+
+  sessionid=$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'HTTPS_XSSID=[^;]*;' | tr -d ';')
+  _secure_debug2 "sessionid" "$sessionid"
+
+  export _H1="Cookie: $sessionid"
+  _secure_debug2 "_H1" "$_H1"
+
+  return 0
+}
+
+_zyxel_gs1900_validate_device_compatibility() {
+  # Check the switches model and firmware version and throw errors
+  # if this script isn't compatible.
+  device_info_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=12" | tr -d '\n')
+
+  model_name=$(_zyxel_gs1900_get_model "$device_info_html")
+  _debug2 "model_name" "$model_name"
+  if [ -z "$model_name" ]; then
+    _err "Could not find the switch model name."
+    _err "Please re-run with --debug and report a bug."
+    return $?
+  fi
+
+  if ! expr "$model_name" : "GS1900-" >/dev/null; then
+    _err "Switch is an unsupported model: $model_name"
+    return 1
+  fi
+
+  firmware_version=$(_zyxel_gs1900_get_firmware_version "$device_info_html")
+  _debug2 "firmware_version" "$firmware_version"
+  if [ -z "$firmware_version" ]; then
+    _err "Could not find the switch firmware version."
+    _err "Please re-run with --debug and report a bug."
+    return $?
+  fi
+
+  _debug2 "_zyxel_gs1900_minimum_firmware_version" "$_zyxel_gs1900_minimum_firmware_version"
+  minimum_major_version=$(_zyxel_gs1900_parse_major_version "$_zyxel_gs1900_minimum_firmware_version")
+  _debug2 "minimum_major_version" "$minimum_major_version"
+  minimum_minor_version=$(_zyxel_gs1900_parse_minor_version "$_zyxel_gs1900_minimum_firmware_version")
+  _debug2 "minimum_minor_version" "$minimum_minor_version"
+
+  _debug2 "firmware_version" "$firmware_version"
+  firmware_major_version=$(_zyxel_gs1900_parse_major_version "$firmware_version")
+  _debug2 "firmware_major_version" "$firmware_major_version"
+  firmware_minor_version=$(_zyxel_gs1900_parse_minor_version "$firmware_version")
+  _debug2 "firmware_minor_version" "$firmware_minor_version"
+
+  _ret=0
+  if [ "$firmware_major_version" -lt "$minimum_major_version" ]; then
+    _ret=1
+  elif [ "$firmware_major_version" -eq "$minimum_major_version" ] && [ "$firmware_minor_version" -lt "$minimum_minor_version" ]; then
+    _ret=1
+  fi
+
+  if [ "$_ret" != "0" ]; then
+    _err "Unsupported firmware version $firmware_version. Please upgrade to at least version $_zyxel_gs1900_minimum_firmware_version."
+  fi
+
+  return $?
+}
+
+_zyxel_gs1900_should_update() {
+  # Get the remote certificate serial number
+  _remote_cert=$(${ACME_OPENSSL_BIN:-openssl} s_client -showcerts -connect "${DEPLOY_ZYXEL_SWITCH}:443" 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p')
+  _debug3 "_remote_cert" "$_remote_cert"
+
+  _remote_cert_serial=$(printf "%s" "${_remote_cert}" | ${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial)
+  _debug2 "_remote_cert_serial" "$_remote_cert_serial"
+
+  # Get our certificate serial number
+  _our_cert_serial=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial <"${_ccert}")
+  _debug2 "_our_cert_serial" "$_our_cert_serial"
+
+  [ "${_remote_cert_serial}" != "${_our_cert_serial}" ]
+}
+
+_zyxel_gs1900_upload_certificate() {
+  # Generate a PKCS12 certificate with a temporary password since the web interface
+  # requires a password be present. Then upload that certificate.
+  temp_cert_password=$(head /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 64)
+  _secure_debug2 "temp_cert_password" "$temp_cert_password"
+
+  temp_pkcs12="$(_mktemp)"
+  _debug2 "temp_pkcs12" "$temp_pkcs12"
+  _toPkcs "$temp_pkcs12" "$_ckey" "$_ccert" "$_cca" "$temp_cert_password"
+  if [ "$?" != "0" ]; then
+    _err "Failed to generate a pkcs12 certificate."
+    _err "Please re-run with --debug and report a bug."
+
+    # ensure the temporary certificate file is cleaned up
+    [ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}"
+
+    return $?
+  fi
+
+  # Load the upload page
+  upload_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5914" | tr -d '\n')
+
+  # Get the first instance of XSSID from the upload page
+  form_xss_value=$(printf "%s" "$upload_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g' | head -n 1)
+  _secure_debug2 "form_xss_value" "$form_xss_value"
+
+  _info "Generating the certificate upload request"
+  upload_post_request="$(_mktemp)"
+  upload_post_boundary="---------------------------$(date +%Y%m%d%H%M%S)"
+
+  {
+    printf -- "--%s\r\n" "${upload_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"XSSID\"\r\n\r\n%s\r\n" "${form_xss_value}"
+    printf -- "--%s\r\n" "${upload_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"http_file\"; filename=\"temp_pkcs12.pfx\"\r\n"
+    printf "Content-Type: application/pkcs12\r\n\r\n"
+    cat "${temp_pkcs12}"
+    printf "\r\n"
+    printf -- "--%s\r\n" "${upload_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"pwd\"\r\n\r\n%s\r\n" "${temp_cert_password}"
+    printf -- "--%s\r\n" "${upload_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"cmd\"\r\n\r\n%s\r\n" "31"
+    printf -- "--%s\r\n" "${upload_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"sysSubmit\"\r\n\r\n%s\r\n" "Import"
+    printf -- "--%s--\r\n" "${upload_post_boundary}"
+  } >"${upload_post_request}"
+
+  _info "Upload certificate to the switch"
+
+  # Unfortunately we cannot rely upon the switch response across switch models
+  # to return a consistent body return - so we cannot inspect the result of this
+  # upload to determine success.
+  upload_response=$(_zyxel_upload_pkcs12 "${upload_post_request}" "${upload_post_boundary}" 2>&1)
+  _debug3 "Upload response: ${upload_response}"
+  rm "${upload_post_request}"
+
+  # Pause for a few seconds to give the switch a chance to process the certificate
+  # For some reason I've found this to be necessary on my GS1900-24E
+  _debug2 "Waiting 4 seconds for the switch to process the newly uploaded certificate."
+  sleep "4"
+
+  # Check to see whether or not our update was successful
+  _ret=0
+  _zyxel_gs1900_should_update
+  if [ "$?" != "0" ]; then
+    _info "The certificate was updated successfully"
+  else
+    _ret=1
+    _err "The certificate upload does not appear to have worked."
+    _err "The remote certificate does not match the certificate we tried to upload."
+    _err "Please re-run with --debug 2 and review for unexpected errors. If none can be found please submit a bug."
+  fi
+
+  # ensure the temporary files are cleaned up
+  [ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}"
+
+  return $_ret
+}
+
+# make the certificate upload request using either
+# --data binary with @ for file access in CURL
+# or using --post-file for wget to ensure we upload
+# the pkcs12 without getting tripped up on null bytes
+#
+# Usage _zyxel_upload_pkcs12 [body file name] [post boundary marker]
+_zyxel_upload_pkcs12() {
+  bodyfilename="$1"
+  multipartformmarker="$2"
+  _post_url="${_zyxel_switch_base_uri}/cgi-bin/httpuploadcert.cgi"
+  httpmethod="POST"
+  _postContentType="multipart/form-data; boundary=${multipartformmarker}"
+
+  if [ -z "$httpmethod" ]; then
+    httpmethod="POST"
+  fi
+  _debug $httpmethod
+  _debug "_post_url" "$_post_url"
+  _debug2 "bodyfilename" "$bodyfilename"
+  _debug2 "_postContentType" "$_postContentType"
+
+  _inithttp
+
+  if [ "$_ACME_CURL" ] && [ "${ACME_USE_WGET:-0}" = "0" ]; then
+    _CURL="$_ACME_CURL"
+    if [ "$HTTPS_INSECURE" ]; then
+      _CURL="$_CURL --insecure  "
+    fi
+    if [ "$httpmethod" = "HEAD" ]; then
+      _CURL="$_CURL -I  "
+    fi
+    _debug "_CURL" "$_CURL"
+
+    response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data-binary "@${bodyfilename}" "$_post_url")"
+
+    _ret="$?"
+    if [ "$_ret" != "0" ]; then
+      _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret"
+      if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
+        _err "Here is the curl dump log:"
+        _err "$(cat "$_CURL_DUMP")"
+      fi
+    fi
+  elif [ "$_ACME_WGET" ]; then
+    _WGET="$_ACME_WGET"
+    if [ "$HTTPS_INSECURE" ]; then
+      _WGET="$_WGET --no-check-certificate "
+    fi
+    _debug "_WGET" "$_WGET"
+
+    response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-file="${bodyfilename}" "$_post_url" 2>"$HTTP_HEADER")"
+
+    _ret="$?"
+    if [ "$_ret" = "8" ]; then
+      _ret=0
+      _debug "wget returned 8 as the server returned a 'Bad Request' response. Let's process the response later."
+    fi
+    if [ "$_ret" != "0" ]; then
+      _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret"
+    fi
+    if _contains "$_WGET" " -d "; then
+      # Demultiplex wget debug output
+      cat "$HTTP_HEADER" >&2
+      _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
+    fi
+    # remove leading whitespaces from header to match curl format
+    _sed_i 's/^  //g' "$HTTP_HEADER"
+  else
+    _ret="$?"
+    _err "Neither curl nor wget have been found, cannot make $httpmethod request."
+  fi
+  _debug "_ret" "$_ret"
+  printf "%s" "$response"
+  return $_ret
+}
+
+_zyxel_gs1900_trigger_reboot() {
+  # Trigger a reboot via the management reboot page in the web ui
+  reboot_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5888" | tr -d '\n')
+  reboot_xss_value=$(printf "%s" "$reboot_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g')
+  _secure_debug2 "reboot_xss_value" "$reboot_xss_value"
+
+  reboot_response_html=$(_post "XSSID=${reboot_xss_value}&cmd=5889&sysSubmit=Reboot" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi" '' "POST" "application/x-www-form-urlencoded")
+  reboot_message=$(printf "%s" "$reboot_response_html" | tr -d '\t\r\n\v\f' | _egrep_o "Rebooting now...")
+
+  if [ -z "$reboot_message" ]; then
+    _err "Failed to trigger switch reboot!"
+    return 1
+  fi
+
+  return 0
+}
+
+# password
+_zyxel_gs1900_password_obfuscate() {
+  # Return the password obfuscated via the same method used by the
+  # switch's web UI login process
+  echo "$1" | awk '{
+    encoded = "";
+    password = $1;
+    allowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    len = length($1);
+    pwi = length($1);
+
+    for (i=1; i <= (321 - pwi); i++)
+    {
+      if (0 == i % 5 && pwi > 0)
+      {
+        encoded = (encoded)(substr(password, pwi--, 1));
+      }
+      else if (i == 123)
+      {
+        if (len < 10)
+        {
+          encoded = (encoded)(0);
+        }
+        else
+        {
+          encoded = (encoded)(int(len / 10));
+        }
+      }
+      else if (i == 289)
+      {
+        encoded = (encoded)(len % 10)
+      }
+      else
+      {
+        encoded = (encoded)(substr(allowed, int(rand() * length(allowed)), 1))
+      }
+    }
+    printf("%s", encoded);
+  }'
+}
+
+# html label
+_zyxel_html_table_lookup() {
+  # Look up a value in the html representing the status page of the switch
+  # when provided with the html of the page and the label (i.e. "Model Name:")
+  html="$1"
+  label=$(printf "%s" "$2" | tr -d ' ')
+  lookup_result=$(printf "%s" "$html" | tr -d "\t\r\n\v\f" | sed 's/<tr>/\n<tr>/g' | sed 's/<td[^>]*>/<td>/g' | tr -d ' ' | grep -i "$label" | sed "s/<tr><td>$label<\/td><td>\([^<]\{1,\}\)<\/td><\/tr>/\1/i")
+  printf "%s" "$lookup_result"
+  return 0
+}
+
+# html
+_zyxel_gs1900_get_model() {
+  html="$1"
+  model_name=$(_zyxel_html_table_lookup "$html" "Model Name:")
+  printf "%s" "$model_name"
+}
+
+# html
+_zyxel_gs1900_get_firmware_version() {
+  html="$1"
+  firmware_version=$(_zyxel_html_table_lookup "$html" "Firmware Version:" | _egrep_o "V[^.]+.[^(]+")
+  printf "%s" "$firmware_version"
+}
+
+# version_number
+_zyxel_gs1900_parse_major_version() {
+  printf "%s" "$1" | sed 's/^V\([0-9]\{1,\}\).\{1,\}$/\1/gi'
+}
+
+# version_number
+_zyxel_gs1900_parse_minor_version() {
+  printf "%s" "$1" | sed 's/^.\{1,\}\.\([0-9]\{1,\}\)$/\1/gi'
+}

+ 2 - 2
dnsapi/dns_1984hosting.sh

@@ -128,7 +128,7 @@ _1984hosting_login() {
 
   _get "https://1984.hosting/accounts/login/" | grep "csrfmiddlewaretoken"
   csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
-  sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
+  sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
 
   if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then
     _err "One or more cookies are empty: '$csrftoken', '$sessionid'."
@@ -145,7 +145,7 @@ _1984hosting_login() {
   _debug2 response "$response"
 
   if _contains "$response" '"loggedin": true'; then
-    One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
+    One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
     One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
     export One984HOSTING_SESSIONID_COOKIE
     export One984HOSTING_CSRFTOKEN_COOKIE

+ 109 - 51
dnsapi/dns_active24.sh

@@ -1,17 +1,17 @@
 #!/usr/bin/env sh
 # shellcheck disable=SC2034
-dns_active24_info='Active24.com
-Site: Active24.com
+dns_active24_info='Active24.cz
+Site: Active24.cz
 Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_active24
 Options:
- ACTIVE24_Token API Token
+ Active24_ApiKey API Key. Called "Identifier" in the Active24 Admin
+ Active24_ApiSecret API Secret. Called "Secret key" in the Active24 Admin
 Issues: github.com/acmesh-official/acme.sh/issues/2059
-Author: Milan Pála
 '
 
-ACTIVE24_Api="https://api.active24.com"
-
-########  Public functions #####################
+Active24_Api="https://rest.active24.cz"
+# export Active24_ApiKey=ak48l3h7-ak5d-qn4t-p8gc-b6fs8c3l
+# export Active24_ApiSecret=ajvkeo3y82ndsu2smvxy3o36496dcascksldncsq
 
 # Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 # Used to add txt record
@@ -22,8 +22,8 @@ dns_active24_add() {
   _active24_init
 
   _info "Adding txt record"
-  if _active24_rest POST "dns/$_domain/txt/v1" "{\"name\":\"$_sub_domain\",\"text\":\"$txtvalue\",\"ttl\":0}"; then
-    if _contains "$response" "errors"; then
+  if _active24_rest POST "/v2/service/$_service_id/dns/record" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":300}"; then
+    if _contains "$response" "error"; then
       _err "Add txt record error."
       return 1
     else
@@ -31,6 +31,7 @@ dns_active24_add() {
       return 0
     fi
   fi
+
   _err "Add txt record error."
   return 1
 }
@@ -44,19 +45,25 @@ dns_active24_rm() {
   _active24_init
 
   _debug "Getting txt records"
-  _active24_rest GET "dns/$_domain/records/v1"
+  # The API needs to send data in body in order the filter to work
+  # TODO: web can also add content $txtvalue to filter and then get the id from response
+  _active24_rest GET "/v2/service/$_service_id/dns/record" "{\"page\":1,\"descending\":true,\"sortBy\":\"name\",\"rowsPerPage\":100,\"totalRecords\":0,\"filters\":{\"type\":[\"TXT\"],\"name\":\"${_sub_domain}\"}}"
+  #_active24_rest GET "/v2/service/$_service_id/dns/record?rowsPerPage=100"
 
-  if _contains "$response" "errors"; then
+  if _contains "$response" "error"; then
     _err "Error"
     return 1
   fi
 
-  hash_ids=$(echo "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o "hashId\":\"[^\"]+" | cut -c10-)
+  # Note: it might never be more than one record actually, NEEDS more INVESTIGATION
+  record_ids=$(printf "%s" "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o '"id" *: *[^,]+' | cut -d ':' -f 2)
+  _debug2 record_ids "$record_ids"
 
-  for hash_id in $hash_ids; do
-    _debug "Removing hash_id" "$hash_id"
-    if _active24_rest DELETE "dns/$_domain/$hash_id/v1" ""; then
-      if _contains "$response" "errors"; then
+  for redord_id in $record_ids; do
+    _debug "Removing record_id" "$redord_id"
+    _debug "txtvalue" "$txtvalue"
+    if _active24_rest DELETE "/v2/service/$_service_id/dns/record/$redord_id" ""; then
+      if _contains "$response" "error"; then
         _err "Unable to remove txt record."
         return 1
       else
@@ -70,21 +77,15 @@ dns_active24_rm() {
   return 1
 }
 
-####################  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
+  p=1
 
-  if ! _active24_rest GET "dns/domains/v1"; then
+  if ! _active24_rest GET "/v1/user/self/service"; then
     return 1
   fi
 
-  i=1
-  p=1
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug "h" "$h"
@@ -104,45 +105,102 @@ _get_root() {
   return 1
 }
 
-_active24_rest() {
-  m=$1
-  ep="$2"
-  data="$3"
-  _debug "$ep"
-
-  export _H1="Authorization: Bearer $ACTIVE24_Token"
-
-  if [ "$m" != "GET" ]; then
-    _debug "data" "$data"
-    response="$(_post "$data" "$ACTIVE24_Api/$ep" "" "$m" "application/json")"
-  else
-    response="$(_get "$ACTIVE24_Api/$ep")"
+_active24_init() {
+  Active24_ApiKey="${Active24_ApiKey:-$(_readaccountconf_mutable Active24_ApiKey)}"
+  Active24_ApiSecret="${Active24_ApiSecret:-$(_readaccountconf_mutable Active24_ApiSecret)}"
+  #Active24_ServiceId="${Active24_ServiceId:-$(_readaccountconf_mutable Active24_ServiceId)}"
+
+  if [ -z "$Active24_ApiKey" ] || [ -z "$Active24_ApiSecret" ]; then
+    Active24_ApiKey=""
+    Active24_ApiSecret=""
+    _err "You don't specify Active24 api key and ApiSecret yet."
+    _err "Please create your key and try again."
+    return 1
   fi
 
-  if [ "$?" != "0" ]; then
-    _err "error $ep"
+  #save the credentials to the account conf file.
+  _saveaccountconf_mutable Active24_ApiKey "$Active24_ApiKey"
+  _saveaccountconf_mutable Active24_ApiSecret "$Active24_ApiSecret"
+
+  _debug "A24 API CHECK"
+  if ! _active24_rest GET "/v2/check"; then
+    _err "A24 API check failed with: $response"
     return 1
   fi
-  _debug2 response "$response"
-  return 0
-}
 
-_active24_init() {
-  ACTIVE24_Token="${ACTIVE24_Token:-$(_readaccountconf_mutable ACTIVE24_Token)}"
-  if [ -z "$ACTIVE24_Token" ]; then
-    ACTIVE24_Token=""
-    _err "You didn't specify a Active24 api token yet."
-    _err "Please create the token and try again."
+  if ! echo "$response" | tr -d " " | grep \"verified\":true >/dev/null; then
+    _err "A24 API check failed with: $response"
     return 1
   fi
 
-  _saveaccountconf_mutable ACTIVE24_Token "$ACTIVE24_Token"
-
   _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"
+  _active24_get_service_id "$_domain"
+  _debug _service_id "$_service_id"
+}
+
+_active24_get_service_id() {
+  _d=$1
+  if ! _active24_rest GET "/v1/user/self/zone/${_d}"; then
+    return 1
+  else
+    response=$(echo "$response" | _json_decode)
+    _service_id=$(echo "$response" | _egrep_o '"id" *: *[^,]+' | cut -d ':' -f 2)
+  fi
+}
+
+_active24_rest() {
+  m=$1
+  ep_qs=$2 # with query string
+  # ep=$2
+  ep=$(printf "%s" "$ep_qs" | cut -d '?' -f1) # no query string
+  data="$3"
+
+  _debug "A24 $ep"
+  _debug "A24 $Active24_ApiKey"
+  _debug "A24 $Active24_ApiSecret"
+
+  timestamp=$(_time)
+  datez=$(date -u +"%Y%m%dT%H%M%SZ")
+  canonicalRequest="${m} ${ep} ${timestamp}"
+  signature=$(printf "%s" "$canonicalRequest" | _hmac sha1 "$(printf "%s" "$Active24_ApiSecret" | _hex_dump | tr -d " ")" hex)
+  authorization64="$(printf "%s:%s" "$Active24_ApiKey" "$signature" | _base64)"
+
+  export _H1="Date: ${datez}"
+  export _H2="Accept: application/json"
+  export _H3="Content-Type: application/json"
+  export _H4="Authorization: Basic ${authorization64}"
+
+  _debug2 H1 "$_H1"
+  _debug2 H2 "$_H2"
+  _debug2 H3 "$_H3"
+  _debug2 H4 "$_H4"
+
+  # _sleep 1
+
+  if [ "$m" != "GET" ]; then
+    _debug2 "${m} $Active24_Api${ep_qs}"
+    _debug "data" "$data"
+    response="$(_post "$data" "$Active24_Api${ep_qs}" "" "$m" "application/json")"
+  else
+    if [ -z "$data" ]; then
+      _debug2 "GET $Active24_Api${ep_qs}"
+      response="$(_get "$Active24_Api${ep_qs}")"
+    else
+      _debug2 "GET $Active24_Api${ep_qs} with data: ${data}"
+      response="$(_post "$data" "$Active24_Api${ep_qs}" "" "$m" "application/json")"
+    fi
+  fi
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
 }

+ 11 - 2
dnsapi/dns_azure.sh

@@ -340,8 +340,17 @@ _azure_getaccess_token() {
 
   if [ "$managedIdentity" = true ]; then
     # https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
-    export _H1="Metadata: true"
-    response="$(_get http://169.254.169.254/metadata/identity/oauth2/token\?api-version=2018-02-01\&resource=https://management.azure.com/)"
+    if [ -n "$IDENTITY_ENDPOINT" ]; then
+      # Some Azure environments may set IDENTITY_ENDPOINT (formerly MSI_ENDPOINT) to have an alternative metadata endpoint
+      url="$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=https://management.azure.com/"
+      headers="X-IDENTITY-HEADER: $IDENTITY_HEADER"
+    else
+      url="http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
+      headers="Metadata: true"
+    fi
+
+    export _H1="$headers"
+    response="$(_get "$url")"
     response="$(echo "$response" | _normalizeJson)"
     accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
     expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")

+ 3 - 2
dnsapi/dns_cloudns.sh

@@ -197,10 +197,11 @@ _dns_cloudns_http_api_call() {
     auth_user="auth-id=$CLOUDNS_AUTH_ID"
   fi
 
+  encoded_password=$(echo "$CLOUDNS_AUTH_PASSWORD" | tr -d "\n\r" | _url_encode)
   if [ -z "$2" ]; then
-    data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD"
+    data="$auth_user&auth-password=$encoded_password"
   else
-    data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD&$2"
+    data="$auth_user&auth-password=$encoded_password&$2"
   fi
 
   response="$(_get "$CLOUDNS_API/$method?$data")"

+ 7 - 7
dnsapi/dns_edgecenter.sh

@@ -1,13 +1,13 @@
 #!/usr/bin/env sh
 # shellcheck disable=SC2034
-
-# EdgeCenter DNS API integration for acme.sh
-# Author: Konstantin Ruchev <[email protected]>
-dns_edgecenter_info='edgecenter DNS API
-Site: https://edgecenter.ru
-Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_edgecenter
+dns_edgecenter_info='EdgeCenter.ru
+Site: EdgeCenter.ru
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_edgecenter
 Options:
- EDGECENTER_API_KEY auth APIKey'
+ EDGECENTER_API_KEY API Key
+Issues: github.com/acmesh-official/acme.sh/issues/6313
+Author: Konstantin Ruchev <[email protected]>
+'
 
 EDGECENTER_API="https://api.edgecenter.ru"
 DOMAIN_TYPE=

+ 2 - 2
dnsapi/dns_freemyip.sh

@@ -1,11 +1,11 @@
 #!/usr/bin/env sh
 # shellcheck disable=SC2034
 dns_freemyip_info='FreeMyIP.com
-Site: freemyip.com
+Site: FreeMyIP.com
 Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_freemyip
 Options:
  FREEMYIP_Token API Token
-Issues: github.com/acmesh-official/acme.sh/issues/{XXXX}
+Issues: github.com/acmesh-official/acme.sh/issues/6247
 Author: Recolic Keghart <[email protected]>, @Giova96
 '
 

+ 212 - 0
dnsapi/dns_spaceship.sh

@@ -0,0 +1,212 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_spaceship_info='Spaceship.com
+Site: Spaceship.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_spaceship
+Options:
+ SPACESHIP_API_KEY Spaceship API Key
+ SPACESHIP_API_SECRET Spaceship API Secret
+ SPACESHIP_ROOT_DOMAIN (Optional) Manually specify the root domain if auto-detection fails
+Issues: github.com/acmesh-official/acme.sh/issues/6304
+Author: Meow <https://github.com/Meo597>
+'
+
+# Spaceship API
+# https://docs.spaceship.dev/
+
+########  Public functions #####################
+
+SPACESHIP_API_BASE="https://spaceship.dev/api/v1"
+
+# Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+dns_spaceship_add() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  _info "Adding TXT record for $fulldomain with value $txtvalue"
+
+  # Initialize API credentials and headers
+  if ! _spaceship_init; then
+    return 1
+  fi
+
+  # Detect root zone
+  if ! _get_root "$fulldomain"; then
+    return 1
+  fi
+
+  # Extract subdomain part relative to root domain
+  subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//")
+  if [ "$subdomain" = "$fulldomain" ]; then
+    _err "Failed to extract subdomain from $fulldomain relative to root domain $_domain"
+    return 1
+  fi
+  _debug "Extracted subdomain: $subdomain for root domain: $_domain"
+
+  # Escape txtvalue to prevent JSON injection (e.g., quotes in txtvalue)
+  escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g')
+
+  # Prepare payload and URL for adding TXT record
+  # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API
+  payload="{\"force\": true, \"items\": [{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\", \"ttl\": 600}]}"
+  url="$SPACESHIP_API_BASE/dns/records/$_domain"
+
+  # Send API request
+  if _spaceship_api_request "PUT" "$url" "$payload"; then
+    _info "Successfully added TXT record for $fulldomain"
+    return 0
+  else
+    _err "Failed to add TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain."
+    return 1
+  fi
+}
+
+# Usage: fulldomain txtvalue
+# Used to remove the txt record after validation
+dns_spaceship_rm() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  _info "Removing TXT record for $fulldomain with value $txtvalue"
+
+  # Initialize API credentials and headers
+  if ! _spaceship_init; then
+    return 1
+  fi
+
+  # Detect root zone
+  if ! _get_root "$fulldomain"; then
+    return 1
+  fi
+
+  # Extract subdomain part relative to root domain
+  subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//")
+  if [ "$subdomain" = "$fulldomain" ]; then
+    _err "Failed to extract subdomain from $fulldomain relative to root domain $_domain"
+    return 1
+  fi
+  _debug "Extracted subdomain: $subdomain for root domain: $_domain"
+
+  # Escape txtvalue to prevent JSON injection
+  escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g')
+
+  # Prepare payload and URL for deleting TXT record
+  # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API
+  payload="[{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\"}]"
+  url="$SPACESHIP_API_BASE/dns/records/$_domain"
+
+  # Send API request
+  if _spaceship_api_request "DELETE" "$url" "$payload"; then
+    _info "Successfully deleted TXT record for $fulldomain"
+    return 0
+  else
+    _err "Failed to delete TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain."
+    return 1
+  fi
+}
+
+####################  Private functions below ##################################
+
+_spaceship_init() {
+  SPACESHIP_API_KEY="${SPACESHIP_API_KEY:-$(_readaccountconf_mutable SPACESHIP_API_KEY)}"
+  SPACESHIP_API_SECRET="${SPACESHIP_API_SECRET:-$(_readaccountconf_mutable SPACESHIP_API_SECRET)}"
+
+  if [ -z "$SPACESHIP_API_KEY" ] || [ -z "$SPACESHIP_API_SECRET" ]; then
+    _err "Spaceship API credentials are not set. Please set SPACESHIP_API_KEY and SPACESHIP_API_SECRET."
+    _err "Ensure \"$LE_CONFIG_HOME\" directory has restricted permissions (chmod 700 \"$LE_CONFIG_HOME\") to protect credentials."
+    return 1
+  fi
+
+  # Save credentials to account config for future renewals
+  _saveaccountconf_mutable SPACESHIP_API_KEY "$SPACESHIP_API_KEY"
+  _saveaccountconf_mutable SPACESHIP_API_SECRET "$SPACESHIP_API_SECRET"
+
+  # Set common headers for API requests
+  export _H1="X-API-Key: $SPACESHIP_API_KEY"
+  export _H2="X-API-Secret: $SPACESHIP_API_SECRET"
+  export _H3="Content-Type: application/json"
+  return 0
+}
+
+_get_root() {
+  domain="$1"
+
+  # Check manual override
+  SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readdomainconf SPACESHIP_ROOT_DOMAIN)}"
+  if [ -n "$SPACESHIP_ROOT_DOMAIN" ]; then
+    _domain="$SPACESHIP_ROOT_DOMAIN"
+    _debug "Using manually specified or saved root domain: $_domain"
+    _savedomainconf SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN"
+    return 0
+  fi
+
+  _debug "Detecting root zone for '$domain'"
+
+  i=1
+  p=1
+  while true; do
+    _cutdomain=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+
+    _debug "Attempt i=$i: Checking if '$_cutdomain' is root zone (cut ret=$?)"
+
+    if [ -z "$_cutdomain" ]; then
+      _debug "Cut resulted in empty string, root zone not found."
+      break
+    fi
+
+    # Call the API to check if this _cutdomain is a manageable zone
+    if _spaceship_api_request "GET" "$SPACESHIP_API_BASE/dns/records/$_cutdomain?take=1&skip=0"; then
+      # API call succeeded (HTTP 200 OK for GET /dns/records)
+      _domain="$_cutdomain"
+      _debug "Root zone found: '$_domain'"
+
+      # Save the detected root domain
+      _savedomainconf SPACESHIP_ROOT_DOMAIN "$_domain"
+      _info "Root domain '$_domain' saved to configuration for future use."
+
+      return 0
+    fi
+
+    _debug "API check failed for '$_cutdomain'. Continuing search."
+
+    p=$i
+    i=$((i + 1))
+  done
+
+  _err "Could not detect root zone for '$domain'. Please set SPACESHIP_ROOT_DOMAIN manually."
+  return 1
+}
+
+_spaceship_api_request() {
+  method="$1"
+  url="$2"
+  payload="$3"
+
+  _debug2 "Sending $method request to $url with payload $payload"
+  if [ "$method" = "GET" ]; then
+    response="$(_get "$url")"
+  else
+    response="$(_post "$payload" "$url" "" "$method")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "API request failed. Response: $response"
+    return 1
+  fi
+
+  _debug2 "API response body: $response"
+
+  if [ "$method" = "GET" ]; then
+    if _contains "$(_head_n 1 <"$HTTP_HEADER")" '200'; then
+      return 0
+    fi
+  else
+    if _contains "$(_head_n 1 <"$HTTP_HEADER")" '204'; then
+      return 0
+    fi
+  fi
+
+  _debug2 "API response header: $HTTP_HEADER"
+  return 1
+}

+ 1 - 1
dnsapi/dns_tencent.sh

@@ -2,7 +2,7 @@
 # shellcheck disable=SC2034
 dns_tencent_info='Tencent.com
 Site: cloud.Tencent.com
-Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_tencent
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_tencent
 Options:
  Tencent_SecretId Secret ID
  Tencent_SecretKey Secret Key

+ 2 - 2
dnsapi/dns_transip.sh

@@ -24,7 +24,7 @@ dns_transip_add() {
   _debug txtvalue="$txtvalue"
   _transip_setup "$fulldomain" || return 1
   _info "Creating TXT record."
-  if ! _transip_rest POST "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":300}}"; then
+  if ! _transip_rest POST "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":60}}"; then
     _err "Could not add TXT record."
     return 1
   fi
@@ -38,7 +38,7 @@ dns_transip_rm() {
   _debug txtvalue="$txtvalue"
   _transip_setup "$fulldomain" || return 1
   _info "Removing TXT record."
-  if ! _transip_rest DELETE "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":300}}"; then
+  if ! _transip_rest DELETE "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":60}}"; then
     _err "Could not remove TXT record $_sub_domain for $domain"
     return 1
   fi