Jari Turkia
7 years ago
1 changed files with 358 additions and 0 deletions
@ -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 <<END_OF_JSON |
|||
{ |
|||
"records" : [{ |
|||
"name" : "$fulldomain", |
|||
"type" : "TXT", |
|||
"data" : "$txtvalue" |
|||
}] |
|||
} |
|||
END_OF_JSON |
|||
|
|||
_debug "Rackspace API URL to use for adding: $api_url/domains/$RACKSPACE_DOMAIN_ID/records" |
|||
|
|||
json_data=$(curl --silent -X POST --data "$json_data" -H "X-Auth-Token: $token" -H "Content-Type: application/json" -H "Accept: application/json" "$api_url/domains/$RACKSPACE_DOMAIN_ID/records") |
|||
if [ $? -gt 0 ]; then |
|||
_err "Failed to add record to Rackspace Cloud DNS. Fatal error, cannot continue." |
|||
exit 1 |
|||
fi |
|||
|
|||
local status=$(echo "$json_data" | jq -r '.status') |
|||
if [ -z "$status" ] || [ "$status" == "null" ]; then |
|||
local code=$(echo "$json_data" | jq -r '."error-message"') |
|||
if [ -n "$code" ] && [ "$code" != "null" ]; then |
|||
_err "Failed to add record to Rackspace Cloud DNS. No permission to add! Fatal error: $code" |
|||
else |
|||
code=$(echo "$json_data" | jq -r .code) |
|||
_err "Failed to add record to Rackspace Cloud DNS. Status: HTTP/$code" |
|||
fi |
|||
exit 1 |
|||
fi |
|||
if [ "$status" == "RUNNING" ]; then |
|||
local callback_url=$(echo "$json_data" | jq -r '.callbackUrl') |
|||
if [ -z "$callback_url" ]; then |
|||
_err "Attempt to add record to Rackspace Cloud DNS most likely failed. There is no callback URL to query the operation status from." |
|||
return 1 |
|||
fi |
|||
while [ "$status" == "RUNNING" ]; do |
|||
sleep 2 |
|||
json_data=$(curl --silent -H "X-Auth-Token: $token" "$callback_url") |
|||
if [ $? -gt 0 ]; then |
|||
_err "Failed to query DNS add status from $callback_url" |
|||
return 1 |
|||
fi |
|||
status=$(echo "$json_data" | jq -r '.status') |
|||
done |
|||
|
|||
if [ "$status" == "ERROR" ]; then |
|||
_err "Failed to add record to Rackspace Cloud DNS, add status is: $status." |
|||
# See, if the add failed because the record already exists. |
|||
json_data=$(curl --silent -H "X-Auth-Token: $token" -H "Accept: application/json" "$api_url/domains/$RACKSPACE_DOMAIN_ID/records?type=TXT&name=$fulldomain") |
|||
if [ $? == 0 ] && [ -n "$json_data" ] && [ $RACKSPACE_RETRY == 0 ] ; then |
|||
# We have something ... |
|||
local record_id=$(echo "$json_data" | jq -r '.records[0].id') |
|||
if [ -n "$record_id" ] || [ "$record_id" != "null" ]; then |
|||
# Attempt to delete the record |
|||
_info "Found existing record! Deleting record $record_id on domain $RACKSPACE_DOMAIN." |
|||
json_data=$(curl --silent -X DELETE -H "X-Auth-Token: $token" -H "Accept: application/json" "$api_url/domains/$RACKSPACE_DOMAIN_ID/records/$record_id") |
|||
local status=$(echo "$json_data" | jq -r '.status') |
|||
if [ "$status" == "RUNNING" ]; then |
|||
_info "Succesfully deleted the record. Retrying ..." |
|||
RACKSPACE_RETRY=1 |
|||
sleep 3 |
|||
|
|||
# Call myself with same arguments. |
|||
dns_rackspace_add "$1" "$2" |
|||
return $? |
|||
else |
|||
_err "Failed to delete the record. Nothing else to try." |
|||
fi |
|||
fi |
|||
fi |
|||
|
|||
# end if $status = ERROR |
|||
fi |
|||
fi |
|||
if [ "$status" != "COMPLETED" ]; then |
|||
_err "Failed to add record to Rackspace Cloud DNS, add status is: $status." |
|||
return 1 |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
#Usage: fulldomain txtvalue |
|||
#Remove the txt record after validation. |
|||
dns_rackspace_rm() { |
|||
local fulldomain=$1 |
|||
local txtvalue=$2 |
|||
_info "Using Rackspace Cloud DNS API to remove challenge $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 an existing TXT-record. |
|||
if [[ ! "$fulldomain" =~ ^_acme-challenge\.(.+)$ ]]; then |
|||
_err "Failed to extract domain name from $fulldomain." |
|||
return 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(). |
|||
return 1 |
|||
fi |
|||
|
|||
local text_rr=${fulldomain%.$RACKSPACE_DOMAIN} |
|||
_info "Using domain $RACKSPACE_DOMAIN on Rackspace Cloud DNS. Trying to find and remove $text_rr." |
|||
|
|||
_debug "Rackspace API URL to use for searching: $api_url/domains/$RACKSPACE_DOMAIN_ID/records" |
|||
|
|||
# Go search for TXT-records |
|||
json_data=$(curl --silent -H "X-Auth-Token: $token" -H "Accept: application/json" "$api_url/domains/$RACKSPACE_DOMAIN_ID/records?type=TXT&name=$fulldomain") |
|||
if [ $? -gt 0 ]; then |
|||
_err "Failed to search for TXT-record $fulldomain on Rackspace Cloud DNS." |
|||
return 1 |
|||
fi |
|||
|
|||
local record_id=$(echo "$json_data" | jq -r '.records[0].id') |
|||
if [ -z "$record_id" ] || [ "$record_id" == "null" ]; then |
|||
_err "TXT-record $fulldomain was not found. Nothing to delete." |
|||
return 1 |
|||
fi |
|||
|
|||
_info "Deleting record $record_id on domain $RACKSPACE_DOMAIN." |
|||
json_data=$(curl --silent -X DELETE -H "X-Auth-Token: $token" -H "Accept: application/json" "$api_url/domains/$RACKSPACE_DOMAIN_ID/records/$record_id") |
|||
if [ $? -gt 0 ]; then |
|||
_err "Failed to delete record to Rackspace Cloud DNS. API-call failed." |
|||
return 1 |
|||
fi |
|||
|
|||
local status=$(echo "$json_data" | jq -r '.status') |
|||
if [ -z "$status" ] || [ "$status" == "null" ]; then |
|||
status=$(echo "$json_data" | jq -r '."error-message"') |
|||
if [ -n "$status" ] && [ "$status" != "null" ]; then |
|||
_err "Failed to delete record to Rackspace Cloud DNS. No permission to delete! Error: $status" |
|||
else |
|||
_err "Failed to delete record to Rackspace Cloud DNS. Unknown reason." |
|||
fi |
|||
return 1 |
|||
fi |
|||
if [ "$status" != "RUNNING" ]; then |
|||
_err "Failed to delete record to Rackspace Cloud DNS." |
|||
return 1 |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
#################### Private functions below ################################## |
|||
|
|||
|
|||
_rackspace_sanity() { |
|||
local needed=('curl' 'jq') |
|||
local cmd |
|||
|
|||
for cmd in "${needed[@]}"; do |
|||
which "$cmd" >& /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 |
|||
} |
|||
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue