Compare commits
51 commits
v4.0.0-rc.
...
main
Author | SHA1 | Date | |
---|---|---|---|
25d162db91 | |||
![]() |
50e8d2ed00 | ||
![]() |
d6da6b878d | ||
![]() |
33a86eb556 | ||
![]() |
10d3561353 | ||
![]() |
803f77404d | ||
![]() |
590d7d91fc | ||
![]() |
41170b9c08 | ||
![]() |
63bf3512a4 | ||
![]() |
3b10e37607 | ||
![]() |
115f23dead | ||
![]() |
009033d476 | ||
![]() |
d18b1cdb27 | ||
![]() |
b31e5e2f91 | ||
![]() |
3b73350541 | ||
![]() |
6bb80cbdaa | ||
![]() |
6fd9a6f106 | ||
![]() |
1c178d4c09 | ||
![]() |
ae01ba26c1 | ||
![]() |
17fc4c0a35 | ||
![]() |
8dcea0d779 | ||
![]() |
8883641d97 | ||
![]() |
741a2345ea | ||
![]() |
8cf322e162 | ||
![]() |
ddeaedc136 | ||
![]() |
9ab038412f | ||
![]() |
ecaa05abd3 | ||
![]() |
6408be6ccc | ||
![]() |
06c47695fc | ||
![]() |
c89a2d6186 | ||
![]() |
3f3b8cf825 | ||
![]() |
8decfc4b77 | ||
![]() |
660bb11c02 | ||
![]() |
fee71b46be | ||
![]() |
7248341ad6 | ||
![]() |
60bedd0fab | ||
![]() |
56f88e3bab | ||
![]() |
8ffbedd436 | ||
![]() |
678b76f7e8 | ||
![]() |
e4920373ee | ||
![]() |
4008ccfa2d | ||
![]() |
cf4bad127d | ||
![]() |
76fccba151 | ||
![]() |
d2b1a4dfa6 | ||
![]() |
d1f81dc9e4 | ||
![]() |
2de77f17f7 | ||
![]() |
a2e818d6d3 | ||
![]() |
8030a46ca3 | ||
![]() |
59f6c2959a | ||
![]() |
0a687d505b | ||
![]() |
3da4259a41 |
19 changed files with 647 additions and 288 deletions
18
.github/workflows/pr.yml
vendored
18
.github/workflows/pr.yml
vendored
|
@ -29,3 +29,21 @@ jobs:
|
||||||
git show "${out}" >&2
|
git show "${out}" >&2
|
||||||
exit 1
|
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
|
||||||
|
|
39
ChangeLog.md
39
ChangeLog.md
|
@ -3,15 +3,48 @@
|
||||||
This document describes notable changes. For details, see the [source code
|
This document describes notable changes. For details, see the [source code
|
||||||
repository history](https://github.com/ddclient/ddclient/commits/main).
|
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
|
### 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.
|
* The `--ssl` option is now enabled by default.
|
||||||
[#705](https://github.com/ddclient/ddclient/pull/705)
|
[#705](https://github.com/ddclient/ddclient/pull/705)
|
||||||
* Unencrypted (plain) HTTP is now used instead of encrypted (TLS) HTTP if the
|
* 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
|
URL uses `http://` instead of `https://`, even if the `--ssl` option is
|
||||||
enabled. [#608](https://github.com/ddclient/ddclient/pull/608)
|
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
|
* The default web service for `--webv4` and `--webv6` has changed from Google
|
||||||
Domains (which has shut down) to ipify.
|
Domains (which has shut down) to ipify.
|
||||||
[5b104ad1](https://github.com/ddclient/ddclient/commit/5b104ad116c023c3760129cab6e141f04f72b406)
|
[5b104ad1](https://github.com/ddclient/ddclient/commit/5b104ad116c023c3760129cab6e141f04f72b406)
|
||||||
|
@ -53,6 +86,8 @@ repository history](https://github.com/ddclient/ddclient/commits/main).
|
||||||
|
|
||||||
### New features
|
### 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
|
* Simultaneous/separate updating of IPv4 (A) records and IPv6 (AAAA) records
|
||||||
is now supported in the following services: `gandi`
|
is now supported in the following services: `gandi`
|
||||||
([#558](https://github.com/ddclient/ddclient/pull/558)), `nsupdate`
|
([#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)
|
[#726](https://github.com/ddclient/ddclient/pull/726)
|
||||||
* `porkbun`: The update URL hostname is now configurable via the `server`
|
* `porkbun`: The update URL hostname is now configurable via the `server`
|
||||||
option. [#752](https://github.com/ddclient/ddclient/pull/752)
|
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
|
### Bug fixes
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,8 @@ $(subst_files): Makefile
|
||||||
-e 's|@PACKAGE_VERSION[@]|$(PACKAGE_VERSION)|g' \
|
-e 's|@PACKAGE_VERSION[@]|$(PACKAGE_VERSION)|g' \
|
||||||
-e '1 s|^#\!.*perl$$|#\!$(PERL)|g' \
|
-e '1 s|^#\!.*perl$$|#\!$(PERL)|g' \
|
||||||
-e 's|@localstatedir[@]|$(localstatedir)|g' \
|
-e 's|@localstatedir[@]|$(localstatedir)|g' \
|
||||||
|
-e 's|@confdir[@]|$(confdir)|g' \
|
||||||
-e 's|@runstatedir[@]|$(runstatedir)|g' \
|
-e 's|@runstatedir[@]|$(runstatedir)|g' \
|
||||||
-e 's|@sysconfdir[@]|$(sysconfdir)|g' \
|
|
||||||
-e 's|@CURL[@]|$(CURL)|g' \
|
-e 's|@CURL[@]|$(CURL)|g' \
|
||||||
"$${in}" >'$@'.tmp && \
|
"$${in}" >'$@'.tmp && \
|
||||||
{ ! test -x "$${in}" || chmod +x '$@'.tmp; }
|
{ ! test -x "$${in}" || chmod +x '$@'.tmp; }
|
||||||
|
@ -40,7 +40,7 @@ ddclient.conf: $(srcdir)/ddclient.conf.in
|
||||||
|
|
||||||
bin_SCRIPTS = ddclient
|
bin_SCRIPTS = ddclient
|
||||||
|
|
||||||
sysconf_DATA = ddclient.conf
|
conf_DATA = ddclient.conf
|
||||||
|
|
||||||
install-data-local:
|
install-data-local:
|
||||||
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
|
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
|
||||||
|
@ -77,6 +77,7 @@ handwritten_tests = \
|
||||||
t/skip.pl \
|
t/skip.pl \
|
||||||
t/ssl-validate.pl \
|
t/ssl-validate.pl \
|
||||||
t/update_nics.pl \
|
t/update_nics.pl \
|
||||||
|
t/use_cmd.pl \
|
||||||
t/use_web.pl \
|
t/use_web.pl \
|
||||||
t/variable_defaults.pl \
|
t/variable_defaults.pl \
|
||||||
t/write_recap.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-ca-cert.pem \
|
||||||
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-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/dummy-server-key.pem \
|
||||||
|
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem \
|
||||||
t/lib/ddclient/t.pm \
|
t/lib/ddclient/t.pm \
|
||||||
t/lib/ddclient/t/HTTPD.pm \
|
t/lib/ddclient/t/HTTPD.pm \
|
||||||
|
t/lib/ddclient/t/Logger.pm \
|
||||||
t/lib/ddclient/t/ip.pm \
|
t/lib/ddclient/t/ip.pm \
|
||||||
t/lib/ok.pm
|
t/lib/ok.pm
|
||||||
|
|
32
README.md
32
README.md
|
@ -3,6 +3,36 @@
|
||||||
`ddclient` is a Perl client used to update dynamic DNS entries for accounts
|
`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 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
|
## Alternatives
|
||||||
|
|
||||||
You might also want to consider using one of the following, if they support
|
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
|
```shell
|
||||||
./configure \
|
./configure \
|
||||||
--prefix=/usr \
|
--prefix=/usr \
|
||||||
--sysconfdir=/etc/ddclient \
|
--sysconfdir=/etc \
|
||||||
--localstatedir=/var
|
--localstatedir=/var
|
||||||
make
|
make
|
||||||
make VERBOSE=1 check
|
make VERBOSE=1 check
|
||||||
|
|
15
configure.ac
15
configure.ac
|
@ -23,6 +23,18 @@ AC_REQUIRE_AUX_FILE([tap-driver.sh])
|
||||||
AM_INIT_AUTOMAKE([1.11 -Wall -Werror foreign subdir-objects parallel-tests])
|
AM_INIT_AUTOMAKE([1.11 -Wall -Werror foreign subdir-objects parallel-tests])
|
||||||
AM_SILENT_RULES
|
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
|
AC_PROG_MKDIR_P
|
||||||
|
|
||||||
# The Fedora Docker image doesn't come with the 'findutils' package.
|
# 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.
|
# then some tests will fail. Only prints a warning if not installed.
|
||||||
m4_foreach_w([_m], [
|
m4_foreach_w([_m], [
|
||||||
B
|
B
|
||||||
|
Exporter
|
||||||
File::Spec::Functions
|
File::Spec::Functions
|
||||||
File::Temp
|
File::Temp
|
||||||
List::Util
|
List::Util
|
||||||
|
@ -88,7 +101,6 @@ m4_foreach_w([_m], [
|
||||||
# prints a warning if not installed.
|
# prints a warning if not installed.
|
||||||
m4_foreach_w([_m], [
|
m4_foreach_w([_m], [
|
||||||
Carp
|
Carp
|
||||||
Exporter
|
|
||||||
HTTP::Daemon=6.12
|
HTTP::Daemon=6.12
|
||||||
HTTP::Daemon::SSL
|
HTTP::Daemon::SSL
|
||||||
HTTP::Message::PSGI
|
HTTP::Message::PSGI
|
||||||
|
@ -100,6 +112,7 @@ m4_foreach_w([_m], [
|
||||||
Test::Warnings
|
Test::Warnings
|
||||||
Time::HiRes
|
Time::HiRes
|
||||||
URI
|
URI
|
||||||
|
parent
|
||||||
], [AX_PROG_PERL_MODULES([_m], [],
|
], [AX_PROG_PERL_MODULES([_m], [],
|
||||||
[AC_MSG_WARN([some tests may be skipped due to missing module _m])])])
|
[AC_MSG_WARN([some tests may be skipped due to missing module _m])])])
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,9 @@ daemon=300 # check every 300 seconds
|
||||||
syslog=yes # log update msgs to syslog
|
syslog=yes # log update msgs to syslog
|
||||||
mail=root # mail all msgs to root
|
mail=root # mail all msgs to root
|
||||||
mail-failure=root # mail failed update 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.
|
pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
# postscript=script # run script after updating. The new IP is
|
# postscript=script # run script after updating. The new IP is
|
||||||
# added as argument.
|
# 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)
|
## 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
|
#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)
|
## 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.
|
## by default, checkip.dyndns.org is used if you use the dyndns protocol.
|
||||||
## Using use=web is enough to get it working.
|
## 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)
|
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
|
||||||
##
|
##
|
||||||
# protocol = nfsn, \
|
# protocol=nfsn, \
|
||||||
|
# zone=example.com, \
|
||||||
# login=member-login, \
|
# login=member-login, \
|
||||||
# password=api-key, \
|
# password=api-key \
|
||||||
# zone=example.com \
|
|
||||||
# example.com,subdomain.example.com
|
# example.com,subdomain.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
142
ddclient.in
142
ddclient.in
|
@ -78,7 +78,7 @@ use Sys::Hostname;
|
||||||
#
|
#
|
||||||
# For consistency and to match user expectations, the release part of the version is always three
|
# For consistency and to match user expectations, the release part of the version is always three
|
||||||
# components: MAJOR.MINOR.PATCH.
|
# 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 {
|
sub parse_version {
|
||||||
my ($v) = @_;
|
my ($v) = @_;
|
||||||
|
@ -132,7 +132,7 @@ sub subst_var {
|
||||||
return $subst;
|
return $subst;
|
||||||
}
|
}
|
||||||
|
|
||||||
my $etc = subst_var('@sysconfdir@', '/etc/ddclient');
|
my $etc = subst_var('@confdir@', '/etc/ddclient');
|
||||||
my $cachedir = subst_var('@localstatedir@', '/var') . '/cache/ddclient';
|
my $cachedir = subst_var('@localstatedir@', '/var') . '/cache/ddclient';
|
||||||
our @curl = (subst_var('@CURL@', 'curl'));
|
our @curl = (subst_var('@CURL@', 'curl'));
|
||||||
|
|
||||||
|
@ -704,6 +704,7 @@ our %cfgvars = (
|
||||||
'priority' => setv(T_STRING,0, 'notice', undef),
|
'priority' => setv(T_STRING,0, 'notice', undef),
|
||||||
'mail' => setv(T_EMAIL, 0, undef, undef),
|
'mail' => setv(T_EMAIL, 0, undef, undef),
|
||||||
'mail-failure' => 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),
|
'max-warn' => setv(T_NUMBER,0, 1, undef),
|
||||||
|
|
||||||
'exec' => setv(T_BOOL, 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"],
|
["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", "=s", "--mail=<address> : e-mail messages to <address>"],
|
||||||
["mail-failure", "=s", "--mail-failure=<addr> : e-mail messages for failed updates to <addr>"],
|
["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"],
|
["exec", "!", "--{no}exec : do {not} execute; just show what would be done"],
|
||||||
["debug", "!", "--{no}debug : print {no} debugging information"],
|
["debug", "!", "--{no}debug : print {no} debugging information"],
|
||||||
["verbose", "!", "--{no}verbose : print {no} verbose information"],
|
["verbose", "!", "--{no}verbose : print {no} verbose information"],
|
||||||
|
@ -2404,8 +2406,10 @@ sub sendmail {
|
||||||
$recipients = opt('mail-failure');
|
$recipients = opt('mail-failure');
|
||||||
}
|
}
|
||||||
if ($emailbody && $recipients && $emailbody ne $last_emailbody) {
|
if ($emailbody && $recipients && $emailbody ne $last_emailbody) {
|
||||||
|
my $sender = opt('mail-from') // '';
|
||||||
pipecmd("sendmail -oi $recipients",
|
pipecmd("sendmail -oi $recipients",
|
||||||
"To: $recipients",
|
"To: $recipients",
|
||||||
|
$sender ne '' ? ("From: $sender") : (),
|
||||||
"Subject: status report from $program\@$hostname",
|
"Subject: status report from $program\@$hostname",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
$emailbody,
|
$emailbody,
|
||||||
|
@ -2506,9 +2510,11 @@ sub ynu {
|
||||||
# provided (it is ignored if the `msg` keyword is present).
|
# provided (it is ignored if the `msg` keyword is present).
|
||||||
sub log {
|
sub log {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
my %args = (@_ % 2 ? (msg => pop) : (), @_);
|
my %args = (label => '', @_ % 2 ? (msg => pop) : (), @_);
|
||||||
$args{ctx} = [$args{ctx} // ()] if ref($args{ctx}) eq '';
|
$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 {
|
sub _log {
|
||||||
|
@ -2517,10 +2523,11 @@ sub ynu {
|
||||||
# the caller's arrayref (in case it is reused in a future call).
|
# the caller's arrayref (in case it is reused in a future call).
|
||||||
$args->{ctx} = [@{$self->{ctx}}, @{$args->{ctx}}];
|
$args->{ctx} = [@{$self->{ctx}}, @{$args->{ctx}}];
|
||||||
return $self->{parent}->_log($args) if defined($self->{parent});
|
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} // '';
|
my $buffer = $args->{msg} // '';
|
||||||
chomp($buffer);
|
chomp($buffer);
|
||||||
if (!$args->{raw}) {
|
if (!$args->{raw}) {
|
||||||
$args->{label} //= '';
|
|
||||||
my $prefix = $args->{label} ne '' ? sprintf("%-8s ", $args->{label} . ':') : '';
|
my $prefix = $args->{label} ne '' ? sprintf("%-8s ", $args->{label} . ':') : '';
|
||||||
$prefix .= "[$_]" for @{$args->{ctx}};
|
$prefix .= "[$_]" for @{$args->{ctx}};
|
||||||
$prefix .= '> ' if $prefix;
|
$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:
|
# Intended use:
|
||||||
|
@ -2548,12 +2569,12 @@ sub pushlogctx { my ($ctx) = @_; return ddclient::Logger->new($ctx, $_l); }
|
||||||
|
|
||||||
sub logmsg { $_l->log(@_); }
|
sub logmsg { $_l->log(@_); }
|
||||||
sub _logmsg_fmt { $_[0] eq 'ctx' ? (shift, shift) : (), (@_ > 1) ? sprintf(shift, @_) : shift; }
|
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 info { logmsg(email => 1, label => 'INFO', _logmsg_fmt(@_)); }
|
||||||
sub debug { logmsg( label => 'DEBUG', _logmsg_fmt(@_)) if opt('debug'); }
|
sub debug { logmsg( label => 'DEBUG', _logmsg_fmt(@_)); }
|
||||||
sub warning { logmsg(email => 1, label => 'WARNING', _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 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)); }
|
sub prettytime { return scalar(localtime(shift)); }
|
||||||
|
|
||||||
|
@ -3337,10 +3358,9 @@ sub get_ipv4 {
|
||||||
} elsif ($p{'usev4'} eq 'cmdv4') {
|
} elsif ($p{'usev4'} eq 'cmdv4') {
|
||||||
## Obtain IPv4 address by executing the command in "cmdv4=<command>"
|
## Obtain IPv4 address by executing the command in "cmdv4=<command>"
|
||||||
warning("'--cmd-skip' ignored for '--usev4=$p{'usev4'}'")
|
warning("'--cmd-skip' ignored for '--usev4=$p{'usev4'}'")
|
||||||
if (opt('verbose') && $p{'cmd-skip'});
|
if opt('verbose') && defined($p{'cmd-skip'});
|
||||||
if ($arg) {
|
if ($arg) {
|
||||||
my $sys_cmd = quotemeta($arg);
|
$reply = qx{$arg};
|
||||||
$reply = qx{$sys_cmd};
|
|
||||||
$reply = '' if $?;
|
$reply = '' if $?;
|
||||||
}
|
}
|
||||||
} elsif ($p{'usev4'} eq 'webv4') {
|
} elsif ($p{'usev4'} eq 'webv4') {
|
||||||
|
@ -3450,10 +3470,10 @@ sub get_ipv6 {
|
||||||
$ipv6 = get_ip_from_interface($arg, 6);
|
$ipv6 = get_ip_from_interface($arg, 6);
|
||||||
} elsif ($p{'usev6'} eq 'cmdv6' || $p{'usev6'} eq 'cmd') {
|
} elsif ($p{'usev6'} eq 'cmdv6' || $p{'usev6'} eq 'cmd') {
|
||||||
## Obtain IPv6 address by executing the command in "cmdv6=<command>"
|
## 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) {
|
if ($arg) {
|
||||||
my $sys_cmd = quotemeta($arg);
|
$reply = qx{$arg};
|
||||||
$reply = qx{$sys_cmd};
|
|
||||||
$reply = '' if $?;
|
$reply = '' if $?;
|
||||||
}
|
}
|
||||||
} elsif ($p{'usev6'} eq 'webv6' || $p{'usev6'} eq 'web') {
|
} 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',
|
'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',
|
'numhost' => 'System error: Too many or too few hosts found. Contact support@dyndns.org',
|
||||||
'dnserr' => 'System error: DNS error encountered. 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(
|
my @group_by_attrs = qw(
|
||||||
backupmx
|
backupmx
|
||||||
|
@ -4077,54 +4097,55 @@ EoEXAMPLE
|
||||||
######################################################################
|
######################################################################
|
||||||
sub nic_dnsexit2_update {
|
sub nic_dnsexit2_update {
|
||||||
my $self = shift;
|
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 (@_) {
|
for my $h (@_) {
|
||||||
$config{$h}{'zone'} = $h if !defined(opt('zone', $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 {
|
sub dnsexit2_update_hostgroup {
|
||||||
my ($h) = @_;
|
my ($group) = @_;
|
||||||
local $_l = pushlogctx($h);
|
return unless @{$group->{hosts}} > 0;
|
||||||
my $name = $h;
|
local $_l = pushlogctx(join(', ', @{$group->{hosts}}));
|
||||||
# Remove the zone suffix from $name. If the zone eq $name, $name can be left alone or
|
my %hostips;
|
||||||
# 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;
|
|
||||||
my @updates;
|
my @updates;
|
||||||
for my $ipv ('4', '6') {
|
for my $h (@{$group->{hosts}}) {
|
||||||
my $ip = delete($config{$h}{"wantipv$ipv"}) or next;
|
local $_l = pushlogctx($h) if @{$group->{hosts}} > 1;
|
||||||
$ips{$ipv} = $ip;
|
my $name = $h;
|
||||||
info("updating IPv$ipv address to $ip");
|
# Remove the zone suffix from $name. If the zone eq $name, $name can be left alone or
|
||||||
$recap{$h}{"status-ipv$ipv"} = 'failed';
|
# set to the empty string; both have identical semantics. For consistency, always
|
||||||
push(@updates, {
|
# remove the zone even if it means $name becomes the empty string.
|
||||||
name => $name,
|
if ($name =~ s/(?:^|\.)\Q$group->{cfg}{'zone'}\E$//) {
|
||||||
type => ($ipv eq '6') ? 'AAAA' : 'A',
|
# The zone was successfully trimmed from $name.
|
||||||
content => $ip,
|
} else {
|
||||||
ttl => opt('ttl', $h),
|
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.
|
||||||
my $url = opt('server', $h) . opt('path', $h);
|
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(
|
my $reply = geturl(
|
||||||
proxy => opt('proxy'),
|
proxy => opt('proxy'),
|
||||||
url => $url,
|
url => $group->{cfg}{'server'} . $group->{cfg}{'path'},
|
||||||
headers => [
|
headers => [
|
||||||
'Content-Type: application/json',
|
'Content-Type: application/json',
|
||||||
'Accept: application/json',
|
'Accept: application/json',
|
||||||
],
|
],
|
||||||
method => 'POST',
|
method => 'POST',
|
||||||
data => encode_json({
|
data => encode_json({
|
||||||
apikey => opt('password', $h),
|
apikey => $group->{cfg}{'password'},
|
||||||
domain => $zone,
|
domain => $group->{cfg}{'zone'},
|
||||||
update => \@updates,
|
update => \@updates,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -4171,12 +4192,15 @@ sub dnsexit2_update_host {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
success($message);
|
success($message);
|
||||||
$recap{$h}{'mtime'} = $now;
|
keys(%hostips); # Reset internal iterator.
|
||||||
keys(%ips); # Reset internal iterator.
|
while (my ($h, $ips) = each(%hostips)) {
|
||||||
while (my ($ipv, $ip) = each(%ips)) {
|
$recap{$h}{'mtime'} = $now;
|
||||||
$recap{$h}{"ipv$ipv"} = $ip;
|
keys(%$ips); # Reset internal iterator.
|
||||||
$recap{$h}{"status-ipv$ipv"} = 'good';
|
while (my ($ipv, $ip) = each(%$ips)) {
|
||||||
success("updated IPv$ipv address to $ip");
|
$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',
|
'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',
|
'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',
|
'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))) {
|
for my $group (group_hosts_by(\@_, qw(login password server wantipv4 wantipv6))) {
|
||||||
my @hosts = @{$group->{hosts}};
|
my @hosts = @{$group->{hosts}};
|
||||||
|
@ -5585,7 +5609,7 @@ sub nic_henet_update {
|
||||||
'badsys' => 'The system parameter given was not valid',
|
'badsys' => 'The system parameter given was not valid',
|
||||||
'nohost' => 'The hostname specified does not exist in the database',
|
'nohost' => 'The hostname specified does not exist in the database',
|
||||||
'abuse' => 'The hostname specified is blocked for abuse',
|
'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 (@_) {
|
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.',
|
'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.',
|
'numhost' => 'System error: Too many or too few hosts found.',
|
||||||
'dnserr' => 'System error: DNS error encountered.',
|
'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(
|
my @group_by_attrs = qw(
|
||||||
login
|
login
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
use Test::More;
|
use Test::More;
|
||||||
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
BEGIN {
|
use ddclient::t::HTTPD;
|
||||||
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
|
|
||||||
ddclient::t::HTTPD->import();
|
|
||||||
}
|
|
||||||
use ddclient::t::ip;
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
$ddclient::globals{'ssl_ca_file'} = $ca_file;
|
$ddclient::globals{'ssl_ca_file'} = $ca_file;
|
||||||
|
|
||||||
for my $ipv ('4', '6') {
|
for my $ipv ('4', '6') {
|
||||||
|
|
80
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem
Normal file
80
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem
Normal 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-----
|
|
@ -7,21 +7,42 @@ use warnings;
|
||||||
use parent qw(ddclient::Test::Fake::HTTPD);
|
use parent qw(ddclient::Test::Fake::HTTPD);
|
||||||
|
|
||||||
use Exporter qw(import);
|
use Exporter qw(import);
|
||||||
use JSON::PP;
|
|
||||||
use Test::More;
|
use Test::More;
|
||||||
BEGIN { require 'ddclient'; }
|
BEGIN { require 'ddclient'; }
|
||||||
use ddclient::t::ip;
|
use ddclient::t::ip;
|
||||||
|
|
||||||
our @EXPORT = qw(
|
our @EXPORT = qw(
|
||||||
httpd
|
httpd
|
||||||
|
httpd_ok httpd_required $httpd_supported $httpd_support_error
|
||||||
httpd_ipv6_ok httpd_ipv6_required $httpd_ipv6_supported $httpd_ipv6_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
|
httpd_ssl_ok httpd_ssl_required $httpd_ssl_supported $httpd_ssl_support_error
|
||||||
$ca_file $certdir
|
$ca_file $certdir $other_ca_file
|
||||||
$textplain
|
$textplain
|
||||||
);
|
);
|
||||||
|
|
||||||
our $httpd_ssl_support_error;
|
our $httpd_supported;
|
||||||
our $httpd_ssl_supported = eval { require HTTP::Daemon::SSL; 1; } or $httpd_ssl_support_error = $@;
|
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 {
|
sub httpd_ssl_ok {
|
||||||
ok($httpd_ssl_supported, "SSL is supported") or diag($httpd_ssl_support_error);
|
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;
|
plan(skip_all => $httpd_ssl_support_error) if !$httpd_ssl_supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
our $httpd_ipv6_support_error;
|
our $httpd_ipv6_supported = $httpd_supported;
|
||||||
our $httpd_ipv6_supported = $ipv6_supported or $httpd_ipv6_support_error = $ipv6_support_error;
|
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); }
|
$httpd_ipv6_supported = eval { require HTTP::Daemon; HTTP::Daemon->VERSION(6.12); }
|
||||||
or $httpd_ipv6_support_error = $@
|
or $httpd_ipv6_support_error = $@
|
||||||
if $httpd_ipv6_supported;
|
if $httpd_ipv6_supported;
|
||||||
|
@ -104,6 +128,7 @@ sub reset {
|
||||||
|
|
||||||
our $certdir = "$ENV{abs_top_srcdir}/t/lib/ddclient/Test/Fake/HTTPD";
|
our $certdir = "$ENV{abs_top_srcdir}/t/lib/ddclient/Test/Fake/HTTPD";
|
||||||
our $ca_file = "$certdir/dummy-ca-cert.pem";
|
our $ca_file = "$certdir/dummy-ca-cert.pem";
|
||||||
|
our $other_ca_file = "$certdir/other-ca-cert.pem";
|
||||||
|
|
||||||
my %daemons;
|
my %daemons;
|
||||||
|
|
||||||
|
@ -111,6 +136,7 @@ sub httpd {
|
||||||
my ($ipv, $ssl) = @_;
|
my ($ipv, $ssl) = @_;
|
||||||
$ipv //= '';
|
$ipv //= '';
|
||||||
$ssl = !!$ssl;
|
$ssl = !!$ssl;
|
||||||
|
return undef if !$httpd_supported;
|
||||||
return undef if $ipv eq '6' && !$httpd_ipv6_supported;
|
return undef if $ipv eq '6' && !$httpd_ipv6_supported;
|
||||||
return undef if $ssl && !$httpd_ssl_supported;
|
return undef if $ssl && !$httpd_ssl_supported;
|
||||||
if (!defined($daemons{$ipv}{$ssl})) {
|
if (!defined($daemons{$ipv}{$ssl})) {
|
||||||
|
|
39
t/lib/ddclient/t/Logger.pm
Normal file
39
t/lib/ddclient/t/Logger.pm
Normal 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;
|
|
@ -2,10 +2,10 @@ use Test::More;
|
||||||
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
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 JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
|
||||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
BEGIN {
|
use ddclient::t::HTTPD;
|
||||||
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
|
use ddclient::t::Logger;
|
||||||
ddclient::t::HTTPD->import();
|
|
||||||
}
|
httpd_required();
|
||||||
|
|
||||||
ddclient::load_json_support('directnic');
|
ddclient::load_json_support('directnic');
|
||||||
|
|
||||||
|
@ -30,23 +30,6 @@ httpd()->run(sub {
|
||||||
return [400, $headers, ['unexpected request: ' . $req->uri()]]
|
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 $hostname = httpd()->endpoint();
|
||||||
my @test_cases = (
|
my @test_cases = (
|
||||||
{
|
{
|
||||||
|
@ -150,7 +133,7 @@ for my $tc (@test_cases) {
|
||||||
diag('==============================================================================');
|
diag('==============================================================================');
|
||||||
local $ddclient::globals{debug} = 1;
|
local $ddclient::globals{debug} = 1;
|
||||||
local $ddclient::globals{verbose} = 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::config = %{$tc->{cfg}};
|
||||||
local %ddclient::recap;
|
local %ddclient::recap;
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,10 +2,13 @@ use Test::More;
|
||||||
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
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 JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
|
||||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
BEGIN {
|
use ddclient::t::HTTPD;
|
||||||
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
|
use ddclient::t::Logger;
|
||||||
ddclient::t::HTTPD->import();
|
|
||||||
}
|
httpd_required();
|
||||||
|
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
|
||||||
ddclient::load_json_support('dnsexit2');
|
ddclient::load_json_support('dnsexit2');
|
||||||
|
|
||||||
|
@ -18,143 +21,222 @@ httpd()->run(sub {
|
||||||
})]];
|
})]];
|
||||||
});
|
});
|
||||||
|
|
||||||
local $ddclient::globals{verbose} = 1;
|
sub cmp_update {
|
||||||
|
my ($a, $b) = @_;
|
||||||
sub decode_and_sort_array {
|
return $a->{name} cmp $b->{name} || $a->{type} cmp $b->{type};
|
||||||
my ($data) = @_;
|
|
||||||
if (!ref $data) {
|
|
||||||
$data = decode_json($data);
|
|
||||||
}
|
|
||||||
@{$data->{update}} = sort { $a->{type} cmp $b->{type} } @{$data->{update}};
|
|
||||||
return $data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subtest 'Testing nic_dnsexit2_update' => sub {
|
sub sort_updates {
|
||||||
httpd()->reset();
|
my ($req) = @_;
|
||||||
local %ddclient::config = (
|
return {
|
||||||
'host.my.example.com' => {
|
%$req,
|
||||||
'usev4' => 'ipv4',
|
update => [sort({ cmp_update($a, $b); } @{$req->{update}})],
|
||||||
'wantipv4' => '192.0.2.1',
|
};
|
||||||
'usev6' => 'ipv6',
|
}
|
||||||
'wantipv6' => '2001:db8::1',
|
|
||||||
'protocol' => 'dnsexit2',
|
sub sort_reqs {
|
||||||
'password' => 'mytestingpassword',
|
my @reqs = map(sort_updates($_), @_);
|
||||||
'zone' => 'my.example.com',
|
my @sorted = sort({
|
||||||
'server' => httpd()->endpoint(),
|
my $ret = $a->{domain} cmp $b->{domain};
|
||||||
'path' => '/update',
|
$ret = @{$a->{update}} <=> @{$b->{update}} if !$ret;
|
||||||
'ttl' => 5
|
my $i = 0;
|
||||||
});
|
while (!$ret && $i < @{$a->{update}} && $i < @{$b->{update}}) {
|
||||||
ddclient::nic_dnsexit2_update(undef, 'host.my.example.com');
|
$ret = cmp_update($a->{update}[$i], $b->{update}[$i]);
|
||||||
my @requests = httpd()->reset();
|
}
|
||||||
is(scalar(@requests), 1, 'expected number of update requests');
|
return $ret;
|
||||||
my $req = shift(@requests);
|
} @reqs);
|
||||||
is($req->method(), 'POST', 'Method is correct');
|
return @sorted;
|
||||||
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 @test_cases = (
|
||||||
my $got = decode_and_sort_array($req->content());
|
{
|
||||||
my $want = decode_and_sort_array({
|
desc => 'both IPv4 and IPv6 are updated together',
|
||||||
'domain' => 'my.example.com',
|
cfg => {
|
||||||
'apikey' => 'mytestingpassword',
|
'host.my.example.com' => {
|
||||||
'update' => [
|
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',
|
apikey => 'key',
|
||||||
'name' => 'host',
|
domain => 'host1.example.com',
|
||||||
'content' => '192.0.2.1',
|
update => [
|
||||||
'ttl' => 5,
|
{
|
||||||
|
content => '192.0.2.1',
|
||||||
|
name => '',
|
||||||
|
ttl => 5,
|
||||||
|
type => 'A',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'type' => 'AAAA',
|
apikey => 'key',
|
||||||
'name' => 'host',
|
domain => 'example.com',
|
||||||
'content' => '2001:db8::1',
|
update => [
|
||||||
'ttl' => 5,
|
{
|
||||||
}
|
content => '2001:db8::1',
|
||||||
]
|
name => 'host2',
|
||||||
});
|
ttl => 10,
|
||||||
is_deeply($got, $want, 'Data is correct');
|
type => 'AAAA',
|
||||||
};
|
},
|
||||||
|
],
|
||||||
subtest 'Testing nic_dnsexit2_update without a zone set' => sub {
|
},
|
||||||
httpd()->reset();
|
],
|
||||||
local %ddclient::config = (
|
},
|
||||||
'myhost.example.com' => {
|
{
|
||||||
'usev4' => 'ipv4',
|
desc => 'two hosts, same zone',
|
||||||
'wantipv4' => '192.0.2.1',
|
cfg => {
|
||||||
'protocol' => 'dnsexit2',
|
'host1.example.com' => {
|
||||||
'password' => 'anotherpassword',
|
ttl => 5,
|
||||||
'server' => httpd()->endpoint(),
|
wantipv4 => '192.0.2.1',
|
||||||
'path' => '/update-alt',
|
zone => 'example.com',
|
||||||
'ttl' => 10
|
},
|
||||||
});
|
'host2.example.com' => {
|
||||||
ddclient::nic_dnsexit2_update(undef, 'myhost.example.com');
|
ttl => 10,
|
||||||
my @requests = httpd()->reset();
|
wantipv6 => '2001:db8::1',
|
||||||
is(scalar(@requests), 1, 'expected number of update requests');
|
zone => 'example.com',
|
||||||
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' => {
|
want => [
|
||||||
'usev6' => 'ipv6',
|
{
|
||||||
'wantipv6' => '2001:db8::1',
|
apikey => 'key',
|
||||||
'protocol' => 'dnsexit2',
|
domain => 'example.com',
|
||||||
'password' => 'testingpassword',
|
update => [
|
||||||
'server' => httpd()->endpoint(),
|
{
|
||||||
'path' => '/update',
|
content => '192.0.2.1',
|
||||||
'ttl' => 10,
|
name => 'host1',
|
||||||
'zone' => 'example.com'
|
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$/);
|
||||||
ddclient::nic_dnsexit2_update(undef, 'host1.example.com', 'host2.example.com');
|
my $err = do {
|
||||||
my @requests = httpd()->reset();
|
local $ddclient::_l = $l;
|
||||||
my @got = map(decode_and_sort_array($_->content()), @requests);
|
local $@;
|
||||||
my @want = (
|
(eval { ddclient::nic_dnsexit2_update(undef, @hosts); 1; })
|
||||||
decode_and_sort_array({
|
? undef : ($@ // 'unknown error');
|
||||||
'domain' => 'host1.example.com',
|
};
|
||||||
'apikey' => 'testingpassword',
|
my @requests = httpd()->reset();
|
||||||
'update' => [{
|
my @got;
|
||||||
'type' => 'A',
|
for (my $i = 0; $i < @requests; $i++) {
|
||||||
'name' => '',
|
subtest("request $i" => sub {
|
||||||
'content' => '192.0.2.1',
|
my $req = $requests[$i];
|
||||||
'ttl' => 5,
|
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');
|
||||||
decode_and_sort_array({
|
is($req->header('accept'), 'application/json', 'Accept is JSON');
|
||||||
'domain' => 'example.com',
|
my $got = decode_json($req->content());
|
||||||
'apikey' => 'testingpassword',
|
is(ref($got), 'HASH', 'request content is a JSON object');
|
||||||
'update' => [{
|
is(ref($got->{update}), 'ARRAY', 'JSON object has array "update" property');
|
||||||
'type' => 'AAAA',
|
push(@got, $got);
|
||||||
'name' => 'host2',
|
});
|
||||||
'content' => '2001:db8::1',
|
}
|
||||||
'ttl' => 10,
|
@got = sort_reqs(@got);
|
||||||
}],
|
my @want = sort_reqs(@{$tc->{want} // []});
|
||||||
}),
|
is_deeply(\@got, \@want, 'request objects match');
|
||||||
);
|
subtest('expected (or lack of) error' => sub {
|
||||||
is_deeply(\@got, \@want, 'data is correct');
|
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();
|
done_testing();
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
use Test::More;
|
use Test::More;
|
||||||
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
use MIME::Base64;
|
use MIME::Base64;
|
||||||
use Scalar::Util qw(blessed);
|
|
||||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
BEGIN {
|
use ddclient::t::HTTPD;
|
||||||
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
|
use ddclient::t::Logger;
|
||||||
ddclient::t::HTTPD->import();
|
|
||||||
}
|
httpd_required();
|
||||||
|
|
||||||
httpd()->run(sub {
|
httpd()->run(sub {
|
||||||
my ($req) = @_;
|
my ($req) = @_;
|
||||||
|
@ -20,23 +19,6 @@ httpd()->run(sub {
|
||||||
return undef;
|
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 = (
|
my @test_cases = (
|
||||||
{
|
{
|
||||||
desc => 'IPv4, single host, good',
|
desc => 'IPv4, single host, good',
|
||||||
|
@ -248,7 +230,7 @@ for my $tc (@test_cases) {
|
||||||
diag('==============================================================================');
|
diag('==============================================================================');
|
||||||
local $ddclient::globals{debug} = 1;
|
local $ddclient::globals{debug} = 1;
|
||||||
local $ddclient::globals{verbose} = 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::config;
|
||||||
local %ddclient::recap;
|
local %ddclient::recap;
|
||||||
$ddclient::config{$_} = {
|
$ddclient::config{$_} = {
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
use Test::More;
|
use Test::More;
|
||||||
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
BEGIN {
|
use ddclient::t::HTTPD;
|
||||||
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
|
|
||||||
ddclient::t::HTTPD->import();
|
|
||||||
}
|
|
||||||
use ddclient::t::ip;
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
httpd('4')->run(
|
httpd('4')->run(
|
||||||
sub { return [200, ['Content-Type' => 'text/plain'], ['127.0.0.1 skip 127.0.0.2']]; });
|
sub { return [200, ['Content-Type' => 'text/plain'], ['127.0.0.1 skip 127.0.0.2']]; });
|
||||||
httpd('6')->run(
|
httpd('6')->run(
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
use Test::More;
|
use Test::More;
|
||||||
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
BEGIN {
|
use ddclient::t::HTTPD;
|
||||||
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
|
|
||||||
ddclient::t::HTTPD->import();
|
|
||||||
}
|
|
||||||
use ddclient::t::ip;
|
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"
|
httpd_required();
|
||||||
# so that we can test what happens when certificate validation fails.
|
httpd_ssl_required();
|
||||||
|
|
||||||
httpd('4', 1)->run(sub { return [200, $textplain, ['127.0.0.1']]; });
|
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);
|
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) {
|
for my $tc (@test_cases) {
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
|
||||||
SKIP: {
|
SKIP: {
|
||||||
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
|
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;
|
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"
|
%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})
|
is(ddclient::get_ipv4(ddclient::strategy_inputs('usev4', $h)), $tc->{want}, $tc->{desc})
|
||||||
if ($tc->{cfg}{usev4});
|
if ($tc->{cfg}{usev4});
|
||||||
|
|
|
@ -6,12 +6,11 @@ BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(
|
||||||
use List::Util qw(max);
|
use List::Util qw(max);
|
||||||
use Scalar::Util qw(refaddr);
|
use Scalar::Util qw(refaddr);
|
||||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
BEGIN {
|
use ddclient::t::HTTPD;
|
||||||
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
|
|
||||||
ddclient::t::HTTPD->import();
|
|
||||||
}
|
|
||||||
use ddclient::t::ip;
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
httpd('4')->run();
|
httpd('4')->run();
|
||||||
httpd('6')->run() if httpd('6');
|
httpd('6')->run() if httpd('6');
|
||||||
local %ddclient::builtinweb = (
|
local %ddclient::builtinweb = (
|
||||||
|
|
41
t/use_cmd.pl
Normal file
41
t/use_cmd.pl
Normal 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();
|
18
t/use_web.pl
18
t/use_web.pl
|
@ -1,13 +1,11 @@
|
||||||
use Test::More;
|
use Test::More;
|
||||||
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
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'; } or BAIL_OUT($@); }
|
||||||
BEGIN {
|
use ddclient::t::HTTPD;
|
||||||
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
|
|
||||||
ddclient::t::HTTPD->import();
|
|
||||||
}
|
|
||||||
use ddclient::t::ip;
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
my $builtinweb = 't/use_web.pl builtinweb';
|
my $builtinweb = 't/use_web.pl builtinweb';
|
||||||
my $h = 't/use_web.pl hostname';
|
my $h = 't/use_web.pl hostname';
|
||||||
|
|
||||||
|
@ -70,16 +68,6 @@ for my $ipv ('4', '6') {
|
||||||
}
|
}
|
||||||
|
|
||||||
for my $tc (@test_cases) {
|
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};
|
local $ddclient::builtinweb{$builtinweb} = $tc->{biw};
|
||||||
$ddclient::builtinweb if 0;
|
$ddclient::builtinweb if 0;
|
||||||
local $ddclient::config{$h} = $tc->{cfg};
|
local $ddclient::config{$h} = $tc->{cfg};
|
||||||
|
|
Loading…
Reference in a new issue