diff --git a/.gitignore b/.gitignore index 2eea525..eec8101 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -.env \ No newline at end of file +.env +ip.log + +node_modules diff --git a/.sample.env b/.sample.env index 7cc7ae4..7cc9b7e 100644 --- a/.sample.env +++ b/.sample.env @@ -1,4 +1,9 @@ +# Required EMAIL=my@mail.com KEY=my_api_key ZONE=example.org -DNS_RECORD=some.example.org \ No newline at end of file +DNS_RECORD=some.example.org + +# Optional +#CRON=* * * * * +#RESOLVER=http://ipv4.icanhazip.com/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 235d80e..484c724 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,8 @@ -FROM alpine:3.8 +FROM node:14-alpine -ENV file /usr/local/bin/run.sh +WORKDIR /app -RUN apk add --no-cache --update curl grep bash +ADD ./package.json script.js yarn.lock ./ +RUN yarn -RUN echo '* * * * * ${file}' > /etc/crontabs/root - -COPY ./run.sh ${file} -RUN chmod +x ${file} - -CMD ["crond", "-l2", "-f"] \ No newline at end of file +CMD ["node", "script.js"] \ No newline at end of file diff --git a/README.md b/README.md index 98c4236..ce84985 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Docker DDNS Cloudflare -This container is an adapted version of [this](https://gist.github.com/benkulbertis/fff10759c2391b6618dd) script. It runs once every minute and only makes requests if the IP has changed since last time. The IP is resolved by https://canihazip.com. + +Simple container for setting setting and updating to your local ip address. +Only makes requests if the IP has changed since last time. +By default it runs once every minute and the IP is resolved by https://canihazip.com. ## Quickstart 🚀 @@ -20,7 +23,7 @@ DNS_RECORD=some.example.org docker run -d --name ddns --restart always --env-file .env cupcakearmy/ddns-cloudflare ``` -To check logs: +To check logs: ```bash docker logs ddns @@ -36,3 +39,26 @@ cp .sample.env .env # Edit the .env file with your data docker-compose up -d ``` + +## Customize + +### Custom CRON + +By default the script runs every 5 minutes. You can customize this by simply setting the `CRON` value in the `.env` file. + +```bash +# .env + +# e.g. every minute +CRON=* * * * * +``` + +### Custom Resolver + +By default the script checks the own ip by calling `https://api.ipify.org/`. This also can be configured. It has to be an endpoint that return a plain text containing the ip by get request. + +```bash +# .env + +RESOLVER=https://ipv4.icanhazip.com/ +``` diff --git a/package.json b/package.json new file mode 100644 index 0000000..e7e8520 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "license": "MIT", + "dependencies": { + "axios": "^0.19.2", + "cloudflare": "^2.7.0", + "cron": "^1.8.2", + "dotenv": "^8.2.0" + } +} diff --git a/run.sh b/run.sh deleted file mode 100644 index 2e28583..0000000 --- a/run.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -# CHANGE THESE -auth_email=${EMAIL} -auth_key=${KEY} -zone_name=${ZONE} -record_name=${DNS_RECORD} - -# MAYBE CHANGE THESE -ip=$(curl -s http://ipv4.icanhazip.com) -ip_file="ip.txt" -id_file="cloudflare.ids" -log_file="cloudflare.log" - -# LOGGER -log() { - if [ "$1" ]; then - echo -e "[$(date)] - $1" >> $log_file - fi -} - -# SCRIPT START -log "Check Initiated" - -if [ -f $ip_file ]; then - old_ip=$(cat $ip_file) - if [ $ip == $old_ip ]; then - echo "IP has not changed." - exit 0 - fi -fi - -if [ -f $id_file ] && [[ $(wc -l $id_file | cut -d " " -f 1) == 2 ]]; then - zone_identifier=$(head -1 $id_file) - record_identifier=$(tail -1 $id_file) -else - zone_identifier=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$zone_name" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" | grep -Po '(?<="id":")[^"]*' | head -1 ) - record_identifier=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$record_name" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" | grep -Po '(?<="id":")[^"]*') - echo "$zone_identifier" > $id_file - echo "$record_identifier" >> $id_file -fi - -update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_identifier" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" --data "{\"id\":\"$zone_identifier\",\"type\":\"A\",\"name\":\"$record_name\",\"content\":\"$ip\"}") - -if [[ $update == *"\"success\":false"* ]]; then - message="API UPDATE FAILED. DUMPING RESULTS:\n$update" - log "$message" - echo -e "$message" - exit 1 -else - message="$(date) - IP changed to: $ip" - echo "$ip" > $ip_file - log "$message" - echo "$message" -fi diff --git a/script.js b/script.js new file mode 100644 index 0000000..95453f9 --- /dev/null +++ b/script.js @@ -0,0 +1,69 @@ +const { readFileSync, writeFileSync, existsSync } = require('fs') +const Cloudflare = require('cloudflare') +const Axios = require('axios') +const { CronJob } = require('cron') + +require('dotenv').config() + +const { EMAIL, KEY, ZONE, DNS_RECORD, CRON, RESOLVER } = process.env + +const cf = Cloudflare({ + email: EMAIL, + key: KEY, +}) + +function log(message) { + const timestamp = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '') + console.log(timestamp + '\t' + message) +} + +async function getCurrentIp() { + const { data } = await Axios({ + url: RESOLVER || 'https://api.ipify.org/', + method: 'GET', + }) + return data +} + +async function checkIfUpdateIsRequired() { + const LOG = './ip.log' + const current = await getCurrentIp() + const saved = existsSync(LOG) ? readFileSync(LOG, 'utf-8') : null + + if (current === saved) return false + else { + writeFileSync(LOG, current, { encoding: 'utf-8' }) + return current + } +} + +async function update(newIP) { + const { result: zones } = await cf.zones.browse({ name: ZONE }) + if (!zones.length) throw new Error(`No ZONE "${ZONE}" found`) + + const zoneId = zones[0].id + const { result: records } = await cf.dnsRecords.browse(zoneId, { name: DNS_RECORD }) + + const recordAlreadyExists = records.length + if (recordAlreadyExists) { + const { id: recordId, ...rest } = records[0] + await cf.dnsRecords.edit(zoneId, recordId, { ...rest, content: newIP }) + log(`Updated:\t${DNS_RECORD} → ${newIP} `) + } else { + await cf.dnsRecords.add(zoneId, { + name: DNS_RECORD, + type: 'A', + content: newIP, + }) + log(`Created:\t${DNS_RECORD} → ${newIP} `) + } +} + +async function main() { + const changed = await checkIfUpdateIsRequired() + log(`Running. Update required: ${!!changed}`) + if (changed) await update(changed).catch((e) => console.error(e.message)) +} + +new CronJob(CRON || '*/5 * * * *', main, null, true, null, null, true) +log('Started service.') diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..d933716 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,216 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +autocreate@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/autocreate/-/autocreate-1.2.0.tgz#522167992c4172c15479e5f88f3486a452a40cba" + integrity sha1-UiFnmSxBcsFUeeX4jzSGpFKkDLo= + +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + +capture-stack-trace@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" + integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw== + +cloudflare@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/cloudflare/-/cloudflare-2.7.0.tgz#ff9249a63ff9b662bd863da21e965c8f82f76012" + integrity sha512-yhroBpn2VBczFwiRLpyUc431XiWE+xNs8YvtjAsj1vEA1pVwhpje6BzgLW5iZbulmCuPX48lvX8HizeMWk713g== + dependencies: + autocreate "^1.1.0" + es-class "^2.1.1" + got "^6.3.0" + https-proxy-agent "^2.1.1" + object-assign "^4.1.0" + should-proxy "^1.0.4" + url-pattern "^1.0.3" + +create-error-class@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y= + dependencies: + capture-stack-trace "^1.0.0" + +cron@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/cron/-/cron-1.8.2.tgz#4ac5e3c55ba8c163d84f3407bde94632da8370ce" + integrity sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg== + dependencies: + moment-timezone "^0.5.x" + +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +dotenv@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + +es-class@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/es-class/-/es-class-2.1.1.tgz#6ec2243b5a1e3581c0b7eecee0130c9c0d6fb2b7" + integrity sha1-bsIkO1oeNYHAt+7O4BMMnA1vsrc= + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= + +got@^6.3.0: + version "6.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" + integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA= + dependencies: + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" + url-parse-lax "^1.0.0" + +https-proxy-agent@^2.1.1: + version "2.2.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= + +is-retry-allowed@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" + integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== + +is-stream@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +lowercase-keys@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +moment-timezone@^0.5.x: + version "0.5.31" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" + integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + +safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +should-proxy@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/should-proxy/-/should-proxy-1.0.4.tgz#c805a501abf69539600634809e62fbf238ba35e4" + integrity sha1-yAWlAav2lTlgBjSAnmL78ji6NeQ= + +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= + +unzip-response@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" + integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= + dependencies: + prepend-http "^1.0.1" + +url-pattern@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/url-pattern/-/url-pattern-1.0.3.tgz#0409292471b24f23c50d65a47931793d2b5acfc1" + integrity sha1-BAkpJHGyTyPFDWWkeTF5PStaz8E=