#!/bin/bash # # shellcheck disable=2317,2086,2178,2001,2004,2181,2068 function deploy_challenge { local FIRSTDOMAIN="${1}" local SLD=$(sed -E 's/(.*\.)*([^.]+)\..*/\2/' <<< "${FIRSTDOMAIN}") local TLD=$(sed -E 's/.*\.([^.]+)/\1/' <<< "${FIRSTDOMAIN}") local POSTDATA=( "--data-urlencode" "apiuser=${apiusr}" "--data-urlencode" "apikey=${apikey}" "--data-urlencode" "username=${apiusr}" "--data-urlencode" "ClientIp=${cliip}" "--data-urlencode" "SLD=${SLD}" "--data-urlencode" "TLD=${TLD}" ) local HOSTS_URI="https://api.namecheap.com/xml.response" local num=0 # get list of current records for domain local GET_HOSTS=( "--data-urlencode" "Command=namecheap.domains.dns.getHosts" ) local records_list=$(curl -s "${POSTDATA[@]}" "${GET_HOSTS[@]}" "${HOSTS_URI}" | sed -En 's/= 3 )); do ((num++)) # DOMAIN # The domain name (CN or subject alternative name) being validated. DOMAIN="${1}"; shift # TOKEN_FILENAME # The name of the file containing the token to be served for HTTP # validation. Should be served by your web server as # /.well-known/acme-challenge/${TOKEN_FILENAME}. TOKEN_FILENAME="${1}"; shift # TOKEN_VALUE # The token value that needs to be served for validation. For DNS # validation, this is what you want to put in the _acme-challenge # TXT record. For HTTP validation it is the value that is expected # be found in the $TOKEN_FILENAME file. TOKEN_VALUE[$count]="${1}"; shift SUB[$count]=$(sed -E "s/$SLD.$TLD//" <<< "${DOMAIN}") CHALLENGE_HOSTNAME=$(sed -E "s/\.$//" <<< "${SUB[$count]}") POSTDATA+=( "--data-urlencode" "hostname${num}=_acme-challenge${CHALLENGE_HOSTNAME:+.$CHALLENGE_HOSTNAME}" "--data-urlencode" "recordtype${num}=TXT" "--data-urlencode" "address${num}=${TOKEN_VALUE[$count]}" "--data-urlencode" "ttl${num}=60" ) ((count++)) done local items=$count api_return=$(curl -s --request POST "${POSTDATA[@]}" "${HOSTS_URI}") echo ${api_return} | grep -qi 'Status="OK"' if [[ $? != 0 ]]; then echo "ERROR: Post to API failed: ${api_return}" return 1 fi # get nameservers for domain IFS=$'\n' read -r -d '' -a nameservers < <( dig @1.1.1.1 +short ns $SLD.$TLD && printf '\0' ) # wait up to 2 minutes for DNS updates to be provisioned (check at 15 second intervals) timer=0 count=0 while [ $count -lt $items ]; do # check for DNS propagation while true; do for ns in ${nameservers[@]}; do dig @${ns} txt "_acme-challenge${SUB[$count]:+.${SUB[$count]%.}}.$SLD.$TLD" | grep -qe "${TOKEN_VALUE[$count]}" if [[ $? == 0 ]]; then break 2 fi done if [[ "$timer" -ge 180 ]]; then # time has exceeded 3 minutes send_error $FIRSTDOMAIN return 1 else echo " + DNS not propagated. Waiting 15s for record creation and replication... Total time elapsed has been $timer seconds." ((timer+=15)) sleep 15 fi done ((count++)) done return 0 } function clean_challenge { local DOMAIN="${1}" local TOKEN_FILENAME="${2}" local TOKEN_VALUE="${3}" # This hook is called after attempting to validate each domain, # whether or not validation was successful. Here you can delete # files or DNS records that are no longer needed. # # The parameters are the same as for deploy_challenge. local FIRSTDOMAIN="${1}" local SLD=$(sed -E 's/(.*\.)*([^.]+)\..*/\2/' <<< "${FIRSTDOMAIN}") local TLD=$(sed -E 's/.*\.([^.]+)/\1/' <<< "${FIRSTDOMAIN}") local POSTDATA=( "--data-urlencode" "apiuser=${apiusr}" "--data-urlencode" "apikey=${apikey}" "--data-urlencode" "username=${apiusr}" "--data-urlencode" "ClientIp=${cliip}" "--data-urlencode" "SLD=${SLD}" "--data-urlencode" "TLD=${TLD}" ) local HOSTS_URI="https://api.namecheap.com/xml.response" local num=0 # get list of current records for domain local GET_HOSTS=( "--data-urlencode" "Command=namecheap.domains.dns.getHosts" ) local records_list=$(curl -s "${POSTDATA[@]}" "${GET_HOSTS[@]}" "${HOSTS_URI}" | sed -En 's//dev/null 2>&1 } function deploy_cert { local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}" # This hook is called once for each certificate that has been # produced. Here you might, for instance, copy your new certificates # to service-specific locations # # Parameters: # - DOMAIN # The primary domain name, i.e. the certificate common # name (CN). # - KEYFILE # The path of the file containing the private key. # - CERTFILE # The path of the file containing the signed certificate. # - FULLCHAINFILE # The path of the file containing the full certificate chain. # - CHAINFILE # The path of the file containing the intermediate certificate(s). # - TIMESTAMP # Timestamp when the specified certificate was created. echo "Deploying certificate for ${DOMAIN}..." # copy new certificate to /etc/pki/tls/certs folder cp "${CERTFILE}" "${DEPLOYED_CERTDIR}/${DOMAIN}.crt" echo " + certificate copied" # copy new key to /etc/pki/tls/private folder cp "${KEYFILE}" "${DEPLOYED_KEYDIR}/${DOMAIN}.key" echo " + key copied" # copy new chain file which contains the intermediate certificate(s) cp "${CHAINFILE}" "${DEPLOYED_CERTDIR}/letsencrypt-intermediate-certificates.pem" echo " + intermediate certificate chain copied" # combine certificate and chain file (used by Nginx) cat "${CERTFILE}" "${CHAINFILE}" > "${DEPLOYED_CERTDIR}/${DOMAIN}-chain.crt" echo " + combine certificate and intermediate certificate chain" # send email notification send_notification $DOMAIN return 0 } function unchanged_cert { local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" # This hook is called once for each certificate that is still # valid and therefore wasn't reissued. # # Parameters: # - DOMAIN # The primary domain name, i.e. the certificate common # name (CN). # - KEYFILE # The path of the file containing the private key. # - CERTFILE # The path of the file containing the signed certificate. # - FULLCHAINFILE # The path of the file containing the full certificate chain. # - CHAINFILE # The path of the file containing the intermediate certificate(s). } function invalid_challenge() { local DOMAIN="${1}" RESPONSE="${2}" # This hook is called if the challenge response has failed, so domain # owners can be aware and act accordingly. # # Parameters: # - DOMAIN # The primary domain name, i.e. the certificate common # name (CN). # - RESPONSE # The response that the verification server returned } function request_failure() { local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" # This hook is called when an HTTP request fails (e.g., when the ACME # server is busy, returns an error, etc). It will be called upon any # response code that does not start with '2'. Useful to alert admins # about problems with requests. # # Parameters: # - STATUSCODE # The HTML status code that originated the error. # - REASON # The specified reason for the error. # - REQTYPE # The kind of request that was made (GET, POST...) } function startup_hook() { # This hook is called before the cron command to do some initial tasks # (e.g. starting a webserver). : } function exit_hook() { # This hook is called at the end of the cron command and can be used to # do some final (cleanup or other) tasks. : } # Setup default config values, load configuration file function load_config() { # Default values apiusr= apikey= RECORDS_BACKUP=${BASEDIR}/records_backup DEPLOYED_CERTDIR=$PWD/certs DEPLOYED_KEYDIR=$PWD/private mkdir -p "$DEPLOYED_KEYDIR" "$DEPLOYED_CERTDIR" "$RECORDS_BACKUP" # Check if config file exists if [[ ! -f "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/config" ]]; then echo "#" >&2 echo "# !! WARNING !! No main config file found, using default config!" >&2 echo "#" >&2 else . "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/config" fi } send_error() { >&2 echo "Failed to recertify ${FIRSTDOMAIN}" } send_notification() { echo "SUCCESS for ${FIRSTDOMAIN}" } # load config values load_config mkdir -p "$RECORDS_BACKUP" "$DEPLOYED_CERTDIR" "$DEPLOYED_KEYDIR" # get this client's ip address cliip=$(curl -s -4 https://ifconfig.co/ip) HANDLER="$1"; shift if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|unchanged_cert|invalid_challenge|request_failure|startup_hook|exit_hook)$ ]]; then "$HANDLER" "$@" fi exit $?