diff --git a/cfddns.sh b/cfddns.sh index 161bcd7..d7f2b85 100755 --- a/cfddns.sh +++ b/cfddns.sh @@ -1,26 +1,27 @@ -#!/bin/sh - +#!/bin/bash +# +# Entrance File and Config File for cloudflare-ddns. # Import utility function here # You `cloudflare-ddns` file ABSOLUTE PATH here! . /config/scripts/cloudflare-ddns-edgeos/cloudflare-ddns -######## -# Alter here -######## +####################### +# -Config- Alter here # +####################### # Access Token generated from Cloudflare API token page -access_token="" +CF_ACCESS_TOKEN="" # Can be seen from your zone page, bottom right Zone id -zone_identifier="" +ZONE_IDENTIFIER="" # You DNS record name, with A subdomain -record_name="" +RECORD_NAME="" -######## -# Alter stops -######## +####################### +# -Config- Stop here # +####################### -updateMain +main diff --git a/cloudflare-ddns b/cloudflare-ddns index dddc552..d64cec6 100755 --- a/cloudflare-ddns +++ b/cloudflare-ddns @@ -1,172 +1,286 @@ -#!/bin/sh +#!/bin/bash +# +# Cloudflare DDNS functions script + +# set -o xtrace + +############################ +# --- Global Variables --- # +############################ +export CF_ACCESS_TOKEN="" # Start with "Bearer " +export ZONE_IDENTIFIER="" +export RECORD_NAME="" + +LAN_IP_SEGMENTS="^$" +LAN_IP_SEGMENTS="${LAN_IP_SEGMENTS}|(^10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$)" +LAN_IP_SEGMENTS="${LAN_IP_SEGMENTS}|(^127\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$)" +LAN_IP_SEGMENTS="${LAN_IP_SEGMENTS}|(^169\.254\.[0-9]{1,3}\.[0-9]{1,3}$)" +LAN_IP_SEGMENTS="${LAN_IP_SEGMENTS}|(^172\.(1[6-9]|2[0-9]|3[0-1])\.[0-9]{1,3}\.[0-9]{1,3}$)" +LAN_IP_SEGMENTS="${LAN_IP_SEGMENTS}|(^192\.168\.[0-9]{1,3}\.[0-9]{1,3}$)" + +readonly LAN_IP_SEGMENTS + +############################ +# -- Utilities Function -- # +############################ +err() { + echo >&2 "[ERROR] | $(date +'%Y-%m-%dT%H:%M:%S') | $*" +} + +info() { + echo >&2 "[INFO] | $(date +'%Y-%m-%dT%H:%M:%S') | $*" +} -export access_token="" # Start with "Bearer" -export zone_identifier="" -export record_name="" -getWanIpv4Address() { - if [ "$(uname)" != "Linux" ]; then - echo "non-Linux is not supported." >&2 +############################ +# Cloudflare-ddns Section # +############################ + +####################################### +# Get WAN ipv4 address via command-line tool `ip`, +# if not available, or behind NAT, via remote api. +# Globals: +# LAN_IP_SEGMENTS +# Arguments: +# None +# Outputs: +# None +# Returns: +# Host ipv4 address, non-zero on error. +####################################### +get_wan_ipv4_addr() { + + local ipv4_addr + + if [[ ! "$(uname)" == "Linux" ]]; then + err "non-Linux Platform is not supported." return 1 fi - local lanIpSegments="^$" - local ipv4Address="" - - lanIpSegments="$lanIpSegments|(^10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$)" - lanIpSegments="$lanIpSegments|(^127\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$)" - lanIpSegments="$lanIpSegments|(^169\.254\.[0-9]{1,3}\.[0-9]{1,3}$)" - lanIpSegments="$lanIpSegments|(^172\.(1[6-9]|2[0-9]|3[0-1])\.[0-9]{1,3}\.[0-9]{1,3}$)" - lanIpSegments="$lanIpSegments|(^192\.168\.[0-9]{1,3}\.[0-9]{1,3}$)" + ipv4_addr="" - echo "Getting Network Ip locally using cli-program ip..." >&2 + info "Getting Network Ip locally using cli-program ip..." - ipv4Address=$( + ipv4_addr=$( ip -oneline -4 address | grep -v -E '\slo|\sdocker' | awk '{ print $4 }' | cut -d'/' -f1 | - grep -v -E "$lanIpSegments" + grep -v -E "${LAN_IP_SEGMENTS}" ) - if [ -z "$ipv4Address" ]; then - echo "Failed to Get Network Ip locally." >&2 - echo "Using api.ipify.org to retrieve ip..." >&2 + if [[ -z "${ipv4_addr}" ]]; then + err "Failed to Get Network Ip locally." + info "Using api.ipify.org to retrieve ip..." if command -v curl >/dev/null 2>&1; then - echo "Using curl..." >&2 - ipv4Address=$(curl --silent -L https://api.ipify.org) - echo "ip.ipify.org => $ipv4Address" >&2 - elif command -v wget >/dev/null 2>&1; then - ipv4Address=$(wget --quiet -O- https://api.ipify.org) - echo "ip.ipify.org => $ipv4Address" >&2 + info "Using curl..." + ipv4_addr=$(curl --silent -L https://api.ipify.org) + info "api called from ip.ipify.org => ${ipv4_addr}" else - echo "curl and wget are both unavailable/uninstalled. " >&2 - echo "Please install one of them to resolve dependency problem" >&2 + err "curl is unavailable/uninstalled. " + err "Please install curl to resolve dependency problem" return 1 fi else - echo "cli-program ip => $ipv4Address" >&2 + info "command-line tool ip => ${ipv4_addr}" fi - echo "$ipv4Address" + echo "${ipv4_addr}" } + +####################################### +# Get DNS Record detail from cloudflare v4 api using curl GET. +# https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records +# Globals: +# ZONE_IDENTIFIER +# CF_ACCESS_TOKEN +# RECORD_NAME +# Arguments: +# None +# Outputs: +# None +# Returns: +# json-formatted: { "record_id": $id, "record_ip": $ip } +# , non-zero on error. +####################################### getCurrent4Record() { local _retrieve_json local _active_record_count + local current_record_ip + local current_record_id - local currentRecordIp - local currentRecordId - - echo "Curl to Cloudflare v4 api..." >&2 + info "Curl to Cloudflare v4 api..." _retrieve_json=$( - curl --silent -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$record_name&type=A" \ - -H "Authorization: $access_token" \ + curl --silent -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_IDENTIFIER}/dns_records?name=${RECORD_NAME}&type=A" \ + -H "Authorization: ${CF_ACCESS_TOKEN}" \ -H "Content-Type: application/json" ) - # echo "$_retrieve_json" | jq -r '.' - _active_record_count=$(echo "$_retrieve_json" | jq -r '.result_info.count') + _active_record_count=$(echo "${_retrieve_json}" | jq -r '.result_info.count') - if [ $_active_record_count = 0 ]; then - echo "Record NOT Found." >&2 - return 1 + if [[ ${_active_record_count} == 0 ]]; then + err "Record NOT Found." + return 127 fi - currentRecordIp=$(echo "$_retrieve_json" | jq -r '.result[0].content') - currentRecordId=$(echo "$_retrieve_json" | jq -r '.result[0].id') - echo "$record_name : $currentRecordId" >&2 - echo "$record_name --> $currentRecordIp" >&2 + current_record_ip=$(echo "${_retrieve_json}" | jq -r '.result[0].content') + current_record_id=$(echo "${_retrieve_json}" | jq -r '.result[0].id') + info "${RECORD_NAME} has cloudflare-side id: ${current_record_id}" + info "${RECORD_NAME} currently points to --> ${current_record_ip}" jq -r -c --null-input \ - --arg id "$currentRecordId" \ - --arg ip "$currentRecordIp" \ - '{ recordId: $id, recordIp: $ip }' + --arg id "${current_record_id}" \ + --arg ip "${current_record_ip}" \ + '{ record_id: $id, record_ip: $ip }' - # echo "{\"record_id\": \"$currentRecordId\", \"record_ip\": \"$currentRecordIp\"}" +} + + +####################################### +# Create DNS Record using curl POST via cloudflare v4 api. +# https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record +# Globals: +# ZONE_IDENTIFIER +# CF_ACCESS_TOKEN +# RECORD_NAME +# Arguments: +# host_ip: Host ipv4 address obtained from get_wan_ipv4_addr() +# Outputs: +# None +# Returns: +# None +# +# *Exists* Exit route for shell script. +####################################### +create4RecordIfNotExists() { + + local host_ip="$1" + local _retrieve_json - # echo "$currentRecordId $currentRecordIp" + info "Attempting to CREATE record via curl POST..." + + _retrieve_json=$( + curl --silent -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_IDENTIFIER}/dns_records" \ + -H "Authorization: ${CF_ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + --data "$( + jq -r -c --null-input \ + --arg record_name "${RECORD_NAME}" \ + --arg record_ip "${host_ip}" \ + '{"type": "A", "name": $record_name, "content": $record_ip, "ttl": 100, "proxied": false}' + )" + ) + + if [[ $(echo "${_retrieve_json}" | jq -r '.success') == "true" ]]; then + info "${RECORD_NAME} is CREATED." + info "If this is the first time you run this script." + info "Don't worry, it ran well. Proceed to next step in Documentation." + info "Record detail will follow in 3 seconds......" + info "If NOT --> (Be advised) Your record name is not found on cloudflare." + info ", which leads to auto-re-creation of the record." + sleep 3 + info "$(getCurrent4Record)" + exit 0 + else + err "${RECORD_NAME} creation failed. Result pretty print as below." + err "${_retrieve_json}" | jq -r '.' + exit 1 + fi } -# TODO: JSON Build function -# $1 Record Id -# $2 Record Ip +####################################### +# Update specific DNS record detail to cloudflare v4 api using curl PUT. +# https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record +# Globals: +# ZONE_IDENTIFIER +# CF_ACCESS_TOKEN +# RECORD_NAME +# Arguments: +# pending_record_id: dns record id which needs updated. +# pending_record_ip: dns record ip/content which needs updates +# Outputs: +# None +# Returns: +# 0 if record is updated successfully, non-zero on error. +####################################### updateCurrent4Record() { - local recordId="$1" - local recordIp="$2" - - echo "Attempting to point recordId $recordId to $recordIp via curl PUT..." >&2 + local pending_record_id="$1" + local pending_record_ip="$2" local _retrieve_json - local _put_json + + info "Attempting to point record id ${pending_record_id} to ${pending_record_ip} via curl PUT..." _retrieve_json=$( - curl --silent -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$recordId" \ - -H "Authorization: $access_token" \ + curl --silent -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_IDENTIFIER}/dns_records/${pending_record_id}" \ + -H "Authorization: ${CF_ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ --data "$( jq -r -c --null-input \ - --arg record_name "$record_name" \ - --arg recordIp "$recordIp" \ - '{"type": "A", "name": $record_name, "content": $recordIp, "ttl": 100, "proxied": false}' + --arg record_name "${RECORD_NAME}" \ + --arg record_ip "${pending_record_ip}" \ + '{"type": "A", "name": $record_name, "content": $record_ip, "ttl": 100, "proxied": false}' )" ) - if [ $(echo "$_retrieve_json" | jq -r '.success') = "true" ]; then - echo "$record_name is updated, now points to $recordIp" >&2 + if [[ $(echo "${_retrieve_json}" | jq -r '.success') == "true" ]]; then + info "${RECORD_NAME} is updated, now points to ${pending_record_ip}" return 0 else - echo "$record_name update failed. Result pretty print as below." >&2 - echo "$_retrieve_json" | jq -r '.' >&2 + err "${RECORD_NAME} update failed. Result pretty print as below." + err $(echo "${_retrieve_json}" | jq -r '.') return 1 fi return 0 } -updateMain() { - local _error - local _tmp - local recordId - local recordIp +main() { - local hostIp + local _error + local _return_tmp + local record_id + local record_ip + local host_ip - echo "Getting Host Ip Address" + info "Getting Host Ip Address" - hostIp=$(getWanIpv4Address) + host_ip=$(get_wan_ipv4_addr) _error="$?" - echo "$hostIp" - if [ "$_error" -ne 0 ]; then + if [[ "${_error}" -ne 0 ]]; then return 1 fi - echo "Getting Record Ip from Cloudflare" + info "Getting Record Ip from Cloudflare" - _tmp=$(getCurrent4Record) + _return_tmp=$(getCurrent4Record) _error="$?" - recordId=$(echo "$_tmp" | jq -r '.recordId') - recordIp=$(echo "$_tmp" | jq -r '.recordIp') - if [ "$_error" -ne 0 ]; then + if [[ "${_error}" -eq 0 ]]; then + record_id=$(echo "${_return_tmp}" | jq -r '.record_id') + record_ip=$(echo "${_return_tmp}" | jq -r '.record_ip') + elif [[ "${_error}" -eq 127 ]]; then + create4RecordIfNotExists ${host_ip} + else return 1 fi - if [ "$hostIp" = "$recordIp" ]; then - echo "Ip stays the same, no need to update. Exiting." + if [[ "${host_ip}" == "${record_ip}" ]]; then + info "Ip stays the same, no need to update. Exiting." return 0 fi - echo "Updating Record Ip" + info "Updating Record Ip" - updateCurrent4Record "$recordId" "$hostIp" + updateCurrent4Record "${record_id}" "${host_ip}" _error="$?" - if [ "$_error" -ne 0 ]; then + if [[ "${_error}" -ne 0 ]]; then return 1 fi