Skip to content
This repository has been archived by the owner on Jan 29, 2024. It is now read-only.

Commit

Permalink
adds/updates records; uses jq instead of grep (#2)
Browse files Browse the repository at this point in the history
* adds a record if it doesn't exist yet; uses jq to extract information more reliably from cloudflare responses

* configuration validation, and defaults for TTL and PROXY; better cloudflare response parsing and error handling/reporting; formatting and comments

* added bash to docker image; errors out when trying to update a CNAME record; creates a new record if one doesn't exist if FORCE_CREATE; runs only once w/o setting up cron if RUNONCE is set; doc updates; removed defaults of env vars (script reports what's missing instead); linted script
  • Loading branch information
acolomba authored and joshuaavalon committed Jun 28, 2018
1 parent 62f031b commit 4ae7175
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 59 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# File created by running script locally
ip.dat

# Windows image file caches
Thumbs.db
ehthumbs.db
Expand Down Expand Up @@ -42,4 +45,4 @@ Network Trash Folder
Temporary Items
.apdisk

.idea
.idea
22 changes: 12 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
FROM alpine:3.6

ADD cloudflare.sh /cloudflare.sh
ADD cron /var/spool/cron/crontabs/root
ADD crontab /var/spool/cron/crontabs/root

ENV ZONE=example.com \
HOST=example.com \
[email protected] \
API=1111111111111111 \
TTL=1 \
PROXY=true \
DEBUG=false
ENV ZONE= \
HOST= \
EMAIL= \
API= \
TTL= \
PROXY= \
DEBUG= \
FORCE_CREATE= \
RUNONCE=

RUN apk add --update curl && \
RUN apk add --update bash jq curl && \
rm -rf /var/cache/apk/* && \
chmod +x /cloudflare.sh

CMD /cloudflare.sh && \
crond -f
test -z "$RUNONCE" && crond -f
63 changes: 42 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# joshuaavalon/cloudflare-ddns
[![version](https://images.microbadger.com/badges/version/joshuaavalon/cloudflare-ddns.svg)](https://microbadger.com/images/joshuaavalon/cloudflare-ddns)
[![](https://images.microbadger.com/badges/image/joshuaavalon/cloudflare-ddns.svg)](https://microbadger.com/images/joshuaavalon/cloudflare-ddns)
![Docker Pulls](https://img.shields.io/docker/pulls/joshuaavalon/cloudflare-ddns.svg)
![Docker Stars](https://img.shields.io/docker/stars/joshuaavalon/cloudflare-ddns.svg?colorB=dfb317)
[![Docker Automated build](https://img.shields.io/docker/automated/joshuaavalon/cloudflare-ddns.svg)](https://hub.docker.com/r/joshuaavalon/cloudflare-ddns/)
[![Docker build](https://img.shields.io/docker/build/joshuaavalon/cloudflare-ddns.svg)](https://hub.docker.com/r/joshuaavalon/cloudflare-ddns/)
[![version](https://images.microbadger.com/badges/version/joshuaavalon/cloudflare-ddns.svg)](https://microbadger.com/images/joshuaavalon/cloudflare-ddns)
[![](https://images.microbadger.com/badges/image/joshuaavalon/cloudflare-ddns.svg)](https://microbadger.com/images/joshuaavalon/cloudflare-ddns)
![Docker Pulls](https://img.shields.io/docker/pulls/joshuaavalon/cloudflare-ddns.svg)
![Docker Stars](https://img.shields.io/docker/stars/joshuaavalon/cloudflare-ddns.svg?colorB=dfb317)
[![Docker Automated build](https://img.shields.io/docker/automated/joshuaavalon/cloudflare-ddns.svg)](https://hub.docker.com/r/joshuaavalon/cloudflare-ddns/)
[![Docker build](https://img.shields.io/docker/build/joshuaavalon/cloudflare-ddns.svg)](https://hub.docker.com/r/joshuaavalon/cloudflare-ddns/)
[![MIT](https://img.shields.io/github/license/joshuaavalon/docker-cloudflare.svg)](https://github.com/docker-cloudflare/blob/master/LICENSE)

The is a simple docker using curl to update DNS record on [Cloudflare](https://www.cloudflare.com). Inspired by [rasmusbe/cloudflare-update-record.sh](https://gist.github.com/rasmusbe/fc2e270095f1a3b41348/)
Expand All @@ -15,28 +15,49 @@ Compare to [nouchka/cloudflare-dyndns](https://hub.docker.com/r/nouchka/cloudfla
* Update when IP is changed

## Usage

### Running once interactively

```
docker run \
-e RUNONCE=1 \
-e ZONE=example.com \
-e HOST=example.com \
-e [email protected] \
-e API=1111111111111111 \
-e TTL=1 \
-e PROXY=true \
joshuaavalon/cloudflare-ddns
```

### Running as a daemon

```
docker run \
-d \
-e ZONE=example.com \
-e HOST=example.com \
-e [email protected] \
-e API=1111111111111111 \
-e TTL=1 \
-e PROXY=true \
--name cloudflare \
docker run \
-d \
-e ZONE=example.com \
-e HOST=example.com \
-e [email protected] \
-e API=1111111111111111 \
-e TTL=1 \
-e PROXY=true \
joshuaavalon/cloudflare-ddns
```


## Parameters
`ZONE`: Domain, e.g. example.com. Default: `example.com`
`ZONE`: Domain, e.g. example.com.

`HOST`: DNS record to be updated, e.g. example.com, subdomain.example.com.

`EMAIL`: Cloudflare Email.

`HOST`: DNS record to be updated, e.g. example.com, subdomain.example.com. Default: `example.com`
`API`: Cloudflare API key.

`EMAIL`: Cloudflare Email. Default: `[email protected]`
`TTL`: (OPTIONAL) Time to live for DNS record. Value of 1 is 'automatic'. Min value:120; Max value:2147483647. Default: `1`

`API`: Cloudflare API key. Default: `1111111111111111`
`PROXY`: (OPTIONAL) Whether the record is receiving the performance and security benefits of Cloudflare. `true` to enable; `false` to disable. Default: `true`

`TTL`: Time to live for DNS record. Value of 1 is 'automatic'. Min value:1; Max value:2147483647. Default: `1`
`FORCE_CREATE`: (OPTIONAL) When set, a record will be created if one does not exist already.

`PROXY`: Whether the record is receiving the performance and security benefits of Cloudflare. `true` to enable; `false` to disable. Default: `true`
`RUNONCE`: (OPTIONAL) When set, only a single update is attempted, and the script exists without setting up a cron process.
161 changes: 134 additions & 27 deletions cloudflare.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,50 +1,157 @@
#!/bin/sh
#!/bin/bash

# Enforces required env variables
required_vars=(ZONE HOST EMAIL API)
for required_var in "${required_vars[@]}"; do
if [[ -z ${!required_var} ]]; then
error=1
echo >&2 "Error: $required_var env variable not set."
fi
done

if [[ -n $error ]]; then
exit 1
fi

# PROXY defaults to true
PROXY=${PROXY:-true}

# TTL defaults to 1 (automatic), and is validated
TTL=${TTL:-1}
if [[ $TTL != 1 ]] && [[ $TTL -lt 120 || $TTL -gt 2147483647 ]]; then
echo >&2 "Error: Invalid TTL value $TTL; must be either 1 (automatic) or between 120 and 2147483647 inclusive."
exit 1
fi

echo "Current time: $(date "+%Y-%m-%d %H:%M:%S")"
ip_file="ip"

# Determines the current IP address
new_ip=$(curl -s http://ipecho.net/plain)

# Fallbacks
if [ -z "$new_ip" ]; then
# IP address service fallbacks
if [[ -z $new_ip ]]; then
new_ip=$(curl -s http://whatismyip.akamai.com)
fi
if [ -z "$new_ip" ]; then
if [[ -z $new_ip ]]; then
new_ip=$(curl -s http://icanhazip.com/)
fi
if [ -z "$new_ip" ]; then
if [[ -z $new_ip ]]; then
new_ip=$(curl -s https://tnx.nl/ip)
fi

if [ -z "$new_ip" ]; then
echo "Empty IP !"
exit 0
if [[ -z $new_ip ]]; then
echo >&2 "Error: Unable to reach any service to determine the IP address."
exit 1
fi

if [ -f $ip_file ]; then
ip=$(cat $ip_file)
if [ "$ip" = "$new_ip" ]; then
echo "Same ip: $ip"
exit 0
fi
# Compares with last IP address set, if any
ip_file="ip.dat"
if [[ -f $ip_file ]]; then
ip=$(<$ip_file)
if [[ $ip = "$new_ip" ]]; then
echo "IP is unchanged : $ip. Exiting."
exit 0
fi
fi

ip="$new_ip"
echo "IP: $ip"
zone_id=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$ZONE" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $API" -H "Content-Type: application/json" | grep -Eo '"id":.?"\w*?"' |head -1|grep -o ':.*".*"'|grep -o '\w*')
echo "Zone ID: $zone_id"
record_id=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?name=$HOST" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $API" -H "Content-Type: application/json" | grep -Eo '"id":.?"\w*?"' |head -1|grep -o ':.*".*"'|grep -o '\w*')
echo "Record ID: $record_id"
update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$record_id" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $API" -H "Content-Type: application/json" --data "{\"id\":\"$zone_id\",\"type\":\"A\",\"name\":\"$HOST\",\"content\":\"$ip\",\"ttl\":$TTL,\"proxied\":$PROXY}")

if $DEBUG; then
echo "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$record_id" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $API" -H "Content-Type: application/json" --data "{\"id\":\"$zone_id\",\"type\":\"A\",\"name\":\"$HOST\",\"content\":\"$ip\",\"ttl\":$TTL,\"proxied\":$PROXY}"
# Fetches the zone information for the account
zone_response=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$ZONE" \
-H "X-Auth-Email: $EMAIL" \
-H "X-Auth-Key: $API" \
-H "Content-Type: application/json")


# If not successful, errors out
if [[ $(jq <<<"$zone_response" -r '.success') != "true" ]]; then
messages=$(jq <<<"$zone_response" -r '[.errors[] | .message] |join(" - ")')
echo >&2 "Error: $messages"
exit 1
fi

# Selects the zone id
zone_id=$(jq <<<"$zone_response" -r ".result[0].id")


# If no zone id was found for the account, errors out.
if [[ -z $zone_id ]]; then
echo >&2 "Error: Could not determine Zone ID for $ZONE"
exit 1
fi

echo "Zone $ZONE id : $zone_id"


# Tries to fetch the record of the host
dns_record_response=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?name=$HOST" \
-H "X-Auth-Email: $EMAIL" \
-H "X-Auth-Key: $API" \
-H "Content-Type: application/json")

if [[ $(jq <<<"$dns_record_response" -r '.success') != "true" ]]; then
messages=$(jq <<<"$dns_record_response" -r '[.errors[] | .message] |join(" - ")')
echo >&2 "Error: $messages"
exit 1
fi

# DNS record to add or update
read -r -d '' new_dns_record <<EOF
{
"type": "A",
"name": "$HOST",
"content": "$ip",
"ttl": $TTL,
"priority": 10,
"proxied": $PROXY
}
EOF

# Adds or updates the record
dns_record_id=$(jq <<<"$dns_record_response" -r '.result[] | select(.type =="A") |.id')
if [[ -z $dns_record_id ]]; then

# Makes sure we don't have a CNAME by the same name first
cname_dns_record=$(jq <<<"$dns_record_response" -r '.result[] | select(.type =="CNAME")')
if [[ -n $(jq <<<"$cname_dns_record" -r '.id') ]]; then
dns_record_content=$(jq <<<"$cname_dns_record" -r '.content')
echo >&2 "Error: CNAME entry found for $HOST. Remove it or set HOST=$dns_record_content instead."
exit 1
fi

# If not asked to create a record, stops
if [[ -z $FORCE_CREATE ]]; then
echo >&2 "Error: No existing record. Set FORCE_CREATE to any value to force its creation."
exit 1
fi

# Creates a new record
echo "Creating new record for host $HOST"

dns_record_response=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records" \
-H "X-Auth-Email: $EMAIL" \
-H "X-Auth-Key: $API" \
-H "Content-Type: application/json" \
--data "$new_dns_record")
else
# If a record is found, updates the existing record
echo "Updating record $dns_record_id for host $HOST"

dns_record_response=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$dns_record_id" \
-H "X-Auth-Email: $EMAIL" \
-H "X-Auth-Key: $API" \
-H "Content-Type: application/json" \
--data "$new_dns_record")
fi

if echo "$update" | grep -q "\"success\":true"; then
echo "IP changed to: $ip"
echo "$ip" > $ip_file
if [[ $(jq <<<"$dns_record_response" -r '.success') = "true" ]]; then
# Records the IP set set if successful
echo "IP changed to: $ip"
echo "$ip" > $ip_file
else
printf "Update failed:\\n%s" "$update"
exit 1
# Prints out error messages if unsuccessful
messages=$(jq <<<"$dns_record_response" -r '[.errors[] | .error.message] |join(" - ")')
echo >&2 "Error: $messages"
exit 1
fi
File renamed without changes.

0 comments on commit 4ae7175

Please sign in to comment.