diff --git a/Lib/GetSslConf.php b/Lib/GetSslConf.php index 5ef77e7..e3ba71e 100644 --- a/Lib/GetSslConf.php +++ b/Lib/GetSslConf.php @@ -10,6 +10,7 @@ namespace Modules\ModuleGetSsl\Lib; use MikoPBX\Common\Models\LanInterfaces; +use MikoPBX\Core\System\PBX; use MikoPBX\Core\System\Processes; use MikoPBX\Core\System\Util; use MikoPBX\Modules\Config\ConfigClass; @@ -120,9 +121,13 @@ public function onAfterPbxStarted(): void Util::createUpdateSymlink($busyBoxPath, self::NS_LOOKUP_BIN, true); } - Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null"); - file_put_contents("$confDir/getssl.cfg", $conf); + $extHostname = $this->getHostname(); + + file_put_contents("$confDir/$extHostname/getssl.cfg", $conf); + if(!empty($extHostname)){ + Util::createUpdateSymlink("$confDir/getssl.cfg", "$confDir/$extHostname/getssl.cfg", true); + } } /** @@ -146,6 +151,7 @@ public function onAfterModuleDisable(): void public function onAfterModuleEnable(): void { $this->onAfterPbxStarted(); + PBX::managerReload(); } /** @@ -160,7 +166,7 @@ public function createCronTasks(&$tasks): void } $workerPath = $this->moduleDir.'/db/getssl'; $getSslPath = self::GET_SSL_BIN; - $tasks[] = "0 1 * * * {$getSslPath} -a -u -q -w '$workerPath' > /dev/null 2> /dev/null".PHP_EOL; + $tasks[] = "0 1 * * * {$getSslPath} -a -U -q -w '$workerPath' > /dev/null 2> /dev/null".PHP_EOL; } /** @@ -178,9 +184,8 @@ public function getModuleDir():string */ public function startGetCertSsl():PBXApiResult { - $result = new PBXApiResult(); - $res = LanInterfaces::findFirst("internet = '1'")->toArray(); - $extHostname = $res['exthostname']??''; + $result = new PBXApiResult(); + $extHostname = $this->getHostname(); if(empty($extHostname)){ // Имя хост не заполнено. @@ -197,6 +202,7 @@ public function startGetCertSsl():PBXApiResult $result->data = [ 'pid' => $pid ]; + PBX::managerReload(); $result->success = true; return $result; } @@ -225,4 +231,13 @@ public function checkResult():PBXApiResult return $result; } + /** + * Возвращает имя хост. + * @return string + */ + private function getHostname():string + { + $res = LanInterfaces::findFirst("internet = '1'")->toArray(); + return $res['exthostname']??''; + } } \ No newline at end of file diff --git a/bin/getssl b/bin/getssl index fc61831..09d55d4 100755 --- a/bin/getssl +++ b/bin/getssl @@ -276,6 +276,13 @@ # 2021-10-01 Show help if no domain specified (#705)(2.44) # 2021-10-08 Extract release tag from release api using awk (fix BSD issues) # 2021-10-11 Fix broken upgrade url (#718)(2.45) +# 2021-10-22 Copy fullchain to DOMAIN_CHAIN_LOCATION (amartin-git) +# 2021-11-10 Detect Solaris and use gnu tools (#701)(miesi) +# 2021-11-12 Support acme-dns and fix CNAME issues (#722)(#308) +# 2021-12-14 Enhancements for GoDaddy (support more levels of domain names, no longer require GODADDY_BASE, and actual deletion of resource records) +# 2021-12-22 Don't show usage if run with --upgrade (#728) +# 2021-12-23 Don't use +idnout if dig shows a warning (#688) +# 2022-01-06 Support --account-id (#716)(2.46) # ---------------------------------------------------------------------------------------- case :$SHELLOPTS: in @@ -284,7 +291,7 @@ esac PROGNAME=${0##*/} PROGDIR="$(cd "$(dirname "$0")" || exit; pwd -P;)" -VERSION="2.45" +VERSION="2.46" # defaults ACCOUNT_KEY_LENGTH=4096 @@ -354,6 +361,7 @@ _QUIET=0 _RECREATE_CSR=0 _REDIRECT_OUTPUT="1>/dev/null 2>&1" _REVOKE=0 +_SHOW_ACCOUNT_ID=0 _TEST_SKIP_CNAME_CALL=0 _TEST_SKIP_SOA_CALL=0 _UPGRADE=0 @@ -438,7 +446,7 @@ cert_install() { # copy certs to the correct location (creating concatenated fi else to_location="${DOMAIN_CHAIN_LOCATION}" fi - cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem" + cat "$FULL_CHAIN" > "$TEMP_DIR/${DOMAIN}_chain.pem" copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem" "$to_location" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then cat "${CERT_FILE%.*}.ec.crt" "${CA_CERT%.*}.ec.crt" > "$TEMP_DIR/${DOMAIN}_chain.pem.ec" @@ -536,12 +544,6 @@ check_challenge_completion() { # checks with the ACME server if our challenge is debug "sleep 5 secs before testing verify again" sleep 5 done - - if [[ "$DEACTIVATE_AUTH" == "true" ]]; then - deactivate_url=$(echo "$responseHeaders" | grep "^Link" | awk -F"[<>]" '{print $2}') - deactivate_url_list="$deactivate_url_list $deactivate_url" - debug "adding url to deactivate list - $deactivate_url" - fi } check_challenge_completion_dns() { # perform validation via DNS challenge @@ -575,10 +577,19 @@ check_challenge_completion_dns() { # perform validation via DNS challenge # shellcheck disable=SC2086 debug "$DNS_CHECK_FUNC" $DNS_CHECK_OPTIONS TXT "${rr}" "@${ns}" # shellcheck disable=SC2086 - check_result=$($DNS_CHECK_FUNC $DNS_CHECK_OPTIONS TXT "${rr}" "@${ns}" \ - | grep -i "^${rr}" \ - | grep 'IN\WTXT'|awk -F'"' '{ print $2}') + check_output=$($DNS_CHECK_FUNC $DNS_CHECK_OPTIONS TXT "${rr}" "@${ns}") + check_result=$(grep -i "^${rr}"<<<"${check_output}"|grep 'IN\WTXT'|awk -F'"' '{ print $2}') debug "check_result=\"$check_result\"" + + # Check if rr is a CNAME + if [[ -z "$check_result" ]]; then + rr_cname=$(grep -i "^${rr}"<<<"${check_output}"|grep 'IN\WCNAME'|awk '{ print $5}') + debug "cname check=\"$rr_cname\"" + if [[ -n "$rr_cname" ]]; then + check_result=$(grep -i "^${rr_cname}"<<<"${check_output}"|grep 'IN\WTXT'|awk -F'"' '{ print $2}' | uniq) + fi + fi + if [[ -z "$check_result" ]]; then # shellcheck disable=SC2086 debug "$DNS_CHECK_FUNC" $DNS_CHECK_OPTIONS ANY "${rr}" "@${ns}" @@ -589,14 +600,20 @@ check_challenge_completion_dns() { # perform validation via DNS challenge debug "check_result=\"$check_result\"" fi elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then + debug "$DNS_CHECK_FUNC" -t TXT "${rr}" "${ns}" check_result=$($DNS_CHECK_FUNC -t TXT "${rr}" "${ns}" \ | grep 'descriptive text'|awk -F'"' '{ print $2}') + debug "check_result=\"$check_result\"" else + debug "$DNS_CHECK_FUNC" -type=txt "${rr}" "${ns}" check_result=$(nslookup -type=txt "${rr}" "${ns}" \ | grep 'text ='|awk -F'"' '{ print $2}') + debug "check_result=\"$check_result\"" if [[ -z "$check_result" ]]; then + debug "$DNS_CHECK_FUNC" -type=any "${rr}" "${ns}" check_result=$(nslookup -type=any "${rr}" "${ns}" \ | grep 'text ='|awk -F'"' '{ print $2}') + debug "check_result=\"$check_result\"" fi fi debug "expecting \"$auth_key\"" @@ -829,7 +846,7 @@ check_getssl_upgrade() { # check if a more recent release is available # shellcheck disable=SC2086 status=$(curl ${_NOMETER:---silent} -w "%{http_code}" --user-agent "$CURL_USERAGENT" "$CODE_LOCATION" --output "$TEMP_UPGRADE_FILE") errcode=$? -debug errcode=$errcode + debug curl errcode=$errcode if [[ $errcode -eq 60 ]]; then error_exit "curl needs updating, your version does not support SNI (multiple SSL domains on a single IP)" @@ -1201,6 +1218,11 @@ create_order() { fi ((dn++)) done + if [[ "$DEACTIVATE_AUTH" == "true" ]]; then + deactivate_url_list+=" $l " + debug "url added to deactivate list ${l}" + debug "deactivate list is now $deactivate_url_list" + fi done fi } @@ -1256,39 +1278,42 @@ error_exit() { # give error message on error exit } find_dns_utils() { - HAS_NSLOOKUP=false - HAS_DIG_OR_DRILL="" - DIG_SUPPORTS_NOIDNOUT=false - HAS_HOST=false - if [[ -n "$(command -v nslookup 2>/dev/null)" ]]; then - debug "HAS NSLOOKUP=true" - HAS_NSLOOKUP=true - fi - - if [[ -n "$(command -v drill 2>/dev/null)" ]]; then - HAS_DIG_OR_DRILL="drill" - elif [[ -n "$(command -v dig 2>/dev/null)" ]] && dig >/dev/null 2>&1; then - if dig -r >/dev/null 2>&1; then - # use dig -r so ~/.digrc is not used - HAS_DIG_OR_DRILL="dig -r" - else - HAS_DIG_OR_DRILL="dig" - fi + HAS_NSLOOKUP=false + HAS_DIG_OR_DRILL="" + DIG_SUPPORTS_NOIDNOUT=false + HAS_HOST=false + if [[ -n "$(command -v nslookup 2>/dev/null)" ]]; then + debug "HAS NSLOOKUP=true" + HAS_NSLOOKUP=true + fi + + if [[ -n "$(command -v drill 2>/dev/null)" ]]; then + HAS_DIG_OR_DRILL="drill" + elif [[ -n "$(command -v dig 2>/dev/null)" ]] && dig >/dev/null 2>&1; then + if dig -r >/dev/null 2>&1; then + # use dig -r so ~/.digrc is not used + HAS_DIG_OR_DRILL="dig -r" + else + HAS_DIG_OR_DRILL="dig" fi + fi - if [[ -n "$HAS_DIG_OR_DRILL" ]]; then - if $HAS_DIG_OR_DRILL +noidnout >/dev/null 2>&1; then - DIG_SUPPORTS_NOIDNOUT=true - fi - - debug "HAS DIG_OR_DRILL=$HAS_DIG_OR_DRILL" - debug "DIG_SUPPORTS_NOIDNOUT=$DIG_SUPPORTS_NOIDNOUT" + if [[ -n "$HAS_DIG_OR_DRILL" ]]; then + if dig_output=$($HAS_DIG_OR_DRILL +noidnout localhost 2>&1 >/dev/null); then + # dig +noidnout on Ubuntu 18 succeeds, but outputs warning message to stderr - issue #688) + if [[ "$dig_output" != ";; IDN support not enabled" ]]; then + DIG_SUPPORTS_NOIDNOUT=true + fi fi - if [[ -n "$(command -v host 2>/dev/null)" ]]; then - debug "HAS HOST=true" - HAS_HOST=true - fi + debug "HAS DIG_OR_DRILL=$HAS_DIG_OR_DRILL" + debug "DIG_SUPPORTS_NOIDNOUT=$DIG_SUPPORTS_NOIDNOUT" + fi + + if [[ -n "$(command -v host 2>/dev/null)" ]]; then + debug "HAS HOST=true" + HAS_HOST=true + fi } find_ftp_command() { @@ -1352,12 +1377,6 @@ for d in "${alldomains[@]}"; do if [[ $response_status == "valid" ]]; then info "$d is already validated" - if [[ "$DEACTIVATE_AUTH" == "true" ]]; then - deactivate_url="$(echo "$responseHeaders" | awk ' $1 ~ "^Location" {print $2}' | tr -d "\r")" - deactivate_url_list+=" $deactivate_url " - debug "url added to deactivate list ${deactivate_url}" - debug "deactivate list is now $deactivate_url_list" - fi # increment domain-counter ((dn++)) else @@ -1554,6 +1573,7 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n # domain is a CNAME: resolve it and continue with that debug Domain is a CNAME, actual domain is "$cname" gad_d=${cname} + res= fi # Use SOA +trace to find the name server @@ -1629,6 +1649,7 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n primary_ns="$primary_ns $PUBLIC_DNS_SERVER" fi + debug set primary_ns="$primary_ns" return fi fi @@ -1639,26 +1660,48 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n # shellcheck disable=SC2086 res=$(nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns "$gad_d" ${gad_s}) + # check for CNAME (assumes gad_d is _acme-challenge.{host}) + if [[ "$(grep -c "NXDOMAIN"<<<"$res")" -gt 0 ]]; then + debug "Cannot find nameserver record for $gad_d, using parent domain ${gad_d#*.}" + gad_d="${gad_d#*.}" + debug "nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns $gad_d ${gad_s}" + # shellcheck disable=SC2086 + res=$(nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns "$gad_d" ${gad_s}) + fi + if [[ "$(echo "$res" | grep -c "Non-authoritative")" -gt 0 ]]; then # this is a Non-authoritative server, need to check for an authoritative one. + debug "Response from non-authoritative server looking for authoritative server" + gad_s=$(echo "$res" | awk '$2 ~ "nameserver" {print $4; exit }' |sed 's/\.$//g') - if [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then + # If the previous line fails to find the nameserver, use the original + if [[ -z "$gad_s" ]]; then + gad_s="$orig_gad_s" + fi + + if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then + debug "$gad_d" appears to be a CNAME + gad_d=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g') + debug "Using $gad_d instead" + elif [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then # if domain name doesn't exist, then find auth servers for next level up + debug "Couldn't find NS or SOA for domain name, using nslookup $DNS_CHECK_OPTIONS -debug ${gad_d#*.} ${orig_gad_s}" + # shellcheck disable=SC2086 + res=$(nslookup $DNS_CHECK_OPTIONS -debug "${gad_d#*.}" ${orig_gad_s}) gad_s=$(echo "$res" | awk '$1 ~ "origin" {print $3; exit }') gad_d=$(echo "$res" | awk '$1 ~ "->" {print $2; exit}') # handle scenario where awk returns nothing if [[ -z "$gad_d" ]]; then - gad_d="$orig_gad_d" + gad_d="${orig_gad_d}" fi fi + debug "Using nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns $gad_d ${gad_s}" # shellcheck disable=SC2086 res=$(nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns "$gad_d" ${gad_s}) fi - if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then - gad_d=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g') - elif [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then + if [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then gad_s=$(echo "$res" | awk ' $1 ~ "origin" {print $3; exit }') gad_d=$(echo "$res"| awk '$1 ~ "->" {print $2; exit}') # handle scenario where awk returns nothing @@ -1680,6 +1723,11 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') fi + if [[ "$CHECK_PUBLIC_DNS_SERVER" == "true" ]]; then + primary_ns="$primary_ns $PUBLIC_DNS_SERVER" + fi + + debug set primary_ns="$primary_ns" return fi fi @@ -1824,6 +1872,14 @@ get_os() { # function to get the current Operating System os="cygwin" elif [[ ${uname_res:0:5} == "MINGW" ]]; then os="mingw" + elif [[ ${uname_res} == "SunOS" ]]; then + os="solaris" + if [ -d /usr/gnu/bin ]; then + export PATH=/usr/gnu/bin:$PATH + else + echo "Path with required GNU commands not found, please install /usr/gnu/bin" + exit 1 + fi else os="unknown" fi @@ -1906,13 +1962,14 @@ help_message() { # print out the help message -i, --install Install certificates and reload service -q, --quiet Quiet mode (only outputs on error, success of new cert, or getssl was upgraded) -Q, --mute Like -q, but also mute notification about successful upgrade - -r, --revoke "cert" "key" [CA_server] Revoke a certificate (the cert and key are required) + -r, --revoke "cert" "key" [CA_server] Revoke a certificate (the cert and key are required) -u, --upgrade Upgrade getssl if a more recent version is available - can be used with or without domain(s) -X, --experimental tag Upgrade to experimental releases, specified by tag (e.g. v9.43) -U, --nocheck Do not check if a more recent version is available -v --version Display current version of $PROGNAME -w working_dir "Working directory" --preferred-chain "chain" Use an alternate chain for the certificate + --account-id Display account id and exit _EOF_ } @@ -2542,7 +2599,7 @@ urlbase64_decode() { usage() { # echos out the program usage echo "Usage: $PROGNAME [-h|--help] [-d|--debug] [-c|--create] [-f|--force] [-a|--all] [-q|--quiet]"\ "[-Q|--mute] [-u|--upgrade] [-X|--experimental tag] [-U|--nocheck] [-r|--revoke cert key] [-w working_dir]"\ - "[--preferred-chain chain] domain" + "[--preferred-chain chain] [--account-id] domain" } write_domain_template() { # write out a template file for a domain. @@ -2776,6 +2833,8 @@ while [[ -n ${1+defined} ]]; do shift; WORKING_DIR="$1" ;; -preferred-chain | --preferred-chain) shift; PREFERRED_CHAIN="$1" ;; + --account-id) + _SHOW_ACCOUNT_ID=1 ;; --source) return ;; -*) @@ -2848,9 +2907,13 @@ if [[ $_UPGRADE_CHECK -eq 1 ]]; then check_getssl_upgrade # if nothing in command line and no revocation and not only config check, # then exit after upgrade - if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} -ne 1 ]] && [[ ${_REVOKE} -ne 1 ]] && [ "${_ONLY_CHECK_CONFIG}" -ne 1 ]; then + if [[ -z "$DOMAIN" ]] \ + && [[ ${_CHECK_ALL} -ne 1 ]] \ + && [[ ${_REVOKE} -ne 1 ]] \ + && [ "${_ONLY_CHECK_CONFIG}" -ne 1 ] \ + && [[ ${_SHOW_ACCOUNT_ID} -ne 1 ]]; then # if nothing in command line, print help before exit. - if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} -ne 1 ]]; then + if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} -ne 1 ]] && [[ ${_UPGRADE} -ne 1 ]]; then help_message fi graceful_exit @@ -2955,11 +3018,14 @@ if [[ ${_CHECK_ALL} -eq 1 ]]; then if [[ ${_QUIET} -eq 1 ]]; then cmd="$cmd -q" fi + # check if $dir is a directory with a getssl.cfg in it if [[ -f "$dir/getssl.cfg" ]]; then cmd="$cmd -w $WORKING_DIR \"$(basename "$dir")\"" debug "CMD: $cmd" eval "$cmd" + else + debug "conf file not found $dir/getssl.cfg" fi fi done @@ -3081,7 +3147,7 @@ if [[ $API -eq 2 ]]; then fi # if check_remote is true then connect and obtain the current certificate (if not forcing renewal) -if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then +if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]] && [[ $_SHOW_ACCOUNT_ID -eq 0 ]]; then real_d=${DOMAIN##\*.} debug "getting certificate for $DOMAIN from remote server ($real_d)" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then @@ -3200,7 +3266,7 @@ if [[ "$DUAL_RSA_ECDSA" == "false" ]] && [[ -s "$DOMAIN_DIR/${DOMAIN}.key" ]]; t fi # if there is an existing certificate file, check details. -if [[ -s "$CERT_FILE" ]]; then +if [[ -s "$CERT_FILE" ]] && [[ $_SHOW_ACCOUNT_ID -eq 0 ]]; then debug "certificate $CERT_FILE exists" enddate=$(openssl x509 -in "$CERT_FILE" -noout -enddate 2>/dev/null| cut -d= -f 2-) debug "local cert is valid until $enddate" @@ -3228,7 +3294,7 @@ if [[ -s "$CERT_FILE" ]]; then fi # end of .... if there is an existing certificate file, check details. -if [[ ! -t 0 ]] && [[ "$PREVENT_NON_INTERACTIVE_RENEWAL" = "true" ]]; then +if [[ ! -t 0 ]] && [[ "$PREVENT_NON_INTERACTIVE_RENEWAL" = "true" ]] && [[ $_SHOW_ACCOUNT_ID -eq 0 ]]; then errmsg="$DOMAIN due for renewal," errmsg="${errmsg} but not completed due to PREVENT_NON_INTERACTIVE_RENEWAL=true in config" error_exit "$errmsg" @@ -3277,16 +3343,16 @@ info "Registering account" # send the request to the ACME server. if [[ $API -eq 1 ]]; then if [[ "$ACCOUNT_EMAIL" ]] ; then - regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}' + regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}' else - regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}' + regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}' fi send_signed_request "$URL_new_reg" "$regjson" elif [[ $API -eq 2 ]]; then if [[ "$ACCOUNT_EMAIL" ]] ; then - regjson='{"termsOfServiceAgreed": true, "contact": ["mailto: '$ACCOUNT_EMAIL'"]}' + regjson='{"termsOfServiceAgreed": true, "contact": ["mailto: '$ACCOUNT_EMAIL'"]}' else - regjson='{"termsOfServiceAgreed": true}' + regjson='{"termsOfServiceAgreed": true}' fi send_signed_request "$URL_newAccount" "$regjson" else @@ -3297,19 +3363,24 @@ fi if [[ "$code" == "" ]] || [[ "$code" == '201' ]] ; then info "Registered" KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ') - debug "KID=_$KID}_" + debug "AccountId=$KID}" echo "$response" > "$TEMP_DIR/account.json" elif [[ "$code" == '409' ]] ; then KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ') debug responseHeaders "$responseHeaders" - debug "Already registered KID=$KID" + debug "Already registered, AccountId=$KID" elif [[ "$code" == '200' ]] ; then KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ') debug responseHeaders "$responseHeaders" - debug "Already registered account, KID=${KID}" + debug "Already registered account, AccountId=${KID}" else error_exit "Error registering account ...$responseHeaders ... $(json_get "$response" detail)" fi + +if [[ ${_SHOW_ACCOUNT_ID} -eq 1 ]]; then + echo "Account Id is: $KID" + graceful_exit +fi # end of registering account with CA # verify each domain diff --git a/bin/utils/dns_scripts/00GoDaddy-README.txt b/bin/utils/dns_scripts/00GoDaddy-README.txt deleted file mode 100644 index 9973556..0000000 --- a/bin/utils/dns_scripts/00GoDaddy-README.txt +++ /dev/null @@ -1,63 +0,0 @@ -Using GoDaddy DNS for LetsEncrypt domain validation. - -Quick guide to setting up getssl for domain validation of -GoDaddy DNS domains. - -There are two prerequisites to using getssl with GoDaddy DNS: - -1) Obtain an API access key from developer.godaddy.com - At first sign-up, you will be required to take a "test" key. - This is NOT what you need. Accept it, then get a "Production" - key. At this writing, there is no charge - but you must have - a GoDaddy customer account. - - You must get the API key for the account which owns the domain - that you want to get certificates for. If the domains that you - manage are owned by more than one account, get a key for each. - - The access key consists of a "Key" and a "Secret". You need - both. - -2) Obtain JSON.sh - https://github.com/dominictarr/JSON.sh - -With those in hand, the installation procedure is: - -1) Put JSON.sh in the getssl DNS scripts directory - Default: /usr/share/getssl/dns_scripts - -2) Open your config file (the global file in ~/.getssl/getssl.cfg - or the per-account file in ~/.getssl/example.net/getssl.cfg - -3) Set the following options: - VALIDATE_VIA_DNS="true" - DNS_ADD_COMMAND="/usr/share/getssl/dns_scripts/dns_add_godaddy" - DNS_DEL_COMMAND="/usr/share/getssl/dns_scripts/dns_del_godaddy" - # The API key for your account/this domain - export GODADDY_KEY="..." GODADDY_SECRET="..." - # The base domain name(s) in which the challege records are stored - # E.g. if www.example.net is in the example.net zone: - export GODADDY_BASE="example.com example.net" - - 4) Set any other options that you wish (per the standard - directions.) Use the test CA to make sure that - everything is setup correctly. - -That's it. getssl example.net will now validate with DNS. - -To trace record additions and removals, run getssl as -GODADDY_TRACE=Y getssl example.net - -There are additional options, which are documented in the -*godaddy" files and dns_godaddy -h. - -Copyright (C) 2017, 2018 Timothe Litt litt at acm _dot org - -This sofware may be freely used providing this notice is included with -all copies. The name of the author may not be used to endorse -any other product or derivative work. No warranty is provided -and the user assumes all responsibility for use of this software. - -Report any issues to https://github.com/tlhackque/getssl/issues. - -Enjoy. - diff --git a/bin/utils/dns_scripts/DNS_IONOS.md b/bin/utils/dns_scripts/DNS_IONOS.md new file mode 100644 index 0000000..d3a685e --- /dev/null +++ b/bin/utils/dns_scripts/DNS_IONOS.md @@ -0,0 +1,9 @@ +# Do DNS-01 verification using IONOS DNS API + +The getting started guide explains how to obtain API Keys https://developer.hosting.ionos.de/docs/getstarted + +All API Documentation can be found here https://developer.hosting.ionos.de/docs/dns + +JSON processing in bash is ... hard. So I choose `jq` to do the heavylifting. Other authors choose python so if +you think I did a bad decision feel free to implement this whith python/perl/ruby... + diff --git a/bin/utils/dns_scripts/GoDaddy-README.txt b/bin/utils/dns_scripts/GoDaddy-README.txt index d58ba73..54db3b8 100644 --- a/bin/utils/dns_scripts/GoDaddy-README.txt +++ b/bin/utils/dns_scripts/GoDaddy-README.txt @@ -34,6 +34,8 @@ With those in hand, the installation procedure is: DNS_DEL_COMMAND="/usr/share/getssl/dns_scripts/dns_del_godaddy" # The API key for your account/this domain export GODADDY_KEY="..." GODADDY_SECRET="..." + # If you have been using GODADDY_BASE previously, then it is no + longer necessary. The base domain will automatically be determined. 4) Set any other options that you wish (per the standard directions.) Use the test CA to make sure that diff --git a/bin/utils/dns_scripts/dns_add_acmedns b/bin/utils/dns_scripts/dns_add_acmedns new file mode 100755 index 0000000..a8c3073 --- /dev/null +++ b/bin/utils/dns_scripts/dns_add_acmedns @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Need to add your API user and key below or set as env variable +apiuser=${ACMEDNS_API_USER:-''} +apikey=${ACMEDNS_API_KEY:-''} +apisubdomain=${ACMEDNS_SUBDOMAIN:-''} + +# This script adds a token to acme-dns.io DNS for the ACME challenge +# usage dns_add_acme-dns "domain name" "token" +# return codes are; +# 0 - success +# 1 - error returned from server + +fulldomain="${1}" +token="${2}" + +API='https://auth.acme-dns.io/update' + +# Check initial parameters +if [[ -z "$fulldomain" ]]; then + echo "DNS script requires full domain name as first parameter" + exit 1 +fi +if [[ -z "$token" ]]; then + echo "DNS script requires challenge token as second parameter" + exit 1 +fi + +curl_params=( + -H "accept: application/json" + -H "X-Api-Key: $apikey" + -H "X-Api-User: $apiuser" + -H 'Content-Type: application/json' +) + +generate_post_data() +{ + cat </tmp/$$.zones + +ZONE=$DNS_RR + +do=true +while $do; do + ZONE_ID=$(awk -F\; '/^'"$ZONE"';/{print $2}' &2 $0: requires JSON.sh as "$JSON" @@ -149,19 +142,12 @@ if ! [[ "$op" =~ ^(add|del)$ ]]; then echo "Operation must be \"add\" or \"del\"" >&2 exit 3 fi -domain="$2" -domain="${domain%'.'}" -if [ -z "$domain" ]; then - echo "'domain' parameter is required, see -h" >&2 - exit 3 -fi -name="$3" +name="$2" if [ -z "$name" ]; then echo "'name' parameter is required, see -h" >&2 exit 3 fi -! [[ "$name" =~ [.]$ ]] && name="${name}.${domain}." -data="$4" +data="$3" if [ -z "$data" ]; then echo "'data' parameter is required, see -h" >&2 exit 3 @@ -174,7 +160,7 @@ elif [ -z "$5" ]; then elif ! [[ "$5" =~ ^[0-9]+$ ]]; then echo "TTL $5 is not numeric" >&2 exit 3 -elif [ $5 -lt 600 ]; then +elif [ "$5" -lt 600 ]; then [ -n "$VERB" ] && \ echo "$5 is less than GoDaddy minimum of 600; increased to 600" >&2 ttl="600" @@ -185,14 +171,14 @@ fi # --- Done with parameters [ -n "$DEBUG" ] && \ - echo "$PROG: $op $domain $name \"$data\" $ttl" >&2 + echo "$PROG: $op $name \"$data\" $ttl" >&2 # Authorization header has secret and key authhdr="Authorization: sso-key $GODADDY_KEY:$GODADDY_SECRET" if [ -n "$TRACE" ]; then - function timestamp { local tm="`LC_TIME=C date '+%T.%N'`" + function timestamp { local tm="$(LC_TIME=C date '+%T.%N')" local class="$1"; shift echo "${tm:0:15} ** ${class}: $*" >>"$TRACE" } @@ -207,28 +193,92 @@ fi [ -n "$DEBUG" ] && echo "$authhdr" >&2 -if [ "$op" = "add" ]; then - # May need to retry due to zone cuts - - while [[ "$domain" =~ [^.]+\.[^.]+ ]]; do - - reqname="$name" - # The API doesn't trim the base domain from the name (it used to) - # If specified, remove any listed base. - if [ -n "$GODADDY_BASE" ]; then - for GDB in $GODADDY_BASE; do - gdb="`echo "$GDB" | sed -e's/\\.$//;s/\\./\\\\./g;'`" - gdb="^(.+)\\.$gdb\\.?$" - if [[ "$name" =~ $gdb ]]; then - reqname="${BASH_REMATCH[1]}" - break; - fi - done - else - eval 'reqname="$''{name%'"'.$domain.'}"'"' +#strip off the last period +if [[ "$name" =~ ^.+\. ]]; then + name=${name%?} +fi + +reqdomain= +reqname= + +# GoDaddy REST API URL is in the format /v1/domains/{domain}/records/{type}/{name} +# for adding/updating (PUT) or deleting (DELETE) a record. The API will support up +# to three segments in domain names (ex. mydomain.com and www.mydomain.com) +# in order to determine which domain the API call will affect (both mydomain.com and +# www.mydomain.com will result in the modification of the mydomain.com domain. Any +# more than three segments (ex. sub.something.mydomain.com will result in +# the API throwing a MISMATCH_FORMAT error. +# +# Examples +# 1. If mydomain.com was provided to this script as the domain parameter, and +# _acme-challengemydomain.com was provided as the name, then the URL +# /v1/domains/mydomain.com/records/TXT/_acme-challenge will be used which +# +# 2. If www.mydomain.com was provided to this script as the domain parameter, +# and _acme-challenge.www.mydomain.com was provided as the name, then the +# URL /v1/domains/mydomain.com/records/TXT/_acme-challenge.www will be used. + +# Determine the domain and the name to use for the API the URL +# The name parameter given to us is in the format challenge.domain. +# (ex _acme-challenge.mydomain.com. - note the trailing period). We will just +# use the name given us to determine the domain + +while [[ "$name" =~ ^([^.]+)\.([^.]+.*) ]]; do + if [ -n "${reqname}" ]; then reqname="${reqname}."; fi + reqname="${reqname}${BASH_REMATCH[1]}" + testdomain="${BASH_REMATCH[2]}" + name=$testdomain + if [[ ! "$name" =~ [^.]+\.[^.]+ ]]; then + exit 1 + fi + + url="$API/$testdomain" + + [ -n "$DEBUG" ] && echo "Looking for domain ${testdomain}" + + response="$(curl -i -s -X GET --config - "${url}" <&2 <&2 + exit $sts fi - url="$API/$domain/records/TXT/$reqname" + if echo "$response" | grep -q '^HTTP/.* 200 '; then + [ -n "$DEBUG" ] && echo "Found domain ${testdomain}" + reqdomain=${testdomain} + break + fi + + code="$(echo "$response" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//')" + if [ "$code" = 'NOT_FOUND' ]; then + continue + fi +done + + +if [ -z "$reqdomain" ] || [ -z "$reqname" ]; then + echo "Unable to determine domain or RR name" >&2 + exit 3 +fi + + + +if [ "$op" = "add" ]; then + + url="$API/$reqdomain/records/TXT/$reqname" request='[{"data":"'$data'","ttl":'$ttl'}]' [ -n "$DEBUG" ] && cat >&2 <&2 - exit $sts - fi - if ! echo "$result" | grep -q '^HTTP/.* 200 '; then - code="`echo "$result" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//'`" - msg="`echo "$result" | grep '"message":' | sed -e's/^.*"message":"//; s/\".*$//'`" - if [ "$code" = "DUPLICATE_RECORD" ]; then - if [ -n "$VERB" ]; then - echo "$msg in $domain" >&2 - fi - exit 0 # Duplicate record is still success - fi - if [ "$code" = 'UNKNOWN_DOMAIN' ]; then - if [[ "$domain" =~ ^([^.]+)\.([^.]+\.[^.]+.*) ]]; then - [ -n "$DEBUG" ] && \ - echo "$domain unknown, trying ${BASH_REMATCH[2]}" >&2 - domain="${BASH_REMATCH[2]}" - continue; - fi + if [ $sts -ne 0 ]; then + echo "curl error $sts adding record" >&2 + exit $sts + fi + if ! echo "$result" | grep -q '^HTTP/.* 200 '; then + code="$(echo "$result" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//')" + msg="$(echo "$result" | grep '"message":' | sed -e's/^.*"message":"//; s/\".*$//')" + if [ "$code" = "DUPLICATE_RECORD" ]; then + if [ -n "$VERB" ]; then + echo "$msg in $reqdomain" >&2 fi - echo "Request failed $msg" >&2 - exit 1 + exit 0 # Duplicate record is still success fi - [ -n "$VERB" ] && echo "$domain: added $name $ttl TXT \"$data\"" >&2 - exit 0 - done + echo "Request failed $msg" >&2 + exit 1 + fi + [ -n "$VERB" ] && echo "$reqdomain: added $reqname $ttl TXT \"$data\"" >&2 + exit 0 + fi -# ----- Delete - -# There is no delete API -# But, it is possible to replace all TXT records. -# -# So, first query for all TXT records - -# May need to retry due to zone cuts - -while [[ "$domain" =~ [^.]+\.[^.]+ ]]; do - url="$API/$domain/records/TXT" - [ -n "$DEBUG" ] && echo "Query for TXT records to: $url" >&2 +if [ "$op" = "del" ]; then + url="$API/$reqdomain/records/TXT/$reqname" + [ -n "$DEBUG" ] && echo "Deleting challenge TXT records at: $url" >&2 - current="$(curl -i -s -X GET --config - "$url" <&2 @@ -312,132 +346,14 @@ Response $current -------- EOF - if ! echo "$current" | grep -q '^HTTP/.* 200 '; then - code="`echo "$current" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//'`" - msg="`echo "$current" | grep '"message":' | sed -e's/^.*"message":"//; s/\".*$//'`" - if [ "$code" = "UNKNOWN_DOMAIN" ]; then - if [[ "$domain" =~ ^([^.]+)\.([^.]+\.[^.]+.*) ]]; then - [ -n "$DEBUG" ] && echo \ - "$domain unknown, trying ${BASH_REMATCH[2]}" >&2 - domain="${BASH_REMATCH[2]}" - continue; - fi - fi + + if ! echo "$current" | grep -q '^HTTP/.* 204 '; then + code="$(echo "$current" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//')" + msg="$(echo "$current" | grep '"message":' | sed -e's/^.*"message":"//; s/\".*$//')" echo "Request failed $msg" >&2 exit 1 fi - # Remove headers - - current="$(echo "$current" | sed -e'0,/^\r*$/d')" - break -done - - # The zone cut is known, so the replace can't fail due to UNKNOWN domain - -if [ "$current" = '[]' ]; then # No TXT records in zone - [ -n "$VERB" ] && echo "$domain: $name TXT \"$data\" does not exist" >&2 - [ -n "$DEBUG" ] && echo "No TXT records in $domain" >&2 - exit 1 # Intent was to change, so error status -fi - -[ -n "$DEBUG" ] && echo "Response is valid" - -# Prepare request to replace TXT RRSET - -# Parse JSON and select only the record structures, which are [index] { ...} -current="$(echo "$current" | $JSON | sed -n -e'/^\[[0-9][0-9]*\]/{ s/^\[[0-9][0-9]*\]//; p}')" -base="$current" - -[ -n "$DEBUG" ] && cat >&2 <&2 - exit 1 # Intent was to change DNS, so this is an error -fi - -# Remove whitespace and insert needed commmas -# -fmtnew="$new" -new=$(echo "$new" | sed -e"s/}/},/g; \$s/},/}/;" | tr -d '\t\n') - -if [ -z "$new" ]; then - [ -n "$VERB" ] && echo "Replacing last TXT record with a dummy (see -h)" >&2 - new='{"type":"TXT","name":"_dummy.record_","data":"_This record is not used_","ttl":601}' - dummy="t" - TAB=$'\t' - fmtnew="${TAB}$new" - if [ "$fmtnew" = "$base" ]; then - [ -n "$VERB" ] && echo "This tool can't delete a placeholder when it is the only TXT record" >&2 - exit 0 # Not really success, but retrying won't help. - fi + [ -n "$VERB" ] && echo "$reqdomain: deleted $reqname TXT \"$data\"" >&2 + exit 0 fi - -request="[$new]" - -[ -n "$DEBUG" ] && cat >&2 <&2 <&2 <&2 - exit $sts -fi -if ! echo "$result" | grep -q '^HTTP/.* 200 '; then - result="$(echo "$result" | sed -e'0,/^\r*$/d')" - code="`echo "$result" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//'`" - msg="`echo "$result" | grep '"message":' | sed -e's/^.*"message":"//; s/\".*$//'`" - echo "Request failed $msg" >&2 - exit 1 -fi - -if [ -n "$VERB" ]; then - if [ -n "$dummy" ]; then - echo "$domain: replaced $name TXT \"$data\" with a placeholder" >&2 - else - echo "$domain: deleted $name TXT \"$data\"" >&2 - fi -fi -exit 0 -