Created
April 1, 2022 15:45
-
-
Save fnando/fda47701c68028fe3b46b38bc49ebabf to your computer and use it in GitHub Desktop.
Dynamic DNS with DNSimple and Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
require "net/http" | |
require "json" | |
module Net | |
def HTTP.patch(url, data, header = nil) | |
start(url.hostname, url.port, use_ssl: url.scheme == "https") do |http| | |
http.patch(url, data, header) | |
end | |
end | |
end | |
class DDNS | |
attr_reader :cache_file, :cache_dir, :dnsimple_api_key, :records | |
def initialize(dnsimple_api_key:, cache_dir:, records:) | |
@dnsimple_api_key = dnsimple_api_key | |
@cache_dir = cache_dir | |
@cache_file = File.join(cache_dir, "ddns.json") | |
@records = records | |
end | |
def cache | |
@cache ||= if File.file?(cache_file) | |
JSON.parse(File.read(cache_file), symbolize_names: true) | |
else | |
{} | |
end | |
end | |
def run | |
loop do | |
system "clear" | |
set_dnsimple_account_id | |
unless cache[:dnsimple_account_id] | |
log("no dnsimple account id, waiting 30s before retrying.") | |
sleep(30) | |
next | |
end | |
check_public_ip_address | |
rescue StandardError => error | |
log("ERROR: #{error.class}: #{error.message}") | |
ensure | |
log("waiting 60s before new check") | |
sleep 60 | |
end | |
end | |
def check_public_ip_address | |
details = JSON.parse(Net::HTTP.get(URI("https://ifconfig.me/all.json"))) | |
ip_address = details.fetch("ip_addr") | |
log("ip address is #{ip_address.inspect}") | |
records.each {|record| set_dns(record, ip_address) } | |
end | |
def set_dns(record, ip_address) | |
entries = dnsimple_get( | |
"#{cache[:dnsimple_account_id]}/zones/#{record[:domain]}/records?type=A" | |
).fetch(:data) | |
entry = entries.find {|e| e[:name] == record[:name].to_s } | |
if entry | |
if entry[:content] == ip_address | |
log("dns record is already pointing to #{ip_address}") | |
else | |
update_dns_record(entry, ip_address) | |
end | |
else | |
create_dns_record(record, ip_address) | |
end | |
end | |
def create_dns_record(record, ip_address) | |
log("creating dns record for #{record[:name]}.#{record[:domain]}") | |
response = dnsimple_post( | |
"#{cache[:dnsimple_account_id]}/zones/#{record[:domain]}/records", | |
name: record[:name] || "", | |
type: "A", | |
content: ip_address, | |
ttl: record[:ttl] || 60 | |
) | |
log("result when creating dns record: #{response.code}") | |
end | |
def update_dns_record(entry, ip_address) | |
log("updating dns record for #{entry[:name]}.#{entry[:zone_id]}") | |
response = dnsimple_patch( | |
"#{cache[:dnsimple_account_id]}/zones/#{entry[:zone_id]}/records/#{entry[:id]}", | |
content: ip_address | |
) | |
log("result when updating dns record: #{response.code}") | |
end | |
def set_dnsimple_account_id | |
return if cache[:dnsimple_account_id] | |
account_id = dnsimple_get("accounts").dig(:data, 0, :id) | |
cache[:dnsimple_account_id] = account_id | |
update_cache(dnsimple_account_id: account_id) | |
end | |
def log(*messages) | |
puts "[#{Time.now}] #{messages.join(' ')}" | |
end | |
def update_cache(**values) | |
@cache = cache.merge(values) | |
File.open(cache_file, "w") do |io| | |
io << JSON.pretty_generate(cache) | |
end | |
end | |
def dnsimple_get(path) | |
url = File.join("https://api.dnsimple.com/v2", path) | |
log("GET #{url}") | |
JSON.parse( | |
Net::HTTP.get( | |
URI(url), | |
"Authorization" => "Bearer #{dnsimple_api_key}" | |
), | |
symbolize_names: true | |
) | |
end | |
def dnsimple_post(path, data) | |
url = File.join("https://api.dnsimple.com/v2", path) | |
log("POST #{url}") | |
Net::HTTP.post( | |
URI(url), | |
JSON.dump(data), | |
"Authorization" => "Bearer #{dnsimple_api_key}", | |
"Content-Type" => "application/json" | |
) | |
end | |
def dnsimple_patch(path, data) | |
url = File.join("https://api.dnsimple.com/v2", path) | |
log("PATCH #{url}") | |
Net::HTTP.patch( | |
URI(url), | |
JSON.dump(data), | |
"Authorization" => "Bearer #{dnsimple_api_key}", | |
"Content-Type" => "application/json" | |
) | |
end | |
end | |
DDNS.new( | |
dnsimple_api_key: "<DNS SIMPLE API KEY>", | |
cache_dir: File.dirname(__FILE__), | |
records: [ | |
# name is the subdomain (optional). | |
{name: "test", domain: "fnando.com"} | |
] | |
).run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment