diff --git a/ddclient.in b/ddclient.in index 7b7359c..750bd94 100755 --- a/ddclient.in +++ b/ddclient.in @@ -723,6 +723,19 @@ my %services = ( 'server' => setv(T_FQDNP, 1, 0, 'domains.google.com', undef), }, }, + 'hetzner' => { + 'updateable' => undef, + 'update' => \&nic_hetzner_update, + 'examples' => \&nic_hetzner_examples, + 'variables' => { + %{$variables{'service-common-defaults'}}, + 'login' => setv(T_LOGIN, 0, 0, 'token', undef), + 'min-interval' => setv(T_DELAY, 0, 0, interval('1m'), 0), + 'server' => setv(T_FQDNP, 1, 0, 'dns.hetzner.com/api/v1', undef), + 'ttl' => setv(T_NUMBER, 0, 0, 60, 60), + 'zone' => setv(T_FQDN, 1, 0, '', undef), + }, + }, 'namecheap' => { 'updateable' => undef, 'update' => \&nic_namecheap_update, @@ -1658,7 +1671,7 @@ sub init_config { $proto = opt('protocol') if !defined($proto); load_sha1_support($proto) if (grep (/^$proto$/, ("freedns", "nfsn"))); - load_json_support($proto) if (grep (/^$proto$/, ("1984", "cloudflare", "gandi", "godaddy", "yandex", "nfsn"))); + load_json_support($proto) if (grep (/^$proto$/, ("1984", "cloudflare", "gandi", "godaddy", "hetzner", "yandex", "nfsn"))); if (!exists($services{$proto})) { warning("skipping host: %s: unrecognized protocol '%s'", $h, $proto); @@ -5872,6 +5885,155 @@ sub nic_cloudflare_update { } } +###################################################################### +## nic_hetzner_examples +## +## written by Joerg Werner +## +###################################################################### +sub nic_hetzner_examples { + return <<"EoEXAMPLE"; +o 'hetzner' + +The 'hetzner' protocol is used by DNS service offered by www.hetzner.com. + +Configuration variables applicable to the 'hetzner' protocol are: + protocol=hetzner ## + server=fqdn.of.service ## can be omitted, defaults to dns.hetzner.com/api/v1 + password=service-password ## API token + fully.qualified.host ## the host registered with the service. + +Example ${program}.conf file entries: + protocol=hetzner, \\ + zone=dns.zone, \\ + password=my-hetzner-api-token \\ + my-toplevel-domain.com,my-other-domain.com +EoEXAMPLE +} +###################################################################### +## nic_hetzner_update +###################################################################### +sub nic_hetzner_update { + debug("\nnic_hetzner_update -------------------"); + + ## group hosts with identical attributes together + my %groups = group_hosts_by([ @_ ], [ qw(ssh login password server wildcard mx backupmx zone) ]); + + ## update each set of hosts that had similar configurations + foreach my $sig (keys %groups) { + my @hosts = @{$groups{$sig}}; + my $hosts = join(',', @hosts); + my $key = $hosts[0]; + + my $headers = "Auth-API-Token: $config{$key}{'password'}\n"; + $headers .= "Content-Type: application/json"; + + # FQDNs + for my $domain (@hosts) { + (my $hostname = $domain) =~ s/\.$config{$key}{zone}$//; + my $ipv4 = delete $config{$domain}{'wantipv4'}; + my $ipv6 = delete $config{$domain}{'wantipv6'}; + + info("getting Hetzner Zone ID for %s", $domain); + + # Get zone ID + my $url = "https://$config{$key}{'server'}/zones?name=" . $config{$key}{'zone'}; + + my $reply = geturl(proxy => opt('proxy'), + url => $url, + headers => $headers + ); + unless ($reply && header_ok($domain, $reply)) { + failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'}); + next; + } + + # Strip header + $reply =~ qr/{(?:[^{}]*|(?R))*}/mp; + my $response = eval {decode_json(${^MATCH})}; + unless ($response && $response->{zones}) { + failed("updating %s: invalid json or result.", $domain); + next; + } + + # Pull the ID out of the json, messy + my ($zone_id) = map {$_->{name} eq $config{$key}{'zone'} ? $_->{id} : ()} @{$response->{zones}}; + unless ($zone_id) { + failed("updating %s: No zone ID found.", $config{$key}{'zone'}); + next; + } + info("Zone ID is %s", $zone_id); + + + # IPv4 and IPv6 handling are similar enough to do in a loop... + foreach my $ip ($ipv4, $ipv6) { + next if (!$ip); + my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; + my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A'; + + info("updating %s: setting IPv$ipv address to %s", $domain, $ip); + $config{$domain}{"status-ipv$ipv"} = 'failed'; + + # Get DNS 'A' or 'AAAA' record ID + $url = "https://$config{$key}{'server'}/records?$zone_id"; + $reply = geturl(proxy => opt('proxy'), + url => $url, + headers => $headers + ); + unless ($reply && header_ok($domain, $reply)) { + failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'}); + next; + } + # Strip header + $reply =~ qr/{(?:[^{}]*|(?R))*}/mp; + $response = eval {decode_json(${^MATCH})}; + unless ($response && $response->{records}) { + failed("updating %s: invalid json or result.", $domain); + next; + } + # Pull the ID out of the json, messy + my ($dns_rec_id) = map { ($_->{name} eq $hostname && $_->{type} eq $type) ? $_->{id} : ()} @{$response->{records}}; + + # Set domain + my $http_method=""; + if ($dns_rec_id) + { + debug("updating %s: DNS '$type' record ID: $dns_rec_id", $domain); + $url = "https://$config{$key}{'server'}/records/$dns_rec_id"; + $http_method = "PUT"; + } else { + debug("creating %s: DNS '$type'", $domain); + $url = "https://$config{$key}{'server'}/records"; + $http_method = "POST"; + } + my $data = "{\"zone_id\":\"$zone_id\", \"name\": \"$hostname\", \"value\": \"$ip\", \"type\": \"$type\", \"ttl\": $config{$domain}{'ttl'}}"; + + $reply = geturl(proxy => opt('proxy'), + url => $url, + headers => $headers, + method => $http_method, + data => $data + ); + unless ($reply && header_ok($domain, $reply)) { + failed("updating %s: Could not connect to %s.", $domain, $config{$domain}{'server'}); + next; + } + # Strip header + $reply =~ qr/{(?:[^{}]*|(?R))*}/mp; + $response = eval {decode_json(${^MATCH})}; + if ($response && $response->{record}) { + success("updating %s: IPv$ipv address set to %s", $domain, $ip); + $config{$domain}{"ipv$ipv"} = $ip; + $config{$domain}{'mtime'} = $now; + $config{$domain}{"status-ipv$ipv"} = 'good'; + } else { + failed("updating %s: invalid json or result.", $domain); + } + } + } + } +} + ###################################################################### ## nic_yandex_examples ######################################################################