#!/usr/bin/env ruby # # Original, including IsoLinearCHiP's improvements: https://gist.github.com/IsoLinearCHiP/d6bc594143102e6acd131fc65c080f64 # Current version, adapted to Hetzner phasing out dns.h.d for console.h.d: API="https://api.hetzner.cloud/v1" require 'json' require 'socket' require 'net/http' require 'fileutils' require 'pstore' FileUtils.mkdir_p "#{Dir.home}/.local/share/update-ddns" @pstore = PStore.new "#{Dir.home}/.local/share/update-ddns/cache.pstore" def api(path, records=nil) cmd = if records "curl -s -X POST -H 'Authorization: Bearer #{@token}' -H 'Content-Type: application/json' -d '{\"records\": #{records.to_json}}' #{API}#{path}" else "curl -s -H 'Authorization: Bearer #{@token}' #{API}#{path}" end json = JSON.parse `#{cmd}` if json['error'] warn "Error: #{json['error']['message']}" exit! end json end def cache(key, value=nil) if value @pstore.transaction{@pstore[key] = value} end @pstore.transaction(true){return @pstore[key]} end def current_public_ip Socket.getifaddrs .map{|iface| [iface.name, iface.addr.ip_address] if iface.addr.ipv4? if iface.addr} .compact .find{|x| x[0]=='ppp0'}[1] end def query @token = cache('api_token') zone = ARGV[0] rrset_name = ARGV[1] zone_id = cache('zone_id') zone_id ||= cache('zone_id', api('/zones').find{|x| x['name'] == @zone}['id']) public_ip = cache('current_public_ip', current_public_ip) puts "Current WAN IP: #{public_ip}" ips_on_record = api("/zones/#{zone_id}/rrsets/#{rrset_name}/A")['rrset']['records'].collect{|x| x['value']} if ips_on_record.last != public_ip api("/zones/#{zone_id}/rrsets/#{rrset_name}/A/actions/set_records", [{value: public_ip, comment: 'dynamically updated'}]) puts "record updated" else puts "no update necessary" end end case ARGV.size when 1 cache('api_token', ARGV[0]) puts "API token set" when 2 (warn "Error: API token not set"; exit!) unless cache('api_token') query else warn "Usage: update-ddns | update-ddns " exit! end