From 611b41e7a696f96955e1c1a88c435b9a10311108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20du=20Bo=C3=BFs?= Date: Tue, 21 Feb 2023 19:33:56 +0100 Subject: [PATCH 1/6] Merge configs for the same hosts instead of using the last one --- ddclient.in | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ddclient.in b/ddclient.in index 530458f..38f68e5 100755 --- a/ddclient.in +++ b/ddclient.in @@ -1639,9 +1639,14 @@ sub _read_config { ## allow {host} to be a comma separated list of hosts foreach my $h (split_by_comma($host)) { - ## save a copy of the current globals - $config{$h} = { %locals }; - $config{$h}{'host'} = $h; + if ($config{$h}) { + ## host already defined, merging configs + $config{$h} = { %{merge($config{$h}, \%locals)} }; + } else { + ## save a copy of the current globals + $config{$h} = { %locals }; + $config{$h}{'host'} = $h; + } } } %passwords = (); From 36de4e0b88969f9fcf8b6bf9c9d3864705a75822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20du=20Bo=C3=BFs?= Date: Tue, 21 Feb 2023 19:45:41 +0100 Subject: [PATCH 2/6] updating changelog --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index f3919f2..2496130 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -25,6 +25,8 @@ repository history](https://github.com/ddclient/ddclient/commits/master). * DynDNS2 now uses the newer ipv4/ipv6 syntax's * The OVH provider now ignores extra data returned + * Allow to define usev4 and usev6 options per hostname + * Merge multiple configs for the same hostname instead of use the last ## 2022-10-20 v3.10.0 From b225d37528ce00f0785f94c72268756c439ec264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20du=20Bo=C3=BFs?= Date: Tue, 21 Feb 2023 21:10:28 +0100 Subject: [PATCH 3/6] Remove README.ssl from makefile as it's deleted on master --- Makefile.am | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index a9543ee..99ab507 100644 --- a/Makefile.am +++ b/Makefile.am @@ -6,7 +6,6 @@ EXTRA_DIST = \ ChangeLog.md \ README.cisco \ README.md \ - README.ssl \ autogen \ sample-ddclient-wrapper.sh \ sample-etc_cron.d_ddclient \ From 07fbbeb5bb5b54797efab34e4f3115fdb487a13b Mon Sep 17 00:00:00 2001 From: Thomas Hebb Date: Sat, 25 Feb 2023 23:28:45 -0500 Subject: [PATCH 4/6] Fix a few whitespace issues and a typo --- ddclient.in | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ddclient.in b/ddclient.in index 530458f..a37bb16 100755 --- a/ddclient.in +++ b/ddclient.in @@ -931,13 +931,13 @@ my %services = ( }, }, 'keysystems' => { - 'updateable' => undef, - 'update' => \&nic_keysystems_update, - 'examples' => \&nic_keysystems_examples, - 'variables' => merge( + 'updateable' => undef, + 'update' => \&nic_keysystems_update, + 'examples' => \&nic_keysystems_examples, + 'variables' => merge( $variables{'keysystems-common-defaults'}, $variables{'service-common-defaults'}, - ), + ), }, 'dnsexit' => { 'updateable' => undef, @@ -962,10 +962,10 @@ my %services = ( 'update' => \&nic_enom_update, 'examples' => \&nic_enom_examples, 'variables' => { - %{$variables{'service-common-defaults'}}, - 'server' => setv(T_FQDNP, 1, 0, 'dynamic.name-services.com', undef), - 'min-interval' => setv(T_DELAY, 0, 0, 0, interval('5m')), - }, + %{$variables{'service-common-defaults'}}, + 'server' => setv(T_FQDNP, 1, 0, 'dynamic.name-services.com', undef), + 'min-interval' => setv(T_DELAY, 0, 0, 0, interval('5m')), + }, }, ); $variables{'merged'} = { @@ -1371,7 +1371,7 @@ sub write_cache { ## merge the updated host entries into the cache. foreach my $h (keys %config) { if (!exists $cache{$h} || $config{$h}{'update'}) { - map { defined($config{$h}{$_}) ? ($cache{$h}{$_} = $config{$h}{$_}) : () } @{$config{$h}{'cacheable'}}; + map { defined($config{$h}{$_}) ? ($cache{$h}{$_} = $config{$h}{$_}) : () } @{$config{$h}{'cacheable'}}; } else { map { $cache{$h}{$_} = $config{$h}{$_} } qw(atime wtime status); } @@ -5703,7 +5703,7 @@ sub nic_changeip_update { } ###################################################################### -## nic_googledomains_examples +## nic_godaddy_examples ## ## written by awalon ## From 4458cceb1b29b4b85fbe4f38f3381a6621048d00 Mon Sep 17 00:00:00 2001 From: Lenard Hess Date: Sun, 26 Feb 2023 11:31:03 +0100 Subject: [PATCH 5/6] Add environment variable to override FRITZ!Box hostname For setups with a different DNS server than the FRITZ!Box, the fritz.box DNS entry may be missing. --- sample-get-ip-from-fritzbox | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sample-get-ip-from-fritzbox b/sample-get-ip-from-fritzbox index 079df07..05c8277 100755 --- a/sample-get-ip-from-fritzbox +++ b/sample-get-ip-from-fritzbox @@ -9,12 +9,13 @@ # # All credits for this one liner go to the author of this blog: # http://scytale.name/blog/2010/01/fritzbox-wan-ip -# As the author explains its not required to tamper with the provided IP for the FritzBox -# as it always binds to that address for UPnP. # Disclaimer: It might be necessary to make the script executable +# Set default hostname to connect to the FritzBox +: ${FRITZ_BOX_HOSTNAME:=fritz.box} + curl -s -H 'Content-Type: text/xml; charset="utf-8"' \ -H 'SOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress' \ -d ' ' \ - 'http://fritz.box:49000/igdupnp/control/WANIPConn1' | \ + "http://$FRITZ_BOX_HOSTNAME:49000/igdupnp/control/WANIPConn1" | \ grep -Eo '\<[[:digit:]]{1,3}(\.[[:digit:]]{1,3}){3}\>' From c436a34ede5b0d39133071c8d1a0144d6c0d6c41 Mon Sep 17 00:00:00 2001 From: Thomas Hebb Date: Sat, 25 Feb 2023 23:31:41 -0500 Subject: [PATCH 6/6] Add support for Digital Ocean --- ddclient.conf.in | 8 +++ ddclient.in | 130 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/ddclient.conf.in b/ddclient.conf.in index 410b356..6a7a205 100644 --- a/ddclient.conf.in +++ b/ddclient.conf.in @@ -347,3 +347,11 @@ ssl=yes # use ssl-support. Works with # login=domain.name, # password=domain-password # my-domain.com + +## +## DigitalOcean (www.digitalocean.com) +## +#protocol=digitalocean, \ +#zone=example.com, \ +#password=api-token \ +#example.com,sub.example.com diff --git a/ddclient.in b/ddclient.in index fb2dfec..a4f4c12 100755 --- a/ddclient.in +++ b/ddclient.in @@ -605,6 +605,17 @@ my %services = ( 'password' => setv(T_STRING, 0, 0, 'unused', undef), }, }, + 'digitalocean' => { + 'updateable' => undef, + 'update' => \&nic_digitalocean_update, + 'examples' => \&nic_digitalocean_examples, + 'variables' => { + %{$variables{'service-common-defaults'}}, + 'server' => setv(T_FQDNP, 1, 0, 'api.digitalocean.com', undef), + 'zone' => setv(T_FQDN, 1, 0, '', undef), + 'login' => setv(T_LOGIN, 0, 0, 'unused', undef), + }, + }, 'dinahosting' => { 'updateable' => undef, 'update' => \&nic_dinahosting_update, @@ -1795,7 +1806,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", "hetzner", "yandex", "nfsn", "njalla", "porkbun"))); + load_json_support($proto) if (grep (/^$proto$/, ("1984", "cloudflare", "digitalocean", "gandi", "godaddy", "hetzner", "yandex", "nfsn", "njalla", "porkbun"))); if (!exists($services{$proto})) { warning("skipping host: %s: unrecognized protocol '%s'", $h, $proto); @@ -7839,6 +7850,123 @@ sub nic_enom_update { } } +sub nic_digitalocean_examples { + return <<"EoEXAMPLE"; +o 'digitalocean' + +The 'digitalocean' protocol updates domains hosted by Digital Ocean (https://www.digitalocean.com/). + +This protocol supports both IPv4 and IPv6. It will only update an existing record; it will not +create a new one. So, before using it, make sure there's already one (and at most one) of each +record type (A and/or AAAA) you plan to update present in your Digital Ocean zone. + +This protocol implements the API documented here: + https://docs.digitalocean.com/reference/api/api-reference/. + +You can get your API token by following these instructions: + https://docs.digitalocean.com/reference/api/create-personal-access-token/ + +Available configuration variables: + * server (optional): API server. Defaults to 'api.digitalocean.com'. + * zone (required): DNS zone under which the hostname falls. + * password (required): API token from DigitalOcean Control Panel. See instructions linked above. + +Example ${program}.conf file entries: + protocol=digitalocean, \\ + zone=example.com, \\ + password=api-token \\ + example.com,sub.example.com +EoEXAMPLE +} + +sub nic_digitalocean_update_one { + my ($h, $ip, $ipv) = @_; + + info("setting %s address to %s for %s", $ipv, $ip, $h); + + my $server = $config{$h}{'server'}; + my $type = $ipv eq 'ipv6' ? 'AAAA' : 'A'; + + my $headers; + $headers = "Content-Type: application/json\n"; + $headers .= "Authorization: Bearer $config{$h}{'password'}\n"; + + my $list_url; + $list_url = "https://$server/v2/domains/$config{$h}{'zone'}/records"; + $list_url .= "?name=$h"; + $list_url .= "&type=$type"; + + my $list_resp = geturl( + proxy => opt('proxy'), + url => $list_url, + headers => $headers, + ); + unless ($list_resp && header_ok($h, $list_resp)) { + $config{$h}{"status-$ipv"} = 'failed'; + failed("listing %s %s: Failed connection or bad response from %s.", $h, $ipv, $server); + return; + } + $list_resp =~ s/^.*?\n\n//s; # Strip header + + my $list = eval { decode_json($list_resp) }; + if ($@) { + $config{$h}{"status-$ipv"} = 'failed'; + failed("listing %s %s: JSON decoding failure", $h, $ipv); + return; + } + + my $elem = $list; + unless ((ref($elem) eq 'HASH') && + (ref ($elem = $elem->{'domain_records'}) eq 'ARRAY') && + (@$elem == 1 && ref ($elem = $elem->[0]) eq 'HASH')) { + $config{$h}{"status-$ipv"} = 'failed'; + failed("listing %s %s: no record, multiple records, or malformed JSON", $h, $ipv); + return; + } + + my $current_ip = $elem->{'data'}; + my $record_id = $elem->{'id'}; + + if ($current_ip eq $ip) { + info("updating %s %s: IP is already %s, no update needed.", $h, $ipv, $ip); + } else { + my $update_data = encode_json({'type' => $type, 'data' => $ip}); + my $update_resp = geturl( + proxy => opt('proxy'), + url => "https://$server/v2/domains/$config{$h}{'zone'}/records/$record_id", + method => 'PATCH', + headers => $headers, + data => $update_data, + ); + unless ($update_resp && header_ok($h, $update_resp)) { + $config{$h}{"status-$ipv"} = 'failed'; + failed("updating %s %s: Failed connection or bad response from %s.", $h, $ipv, $server); + return; + } + } + + $config{$h}{"status-$ipv"} = 'good'; + $config{$h}{"ip-$ipv"} = $ip; + $config{$h}{"mtime"} = $now; +} + +sub nic_digitalocean_update { + debug("\nnic_digitalocean_update -------------------"); + + foreach my $h (@_) { + my $ipv4 = delete $config{$h}{'wantipv4'}; + my $ipv6 = delete $config{$h}{'wantipv6'}; + + if ($ipv4) { + nic_digitalocean_update_one($h, $ipv4, 'ipv4'); + } + + if ($ipv6) { + nic_digitalocean_update_one($h, $ipv6, 'ipv6'); + } + } +} + # Execute main() if this file is run as a script or run via PAR (https://metacpan.org/pod/PAR), # otherwise do nothing. This "modulino" pattern makes it possible to import this file as a module # and test its functions directly; there's no need for test-only command-line arguments or stdout