Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
f7419a052b
15 changed files with 2155 additions and 1480 deletions
41
.github/workflows/ci.yml
vendored
41
.github/workflows/ci.yml
vendored
|
|
@ -33,6 +33,7 @@ jobs:
|
|||
libtest-tcp-perl \
|
||||
libtest-warnings-perl \
|
||||
liburi-perl \
|
||||
libwww-perl \
|
||||
net-tools \
|
||||
make \
|
||||
;
|
||||
|
|
@ -61,33 +62,28 @@ jobs:
|
|||
- fedora:39
|
||||
- fedora:latest
|
||||
- fedora:rawhide
|
||||
# RedHat UBI is mostly garbage due to a profound lack of basic
|
||||
# packages. It is tested anyway because it's the closest available
|
||||
# approximation of RHEL. Some of the packages needed for some tests
|
||||
# aren't available, so those tests will be skipped. I guess it's
|
||||
# still better than nothing.
|
||||
- registry.access.redhat.com/ubi7/ubi:latest
|
||||
- registry.access.redhat.com/ubi8/ubi:latest
|
||||
- registry.access.redhat.com/ubi9/ubi:latest
|
||||
- almalinux:8
|
||||
- almalinux:latest
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
steps:
|
||||
- if: ${{ matrix.image != 'registry.access.redhat.com/ubi7/ubi:latest' }}
|
||||
uses: actions/checkout@v4
|
||||
# ubi7 is too old for checkout@v4.
|
||||
- if: ${{ matrix.image == 'registry.access.redhat.com/ubi7/ubi:latest' }}
|
||||
uses: actions/checkout@v3
|
||||
- name: install dependencies
|
||||
# The --skip-broken argument works around RedHat UBI's missing packages.
|
||||
# (They're only used for testing, so it's OK to not install them.)
|
||||
- uses: actions/checkout@v4
|
||||
- name: enable repositories (AlmaLinux 8)
|
||||
if: ${{ matrix.image == 'almalinux:8' }}
|
||||
run: |
|
||||
inst="dnf --refresh --skip-broken install -y"
|
||||
case '${{ matrix.image }}' in
|
||||
# RedHat UBI 7 (RHEL 7) doesn't have dnf.
|
||||
*ubi7*) inst="yum --skip-broken install -y";;
|
||||
esac
|
||||
${inst} \
|
||||
dnf --refresh install -y 'dnf-command(config-manager)' epel-release &&
|
||||
dnf config-manager --set-enabled powertools
|
||||
- name: enable repositories (AlmaLinux latest)
|
||||
if: ${{ matrix.image == 'almalinux:latest' }}
|
||||
run: |
|
||||
dnf --refresh install -y 'dnf-command(config-manager)' epel-release &&
|
||||
dnf config-manager --set-enabled crb
|
||||
- name: install dependencies
|
||||
# The --skip-broken argument works around missing packages. (They're
|
||||
# only used for testing, so it's OK to not install them.)
|
||||
run: |
|
||||
dnf --refresh install --skip-broken -y \
|
||||
automake \
|
||||
findutils \
|
||||
iproute \
|
||||
|
|
@ -102,6 +98,7 @@ jobs:
|
|||
perl-Test-TCP \
|
||||
perl-Test-Warnings \
|
||||
perl-core \
|
||||
perl-libwww-perl \
|
||||
net-tools \
|
||||
;
|
||||
- name: autogen
|
||||
|
|
|
|||
41
ChangeLog.md
41
ChangeLog.md
|
|
@ -3,18 +3,37 @@
|
|||
This document describes notable changes. For details, see the [source code
|
||||
repository history](https://github.com/ddclient/ddclient/commits/master).
|
||||
|
||||
## v3.11.3~alpha (unreleased work-in-progress)
|
||||
## v4.0.0~alpha (unreleased work-in-progress)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The `--ssl` option is now enabled by default.
|
||||
[#705](https://github.com/ddclient/ddclient/pull/705)
|
||||
* Unencrypted (plain) HTTP is now used instead of encrypted (TLS) HTTP if the
|
||||
URL uses `http://` instead of `https://`, even if the `--ssl` option is
|
||||
enabled. [#608](https://github.com/ddclient/ddclient/pull/608)
|
||||
* The `googledomains` built-in web IP discovery service
|
||||
(`--webv4=googledomains`, `--webv6=googledomains`, and
|
||||
`--web=googledomains`) is deprecated due to the service shutting down. It
|
||||
will be removed in a future version of ddclient.
|
||||
[5b104ad1](https://github.com/ddclient/ddclient/commit/5b104ad116c023c3760129cab6e141f04f72b406)
|
||||
* The default web service for `--webv4` and `--webv6` has changed from Google
|
||||
Domains (which is shutting down) to ipify.
|
||||
[5b104ad1](https://github.com/ddclient/ddclient/commit/5b104ad116c023c3760129cab6e141f04f72b406)
|
||||
* All log messages are now written to STDERR, not a mix of STDOUT and STDERR.
|
||||
[#676](https://github.com/ddclient/ddclient/pull/676)
|
||||
* For `--protocol=freedns` and `--protocol=nfsn`, the core module
|
||||
`Digest::SHA` is now required. Previously, `Digest::SHA1` was used (if
|
||||
available) as an alternative to `Digest::SHA`.
|
||||
[#685](https://github.com/ddclient/ddclient/pull/685)
|
||||
* The `he` built-in web IP discovery service (`--webv4=he`, `--webv6=he`, and
|
||||
`--web=he`) was renamed to `he.net` for consistency with the new `he.net`
|
||||
protocol. The old name is still accepted but is deprecated and will be
|
||||
removed in a future version of ddclient.
|
||||
[#682](https://github.com/ddclient/ddclient/pull/682)
|
||||
* 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)
|
||||
|
||||
### New features
|
||||
|
||||
|
|
@ -41,6 +60,16 @@ repository history](https://github.com/ddclient/ddclient/commits/master).
|
|||
* The second and subsequent lines in a multi-line log message are now prefixed
|
||||
with a `|` character.
|
||||
[#676](https://github.com/ddclient/ddclient/pull/676)
|
||||
* `emailonly`: New `protocol` option that simply emails you when your IP
|
||||
address changes. [#654](https://github.com/ddclient/ddclient/pull/654)
|
||||
* `he.net`: Added support for updating Hurricane Electric records.
|
||||
[#682](https://github.com/ddclient/ddclient/pull/682)
|
||||
* `dyndns2`, `domeneshop`, `dnsmadeeasy`, `keysystems`, `woima`: The `server`
|
||||
option can now include `http://` or `https://` to control the use of TLS.
|
||||
If omitted, the value of the `ssl` option is used to determine the scheme.
|
||||
[#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)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
|
@ -73,6 +102,14 @@ 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)
|
||||
* `infomaniak`: Fixed frequent forced updates after 25 days (`max-interval`).
|
||||
[#691](https://github.com/ddclient/ddclient/issues/691)
|
||||
* `infomaniak`: Fixed incorrect parsing of server response.
|
||||
[#692](https://github.com/ddclient/ddclient/issues/692)
|
||||
* `regfishde`: Fixed IPv6 support.
|
||||
[#691](https://github.com/ddclient/ddclient/issues/691)
|
||||
|
||||
## 2023-11-23 v3.11.2
|
||||
|
||||
|
|
@ -136,7 +173,7 @@ Refer to [v3.11 release plan discussions](https://github.com/ddclient/ddclient/i
|
|||
|
||||
* Added support for domaindiscount24.com
|
||||
* Added support for njal.la
|
||||
|
||||
|
||||
## 2022-05-15 v3.10.0_2
|
||||
|
||||
### Bug fixes
|
||||
|
|
|
|||
|
|
@ -63,8 +63,13 @@ 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/geturl_response.pl \
|
||||
t/group_hosts_by.pl \
|
||||
t/header_ok.pl \
|
||||
t/interval_expired.pl \
|
||||
t/is-and-extract-ipv4.pl \
|
||||
t/is-and-extract-ipv6.pl \
|
||||
t/is-and-extract-ipv6-global.pl \
|
||||
|
|
@ -72,7 +77,8 @@ handwritten_tests = \
|
|||
t/parse_assignments.pl \
|
||||
t/skip.pl \
|
||||
t/ssl-validate.pl \
|
||||
t/write_cache.pl
|
||||
t/variable_defaults.pl \
|
||||
t/write_recap.pl
|
||||
generated_tests = \
|
||||
t/version.pl
|
||||
TESTS = $(handwritten_tests) $(generated_tests)
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -17,11 +17,13 @@ Dynamic DNS services currently supported include:
|
|||
* [ChangeIP](https://www.changeip.com)
|
||||
* [CloudFlare](https://www.cloudflare.com)
|
||||
* [ClouDNS](https://www.cloudns.net)
|
||||
* [DDNS.fm](https://www.ddns.fm/)
|
||||
* [DigitalOcean](https://www.digitalocean.com/)
|
||||
* [dinahosting](https://dinahosting.com)
|
||||
* [DonDominio](https://www.dondominio.com)
|
||||
* [DNS Made Easy](https://dnsmadeeasy.com)
|
||||
* [DNSExit](https://dnsexit.com/dns/dns-api)
|
||||
* [dnsHome.de](https://www.dnshome.de)
|
||||
* [Domeneshop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
|
||||
* [DslReports](https://www.dslreports.com)
|
||||
* [Duck DNS](https://duckdns.org)
|
||||
|
|
@ -33,6 +35,7 @@ Dynamic DNS services currently supported include:
|
|||
* [Gandi](https://gandi.net)
|
||||
* [GoDaddy](https://www.godaddy.com)
|
||||
* [Google](https://domains.google)
|
||||
* [Hurricane Electric](https://dns.he.net)
|
||||
* [Infomaniak](https://faq.infomaniak.com/2376)
|
||||
* [Loopia](https://www.loopia.se)
|
||||
* [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
|
||||
|
|
@ -130,9 +133,16 @@ Note that any issues prior to version v3.9.1 will not be listed here.
|
|||
If a fix is committed but not yet part of any tagged release, the notes here will reference the not-yet-released version number.
|
||||
|
||||
### v3.11.2 - v3.9.1: SSL parameter breaks HTTP-only IP acquisition
|
||||
The `ssl` parameter forces all connections to use HTTPS. While technically working as expected, this behavior keeps coming up as a pain point when using HTTP-only IP querying sites such as http://checkip.dyndns.org. For the future (v3.11.3), the behavior is changed to respect `http://` in a URL. A separate parameter to disallow all HTTP connections or warn about them may be added later.
|
||||
|
||||
**Fix**: v3.11.3 will use HTTP to connect to URLs starting with `http://`. See [here](https://github.com/ddclient/ddclient/pull/608) for more info.
|
||||
The `ssl` parameter forces all connections to use HTTPS. While technically
|
||||
working as expected, this behavior keeps coming up as a pain point when using
|
||||
HTTP-only IP querying sites such as http://checkip.dyndns.org. Starting with
|
||||
v4.0.0, the behavior is changed to respect `http://` in a URL. A separate
|
||||
parameter to disallow all HTTP connections or warn about them may be added
|
||||
later.
|
||||
|
||||
**Fix**: v4.0.0 uses HTTP to connect to URLs starting with `http://`. See
|
||||
[here](https://github.com/ddclient/ddclient/pull/608) for more info.
|
||||
|
||||
**Workaround**: Disable the SSL parameter
|
||||
|
||||
|
|
|
|||
17
configure.ac
17
configure.ac
|
|
@ -36,7 +36,18 @@ AC_PROG_MKDIR_P
|
|||
AC_PATH_PROG([FIND], [find])
|
||||
AS_IF([test -z "${FIND}"], [AC_MSG_ERROR(['find' utility not found])])
|
||||
|
||||
AC_PATH_PROG([CURL], [curl])
|
||||
AC_ARG_WITH([curl],
|
||||
[AS_HELP_STRING([[--with-curl[=CURL]]], [use CURL as absolute path to curl executable])],
|
||||
[],
|
||||
[with_curl=yes])
|
||||
AS_CASE([${with_curl}],
|
||||
[[yes]], [AC_PATH_PROG([CURL], [curl])],
|
||||
[[no]], [CURL=],
|
||||
[
|
||||
AC_MSG_CHECKING([for curl])
|
||||
CURL=${with_curl}
|
||||
AC_MSG_RESULT([${CURL}])
|
||||
]);
|
||||
AS_IF([test -z "${CURL}"], [AC_MSG_ERROR([curl not found])])
|
||||
|
||||
AX_WITH_PROG([PERL], perl)
|
||||
|
|
@ -49,6 +60,7 @@ AC_SUBST([PERL])
|
|||
# package doesn't depend on all of them, so their availability can't
|
||||
# be assumed.
|
||||
m4_foreach_w([_m], [
|
||||
Data::Dumper
|
||||
File::Basename
|
||||
File::Path
|
||||
File::Temp
|
||||
|
|
@ -63,7 +75,6 @@ m4_foreach_w([_m], [
|
|||
# then some tests will fail. Only prints a warning if not installed.
|
||||
m4_foreach_w([_m], [
|
||||
B
|
||||
Data::Dumper
|
||||
File::Spec::Functions
|
||||
File::Temp
|
||||
], [AX_PROG_PERL_MODULES([_m], [],
|
||||
|
|
@ -80,6 +91,8 @@ m4_foreach_w([_m], [
|
|||
HTTP::Message::PSGI
|
||||
HTTP::Request
|
||||
HTTP::Response
|
||||
JSON::PP
|
||||
LWP::UserAgent
|
||||
Scalar::Util
|
||||
Test::MockModule
|
||||
Test::TCP
|
||||
|
|
|
|||
|
|
@ -16,13 +16,16 @@
|
|||
## are mentioned here.
|
||||
##
|
||||
######################################################################
|
||||
|
||||
## Use encryption (TLS) when the scheme (either "http://" or "https://") is
|
||||
## missing from a URL. Defaults to "yes".
|
||||
#ssl=yes
|
||||
|
||||
daemon=300 # check every 300 seconds
|
||||
syslog=yes # log update msgs to syslog
|
||||
mail=root # mail all msgs to root
|
||||
mail-failure=root # mail failed update msgs to root
|
||||
pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||
ssl=yes # use ssl-support. Works with
|
||||
# ssl-library
|
||||
# postscript=script # run script after updating. The
|
||||
# new IP is added as argument.
|
||||
#
|
||||
|
|
@ -222,6 +225,13 @@ ssl=yes # use ssl-support. Works with
|
|||
# password=my-auto-generated-password
|
||||
# my.domain.tld, otherhost.domain.tld
|
||||
|
||||
##
|
||||
## Hurricane Electric (dns.he.net)
|
||||
##
|
||||
# protocol=he.net, \
|
||||
# password=my-genereated-password \
|
||||
# myhost.example.com
|
||||
|
||||
##
|
||||
## Duckdns (http://www.duckdns.org/)
|
||||
##
|
||||
|
|
@ -238,6 +248,14 @@ ssl=yes # use ssl-support. Works with
|
|||
# password=my-token
|
||||
# myhost
|
||||
|
||||
##
|
||||
## DDNS.FM (https://ddns.fm/)
|
||||
##
|
||||
#
|
||||
# protocol=ddns.fm,
|
||||
# password=my-token
|
||||
# myhost.example.com
|
||||
|
||||
##
|
||||
## MyOnlinePortal (http://myonlineportal.net)
|
||||
##
|
||||
|
|
@ -391,3 +409,18 @@ ssl=yes # use ssl-support. Works with
|
|||
# password=ddns_password
|
||||
# redirect=2
|
||||
# example.com
|
||||
|
||||
##
|
||||
## Email Only
|
||||
##
|
||||
# protocol=emailonly
|
||||
# host.example.com
|
||||
|
||||
##
|
||||
## dnsHome.de
|
||||
##
|
||||
# protocol=dyndns2 \
|
||||
# server=www.dnshome.de \
|
||||
# login=subdomain.domain.tld \
|
||||
# password=your_password \
|
||||
# subdomain.domain.tld
|
||||
|
|
|
|||
2943
ddclient.in
2943
ddclient.in
File diff suppressed because it is too large
Load diff
203
t/dnsexit2.pl
Normal file
203
t/dnsexit2.pl
Normal 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();
|
||||
|
|
@ -39,23 +39,30 @@ subtest "get_ip_from_interface tests" => sub {
|
|||
}
|
||||
};
|
||||
|
||||
subtest "Get default interface and IP for test system" => sub {
|
||||
subtest "Get default interface and IP for test system (IPv4)" => sub {
|
||||
my $interface = ddclient::get_default_interface(4);
|
||||
if ($interface) {
|
||||
isnt($interface, "lo", "Check for loopback 'lo'");
|
||||
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
||||
my $ip1 = ddclient::get_ip_from_interface("default", 4);
|
||||
my $ip2 = ddclient::get_ip_from_interface($interface, 4);
|
||||
is($ip1, $ip2, "Check IPv4 from default interface");
|
||||
plan(skip_all => 'no IPv4 interface') if !$interface;
|
||||
isnt($interface, "lo", "Check for loopback 'lo'");
|
||||
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
||||
my $ip1 = ddclient::get_ip_from_interface("default", 4);
|
||||
my $ip2 = ddclient::get_ip_from_interface($interface, 4);
|
||||
is($ip1, $ip2, "Check IPv4 from default interface");
|
||||
SKIP: {
|
||||
skip('default interface does not have an appropriate IPv4 addresses') if !$ip1;
|
||||
ok(ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)");
|
||||
}
|
||||
$interface = ddclient::get_default_interface(6);
|
||||
if ($interface) {
|
||||
isnt($interface, "lo", "Check for loopback 'lo'");
|
||||
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
||||
my $ip1 = ddclient::get_ip_from_interface("default", 6);
|
||||
my $ip2 = ddclient::get_ip_from_interface($interface, 6);
|
||||
is($ip1, $ip2, "Check IPv6 from default interface");
|
||||
};
|
||||
|
||||
subtest "Get default interface and IP for test system (IPv6)" => sub {
|
||||
my $interface = ddclient::get_default_interface(6);
|
||||
plan(skip_all => 'no IPv6 interface') if !$interface;
|
||||
isnt($interface, "lo", "Check for loopback 'lo'");
|
||||
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
||||
my $ip1 = ddclient::get_ip_from_interface("default", 6);
|
||||
my $ip2 = ddclient::get_ip_from_interface($interface, 6);
|
||||
is($ip1, $ip2, "Check IPv6 from default interface");
|
||||
SKIP: {
|
||||
skip('default interface does not have an appropriate IPv6 addresses') if !$ip1;
|
||||
ok(ddclient::is_ipv6($ip1), "Valid IPv6 from get_ip_from_interface($interface)");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
27
t/geturl_response.pl
Normal file
27
t/geturl_response.pl
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use Test::More;
|
||||
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||
|
||||
# Fake curl. Use the printf utility, which can process escapes. This allows Perl to drive the fake
|
||||
# curl with plain ASCII and get arbitrary bytes back, avoiding problems caused by any encoding that
|
||||
# might be done by Perl (e.g., "use open ':encoding(UTF-8)';").
|
||||
my @fakecurl = ('sh', '-c', 'printf %b "$1"', '--');
|
||||
|
||||
my @test_cases = (
|
||||
{
|
||||
desc => 'binary body',
|
||||
# Body is UTF-8 encoded ✨ (U+2728 Sparkles) followed by a 0xff byte (invalid UTF-8).
|
||||
printf => join('\r\n', ('HTTP/1.1 200 OK', '', '\0342\0234\0250\0377')),
|
||||
# The raw bytes should come through as equally valued codepoints. They must not be decoded.
|
||||
want => "HTTP/1.1 200 OK\n\n\xe2\x9c\xa8\xff",
|
||||
},
|
||||
);
|
||||
|
||||
for my $tc (@test_cases) {
|
||||
@ddclient::curl = (@fakecurl, $tc->{printf});
|
||||
$ddclient::curl if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||
my $got = ddclient::geturl(url => 'http://ignored');
|
||||
is($got, $tc->{want}, $tc->{desc});
|
||||
}
|
||||
|
||||
done_testing();
|
||||
110
t/group_hosts_by.pl
Normal file
110
t/group_hosts_by.pl
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
use Test::More;
|
||||
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||
eval { require Data::Dumper; } or skip($@, 1);
|
||||
Data::Dumper->import();
|
||||
|
||||
my $h1 = 'h1';
|
||||
my $h2 = 'h2';
|
||||
my $h3 = 'h3';
|
||||
|
||||
$ddclient::config{$h1} = {
|
||||
common => 'common',
|
||||
h1h2 => 'h1 and h2',
|
||||
unique => 'h1',
|
||||
falsy => 0,
|
||||
maybeunset => 'unique',
|
||||
};
|
||||
$ddclient::config{$h2} = {
|
||||
common => 'common',
|
||||
h1h2 => 'h1 and h2',
|
||||
unique => 'h2',
|
||||
falsy => '',
|
||||
maybeunset => undef, # should not be grouped with unset
|
||||
};
|
||||
$ddclient::config{$h3} = {
|
||||
common => 'common',
|
||||
h1h2 => 'unique',
|
||||
unique => 'h3',
|
||||
falsy => undef,
|
||||
# maybeunset is intentionally not set
|
||||
};
|
||||
|
||||
my @test_cases = (
|
||||
{
|
||||
desc => 'empty attribute set yields single group with all hosts',
|
||||
groupby => [qw()],
|
||||
want => [{cfg => {}, hosts => [$h1, $h2, $h3]}],
|
||||
},
|
||||
{
|
||||
desc => 'common attribute yields single group with all hosts',
|
||||
groupby => [qw(common)],
|
||||
want => [{cfg => {common => 'common'}, hosts => [$h1, $h2, $h3]}],
|
||||
},
|
||||
{
|
||||
desc => 'subset share a value',
|
||||
groupby => [qw(h1h2)],
|
||||
want => [
|
||||
{cfg => {h1h2 => 'h1 and h2'}, hosts => [$h1, $h2]},
|
||||
{cfg => {h1h2 => 'unique'}, hosts => [$h3]},
|
||||
],
|
||||
},
|
||||
{
|
||||
desc => 'all unique',
|
||||
groupby => [qw(unique)],
|
||||
want => [
|
||||
{cfg => {unique => 'h1'}, hosts => [$h1]},
|
||||
{cfg => {unique => 'h2'}, hosts => [$h2]},
|
||||
{cfg => {unique => 'h3'}, hosts => [$h3]},
|
||||
],
|
||||
},
|
||||
{
|
||||
desc => 'combination',
|
||||
groupby => [qw(common h1h2)],
|
||||
want => [
|
||||
{cfg => {common => 'common', h1h2 => 'h1 and h2'}, hosts => [$h1, $h2]},
|
||||
{cfg => {common => 'common', h1h2 => 'unique'}, hosts => [$h3]},
|
||||
],
|
||||
},
|
||||
{
|
||||
desc => 'falsy values',
|
||||
groupby => [qw(falsy)],
|
||||
want => [
|
||||
{cfg => {falsy => 0}, hosts => [$h1]},
|
||||
{cfg => {falsy => ''}, hosts => [$h2]},
|
||||
{cfg => {falsy => undef}, hosts => [$h3]},
|
||||
],
|
||||
},
|
||||
{
|
||||
desc => 'set, unset, undef',
|
||||
groupby => [qw(maybeunset)],
|
||||
want => [
|
||||
{cfg => {maybeunset => 'unique'}, hosts => [$h1]},
|
||||
{cfg => {maybeunset => undef}, hosts => [$h2]},
|
||||
{cfg => {}, hosts => [$h3]},
|
||||
],
|
||||
},
|
||||
{
|
||||
desc => 'missing attribute',
|
||||
groupby => [qw(thisdoesnotexist)],
|
||||
want => [{cfg => {}, hosts => [$h1, $h2, $h3]}],
|
||||
},
|
||||
);
|
||||
|
||||
for my $tc (@test_cases) {
|
||||
my @got = ddclient::group_hosts_by([$h1, $h2, $h3], @{$tc->{groupby}});
|
||||
# @got is used as a set of sets. Sort everything to make comparison easier.
|
||||
$_->{hosts} = [sort(@{$_->{hosts}})] for @got;
|
||||
@got = sort({
|
||||
for (my $i = 0; $i < @{$a->{hosts}} && $i < @{$b->{hosts}}; ++$i) {
|
||||
my $x = $a->{hosts}[$i] cmp $b->{hosts}[$i];
|
||||
return $x if $x != 0;
|
||||
}
|
||||
return @{$a->{hosts}} <=> @{$b->{hosts}};
|
||||
} @got);
|
||||
is_deeply(\@got, $tc->{want}, $tc->{desc})
|
||||
or diag(Data::Dumper->new([\@got, $tc->{want}],
|
||||
[qw(got want)])->Sortkeys(1)->Useqq(1)->Dump());
|
||||
}
|
||||
|
||||
done_testing();
|
||||
74
t/header_ok.pl
Normal file
74
t/header_ok.pl
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
use Test::More;
|
||||
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||
my $have_mock = eval { require Test::MockModule; };
|
||||
|
||||
my $failmsg;
|
||||
my $module;
|
||||
if ($have_mock) {
|
||||
$module = Test::MockModule->new('ddclient');
|
||||
# Note: 'mock' is used instead of 'redefine' because 'redefine' is not available in the versions
|
||||
# of Test::MockModule distributed with old Debian and Ubuntu releases.
|
||||
$module->mock('failed', sub { $failmsg //= ''; $failmsg .= sprintf(shift, @_) . "\n"; });
|
||||
}
|
||||
|
||||
my @test_cases = (
|
||||
{
|
||||
desc => 'malformed not OK',
|
||||
input => 'malformed',
|
||||
want => 0,
|
||||
wantmsg => qr/unexpected/,
|
||||
},
|
||||
{
|
||||
desc => 'HTTP/1.1 200 OK',
|
||||
input => 'HTTP/1.1 200 OK',
|
||||
want => 1,
|
||||
},
|
||||
{
|
||||
desc => 'HTTP/2 200 OK',
|
||||
input => 'HTTP/2 200 OK',
|
||||
want => 1,
|
||||
},
|
||||
{
|
||||
desc => 'HTTP/3 200 OK',
|
||||
input => 'HTTP/3 200 OK',
|
||||
want => 1,
|
||||
},
|
||||
{
|
||||
desc => '401 not OK, fallback message',
|
||||
input => 'HTTP/1.1 401 ',
|
||||
want => 0,
|
||||
wantmsg => qr/authentication failed/,
|
||||
},
|
||||
{
|
||||
desc => '403 not OK, fallback message',
|
||||
input => 'HTTP/1.1 403 ',
|
||||
want => 0,
|
||||
wantmsg => qr/not authorized/,
|
||||
},
|
||||
{
|
||||
desc => 'other 4xx not OK',
|
||||
input => 'HTTP/1.1 456 bad',
|
||||
want => 0,
|
||||
wantmsg => qr/bad/,
|
||||
},
|
||||
{
|
||||
desc => 'only first line is logged on error',
|
||||
input => "HTTP/1.1 404 not found\n\nbody",
|
||||
want => 0,
|
||||
wantmsg => qr/(?!body)/,
|
||||
},
|
||||
);
|
||||
|
||||
for my $tc (@test_cases) {
|
||||
subtest $tc->{desc} => sub {
|
||||
$failmsg = '';
|
||||
is(ddclient::header_ok('host', $tc->{input}), $tc->{want}, 'return value matches');
|
||||
SKIP: {
|
||||
skip('Test::MockModule not available') if !$have_mock;
|
||||
like($failmsg, $tc->{wantmsg} // qr/^$/, 'fail message matches');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
done_testing();
|
||||
51
t/interval_expired.pl
Normal file
51
t/interval_expired.pl
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use Test::More;
|
||||
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||
|
||||
my $h = 't/interval_expired.pl';
|
||||
|
||||
my $default_now = 1000000000;
|
||||
|
||||
my @test_cases = (
|
||||
{
|
||||
interval => 'inf',
|
||||
want => 0,
|
||||
},
|
||||
{
|
||||
now => 'inf',
|
||||
interval => 'inf',
|
||||
want => 0,
|
||||
},
|
||||
{
|
||||
cache => '-inf',
|
||||
interval => 'inf',
|
||||
want => 0,
|
||||
},
|
||||
{
|
||||
cache => undef, # Falsy cache value.
|
||||
interval => 'inf',
|
||||
want => 0,
|
||||
},
|
||||
{
|
||||
now => 0,
|
||||
cache => 0, # Different kind of falsy cache value.
|
||||
interval => 'inf',
|
||||
want => 0,
|
||||
},
|
||||
);
|
||||
|
||||
for my $tc (@test_cases) {
|
||||
$tc->{now} //= $default_now;
|
||||
# For convenience, $tc->{cache} is an offset from $tc->{now}, not an absolute time..
|
||||
my $cachetime = $tc->{now} + $tc->{cache} if defined($tc->{cache});
|
||||
$ddclient::config{$h} = {'interval' => $tc->{interval}};
|
||||
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||
$ddclient::cache{$h} = {'cached-time' => $cachetime} if defined($cachetime);
|
||||
%ddclient::cache if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||
$ddclient::now = $tc->{now};
|
||||
$ddclient::now if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||
my $desc = "now=$tc->{now}, cache=${\($cachetime // 'undef')}, interval=$tc->{interval}";
|
||||
is(ddclient::interval_expired($h, 'cached-time', 'interval'), $tc->{want}, $desc);
|
||||
}
|
||||
|
||||
done_testing();
|
||||
32
t/variable_defaults.pl
Normal file
32
t/variable_defaults.pl
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use Test::More;
|
||||
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||
|
||||
my %variable_collections = (
|
||||
map({ ($_ => $ddclient::variables{$_}) } grep($_ ne 'merged', keys(%ddclient::variables))),
|
||||
map({ ("protocol=$_" => $ddclient::protocols{$_}{variables}); } keys(%ddclient::protocols)),
|
||||
);
|
||||
my %seen;
|
||||
my @test_cases = (
|
||||
map({
|
||||
my $vcn = $_;
|
||||
my $vc = $variable_collections{$_};
|
||||
map({
|
||||
my $def = $vc->{$_};
|
||||
my $seen = exists($seen{$def});
|
||||
$seen{$def} = undef;
|
||||
({desc => "$vcn $_", def => $vc->{$_}}) x !$seen;
|
||||
} sort(keys(%$vc)));
|
||||
} sort(keys(%variable_collections))),
|
||||
);
|
||||
for my $tc (@test_cases) {
|
||||
if ($tc->{def}{required}) {
|
||||
is($tc->{def}{default}, undef, "'$tc->{desc}' (required) has no default");
|
||||
} else {
|
||||
my $norm;
|
||||
my $valid = eval { $norm = ddclient::check_value($tc->{def}{default}, $tc->{def}); 1; };
|
||||
ok($valid, "'$tc->{desc}' (optional) has a valid default");
|
||||
is($norm, $tc->{def}{default}, "'$tc->{desc}' default normalizes to itself") if $valid;
|
||||
}
|
||||
}
|
||||
done_testing();
|
||||
|
|
@ -35,7 +35,7 @@ my @test_cases = (
|
|||
|
||||
for my $tc (@test_cases) {
|
||||
$warning = undef;
|
||||
ddclient::write_cache($tc->{f});
|
||||
ddclient::write_recap($tc->{f});
|
||||
subtest $tc->{name} => sub {
|
||||
if (defined($tc->{warning_regex})) {
|
||||
like($warning, $tc->{warning_regex}, "expected warning message");
|
||||
Loading…
Reference in a new issue