Merge pull request #674 from jortkoopmans/bugfix/673_Fix_DnsExit_subdomain

Fix DNSExit provider when provided with a zone and a non-identical hostname
This commit is contained in:
Richard Hansen 2024-06-02 16:58:43 -04:00 committed by GitHub
commit 09ce262c82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 316 additions and 90 deletions

View file

@ -33,6 +33,7 @@ jobs:
libtest-tcp-perl \
libtest-warnings-perl \
liburi-perl \
libwww-perl \
net-tools \
make \
;
@ -102,6 +103,7 @@ jobs:
perl-Test-TCP \
perl-Test-Warnings \
perl-core \
perl-libwww-perl \
net-tools \
;
- name: autogen

View file

@ -75,6 +75,8 @@ repository history](https://github.com/ddclient/ddclient/commits/master).
[#667](https://github.com/ddclient/ddclient/pull/667)
* Fixed unnecessary repeated updates for some services.
[#670](https://github.com/ddclient/ddclient/pull/670)
* Fixed DNSExit provider when configured with a zone and non-identical
hostname. [#673](https://github.com/ddclient/ddclient/issues/673)
## 2023-11-23 v3.11.2

View file

@ -63,6 +63,7 @@ AM_PL_LOG_FLAGS = -Mstrict -w \
-MDevel::Autoflush
handwritten_tests = \
t/builtinfw_query.pl \
t/dnsexit2.pl \
t/get_ip_from_if.pl \
t/geturl_connectivity.pl \
t/group_hosts_by.pl \

View file

@ -80,6 +80,8 @@ m4_foreach_w([_m], [
HTTP::Message::PSGI
HTTP::Request
HTTP::Response
JSON::PP
LWP::UserAgent
Scalar::Util
Test::MockModule
Test::TCP

View file

@ -4140,6 +4140,8 @@ o 'dnsexit2'
The 'dnsexit2' protocol is the updated protocol for the (free) dynamic hostname services
of 'DNSExit' (www.dnsexit.com). Their API is accepting JSON payload.
Note that we only update the record, it must already exist in the DNSExit system
(A and/or AAAA records where applicable).
Configuration variables applicable to the 'dnsexit2' protocol are:
protocol=dnsexit2 ##
@ -4173,10 +4175,25 @@ EoEXAMPLE
######################################################################
sub nic_dnsexit2_update {
debug("\nnic_dnsexit2_update -------------------");
# The DNSExit API does not support updating multiple hosts at a time.
# The DNSExit API does not support updating hosts with different zones at the same time,
# handling update per host.
for my $h (@_) {
$config{$h}{'zone'} //= $h;
dnsexit2_update_host($h);
}
}
sub dnsexit2_update_host {
my ($h) = @_;
my $name = $h;
# Remove the zone suffix from $name. If the zone eq $name, $name can be left alone or
# set to the empty string; both have identical semantics. For consistency, always
# remove the zone even if it means $name becomes the empty string.
if ($name =~ s/(?:^|\.)\Q$config{$h}{'zone'}\E$//) {
# The zone was successfully trimmed from $name.
} else {
fatal("Hostname %s does not end with the zone %s", $h, $config{$h}{'zone'});
}
# The IPv4 and IPv6 addresses must be updated together in a single API call.
my %ips;
my @updates;
@ -4186,7 +4203,7 @@ sub nic_dnsexit2_update {
info("Going to update IPv%s address to %s for %s.", $ipv, $ip, $h);
$config{$h}{"status-ipv$ipv"} = 'failed';
push(@updates, {
name => $h,
name => $name,
type => ($ipv eq '6') ? 'AAAA' : 'A',
content => $ip,
ttl => $config{$h}{'ttl'},
@ -4206,7 +4223,7 @@ sub nic_dnsexit2_update {
);
unless ($reply && header_ok($h, $reply)) {
failed("updating %s: Could not connect to %s", $h, $url);
last;
return;
};
debug("%s", $reply);
(my $http_status) = ($reply =~ m%^s*HTTP/.*\s+(\d+)%i);
@ -4215,18 +4232,18 @@ sub nic_dnsexit2_update {
failed("Failed to update Host\n%s", $h);
failed("HTTP response code\n%s", $http_status);
failed("Full reply\n%s", $reply) unless opt('verbose');
next;
return;
}
my $body = ($reply =~ s/^.*?\r?\n\r?\n//sr);
my $response = eval { decode_json($body); };
if (!$response) {
failed("failed to parse response: $@");
next;
return;
}
if (!defined($response->{'code'}) || !defined($response->{'message'})) {
failed("Did not receive expected 'code' and 'message' keys in server response:\n%s",
$body);
next;
return;
}
my %codemeaning = (
'0' => ['good', 'Success! Actions got executed successfully.'],
@ -4240,7 +4257,7 @@ sub nic_dnsexit2_update {
);
if (!exists($codemeaning{$response->{'code'}})) {
failed("Status code %s is unknown!", $response->{'code'});
next;
return;
}
my ($status, $message) = @{$codemeaning{$response->{'code'}}};
info("Status: %s -- Message: %s", $status, $message);
@ -4256,7 +4273,7 @@ sub nic_dnsexit2_update {
} else {
failed("Unexpected status: %s", $status);
}
next;
return;
}
success("%s", $message);
$config{$h}{'mtime'} = $now;
@ -4267,7 +4284,6 @@ sub nic_dnsexit2_update {
success("Updated %s successfully to IPv%s address %s at time %s", $h, $ipv, $ip, prettytime($config{$h}{'mtime'}));
}
}
}
######################################################################
## nic_noip_update

203
t/dnsexit2.pl Normal file
View file

@ -0,0 +1,203 @@
use Test::More;
eval { require JSON::PP; } or plan(skip_all => $@);
JSON::PP->import(qw(encode_json decode_json));
eval { require 'ddclient'; } or BAIL_OUT($@);
eval { require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@);
eval { require LWP::UserAgent; } or plan(skip_all => $@);
ddclient::load_json_support('dnsexit2');
my @requests; # Declare global variable to store requests, used for tests.
my @httpd_requests; # Declare variable specificly used for the httpd process (which cannot be shared with tests).
my $httpd = ddclient::Test::Fake::HTTPD->new();
$httpd->run(sub {
my ($req) = @_;
if ($req->uri->as_string eq '/get_requests') {
return [200, ['Content-Type' => 'application/json'], [encode_json(\@httpd_requests)]];
} elsif ($req->uri->as_string eq '/reset_requests') {
@httpd_requests = ();
return [200, ['Content-Type' => 'application/json'], [encode_json({ message => 'OK' })]];
}
my $request_info = {
method => $req->method,
uri => $req->uri->as_string,
content => $req->content,
headers => $req->headers->as_string
};
push @httpd_requests, $request_info;
return [200, ['Content-Type' => 'application/json'], [encode_json({
code => 0,
message => 'Success'
})]];
});
diag(sprintf("started IPv4 server running at %s", $httpd->endpoint()));
my $ua = LWP::UserAgent->new;
sub test_nic_dnsexit2_update {
my ($config, @hostnames) = @_;
%ddclient::config = %$config;
ddclient::nic_dnsexit2_update(@hostnames);
}
sub decode_and_sort_array {
my ($data) = @_;
if (!ref $data) {
$data = decode_json($data);
}
@{$data->{update}} = sort { $a->{type} cmp $b->{type} } @{$data->{update}};
return $data;
}
sub reset_test_data {
my $response = $ua->get($httpd->endpoint . '/reset_requests');
die "Failed to reset requests" unless $response->is_success;
@requests = ();
}
sub get_requests {
my $res = $ua->get($httpd->endpoint . '/get_requests');
die "Failed to get requests: " . $res->status_line unless $res->is_success;
return @{decode_json($res->decoded_content)};
}
subtest 'Testing nic_dnsexit2_update' => sub {
my %config = (
'host.my.zone.com' => {
'ssl' => 'no',
'verbose' => 'yes',
'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4',
'usev6' => 'ipv6',
'wantipv6' => '2001:4860:4860::8888',
'protocol' => 'dnsexit2',
'password' => 'mytestingpassword',
'zone' => 'my.zone.com',
'server' => $httpd->host_port(),
'path' => '/update',
'ttl' => 5
});
test_nic_dnsexit2_update(\%config, 'host.my.zone.com');
@requests = get_requests();
is($requests[0]->{method}, 'POST', 'Method is correct');
is($requests[0]->{uri}, '/update', 'URI contains correct path');
like($requests[0]->{headers}, qr/Content-Type: application\/json/, 'Content-Type header is correct');
like($requests[0]->{headers}, qr/Accept: application\/json/, 'Accept header is correct');
my $data = decode_and_sort_array($requests[0]->{content});
my $expected_data = decode_and_sort_array({
'domain' => 'my.zone.com',
'apikey' => 'mytestingpassword',
'update' => [
{
'type' => 'A',
'name' => 'host',
'content' => '8.8.4.4',
'ttl' => 5,
},
{
'type' => 'AAAA',
'name' => 'host',
'content' => '2001:4860:4860::8888',
'ttl' => 5,
}
]
});
is_deeply($data, $expected_data, 'Data is correct');
reset_test_data();
};
subtest 'Testing nic_dnsexit2_update without a zone set' => sub {
my %config = (
'myhost.zone.com' => {
'ssl' => 'yes',
'verbose' => 'yes',
'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4',
'protocol' => 'dnsexit2',
'password' => 'anotherpassword',
'server' => $httpd->host_port(),
'path' => '/update-alt',
'ttl' => 10
});
test_nic_dnsexit2_update(\%config, 'myhost.zone.com');
@requests = get_requests();
my $data = decode_and_sort_array($requests[0]->{content});
my $expected_data = decode_and_sort_array({
'domain' => 'myhost.zone.com',
'apikey' => 'anotherpassword',
'update' => [
{
'type' => 'A',
'name' => '',
'content' => '8.8.4.4',
'ttl' => 10,
}
]
});
is_deeply($data, $expected_data, 'Data is correct');
reset_test_data($ua);
};
subtest 'Testing nic_dnsexit2_update with two hostnames, one with a zone and one without' => sub {
my %config = (
'host1.zone.com' => {
'ssl' => 'yes',
'verbose' => 'yes',
'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4',
'protocol' => 'dnsexit2',
'password' => 'testingpassword',
'server' => $httpd->host_port(),
'path' => '/update',
'ttl' => 5
},
'host2.zone.com' => {
'ssl' => 'yes',
'verbose' => 'yes',
'usev6' => 'ipv6',
'wantipv6' => '2001:4860:4860::8888',
'protocol' => 'dnsexit2',
'password' => 'testingpassword',
'server' => $httpd->host_port(),
'path' => '/update',
'ttl' => 10,
'zone' => 'zone.com'
}
);
test_nic_dnsexit2_update(\%config, 'host1.zone.com', 'host2.zone.com');
my $expected_data1 = decode_and_sort_array({
'domain' => 'host1.zone.com',
'apikey' => 'testingpassword',
'update' => [
{
'type' => 'A',
'name' => '',
'content' => '8.8.4.4',
'ttl' => 5,
}
]
});
my $expected_data2 = decode_and_sort_array({
'domain' => 'zone.com',
'apikey' => 'testingpassword',
'update' => [
{
'type' => 'AAAA',
'name' => 'host2',
'content' => '2001:4860:4860::8888',
'ttl' => 10,
}
]
});
@requests = get_requests();
for my $i (0..1) {
my $data = decode_and_sort_array($requests[$i]->{content});
is_deeply($data, $expected_data1, 'Data is correct for call host1') if $i == 0;
is_deeply($data, $expected_data2, 'Data is correct for call host2') if $i == 1;
}
reset_test_data();
};
done_testing();