diff --git a/.github/workflows/pr_dns.yml b/.github/workflows/pr_dns.yml index 4d7a325d..58630e8b 100644 --- a/.github/workflows/pr_dns.yml +++ b/.github/workflows/pr_dns.yml @@ -23,6 +23,7 @@ jobs: 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, 都必须确保通过这个测试. ` diff --git a/Dockerfile b/Dockerfile index 2ad50e6a..3f400283 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,11 +15,11 @@ RUN apk --no-cache add -f \ jq \ cronie -ENV LE_CONFIG_HOME /acme.sh +ENV LE_CONFIG_HOME=/acme.sh ARG AUTO_UPGRADE=1 -ENV AUTO_UPGRADE $AUTO_UPGRADE +ENV AUTO_UPGRADE=$AUTO_UPGRADE #Install COPY ./ /install_acme.sh/ diff --git a/acme.sh b/acme.sh index e39a146b..2cf681eb 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.1.0 +VER=3.1.1 PROJECT_NAME="acme.sh" @@ -921,6 +921,9 @@ _sed_i() { if sed -h 2>&1 | grep "\-i\[SUFFIX]" >/dev/null 2>&1; then _debug "Using sed -i" sed -i "$options" "$filename" + elif sed -h 2>&1 | grep "\-i extension" >/dev/null 2>&1; then + _debug "Using FreeBSD sed -i" + sed -i "" "$options" "$filename" else _debug "No -i support in sed" text="$(cat "$filename")" @@ -1442,7 +1445,7 @@ _toPkcs() { else ${ACME_OPENSSL_BIN:-openssl} pkcs12 -export -out "$_cpfx" -inkey "$_ckey" -in "$_ccert" -certfile "$_cca" fi - if [ "$?" == "0" ]; then + if [ "$?" = "0" ]; then _savedomainconf "Le_PFXPassword" "$pfxPassword" fi @@ -1628,6 +1631,11 @@ _time2str() { return fi + #Omnios + if date -u -r "$1" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null; then + return + fi + #Solaris if printf "%(%Y-%m-%dT%H:%M:%SZ)T\n" $1 2>/dev/null; then return @@ -1811,7 +1819,11 @@ _date2time() { return fi #Omnios - if da="$(echo "$1" | tr -d "Z" | tr "T" ' ')" perl -MTime::Piece -e 'print Time::Piece->strptime($ENV{da}, "%Y-%m-%d %H:%M:%S")->epoch, "\n";' 2>/dev/null; then + if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then + return + fi + #Omnios + if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%dT%H:%M:%SZ\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then return fi _err "Cannot parse _date2time $1" @@ -2193,7 +2205,6 @@ _send_signed_request() { _debug2 _headers "$_headers" _CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" fi - _debug2 _CACHED_NONCE "$_CACHED_NONCE" if [ "$?" != "0" ]; then _err "Cannot connect to $nonceurl to get nonce." return 1 @@ -4994,9 +5005,11 @@ $_authorizations_map" _debug "Writing token: $token to $wellknown_path/$token" - mkdir -p "$wellknown_path" - - if ! printf "%s" "$keyauthorization" >"$wellknown_path/$token"; then + # Ensure .well-known is visible to web server user/group + # https://github.com/Neilpang/acme.sh/pull/32 + if ! (umask ugo+rx && + mkdir -p "$wellknown_path" && + printf "%s" "$keyauthorization" >"$wellknown_path/$token"); then _err "$d: Cannot write token to file: $wellknown_path/$token" _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup @@ -5810,7 +5823,7 @@ _deploy() { return 1 fi - if ! $d_command "$_d" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH"; then + if ! $d_command "$_d" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CERT_PFX_PATH"; then _err "Error deploying for domain: $_d" return 1 fi @@ -6053,7 +6066,7 @@ installcronjob() { _script="$(_readlink "$_SCRIPT_")" _debug _script "$_script" if [ -f "$_script" ]; then - _info "Usinging the current script from: $_script" + _info "Using the current script from: $_script" lesh="$_script" else _err "Cannot install cronjob, $PROJECT_ENTRY not found." @@ -6805,7 +6818,7 @@ _send_notify() { _nsource="$NOTIFY_SOURCE" if [ -z "$_nsource" ]; then - _nsource="$(hostname)" + _nsource="$(uname -n)" fi _nsubject="$_nsubject by $_nsource" @@ -7007,7 +7020,7 @@ Parameters: --accountconf Specifies a customized account config file. --home Specifies the home dir for $PROJECT_NAME. - --cert-home Specifies the home dir to save all the certs, only valid for '--install' command. + --cert-home Specifies the home dir to save all the certs. --config-home Specifies the home dir to save all the configurations. --useragent Specifies the user agent string. it will be saved for future use too. -m, --email Specifies the account email, only valid for the '--install' and '--update-account' command. diff --git a/deploy/docker.sh b/deploy/docker.sh index c9815d5b..264963ae 100755 --- a/deploy/docker.sh +++ b/deploy/docker.sh @@ -18,6 +18,7 @@ docker_deploy() { _ccert="$3" _cca="$4" _cfullchain="$5" + _cpfx="$6" _debug _cdomain "$_cdomain" _getdeployconf DEPLOY_DOCKER_CONTAINER_LABEL _debug2 DEPLOY_DOCKER_CONTAINER_LABEL "$DEPLOY_DOCKER_CONTAINER_LABEL" @@ -88,6 +89,12 @@ docker_deploy() { _savedeployconf DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE" fi + _getdeployconf DEPLOY_DOCKER_CONTAINER_PFX_FILE + _debug2 DEPLOY_DOCKER_CONTAINER_PFX_FILE "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" + if [ "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" ]; then + _savedeployconf DEPLOY_DOCKER_CONTAINER_PFX_FILE "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" + fi + _getdeployconf DEPLOY_DOCKER_CONTAINER_RELOAD_CMD _debug2 DEPLOY_DOCKER_CONTAINER_RELOAD_CMD "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then @@ -125,6 +132,12 @@ docker_deploy() { fi fi + if [ "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" ]; then + if ! _docker_cp "$_cid" "$_cpfx" "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"; then + return 1 + fi + 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 diff --git a/deploy/haproxy.sh b/deploy/haproxy.sh index c8491d92..19509e3b 100644 --- a/deploy/haproxy.sh +++ b/deploy/haproxy.sh @@ -357,7 +357,7 @@ haproxy_deploy() { _info "Update existing certificate '${_pem}' over HAProxy ${_socketname}." fi _socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'" - _debug _socat_cert_set_cmd "${_socat_cert_set_cmd}" + _secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}" eval "${_socat_cert_set_cmd}" _ret=$? if [ "${_ret}" != "0" ]; then diff --git a/deploy/proxmoxbs.sh b/deploy/proxmoxbs.sh new file mode 100644 index 00000000..d1146454 --- /dev/null +++ b/deploy/proxmoxbs.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env sh + +# Deploy certificates to a proxmox backup server using the API. +# +# Environment variables that can be set are: +# `DEPLOY_PROXMOXBS_SERVER`: The hostname of the proxmox backup server. Defaults to +# _cdomain. +# `DEPLOY_PROXMOXBS_SERVER_PORT`: The port number the management interface is on. +# Defaults to 8007. +# `DEPLOY_PROXMOXBS_USER`: The user we'll connect as. Defaults to root. +# `DEPLOY_PROXMOXBS_USER_REALM`: The authentication realm the user authenticates +# with. Defaults to pam. +# `DEPLOY_PROXMOXBS_API_TOKEN_NAME`: The name of the API token created for the +# user account. Defaults to acme. +# `DEPLOY_PROXMOXBS_API_TOKEN_KEY`: The API token. Required. + +proxmoxbs_deploy() { + _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" + + # "Sane" defaults. + _getdeployconf DEPLOY_PROXMOXBS_SERVER + if [ -z "$DEPLOY_PROXMOXBS_SERVER" ]; then + _target_hostname="$_cdomain" + else + _target_hostname="$DEPLOY_PROXMOXBS_SERVER" + _savedeployconf DEPLOY_PROXMOXBS_SERVER "$DEPLOY_PROXMOXBS_SERVER" + fi + _debug2 DEPLOY_PROXMOXBS_SERVER "$_target_hostname" + + _getdeployconf DEPLOY_PROXMOXBS_SERVER_PORT + if [ -z "$DEPLOY_PROXMOXBS_SERVER_PORT" ]; then + _target_port="8007" + else + _target_port="$DEPLOY_PROXMOXBS_SERVER_PORT" + _savedeployconf DEPLOY_PROXMOXBS_SERVER_PORT "$DEPLOY_PROXMOXBS_SERVER_PORT" + fi + _debug2 DEPLOY_PROXMOXBS_SERVER_PORT "$_target_port" + + # Complete URL. + _target_url="https://${_target_hostname}:${_target_port}/api2/json/nodes/localhost/certificates/custom" + _debug TARGET_URL "$_target_url" + + # More "sane" defaults. + _getdeployconf DEPLOY_PROXMOXBS_USER + if [ -z "$DEPLOY_PROXMOXBS_USER" ]; then + _proxmoxbs_user="root" + else + _proxmoxbs_user="$DEPLOY_PROXMOXBS_USER" + _savedeployconf DEPLOY_PROXMOXBS_USER "$DEPLOY_PROXMOXBS_USER" + fi + _debug2 DEPLOY_PROXMOXBS_USER "$_proxmoxbs_user" + + _getdeployconf DEPLOY_PROXMOXBS_USER_REALM + if [ -z "$DEPLOY_PROXMOXBS_USER_REALM" ]; then + _proxmoxbs_user_realm="pam" + else + _proxmoxbs_user_realm="$DEPLOY_PROXMOXBS_USER_REALM" + _savedeployconf DEPLOY_PROXMOXBS_USER_REALM "$DEPLOY_PROXMOXBS_USER_REALM" + fi + _debug2 DEPLOY_PROXMOXBS_USER_REALM "$_proxmoxbs_user_realm" + + _getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME + if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_NAME" ]; then + _proxmoxbs_api_token_name="acme" + else + _proxmoxbs_api_token_name="$DEPLOY_PROXMOXBS_API_TOKEN_NAME" + _savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME "$DEPLOY_PROXMOXBS_API_TOKEN_NAME" + fi + _debug2 DEPLOY_PROXMOXBS_API_TOKEN_NAME "$_proxmoxbs_api_token_name" + + # This is required. + _getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY + if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_KEY" ]; then + _err "API key not provided." + return 1 + else + _proxmoxbs_api_token_key="$DEPLOY_PROXMOXBS_API_TOKEN_KEY" + _savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY "$DEPLOY_PROXMOXBS_API_TOKEN_KEY" + fi + _debug2 DEPLOY_PROXMOXBS_API_TOKEN_KEY "$_proxmoxbs_api_token_key" + + # PBS API Token header value. Used in "Authorization: PBSAPIToken". + _proxmoxbs_header_api_token="${_proxmoxbs_user}@${_proxmoxbs_user_realm}!${_proxmoxbs_api_token_name}:${_proxmoxbs_api_token_key}" + _debug2 "Auth Header" "$_proxmoxbs_header_api_token" + + # Ugly. I hate putting heredocs inside functions because heredocs don't + # account for whitespace correctly but it _does_ work and is several times + # cleaner than anything else I had here. + # + # This dumps the json payload to a variable that should be passable to the + # _psot function. + _json_payload=$( + cat < +# +# ```sh +# acme.sh --deploy -d ruckus.example.com --deploy-hook ruckus +# ``` +# +# Then you need to set the environment variables for the +# deploy script to work. +# +# ```sh +# export RUCKUS_HOST=myruckus.example.com +# export RUCKUS_USER=myruckususername +# export RUCKUS_PASS=myruckuspassword +# +# acme.sh --deploy -d myruckus.example.com --deploy-hook ruckus +# ``` +# +# returns 0 means success, otherwise error. + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +ruckus_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + _err_code=0 + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + _getdeployconf RUCKUS_HOST + _getdeployconf RUCKUS_USER + _getdeployconf RUCKUS_PASS + + if [ -z "$RUCKUS_HOST" ]; then + _debug "Using _cdomain as RUCKUS_HOST, please set if not correct." + RUCKUS_HOST="$_cdomain" + fi + + if [ -z "$RUCKUS_USER" ]; then + _err "Need to set the env variable RUCKUS_USER" + return 1 + fi + + if [ -z "$RUCKUS_PASS" ]; then + _err "Need to set the env variable RUCKUS_PASS" + return 1 + fi + + _savedeployconf RUCKUS_HOST "$RUCKUS_HOST" + _savedeployconf RUCKUS_USER "$RUCKUS_USER" + _savedeployconf RUCKUS_PASS "$RUCKUS_PASS" + + _debug RUCKUS_HOST "$RUCKUS_HOST" + _debug RUCKUS_USER "$RUCKUS_USER" + _secure_debug RUCKUS_PASS "$RUCKUS_PASS" + + export ACME_HTTP_NO_REDIRECTS=1 + + _info "Discovering the login URL" + _get "https://$RUCKUS_HOST" >/dev/null + _login_url="$(_response_header 'Location')" + if [ -n "$_login_url" ]; then + _login_path=$(echo "$_login_url" | sed 's|https\?://[^/]\+||') + if [ -z "$_login_path" ]; then + # redirect was to a different host + _err "Connection failed: redirected to a different host. Configure Unleashed with a Preferred Master or Management Interface." + return 1 + fi + fi + + if [ -z "${_login_url}" ]; then + _err "Connection failed: couldn't find login page." + return 1 + fi + + _base_url=$(dirname "$_login_url") + _login_page=$(basename "$_login_url") + + if [ "$_login_page" = "index.html" ]; then + _err "Connection temporarily unavailable: Unleashed Rebuilding." + return 1 + fi + + if [ "$_login_page" = "wizard.jsp" ]; then + _err "Connection failed: Setup Wizard not complete." + return 1 + fi + + _info "Login" + _username_encoded="$(printf "%s" "$RUCKUS_USER" | _url_encode)" + _password_encoded="$(printf "%s" "$RUCKUS_PASS" | _url_encode)" + _login_query="$(printf "%s" "username=${_username_encoded}&password=${_password_encoded}&ok=Log+In")" + _post "$_login_query" "$_login_url" >/dev/null + + _login_code="$(_response_code)" + if [ "$_login_code" = "200" ]; then + _err "Login failed: incorrect credentials." + return 1 + fi + + _info "Collect Session Cookie" + _H1="Cookie: $(_response_cookie)" + export _H1 + _info "Collect CSRF Token" + _H2="X-CSRF-Token: $(_response_header 'HTTP_X_CSRF_TOKEN')" + export _H2 + + _info "Uploading certificate" + _post_upload "uploadcert" "$_cfullchain" + + _info "Uploading private key" + _post_upload "uploadprivatekey" "$_ckey" + + _info "Replacing certificate" + _replace_cert_ajax='' + _post "$_replace_cert_ajax" "$_base_url/_cmdstat.jsp" >/dev/null + + _info "Rebooting" + _cert_reboot_ajax='' + _post "$_cert_reboot_ajax" "$_base_url/_cmdstat.jsp" >/dev/null + + return 0 +} + +_response_code() { + _egrep_o <"$HTTP_HEADER" "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n" | _egrep_o "^[0-9]*" +} + +_response_header() { + grep <"$HTTP_HEADER" -i "^$1:" | cut -d ':' -f 2- | tr -d "\r\n\t " +} + +_response_cookie() { + _response_header 'Set-Cookie' | sed 's/;.*//' +} + +_post_upload() { + _post_action="$1" + _post_file="$2" + + _post_boundary="----FormBoundary$(date "+%s%N")" + + _post_data="$({ + printf -- "--%s\r\n" "$_post_boundary" + printf -- "Content-Disposition: form-data; name=\"u\"; filename=\"%s\"\r\n" "$_post_action" + printf -- "Content-Type: application/octet-stream\r\n\r\n" + printf -- "%s\r\n" "$(cat "$_post_file")" + + printf -- "--%s\r\n" "$_post_boundary" + printf -- "Content-Disposition: form-data; name=\"action\"\r\n\r\n" + printf -- "%s\r\n" "$_post_action" + + printf -- "--%s\r\n" "$_post_boundary" + printf -- "Content-Disposition: form-data; name=\"callback\"\r\n\r\n" + printf -- "%s\r\n" "uploader_$_post_action" + + printf -- "--%s--\r\n\r\n" "$_post_boundary" + })" + + _post "$_post_data" "$_base_url/_upload.jsp?request_type=xhr" "" "" "multipart/form-data; boundary=$_post_boundary" >/dev/null +} diff --git a/deploy/strongswan.sh b/deploy/strongswan.sh index 3d5f1b34..14567d17 100644 --- a/deploy/strongswan.sh +++ b/deploy/strongswan.sh @@ -10,46 +10,89 @@ #domain keyfile certfile cafile fullchain strongswan_deploy() { - _cdomain="$1" - _ckey="$2" - _ccert="$3" - _cca="$4" - _cfullchain="$5" - + _cdomain="${1}" + _ckey="${2}" + _ccert="${3}" + _cca="${4}" + _cfullchain="${5}" _info "Using strongswan" - - if [ -x /usr/sbin/ipsec ]; then - _ipsec=/usr/sbin/ipsec - elif [ -x /usr/sbin/strongswan ]; then - _ipsec=/usr/sbin/strongswan - elif [ -x /usr/local/sbin/ipsec ]; then - _ipsec=/usr/local/sbin/ipsec - else + if _exists ipsec; then + _ipsec=ipsec + elif _exists strongswan; then + _ipsec=strongswan + fi + if _exists swanctl; then + _swanctl=swanctl + fi + # For legacy stroke mode + if [ -n "${_ipsec}" ]; then + _info "${_ipsec} command detected" + _confdir=$(${_ipsec} --confdir) + if [ -z "${_confdir}" ]; then + _err "no strongswan --confdir is detected" + return 1 + fi + _info _confdir "${_confdir}" + __deploy_cert "$@" "stroke" "${_confdir}" + ${_ipsec} reload + fi + # For modern vici mode + if [ -n "${_swanctl}" ]; then + _info "${_swanctl} command detected" + for _dir in /usr/local/etc/swanctl /etc/swanctl /etc/strongswan/swanctl; do + if [ -d ${_dir} ]; then + _confdir=${_dir} + _info _confdir "${_confdir}" + break + fi + done + if [ -z "${_confdir}" ]; then + _err "no swanctl config dir is found" + return 1 + fi + __deploy_cert "$@" "vici" "${_confdir}" + ${_swanctl} --load-creds + fi + if [ -z "${_swanctl}" ] && [ -z "${_ipsec}" ]; then _err "no strongswan or ipsec command is detected" + _err "no swanctl is detected" return 1 fi +} - _info _ipsec "$_ipsec" +#################### Private functions below ################################## - _confdir=$($_ipsec --confdir) - if [ $? -ne 0 ] || [ -z "$_confdir" ]; then - _err "no strongswan --confdir is detected" +__deploy_cert() { + _cdomain="${1}" + _ckey="${2}" + _ccert="${3}" + _cca="${4}" + _cfullchain="${5}" + _swan_mode="${6}" + _confdir="${7}" + _debug _cdomain "${_cdomain}" + _debug _ckey "${_ckey}" + _debug _ccert "${_ccert}" + _debug _cca "${_cca}" + _debug _cfullchain "${_cfullchain}" + _debug _swan_mode "${_swan_mode}" + _debug _confdir "${_confdir}" + if [ "${_swan_mode}" = "vici" ]; then + _dir_private="private" + _dir_cert="x509" + _dir_ca="x509ca" + elif [ "${_swan_mode}" = "stroke" ]; then + _dir_private="ipsec.d/private" + _dir_cert="ipsec.d/certs" + _dir_ca="ipsec.d/cacerts" + else + _err "unknown StrongSwan mode ${_swan_mode}" return 1 fi - - _info _confdir "$_confdir" - - _debug _cdomain "$_cdomain" - _debug _ckey "$_ckey" - _debug _ccert "$_ccert" - _debug _cca "$_cca" - _debug _cfullchain "$_cfullchain" - - cat "$_ckey" >"${_confdir}/ipsec.d/private/$(basename "$_ckey")" - cat "$_ccert" >"${_confdir}/ipsec.d/certs/$(basename "$_ccert")" - cat "$_cca" >"${_confdir}/ipsec.d/cacerts/$(basename "$_cca")" - cat "$_cfullchain" >"${_confdir}/ipsec.d/cacerts/$(basename "$_cfullchain")" - - $_ipsec reload - + cat "${_ckey}" >"${_confdir}/${_dir_private}/$(basename "${_ckey}")" + cat "${_ccert}" >"${_confdir}/${_dir_cert}/$(basename "${_ccert}")" + cat "${_cca}" >"${_confdir}/${_dir_ca}/$(basename "${_cca}")" + if [ "${_swan_mode}" = "stroke" ]; then + cat "${_cfullchain}" >"${_confdir}/${_dir_ca}/$(basename "${_cfullchain}")" + fi } diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index 0d01e199..3bfc9b02 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -186,8 +186,8 @@ synology_dsm_deploy() { if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then _getdeployconf SYNO_LOCAL_HOSTNAME _debug SYNO_LOCAL_HOSTNAME "${SYNO_LOCAL_HOSTNAME:-}" - if [ "$SYNO_LOCAL_HOSTNAME" != "1" ] && [ "$SYNO_LOCAL_HOSTNAME" == "$SYNO_HOSTNAME" ]; then - if [ "$SYNO_HOSTNAME" != "localhost" ] && [ "$SYNO_HOSTNAME" != "127.0.0.1" ]; then + if [ "$SYNO_HOSTNAME" != "localhost" ] && [ "$SYNO_HOSTNAME" != "127.0.0.1" ]; then + if [ "$SYNO_LOCAL_HOSTNAME" != "1" ]; then _err "SYNO_USE_TEMP_ADMIN=1 only support local deployment, though if you are sure that the hostname $SYNO_HOSTNAME is targeting to your **current local machine**, execute 'export SYNO_LOCAL_HOSTNAME=1' then rerun." return 1 fi @@ -320,7 +320,7 @@ synology_dsm_deploy() { _cleardeployconf SYNO_DEVICE_ID _cleardeployconf SYNO_DEVICE_NAME _savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN" - _savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_HOSTNAME" + _savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME" else _savedeployconf SYNO_USERNAME "$SYNO_USERNAME" _savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD" @@ -411,7 +411,7 @@ _temp_admin_create() { _username="$1" _password="$2" synouser --del "$_username" >/dev/null 2>/dev/null - synouser --add "$_username" "$_password" "" 0 "scruelt@hotmail.com" 0 >/dev/null + synouser --add "$_username" "$_password" "" 0 "" 0 >/dev/null } _temp_admin_cleanup() { diff --git a/deploy/truenas.sh b/deploy/truenas.sh index c79e6dac..6a008bd7 100644 --- a/deploy/truenas.sh +++ b/deploy/truenas.sh @@ -9,7 +9,7 @@ # # Following environment variables must be set: # -# export DEPLOY_TRUENAS_APIKEY="/ui/apikeys +# export DEPLOY_TRUENAS_APIKEY="/dev/null 2>&1 # fail quietly if we're not running as root + fi + # Update unifi service for certificate cipher compatibility + _unifi_system_properties="${DEPLOY_UNIFI_SYSTEM_PROPERTIES:-/usr/lib/unifi/data/system.properties}" if ${ACME_OPENSSL_BIN:-openssl} pkcs12 \ -in "$_import_pkcs12" \ -password pass:aircontrolenterprise \ -nokeys | ${ACME_OPENSSL_BIN:-openssl} x509 -text \ -noout | grep -i "signature" | grep -iq ecdsa >/dev/null 2>&1; then - cp -f /usr/lib/unifi/data/system.properties /usr/lib/unifi/data/system.properties_original - _info "Updating system configuration for cipher compatibility." - _info "Saved original system config to /usr/lib/unifi/data/system.properties_original" - sed -i '/unifi\.https\.ciphers/d' /usr/lib/unifi/data/system.properties - echo "unifi.https.ciphers=ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES128-GCM-SHA256" >>/usr/lib/unifi/data/system.properties - sed -i '/unifi\.https\.sslEnabledProtocols/d' /usr/lib/unifi/data/system.properties - echo "unifi.https.sslEnabledProtocols=TLSv1.3,TLSv1.2" >>/usr/lib/unifi/data/system.properties - _info "System configuration updated." + if [ -f "$(dirname "${DEPLOY_UNIFI_KEYSTORE}")/system.properties" ]; then + _unifi_system_properties="$(dirname "${DEPLOY_UNIFI_KEYSTORE}")/system.properties" + else + _unifi_system_properties="/usr/lib/unifi/data/system.properties" + fi + if [ -f "${_unifi_system_properties}" ]; then + cp -f "${_unifi_system_properties}" "${_unifi_system_properties}"_original + _info "Updating system configuration for cipher compatibility." + _info "Saved original system config to ${_unifi_system_properties}_original" + sed -i '/unifi\.https\.ciphers/d' "${_unifi_system_properties}" + echo "unifi.https.ciphers=ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES128-GCM-SHA256" >>"${_unifi_system_properties}" + sed -i '/unifi\.https\.sslEnabledProtocols/d' "${_unifi_system_properties}" + echo "unifi.https.sslEnabledProtocols=TLSv1.3,TLSv1.2" >>"${_unifi_system_properties}" + _info "System configuration updated." + fi fi rm "$_import_pkcs12" # Restarting unifi-core will bring up unifi, doing it out of order results in # a certificate error, and breaks wifiman. - # Restart if we aren't doing unifi-core, otherwise stop for later restart. - if systemctl -q is-active unifi; then - if [ ! -f "${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}/unifi-core.key" ]; then - _reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl restart unifi" - else - _reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl stop unifi" - fi + # Restart if we aren't doing Unifi OS (e.g. unifi-core service), otherwise stop for later restart. + _unifi_reload="${DEPLOY_UNIFI_RELOAD:-systemctl restart unifi}" + if [ ! -f "${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}/unifi-core.key" ]; then + _reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_reload" + else + _info "Stopping Unifi Controller for later restart." + _unifi_stop=$(echo "${_unifi_reload}" | sed -e 's/restart/stop/') + $_unifi_stop + _reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_reload" + _info "Unifi Controller stopped." fi _services_updated="${_services_updated} unifi" _info "Install Unifi Controller certificate success!" @@ -181,13 +207,24 @@ unifi_deploy() { return 1 fi # Cloud Key expects to load the keystore from /etc/ssl/private/unifi.keystore.jks. - # Normally /usr/lib/unifi/data/keystore is a symlink there (so the keystore was - # updated above), but if not, we don't know how to handle this installation: - if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then - _err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'" - return 1 + # It appears that unifi won't start if this is a symlink, so we'll copy it instead. + + # if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then + # _err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'" + # return 1 + # fi + + _info "Updating ${_cloudkey_certdir}/unifi.keystore.jks" + if [ -e "${_cloudkey_certdir}/unifi.keystore.jks" ]; then + if [ -L "${_cloudkey_certdir}/unifi.keystore.jks" ]; then + rm -f "${_cloudkey_certdir}/unifi.keystore.jks" + else + mv "${_cloudkey_certdir}/unifi.keystore.jks" "${_cloudkey_certdir}/unifi.keystore.jks_original" + fi fi + cp "${_unifi_keystore}" "${_cloudkey_certdir}/unifi.keystore.jks" + cat "$_cfullchain" >"${_cloudkey_certdir}/cloudkey.crt" cat "$_ckey" >"${_cloudkey_certdir}/cloudkey.key" (cd "$_cloudkey_certdir" && tar -cf cert.tar cloudkey.crt cloudkey.key unifi.keystore.jks) @@ -215,14 +252,14 @@ unifi_deploy() { # Save the existing certs in case something goes wrong. cp -f "${_unifi_core_config}"/unifi-core.crt "${_unifi_core_config}"/unifi-core_original.crt cp -f "${_unifi_core_config}"/unifi-core.key "${_unifi_core_config}"/unifi-core_original.key - _info "Previous certificate and key saved to ${_unifi_core_config}/unifi-core_original.crt/key." + _info "Previous certificate and key saved to ${_unifi_core_config}/unifi-core_original.crt.key." cat "$_cfullchain" >"${_unifi_core_config}/unifi-core.crt" cat "$_ckey" >"${_unifi_core_config}/unifi-core.key" - if systemctl -q is-active unifi-core; then - _reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl restart unifi-core" - fi + _unifi_os_reload="${DEPLOY_UNIFI_OS_RELOAD:-systemctl restart unifi-core}" + _reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_os_reload" + _info "Install UnifiOS certificate success!" _services_updated="${_services_updated} unifi-core" elif [ "$DEPLOY_UNIFI_CORE_CONFIG" ]; then @@ -261,6 +298,8 @@ unifi_deploy() { _savedeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR" _savedeployconf DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG" _savedeployconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD" + _savedeployconf DEPLOY_UNIFI_OS_RELOAD "$DEPLOY_UNIFI_OS_RELOAD" + _savedeployconf DEPLOY_UNIFI_SYSTEM_PROPERTIES "$DEPLOY_UNIFI_SYSTEM_PROPERTIES" return 0 } diff --git a/dnsapi/dns_alviy.sh b/dnsapi/dns_alviy.sh index 2217b0df..8aa7da34 100644 --- a/dnsapi/dns_alviy.sh +++ b/dnsapi/dns_alviy.sh @@ -1,11 +1,12 @@ #!/usr/bin/env sh -# Alviy domain api -# -# Get API key and secret from https://cloud.alviy.com/token -# -# Alviy_token="some-secret-key" -# -# Ex.: acme.sh --issue --staging --dns dns_alviy -d "*.s.example.com" -d "s.example.com" +# shellcheck disable=SC2034 +dns_alviy_info='Alviy.com +Site: Alviy.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_alviy +Options: + Alviy_token API token. Get it from the https://cloud.alviy.com/token +Issues: github.com/acmesh-official/acme.sh/issues/5115 +' Alviy_Api="https://cloud.alviy.com/api/v1" diff --git a/dnsapi/dns_azure.sh b/dnsapi/dns_azure.sh index 7d818cc2..03feaf63 100644 --- a/dnsapi/dns_azure.sh +++ b/dnsapi/dns_azure.sh @@ -9,6 +9,7 @@ Options: AZUREDNS_APPID App ID. App ID of the service principal AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false" + AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Optional. ' wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS @@ -31,6 +32,7 @@ dns_azure_add() { AZUREDNS_TENANTID="" AZUREDNS_APPID="" AZUREDNS_CLIENTSECRET="" + AZUREDNS_BEARERTOKEN="" _err "You didn't specify the Azure Subscription ID" return 1 fi @@ -45,37 +47,45 @@ dns_azure_add() { _saveaccountconf_mutable AZUREDNS_TENANTID "" _saveaccountconf_mutable AZUREDNS_APPID "" _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "" + _saveaccountconf_mutable AZUREDNS_BEARERTOKEN "" else - _info "You didn't ask to use Azure managed identity, checking service principal credentials" + _info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token" AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" + AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}" + if [ -z "$AZUREDNS_BEARERTOKEN" ]; then + if [ -z "$AZUREDNS_TENANTID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + AZUREDNS_BEARERTOKEN="" + _err "You didn't specify the Azure Tenant ID " + return 1 + fi - if [ -z "$AZUREDNS_TENANTID" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure Tenant ID " - return 1 - fi - - if [ -z "$AZUREDNS_APPID" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure App ID" - return 1 - fi + if [ -z "$AZUREDNS_APPID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + AZUREDNS_BEARERTOKEN="" + _err "You didn't specify the Azure App ID" + return 1 + fi - if [ -z "$AZUREDNS_CLIENTSECRET" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure Client Secret" - return 1 + if [ -z "$AZUREDNS_CLIENTSECRET" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + AZUREDNS_BEARERTOKEN="" + _err "You didn't specify the Azure Client Secret" + return 1 + fi + else + _info "Using provided bearer token" fi #save account details to account conf file, don't opt in for azure manages identity check. @@ -83,9 +93,14 @@ dns_azure_add() { _saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID" _saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID" _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET" + _saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$AZUREDNS_BEARERTOKEN" fi - accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") + if [ -z "$AZUREDNS_BEARERTOKEN" ]; then + accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") + else + accesstoken=$(echo "$AZUREDNS_BEARERTOKEN" | sed "s/Bearer //g") + fi if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then _err "invalid domain" @@ -147,6 +162,7 @@ dns_azure_rm() { AZUREDNS_TENANTID="" AZUREDNS_APPID="" AZUREDNS_CLIENTSECRET="" + AZUREDNS_BEARERTOKEN="" _err "You didn't specify the Azure Subscription ID " return 1 fi @@ -155,40 +171,51 @@ dns_azure_rm() { if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then _info "Using Azure managed identity" else - _info "You didn't ask to use Azure managed identity, checking service principal credentials" + _info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token" AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" + AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}" + if [ -z "$AZUREDNS_BEARERTOKEN" ]; then + if [ -z "$AZUREDNS_TENANTID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + AZUREDNS_BEARERTOKEN="" + _err "You didn't specify the Azure Tenant ID " + return 1 + fi - if [ -z "$AZUREDNS_TENANTID" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure Tenant ID " - return 1 - fi - - if [ -z "$AZUREDNS_APPID" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure App ID" - return 1 - fi + if [ -z "$AZUREDNS_APPID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + AZUREDNS_BEARERTOKEN="" + _err "You didn't specify the Azure App ID" + return 1 + fi - if [ -z "$AZUREDNS_CLIENTSECRET" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure Client Secret" - return 1 + if [ -z "$AZUREDNS_CLIENTSECRET" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + AZUREDNS_BEARERTOKEN="" + _err "You didn't specify the Azure Client Secret" + return 1 + fi + else + _info "Using provided bearer token" fi fi - accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") + if [ -z "$AZUREDNS_BEARERTOKEN" ]; then + accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") + else + accesstoken=$(echo "$AZUREDNS_BEARERTOKEN" | sed "s/Bearer //g") + fi if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then _err "invalid domain" @@ -295,7 +322,7 @@ _azure_getaccess_token() { clientID=$3 clientSecret=$4 - accesstoken="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}" + accesstoken="${AZUREDNS_ACCESSTOKEN:-$(_readaccountconf_mutable AZUREDNS_ACCESSTOKEN)}" expires_on="${AZUREDNS_TOKENVALIDTO:-$(_readaccountconf_mutable AZUREDNS_TOKENVALIDTO)}" # can we reuse the bearer token? @@ -339,7 +366,7 @@ _azure_getaccess_token() { _err "error $response" return 1 fi - _saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$accesstoken" + _saveaccountconf_mutable AZUREDNS_ACCESSTOKEN "$accesstoken" _saveaccountconf_mutable AZUREDNS_TOKENVALIDTO "$expires_on" printf "%s" "$accesstoken" return 0 diff --git a/dnsapi/dns_beget.sh b/dnsapi/dns_beget.sh new file mode 100755 index 00000000..aa43caed --- /dev/null +++ b/dnsapi/dns_beget.sh @@ -0,0 +1,281 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_beget_info='Beget.com +Site: Beget.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_beget +Options: + BEGET_User API user + BEGET_Password API password +Issues: github.com/acmesh-official/acme.sh/issues/6200 +Author: ARNik arnik@arnik.ru +' + +Beget_Api="https://api.beget.com/api" + +#################### Public functions #################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +dns_beget_add() { + fulldomain=$1 + txtvalue=$2 + _debug "dns_beget_add() $fulldomain $txtvalue" + fulldomain=$(echo "$fulldomain" | _lower_case) + + Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}" + Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}" + + if [ -z "$Beget_Username" ] || [ -z "$Beget_Password" ]; then + Beget_Username="" + Beget_Password="" + _err "You must export variables: Beget_Username, and Beget_Password" + return 1 + fi + + #save the credentials to the account conf file. + _saveaccountconf_mutable Beget_Username "$Beget_Username" + _saveaccountconf_mutable Beget_Password "$Beget_Password" + + _info "Prepare subdomain." + if ! _prepare_subdomain "$fulldomain"; then + _err "Can't prepare subdomain." + return 1 + fi + + _info "Get domain records" + data="{\"fqdn\":\"$fulldomain\"}" + res=$(_api_call "$Beget_Api/dns/getData" "$data") + if ! _is_api_reply_ok "$res"; then + _err "Can't get domain records." + return 1 + fi + + _info "Add new TXT record" + data="{\"fqdn\":\"$fulldomain\",\"records\":{" + data=${data}$(_parce_records "$res" "A") + data=${data}$(_parce_records "$res" "AAAA") + data=${data}$(_parce_records "$res" "CAA") + data=${data}$(_parce_records "$res" "MX") + data=${data}$(_parce_records "$res" "SRV") + data=${data}$(_parce_records "$res" "TXT") + data=$(echo "$data" | sed 's/,$//') + data=${data}'}}' + + str=$(_txt_to_dns_json "$txtvalue") + data=$(_add_record "$data" "TXT" "$str") + + res=$(_api_call "$Beget_Api/dns/changeRecords" "$data") + if ! _is_api_reply_ok "$res"; then + _err "Can't change domain records." + return 1 + fi + + return 0 +} + +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation +dns_beget_rm() { + fulldomain=$1 + txtvalue=$2 + _debug "dns_beget_rm() $fulldomain $txtvalue" + fulldomain=$(echo "$fulldomain" | _lower_case) + + Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}" + Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}" + + _info "Get current domain records" + data="{\"fqdn\":\"$fulldomain\"}" + res=$(_api_call "$Beget_Api/dns/getData" "$data") + if ! _is_api_reply_ok "$res"; then + _err "Can't get domain records." + return 1 + fi + + _info "Remove TXT record" + data="{\"fqdn\":\"$fulldomain\",\"records\":{" + data=${data}$(_parce_records "$res" "A") + data=${data}$(_parce_records "$res" "AAAA") + data=${data}$(_parce_records "$res" "CAA") + data=${data}$(_parce_records "$res" "MX") + data=${data}$(_parce_records "$res" "SRV") + data=${data}$(_parce_records "$res" "TXT") + data=$(echo "$data" | sed 's/,$//') + data=${data}'}}' + + str=$(_txt_to_dns_json "$txtvalue") + data=$(_rm_record "$data" "$str") + + res=$(_api_call "$Beget_Api/dns/changeRecords" "$data") + if ! _is_api_reply_ok "$res"; then + _err "Can't change domain records." + return 1 + fi + + return 0 +} + +#################### Private functions below #################### + +# Create subdomain if needed +# Usage: _prepare_subdomain [fulldomain] +_prepare_subdomain() { + fulldomain=$1 + + _info "Detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + if [ -z "$_sub_domain" ]; then + _debug "$fulldomain is a root domain." + return 0 + fi + + _info "Get subdomain list" + res=$(_api_call "$Beget_Api/domain/getSubdomainList") + if ! _is_api_reply_ok "$res"; then + _err "Can't get subdomain list." + return 1 + fi + + if _contains "$res" "\"fqdn\":\"$fulldomain\""; then + _debug "Subdomain $fulldomain already exist." + return 0 + fi + + _info "Subdomain $fulldomain does not exist. Let's create one." + data="{\"subdomain\":\"$_sub_domain\",\"domain_id\":$_domain_id}" + res=$(_api_call "$Beget_Api/domain/addSubdomainVirtual" "$data") + if ! _is_api_reply_ok "$res"; then + _err "Can't create subdomain." + return 1 + fi + + _debug "Cleanup subdomen records" + data="{\"fqdn\":\"$fulldomain\",\"records\":{}}" + res=$(_api_call "$Beget_Api/dns/changeRecords" "$data") + if ! _is_api_reply_ok "$res"; then + _debug "Can't cleanup $fulldomain records." + fi + + data="{\"fqdn\":\"www.$fulldomain\",\"records\":{}}" + res=$(_api_call "$Beget_Api/dns/changeRecords" "$data") + if ! _is_api_reply_ok "$res"; then + _debug "Can't cleanup www.$fulldomain records." + fi + + return 0 +} + +# Usage: _get_root _acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=32436365 +_get_root() { + fulldomain=$1 + i=1 + p=1 + + _debug "Get domain list" + res=$(_api_call "$Beget_Api/domain/getList") + if ! _is_api_reply_ok "$res"; then + _err "Can't get domain list." + return 1 + fi + + while true; do + h=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100) + _debug h "$h" + + if [ -z "$h" ]; then + return 1 + fi + + if _contains "$res" "$h"; then + _domain_id=$(echo "$res" | _egrep_o "\"id\":[0-9]*,\"fqdn\":\"$h\"" | cut -d , -f1 | cut -d : -f2) + if [ "$_domain_id" ]; then + if [ "$h" != "$fulldomain" ]; then + _sub_domain=$(echo "$fulldomain" | cut -d . -f 1-"$p") + else + _sub_domain="" + fi + _domain=$h + return 0 + fi + return 1 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +# Parce DNS records from json string +# Usage: _parce_records [j_str] [record_name] +_parce_records() { + j_str=$1 + record_name=$2 + res="\"$record_name\":[" + res=${res}$(echo "$j_str" | _egrep_o "\"$record_name\":\[.*" | cut -d '[' -f2 | cut -d ']' -f1) + res=${res}"]," + echo "$res" +} + +# Usage: _add_record [data] [record_name] [record_data] +_add_record() { + data=$1 + record_name=$2 + record_data=$3 + echo "$data" | sed "s/\"$record_name\":\[/\"$record_name\":\[$record_data,/" | sed "s/,\]/\]/" +} + +# Usage: _rm_record [data] [record_data] +_rm_record() { + data=$1 + record_data=$2 + echo "$data" | sed "s/$record_data//g" | sed "s/,\+/,/g" | + sed "s/{,/{/g" | sed "s/,}/}/g" | + sed "s/\[,/\[/g" | sed "s/,\]/\]/g" +} + +_txt_to_dns_json() { + echo "{\"ttl\":600,\"txtdata\":\"$1\"}" +} + +# Usage: _api_call [api_url] [input_data] +_api_call() { + api_url="$1" + input_data="$2" + + _debug "_api_call $api_url" + _debug "Request: $input_data" + + # res=$(curl -s -L -D ./http.header \ + # "$api_url" \ + # --data-urlencode login=$Beget_Username \ + # --data-urlencode passwd=$Beget_Password \ + # --data-urlencode input_format=json \ + # --data-urlencode output_format=json \ + # --data-urlencode "input_data=$input_data") + + url="$api_url?login=$Beget_Username&passwd=$Beget_Password&input_format=json&output_format=json" + if [ -n "$input_data" ]; then + url=${url}"&input_data=" + url=${url}$(echo "$input_data" | _url_encode) + fi + res=$(_get "$url") + + _debug "Reply: $res" + echo "$res" +} + +# Usage: _is_api_reply_ok [api_reply] +_is_api_reply_ok() { + _contains "$1" '^{"status":"success","answer":{"status":"success","result":.*}}$' +} diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh index 04a515aa..a585e772 100644 --- a/dnsapi/dns_cyon.sh +++ b/dnsapi/dns_cyon.sh @@ -215,10 +215,8 @@ _cyon_change_domain_env() { if ! _cyon_check_if_2fa_missed "${domain_env_response}"; then return 1; fi - domain_env_success="$(printf "%s" "${domain_env_response}" | _egrep_o '"authenticated":\w*' | cut -d : -f 2)" - # Bail if domain environment change fails. - if [ "${domain_env_success}" != "true" ]; then + if [ "$(printf "%s" "${domain_env_response}" | _cyon_get_environment_change_status)" != "true" ]; then _err " $(printf "%s" "${domain_env_response}" | _cyon_get_response_message)" _err "" return 1 @@ -232,7 +230,7 @@ _cyon_add_txt() { _info " - Adding DNS TXT entry..." add_txt_url="https://my.cyon.ch/domain/dnseditor/add-record-async" - add_txt_data="zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}" + add_txt_data="name=${fulldomain_idn}.&ttl=900&type=TXT&dnscontent=${txtvalue}" add_txt_response="$(_post "$add_txt_data" "$add_txt_url")" _debug add_txt_response "${add_txt_response}" @@ -241,9 +239,10 @@ _cyon_add_txt() { add_txt_message="$(printf "%s" "${add_txt_response}" | _cyon_get_response_message)" add_txt_status="$(printf "%s" "${add_txt_response}" | _cyon_get_response_status)" + add_txt_validation="$(printf "%s" "${add_txt_response}" | _cyon_get_validation_status)" # Bail if adding TXT entry fails. - if [ "${add_txt_status}" != "true" ]; then + if [ "${add_txt_status}" != "true" ] || [ "${add_txt_validation}" != "true" ]; then _err " ${add_txt_message}" _err "" return 1 @@ -305,13 +304,21 @@ _cyon_get_response_message() { } _cyon_get_response_status() { - _egrep_o '"status":\w*' | cut -d : -f 2 + _egrep_o '"status":[a-zA-z0-9]*' | cut -d : -f 2 +} + +_cyon_get_validation_status() { + _egrep_o '"valid":[a-zA-z0-9]*' | cut -d : -f 2 } _cyon_get_response_success() { _egrep_o '"onSuccess":"[^"]*"' | cut -d : -f 2 | tr -d '"' } +_cyon_get_environment_change_status() { + _egrep_o '"authenticated":[a-zA-z0-9]*' | cut -d : -f 2 +} + _cyon_check_if_2fa_missed() { # Did we miss the 2FA? if test "${1#*multi_factor_form}" != "${1}"; then diff --git a/dnsapi/dns_fornex.sh b/dnsapi/dns_fornex.sh index cede0cd0..91e5491b 100644 --- a/dnsapi/dns_fornex.sh +++ b/dnsapi/dns_fornex.sh @@ -9,7 +9,7 @@ Issues: github.com/acmesh-official/acme.sh/issues/3998 Author: Timur Umarov ' -FORNEX_API_URL="https://fornex.com/api/dns/v0.1" +FORNEX_API_URL="https://fornex.com/api" ######## Public functions ##################### @@ -30,12 +30,10 @@ dns_fornex_add() { fi _info "Adding record" - if _rest POST "$_domain/entry_set/add/" "host=$fulldomain&type=TXT&value=$txtvalue&apikey=$FORNEX_API_KEY"; then + if _rest POST "dns/domain/$_domain/entry_set/" "{\"host\" : \"${fulldomain}\" , \"type\" : \"TXT\" , \"value\" : \"${txtvalue}\" , \"ttl\" : null}"; then _debug _response "$response" - if _contains "$response" '"ok": true' || _contains "$response" 'Такая запись уже существует.'; then - _info "Added, OK" - return 0 - fi + _info "Added, OK" + return 0 fi _err "Add txt record error." return 1 @@ -58,21 +56,21 @@ dns_fornex_rm() { fi _debug "Getting txt records" - _rest GET "$_domain/entry_set.json?apikey=$FORNEX_API_KEY" + _rest GET "dns/domain/$_domain/entry_set?type=TXT&q=$fulldomain" if ! _contains "$response" "$txtvalue"; then _err "Txt record not found" return 1 fi - _record_id="$(echo "$response" | _egrep_o "{[^{]*\"value\"*:*\"$txtvalue\"[^}]*}" | sed -n -e 's#.*"id": \([0-9]*\).*#\1#p')" + _record_id="$(echo "$response" | _egrep_o "\{[^\{]*\"value\"*:*\"$txtvalue\"[^\}]*\}" | sed -n -e 's#.*"id":\([0-9]*\).*#\1#p')" _debug "_record_id" "$_record_id" if [ -z "$_record_id" ]; then _err "can not find _record_id" return 1 fi - if ! _rest POST "$_domain/entry_set/$_record_id/delete/" "apikey=$FORNEX_API_KEY"; then + if ! _rest DELETE "dns/domain/$_domain/entry_set/$_record_id/"; then _err "Delete record error." return 1 fi @@ -97,11 +95,11 @@ _get_root() { return 1 fi - if ! _rest GET "domain_list.json?q=$h&apikey=$FORNEX_API_KEY"; then + if ! _rest GET "dns/domain/"; then return 1 fi - if _contains "$response" "\"$h\"" >/dev/null; then + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then _domain=$h return 0 else @@ -134,7 +132,9 @@ _rest() { data="$3" _debug "$ep" - export _H1="Accept: application/json" + export _H1="Authorization: Api-Key $FORNEX_API_KEY" + export _H2="Content-Type: application/json" + export _H3="Accept: application/json" if [ "$m" != "GET" ]; then _debug data "$data" diff --git a/dnsapi/dns_freemyip.sh b/dnsapi/dns_freemyip.sh new file mode 100644 index 00000000..0bad3809 --- /dev/null +++ b/dnsapi/dns_freemyip.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_freemyip_info='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} +Author: Recolic Keghart , @Giova96 +' + +FREEMYIP_DNS_API="https://freemyip.com/update?" + +################ Public functions ################ + +#Usage: dns_freemyip_add fulldomain txtvalue +dns_freemyip_add() { + fulldomain="$1" + txtvalue="$2" + + _info "Add TXT record $txtvalue for $fulldomain using freemyip.com api" + + FREEMYIP_Token="${FREEMYIP_Token:-$(_readaccountconf_mutable FREEMYIP_Token)}" + if [ -z "$FREEMYIP_Token" ]; then + FREEMYIP_Token="" + _err "You don't specify FREEMYIP_Token yet." + _err "Please specify your token and try again." + return 1 + fi + + #save the credentials to the account conf file. + _saveaccountconf_mutable FREEMYIP_Token "$FREEMYIP_Token" + + if _is_root_domain_published "$fulldomain"; then + _err "freemyip API don't allow you to set multiple TXT record for the same subdomain!" + _err "You must apply certificate for only one domain at a time!" + _err "====" + _err "For example, aaa.yourdomain.freemyip.com and bbb.yourdomain.freemyip.com and yourdomain.freemyip.com ALWAYS share the same TXT record. They will overwrite each other if you apply multiple domain at the same time." + _debug "If you are testing this workflow in github pipeline or acmetest, please set TEST_DNS_NO_SUBDOMAIN=1 and TEST_DNS_NO_WILDCARD=1" + return 1 + fi + + # txtvalue must be url-encoded. But it's not necessary for acme txt value. + _freemyip_get_until_ok "${FREEMYIP_DNS_API}token=$FREEMYIP_Token&domain=$fulldomain&txt=$txtvalue" 2>&1 + return $? +} + +#Usage: dns_freemyip_rm fulldomain txtvalue +dns_freemyip_rm() { + fulldomain="$1" + txtvalue="$2" + + _info "Delete TXT record $txtvalue for $fulldomain using freemyip.com api" + + FREEMYIP_Token="${FREEMYIP_Token:-$(_readaccountconf_mutable FREEMYIP_Token)}" + if [ -z "$FREEMYIP_Token" ]; then + FREEMYIP_Token="" + _err "You don't specify FREEMYIP_Token yet." + _err "Please specify your token and try again." + return 1 + fi + + #save the credentials to the account conf file. + _saveaccountconf_mutable FREEMYIP_Token "$FREEMYIP_Token" + + # Leave the TXT record as empty or "null" to delete the record. + _freemyip_get_until_ok "${FREEMYIP_DNS_API}token=$FREEMYIP_Token&domain=$fulldomain&txt=" 2>&1 + return $? +} + +################ Private functions below ################ +_get_root() { + _fmi_d="$1" + + echo "$_fmi_d" | rev | cut -d '.' -f 1-3 | rev +} + +# There is random failure while calling freemyip API too fast. This function automatically retry until success. +_freemyip_get_until_ok() { + _fmi_url="$1" + for i in $(seq 1 8); do + _debug "HTTP GET freemyip.com API '$_fmi_url', retry $i/8..." + _get "$_fmi_url" | tee /dev/fd/2 | grep OK && return 0 + _sleep 1 # DO NOT send the request too fast + done + _err "Failed to request freemyip API: $_fmi_url . Server does not say 'OK'" + return 1 +} + +# Verify in public dns if domain is already there. +_is_root_domain_published() { + _fmi_d="$1" + _webroot="$(_get_root "$_fmi_d")" + + _info "Verifying '""$_fmi_d""' freemyip webroot (""$_webroot"") is not published yet" + for i in $(seq 1 3); do + _debug "'$_webroot' ns lookup, retry $i/3..." + if [ "$(_ns_lookup "$_fmi_d" TXT)" ]; then + _debug "'$_webroot' already has a TXT record published!" + return 0 + fi + _sleep 10 # Give it some time to propagate the TXT record + done + return 1 +} diff --git a/dnsapi/dns_he_ddns.sh b/dnsapi/dns_he_ddns.sh new file mode 100644 index 00000000..cd7d1ec2 --- /dev/null +++ b/dnsapi/dns_he_ddns.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_he_ddns_info='Hurricane Electric HE.net DDNS +Site: dns.he.net +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_he_ddns +Options: + HE_DDNS_KEY The DDNS key +Author: Markku Leiniö +' + +HE_DDNS_URL="https://dyn.dns.he.net/nic/update" + +######## Public functions ##################### + +#Usage: dns_he_ddns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_he_ddns_add() { + fulldomain=$1 + txtvalue=$2 + HE_DDNS_KEY="${HE_DDNS_KEY:-$(_readaccountconf_mutable HE_DDNS_KEY)}" + if [ -z "$HE_DDNS_KEY" ]; then + HE_DDNS_KEY="" + _err "You didn't specify a DDNS key for accessing the TXT record in HE API." + return 1 + fi + #Save the DDNS key to the account conf file. + _saveaccountconf_mutable HE_DDNS_KEY "$HE_DDNS_KEY" + + _info "Using Hurricane Electric DDNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + response="$(_post "hostname=$fulldomain&password=$HE_DDNS_KEY&txt=$txtvalue" "$HE_DDNS_URL")" + _info "Response: $response" + _contains "$response" "good" && return 0 || return 1 +} + +# dns_he_ddns_rm() is not doing anything because the API call always updates the +# contents of the existing record (that the API key gives access to). + +dns_he_ddns_rm() { + fulldomain=$1 + _debug "Delete TXT record called for '${fulldomain}', not doing anything." + return 0 +} diff --git a/dnsapi/dns_hetzner.sh b/dnsapi/dns_hetzner.sh old mode 100644 new mode 100755 index 5a9cf2d9..f1bddc61 --- a/dnsapi/dns_hetzner.sh +++ b/dnsapi/dns_hetzner.sh @@ -212,7 +212,7 @@ _get_root() { _response_has_error() { unset _response_error - err_part="$(echo "$response" | _egrep_o '"error":{[^}]*}')" + err_part="$(echo "$response" | _egrep_o '"error":\{[^\}]*\}')" if [ -n "$err_part" ]; then err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2) diff --git a/dnsapi/dns_ionos_cloud.sh b/dnsapi/dns_ionos_cloud.sh index fa229e03..f255092f 100644 --- a/dnsapi/dns_ionos_cloud.sh +++ b/dnsapi/dns_ionos_cloud.sh @@ -1,12 +1,14 @@ #!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_ionos_cloud_info='IONOS Cloud DNS +Site: ionos.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_ionos_cloud +Options: + IONOS_TOKEN API Token. +Issues: github.com/acmesh-official/acme.sh/issues/5243 +' # Supports IONOS Cloud DNS API v1.15.4 -# -# Usage: -# Export IONOS_TOKEN before calling acme.sh: -# $ export IONOS_TOKEN="..." -# -# $ acme.sh --issue --dns dns_ionos_cloud ... IONOS_CLOUD_API="https://dns.de-fra.ionos.com" IONOS_CLOUD_ROUTE_ZONES="/zones" diff --git a/dnsapi/dns_limacity.sh b/dnsapi/dns_limacity.sh index fb12f8c6..5734be9e 100644 --- a/dnsapi/dns_limacity.sh +++ b/dnsapi/dns_limacity.sh @@ -1,13 +1,13 @@ #!/usr/bin/env sh - -# Created by Laraveluser -# -# Pass credentials before "acme.sh --issue --dns dns_limacity ..." -# -- -# export LIMACITY_APIKEY="" -# -- -# -# Pleas note: APIKEY must have following roles: dns.admin, domains.reader +# shellcheck disable=SC2034 +dns_limacity_info='lima-city.de +Site: www.lima-city.de +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_limacity +Options: + LIMACITY_APIKEY API Key. Note: The API Key must have following roles: dns.admin, domains.reader +Issues: github.com/acmesh-official/acme.sh/issues/4758 +Author: @Laraveluser +' ######## Public functions ##################### diff --git a/dnsapi/dns_linode_v4.sh b/dnsapi/dns_linode_v4.sh index f6a61a99..3c6997a0 100755 --- a/dnsapi/dns_linode_v4.sh +++ b/dnsapi/dns_linode_v4.sh @@ -76,7 +76,7 @@ dns_linode_v4_rm() { _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" - if _rest GET "/$_domain_id/records" && [ -n "$response" ]; then + if _H4="X-Filter: { \"type\": \"TXT\", \"name\": \"$_sub_domain\" }" _rest GET "/$_domain_id/records" && [ -n "$response" ]; then response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")" resource="$(echo "$response" | _egrep_o "\{.*\"name\": *\"$_sub_domain\".*}")" @@ -131,34 +131,42 @@ _Linode_API() { # _domain=domain.com # _domain_id=12345 _get_root() { - domain=$1 + full_host_str="$1" + i=2 p=1 + while true; do + # loop through the received string (e.g. _acme-challenge.sub3.sub2.sub1.domain.tld), + # starting from the lowest subdomain, and check if it's a hosted domain + tst_hosted_domain=$(printf "%s" "$full_host_str" | cut -d . -f "$i"-100) + _debug tst_hosted_domain "$tst_hosted_domain" + if [ -z "$tst_hosted_domain" ]; then + #not valid + _err "Couldn't get domain from string '$full_host_str'." + return 1 + fi - if _rest GET; then - response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")" - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug h "$h" - if [ -z "$h" ]; then - #not valid - return 1 - fi - - hostedzone="$(echo "$response" | _egrep_o "\{.*\"domain\": *\"$h\".*}")" + _debug "Querying Linode APIv4 for hosted zone: $tst_hosted_domain" + if _H4="X-Filter: {\"domain\":\"$tst_hosted_domain\"}" _rest GET; then + _debug "Got response from API: $response" + response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")" + hostedzone="$(echo "$response" | _egrep_o "\{.*\"domain\": *\"$tst_hosted_domain\".*}")" if [ "$hostedzone" ]; then _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"id\": *[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) + _debug "Found domain hosted on Linode DNS. Zone: $tst_hosted_domain, id: $_domain_id" if [ "$_domain_id" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h + _sub_domain=$(printf "%s" "$full_host_str" | cut -d . -f 1-"$p") + _domain=$tst_hosted_domain return 0 fi return 1 fi + p=$i i=$(_math "$i" + 1) - done - fi + fi + done + return 1 } diff --git a/dnsapi/dns_miab.sh b/dnsapi/dns_miab.sh index c2aa9789..0824a4e7 100644 --- a/dnsapi/dns_miab.sh +++ b/dnsapi/dns_miab.sh @@ -16,8 +16,7 @@ Author: Darven Dissek, William Gertz #Usage: dns_miab_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_miab_add() { fulldomain=$1 - # Added "value=" and "&ttl=300" to accomodate the new TXT record format used by the MIAB/PMIAB API - txtvalue="value=$2&ttl=300" + txtvalue=$2 _info "Using miab challenge add" _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" diff --git a/dnsapi/dns_mijnhost.sh b/dnsapi/dns_mijnhost.sh new file mode 100644 index 00000000..9dafc702 --- /dev/null +++ b/dnsapi/dns_mijnhost.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_mijnhost_info='mijn.host +Domains: mijn.host +Site: mijn.host +Docs: https://mijn.host/api/doc/ +Issues: https://github.com/acmesh-official/acme.sh/issues/6177 +Author: peterv99 +Options: + MIJNHOST_API_KEY API Key +' + +######## Public functions ###################### Constants for your mijn-host API +MIJNHOST_API="https://mijn.host/api/v2" + +# Add TXT record for domain verification +dns_mijnhost_add() { + fulldomain=$1 + txtvalue=$2 + + MIJNHOST_API_KEY="${MIJNHOST_API_KEY:-$(_readaccountconf_mutable MIJNHOST_API_KEY)}" + if [ -z "$MIJNHOST_API_KEY" ]; then + MIJNHOST_API_KEY="" + _err "You haven't specified your mijn-host API key yet." + _err "Please add MIJNHOST_API_KEY to the env." + return 1 + fi + + # Save the API key for future use + _saveaccountconf_mutable MIJNHOST_API_KEY "$MIJNHOST_API_KEY" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + _debug2 _sub_domain "$_sub_domain" + _debug2 _domain "$_domain" + _debug "Adding DNS record" "${fulldomain}." + + # Construct the API URL + api_url="$MIJNHOST_API/domains/$_domain/dns" + + # Getting previous records + _mijnhost_rest GET "$api_url" "" + + if [ "$_code" != "200" ]; then + _err "Error getting current DNS enties ($_code)" + return 1 + fi + + records=$(echo "$response" | _egrep_o '"records":\[.*\]' | sed 's/"records"://') + + _debug2 "Current records" "$records" + + # Build the payload for the API + data="{\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"value\":\"$txtvalue\",\"ttl\":300}" + + _debug2 "Record to add" "$data" + + # Updating the records + updated_records=$(echo "$records" | sed -E "s/\]( *$)/,$data\]/") + + _debug2 "Updated records" "$updated_records" + + # data + data="{\"records\": $updated_records}" + + _mijnhost_rest PUT "$api_url" "$data" + + if [ "$_code" = "200" ]; then + _info "DNS record succesfully added." + return 0 + else + _err "Error adding DNS record ($_code)." + return 1 + fi +} + +# Remove TXT record after verification +dns_mijnhost_rm() { + fulldomain=$1 + txtvalue=$2 + + MIJNHOST_API_KEY="${MIJNHOST_API_KEY:-$(_readaccountconf_mutable MIJNHOST_API_KEY)}" + if [ -z "$MIJNHOST_API_KEY" ]; then + MIJNHOST_API_KEY="" + _err "You haven't specified your mijn-host API key yet." + _err "Please add MIJNHOST_API_KEY to the env." + return 1 + fi + + _debug "Detecting root zone for" "${fulldomain}." + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + _debug "Removing DNS record for TXT value" "${txtvalue}." + + # Construct the API URL + api_url="$MIJNHOST_API/domains/$_domain/dns" + + # Get current records + _mijnhost_rest GET "$api_url" "" + + if [ "$_code" != "200" ]; then + _err "Error getting current DNS enties ($_code)" + return 1 + fi + + _debug2 "Get current records response:" "$response" + + records=$(echo "$response" | _egrep_o '"records":\[.*\]' | sed 's/"records"://') + + _debug2 "Current records:" "$records" + + updated_records=$(echo "$records" | sed -E "s/\{[^}]*\"value\":\"$txtvalue\"[^}]*\},?//g" | sed 's/,]/]/g') + + _debug2 "Updated records:" "$updated_records" + + # Build the new payload + data="{\"records\": $updated_records}" + + # Use the _put method to update the records + _mijnhost_rest PUT "$api_url" "$data" + + if [ "$_code" = "200" ]; then + _info "DNS record removed successfully." + return 0 + else + _err "Error removing DNS record ($_code)." + return 1 + fi +} + +# Helper function to detect the root zone +_get_root() { + domain=$1 + + # Get current records + _debug "Getting current domains" + _mijnhost_rest GET "$MIJNHOST_API/domains" "" + + if [ "$_code" != "200" ]; then + _err "error getting current domains ($_code)" + return 1 + fi + + # Extract root domains from response + rootDomains=$(echo "$response" | _egrep_o '"domain":"[^"]*"' | sed -E 's/"domain":"([^"]*)"/\1/') + _debug "Root domains:" "$rootDomains" + + for rootDomain in $rootDomains; do + if _contains "$domain" "$rootDomain"; then + _domain="$rootDomain" + _sub_domain=$(echo "$domain" | sed "s/.$rootDomain//g") + _debug "Found root domain" "$_domain" "and subdomain" "$_sub_domain" "for" "$domain" + return 0 + fi + done + return 1 +} + +# Helper function for rest calls +_mijnhost_rest() { + m=$1 + ep="$2" + data="$3" + + MAX_REQUEST_RETRY_TIMES=15 + _request_retry_times=0 + _retry_sleep=5 #Initial sleep time in seconds. + + while [ "${_request_retry_times}" -lt "$MAX_REQUEST_RETRY_TIMES" ]; do + _debug2 _request_retry_times "$_request_retry_times" + export _H1="API-Key: $MIJNHOST_API_KEY" + export _H2="Content-Type: application/json" + # clear headers from previous request to avoid getting wrong http code on timeouts + : >"$HTTP_HEADER" + _debug "$ep" + if [ "$m" != "GET" ]; then + _debug2 "data $data" + response="$(_post "$data" "$ep" "" "$m")" + else + response="$(_get "$ep")" + fi + _ret="$?" + _debug2 "response $response" + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + _debug "http response code $_code" + if [ "$_code" = "401" ]; then + # we have an invalid API token, maybe it is expired? + _err "Access denied. Invalid API token." + return 1 + fi + + if [ "$_ret" != "0" ] || [ -z "$_code" ] || [ "$_code" = "400" ] || _contains "$response" "DNS records not managed by mijn.host"; then #Sometimes API errors out + _request_retry_times="$(_math "$_request_retry_times" + 1)" + _info "REST call error $_code retrying $ep in ${_retry_sleep}s" + _sleep "$_retry_sleep" + _retry_sleep="$(_math "$_retry_sleep" \* 2)" + continue + fi + break + done + if [ "$_request_retry_times" = "$MAX_REQUEST_RETRY_TIMES" ]; then + _err "Error mijn.host API call was retried $MAX_REQUEST_RETRY_TIMES times." + _err "Calling $ep failed." + return 1 + fi + response="$(echo "$response" | _normalizeJson)" + return 0 +} diff --git a/dnsapi/dns_myapi.sh b/dnsapi/dns_myapi.sh index c9f5eb9f..101854d5 100755 --- a/dnsapi/dns_myapi.sh +++ b/dnsapi/dns_myapi.sh @@ -1,12 +1,14 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 dns_myapi_info='Custom API Example - A sample custom DNS API script. -Domains: example.com + A sample custom DNS API script description. +Domains: example.com example.net Site: github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_duckdns +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_myapi Options: - MYAPI_Token API Token. Get API Token from https://example.com/api/. Optional. + MYAPI_Token API Token. Get API Token from https://example.com/api/ + MYAPI_Variable2 Option 2. Default "default value". + MYAPI_Variable2 Option 3. Optional. Issues: github.com/acmesh-official/acme.sh Author: Neil Pang ' diff --git a/dnsapi/dns_netcup.sh b/dnsapi/dns_netcup.sh index 687b99bc..8609adf6 100644 --- a/dnsapi/dns_netcup.sh +++ b/dnsapi/dns_netcup.sh @@ -19,7 +19,7 @@ client="" dns_netcup_add() { _debug NC_Apikey "$NC_Apikey" - login + _login if [ "$NC_Apikey" = "" ] || [ "$NC_Apipw" = "" ] || [ "$NC_CID" = "" ]; then _err "No Credentials given" return 1 @@ -61,7 +61,7 @@ dns_netcup_add() { } dns_netcup_rm() { - login + _login fulldomain=$1 txtvalue=$2 @@ -125,7 +125,7 @@ dns_netcup_rm() { logout } -login() { +_login() { tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST") sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4) _debug "$tmp" diff --git a/dnsapi/dns_omglol.sh b/dnsapi/dns_omglol.sh index b6c06aee..df080bcf 100644 --- a/dnsapi/dns_omglol.sh +++ b/dnsapi/dns_omglol.sh @@ -1,23 +1,19 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 -dns_myapi_info='omg.lol - Based on the omg.lol API, defined at https://api.omg.lol/ -Domains: omg.lol -Site: github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_duckdns +dns_omglol_info='omg.lol +Site: omg.lol +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_omglol Options: - OMG_ApiKey API Key from omg.lol. This is accesible from the bottom of the account page at https://home.omg.lol/account - OMG_Address This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard -Issues: github.com/acmesh-official/acme.sh + OMG_ApiKey API Key. This is accessible from the bottom of the account page at https://home.omg.lol/account + OMG_Address Address. This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard +Issues: github.com/acmesh-official/acme.sh/issues/5299 Author: @Kholin ' -#returns 0 means success, otherwise error. +# See API Docs https://api.omg.lol/ ######## Public functions ##################### -# Please Read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide - #Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_omglol_add() { fulldomain=$1 @@ -244,8 +240,8 @@ omg_delete() { omg_validate_delete "$output" } -# Validate the response on request to delete. Confirm stastus is success and -# Message indicates deletion was successful +# Validate the response on request to delete. +# Confirm status is success and message indicates deletion was successful. # Input: Response - HTTP response received from delete request omg_validate_delete() { response=$1 diff --git a/dnsapi/dns_openprovider.sh b/dnsapi/dns_openprovider.sh index b584fad2..2dec9934 100755 --- a/dnsapi/dns_openprovider.sh +++ b/dnsapi/dns_openprovider.sh @@ -2,6 +2,7 @@ # shellcheck disable=SC2034 dns_openprovider_info='OpenProvider.eu Site: OpenProvider.eu +Domains: OpenProvider.com Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_openprovider Options: OPENPROVIDER_USER Username diff --git a/dnsapi/dns_pdns.sh b/dnsapi/dns_pdns.sh index 6cce54da..ec19ad25 100755 --- a/dnsapi/dns_pdns.sh +++ b/dnsapi/dns_pdns.sh @@ -7,7 +7,7 @@ Options: PDNS_Url API URL. E.g. "http://ns.example.com:8081" PDNS_ServerId Server ID. E.g. "localhost" PDNS_Token API Token - PDNS_Ttl=60 Domain TTL. Default: "60". + PDNS_Ttl Domain TTL. Default: "60". ' DEFAULT_PDNS_TTL=60 @@ -20,6 +20,11 @@ dns_pdns_add() { fulldomain=$1 txtvalue=$2 + PDNS_Url="${PDNS_Url:-$(_readaccountconf_mutable PDNS_Url)}" + PDNS_ServerId="${PDNS_ServerId:-$(_readaccountconf_mutable PDNS_ServerId)}" + PDNS_Token="${PDNS_Token:-$(_readaccountconf_mutable PDNS_Token)}" + PDNS_Ttl="${PDNS_Ttl:-$(_readaccountconf_mutable PDNS_Ttl)}" + if [ -z "$PDNS_Url" ]; then PDNS_Url="" _err "You don't specify PowerDNS address." @@ -46,12 +51,12 @@ dns_pdns_add() { fi #save the api addr and key to the account conf file. - _saveaccountconf PDNS_Url "$PDNS_Url" - _saveaccountconf PDNS_ServerId "$PDNS_ServerId" - _saveaccountconf PDNS_Token "$PDNS_Token" + _saveaccountconf_mutable PDNS_Url "$PDNS_Url" + _saveaccountconf_mutable PDNS_ServerId "$PDNS_ServerId" + _saveaccountconf_mutable PDNS_Token "$PDNS_Token" if [ "$PDNS_Ttl" != "$DEFAULT_PDNS_TTL" ]; then - _saveaccountconf PDNS_Ttl "$PDNS_Ttl" + _saveaccountconf_mutable PDNS_Ttl "$PDNS_Ttl" fi _debug "Detect root zone" @@ -73,6 +78,11 @@ dns_pdns_rm() { fulldomain=$1 txtvalue=$2 + PDNS_Url="${PDNS_Url:-$(_readaccountconf_mutable PDNS_Url)}" + PDNS_ServerId="${PDNS_ServerId:-$(_readaccountconf_mutable PDNS_ServerId)}" + PDNS_Token="${PDNS_Token:-$(_readaccountconf_mutable PDNS_Token)}" + PDNS_Ttl="${PDNS_Ttl:-$(_readaccountconf_mutable PDNS_Ttl)}" + if [ -z "$PDNS_Ttl" ]; then PDNS_Ttl="$DEFAULT_PDNS_TTL" fi diff --git a/dnsapi/dns_selectel.sh b/dnsapi/dns_selectel.sh index 8b52b24e..434bc483 100644 --- a/dnsapi/dns_selectel.sh +++ b/dnsapi/dns_selectel.sh @@ -1,14 +1,31 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 -dns_selectel_info='Selectel.com -Domains: Selectel.ru -Site: Selectel.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_selectel -Options: - SL_Key API Key -' -SL_Api="https://api.selectel.ru/domains/v1" +# dns_selectel_info='Selectel.com +# Domains: Selectel.ru +# Site: Selectel.com +# Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_selectel +# Options: +# Variables that must be defined before running +# SL_Ver can take one of the values 'v1' or 'v2', default is 'v1' +# SL_Ver='v1', when using version API legacy (v1) +# SL_Ver='v2', when using version API actual (v2) +# when using API version v1, i.e. SL_Ver is 'v1' or not defined: +# SL_Key - API Key, required +# when using API version v2: +# SL_Ver - required as 'v2' +# SL_Login_ID - account ID, required +# SL_Project_Name - name project, required +# SL_Login_Name - service user name, required +# SL_Pswd - service user password, required +# SL_Expire - token lifetime in minutes (0-1440), default 1400 minutes +# +# Issues: github.com/acmesh-official/acme.sh/issues/5126 +# + +SL_Api="https://api.selectel.ru/domains" +auth_uri="https://cloud.api.selcloud.ru/identity/v3/auth/tokens" +_sl_sep='#' ######## Public functions ##################### @@ -17,17 +34,14 @@ dns_selectel_add() { fulldomain=$1 txtvalue=$2 - SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}" - - if [ -z "$SL_Key" ]; then - SL_Key="" - _err "You don't specify selectel.ru api key yet." - _err "Please create you key and try again." + if ! _sl_init_vars; then return 1 fi - - #save the api key to the account conf file. - _saveaccountconf_mutable SL_Key "$SL_Key" + _debug2 SL_Ver "$SL_Ver" + _debug2 SL_Expire "$SL_Expire" + _debug2 SL_Login_Name "$SL_Login_Name" + _debug2 SL_Login_ID "$SL_Login_ID" + _debug2 SL_Project_Name "$SL_Project_Name" _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -39,11 +53,63 @@ dns_selectel_add() { _debug _domain "$_domain" _info "Adding record" - if _sl_rest POST "/$_domain_id/records/" "{\"type\": \"TXT\", \"ttl\": 60, \"name\": \"$fulldomain\", \"content\": \"$txtvalue\"}"; then - if _contains "$response" "$txtvalue" || _contains "$response" "record_already_exists"; then + if [ "$SL_Ver" = "v2" ]; then + _ext_srv1="/zones/" + _ext_srv2="/rrset/" + _text_tmp=$(echo "$txtvalue" | sed -En "s/[\"]*([^\"]*)/\1/p") + _text_tmp='\"'$_text_tmp'\"' + _data="{\"type\": \"TXT\", \"ttl\": 60, \"name\": \"${fulldomain}.\", \"records\": [{\"content\":\"$_text_tmp\"}]}" + elif [ "$SL_Ver" = "v1" ]; then + _ext_srv1="/" + _ext_srv2="/records/" + _data="{\"type\":\"TXT\",\"ttl\":60,\"name\":\"$fulldomain\",\"content\":\"$txtvalue\"}" + else + _err "Error. Unsupported version API $SL_Ver" + return 1 + fi + _ext_uri="${_ext_srv1}$_domain_id${_ext_srv2}" + _debug _ext_uri "$_ext_uri" + _debug _data "$_data" + + if _sl_rest POST "$_ext_uri" "$_data"; then + if _contains "$response" "$txtvalue"; then _info "Added, OK" return 0 fi + if _contains "$response" "already_exists"; then + # record TXT with $fulldomain already exists + if [ "$SL_Ver" = "v2" ]; then + # It is necessary to add one more content to the comments + # read all records rrset + _debug "Getting txt records" + _sl_rest GET "${_ext_uri}" + # There is already a $txtvalue value, no need to add it + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + _info "Txt record ${fulldomain} with value ${txtvalue} already exists" + return 0 + fi + # group \1 - full record rrset; group \2 - records attribute value, exactly {"content":"\"value1\""},{"content":"\"value2\""}",... + _record_seg="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*${fulldomain}[^}]*records[^}]*\[(\{[^]]*\})\][^}]*}).*/\1/p")" + _record_array="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*${fulldomain}[^}]*records[^}]*\[(\{[^]]*\})\][^}]*}).*/\2/p")" + # record id + _record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2 | tr -d "\"")" + # preparing _data + _tmp_str="${_record_array},{\"content\":\"${_text_tmp}\"}" + _data="{\"ttl\": 60, \"records\": [${_tmp_str}]}" + _debug2 _record_seg "$_record_seg" + _debug2 _record_array "$_record_array" + _debug2 _record_array "$_record_id" + _debug "New data for record" "$_data" + if _sl_rest PATCH "${_ext_uri}${_record_id}" "$_data"; then + _info "Added, OK" + return 0 + fi + elif [ "$SL_Ver" = "v1" ]; then + _info "Added, OK" + return 0 + fi + fi fi _err "Add txt record error." return 1 @@ -54,15 +120,15 @@ dns_selectel_rm() { fulldomain=$1 txtvalue=$2 - SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}" - - if [ -z "$SL_Key" ]; then - SL_Key="" - _err "You don't specify slectel api key yet." - _err "Please create you key and try again." + if ! _sl_init_vars "nosave"; then return 1 fi - + _debug2 SL_Ver "$SL_Ver" + _debug2 SL_Expire "$SL_Expire" + _debug2 SL_Login_Name "$SL_Login_Name" + _debug2 SL_Login_ID "$SL_Login_ID" + _debug2 SL_Project_Name "$SL_Project_Name" + # _debug "First detect the root zone" if ! _get_root "$fulldomain"; then _err "invalid domain" @@ -71,91 +137,195 @@ dns_selectel_rm() { _debug _domain_id "$_domain_id" _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" - + # + if [ "$SL_Ver" = "v2" ]; then + _ext_srv1="/zones/" + _ext_srv2="/rrset/" + elif [ "$SL_Ver" = "v1" ]; then + _ext_srv1="/" + _ext_srv2="/records/" + else + _err "Error. Unsupported version API $SL_Ver" + return 1 + fi + # _debug "Getting txt records" - _sl_rest GET "/${_domain_id}/records/" - + _ext_uri="${_ext_srv1}$_domain_id${_ext_srv2}" + _debug _ext_uri "$_ext_uri" + _sl_rest GET "${_ext_uri}" + # if ! _contains "$response" "$txtvalue"; then _err "Txt record not found" return 1 fi - - _record_seg="$(echo "$response" | _egrep_o "[^{]*\"content\" *: *\"$txtvalue\"[^}]*}")" + # + if [ "$SL_Ver" = "v2" ]; then + _record_seg="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*records[^[]*(\[(\{[^]]*${txtvalue}[^]]*)\])[^}]*}).*/\1/gp")" + _record_arr="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*records[^[]*(\[(\{[^]]*${txtvalue}[^]]*)\])[^}]*}).*/\3/p")" + elif [ "$SL_Ver" = "v1" ]; then + _record_seg="$(echo "$response" | _egrep_o "[^{]*\"content\" *: *\"$txtvalue\"[^}]*}")" + else + _err "Error. Unsupported version API $SL_Ver" + return 1 + fi _debug2 "_record_seg" "$_record_seg" if [ -z "$_record_seg" ]; then _err "can not find _record_seg" return 1 fi - - _record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2)" - _debug2 "_record_id" "$_record_id" + # record id + # the following lines change the algorithm for deleting records with the value $txtvalue + # if you use the 1st line, then all such records are deleted at once + # if you use the 2nd line, then only the first entry from them is deleted + #_record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2 | tr -d "\"")" + _record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2 | tr -d "\"" | sed '1!d')" if [ -z "$_record_id" ]; then _err "can not find _record_id" return 1 fi - - if ! _sl_rest DELETE "/$_domain_id/records/$_record_id"; then - _err "Delete record error." - return 1 + _debug2 "_record_id" "$_record_id" + # delete all record type TXT with text $txtvalue + if [ "$SL_Ver" = "v2" ]; then + # actual + _new_arr="$(echo "$_record_seg" | sed -En "s/.*(\{\"id\"[^}]*records[^[]*(\[(\{[^]]*${txtvalue}[^]]*)\])[^}]*}).*/\3/gp" | sed -En "s/(\},\{)/}\n{/gp" | sed "/${txtvalue}/d" | sed ":a;N;s/\n/,/;ta")" + # uri record for DEL or PATCH + _del_uri="${_ext_uri}${_record_id}" + _debug _del_uri "$_del_uri" + if [ -z "$_new_arr" ]; then + # remove record + if ! _sl_rest DELETE "${_del_uri}"; then + _err "Delete record error: ${_del_uri}." + else + info "Delete record success: ${_del_uri}." + fi + else + # update a record by removing one element in content + _data="{\"ttl\": 60, \"records\": [${_new_arr}]}" + _debug2 _data "$_data" + # REST API PATCH call + if _sl_rest PATCH "${_del_uri}" "$_data"; then + _info "Patched, OK: ${_del_uri}" + else + _err "Patched record error: ${_del_uri}." + fi + fi + else + # legacy + for _one_id in $_record_id; do + _del_uri="${_ext_uri}${_one_id}" + _debug _del_uri "$_del_uri" + if ! _sl_rest DELETE "${_del_uri}"; then + _err "Delete record error: ${_del_uri}." + else + info "Delete record success: ${_del_uri}." + fi + done fi 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 - if ! _sl_rest GET "/"; then - return 1 - fi - - i=2 - p=1 - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug h "$h" - if [ -z "$h" ]; then - #not valid + if [ "$SL_Ver" = 'v1' ]; then + # version API 1 + if ! _sl_rest GET "/"; then return 1 fi - - if _contains "$response" "\"name\" *: *\"$h\","; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h - _debug "Getting domain id for $h" - if ! _sl_rest GET "/$h"; then + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug h "$h" + if [ -z "$h" ]; then return 1 fi - _domain_id="$(echo "$response" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\":" | cut -d : -f 2)" - return 0 + if _contains "$response" "\"name\" *: *\"$h\","; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + _debug "Getting domain id for $h" + if ! _sl_rest GET "/$h"; then + _err "Error read records of all domains $SL_Ver" + return 1 + fi + _domain_id="$(echo "$response" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\":" | cut -d : -f 2)" + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + _err "Error read records of all domains $SL_Ver" + return 1 + elif [ "$SL_Ver" = "v2" ]; then + # version API 2 + _ext_uri='/zones/' + domain="${domain}." + _debug "domain:: " "$domain" + # read records of all domains + if ! _sl_rest GET "$_ext_uri"; then + _err "Error read records of all domains $SL_Ver" + return 1 fi - p=$i - i=$(_math "$i" + 1) - done - return 1 + i=1 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug h "$h" + if [ -z "$h" ]; then + _err "The domain was not found among the registered ones" + return 1 + fi + _domain_record=$(echo "$response" | sed -En "s/.*(\{[^}]*id[^}]*\"name\" *: *\"$h\"[^}]*}).*/\1/p") + _debug "_domain_record:: " "$_domain_record" + if [ -n "$_domain_record" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + _debug "Getting domain id for $h" + _domain_id=$(echo "$_domain_record" | sed -En "s/\{[^}]*\"id\" *: *\"([^\"]*)\"[^}]*\}/\1/p") + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + _err "Error read records of all domains $SL_Ver" + return 1 + else + _err "Error. Unsupported version API $SL_Ver" + return 1 + fi } +################################################################# +# use: method add_url body _sl_rest() { m=$1 ep="$2" data="$3" - _debug "$ep" - export _H1="X-Token: $SL_Key" + _token=$(_get_auth_token) + if [ -z "$_token" ]; then + _err "BAD key or token $ep" + return 1 + fi + if [ "$SL_Ver" = v2 ]; then + _h1_name="X-Auth-Token" + else + _h1_name='X-Token' + fi + export _H1="${_h1_name}: ${_token}" export _H2="Content-Type: application/json" - + _debug2 "Full URI: " "$SL_Api/${SL_Ver}${ep}" + _debug2 "_H1:" "$_H1" + _debug2 "_H2:" "$_H2" if [ "$m" != "GET" ]; then _debug data "$data" - response="$(_post "$data" "$SL_Api/$ep" "" "$m")" + response="$(_post "$data" "$SL_Api/${SL_Ver}${ep}" "" "$m")" else - response="$(_get "$SL_Api/$ep")" + response="$(_get "$SL_Api/${SL_Ver}${ep}")" fi - + # shellcheck disable=SC2181 if [ "$?" != "0" ]; then _err "error $ep" return 1 @@ -163,3 +333,152 @@ _sl_rest() { _debug2 response "$response" return 0 } + +_get_auth_token() { + if [ "$SL_Ver" = 'v1' ]; then + # token for v1 + _debug "Token v1" + _token_keystone=$SL_Key + elif [ "$SL_Ver" = 'v2' ]; then + # token for v2. Get a token for calling the API + _debug "Keystone Token v2" + token_v2=$(_readaccountconf_mutable SL_Token_V2) + if [ -n "$token_v2" ]; then + # The structure with the token was considered. Let's check its validity + # field 1 - SL_Login_Name + # field 2 - token keystone + # field 3 - SL_Login_ID + # field 4 - SL_Project_Name + # field 5 - Receipt time + # separator - '$_sl_sep' + _login_name=$(_getfield "$token_v2" 1 "$_sl_sep") + _token_keystone=$(_getfield "$token_v2" 2 "$_sl_sep") + _project_name=$(_getfield "$token_v2" 4 "$_sl_sep") + _receipt_time=$(_getfield "$token_v2" 5 "$_sl_sep") + _login_id=$(_getfield "$token_v2" 3 "$_sl_sep") + _debug2 _login_name "$_login_name" + _debug2 _login_id "$_login_id" + _debug2 _project_name "$_project_name" + # check the validity of the token for the user and the project and its lifetime + _dt_diff_minute=$((($(date +%s) - _receipt_time) / 60)) + _debug2 _dt_diff_minute "$_dt_diff_minute" + [ "$_dt_diff_minute" -gt "$SL_Expire" ] && unset _token_keystone + if [ "$_project_name" != "$SL_Project_Name" ] || [ "$_login_name" != "$SL_Login_Name" ] || [ "$_login_id" != "$SL_Login_ID" ]; then + unset _token_keystone + fi + _debug "Get exists token" + fi + if [ -z "$_token_keystone" ]; then + # the previous token is incorrect or was not received, get a new one + _debug "Update (get new) token" + _data_auth="{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":{\"user\":{\"name\":\"${SL_Login_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"},\"password\":\"${SL_Pswd}\"}}},\"scope\":{\"project\":{\"name\":\"${SL_Project_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"}}}}}" + export _H1="Content-Type: application/json" + _result=$(_post "$_data_auth" "$auth_uri") + _token_keystone=$(grep 'x-subject-token' "$HTTP_HEADER" | sed -nE "s/[[:space:]]*x-subject-token:[[:space:]]*([[:print:]]*)(\r*)/\1/p") + _dt_curr=$(date +%s) + SL_Token_V2="${SL_Login_Name}${_sl_sep}${_token_keystone}${_sl_sep}${SL_Login_ID}${_sl_sep}${SL_Project_Name}${_sl_sep}${_dt_curr}" + _saveaccountconf_mutable SL_Token_V2 "$SL_Token_V2" + fi + else + # token set empty for unsupported version API + _token_keystone="" + fi + printf -- "%s" "$_token_keystone" +} + +################################################################# +# use: [non_save] +_sl_init_vars() { + _non_save="${1}" + _debug2 _non_save "$_non_save" + + _debug "First init variables" + # version API + SL_Ver="${SL_Ver:-$(_readaccountconf_mutable SL_Ver)}" + if [ -z "$SL_Ver" ]; then + SL_Ver="v1" + fi + if ! [ "$SL_Ver" = "v1" ] && ! [ "$SL_Ver" = "v2" ]; then + _err "You don't specify selectel.ru API version." + _err "Please define specify API version." + fi + _debug2 SL_Ver "$SL_Ver" + if [ "$SL_Ver" = "v1" ]; then + # token + SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}" + + if [ -z "$SL_Key" ]; then + SL_Key="" + _err "You don't specify selectel.ru api key yet." + _err "Please create you key and try again." + return 1 + fi + #save the api key to the account conf file. + if [ -z "$_non_save" ]; then + _saveaccountconf_mutable SL_Key "$SL_Key" + fi + elif [ "$SL_Ver" = "v2" ]; then + # time expire token + SL_Expire="${SL_Expire:-$(_readaccountconf_mutable SL_Expire)}" + if [ -z "$SL_Expire" ]; then + SL_Expire=1400 # 23h 20 min + fi + if [ -z "$_non_save" ]; then + _saveaccountconf_mutable SL_Expire "$SL_Expire" + fi + # login service user + SL_Login_Name="${SL_Login_Name:-$(_readaccountconf_mutable SL_Login_Name)}" + if [ -z "$SL_Login_Name" ]; then + SL_Login_Name='' + _err "You did not specify the selectel.ru API service user name." + _err "Please provide a service user name and try again." + return 1 + fi + if [ -z "$_non_save" ]; then + _saveaccountconf_mutable SL_Login_Name "$SL_Login_Name" + fi + # user ID + SL_Login_ID="${SL_Login_ID:-$(_readaccountconf_mutable SL_Login_ID)}" + if [ -z "$SL_Login_ID" ]; then + SL_Login_ID='' + _err "You did not specify the selectel.ru API user ID." + _err "Please provide a user ID and try again." + return 1 + fi + if [ -z "$_non_save" ]; then + _saveaccountconf_mutable SL_Login_ID "$SL_Login_ID" + fi + # project name + SL_Project_Name="${SL_Project_Name:-$(_readaccountconf_mutable SL_Project_Name)}" + if [ -z "$SL_Project_Name" ]; then + SL_Project_Name='' + _err "You did not specify the project name." + _err "Please provide a project name and try again." + return 1 + fi + if [ -z "$_non_save" ]; then + _saveaccountconf_mutable SL_Project_Name "$SL_Project_Name" + fi + # service user password + SL_Pswd="${SL_Pswd:-$(_readaccountconf_mutable SL_Pswd)}" + if [ -z "$SL_Pswd" ]; then + SL_Pswd='' + _err "You did not specify the service user password." + _err "Please provide a service user password and try again." + return 1 + fi + if [ -z "$_non_save" ]; then + _saveaccountconf_mutable SL_Pswd "$SL_Pswd" "12345678" + fi + else + SL_Ver="" + _err "You also specified the wrong version of the selectel.ru API." + _err "Please provide the correct API version and try again." + return 1 + fi + if [ -z "$_non_save" ]; then + _saveaccountconf_mutable SL_Ver "$SL_Ver" + fi + + return 0 +} diff --git a/dnsapi/dns_technitium.sh b/dnsapi/dns_technitium.sh new file mode 100755 index 00000000..7bc0dd48 --- /dev/null +++ b/dnsapi/dns_technitium.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_technitium_info='Technitium DNS Server +Site: Technitium.com/dns/ +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_technitium +Options: + Technitium_Server Server Address + Technitium_Token API Token +Issues: github.com/acmesh-official/acme.sh/issues/6116 +Author: Henning Reich +' + +dns_technitium_add() { + _info "add txt Record using Technitium" + _Technitium_account + fulldomain=$1 + txtvalue=$2 + response="$(_get "$Technitium_Server/api/zones/records/add?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")" + if _contains "$response" '"status":"ok"'; then + return 0 + fi + _err "Could not add txt record." + return 1 +} + +dns_technitium_rm() { + _info "remove txt record using Technitium" + _Technitium_account + fulldomain=$1 + txtvalue=$2 + response="$(_get "$Technitium_Server/api/zones/records/delete?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")" + if _contains "$response" '"status":"ok"'; then + return 0 + fi + _err "Could not remove txt record" + return 1 +} + +#################### Private functions below ################################## + +_Technitium_account() { + Technitium_Server="${Technitium_Server:-$(_readaccountconf_mutable Technitium_Server)}" + Technitium_Token="${Technitium_Token:-$(_readaccountconf_mutable Technitium_Token)}" + if [ -z "$Technitium_Server" ] || [ -z "$Technitium_Token" ]; then + Technitium_Server="" + Technitium_Token="" + _err "You don't specify Technitium Server and Token yet." + _err "Please create your Token and add server address and try again." + return 1 + fi + + #save the credentials to the account conf file. + _saveaccountconf_mutable Technitium_Server "$Technitium_Server" + _saveaccountconf_mutable Technitium_Token "$Technitium_Token" +} diff --git a/dnsapi/dns_timeweb.sh b/dnsapi/dns_timeweb.sh index 9860872c..544564ea 100644 --- a/dnsapi/dns_timeweb.sh +++ b/dnsapi/dns_timeweb.sh @@ -1,16 +1,13 @@ #!/usr/bin/env sh - -# acme.sh DNS API for Timeweb Cloud provider (https://timeweb.cloud). -# -# Author: https://github.com/nikolaypronchev. -# -# Prerequisites: -# Timeweb Cloud API JWT token. Obtain one from the Timeweb Cloud control panel -# ("API and Terraform" section: https://timeweb.cloud/my/api-keys). The JWT token -# must be provided to this script in one of two ways: -# 1. As the "TW_Token" variable, for example: "export TW_Token=eyJhbG...zUxMiIs"; -# 2. As a "TW_Token" config entry in acme.sh account config file -# (usually located at ~/.acme.sh/account.conf by default). +# shellcheck disable=SC2034 +dns_timeweb_info='Timeweb.Cloud +Site: Timeweb.Cloud +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_timeweb +Options: + TW_Token API JWT token. Get it from the control panel at https://timeweb.cloud/my/api-keys +Issues: github.com/acmesh-official/acme.sh/issues/5140 +Author: Nikolay Pronchev +' TW_Api="https://api.timeweb.cloud/api/v1" diff --git a/dnsapi/dns_west_cn.sh b/dnsapi/dns_west_cn.sh index d0bb7d49..b873bfc0 100644 --- a/dnsapi/dns_west_cn.sh +++ b/dnsapi/dns_west_cn.sh @@ -1,9 +1,13 @@ #!/usr/bin/env sh - -# West.cn Domain api -#WEST_Username="username" -#WEST_Key="sADDsdasdgdsf" -#Set key at https://www.west.cn/manager/API/APIconfig.asp +# shellcheck disable=SC2034 +dns_west_cn_info='West.cn +Site: West.cn +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_west_cn +Options: + WEST_Username API username + WEST_Key API Key. Set at https://www.west.cn/manager/API/APIconfig.asp +Issues: github.com/acmesh-official/acme.sh/issues/4894 +' REST_API="https://api.west.cn/API/v2" diff --git a/dnsapi/dns_world4you.sh b/dnsapi/dns_world4you.sh index be6ef5c8..46cdc4fe 100644 --- a/dnsapi/dns_world4you.sh +++ b/dnsapi/dns_world4you.sh @@ -115,7 +115,7 @@ dns_world4you_rm() { _resethttp export ACME_HTTP_NO_REDIRECTS=1 - body="DeleteDnsRecordForm[recordId]=$recordid&DeleteDnsRecordForm[uniqueFormIdDP]=$formiddp&DeleteDnsRecordForm[_token]=$form_token" + body="DeleteDnsRecordForm[id]=$recordid&DeleteDnsRecordForm[uniqueFormIdDP]=$formiddp&DeleteDnsRecordForm[_token]=$form_token" _info "Removing record..." ret=$(_post "$body" "$WORLD4YOU_API/$paketnr/dns/record/delete" '' POST 'application/x-www-form-urlencoded') _resethttp @@ -202,7 +202,8 @@ _get_paketnr() { fqdn="$1" form="$2" - domains=$(echo "$form" | grep '