diff --git a/dnsapi/dns_rackspace.sh b/dnsapi/dns_rackspace.sh new file mode 100644 index 00000000..9dc62106 --- /dev/null +++ b/dnsapi/dns_rackspace.sh @@ -0,0 +1,358 @@ +#!/bin/bash + +# vim: tabstop=4 shiftwidth=4 softtabstop=4 expandtab: + + +# See: +# https://developer.rackspace.com/docs/cloud-dns/v1/api-reference/ + +# Rackspace API authentication: +# Create file .rackspace.auth with your Rackspace API user credentials. +# The API user needs permission: DNS, Creator (View, Create, Edit) for adding to work. +# For deletion to work: DNS, Admin (View, Create, Edit, Delete) is needed. +# Example of a .rackspace.auth file: +# { "user": "my rackspace user", "key": "my rackspace API key" } + +# Example usage: +# ./acme.sh --keylength 4096 --issue -d "example.com" --dns dns_rackspace --dnssleep 10 + + +######## Public functions ##################### + +RACKSPACE_DOMAIN=0 +RACKSPACE_DOMAIN_ID=0 +RACKSPACE_RETRY=0 + +#Usage: dns_add _acme-challenge.www.domain.com "XKrxp6q0HG9i01zxXp5CPBs" +dns_rackspace_add() { + local fulldomain=$1 + local txtvalue=$2 + _info "Using Rackspace Cloud DNS API to add challenge into $fulldomain" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + _rackspace_sanity + _rackspace_authenticate + + # At this point, there is an authenticated session token that we can use. + local token_file=/tmp/.acme.rackspace.$EUID.token + local token=$(jq -r ".access.token.id" "$token_file") + local api_url=$(jq -r ".access.serviceCatalog[0].endpoints[0].publicURL" "$token_file") + local json_data + + # Try to find a domain from Rackspace that will have the new TXT-record. + # Start by stripping the hard-coded word "_acme-challenge." from the FQDN. + # Remainin record is a potential domain name in Rackspace. + if [[ ! "$fulldomain" =~ ^_acme-challenge\.(.+)$ ]]; then + _err "Failed to extract domain name from $fulldomain. Fatal error, cannot continue." + exit 1 + fi + + _rackspace_get_domain "${BASH_REMATCH[1]}" "$api_url" + if [ $? -gt 0 ]; then + # If the internal operation fails, an error will be emitted in the _rackspace_get_domain(). + # Ultimately, there is no way this operation can continue. + exit 1 + fi + + local text_rr=${fulldomain%.$RACKSPACE_DOMAIN} + if [ "$fulldomain" == "$text_rr" ]; then + _err "Found domain $RACKSPACE_DOMAIN for $fulldomain, but failed to create a RR for it. Fatal error, cannot continue." + exit 1 + fi + _info "Using domain $RACKSPACE_DOMAIN on Rackspace Cloud DNS. Adding $text_rr." + + # Add a record + read -r -d '' json_data <& /dev/null + if [ $? -gt 0 ]; then + _err "Rackspace Cloud DNS API needs command: $cmd" + exit 1 + fi + done +} + +_rackspace_get_domain() { + local domain_to_use="$1" + local api_url="$2" + local domain_to_check + + # Get list of all domains this API user can manage. + local json_data=$(curl --silent -H "X-Auth-Token: $token" -H "Accept: application/json" "$api_url/domains") + if [ $? -gt 0 ] || [ -z "$json_data" ]; then + _err "Failed to retrieve domain list from Rackspace Cloud DNS API. Fatal error, cannot continue." + return 1 + fi + local status=$(echo "$json_data" | jq -r '."error-message"') + if [ -n "$status" ] && [ "$status" != "null" ]; then + _err "Configured user has no permission to retrieve domain list from Rackspace Cloud DNS API. Fatal error, cannot continue." + return 1 + fi + + # Iterate the domain list reverse-sorted. That will do a longest match comparison if there are + # subdomain used for the request, but will also match a shorter domain. + local matching_domain_idx hostmaster_email + while [ -z "$matching_domain_idx" ] && [[ $domain_to_use =~ \..+$ ]]; do + local domain_idx=0 + while [ "$domain_to_check" != "null" ]; do + domain_to_check=$(echo "$json_data" | jq -r '.domains|sort_by(.name)|reverse|.['$domain_idx'].name') + if [ "$domain_to_use" == "$domain_to_check" ]; then + matching_domain_idx=$domain_idx + hostmaster_email=$(echo "$json_data" | jq -r '.domains|sort_by(.name)|reverse|.['$domain_idx'].emailAddress') + RACKSPACE_DOMAIN="$domain_to_use" + RACKSPACE_DOMAIN_ID=$(echo "$json_data" | jq -r '.domains|sort_by(.name)|reverse|.['$domain_idx'].id') + break + fi + domain_idx=$(( domain_idx+1 )) + done + if [ -z "$matching_domain_idx" ]; then + # Eat out one level from the domain, if nothing found + domain_to_use=${domain_to_use#*.} + domain_to_check='' + fi + done + if [ -z "$matching_domain_idx" ]; then + _err "Failed to find the domain for $fulldomain to add a record. Fatal error, cannot continue." + return 1 + fi + if [ -z "$RACKSPACE_DOMAIN_ID" ] || [ "$RACKSPACE_DOMAIN_ID" == "null" ]; then + _err "Failed to get domain ID for domain $domain_to_use. Fatal error, cannot add record." + return 1 + fi + + return 0 +} + +_rackspace_authenticate() { + local token_file=/tmp/.acme.rackspace.$EUID.token + + if [ ! -e "$token_file" ]; then + _rackspace_get_token + fi + + local token=$(jq -r --exit-status .access.token.id "$token_file") + if [ $? -gt 0 ]; then + _rackspace_get_token + token=$(jq -r --exit-status .access.token.id "$token_file") + if [ $? -gt 0 ]; then + _err "Failed to read access token from $token_file" + exit 1 + fi + fi + + curl --silent -H "X-Auth-Token: $token" "https://identity.api.rackspacecloud.com/v2.0/tokens/$token" | jq --exit-status .access.token.tenant > /dev/null + if [ $? -gt 0 ]; then + _err "Failed to verify access token from $token_file" + exit 1 + fi +} + +_rackspace_get_token() { + local token_file=/tmp/.acme.rackspace.$EUID.token + local creds_file user key + local auth_json stat umask code + + creds_file="$_SCRIPT_HOME/.rackspace.auth" + if [ ! -e "$creds_file" ]; then + creds_file="$LE_WORKING_DIR/.rackspace.auth" + if [ ! -e "$creds_file" ]; then + _err "Rackspace Cloud DNS API needs credentials in .rackspace.auth file in $_SCRIPT_HOME/ or $LE_WORKING_DIR/" + exit 1 + fi + fi + + user=$(jq -r .user "$creds_file") + key=$(jq -r .key "$creds_file") + if [ -z "$user" ] || [ -z "$key" ]; then + _err "Failed to read Rackspace Cloud DNS API credentials from $creds_file" + exit 1 + fi + + umask=$(umask) + umask 0077 + auth_json="{\"auth\":{\"RAX-KSKEY:apiKeyCredentials\":{\"username\":\"$user\",\"apiKey\":\"$key\"}}}" + curl --silent https://identity.api.rackspacecloud.com/v2.0/tokens -X POST -d "$auth_json" -H "Content-type: application/json" > $token_file + stat=$? + umask $umask + if [ $stat -gt 0 ]; then + _err "Failed to make an authentication request into Rackspace Cloud DNS API" + exit 1 + fi + jq . $token_file > /dev/null + if [ $? -gt 0 ]; then + _err "Failed to retrieve authentication JSON from Rackspace Cloud DNS API" + exit 1 + fi + + code=$(jq -r --exit-status .access.token.tenant "$token_file") + stat=$? + if [ $stat -gt 0 ]; then + code=$(jq -r .unauthorized.code "$token_file") + _err "Failed to authenticate into Rackspace Cloud DNS API ($stat). Status: HTTP/$code" + rm -f "$token_file" + exit 1 + fi +} +