Compare commits

..

No commits in common. "main" and "v4.0.0-rc.1" have entirely different histories.

19 changed files with 292 additions and 651 deletions

View file

@ -29,21 +29,3 @@ jobs:
git show "${out}" >&2
exit 1
}
no-autosquash:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'pr-permit-autosquash') }}
name: No --autosquash commits
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 'No commits with messages starting with "fixup!", "squash!", or "amend!"'
run: |
log() { printf %s\\n "$*" >&2; }
error() { log "ERROR: $@"; }
fatal() { error "$@"; exit 1; }
try() { log "Running command $@"; "$@" || fatal "'$@' failed"; }
out=$(try git log --oneline '${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}') || exit 1
! grep -E '^[^ ]* (fixup|squash|amend)!' <<EOF || fatal "--autosquash commits not allowed without the 'pr-permit-autosquash' label"
${out}
EOF

View file

@ -3,48 +3,15 @@
This document describes notable changes. For details, see the [source code
repository history](https://github.com/ddclient/ddclient/commits/main).
## v4.0.1-alpha (unreleased work-in-progress)
## 2025-01-19 v4.0.0
## 2024-12-25 v4.0.0-rc.1
### Breaking changes
* ddclient now looks for `ddclient.conf` in `${sysconfdir}/ddclient` by
default instead of `${sysconfdir}`.
[#789](https://github.com/ddclient/ddclient/pull/789)
To retain the previous behavior, pass `'--with-confdir=${sysconfdir}'` to
`configure`. For example:
```shell
# Before v4.0.0:
./configure --sysconfdir=/etc
# Equivalent with v4.0.0 and later (the single quotes are intentional):
./configure --sysconfdir=/etc --with-confdir='${sysconfdir}'
```
or:
```shell
# Before v4.0.0:
./configure --sysconfdir=/etc/ddclient
# Equivalent with v4.0.0 and later:
./configure --sysconfdir=/etc
```
* 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 string argument to `--cmdv4` or `--cmdv6` is now executed as-is by the
system's shell, matching the behavior of the deprecated `--cmd` option.
This makes it possible to pass command-line arguments, which reduces the
need for a custom wrapper script. Beware that the string is also subject to
the shell's command substitution, quote handling, variable expansion, field
splitting, etc., so you may need to add extra escaping to ensure that any
special characters are preserved literally.
[#766](https://github.com/ddclient/ddclient/pull/766)
* The default web service for `--webv4` and `--webv6` has changed from Google
Domains (which has shut down) to ipify.
[5b104ad1](https://github.com/ddclient/ddclient/commit/5b104ad116c023c3760129cab6e141f04f72b406)
@ -86,8 +53,6 @@ repository history](https://github.com/ddclient/ddclient/commits/main).
### New features
* New `--mail-from` option to control the "From:" header of email messages.
[#565](https://github.com/ddclient/ddclient/pull/565)
* Simultaneous/separate updating of IPv4 (A) records and IPv6 (AAAA) records
is now supported in the following services: `gandi`
([#558](https://github.com/ddclient/ddclient/pull/558)), `nsupdate`
@ -134,8 +99,6 @@ repository history](https://github.com/ddclient/ddclient/commits/main).
[#726](https://github.com/ddclient/ddclient/pull/726)
* `porkbun`: The update URL hostname is now configurable via the `server`
option. [#752](https://github.com/ddclient/ddclient/pull/752)
* `dnsexit2`: Multiple hosts are updated in a single API call when possible.
[#684](https://github.com/ddclient/ddclient/pull/684)
### Bug fixes

View file

@ -28,8 +28,8 @@ $(subst_files): Makefile
-e 's|@PACKAGE_VERSION[@]|$(PACKAGE_VERSION)|g' \
-e '1 s|^#\!.*perl$$|#\!$(PERL)|g' \
-e 's|@localstatedir[@]|$(localstatedir)|g' \
-e 's|@confdir[@]|$(confdir)|g' \
-e 's|@runstatedir[@]|$(runstatedir)|g' \
-e 's|@sysconfdir[@]|$(sysconfdir)|g' \
-e 's|@CURL[@]|$(CURL)|g' \
"$${in}" >'$@'.tmp && \
{ ! test -x "$${in}" || chmod +x '$@'.tmp; }
@ -40,7 +40,7 @@ ddclient.conf: $(srcdir)/ddclient.conf.in
bin_SCRIPTS = ddclient
conf_DATA = ddclient.conf
sysconf_DATA = ddclient.conf
install-data-local:
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
@ -77,7 +77,6 @@ handwritten_tests = \
t/skip.pl \
t/ssl-validate.pl \
t/update_nics.pl \
t/use_cmd.pl \
t/use_web.pl \
t/variable_defaults.pl \
t/write_recap.pl
@ -157,9 +156,7 @@ EXTRA_DIST += $(handwritten_tests) \
t/lib/ddclient/Test/Fake/HTTPD/dummy-ca-cert.pem \
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-cert.pem \
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-key.pem \
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem \
t/lib/ddclient/t.pm \
t/lib/ddclient/t/HTTPD.pm \
t/lib/ddclient/t/Logger.pm \
t/lib/ddclient/t/ip.pm \
t/lib/ok.pm

View file

@ -3,36 +3,6 @@
`ddclient` is a Perl client used to update dynamic DNS entries for accounts
on many dynamic DNS services. It uses `curl` for internet access.
on docker compose
```docker-compose
services:
ddclient:
image: lscr.io/linuxserver/ddclient:latest
container_name: ddclient
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Rome
volumes:
- /home/orangepi/dockerfiles/ddclient/config:/config
restart: unless-stopped
```
file ddclient.conf per servizio DDNS di dynu.com da mettere nel folder config
```file
daemon=60 # 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=/var/run/ddclient/ddclient.pid # record PID in file.
use=web, web=checkip.dynu.com/, web-skip='IP Address'
protocol=dyndns2 # default protocol
server=api.dynu.com
# default login
login=FabioMich66 # your default user
password=Master66! # your default password
wildcard=yes
patachina.casacam.net
```
## Alternatives
You might also want to consider using one of the following, if they support
@ -135,7 +105,7 @@ operating system. See the image to the right for a list of distributions with a
```shell
./configure \
--prefix=/usr \
--sysconfdir=/etc \
--sysconfdir=/etc/ddclient \
--localstatedir=/var
make
make VERBOSE=1 check

View file

@ -23,18 +23,6 @@ AC_REQUIRE_AUX_FILE([tap-driver.sh])
AM_INIT_AUTOMAKE([1.11 -Wall -Werror foreign subdir-objects parallel-tests])
AM_SILENT_RULES
m4_define([CONFDIR_DEFAULT], [${sysconfdir}/AC_PACKAGE_NAME])
AC_ARG_WITH(
[confdir],
[AS_HELP_STRING(
[--with-confdir=DIR],
m4_expand([[look for ddclient.conf in DIR @<:@default: ]CONFDIR_DEFAULT[@:>@]]))],
[],
# The single quotes are intentional; see:
# https://www.gnu.org/software/automake/manual/html_node/Uniform.html
[with_confdir='CONFDIR_DEFAULT'])
AC_SUBST([confdir], [${with_confdir}])
AC_PROG_MKDIR_P
# The Fedora Docker image doesn't come with the 'findutils' package.
@ -87,7 +75,6 @@ m4_foreach_w([_m], [
# then some tests will fail. Only prints a warning if not installed.
m4_foreach_w([_m], [
B
Exporter
File::Spec::Functions
File::Temp
List::Util
@ -101,6 +88,7 @@ m4_foreach_w([_m], [
# prints a warning if not installed.
m4_foreach_w([_m], [
Carp
Exporter
HTTP::Daemon=6.12
HTTP::Daemon::SSL
HTTP::Message::PSGI
@ -112,7 +100,6 @@ m4_foreach_w([_m], [
Test::Warnings
Time::HiRes
URI
parent
], [AX_PROG_PERL_MODULES([_m], [],
[AC_MSG_WARN([some tests may be skipped due to missing module _m])])])

View file

@ -25,9 +25,6 @@ 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
# mail-from=root # set the email "From:" header to "root". If
# unset (the default) or empty, the from address
# depends on your system's default behavior.
pid=@runstatedir@/ddclient.pid # record PID in file.
# postscript=script # run script after updating. The new IP is
# added as argument.
@ -53,10 +50,6 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
## To obtain an IP address from FW status page (using fw-login, fw-password)
#use=fw, fw=192.168.1.254/status.htm, fw-skip='IP Address' # found after IP Address
#
## To obtain an IP address via UPnP from router
## Requires miniupnpc to be installed on the system.
#use=cmd, cmd=external-ip
#
## To obtain an IP address from Web status page (using the proxy if defined)
## by default, checkip.dyndns.org is used if you use the dyndns protocol.
## Using use=web is enough to get it working.
@ -137,10 +130,10 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
##
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
##
# protocol=nfsn, \
# zone=example.com, \
# protocol = nfsn, \
# login=member-login, \
# password=api-key \
# password=api-key, \
# zone=example.com \
# example.com,subdomain.example.com
##

View file

@ -78,7 +78,7 @@ use Sys::Hostname;
#
# For consistency and to match user expectations, the release part of the version is always three
# components: MAJOR.MINOR.PATCH.
use version 0.77; our $VERSION = version->declare('v4.0.1.0_0');
use version 0.77; our $VERSION = version->declare('v4.0.0.0_901');
sub parse_version {
my ($v) = @_;
@ -132,7 +132,7 @@ sub subst_var {
return $subst;
}
my $etc = subst_var('@confdir@', '/etc/ddclient');
my $etc = subst_var('@sysconfdir@', '/etc/ddclient');
my $cachedir = subst_var('@localstatedir@', '/var') . '/cache/ddclient';
our @curl = (subst_var('@CURL@', 'curl'));
@ -704,7 +704,6 @@ our %cfgvars = (
'priority' => setv(T_STRING,0, 'notice', undef),
'mail' => setv(T_EMAIL, 0, undef, undef),
'mail-failure' => setv(T_EMAIL, 0, undef, undef),
'mail-from' => setv(T_EMAIL, 0, undef, undef),
'max-warn' => setv(T_NUMBER,0, 1, undef),
'exec' => setv(T_BOOL, 0, 1, undef),
@ -1428,7 +1427,6 @@ my @opt = (
["max-warn", "=i", "--max-warn=<max> : log at most <max> warning messages for undefined IP address"],
["mail", "=s", "--mail=<address> : e-mail messages to <address>"],
["mail-failure", "=s", "--mail-failure=<addr> : e-mail messages for failed updates to <addr>"],
["mail-from", "=s", '--mail-from=<addr> : set the "From:" header in e-mail messages to <addr> if non-empty'],
["exec", "!", "--{no}exec : do {not} execute; just show what would be done"],
["debug", "!", "--{no}debug : print {no} debugging information"],
["verbose", "!", "--{no}verbose : print {no} verbose information"],
@ -2406,10 +2404,8 @@ sub sendmail {
$recipients = opt('mail-failure');
}
if ($emailbody && $recipients && $emailbody ne $last_emailbody) {
my $sender = opt('mail-from') // '';
pipecmd("sendmail -oi $recipients",
"To: $recipients",
$sender ne '' ? ("From: $sender") : (),
"Subject: status report from $program\@$hostname",
"\r\n",
$emailbody,
@ -2510,11 +2506,9 @@ sub ynu {
# provided (it is ignored if the `msg` keyword is present).
sub log {
my $self = shift;
my %args = (label => '', @_ % 2 ? (msg => pop) : (), @_);
my %args = (@_ % 2 ? (msg => pop) : (), @_);
$args{ctx} = [$args{ctx} // ()] if ref($args{ctx}) eq '';
$self->_log(\%args);
$self->_failed() if $args{label} eq 'FAILED';
$self->_abort() if $args{label} eq 'FATAL';
return $self->_log(\%args);
}
sub _log {
@ -2523,11 +2517,10 @@ sub ynu {
# the caller's arrayref (in case it is reused in a future call).
$args->{ctx} = [@{$self->{ctx}}, @{$args->{ctx}}];
return $self->{parent}->_log($args) if defined($self->{parent});
return if $args->{label} eq 'DEBUG' && !ddclient::opt('debug');
return if $args->{label} eq 'INFO' && !ddclient::opt('verbose');
my $buffer = $args->{msg} // '';
chomp($buffer);
if (!$args->{raw}) {
$args->{label} //= '';
my $prefix = $args->{label} ne '' ? sprintf("%-8s ", $args->{label} . ':') : '';
$prefix .= "[$_]" for @{$args->{ctx}};
$prefix .= '> ' if $prefix;
@ -2547,20 +2540,6 @@ sub ynu {
}
}
}
sub _failed {
my ($self) = @_;
return $self->{parent}->_failed() if defined($self->{parent});
$ddclient::result = 'FAILED';
$ddclient::result if 0; # Suppress spurious "used only once: possible typo" warning.
}
sub _abort {
my ($self) = @_;
return $self->{parent}->_abort() if defined($self->{parent});
ddclient::sendmail();
exit(1);
}
}
# Intended use:
@ -2569,12 +2548,12 @@ sub pushlogctx { my ($ctx) = @_; return ddclient::Logger->new($ctx, $_l); }
sub logmsg { $_l->log(@_); }
sub _logmsg_fmt { $_[0] eq 'ctx' ? (shift, shift) : (), (@_ > 1) ? sprintf(shift, @_) : shift; }
sub info { logmsg(email => 1, label => 'INFO', _logmsg_fmt(@_)); }
sub debug { logmsg( label => 'DEBUG', _logmsg_fmt(@_)); }
sub info { logmsg(email => 1, label => 'INFO', _logmsg_fmt(@_)) if opt('verbose'); }
sub debug { logmsg( label => 'DEBUG', _logmsg_fmt(@_)) if opt('debug'); }
sub warning { logmsg(email => 1, label => 'WARNING', _logmsg_fmt(@_)); }
sub fatal { logmsg(email => 1, label => 'FATAL', _logmsg_fmt(@_)); }
sub fatal { logmsg(email => 1, label => 'FATAL', _logmsg_fmt(@_)); sendmail(); exit(1); }
sub success { logmsg(email => 1, label => 'SUCCESS', _logmsg_fmt(@_)); }
sub failed { logmsg(email => 1, label => 'FAILED', _logmsg_fmt(@_)); }
sub failed { logmsg(email => 1, label => 'FAILED', _logmsg_fmt(@_)); $result = 'FAILED'; }
sub prettytime { return scalar(localtime(shift)); }
@ -3358,9 +3337,10 @@ sub get_ipv4 {
} elsif ($p{'usev4'} eq 'cmdv4') {
## Obtain IPv4 address by executing the command in "cmdv4=<command>"
warning("'--cmd-skip' ignored for '--usev4=$p{'usev4'}'")
if opt('verbose') && defined($p{'cmd-skip'});
if (opt('verbose') && $p{'cmd-skip'});
if ($arg) {
$reply = qx{$arg};
my $sys_cmd = quotemeta($arg);
$reply = qx{$sys_cmd};
$reply = '' if $?;
}
} elsif ($p{'usev4'} eq 'webv4') {
@ -3470,10 +3450,10 @@ sub get_ipv6 {
$ipv6 = get_ip_from_interface($arg, 6);
} elsif ($p{'usev6'} eq 'cmdv6' || $p{'usev6'} eq 'cmd') {
## Obtain IPv6 address by executing the command in "cmdv6=<command>"
warning("'--cmd-skip' ignored for '--usev6=$p{'usev6'}'")
if opt('verbose') && defined($p{'cmd-skip'});
warning("'--cmd-skip' ignored") if opt('verbose') && p{'cmd-skip'};
if ($arg) {
$reply = qx{$arg};
my $sys_cmd = quotemeta($arg);
$reply = qx{$sys_cmd};
$reply = '' if $?;
}
} elsif ($p{'usev6'} eq 'webv6' || $p{'usev6'} eq 'web') {
@ -3939,7 +3919,7 @@ sub nic_dyndns2_update {
'abuse' => 'The hostname specified is blocked for abuse; you should receive an email notification which provides an unblock request link. More info can be found on https://www.dyndns.com/support/abuse.html',
'numhost' => 'System error: Too many or too few hosts found. Contact support@dyndns.org',
'dnserr' => 'System error: DNS error encountered. Contact support@dyndns.org',
'nochg' => 'No update required; unnecessary attempts to change the current address are considered abusive',
'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive',
);
my @group_by_attrs = qw(
backupmx
@ -4097,33 +4077,33 @@ EoEXAMPLE
######################################################################
sub nic_dnsexit2_update {
my $self = shift;
# 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 if !defined(opt('zone', $h));
dnsexit2_update_host($h);
}
dnsexit2_update_hostgroup($_) for group_hosts_by(\@_, qw(password path server ssl zone));
}
sub dnsexit2_update_hostgroup {
my ($group) = @_;
return unless @{$group->{hosts}} > 0;
local $_l = pushlogctx(join(', ', @{$group->{hosts}}));
my %hostips;
my @updates;
for my $h (@{$group->{hosts}}) {
local $_l = pushlogctx($h) if @{$group->{hosts}} > 1;
sub dnsexit2_update_host {
my ($h) = @_;
local $_l = pushlogctx($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$group->{cfg}{'zone'}\E$//) {
my $zone = opt('zone', $h);
if ($name =~ s/(?:^|\.)\Q$zone\E$//) {
# The zone was successfully trimmed from $name.
} else {
fatal("hostname does not end with the zone: $group->{cfg}{'zone'}");
fatal("hostname does not end with the zone: " . opt('zone', $h));
}
# The IPv4 and IPv6 addresses must be updated together in a single API call.
my %ips;
my @updates;
for my $ipv ('4', '6') {
my $ip = delete($config{$h}{"wantipv$ipv"}) or next;
$hostips{$h}{$ipv} = $ip;
$ips{$ipv} = $ip;
info("updating IPv$ipv address to $ip");
$recap{$h}{"status-ipv$ipv"} = 'failed';
push(@updates, {
@ -4132,20 +4112,19 @@ sub dnsexit2_update_hostgroup {
content => $ip,
ttl => opt('ttl', $h),
});
}
}
return unless @updates > 0;
};
my $url = opt('server', $h) . opt('path', $h);
my $reply = geturl(
proxy => opt('proxy'),
url => $group->{cfg}{'server'} . $group->{cfg}{'path'},
url => $url,
headers => [
'Content-Type: application/json',
'Accept: application/json',
],
method => 'POST',
data => encode_json({
apikey => $group->{cfg}{'password'},
domain => $group->{cfg}{'zone'},
apikey => opt('password', $h),
domain => $zone,
update => \@updates,
}),
);
@ -4192,16 +4171,13 @@ sub dnsexit2_update_hostgroup {
return;
}
success($message);
keys(%hostips); # Reset internal iterator.
while (my ($h, $ips) = each(%hostips)) {
$recap{$h}{'mtime'} = $now;
keys(%$ips); # Reset internal iterator.
while (my ($ipv, $ip) = each(%$ips)) {
keys(%ips); # Reset internal iterator.
while (my ($ipv, $ip) = each(%ips)) {
$recap{$h}{"ipv$ipv"} = $ip;
$recap{$h}{"status-ipv$ipv"} = 'good';
success("updated IPv$ipv address to $ip");
}
}
}
######################################################################
@ -4218,7 +4194,7 @@ sub nic_noip_update {
'abuse', => 'The hostname specified is blocked for abuse; open a trouble ticket at https://www.no-ip.com',
'numhost' => 'System error: Too many or too few hosts found. open a trouble ticket at https://www.no-ip.com',
'dnserr' => 'System error: DNS error encountered. Contact support@dyndns.org',
'nochg' => 'No update required; unnecessary attempts to change the current address are considered abusive',
'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive',
);
for my $group (group_hosts_by(\@_, qw(login password server wantipv4 wantipv6))) {
my @hosts = @{$group->{hosts}};
@ -5609,7 +5585,7 @@ sub nic_henet_update {
'badsys' => 'The system parameter given was not valid',
'nohost' => 'The hostname specified does not exist in the database',
'abuse' => 'The hostname specified is blocked for abuse',
'nochg' => 'No update required; unnecessary attempts to change the current address are considered abusive',
'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive',
);
for my $h (@_) {
@ -6188,7 +6164,7 @@ sub nic_inwx_update {
'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 the current address are considered abusive',
'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive',
);
my @group_by_attrs = qw(
login

View file

@ -1,11 +1,12 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::ip;
httpd_required();
$ddclient::globals{'ssl_ca_file'} = $ca_file;
for my $ipv ('4', '6') {

View file

@ -1,80 +0,0 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
6c:bf:34:52:19:4d:c9:29:2b:a6:8b:41:59:aa:c6:c5:1f:a2:bb:10
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=Root Certification Authority
Validity
Not Before: Jan 8 08:24:32 2025 GMT
Not After : Jan 9 08:24:32 2125 GMT
Subject: CN=Root Certification Authority
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:c3:3d:19:6b:72:0a:9e:87:c0:28:a1:ff:d0:08:
21:55:52:71:92:f2:98:36:75:fc:95:b4:0c:5e:c9:
98:b3:3c:a1:ee:cf:91:6f:07:bf:82:c9:d5:51:c0:
eb:f8:46:17:41:52:1d:c6:89:ec:63:dd:5c:30:87:
a7:b5:0d:dd:ae:bf:46:fd:de:1a:be:1d:69:83:0d:
fb:d9:5a:33:0b:8d:5f:63:76:fc:a8:b1:54:37:1e:
0b:12:44:93:90:39:1c:48:ee:f0:f2:12:fe:dc:fb:
58:a5:76:3b:e8:e8:94:44:1e:9d:03:22:5f:21:6a:
17:66:d1:4a:bf:12:d7:3c:15:76:11:76:09:ab:bf:
21:ef:0c:a5:a9:e0:08:99:63:19:26:e4:d8:5d:c2:
40:8b:98:e6:5d:df:b3:8c:63:e2:01:7c:5e:fb:55:
39:a8:67:78:80:d2:6b:61:b2:e2:2e:93:c0:9d:91:
0e:a1:79:4f:fc:38:94:ff:6f:65:18:8f:3e:0b:8c:
1f:cd:48:d7:46:5a:a2:76:d6:e0:bd:3c:aa:3d:44:
9e:50:e6:fd:e1:12:1a:ee:a1:9a:69:48:60:63:da:
41:ae:a7:3d:36:1b:95:fb:b7:f1:0d:60:cd:2f:e3:
b1:1f:b1:db:b4:98:a6:62:87:de:54:80:d1:45:43:
5b:25
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
E1:7C:D3:C3:9E:C7:F5:2C:DA:7C:D7:85:78:91:BA:26:88:61:F9:D4
X509v3 Authority Key Identifier:
E1:7C:D3:C3:9E:C7:F5:2C:DA:7C:D7:85:78:91:BA:26:88:61:F9:D4
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
9d:dc:49:c6:14:13:19:38:d9:14:b5:70:f0:3b:01:8e:d7:32:
a7:69:f0:21:68:ec:ad:8c:ee:53:7d:16:64:7d:3e:c2:d2:ac:
5a:54:17:55:84:43:1e:46:1d:42:01:fb:89:e0:db:ec:e8:f0:
3c:22:82:54:1d:38:12:21:45:3c:37:44:3b:2e:c9:4d:ed:8d:
6e:46:f5:a5:cc:ba:39:61:ab:df:cf:1f:d2:c9:40:e2:db:3f:
05:ea:83:14:93:5f:0e:3d:33:be:98:04:80:87:25:3a:6c:ff:
8e:87:6a:32:ed:1e:ec:54:90:9b:2a:6e:12:05:6a:9d:15:48:
3c:ea:c6:9e:ab:71:58:1e:34:95:3f:9b:9e:e3:e5:4b:fb:9e:
32:f2:d6:59:bf:8d:09:d6:e4:9e:9e:47:b9:d6:78:5f:f3:0c:
98:ab:56:f0:18:5d:63:8e:83:ee:c1:f2:84:da:0e:64:af:1c:
18:ff:b3:f9:15:0b:02:50:77:d1:0b:6e:ba:61:bc:9e:c3:37:
63:91:26:e8:ce:77:9a:47:8f:ef:38:8f:9c:7f:f1:ab:7b:65:
a5:96:b6:92:2e:c7:d3:c3:7a:54:0d:d6:76:f5:d6:88:13:3b:
17:e2:02:4e:3b:4d:10:95:0a:bb:47:e9:48:25:76:1d:7b:19:
5c:6f:b8:a1
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgIUbL80UhlNySkrpotBWarGxR+iuxAwDQYJKoZIhvcNAQEL
BQAwJzElMCMGA1UEAwwcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAgFw0y
NTAxMDgwODI0MzJaGA8yMTI1MDEwOTA4MjQzMlowJzElMCMGA1UEAwwcUm9vdCBD
ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAMM9GWtyCp6HwCih/9AIIVVScZLymDZ1/JW0DF7JmLM8oe7PkW8Hv4LJ
1VHA6/hGF0FSHcaJ7GPdXDCHp7UN3a6/Rv3eGr4daYMN+9laMwuNX2N2/KixVDce
CxJEk5A5HEju8PIS/tz7WKV2O+jolEQenQMiXyFqF2bRSr8S1zwVdhF2Cau/Ie8M
pangCJljGSbk2F3CQIuY5l3fs4xj4gF8XvtVOahneIDSa2Gy4i6TwJ2RDqF5T/w4
lP9vZRiPPguMH81I10ZaonbW4L08qj1EnlDm/eESGu6hmmlIYGPaQa6nPTYblfu3
8Q1gzS/jsR+x27SYpmKH3lSA0UVDWyUCAwEAAaNjMGEwHQYDVR0OBBYEFOF808Oe
x/Us2nzXhXiRuiaIYfnUMB8GA1UdIwQYMBaAFOF808Oex/Us2nzXhXiRuiaIYfnU
MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUA
A4IBAQCd3EnGFBMZONkUtXDwOwGO1zKnafAhaOytjO5TfRZkfT7C0qxaVBdVhEMe
Rh1CAfuJ4Nvs6PA8IoJUHTgSIUU8N0Q7LslN7Y1uRvWlzLo5Yavfzx/SyUDi2z8F
6oMUk18OPTO+mASAhyU6bP+Oh2oy7R7sVJCbKm4SBWqdFUg86saeq3FYHjSVP5ue
4+VL+54y8tZZv40J1uSenke51nhf8wyYq1bwGF1jjoPuwfKE2g5krxwY/7P5FQsC
UHfRC266YbyewzdjkSbozneaR4/vOI+cf/Gre2WllraSLsfTw3pUDdZ29daIEzsX
4gJOO00QlQq7R+lIJXYdexlcb7ih
-----END CERTIFICATE-----

View file

@ -7,42 +7,21 @@ use warnings;
use parent qw(ddclient::Test::Fake::HTTPD);
use Exporter qw(import);
use JSON::PP;
use Test::More;
BEGIN { require 'ddclient'; }
use ddclient::t::ip;
our @EXPORT = qw(
httpd
httpd_ok httpd_required $httpd_supported $httpd_support_error
httpd_ipv6_ok httpd_ipv6_required $httpd_ipv6_supported $httpd_ipv6_support_error
httpd_ssl_ok httpd_ssl_required $httpd_ssl_supported $httpd_ssl_support_error
$ca_file $certdir $other_ca_file
$ca_file $certdir
$textplain
);
our $httpd_supported;
our $httpd_support_error;
BEGIN {
$httpd_supported = eval {
require parent; parent->import(qw(ddclient::Test::Fake::HTTPD));
require JSON::PP; JSON::PP->import();
1;
} or $httpd_support_error = $@;
}
sub httpd_ok {
ok($httpd_supported, "HTTPD is supported") or diag($httpd_support_error);
}
sub httpd_required {
plan(skip_all => $httpd_support_error) if !$httpd_supported;
}
our $httpd_ssl_supported = $httpd_supported;
our $httpd_ssl_support_error = $httpd_support_error;
$httpd_ssl_supported = eval { require HTTP::Daemon::SSL; 1; }
or $httpd_ssl_support_error = $@
if $httpd_ssl_supported;
our $httpd_ssl_support_error;
our $httpd_ssl_supported = eval { require HTTP::Daemon::SSL; 1; } or $httpd_ssl_support_error = $@;
sub httpd_ssl_ok {
ok($httpd_ssl_supported, "SSL is supported") or diag($httpd_ssl_support_error);
@ -52,11 +31,8 @@ sub httpd_ssl_required {
plan(skip_all => $httpd_ssl_support_error) if !$httpd_ssl_supported;
}
our $httpd_ipv6_supported = $httpd_supported;
our $httpd_ipv6_support_error = $httpd_support_error;
$httpd_ipv6_supported = $ipv6_supported
or $httpd_ipv6_support_error = $ipv6_support_error
if $httpd_ipv6_supported;
our $httpd_ipv6_support_error;
our $httpd_ipv6_supported = $ipv6_supported or $httpd_ipv6_support_error = $ipv6_support_error;
$httpd_ipv6_supported = eval { require HTTP::Daemon; HTTP::Daemon->VERSION(6.12); }
or $httpd_ipv6_support_error = $@
if $httpd_ipv6_supported;
@ -128,7 +104,6 @@ sub reset {
our $certdir = "$ENV{abs_top_srcdir}/t/lib/ddclient/Test/Fake/HTTPD";
our $ca_file = "$certdir/dummy-ca-cert.pem";
our $other_ca_file = "$certdir/other-ca-cert.pem";
my %daemons;
@ -136,7 +111,6 @@ sub httpd {
my ($ipv, $ssl) = @_;
$ipv //= '';
$ssl = !!$ssl;
return undef if !$httpd_supported;
return undef if $ipv eq '6' && !$httpd_ipv6_supported;
return undef if $ssl && !$httpd_ssl_supported;
if (!defined($daemons{$ipv}{$ssl})) {

View file

@ -1,39 +0,0 @@
package ddclient::t::Logger;
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use parent qw(-norequire ddclient::Logger);
{
package ddclient::t::LoggerAbort;
use overload '""' => qw(stringify);
sub new {
my ($class, %args) = @_;
return bless(\%args, $class);
}
sub stringify {
return 'logged a FATAL message';
}
}
sub new {
my ($class, $parent, $labelre) = @_;
my $self = $class->SUPER::new(undef, $parent);
$self->{logs} = [];
$self->{_labelre} = $labelre;
return $self;
}
sub _log {
my ($self, $args) = @_;
my $lre = $self->{_labelre};
my $lbl = $args->{label};
push(@{$self->{logs}}, $args) if !defined($lre) || (defined($lbl) && $lbl =~ $lre);
return $self->SUPER::_log($args);
}
sub _abort {
my ($self) = @_;
push(@{$self->{logs}}, 'aborted');
die(ddclient::t::LoggerAbort->new());
}
1;

View file

@ -2,10 +2,10 @@ use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
use ddclient::t::Logger;
httpd_required();
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
ddclient::load_json_support('directnic');
@ -30,6 +30,23 @@ httpd()->run(sub {
return [400, $headers, ['unexpected request: ' . $req->uri()]]
});
{
package Logger;
use parent qw(-norequire ddclient::Logger);
sub new {
my ($class, $parent) = @_;
my $self = $class->SUPER::new(undef, $parent);
$self->{logs} = [];
return $self;
}
sub _log {
my ($self, $args) = @_;
push(@{$self->{logs}}, $args)
if ($args->{label} // '') =~ qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/;
return $self->SUPER::_log($args);
}
}
my $hostname = httpd()->endpoint();
my @test_cases = (
{
@ -133,7 +150,7 @@ for my $tc (@test_cases) {
diag('==============================================================================');
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
my $l = Logger->new($ddclient::_l);
local %ddclient::config = %{$tc->{cfg}};
local %ddclient::recap;
{

View file

@ -2,13 +2,10 @@ use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
use ddclient::t::Logger;
httpd_required();
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
ddclient::load_json_support('dnsexit2');
@ -21,222 +18,143 @@ httpd()->run(sub {
})]];
});
sub cmp_update {
my ($a, $b) = @_;
return $a->{name} cmp $b->{name} || $a->{type} cmp $b->{type};
}
local $ddclient::globals{verbose} = 1;
sub sort_updates {
my ($req) = @_;
return {
%$req,
update => [sort({ cmp_update($a, $b); } @{$req->{update}})],
};
}
sub sort_reqs {
my @reqs = map(sort_updates($_), @_);
my @sorted = sort({
my $ret = $a->{domain} cmp $b->{domain};
$ret = @{$a->{update}} <=> @{$b->{update}} if !$ret;
my $i = 0;
while (!$ret && $i < @{$a->{update}} && $i < @{$b->{update}}) {
$ret = cmp_update($a->{update}[$i], $b->{update}[$i]);
sub decode_and_sort_array {
my ($data) = @_;
if (!ref $data) {
$data = decode_json($data);
}
return $ret;
} @reqs);
return @sorted;
@{$data->{update}} = sort { $a->{type} cmp $b->{type} } @{$data->{update}};
return $data;
}
my @test_cases = (
{
desc => 'both IPv4 and IPv6 are updated together',
cfg => {
subtest 'Testing nic_dnsexit2_update' => sub {
httpd()->reset();
local %ddclient::config = (
'host.my.example.com' => {
ttl => 5,
wantipv4 => '192.0.2.1',
wantipv6 => '2001:db8::1',
zone => 'my.example.com',
},
},
want => [{
apikey => 'key',
domain => 'my.example.com',
update => [
{
content => '192.0.2.1',
name => 'host',
ttl => 5,
type => 'A',
},
{
content => '2001:db8::1',
name => 'host',
ttl => 5,
type => 'AAAA',
},
],
}],
},
{
desc => 'zone defaults to host',
cfg => {
'host.my.example.com' => {
ttl => 10,
wantipv4 => '192.0.2.1',
},
},
want => [{
apikey => 'key',
domain => 'host.my.example.com',
update => [
{
content => '192.0.2.1',
name => '',
ttl => 10,
type => 'A',
},
],
}],
},
{
desc => 'two hosts, different zones',
cfg => {
'host1.example.com' => {
ttl => 5,
wantipv4 => '192.0.2.1',
# 'zone' intentionally not set, so it will default to 'host1.example.com'.
},
'host2.example.com' => {
ttl => 10,
wantipv6 => '2001:db8::1',
zone => 'example.com',
},
},
want => [
{
apikey => 'key',
domain => 'host1.example.com',
update => [
{
content => '192.0.2.1',
name => '',
ttl => 5,
type => 'A',
},
],
},
{
apikey => 'key',
domain => 'example.com',
update => [
{
content => '2001:db8::1',
name => 'host2',
ttl => 10,
type => 'AAAA',
},
],
},
],
},
{
desc => 'two hosts, same zone',
cfg => {
'host1.example.com' => {
ttl => 5,
wantipv4 => '192.0.2.1',
zone => 'example.com',
},
'host2.example.com' => {
ttl => 10,
wantipv6 => '2001:db8::1',
zone => 'example.com',
},
},
want => [
{
apikey => 'key',
domain => 'example.com',
update => [
{
content => '192.0.2.1',
name => 'host1',
ttl => 5,
type => 'A',
},
{
content => '2001:db8::1',
name => 'host2',
ttl => 10,
type => 'AAAA',
},
],
},
],
},
{
desc => 'host outside of zone',
cfg => {
'host.example' => {
wantipv4 => '192.0.2.1',
zone => 'example.com',
},
},
want_fatal => qr{hostname does not end with the zone: example.com},
},
);
for my $tc (@test_cases) {
subtest($tc->{desc} => sub {
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
local %ddclient::config = ();
my @hosts = keys(%{$tc->{cfg}});
for my $h (@hosts) {
$ddclient::config{$h} = {
password => 'key',
path => '/update',
server => httpd()->endpoint(),
%{$tc->{cfg}{$h}},
};
}
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^FATAL$/);
my $err = do {
local $ddclient::_l = $l;
local $@;
(eval { ddclient::nic_dnsexit2_update(undef, @hosts); 1; })
? undef : ($@ // 'unknown error');
};
'usev4' => 'ipv4',
'wantipv4' => '192.0.2.1',
'usev6' => 'ipv6',
'wantipv6' => '2001:db8::1',
'protocol' => 'dnsexit2',
'password' => 'mytestingpassword',
'zone' => 'my.example.com',
'server' => httpd()->endpoint(),
'path' => '/update',
'ttl' => 5
});
ddclient::nic_dnsexit2_update(undef, 'host.my.example.com');
my @requests = httpd()->reset();
my @got;
for (my $i = 0; $i < @requests; $i++) {
subtest("request $i" => sub {
my $req = $requests[$i];
is($req->method(), 'POST', 'method is POST');
is($req->uri()->as_string(), '/update', 'path is /update');
is($req->header('content-type'), 'application/json', 'Content-Type is JSON');
is($req->header('accept'), 'application/json', 'Accept is JSON');
my $got = decode_json($req->content());
is(ref($got), 'HASH', 'request content is a JSON object');
is(ref($got->{update}), 'ARRAY', 'JSON object has array "update" property');
push(@got, $got);
});
}
@got = sort_reqs(@got);
my @want = sort_reqs(@{$tc->{want} // []});
is_deeply(\@got, \@want, 'request objects match');
subtest('expected (or lack of) error' => sub {
if (is(defined($err), defined($tc->{want_fatal}), 'error existence') && defined($err)) {
my @got = @{$l->{logs}};
if (is(scalar(@got), 2, 'logged two events')) {
is($got[0]->{label}, 'FATAL', 'first logged event is a FATAL message');
like($got[0]->{msg}, $tc->{want_fatal}, 'first logged event message matches');
is($got[1], 'aborted', 'second logged event is an "aborted" event');
isa_ok($err, qw(ddclient::t::LoggerAbort));
}
is(scalar(@requests), 1, 'expected number of update requests');
my $req = shift(@requests);
is($req->method(), 'POST', 'Method is correct');
is($req->uri()->as_string(), '/update', 'URI contains correct path');
is($req->header('content-type'), 'application/json', 'Content-Type header is correct');
is($req->header('accept'), 'application/json', 'Accept header is correct');
my $got = decode_and_sort_array($req->content());
my $want = decode_and_sort_array({
'domain' => 'my.example.com',
'apikey' => 'mytestingpassword',
'update' => [
{
'type' => 'A',
'name' => 'host',
'content' => '192.0.2.1',
'ttl' => 5,
},
{
'type' => 'AAAA',
'name' => 'host',
'content' => '2001:db8::1',
'ttl' => 5,
}
]
});
is_deeply($got, $want, 'Data is correct');
};
subtest 'Testing nic_dnsexit2_update without a zone set' => sub {
httpd()->reset();
local %ddclient::config = (
'myhost.example.com' => {
'usev4' => 'ipv4',
'wantipv4' => '192.0.2.1',
'protocol' => 'dnsexit2',
'password' => 'anotherpassword',
'server' => httpd()->endpoint(),
'path' => '/update-alt',
'ttl' => 10
});
}
ddclient::nic_dnsexit2_update(undef, 'myhost.example.com');
my @requests = httpd()->reset();
is(scalar(@requests), 1, 'expected number of update requests');
my $req = shift(@requests);
my $got = decode_and_sort_array($req->content());
my $want = decode_and_sort_array({
'domain' => 'myhost.example.com',
'apikey' => 'anotherpassword',
'update' => [
{
'type' => 'A',
'name' => '',
'content' => '192.0.2.1',
'ttl' => 10,
}
]
});
is_deeply($got, $want, 'Data is correct');
};
subtest 'Testing nic_dnsexit2_update with two hostnames, one with a zone and one without' => sub {
httpd()->reset();
local %ddclient::config = (
'host1.example.com' => {
'usev4' => 'ipv4',
'wantipv4' => '192.0.2.1',
'protocol' => 'dnsexit2',
'password' => 'testingpassword',
'server' => httpd()->endpoint(),
'path' => '/update',
'ttl' => 5
},
'host2.example.com' => {
'usev6' => 'ipv6',
'wantipv6' => '2001:db8::1',
'protocol' => 'dnsexit2',
'password' => 'testingpassword',
'server' => httpd()->endpoint(),
'path' => '/update',
'ttl' => 10,
'zone' => 'example.com'
}
);
ddclient::nic_dnsexit2_update(undef, 'host1.example.com', 'host2.example.com');
my @requests = httpd()->reset();
my @got = map(decode_and_sort_array($_->content()), @requests);
my @want = (
decode_and_sort_array({
'domain' => 'host1.example.com',
'apikey' => 'testingpassword',
'update' => [{
'type' => 'A',
'name' => '',
'content' => '192.0.2.1',
'ttl' => 5,
}],
}),
decode_and_sort_array({
'domain' => 'example.com',
'apikey' => 'testingpassword',
'update' => [{
'type' => 'AAAA',
'name' => 'host2',
'content' => '2001:db8::1',
'ttl' => 10,
}],
}),
);
is_deeply(\@got, \@want, 'data is correct');
};
done_testing();

View file

@ -1,11 +1,12 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
use MIME::Base64;
use Scalar::Util qw(blessed);
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
use ddclient::t::Logger;
httpd_required();
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
httpd()->run(sub {
my ($req) = @_;
@ -19,6 +20,23 @@ httpd()->run(sub {
return undef;
});
{
package Logger;
use parent qw(-norequire ddclient::Logger);
sub new {
my ($class, $parent) = @_;
my $self = $class->SUPER::new(undef, $parent);
$self->{logs} = [];
return $self;
}
sub _log {
my ($self, $args) = @_;
push(@{$self->{logs}}, $args)
if ($args->{label} // '') =~ qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/;
return $self->SUPER::_log($args);
}
}
my @test_cases = (
{
desc => 'IPv4, single host, good',
@ -230,7 +248,7 @@ for my $tc (@test_cases) {
diag('==============================================================================');
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
my $l = Logger->new($ddclient::_l);
local %ddclient::config;
local %ddclient::recap;
$ddclient::config{$_} = {

View file

@ -1,11 +1,12 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::ip;
httpd_required();
httpd('4')->run(
sub { return [200, ['Content-Type' => 'text/plain'], ['127.0.0.1 skip 127.0.0.2']]; });
httpd('6')->run(

View file

@ -1,15 +1,17 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::ip;
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
httpd_required();
httpd_ssl_required();
# Note: $ddclient::globals{'ssl_ca_file'} is intentionally NOT set to "$certdir/dummy-ca-cert.pem"
# so that we can test what happens when certificate validation fails.
httpd('4', 1)->run(sub { return [200, $textplain, ['127.0.0.1']]; });
httpd('6', 1)->run(sub { return [200, $textplain, ['::1']]; }) if httpd('6', 1);
my $h = 't/ssl-validate.pl';
@ -68,21 +70,10 @@ my @test_cases = (
);
for my $tc (@test_cases) {
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
SKIP: {
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{ipv6} && !$httpd_ipv6_supported;
# $ddclient::globals{'ssl_ca_file'} is intentionally NOT set to $ca_file so that we can
# test what happens when certificate validation fails. However, if curl can't find any CA
# certificates (which may be the case in some minimal test environments, such as Docker
# images and Debian package builder chroots), it will immediately close the connection
# after it sends the TLS client hello and before it receives the server hello (in Debian
# sid as of 2025-01-08, anyway). This confuses IO::Socket::SSL (used by
# Test::Fake::HTTPD), causing it to hang in the middle of the TLS handshake waiting for
# input that will never arrive. To work around this, the CA certificate file is explicitly
# set to an unrelated certificate so that curl has something to read.
local $ddclient::globals{'ssl_ca_file'} = $other_ca_file;
local $ddclient::config{$h} = $tc->{cfg};
$ddclient::config{$h} = $tc->{cfg};
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
is(ddclient::get_ipv4(ddclient::strategy_inputs('usev4', $h)), $tc->{want}, $tc->{desc})
if ($tc->{cfg}{usev4});

View file

@ -6,11 +6,12 @@ BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(
use List::Util qw(max);
use Scalar::Util qw(refaddr);
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::ip;
httpd_required();
httpd('4')->run();
httpd('6')->run() if httpd('6');
local %ddclient::builtinweb = (

View file

@ -1,41 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
my @test_cases;
for my $ipv ('4', '6') {
my $ip = $ipv eq '4' ? '192.0.2.1' : '2001:db8::1';
for my $use ('use', "usev$ipv") {
my @cmds = ();
push(@cmds, 'cmd') if $use eq 'use' || $ipv eq '6';
push(@cmds, "cmdv$ipv") if $use ne 'use';
for my $cmd (@cmds) {
my $cmdarg = "echo '$ip'";
push(
@test_cases,
{
desc => "$use=$cmd $cmd=\"$cmdarg\"",
cfg => {$use => $cmd, $cmd => $cmdarg},
want => $ip,
},
);
}
}
}
for my $tc (@test_cases) {
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
my $h = 'test-host';
local $ddclient::config{$h} = $tc->{cfg};
is(ddclient::get_ip(ddclient::strategy_inputs('use', $h)), $tc->{want}, $tc->{desc})
if $tc->{cfg}{use};
is(ddclient::get_ipv4(ddclient::strategy_inputs('usev4', $h)), $tc->{want}, $tc->{desc})
if $tc->{cfg}{usev4};
is(ddclient::get_ipv6(ddclient::strategy_inputs('usev6', $h)), $tc->{want}, $tc->{desc})
if $tc->{cfg}{usev6};
}
done_testing();

View file

@ -1,11 +1,13 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
use Scalar::Util qw(blessed);
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::ip;
httpd_required();
my $builtinweb = 't/use_web.pl builtinweb';
my $h = 't/use_web.pl hostname';
@ -68,6 +70,16 @@ for my $ipv ('4', '6') {
}
for my $tc (@test_cases) {
my $subst = sub {
return map({
my $class = blessed($_);
(defined($class) && $class->isa('EndpointPlaceholder')) ? do {
my $uri = ${$_}->clone();
$uri->query_param(tc => $tc->{desc});
$uri;
} : $_;
} @_);
};
local $ddclient::builtinweb{$builtinweb} = $tc->{biw};
$ddclient::builtinweb if 0;
local $ddclient::config{$h} = $tc->{cfg};