From 0892655fd69b9790a669b2c1b46b6f9d9b7dda73 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 18 Jul 2024 04:45:34 -0400 Subject: [PATCH 01/24] dyndns2: Add response handling TODO comments --- ddclient.in | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ddclient.in b/ddclient.in index c13e355..dd9c60c 100755 --- a/ddclient.in +++ b/ddclient.in @@ -4111,6 +4111,21 @@ sub nic_dyndns2_update { next if !header_ok($hosts, $reply); my @reply = split /\n/, $reply; my $state = 'header'; + # From : + # + # If updating multiple hostnames, hostname-specific return codes are given one per line, + # in the same order as the hostnames were specified. Return codes indicating a failure + # with the account or the system are given only once. + # + # TODO: There is no mention of what happens if multiple IP addresses are supplied (e.g., + # IPv4 and IPv6) for a host. If one address fails to update and the other doesn't, is that + # one error status line? An error status line and a success status line? Or is an update + # considered to be all-or-nothing and the status applies to the operation as a whole? If + # the IPv4 address changes but not the IPv6 address does that result in a status of "good" + # because the set of addresses for a host changed even if a subset did not? + # + # TODO: The logic below applies the last line's status to all hosts. Change it to apply + # each status to its corresponding host. for my $line (@reply) { if ($state eq 'header') { $state = 'body'; @@ -4147,6 +4162,10 @@ sub nic_dyndns2_update { failed("updating %s: %s: %s", $hosts, $status, $errors{$status}); } } elsif ($status =~ /w(\d+)(.)/) { + # TODO: does not mention + # anything about wait statuses. Is this obsolete (this code has been here + # since at least 2006)? Or does a different DynDNS-like service emit wait + # lines? my ($wait, $units) = ($1, lc $2); my ($sec, $scale) = ($wait, 1); ($scale, $units) = (1, 'seconds') if $units eq 's'; From db3472a7ce01253a2418cb87045be8a93da9e9e8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 13 Jul 2024 04:01:22 -0400 Subject: [PATCH 02/24] dyndns2: Simplify response parsing --- ddclient.in | 91 ++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/ddclient.in b/ddclient.in index dd9c60c..23d8002 100755 --- a/ddclient.in +++ b/ddclient.in @@ -4109,8 +4109,14 @@ sub nic_dyndns2_update { next; } next if !header_ok($hosts, $reply); - my @reply = split /\n/, $reply; - my $state = 'header'; + # Some services can return 200 OK even if there is an error (e.g., bad authentication, + # updates too frequent) so the body of the response must also be checked. + (my $body = $reply) =~ s/^.*?\n\n//s; + my @reply = split(qr/\n/, $body); + if (!@reply) { + failed("updating %s: Could not connect to %s.", $hosts, $groupcfg{'server'}); + next; + } # From : # # If updating multiple hostnames, hostname-specific return codes are given one per line, @@ -4127,62 +4133,53 @@ sub nic_dyndns2_update { # TODO: The logic below applies the last line's status to all hosts. Change it to apply # each status to its corresponding host. for my $line (@reply) { - if ($state eq 'header') { - $state = 'body'; - } elsif ($state eq 'body') { - $state = 'results' if $line eq ''; - } elsif ($state =~ /^results/) { - $state = 'results2'; - # bug #10: some dyndns providers does not return the IP so - # we can't use the returned IP - my ($status, $returnedips) = split / /, lc $line; + # bug #10: some dyndns providers does not return the IP so + # we can't use the returned IP + my ($status, $returnedips) = split / /, lc $line; + for my $h (@hosts) { + $config{$h}{'status-ipv4'} = $status if $ipv4; + $config{$h}{'status-ipv6'} = $status if $ipv6; + } + if ($status eq 'good') { for my $h (@hosts) { - $config{$h}{'status-ipv4'} = $status if $ipv4; - $config{$h}{'status-ipv6'} = $status if $ipv6; + $config{$h}{'ipv4'} = $ipv4 if $ipv4; + $config{$h}{'ipv6'} = $ipv6 if $ipv6; + $config{$h}{'mtime'} = $now; } - if ($status eq 'good') { + success("updating %s: %s: IPv4 address set to %s", $hosts, $status, $ipv4) if $ipv4; + success("updating %s: %s: IPv6 address set to %s", $hosts, $status, $ipv6) if $ipv6; + } elsif (exists $errors{$status}) { + if ($status eq 'nochg') { + warning("updating %s: %s: %s", $hosts, $status, $errors{$status}); for my $h (@hosts) { $config{$h}{'ipv4'} = $ipv4 if $ipv4; $config{$h}{'ipv6'} = $ipv6 if $ipv6; $config{$h}{'mtime'} = $now; + $config{$h}{'status-ipv4'} = 'good' if $ipv4; + $config{$h}{'status-ipv6'} = 'good' if $ipv6; } - success("updating %s: %s: IPv4 address set to %s", $hosts, $status, $ipv4) if $ipv4; - success("updating %s: %s: IPv6 address set to %s", $hosts, $status, $ipv6) if $ipv6; - } elsif (exists $errors{$status}) { - if ($status eq 'nochg') { - warning("updating %s: %s: %s", $hosts, $status, $errors{$status}); - for my $h (@hosts) { - $config{$h}{'ipv4'} = $ipv4 if $ipv4; - $config{$h}{'ipv6'} = $ipv6 if $ipv6; - $config{$h}{'mtime'} = $now; - $config{$h}{'status-ipv4'} = 'good' if $ipv4; - $config{$h}{'status-ipv6'} = 'good' if $ipv6; - } - } else { - failed("updating %s: %s: %s", $hosts, $status, $errors{$status}); - } - } elsif ($status =~ /w(\d+)(.)/) { - # TODO: does not mention - # anything about wait statuses. Is this obsolete (this code has been here - # since at least 2006)? Or does a different DynDNS-like service emit wait - # lines? - my ($wait, $units) = ($1, lc $2); - my ($sec, $scale) = ($wait, 1); - ($scale, $units) = (1, 'seconds') if $units eq 's'; - ($scale, $units) = (60, 'minutes') if $units eq 'm'; - ($scale, $units) = (60*60, 'hours') if $units eq 'h'; - $sec = $wait * $scale; - for my $h (@hosts) { - $config{$h}{'wtime'} = $now + $sec; - } - warning("updating %s: %s: wait %s %s before further updates", $hosts, $status, $wait, $units); } else { - failed("updating %s: unexpected status (%s)", $hosts, $line); + failed("updating %s: %s: %s", $hosts, $status, $errors{$status}); } + } elsif ($status =~ /w(\d+)(.)/) { + # TODO: does not mention + # anything about wait statuses. Is this obsolete (this code has been here + # since at least 2006)? Or does a different DynDNS-like service emit wait + # lines? + my ($wait, $units) = ($1, lc $2); + my ($sec, $scale) = ($wait, 1); + ($scale, $units) = (1, 'seconds') if $units eq 's'; + ($scale, $units) = (60, 'minutes') if $units eq 'm'; + ($scale, $units) = (60*60, 'hours') if $units eq 'h'; + $sec = $wait * $scale; + for my $h (@hosts) { + $config{$h}{'wtime'} = $now + $sec; + } + warning("updating %s: %s: wait %s %s before further updates", $hosts, $status, $wait, $units); + } else { + failed("updating %s: unexpected status (%s)", $hosts, $line); } } - failed("updating %s: Could not connect to %s.", $hosts, $groupcfg{'server'}) - if $state ne 'results2'; } } From 8a667e3f57ac5fef7f64eae8e416afcb05e14d8d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 13 Jul 2024 04:23:27 -0400 Subject: [PATCH 03/24] dyndns2: Treat `nochg` as `good` to eliminate duplicate code --- ddclient.in | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/ddclient.in b/ddclient.in index 23d8002..d602229 100755 --- a/ddclient.in +++ b/ddclient.in @@ -4136,6 +4136,10 @@ sub nic_dyndns2_update { # bug #10: some dyndns providers does not return the IP so # we can't use the returned IP my ($status, $returnedips) = split / /, lc $line; + if ($status eq 'nochg') { + warning("updating %s: %s: %s", $hosts, $status, $errors{$status}); + $status = 'good'; + } for my $h (@hosts) { $config{$h}{'status-ipv4'} = $status if $ipv4; $config{$h}{'status-ipv6'} = $status if $ipv6; @@ -4149,18 +4153,7 @@ sub nic_dyndns2_update { success("updating %s: %s: IPv4 address set to %s", $hosts, $status, $ipv4) if $ipv4; success("updating %s: %s: IPv6 address set to %s", $hosts, $status, $ipv6) if $ipv6; } elsif (exists $errors{$status}) { - if ($status eq 'nochg') { - warning("updating %s: %s: %s", $hosts, $status, $errors{$status}); - for my $h (@hosts) { - $config{$h}{'ipv4'} = $ipv4 if $ipv4; - $config{$h}{'ipv6'} = $ipv6 if $ipv6; - $config{$h}{'mtime'} = $now; - $config{$h}{'status-ipv4'} = 'good' if $ipv4; - $config{$h}{'status-ipv6'} = 'good' if $ipv6; - } - } else { - failed("updating %s: %s: %s", $hosts, $status, $errors{$status}); - } + failed("updating %s: %s: %s", $hosts, $status, $errors{$status}); } elsif ($status =~ /w(\d+)(.)/) { # TODO: does not mention # anything about wait statuses. Is this obsolete (this code has been here From 88f140d470f2ee661ae4c25f5caab150ea6c49ee Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 18 Jul 2024 05:03:18 -0400 Subject: [PATCH 04/24] dyndns2: Invert condition to improve readability --- ddclient.in | 54 +++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/ddclient.in b/ddclient.in index d602229..4f4bcbe 100755 --- a/ddclient.in +++ b/ddclient.in @@ -4144,34 +4144,36 @@ sub nic_dyndns2_update { $config{$h}{'status-ipv4'} = $status if $ipv4; $config{$h}{'status-ipv6'} = $status if $ipv6; } - if ($status eq 'good') { - for my $h (@hosts) { - $config{$h}{'ipv4'} = $ipv4 if $ipv4; - $config{$h}{'ipv6'} = $ipv6 if $ipv6; - $config{$h}{'mtime'} = $now; + if ($status ne 'good') { + if (exists($errors{$status})) { + failed("updating %s: %s: %s", $hosts, $status, $errors{$status}); + } elsif ($status =~ qr/w(\d+)(.)/) { + # TODO: does not mention + # anything about wait statuses. Is this obsolete (this code has been here + # since at least 2006)? Or does a different DynDNS-like service emit wait + # lines? + my ($wait, $units) = ($1, lc $2); + my ($sec, $scale) = ($wait, 1); + ($scale, $units) = (1, 'seconds') if $units eq 's'; + ($scale, $units) = (60, 'minutes') if $units eq 'm'; + ($scale, $units) = (60*60, 'hours') if $units eq 'h'; + $sec = $wait * $scale; + for my $h (@hosts) { + $config{$h}{'wtime'} = $now + $sec; + } + warning("updating %s: %s: wait %s %s before further updates", $hosts, $status, $wait, $units); + } else { + failed("updating %s: unexpected status (%s)", $hosts, $line); } - success("updating %s: %s: IPv4 address set to %s", $hosts, $status, $ipv4) if $ipv4; - success("updating %s: %s: IPv6 address set to %s", $hosts, $status, $ipv6) if $ipv6; - } elsif (exists $errors{$status}) { - failed("updating %s: %s: %s", $hosts, $status, $errors{$status}); - } elsif ($status =~ /w(\d+)(.)/) { - # TODO: does not mention - # anything about wait statuses. Is this obsolete (this code has been here - # since at least 2006)? Or does a different DynDNS-like service emit wait - # lines? - my ($wait, $units) = ($1, lc $2); - my ($sec, $scale) = ($wait, 1); - ($scale, $units) = (1, 'seconds') if $units eq 's'; - ($scale, $units) = (60, 'minutes') if $units eq 'm'; - ($scale, $units) = (60*60, 'hours') if $units eq 'h'; - $sec = $wait * $scale; - for my $h (@hosts) { - $config{$h}{'wtime'} = $now + $sec; - } - warning("updating %s: %s: wait %s %s before further updates", $hosts, $status, $wait, $units); - } else { - failed("updating %s: unexpected status (%s)", $hosts, $line); + next; } + for my $h (@hosts) { + $config{$h}{'ipv4'} = $ipv4 if $ipv4; + $config{$h}{'ipv6'} = $ipv6 if $ipv6; + $config{$h}{'mtime'} = $now; + } + success("updating %s: %s: IPv4 address set to %s", $hosts, $status, $ipv4) if $ipv4; + success("updating %s: %s: IPv6 address set to %s", $hosts, $status, $ipv6) if $ipv6; } } } From 90de2f9606b4a353d090a6824700c61ebcd84403 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 13 Jul 2024 04:33:44 -0400 Subject: [PATCH 05/24] dyndns2: Improve readability of status parsing --- ddclient.in | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ddclient.in b/ddclient.in index 4f4bcbe..bca675a 100755 --- a/ddclient.in +++ b/ddclient.in @@ -4133,9 +4133,12 @@ sub nic_dyndns2_update { # TODO: The logic below applies the last line's status to all hosts. Change it to apply # each status to its corresponding host. for my $line (@reply) { - # bug #10: some dyndns providers does not return the IP so - # we can't use the returned IP - my ($status, $returnedips) = split / /, lc $line; + # The IP address normally comes after the status, but we ignore it. We could compare + # it with the expected address and mark the update as failed if it differs, but (1) + # some services do not return the IP; and (2) comparison is brittle (e.g., + # 192.000.002.001 vs. 192.0.2.1) and false errors could cause high load on the service + # (an update attempt every min-error-interval instead of every max-interval). + (my $status = $line) =~ s/ .*$//; if ($status eq 'nochg') { warning("updating %s: %s: %s", $hosts, $status, $errors{$status}); $status = 'good'; From 1e73f4a51a92e17b9a43bd5f3ad35f0dd94bb840 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 13 Jul 2024 04:52:05 -0400 Subject: [PATCH 06/24] dyndns2: Wrap long list of group by attributes --- ddclient.in | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ddclient.in b/ddclient.in index bca675a..508a3f7 100755 --- a/ddclient.in +++ b/ddclient.in @@ -4050,7 +4050,6 @@ EoEXAMPLE ###################################################################### sub nic_dyndns2_update { debug("\nnic_dyndns2_update -------------------"); - my @groups = group_hosts_by(\@_, qw(login password server script static custom wildcard mx backupmx wantipv4 wantipv6)); my %errors = ( 'badauth' => 'Bad authorization (username or password)', 'badsys' => 'The system parameter given was not valid', @@ -4064,7 +4063,20 @@ sub nic_dyndns2_update { 'dnserr' => 'System error: DNS error encountered. Contact support@dyndns.org', 'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive', ); - for my $group (@groups) { + my @group_by_attrs = qw( + backupmx + custom + login + mx + password + script + server + static + wantipv4 + wantipv6 + wildcard + ); + for my $group (group_hosts_by(\@_, @group_by_attrs)) { my @hosts = @{$group->{hosts}}; my %groupcfg = %{$group->{cfg}}; my $hosts = join(',', @hosts); From adfd68d5e0e868329ef4aef8ed12f8a944457964 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 18 Jul 2024 05:19:58 -0400 Subject: [PATCH 07/24] dyndns2: Refine log messages --- ddclient.in | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/ddclient.in b/ddclient.in index 508a3f7..e5292f5 100755 --- a/ddclient.in +++ b/ddclient.in @@ -4084,12 +4084,11 @@ sub nic_dyndns2_update { my $ipv6 = $groupcfg{'wantipv6'}; delete $config{$_}{'wantipv4'} for @hosts; delete $config{$_}{'wantipv6'} for @hosts; - info("setting IPv4 address to %s for %s", $ipv4, $hosts) if $ipv4; - info("setting IPv6 address to %s for %s", $ipv6, $hosts) if $ipv6; - verbose("UPDATE:", "updating %s", $hosts); + info("$hosts: setting IPv4 address to $ipv4") if $ipv4; + info("$hosts: setting IPv6 address to $ipv6") if $ipv6; my $url = "$groupcfg{'server'}$groupcfg{'script'}?system="; if ($groupcfg{'custom'}) { - warning("updating %s: 'custom' and 'static' may not be used together. ('static' ignored)", $hosts) + warning("$hosts: 'custom' and 'static' may not be used together ('static' ignored)") if $groupcfg{'static'}; $url .= 'custom'; } elsif ($groupcfg{'static'}) { @@ -4117,7 +4116,7 @@ sub nic_dyndns2_update { password => $groupcfg{'password'}, ) // ''; if ($reply eq '') { - failed("updating %s: Could not connect to %s.", $hosts, $groupcfg{'server'}); + failed("$hosts: Could not connect to $groupcfg{'server'}"); next; } next if !header_ok($hosts, $reply); @@ -4126,7 +4125,7 @@ sub nic_dyndns2_update { (my $body = $reply) =~ s/^.*?\n\n//s; my @reply = split(qr/\n/, $body); if (!@reply) { - failed("updating %s: Could not connect to %s.", $hosts, $groupcfg{'server'}); + failed("$hosts: Could not connect to $groupcfg{'server'}"); next; } # From : @@ -4152,7 +4151,7 @@ sub nic_dyndns2_update { # (an update attempt every min-error-interval instead of every max-interval). (my $status = $line) =~ s/ .*$//; if ($status eq 'nochg') { - warning("updating %s: %s: %s", $hosts, $status, $errors{$status}); + warning("$hosts: $status: $errors{$status}"); $status = 'good'; } for my $h (@hosts) { @@ -4161,7 +4160,7 @@ sub nic_dyndns2_update { } if ($status ne 'good') { if (exists($errors{$status})) { - failed("updating %s: %s: %s", $hosts, $status, $errors{$status}); + failed("$hosts: $status: $errors{$status}"); } elsif ($status =~ qr/w(\d+)(.)/) { # TODO: does not mention # anything about wait statuses. Is this obsolete (this code has been here @@ -4176,9 +4175,9 @@ sub nic_dyndns2_update { for my $h (@hosts) { $config{$h}{'wtime'} = $now + $sec; } - warning("updating %s: %s: wait %s %s before further updates", $hosts, $status, $wait, $units); + warning("$hosts: wait $wait $units before further updates"); } else { - failed("updating %s: unexpected status (%s)", $hosts, $line); + failed("$hosts: unexpected status: $line"); } next; } @@ -4187,8 +4186,8 @@ sub nic_dyndns2_update { $config{$h}{'ipv6'} = $ipv6 if $ipv6; $config{$h}{'mtime'} = $now; } - success("updating %s: %s: IPv4 address set to %s", $hosts, $status, $ipv4) if $ipv4; - success("updating %s: %s: IPv6 address set to %s", $hosts, $status, $ipv6) if $ipv6; + success("$hosts: IPv4 address set to $ipv4") if $ipv4; + success("$hosts: IPv6 address set to $ipv6") if $ipv6; } } } From 26f57bf36aa46f5863610322be9183bd2b83d03a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 18 Jul 2024 05:25:05 -0400 Subject: [PATCH 08/24] dyndns2: Delete obsolete(?) "wait" response handling --- ChangeLog.md | 4 ++++ ddclient.in | 15 --------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 19633f3..b531974 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -34,6 +34,10 @@ repository history](https://github.com/ddclient/ddclient/commits/master). * Deprecated built-in web IP discovery services are not listed in the output of `--list-web-services`. [#682](https://github.com/ddclient/ddclient/pull/682) + * `dyndns2`: Support for "wait" response lines has been removed. The Dyn + documentation does not mention such responses, and the code to handle them, + untouched since at least 2006, is believed to be obsolete. + [#709](https://github.com/ddclient/ddclient/pull/709) ### New features diff --git a/ddclient.in b/ddclient.in index e5292f5..d60f87d 100755 --- a/ddclient.in +++ b/ddclient.in @@ -4161,21 +4161,6 @@ sub nic_dyndns2_update { if ($status ne 'good') { if (exists($errors{$status})) { failed("$hosts: $status: $errors{$status}"); - } elsif ($status =~ qr/w(\d+)(.)/) { - # TODO: does not mention - # anything about wait statuses. Is this obsolete (this code has been here - # since at least 2006)? Or does a different DynDNS-like service emit wait - # lines? - my ($wait, $units) = ($1, lc $2); - my ($sec, $scale) = ($wait, 1); - ($scale, $units) = (1, 'seconds') if $units eq 's'; - ($scale, $units) = (60, 'minutes') if $units eq 'm'; - ($scale, $units) = (60*60, 'hours') if $units eq 'h'; - $sec = $wait * $scale; - for my $h (@hosts) { - $config{$h}{'wtime'} = $now + $sec; - } - warning("$hosts: wait $wait $units before further updates"); } else { failed("$hosts: unexpected status: $line"); } From 30a7c5ad78e9697526e74f28eea0885b33e93782 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 19 Jul 2024 01:47:27 -0400 Subject: [PATCH 09/24] dyndns2: Delete obsolete `custom` and `static` options says: > We will accept these parameters without generating error messages: > > * `system`, previously used to identify update type --- ChangeLog.md | 3 +++ ddclient.conf.in | 20 -------------------- ddclient.in | 20 ++------------------ 3 files changed, 5 insertions(+), 38 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index b531974..f703f3a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -38,6 +38,9 @@ repository history](https://github.com/ddclient/ddclient/commits/master). documentation does not mention such responses, and the code to handle them, untouched since at least 2006, is believed to be obsolete. [#709](https://github.com/ddclient/ddclient/pull/709) + * `dyndns2`: The obsolete `static` and `custom` options have been removed. + Setting the options may produce a warning. + [#709](https://github.com/ddclient/ddclient/pull/709) ### New features diff --git a/ddclient.conf.in b/ddclient.conf.in index 8a336ac..1c7ae35 100644 --- a/ddclient.conf.in +++ b/ddclient.conf.in @@ -81,26 +81,6 @@ pid=@runstatedir@/ddclient.pid # record PID in file. # protocol=dyndns2 \ # your-dynamic-host.dyndns.org -## -## dyndns.org static addresses -## -## (supports variables: wildcard,mx,backupmx) -## -# static=yes, \ -# server=members.dyndns.org, \ -# protocol=dyndns2 \ -# your-static-host.dyndns.org - -## -## dyndns.org custom addresses -## -## (supports variables: wildcard,mx,backupmx) -## -# custom=yes, \ -# server=members.dyndns.org, \ -# protocol=dyndns2 \ -# your-domain.top-level,your-other-domain.top-level - ## ## ZoneEdit (zoneedit.com) ## diff --git a/ddclient.in b/ddclient.in index d60f87d..332f086 100755 --- a/ddclient.in +++ b/ddclient.in @@ -709,7 +709,6 @@ our %variables = ( 'dyndns-common-defaults' => { 'backupmx' => setv(T_BOOL, 0, 1, 0, undef), 'mx' => setv(T_OFQDN, 0, 1, undef, undef), - 'static' => setv(T_BOOL, 0, 1, 0, undef), 'wildcard' => setv(T_BOOL, 0, 1, 0, undef), }, ); @@ -848,6 +847,7 @@ our %protocols = ( 'variables' => { %{$variables{'protocol-common-defaults'}}, %{$variables{'dyndns-common-defaults'}}, + 'static' => setv(T_BOOL, 0, 1, 0, undef), }, }, 'dyndns2' => { @@ -857,7 +857,6 @@ our %protocols = ( 'variables' => { %{$variables{'protocol-common-defaults'}}, %{$variables{'dyndns-common-defaults'}}, - 'custom' => setv(T_BOOL, 0, 1, 0, undef), 'script' => setv(T_STRING, 0, 1, '/nic/update', undef), }, }, @@ -4015,8 +4014,6 @@ Configuration variables applicable to the 'dyndns2' protocol are: server=fqdn.of.service ## defaults to members.dyndns.org script=/path/to/script ## defaults to /nic/update backupmx=no|yes ## indicates that this host is the primary MX for the domain. - static=no|yes ## indicates that this host has a static IP address. - custom=no|yes ## indicates that this host is a 'custom' top-level domain name. mx=any.host.domain ## a host MX'ing for this host definition. wildcard=no|yes ## add a DNS wildcard CNAME record that points to login=service-login ## login name and password registered with the service @@ -4065,13 +4062,11 @@ sub nic_dyndns2_update { ); my @group_by_attrs = qw( backupmx - custom login mx password script server - static wantipv4 wantipv6 wildcard @@ -4086,18 +4081,7 @@ sub nic_dyndns2_update { delete $config{$_}{'wantipv6'} for @hosts; info("$hosts: setting IPv4 address to $ipv4") if $ipv4; info("$hosts: setting IPv6 address to $ipv6") if $ipv6; - my $url = "$groupcfg{'server'}$groupcfg{'script'}?system="; - if ($groupcfg{'custom'}) { - warning("$hosts: 'custom' and 'static' may not be used together ('static' ignored)") - if $groupcfg{'static'}; - $url .= 'custom'; - } elsif ($groupcfg{'static'}) { - $url .= 'statdns'; - } else { - $url .= 'dyndns'; - } - $url .= "&hostname=$hosts"; - $url .= "&myip="; + my $url = "$groupcfg{'server'}$groupcfg{'script'}?hostname=$hosts&myip="; $url .= $ipv4 if $ipv4; if ($ipv6) { $url .= "," if $ipv4; From 83ef1fa99a55b7065ad79ce77a6d417ef42f11e7 Mon Sep 17 00:00:00 2001 From: Starkstromkonsument Date: Sun, 23 Jun 2024 12:23:50 +0200 Subject: [PATCH 10/24] Add new protocol inwx Adoption of protocol dyndns2 to support their custom URL: 'https://dyndns.inwx.com/nic/update?myip=&myipv6=' --- ChangeLog.md | 2 + README.md | 1 + ddclient.conf.in | 8 +++ ddclient.in | 171 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index f703f3a..864f687 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -77,6 +77,8 @@ repository history](https://github.com/ddclient/ddclient/commits/master). [#703](https://github.com/ddclient/ddclient/pull/703) * `ddns.fm`: New `protocol` option for updating [DDNS.FM](https://ddns.fm/) records. [#695](https://github.com/ddclient/ddclient/pull/695) + * `inwx`: New `protocol` option for updating [INWX](https://www.inwx.com/) + records. [#690](https://github.com/ddclient/ddclient/pull/690) ### Bug fixes diff --git a/README.md b/README.md index 5b3275a..ddef13e 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Dynamic DNS services currently supported include: * [Google](https://domains.google) * [Hurricane Electric](https://dns.he.net) * [Infomaniak](https://faq.infomaniak.com/2376) +* [INWX](https://www.inwx.com/) * [Loopia](https://www.loopia.se) * [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns) * [NameCheap](https://www.namecheap.com) diff --git a/ddclient.conf.in b/ddclient.conf.in index 1c7ae35..81cc746 100644 --- a/ddclient.conf.in +++ b/ddclient.conf.in @@ -404,3 +404,11 @@ pid=@runstatedir@/ddclient.pid # record PID in file. # login=subdomain.domain.tld \ # password=your_password \ # subdomain.domain.tld + +## +## INWX +## +# protocol=inwx \ +# login=my-inwx-DynDNS-account-username \ +# password=my-inwx-DynDNS-account-password \ +# myhost.example.org diff --git a/ddclient.in b/ddclient.in index 332f086..ba2eeea 100755 --- a/ddclient.in +++ b/ddclient.in @@ -955,6 +955,16 @@ our %protocols = ( 'zone' => setv(T_FQDN, 1, 0, undef, undef), }, }, + 'inwx' => { + 'force_update' => undef, + 'update' => \&nic_inwx_update, + 'examples' => \&nic_inwx_examples, + 'variables' => { + %{$variables{'protocol-common-defaults'}}, + 'server' => setv(T_FQDNP, 0, 0, 'dyndns.inwx.com', undef), + 'script' => setv(T_STRING, 0, 0, '/nic/update', undef), + }, + }, 'mythicdyn' => { 'force_update' => undef, 'update' => \&nic_mythicdyn_update, @@ -6459,6 +6469,167 @@ sub nic_hetzner_update { } } +###################################################################### +## nic_inwx_examples +###################################################################### +sub nic_inwx_examples { + return <<"EoEXAMPLE"; +o 'inwx' + +The 'inwx' protocol is designed for DynDNS accounts at INWX +. It is similar to the 'dyndns2' protocol except IPv6 +addresses are passed in a separate 'myipv6' URL parameter (rather than included +in the 'myip' parameter): + + https://dyndns.inwx.com/nic/update?myip=&myipv6= + +The 'inwx' protocol was designed around INWX's behavior as of June 2024: + - Omitting the IPv4 address (either no 'myip' URL parameter or '' is + the empty string) will cause INWX to silently set the IPv4 address (A + record) to '127.0.0.1'. No error message is returned. + - Omitting the IPv6 address (either no 'myipv6' URL parameter or '' + is the empty string) will cause INWX to delete the IPv6 address (AAAA + record) if it exists. + - INWX will automatically create an IPv6 AAAA record for your hostname if + necessary. + - 'dyndns.inwx.com' is not reachable via IPv6 (there is no AAAA record). + - GET 'https://dyndns.inwx.com/nic/update' without further parameters will set + the IPv4 A record to the public IP of the requesting host and delete the + IPv6 AAAA record. + - You can ask INWX support to manually convert a DynDNS account into an + IPv6-only account. No A record will be created in that case. + +Configuration variables applicable to the 'inwx' protocol are: + protocol=inwx ## + server=fqdn.of.service ## defaults to dyndns.inwx.com + script=/path/to/script ## defaults to /nic/update + login=service-login ## login name and password registered with the service + password=service-password ## + fully.qualified.host ## the host registered with the service. + +Example ${program}.conf file entries: + ## single host update + protocol=inwx \\ + login=my-inwx-DynDNS-account-username \\ + password=my-inwx-DynDNS-account-password \\ + myhost.example.org +EoEXAMPLE +} + +###################################################################### +## nic_inwx_update +###################################################################### +sub nic_inwx_update { + debug("\nnic_inwx_update -------------------"); + my %errors = ( + 'badauth' => 'Bad authorization (username or password)', + 'badsys' => 'The system parameter given was not valid', + 'notfqdn' => 'A Fully-Qualified Domain Name was not provided', + 'nohost' => 'The hostname specified does not exist in the database', + '!yours' => 'The hostname specified exists, but not under the username currently being used', + '!donator' => 'The offline setting was set, when the user is not a donator', + '!active' => 'The hostname specified is in a Custom DNS domain which has not yet been activated.', + 'abuse' => 'The hostname specified is blocked for abuse; you should receive an email notification which provides an unblock request link.', + 'numhost' => 'System error: Too many or too few hosts found.', + 'dnserr' => 'System error: DNS error encountered.', + 'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive', + ); + my @group_by_attrs = qw( + login + password + server + script + wantipv4 + wantipv6 + ); + for my $group (group_hosts_by(\@_, @group_by_attrs)) { + my @hosts = @{$group->{hosts}}; + my %groupcfg = %{$group->{cfg}}; + my $hosts = join(',', @hosts); + my $ipv4 = $groupcfg{'wantipv4'}; + my $ipv6 = $groupcfg{'wantipv6'}; + delete $config{$_}{'wantipv4'} for @hosts; + delete $config{$_}{'wantipv6'} for @hosts; + info("$hosts: setting IPv4 address to $ipv4") if $ipv4; + info("$hosts: setting IPv6 address to $ipv6") if $ipv6; + my $url = "$groupcfg{'server'}$groupcfg{'script'}?"; + $url .= "myip=$ipv4" if $ipv4; + if ($ipv6) { + if (!$ipv4 && opt('usev4', $hosts) ne 'disabled') { + warning("Skipping IPv6 AAAA record update because INWX requires the IPv4 A record to be updated at the same time but the IPv4 address is unknown."); + next; + } + $url .= "&" if $ipv4; + $url .= "myipv6=$ipv6"; + } + my $reply = geturl( + proxy => opt('proxy'), + url => $url, + login => $groupcfg{'login'}, + password => $groupcfg{'password'}, + ) // ''; + if ($reply eq '') { + failed("$hosts: Could not connect to $groupcfg{'server'}"); + next; + } + next if !header_ok($hosts, $reply); + # INWX can return 200 OK even if there is an error (e.g., bad authentication, + # updates too frequent) so the body of the response must also be checked. + (my $body = $reply) =~ s/^.*?\n\n//s; + my @reply = split(qr/\n/, $body); + if (!@reply) { + failed("$hosts: Could not connect to $groupcfg{'server'}"); + next; + } + # From : + # + # If updating multiple hostnames, hostname-specific return codes are given one per line, + # in the same order as the hostnames were specified. Return codes indicating a failure + # with the account or the system are given only once. + # + # TODO: There is no mention of what happens if multiple IP addresses are supplied (e.g., + # IPv4 and IPv6) for a host. If one address fails to update and the other doesn't, is that + # one error status line? An error status line and a success status line? Or is an update + # considered to be all-or-nothing and the status applies to the operation as a whole? If + # the IPv4 address changes but not the IPv6 address does that result in a status of "good" + # because the set of addresses for a host changed even if a subset did not? + # + # TODO: The logic below applies the last line's status to all hosts. Change it to apply + # each status to its corresponding host. + for my $line (@reply) { + # The IP address normally comes after the status, but we ignore it. We could compare + # it with the expected address and mark the update as failed if it differs, but (1) + # some services do not return the IP; and (2) comparison is brittle (e.g., + # 192.000.002.001 vs. 192.0.2.1) and false errors could cause high load on the service + # (an update attempt every min-error-interval instead of every max-interval). + (my $status = $line) =~ s/ .*$//; + if ($status eq 'nochg') { + warning("$hosts: $status: $errors{$status}"); + $status = 'good'; + } + for my $h (@hosts) { + $config{$h}{'status-ipv4'} = $status if $ipv4; + $config{$h}{'status-ipv6'} = $status if $ipv6; + } + if ($status ne 'good') { + if (exists($errors{$status})) { + failed("$hosts: $status: $errors{$status}"); + } else { + failed("$hosts: unexpected status: $line"); + } + next; + } + for my $h (@hosts) { + $config{$h}{'ipv4'} = $ipv4 if $ipv4; + $config{$h}{'ipv6'} = $ipv6 if $ipv6; + $config{$h}{'mtime'} = $now; + } + success("$hosts: IPv4 address set to $ipv4") if $ipv4; + success("$hosts: IPv6 address set to $ipv6") if $ipv6; + } + } +} + ###################################################################### ## nic_yandex_examples ###################################################################### From 53b373fc9e71c090e506a5810f31030612ec232a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 19 Jul 2024 16:08:41 -0400 Subject: [PATCH 11/24] godaddy: Delete unnecessary comments --- ddclient.in | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ddclient.in b/ddclient.in index ba2eeea..dd5fb12 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5781,7 +5781,7 @@ sub nic_godaddy_update { (my $code) = ($reply =~ m%^s*HTTP/.*\s+(\d+)%i); my $ok = header_ok($host, $reply); my $msg; - $reply =~ s/^.*?\n\n//s; # extract payload + $reply =~ s/^.*?\n\n//s; my $response = eval {decode_json($reply)}; if (!defined($response) && $code != "200") { $$status = "bad"; @@ -5791,7 +5791,6 @@ sub nic_godaddy_update { info("%s.%s -- %s - %s.", $hostname, $zone, $response->{code}, $response->{message}); } if ($ok) { - # read data $config{$host}{"ipv$ipversion"} = $ip; $config{$host}{'mtime'} = $now; $$status = 'good'; @@ -5800,7 +5799,7 @@ sub nic_godaddy_update { next; } elsif ($code == "400") { $msg = 'GoDaddy API URL ($url) was malformed.'; - } elsif ($code == "401") { # authentication error + } elsif ($code == "401") { if ($config{$host}{'login'} && $config{$host}{'login'}) { $msg = 'login or password option incorrect.'; } else { From f82d2af0f262611064150c0f321bbab1bb7e390e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 7 Jun 2024 02:03:50 -0400 Subject: [PATCH 12/24] godaddy: Whitespace fixes --- ddclient.in | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/ddclient.in b/ddclient.in index dd5fb12..884c64d 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5740,29 +5740,23 @@ sub nic_godaddy_update { for my $host (@_) { my $ipv4 = delete $config{$host}{'wantipv4'}; my $ipv6 = delete $config{$host}{'wantipv6'}; - my $zone = $config{$host}{'zone'}; (my $hostname = $host) =~ s/\.\Q$zone\E$//; - for my $ip ($ipv4, $ipv6) { next if (!$ip); - info("%s.%s -- Setting IP address to %s.", $hostname, $zone, $ip); verbose("UPDATE:", "updating %s.%s", $hostname, $zone); - my $ipversion = ($ip eq ($ipv6 // '')) ? '6' : '4'; my $status = \$config{$host}{"status-ipv$ipversion"}; my $rrset_type = ($ipversion eq '6') ? 'AAAA' : 'A'; - my $data = encode_json([ { + my $data = encode_json([{ data => $ip, defined($config{$host}{'ttl'}) ? (ttl => $config{$host}{'ttl'}) : (), name => $hostname, type => $rrset_type, - } ]); - + }]); my $url = "https://$config{$host}{'server'}"; $url .= "/${zone}/records/${rrset_type}/${hostname}"; - my $header = "Content-Type: application/json\n"; $header .= "Accept: application/json\n"; $header .= "Authorization: sso-key $config{$host}{'login'}:$config{$host}{'password'}\n"; @@ -5777,7 +5771,6 @@ sub nic_godaddy_update { failed("%s.%s -- Could not connect to %s.", $hostname, $zone, $config{$host}{'server'}); next; } - (my $code) = ($reply =~ m%^s*HTTP/.*\s+(\d+)%i); my $ok = header_ok($host, $reply); my $msg; @@ -5785,7 +5778,6 @@ sub nic_godaddy_update { my $response = eval {decode_json($reply)}; if (!defined($response) && $code != "200") { $$status = "bad"; - failed("%s.%s -- Unexpected or empty service response, cannot parse data.", $hostname, $zone); } elsif (defined($response->{code})) { info("%s.%s -- %s - %s.", $hostname, $zone, $response->{code}, $response->{message}); @@ -5794,7 +5786,6 @@ sub nic_godaddy_update { $config{$host}{"ipv$ipversion"} = $ip; $config{$host}{'mtime'} = $now; $$status = 'good'; - success("%s.%s -- Updated successfully to %s (status: %s).", $hostname, $zone, $ip, $code); next; } elsif ($code == "400") { @@ -5819,7 +5810,6 @@ sub nic_godaddy_update { } else { $msg = 'Unexpected service response.'; } - $$status = 'bad'; failed("%s.%s -- %s", $hostname, $zone, $msg); } From c63eb0f0603bd44fe74cc3a529f248b766cb98c0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 18 May 2024 01:00:19 -0400 Subject: [PATCH 13/24] godaddy: Fix dubious response body check It doesn't make sense to continue processing if the response body can't be parsed. Maybe GoDaddy returned 200 and there's a bug in the body parsing logic, in which case the `bad` result should actually be `good`. But it's better to assume that the update wasn't saved, in case the server returns 200 with a JSON object that semantically means "failed to update". --- ddclient.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ddclient.in b/ddclient.in index 884c64d..afb2e89 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5776,9 +5776,10 @@ sub nic_godaddy_update { my $msg; $reply =~ s/^.*?\n\n//s; my $response = eval {decode_json($reply)}; - if (!defined($response) && $code != "200") { + if (!defined($response)) { $$status = "bad"; failed("%s.%s -- Unexpected or empty service response, cannot parse data.", $hostname, $zone); + next; } elsif (defined($response->{code})) { info("%s.%s -- %s - %s.", $hostname, $zone, $response->{code}, $response->{message}); } From e01ed55a580c9a478490e33851db9c803979f273 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 7 Jun 2024 00:35:09 -0400 Subject: [PATCH 14/24] godaddy: Simplify and improve error messages --- ddclient.in | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/ddclient.in b/ddclient.in index afb2e89..be3231c 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5744,9 +5744,8 @@ sub nic_godaddy_update { (my $hostname = $host) =~ s/\.\Q$zone\E$//; for my $ip ($ipv4, $ipv6) { next if (!$ip); - info("%s.%s -- Setting IP address to %s.", $hostname, $zone, $ip); - verbose("UPDATE:", "updating %s.%s", $hostname, $zone); my $ipversion = ($ip eq ($ipv6 // '')) ? '6' : '4'; + info("$host: Setting IPv$ipversion address to $ip"); my $status = \$config{$host}{"status-ipv$ipversion"}; my $rrset_type = ($ipversion eq '6') ? 'AAAA' : 'A'; my $data = encode_json([{ @@ -5768,7 +5767,7 @@ sub nic_godaddy_update { data => $data, ); unless ($reply) { - failed("%s.%s -- Could not connect to %s.", $hostname, $zone, $config{$host}{'server'}); + failed("$host: Could not connect to $config{$host}{'server'}"); next; } (my $code) = ($reply =~ m%^s*HTTP/.*\s+(\d+)%i); @@ -5778,16 +5777,16 @@ sub nic_godaddy_update { my $response = eval {decode_json($reply)}; if (!defined($response)) { $$status = "bad"; - failed("%s.%s -- Unexpected or empty service response, cannot parse data.", $hostname, $zone); + failed("$host: Unexpected or empty service response, cannot parse data"); next; } elsif (defined($response->{code})) { - info("%s.%s -- %s - %s.", $hostname, $zone, $response->{code}, $response->{message}); + info("$host: $response->{code} - $response->{message}"); } if ($ok) { $config{$host}{"ipv$ipversion"} = $ip; $config{$host}{'mtime'} = $now; $$status = 'good'; - success("%s.%s -- Updated successfully to %s (status: %s).", $hostname, $zone, $ip, $code); + success("$host: Updated successfully to $ip (status: $code)"); next; } elsif ($code == "400") { $msg = 'GoDaddy API URL ($url) was malformed.'; @@ -5801,18 +5800,18 @@ sub nic_godaddy_update { } elsif ($code == "403") { $msg = 'Customer identified by login and password options denied permission.'; } elsif ($code == "404") { - $msg = "\"${hostname}.${zone}\" not found at GoDaddy, please check zone option and login/password."; + $msg = "\"$host\" not found at GoDaddy, please check zone option and login/password."; } elsif ($code == "422") { - $msg = "\"${hostname}.${zone}\" has invalid domain or lacks A/AAAA record."; + $msg = "\"$host\" has invalid domain or lacks A/AAAA record."; } elsif ($code == "429") { $msg = 'Too many requests to GoDaddy within brief period.'; } elsif ($code == "503") { - $msg = "\"${hostname}.${zone}\" is unavailable."; + $msg = "\"$host\" is unavailable."; } else { $msg = 'Unexpected service response.'; } $$status = 'bad'; - failed("%s.%s -- %s", $hostname, $zone, $msg); + failed("$host: $msg"); } } } From 56f4a2afe22b7ad9837c4033fdb3288a5165ad7d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 7 Jun 2024 01:00:21 -0400 Subject: [PATCH 15/24] godaddy: Don't bother setting status on error It's initialized to a non-'good' value before the function is called, and ddclient doesn't distinguish between different non-good values, so it is sufficient to leave it alone. --- ddclient.in | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ddclient.in b/ddclient.in index be3231c..4ad1361 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5746,7 +5746,6 @@ sub nic_godaddy_update { next if (!$ip); my $ipversion = ($ip eq ($ipv6 // '')) ? '6' : '4'; info("$host: Setting IPv$ipversion address to $ip"); - my $status = \$config{$host}{"status-ipv$ipversion"}; my $rrset_type = ($ipversion eq '6') ? 'AAAA' : 'A'; my $data = encode_json([{ data => $ip, @@ -5776,7 +5775,6 @@ sub nic_godaddy_update { $reply =~ s/^.*?\n\n//s; my $response = eval {decode_json($reply)}; if (!defined($response)) { - $$status = "bad"; failed("$host: Unexpected or empty service response, cannot parse data"); next; } elsif (defined($response->{code})) { @@ -5785,7 +5783,7 @@ sub nic_godaddy_update { if ($ok) { $config{$host}{"ipv$ipversion"} = $ip; $config{$host}{'mtime'} = $now; - $$status = 'good'; + $config{$host}{"status-ipv$ipversion"} = 'good'; success("$host: Updated successfully to $ip (status: $code)"); next; } elsif ($code == "400") { @@ -5810,7 +5808,6 @@ sub nic_godaddy_update { } else { $msg = 'Unexpected service response.'; } - $$status = 'bad'; failed("$host: $msg"); } } From 3258ea34c0b2c8ec1216a3bd245e2f19dbb968cc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 7 Jun 2024 01:16:49 -0400 Subject: [PATCH 16/24] godaddy: Shorten variables for brevity and consistency --- ddclient.in | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/ddclient.in b/ddclient.in index 4ad1361..7feafa5 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5737,27 +5737,27 @@ EoEXAMPLE ###################################################################### sub nic_godaddy_update { debug("\nnic_godaddy_update --------------------"); - for my $host (@_) { - my $ipv4 = delete $config{$host}{'wantipv4'}; - my $ipv6 = delete $config{$host}{'wantipv6'}; - my $zone = $config{$host}{'zone'}; - (my $hostname = $host) =~ s/\.\Q$zone\E$//; + for my $h (@_) { + my $ipv4 = delete $config{$h}{'wantipv4'}; + my $ipv6 = delete $config{$h}{'wantipv6'}; + my $zone = $config{$h}{'zone'}; + (my $hostname = $h) =~ s/\.\Q$zone\E$//; for my $ip ($ipv4, $ipv6) { next if (!$ip); - my $ipversion = ($ip eq ($ipv6 // '')) ? '6' : '4'; - info("$host: Setting IPv$ipversion address to $ip"); - my $rrset_type = ($ipversion eq '6') ? 'AAAA' : 'A'; + my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; + info("$h: Setting IPv$ipv address to $ip"); + my $rrset_type = ($ipv eq '6') ? 'AAAA' : 'A'; my $data = encode_json([{ data => $ip, - defined($config{$host}{'ttl'}) ? (ttl => $config{$host}{'ttl'}) : (), + defined($config{$h}{'ttl'}) ? (ttl => $config{$h}{'ttl'}) : (), name => $hostname, type => $rrset_type, }]); - my $url = "https://$config{$host}{'server'}"; + my $url = "https://$config{$h}{'server'}"; $url .= "/${zone}/records/${rrset_type}/${hostname}"; my $header = "Content-Type: application/json\n"; $header .= "Accept: application/json\n"; - $header .= "Authorization: sso-key $config{$host}{'login'}:$config{$host}{'password'}\n"; + $header .= "Authorization: sso-key $config{$h}{'login'}:$config{$h}{'password'}\n"; my $reply = geturl( proxy => opt('proxy'), url => $url, @@ -5766,30 +5766,30 @@ sub nic_godaddy_update { data => $data, ); unless ($reply) { - failed("$host: Could not connect to $config{$host}{'server'}"); + failed("$h: Could not connect to $config{$h}{'server'}"); next; } (my $code) = ($reply =~ m%^s*HTTP/.*\s+(\d+)%i); - my $ok = header_ok($host, $reply); + my $ok = header_ok($h, $reply); my $msg; $reply =~ s/^.*?\n\n//s; my $response = eval {decode_json($reply)}; if (!defined($response)) { - failed("$host: Unexpected or empty service response, cannot parse data"); + failed("$h: Unexpected or empty service response, cannot parse data"); next; } elsif (defined($response->{code})) { - info("$host: $response->{code} - $response->{message}"); + info("$h: $response->{code} - $response->{message}"); } if ($ok) { - $config{$host}{"ipv$ipversion"} = $ip; - $config{$host}{'mtime'} = $now; - $config{$host}{"status-ipv$ipversion"} = 'good'; - success("$host: Updated successfully to $ip (status: $code)"); + $config{$h}{"ipv$ipv"} = $ip; + $config{$h}{'mtime'} = $now; + $config{$h}{"status-ipv$ipv"} = 'good'; + success("$h: Updated successfully to $ip (status: $code)"); next; } elsif ($code == "400") { $msg = 'GoDaddy API URL ($url) was malformed.'; } elsif ($code == "401") { - if ($config{$host}{'login'} && $config{$host}{'login'}) { + if ($config{$h}{'login'} && $config{$h}{'login'}) { $msg = 'login or password option incorrect.'; } else { $msg = 'login or password option missing.'; @@ -5798,17 +5798,17 @@ sub nic_godaddy_update { } elsif ($code == "403") { $msg = 'Customer identified by login and password options denied permission.'; } elsif ($code == "404") { - $msg = "\"$host\" not found at GoDaddy, please check zone option and login/password."; + $msg = "\"$h\" not found at GoDaddy, please check zone option and login/password."; } elsif ($code == "422") { - $msg = "\"$host\" has invalid domain or lacks A/AAAA record."; + $msg = "\"$h\" has invalid domain or lacks A/AAAA record."; } elsif ($code == "429") { $msg = 'Too many requests to GoDaddy within brief period.'; } elsif ($code == "503") { - $msg = "\"$host\" is unavailable."; + $msg = "\"$h\" is unavailable."; } else { $msg = 'Unexpected service response.'; } - failed("$host: $msg"); + failed("$h: $msg"); } } } From b1c00296042701a1a96f264ef7504a4bf26cdbe8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 7 Jun 2024 01:20:08 -0400 Subject: [PATCH 17/24] godaddy: Simplify `for` loop --- ddclient.in | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ddclient.in b/ddclient.in index 7feafa5..0b9883f 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5738,13 +5738,10 @@ EoEXAMPLE sub nic_godaddy_update { debug("\nnic_godaddy_update --------------------"); for my $h (@_) { - my $ipv4 = delete $config{$h}{'wantipv4'}; - my $ipv6 = delete $config{$h}{'wantipv6'}; my $zone = $config{$h}{'zone'}; (my $hostname = $h) =~ s/\.\Q$zone\E$//; - for my $ip ($ipv4, $ipv6) { - next if (!$ip); - my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; + for my $ipv ('4', '6') { + my $ip = delete($config{$h}{"wantipv$ipv"}) or next; info("$h: Setting IPv$ipv address to $ip"); my $rrset_type = ($ipv eq '6') ? 'AAAA' : 'A'; my $data = encode_json([{ From 6e98c0cdb2148fff63fb991407a6d900cf4c9006 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 7 Jun 2024 01:37:04 -0400 Subject: [PATCH 18/24] godaddy: Invert conditional to improve readability --- ddclient.in | 55 +++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/ddclient.in b/ddclient.in index 0b9883f..087e6c2 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5768,7 +5768,6 @@ sub nic_godaddy_update { } (my $code) = ($reply =~ m%^s*HTTP/.*\s+(\d+)%i); my $ok = header_ok($h, $reply); - my $msg; $reply =~ s/^.*?\n\n//s; my $response = eval {decode_json($reply)}; if (!defined($response)) { @@ -5777,35 +5776,37 @@ sub nic_godaddy_update { } elsif (defined($response->{code})) { info("$h: $response->{code} - $response->{message}"); } - if ($ok) { - $config{$h}{"ipv$ipv"} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{"status-ipv$ipv"} = 'good'; - success("$h: Updated successfully to $ip (status: $code)"); - next; - } elsif ($code == "400") { - $msg = 'GoDaddy API URL ($url) was malformed.'; - } elsif ($code == "401") { - if ($config{$h}{'login'} && $config{$h}{'login'}) { - $msg = 'login or password option incorrect.'; + if (!$ok) { + my $msg; + if ($code == "400") { + $msg = 'GoDaddy API URL ($url) was malformed.'; + } elsif ($code == "401") { + if ($config{$h}{'login'} && $config{$h}{'login'}) { + $msg = 'login or password option incorrect.'; + } else { + $msg = 'login or password option missing.'; + } + $msg .= ' Correct values can be obtained from from https://developer.godaddy.com/keys/.'; + } elsif ($code == "403") { + $msg = 'Customer identified by login and password options denied permission.'; + } elsif ($code == "404") { + $msg = "\"$h\" not found at GoDaddy, please check zone option and login/password."; + } elsif ($code == "422") { + $msg = "\"$h\" has invalid domain or lacks A/AAAA record."; + } elsif ($code == "429") { + $msg = 'Too many requests to GoDaddy within brief period.'; + } elsif ($code == "503") { + $msg = "\"$h\" is unavailable."; } else { - $msg = 'login or password option missing.'; + $msg = 'Unexpected service response.'; } - $msg .= ' Correct values can be obtained from from https://developer.godaddy.com/keys/.'; - } elsif ($code == "403") { - $msg = 'Customer identified by login and password options denied permission.'; - } elsif ($code == "404") { - $msg = "\"$h\" not found at GoDaddy, please check zone option and login/password."; - } elsif ($code == "422") { - $msg = "\"$h\" has invalid domain or lacks A/AAAA record."; - } elsif ($code == "429") { - $msg = 'Too many requests to GoDaddy within brief period.'; - } elsif ($code == "503") { - $msg = "\"$h\" is unavailable."; - } else { - $msg = 'Unexpected service response.'; + failed("$h: $msg"); + next; } - failed("$h: $msg"); + $config{$h}{"ipv$ipv"} = $ip; + $config{$h}{'mtime'} = $now; + $config{$h}{"status-ipv$ipv"} = 'good'; + success("$h: Updated successfully to $ip (status: $code)"); } } } From 4b2155a43c88c1953b2eb2f7b200e308885e6d52 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 7 Jun 2024 02:00:30 -0400 Subject: [PATCH 19/24] godaddy: Combine URL lines to improve readability Also drop unnecessary curly braces for consistency. --- ddclient.in | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ddclient.in b/ddclient.in index 087e6c2..a9e3004 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5750,8 +5750,7 @@ sub nic_godaddy_update { name => $hostname, type => $rrset_type, }]); - my $url = "https://$config{$h}{'server'}"; - $url .= "/${zone}/records/${rrset_type}/${hostname}"; + my $url = "https://$config{$h}{'server'}/$zone/records/$rrset_type/$hostname"; my $header = "Content-Type: application/json\n"; $header .= "Accept: application/json\n"; $header .= "Authorization: sso-key $config{$h}{'login'}:$config{$h}{'password'}\n"; From b9144c01c2c8d4b965e203b7dc08921b0608bae8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 7 Jun 2024 03:02:31 -0400 Subject: [PATCH 20/24] godaddy: Use `eq` for string equality `==` is for numeric equality. --- ddclient.in | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ddclient.in b/ddclient.in index a9e3004..82c9b52 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5777,24 +5777,24 @@ sub nic_godaddy_update { } if (!$ok) { my $msg; - if ($code == "400") { + if ($code eq "400") { $msg = 'GoDaddy API URL ($url) was malformed.'; - } elsif ($code == "401") { + } elsif ($code eq "401") { if ($config{$h}{'login'} && $config{$h}{'login'}) { $msg = 'login or password option incorrect.'; } else { $msg = 'login or password option missing.'; } $msg .= ' Correct values can be obtained from from https://developer.godaddy.com/keys/.'; - } elsif ($code == "403") { + } elsif ($code eq "403") { $msg = 'Customer identified by login and password options denied permission.'; - } elsif ($code == "404") { + } elsif ($code eq "404") { $msg = "\"$h\" not found at GoDaddy, please check zone option and login/password."; - } elsif ($code == "422") { + } elsif ($code eq "422") { $msg = "\"$h\" has invalid domain or lacks A/AAAA record."; - } elsif ($code == "429") { + } elsif ($code eq "429") { $msg = 'Too many requests to GoDaddy within brief period.'; - } elsif ($code == "503") { + } elsif ($code eq "503") { $msg = "\"$h\" is unavailable."; } else { $msg = 'Unexpected service response.'; From bdc69d879f5c9d45d08f3cc5aaee982f54b2da15 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 7 Jun 2024 02:19:22 -0400 Subject: [PATCH 21/24] geturl: Accept an arrayref for headers --- ddclient.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddclient.in b/ddclient.in index 82c9b52..2822677 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2873,7 +2873,7 @@ sub geturl { push(@curlopt, "url=\"".escape_curl_param("${protocol}://${server}/${url}").'"'); # Each header line is added individually - @header_lines = split('\n', $headers); + @header_lines = ref($headers) eq 'ARRAY' ? @$headers : split('\n', $headers); $_ = "header=\"".escape_curl_param($_).'"' for (@header_lines); push(@curlopt, @header_lines); From 12b2c0d03dedb30e740dad2b81986d9088da4ce5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 7 Jun 2024 02:32:30 -0400 Subject: [PATCH 22/24] godaddy: Inline some unnecessary variables --- ddclient.in | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ddclient.in b/ddclient.in index 2822677..cd59f97 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5744,22 +5744,22 @@ sub nic_godaddy_update { my $ip = delete($config{$h}{"wantipv$ipv"}) or next; info("$h: Setting IPv$ipv address to $ip"); my $rrset_type = ($ipv eq '6') ? 'AAAA' : 'A'; - my $data = encode_json([{ - data => $ip, - defined($config{$h}{'ttl'}) ? (ttl => $config{$h}{'ttl'}) : (), - name => $hostname, - type => $rrset_type, - }]); my $url = "https://$config{$h}{'server'}/$zone/records/$rrset_type/$hostname"; - my $header = "Content-Type: application/json\n"; - $header .= "Accept: application/json\n"; - $header .= "Authorization: sso-key $config{$h}{'login'}:$config{$h}{'password'}\n"; my $reply = geturl( proxy => opt('proxy'), url => $url, - headers => $header, + headers => [ + 'Content-Type: application/json', + 'Accept: application/json', + "Authorization: sso-key $config{$h}{'login'}:$config{$h}{'password'}", + ], method => 'PUT', - data => $data, + data => encode_json([{ + data => $ip, + defined($config{$h}{'ttl'}) ? (ttl => $config{$h}{'ttl'}) : (), + name => $hostname, + type => $rrset_type, + }]), ); unless ($reply) { failed("$h: Could not connect to $config{$h}{'server'}"); From dab67eae69ed584134cb51a01711d1e2daf89793 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 19 Jul 2024 17:38:27 -0400 Subject: [PATCH 23/24] README.md: Revise troubleshooting section * Use a bulleted list instead of a numbered list * Fix indentation * Fix formatting * Replace `fw` with `fwv4` * Suggest `curl` instead of `ddclient --geturl` * etc. --- README.md | 61 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index ddef13e..07357b9 100644 --- a/README.md +++ b/README.md @@ -168,41 +168,56 @@ This issue arises when using the `use` parameter in the config and using one of ## TROUBLESHOOTING - 1. enable debugging and verbose messages: ``$ ddclient --daemon=0 --debug --verbose --noquiet`` + * Enable debugging and verbose messages: `ddclient --daemon=0 --debug --verbose` - 2. Do you need to specify a proxy? - If so, just add a ``proxy=your.isp.proxy`` to the ddclient.conf file. + * Do you need to specify a proxy? + If so, just add a `proxy=your.isp.proxy` to the `ddclient.conf` file. - 3. Define the IP address of your router with ``fw=xxx.xxx.xxx.xxx`` in - ``/etc/ddclient/ddclient.conf`` and then try ``$ ddclient --daemon=0 --query`` to see if the router status web page can be understood. + * Define the IP address of your router with `fwv4=xxx.xxx.xxx.xxx` in + `/etc/ddclient/ddclient.conf` and then try `$ ddclient --daemon=0 --query` + to see if the router status web page can be understood. - 4. Need support for another router/firewall? - Define the router status page yourself with: ``fw=url-to-your-router``'s-status-page ``fw-skip=any-string-preceding-your-IP-address`` + * Need support for another router/firewall? + Define the router yourself with: - ddclient does something like this to provide builtin support for - common routers. - For example, the Linksys routers could have been added with: + ``` + usev4=fwv4 + fwv4=url-to-your-router-status-page + fwv4-skip="regular expression matching any string preceding your IP address, if necessary" + ``` - fw=192.168.1.1/Status.htm - fw-skip=WAN.*?IP Address + ddclient does something like this to provide builtin support for common + routers. + For example, the Linksys routers could have been added with: -OR - Send me the output from: - ``$ ddclient --geturl {fw-ip-status-url} [--login login [--password password]]`` - and I'll add it to the next release! + ``` + usev4=fwv4 + fwv4=192.168.1.1/Status.htm + fwv4-skip=WAN.*?IP Address + ``` -ie. for my fw/router I used: ``$ ddclient --geturl 192.168.1.254/status.htm`` + OR [create a new issue](https://github.com/ddclient/ddclient/issues/new) + containing the output from: - 5. Some broadband routers require the use of a password when ddclient - accesses its status page to determine the router's WAN IP address. - If this is the case for your router, add + ``` + curl --include --location http://url.of.your.firewall/ip-status-page + ``` + so that we can add a new firewall definition to a future release of + ddclient. + + * Some broadband routers require the use of a password when ddclient accesses + its status page to determine the router's WAN IP address. + If this is the case for your router, add + + ``` fw-login=your-router-login fw-password=your-router-password + ``` -to the beginning of your ddclient.conf file. -Note that some routers use either 'root' or 'admin' as their login -while some others accept anything. + to the beginning of your ddclient.conf file. + Note that some routers use either 'root' or 'admin' as their login while + some others accept anything. ## USING DDCLIENT WITH `ppp` From 3a57ca137426f702108c96a56c18cfd592e770fb Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 19 Jul 2024 18:01:30 -0400 Subject: [PATCH 24/24] Delete `--geturl` command-line argument and `geturl` option They are not used in any tests, and `curl` is a better choice anyway. --- ChangeLog.md | 2 ++ ddclient.in | 20 -------------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 864f687..cc15c1f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -41,6 +41,8 @@ repository history](https://github.com/ddclient/ddclient/commits/master). * `dyndns2`: The obsolete `static` and `custom` options have been removed. Setting the options may produce a warning. [#709](https://github.com/ddclient/ddclient/pull/709) + * The diagnostic `--geturl` command-line argument was removed. + [#TODO](https://github.com/ddclient/ddclient/pull/TODO) ### New features diff --git a/ddclient.in b/ddclient.in index cd59f97..82a324c 100755 --- a/ddclient.in +++ b/ddclient.in @@ -618,7 +618,6 @@ our %variables = ( 'quiet' => setv(T_BOOL, 0, 0, 0, undef), 'help' => setv(T_BOOL, 0, 0, 0, undef), 'test' => setv(T_BOOL, 0, 0, 0, undef), - 'geturl' => setv(T_STRING,0, 0, undef, undef), 'postscript' => setv(T_POSTS, 0, 0, undef, undef), 'ssl_ca_dir' => setv(T_FILE, 0, 0, undef, undef), @@ -1303,7 +1302,6 @@ my @opt = ( ["fw-banlocal", "!", ""], ## deprecated ["if-skip", "=s", ""], ## deprecated ["test", "!", ""], ## hidden - ["geturl", "=s", ""], ## hidden ["redirect", "=i", "--redirect= : enable and follow at most HTTP 30x redirections"], "", nic_examples(), @@ -1315,9 +1313,6 @@ sub main { $saved_recap = ''; %saved_opt = %opt; $result = 'OK'; - - test_geturl(opt('geturl')) if opt('geturl'); - if (opt('help')) { printf "%s\n", $opt_usage; $opt{'version'}('', ''); @@ -2272,22 +2267,7 @@ sub test_possible_ip { exit 0 unless opt('debug'); } -###################################################################### -## test_geturl - print (and save if -test) result of fetching a URL -###################################################################### -sub test_geturl { - my $url = shift; - my $reply = geturl( - proxy => opt('proxy'), - url => $url, - login => opt('login'), - password => opt('password'), - ); - print "URL $url\n"; - print $reply // "\n"; - exit; -} ###################################################################### ## load_file ######################################################################