Compare commits

..

51 commits

Author SHA1 Message Date
25d162db91 Aggiorna README.md
Some checks failed
CI / test-debian-like (debian:oldstable) (push) Has been cancelled
CI / test-debian-like (debian:stable) (push) Has been cancelled
CI / test-debian-like (debian:testing) (push) Has been cancelled
CI / test-debian-like (ubuntu:20.04) (push) Has been cancelled
CI / test-debian-like (ubuntu:latest) (push) Has been cancelled
CI / test-fedora-like (almalinux:8) (push) Has been cancelled
CI / test-fedora-like (almalinux:latest) (push) Has been cancelled
CI / test-fedora-like (fedora:39) (push) Has been cancelled
CI / test-fedora-like (fedora:latest) (push) Has been cancelled
CI / test-fedora-like (fedora:rawhide) (push) Has been cancelled
2025-02-05 21:38:17 +08:00
Richard Hansen
50e8d2ed00 Post-release version bump 2025-01-19 14:46:10 -05:00
Richard Hansen
d6da6b878d Release v4.0.0 2025-01-19 14:29:22 -05:00
Richard Hansen
33a86eb556
Merge pull request #801 from insanolanbiri/upnp
Add UPnP example in configuration file
2025-01-16 04:10:59 -05:00
Eren Akgün
10d3561353
Add UPnP example in configuration file
This commit demonstrates a simple way to obtain the
IP address using UPnP in the ddclient configuration file.
2025-01-16 11:02:25 +03:00
Richard Hansen
803f77404d Post-release version bump 2025-01-11 03:30:56 -05:00
Richard Hansen
590d7d91fc Release v4.0.0-rc.3 2025-01-11 03:18:38 -05:00
Richard Hansen
41170b9c08
Merge pull request #684 from rhansen/dnsexit2
dnsexit2: Update multiple hosts at a time when possible
2025-01-11 03:09:48 -05:00
Richard Hansen
63bf3512a4 dnsexit2: Update multiple hosts at a time when possible 2025-01-10 19:20:25 -05:00
Richard Hansen
3b10e37607 tests: dnsexit2: Add test for host outside of zone 2025-01-10 19:20:25 -05:00
Richard Hansen
115f23dead tests: Option to select which log messages to capture 2025-01-10 19:20:25 -05:00
Richard Hansen
009033d476 tests: Factor out duplicate log capture code 2025-01-10 19:20:25 -05:00
Richard Hansen
d18b1cdb27 logging: Move filtering and reactions to Logger class
This increases overhead when the verbose or debug option is disabled,
but makes it possible for tests to intercept debug, info, and fatal
messages.
2025-01-10 19:20:25 -05:00
Richard Hansen
b31e5e2f91 tests: dnsexit2: Add test for two hosts in the same zone 2025-01-09 19:33:57 -05:00
Richard Hansen
3b73350541 tests: dnsexit2 Convert tests to table-driven
This makes the tests easier to read and extend.
2025-01-09 19:33:57 -05:00
Richard Hansen
6bb80cbdaa
Merge pull request #766 from greenius/feature/cmdargs
Enable arguments on cmdv4 and cmdv6
2025-01-09 19:33:11 -05:00
Richard Hansen
6fd9a6f106 tests: Add some tests for use=cmd, usev4=cmdv4, usev6=cmdv6 2025-01-09 19:29:00 -05:00
Richard Hansen
1c178d4c09 Add ChangeLog entry for --cmdv4, --cmdv6 change 2025-01-09 19:29:00 -05:00
steven
ae01ba26c1 Do not use quotemeta on cmdv4 and cmdv6 arguments
`quotemeta` prevents executing commands with arguments.  With this
change, it is now possible to get an IP address with:

    usev4=cmdv4
    cmdv4="dig +short myip.opendns.com @resolver1.opendns.com"
2025-01-09 19:29:00 -05:00
Richard Hansen
17fc4c0a35
Merge pull request #783 from indrajitr/eval-cmd-skip
Make 'cmd-skip' warning message consistent for IPv4 and IPv6
2025-01-08 19:13:17 -05:00
Indrajit Raychaudhuri
8dcea0d779 Make 'cmd-skip' warning message consistent for IPv4 and IPv6 2025-01-08 19:09:29 -05:00
Bodo Eggert
8883641d97 fix ddclient --verbose calling 'p' instead of using $p{foo} 2025-01-08 19:08:19 -05:00
Richard Hansen
741a2345ea
Merge pull request #795 from rhansen/missing-semicolon
tests: Add missing semicolon; prevent similar future bugs
2025-01-08 18:27:35 -05:00
Richard Hansen
8cf322e162 tests: Only skip HTTPD tests if dependencies are unavailable
This prevents the tests from passing due to syntax errors in
the ddclient::t::HTTPD module.
2025-01-08 18:24:04 -05:00
Richard Hansen
ddeaedc136 tests: Add missing semicolon
This should have been in commit
06c47695fc.  The tests that use this
module did not fail because an import failure is assumed to be caused
by a missing dependency, not a genuine bug.
2025-01-08 18:24:04 -05:00
Richard Hansen
9ab038412f
Merge pull request #796 from rhansen/vestigial
tests: Delete vestigial code
2025-01-08 18:23:41 -05:00
Richard Hansen
ecaa05abd3 tests: Delete vestigial code
This deleted code came from a previous unpublished prior draft of the
tests and was accidentally not deleted when the approach changed.
2025-01-08 18:20:39 -05:00
Richard Hansen
6408be6ccc
Merge pull request #792 from rhansen/ssl-validate-tests
tests: Fix t/ssl-validate.pl in minimal test environments
2025-01-08 14:43:14 -05:00
Richard Hansen
06c47695fc tests: Fix t/ssl-validate.pl in minimal test environments 2025-01-08 03:33:30 -05:00
Richard Hansen
c89a2d6186 tests: Enable debug logging in t/ssl-validate.pl 2025-01-08 03:33:30 -05:00
Richard Hansen
3f3b8cf825 tests: Localize config setting
This isn't strictly necessary, but is good practice because it
guarantees that the config is cleaned up after each test case.
2025-01-08 03:00:41 -05:00
Richard Hansen
8decfc4b77 Post-release version bump
We'll probably just release `v4.0.0` without an `-rc.3`, but I'm
setting it to `-rc.3` just in case (decreasing the version number can
break automatically built daily Git snapshots).
2025-01-07 14:37:32 -05:00
Richard Hansen
660bb11c02 Release v4.0.0-rc.2 2025-01-07 04:39:01 -05:00
Richard Hansen
fee71b46be
Merge pull request #789 from rhansen/confdir
Change default location of `ddclient.conf` to `${sysconfdir}/ddclient`
2025-01-07 04:36:37 -05:00
Richard Hansen
7248341ad6 Change default location of ddclient.conf to ${sysconfdir}/ddclient 2025-01-07 04:33:54 -05:00
Richard Hansen
60bedd0fab
Merge pull request #790 from rhansen/autosquash
New GitHub workflow to prohibit autosquash commits
2025-01-07 04:33:15 -05:00
Richard Hansen
56f88e3bab New GitHub workflow to prohibit autosquash commits
This prohibits commits whose commit message starts with "squash!",
"fixup!", or "amend!" unless the PR has the 'pr-permit-autosquash'
label.  Such commits are created via the `--fixup` or `--squash`
options to `git commit`, and cause `git rebase --autosquash` to
automatically adjust the todo list appropriately before performing the
rebase.  Their existence implies that the PR should be rebased with
`--autosquash` before merging.
2025-01-07 04:30:40 -05:00
Indrajit Raychaudhuri
8ffbedd436
Merge pull request #754 from Zorks/patch-1
Correct NearlyFreeSpeech.NET example
2025-01-06 20:38:34 -06:00
Indrajit Raychaudhuri
678b76f7e8 nfsn Rearrange zone property in example
Usually, the zone property is not the last property
in the configuration convention that is followed in
ddclient config examples.
2025-01-06 20:27:21 -06:00
Zorks
e4920373ee Correct NearlyFreeSpeech.NET example
extra spaces in the protocol and a trailing comma caused connection issues if not corrected.
2025-01-06 20:27:21 -06:00
Indrajit Raychaudhuri
4008ccfa2d
Merge pull request #565 from TinfoilSubmarine/feature/mail-from
add mail-from option
2025-01-06 20:24:49 -06:00
Indrajit Raychaudhuri
cf4bad127d fixup! add main-from option
move changelog entry to v4.0.0-rc.2
2025-01-06 20:21:56 -06:00
Richard Hansen
76fccba151 fixup! add mail-from option
add changelog entry
2025-01-06 20:20:26 -06:00
Richard Hansen
d2b1a4dfa6 fixup! add mail-from option
move variable declaration closer to usage
2025-01-06 20:20:26 -06:00
Richard Hansen
d1f81dc9e4 fixup! add mail-from option
factor out duplicate code
2025-01-06 20:20:26 -06:00
Richard Hansen
2de77f17f7 fixup! add mail-from option
default to undef
2025-01-06 20:20:26 -06:00
Richard Hansen
a2e818d6d3 fixup! add mail-from option
refine usage wording
2025-01-06 20:20:26 -06:00
Joel Beckmeyer
8030a46ca3 add mail-from option 2025-01-06 20:20:26 -06:00
Richard Hansen
59f6c2959a Prepare for v4.0.0-rc.2 2025-01-06 21:03:47 -05:00
Richard Hansen
0a687d505b
Merge pull request #782 from indrajitr/typo-fix
Fix small typo in 'nochg' message grammar
2024-12-27 14:47:50 -05:00
Indrajit Raychaudhuri
3da4259a41 Fix small typo in 'nochg' message grammar 2024-12-26 18:26:26 -06:00
19 changed files with 647 additions and 288 deletions

View file

@ -29,3 +29,21 @@ 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,15 +3,48 @@
This document describes notable changes. For details, see the [source code
repository history](https://github.com/ddclient/ddclient/commits/main).
## 2024-12-25 v4.0.0-rc.1
## v4.0.1-alpha (unreleased work-in-progress)
## 2025-01-19 v4.0.0
### 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)
@ -53,6 +86,8 @@ 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`
@ -99,6 +134,8 @@ 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
sysconf_DATA = ddclient.conf
conf_DATA = ddclient.conf
install-data-local:
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
@ -77,6 +77,7 @@ 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
@ -156,7 +157,9 @@ 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,6 +3,36 @@
`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
@ -105,7 +135,7 @@ operating system. See the image to the right for a list of distributions with a
```shell
./configure \
--prefix=/usr \
--sysconfdir=/etc/ddclient \
--sysconfdir=/etc \
--localstatedir=/var
make
make VERBOSE=1 check

View file

@ -23,6 +23,18 @@ 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.
@ -75,6 +87,7 @@ 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
@ -88,7 +101,6 @@ 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
@ -100,6 +112,7 @@ 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,6 +25,9 @@ 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.
@ -50,6 +53,10 @@ 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.
@ -130,10 +137,10 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
##
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
##
# protocol = nfsn, \
# protocol=nfsn, \
# zone=example.com, \
# login=member-login, \
# password=api-key, \
# zone=example.com \
# password=api-key \
# 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.0.0_901');
use version 0.77; our $VERSION = version->declare('v4.0.1.0_0');
sub parse_version {
my ($v) = @_;
@ -132,7 +132,7 @@ sub subst_var {
return $subst;
}
my $etc = subst_var('@sysconfdir@', '/etc/ddclient');
my $etc = subst_var('@confdir@', '/etc/ddclient');
my $cachedir = subst_var('@localstatedir@', '/var') . '/cache/ddclient';
our @curl = (subst_var('@CURL@', 'curl'));
@ -704,6 +704,7 @@ 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),
@ -1427,6 +1428,7 @@ 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"],
@ -2404,8 +2406,10 @@ 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,
@ -2506,9 +2510,11 @@ sub ynu {
# provided (it is ignored if the `msg` keyword is present).
sub log {
my $self = shift;
my %args = (@_ % 2 ? (msg => pop) : (), @_);
my %args = (label => '', @_ % 2 ? (msg => pop) : (), @_);
$args{ctx} = [$args{ctx} // ()] if ref($args{ctx}) eq '';
return $self->_log(\%args);
$self->_log(\%args);
$self->_failed() if $args{label} eq 'FAILED';
$self->_abort() if $args{label} eq 'FATAL';
}
sub _log {
@ -2517,10 +2523,11 @@ 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;
@ -2540,6 +2547,20 @@ 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:
@ -2548,12 +2569,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(@_)) if opt('verbose'); }
sub debug { logmsg( label => 'DEBUG', _logmsg_fmt(@_)) if opt('debug'); }
sub info { logmsg(email => 1, label => 'INFO', _logmsg_fmt(@_)); }
sub debug { logmsg( label => 'DEBUG', _logmsg_fmt(@_)); }
sub warning { logmsg(email => 1, label => 'WARNING', _logmsg_fmt(@_)); }
sub fatal { logmsg(email => 1, label => 'FATAL', _logmsg_fmt(@_)); sendmail(); exit(1); }
sub fatal { logmsg(email => 1, label => 'FATAL', _logmsg_fmt(@_)); }
sub success { logmsg(email => 1, label => 'SUCCESS', _logmsg_fmt(@_)); }
sub failed { logmsg(email => 1, label => 'FAILED', _logmsg_fmt(@_)); $result = 'FAILED'; }
sub failed { logmsg(email => 1, label => 'FAILED', _logmsg_fmt(@_)); }
sub prettytime { return scalar(localtime(shift)); }
@ -3337,10 +3358,9 @@ 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') && $p{'cmd-skip'});
if opt('verbose') && defined($p{'cmd-skip'});
if ($arg) {
my $sys_cmd = quotemeta($arg);
$reply = qx{$sys_cmd};
$reply = qx{$arg};
$reply = '' if $?;
}
} elsif ($p{'usev4'} eq 'webv4') {
@ -3450,10 +3470,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") if opt('verbose') && p{'cmd-skip'};
warning("'--cmd-skip' ignored for '--usev6=$p{'usev6'}'")
if opt('verbose') && defined($p{'cmd-skip'});
if ($arg) {
my $sys_cmd = quotemeta($arg);
$reply = qx{$sys_cmd};
$reply = qx{$arg};
$reply = '' if $?;
}
} elsif ($p{'usev6'} eq 'webv6' || $p{'usev6'} eq 'web') {
@ -3919,7 +3939,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 to the current address are considered abusive',
'nochg' => 'No update required; unnecessary attempts to change the current address are considered abusive',
);
my @group_by_attrs = qw(
backupmx
@ -4077,54 +4097,55 @@ 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_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.
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: " . opt('zone', $h));
}
# The IPv4 and IPv6 addresses must be updated together in a single API call.
my %ips;
sub dnsexit2_update_hostgroup {
my ($group) = @_;
return unless @{$group->{hosts}} > 0;
local $_l = pushlogctx(join(', ', @{$group->{hosts}}));
my %hostips;
my @updates;
for my $ipv ('4', '6') {
my $ip = delete($config{$h}{"wantipv$ipv"}) or next;
$ips{$ipv} = $ip;
info("updating IPv$ipv address to $ip");
$recap{$h}{"status-ipv$ipv"} = 'failed';
push(@updates, {
name => $name,
type => ($ipv eq '6') ? 'AAAA' : 'A',
content => $ip,
ttl => opt('ttl', $h),
});
};
my $url = opt('server', $h) . opt('path', $h);
for my $h (@{$group->{hosts}}) {
local $_l = pushlogctx($h) if @{$group->{hosts}} > 1;
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$//) {
# The zone was successfully trimmed from $name.
} else {
fatal("hostname does not end with the zone: $group->{cfg}{'zone'}");
}
# The IPv4 and IPv6 addresses must be updated together in a single API call.
for my $ipv ('4', '6') {
my $ip = delete($config{$h}{"wantipv$ipv"}) or next;
$hostips{$h}{$ipv} = $ip;
info("updating IPv$ipv address to $ip");
$recap{$h}{"status-ipv$ipv"} = 'failed';
push(@updates, {
name => $name,
type => ($ipv eq '6') ? 'AAAA' : 'A',
content => $ip,
ttl => opt('ttl', $h),
});
}
}
return unless @updates > 0;
my $reply = geturl(
proxy => opt('proxy'),
url => $url,
url => $group->{cfg}{'server'} . $group->{cfg}{'path'},
headers => [
'Content-Type: application/json',
'Accept: application/json',
],
method => 'POST',
data => encode_json({
apikey => opt('password', $h),
domain => $zone,
apikey => $group->{cfg}{'password'},
domain => $group->{cfg}{'zone'},
update => \@updates,
}),
);
@ -4171,12 +4192,15 @@ sub dnsexit2_update_host {
return;
}
success($message);
$recap{$h}{'mtime'} = $now;
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");
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)) {
$recap{$h}{"ipv$ipv"} = $ip;
$recap{$h}{"status-ipv$ipv"} = 'good';
success("updated IPv$ipv address to $ip");
}
}
}
@ -4194,7 +4218,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 to the current address are considered abusive',
'nochg' => 'No update required; unnecessary attempts to change the current address are considered abusive',
);
for my $group (group_hosts_by(\@_, qw(login password server wantipv4 wantipv6))) {
my @hosts = @{$group->{hosts}};
@ -5585,7 +5609,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 to the current address are considered abusive',
'nochg' => 'No update required; unnecessary attempts to change the current address are considered abusive',
);
for my $h (@_) {
@ -6164,7 +6188,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 to the current address are considered abusive',
'nochg' => 'No update required; unnecessary attempts to change the current address are considered abusive',
);
my @group_by_attrs = qw(
login

View file

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

View file

@ -0,0 +1,80 @@
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,21 +7,42 @@ 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
$ca_file $certdir $other_ca_file
$textplain
);
our $httpd_ssl_support_error;
our $httpd_ssl_supported = eval { require HTTP::Daemon::SSL; 1; } or $httpd_ssl_support_error = $@;
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;
sub httpd_ssl_ok {
ok($httpd_ssl_supported, "SSL is supported") or diag($httpd_ssl_support_error);
@ -31,8 +52,11 @@ sub httpd_ssl_required {
plan(skip_all => $httpd_ssl_support_error) if !$httpd_ssl_supported;
}
our $httpd_ipv6_support_error;
our $httpd_ipv6_supported = $ipv6_supported or $httpd_ipv6_support_error = $ipv6_support_error;
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;
$httpd_ipv6_supported = eval { require HTTP::Daemon; HTTP::Daemon->VERSION(6.12); }
or $httpd_ipv6_support_error = $@
if $httpd_ipv6_supported;
@ -104,6 +128,7 @@ 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;
@ -111,6 +136,7 @@ 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

@ -0,0 +1,39 @@
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($@); }
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::HTTPD;
use ddclient::t::Logger;
httpd_required();
ddclient::load_json_support('directnic');
@ -30,23 +30,6 @@ 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 = (
{
@ -150,7 +133,7 @@ for my $tc (@test_cases) {
diag('==============================================================================');
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
my $l = Logger->new($ddclient::_l);
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
local %ddclient::config = %{$tc->{cfg}};
local %ddclient::recap;
{

View file

@ -2,10 +2,13 @@ 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($@); }
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::HTTPD;
use ddclient::t::Logger;
httpd_required();
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
ddclient::load_json_support('dnsexit2');
@ -18,143 +21,222 @@ httpd()->run(sub {
})]];
});
local $ddclient::globals{verbose} = 1;
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 cmp_update {
my ($a, $b) = @_;
return $a->{name} cmp $b->{name} || $a->{type} cmp $b->{type};
}
subtest 'Testing nic_dnsexit2_update' => sub {
httpd()->reset();
local %ddclient::config = (
'host.my.example.com' => {
'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();
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' => [
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]);
}
return $ret;
} @reqs);
return @sorted;
}
my @test_cases = (
{
desc => 'both IPv4 and IPv6 are updated together',
cfg => {
'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 => [
{
'type' => 'A',
'name' => 'host',
'content' => '192.0.2.1',
'ttl' => 5,
apikey => 'key',
domain => 'host1.example.com',
update => [
{
content => '192.0.2.1',
name => '',
ttl => 5,
type => 'A',
},
],
},
{
'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
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',
},
},
'host2.example.com' => {
'usev6' => 'ipv6',
'wantipv6' => '2001:db8::1',
'protocol' => 'dnsexit2',
'password' => 'testingpassword',
'server' => httpd()->endpoint(),
'path' => '/update',
'ttl' => 10,
'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}},
};
}
);
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');
};
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');
};
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));
}
}
});
});
}
done_testing();

View file

@ -1,12 +1,11 @@
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($@); }
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::HTTPD;
use ddclient::t::Logger;
httpd_required();
httpd()->run(sub {
my ($req) = @_;
@ -20,23 +19,6 @@ 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',
@ -248,7 +230,7 @@ for my $tc (@test_cases) {
diag('==============================================================================');
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
my $l = Logger->new($ddclient::_l);
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
local %ddclient::config;
local %ddclient::recap;
$ddclient::config{$_} = {

View file

@ -1,12 +1,11 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::HTTPD;
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,16 +1,14 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::HTTPD;
use ddclient::t::ip;
httpd_ssl_required();
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
# 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_required();
httpd_ssl_required();
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);
@ -70,10 +68,21 @@ 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::config{$h} = $tc->{cfg};
# $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 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,12 +6,11 @@ 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($@); }
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::HTTPD;
use ddclient::t::ip;
httpd_required();
httpd('4')->run();
httpd('6')->run() if httpd('6');
local %ddclient::builtinweb = (

41
t/use_cmd.pl Normal file
View file

@ -0,0 +1,41 @@
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,13 +1,11 @@
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($@); }
BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
use ddclient::t::HTTPD;
use ddclient::t::ip;
httpd_required();
my $builtinweb = 't/use_web.pl builtinweb';
my $h = 't/use_web.pl hostname';
@ -70,16 +68,6 @@ 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};