Compare commits

..

No commits in common. "main" and "v3.11.0" have entirely different histories.

50 changed files with 4782 additions and 7018 deletions

View file

@ -1,6 +0,0 @@
# Disable autom4te cache to ensure that any change to ddclient.in triggers a
# rebuild of the configure script (which gets the version of ddclient from
# ddclient.in). See <https://lists.gnu.org/r/automake/2019-10/msg00002.html>.
begin-language: "Autoconf-without-aclocal-m4"
args: --no-cache
end-language: "Autoconf-without-aclocal-m4"

View file

@ -6,7 +6,6 @@ on:
jobs:
test-debian-like:
strategy:
fail-fast: false
matrix:
image:
- ubuntu:latest
@ -33,11 +32,10 @@ jobs:
libtest-tcp-perl \
libtest-warnings-perl \
liburi-perl \
libwww-perl \
net-tools \
make \
;
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: autogen
run: ./autogen
- name: configure
@ -48,45 +46,41 @@ jobs:
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
- name: distribution tarball is complete
run: ./.github/workflows/scripts/dist-tarball-check
- if: ${{ matrix.image == 'debian:testing' }}
uses: actions/upload-artifact@v4
with:
name: distribution-tarball
path: ddclient-*.tar.gz
test-fedora-like:
strategy:
fail-fast: false
matrix:
image:
- fedora:39
- fedora:latest
- fedora:rawhide
- almalinux:8
- almalinux:latest
#test-centos8:
# runs-on: ubuntu-latest
# container: centos:8
# steps:
# - uses: actions/checkout@v2
# - name: install dependencies
# run: |
# dnf --refresh --enablerepo=PowerTools install -y \
# automake \
# make \
# perl-HTTP-Daemon \
# perl-IO-Socket-INET6 \
# perl-Test-Warnings \
# perl-core \
# ;
# - name: autogen
# run: ./autogen
# - name: configure
# run: ./configure
# - name: check
# run: make VERBOSE=1 AM_COLOR_TESTS=always check
# - name: distcheck
# run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
test-fedora:
runs-on: ubuntu-latest
container:
image: ${{ matrix.image }}
container: fedora
steps:
- uses: actions/checkout@v4
- name: enable repositories (AlmaLinux 8)
if: ${{ matrix.image == 'almalinux:8' }}
run: |
dnf --refresh install -y 'dnf-command(config-manager)' epel-release &&
dnf config-manager --set-enabled powertools
- name: enable repositories (AlmaLinux latest)
if: ${{ matrix.image == 'almalinux:latest' }}
run: |
dnf --refresh install -y 'dnf-command(config-manager)' epel-release &&
dnf config-manager --set-enabled crb
- uses: actions/checkout@v2
- name: install dependencies
# The --skip-broken argument works around missing packages. (They're
# only used for testing, so it's OK to not install them.)
run: |
dnf --refresh install --skip-broken -y \
dnf --refresh install -y \
automake \
findutils \
iproute \
make \
curl \
perl \
@ -97,8 +91,6 @@ jobs:
perl-Test-MockModule \
perl-Test-TCP \
perl-Test-Warnings \
perl-core \
perl-libwww-perl \
net-tools \
;
- name: autogen
@ -109,3 +101,29 @@ jobs:
run: make VERBOSE=1 AM_COLOR_TESTS=always check
- name: distcheck
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
test-redhat-ubi7:
runs-on: ubuntu-latest
# we use redhats univeral base image which is not available on docker hub
# https://catalog.redhat.com/software/containers/ubi7/ubi/5c3592dcd70cc534b3a37814
container: registry.access.redhat.com/ubi7/ubi
steps:
- uses: actions/checkout@v2
- name: install dependencies
run: |
yum install -y \
automake \
make \
perl-HTTP-Daemon \
perl-IO-Socket-INET6 \
perl-core \
iproute \
;
- name: autogen
run: ./autogen
- name: configure
run: ./configure
- name: check
run: make VERBOSE=1 AM_COLOR_TESTS=always check
- name: distcheck
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck

View file

@ -1,49 +0,0 @@
name: Pull Request
on:
pull_request:
types:
- labeled
- opened
- reopened
- synchronize
- unlabeled
jobs:
linear-history:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'pr-permit-nonlinear') }}
name: Linear History
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: No new merge commits
run: |
log() { printf %s\\n "$*" >&2; }
error() { log "ERROR: $@"; }
fatal() { error "$@"; exit 1; }
try() { log "Running command $@"; "$@" || fatal "'$@' failed"; }
out=$(try git rev-list -n 1 --merges '${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}') || exit 1
[ -z "${out}" ] || {
error "pull request includes a merge commit and does not have the 'pr-permit-nonlinear' label"
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

@ -34,7 +34,6 @@ try git archive --format=tar --prefix=git-repo/ HEAD \
.github \
.gitignore \
docs/ipv6-design-doc.md \
docs/ProviderGuidelines.md \
shell.nix \
;
# TODO: Delete this next line once support for Automake 1.11 is dropped and

3
.gitignore vendored
View file

@ -7,11 +7,8 @@ release
/Makefile.in
/aclocal.m4
/autom4te.cache/
/build-aux/config.guess
/build-aux/config.sub
/build-aux/install-sh
/build-aux/missing
/build-aux/tap-driver.sh
/config.log
/config.status
/configure

View file

@ -80,7 +80,7 @@ perltidy -l=99 -conv -ci=4 -ola -ce -nbbc -kis -pt=2 -b ddclient
## Git Hygiene
* Please keep your pull request commits rebased on top of `main`.
* Please keep your pull request commits rebased on top of master.
* Please use `git rebase -i` to make your commits easy to review:
- Put unrelated changes in separate commits
- Squash your fixup commits
@ -190,11 +190,11 @@ better to revert the original change then redo it:
### Merging Pull Requests
To facilitate reviews and code archaeology, `main` should have a
To facilitate reviews and code archaeology, `master` should have a
semi-linear commit history like this:
```
* f4e6e90 sandro.jaeckel@gmail.com 2020-05-31 07:29:51 +0200 (main)
* f4e6e90 sandro.jaeckel@gmail.com 2020-05-31 07:29:51 +0200 (master)
|\ Merge pull request #142 from rhansen/config-line-format
| * 30180ed rhansen@rhansen.org 2020-05-30 13:09:38 -0400
|/ Expand comment documenting config line format
@ -231,7 +231,7 @@ has value:
change was made) and the merge timestamp (when it went live).
To achieve a history like the above, the pull request must be rebased
onto `main` before merging. Unfortunately, GitHub does not have a
onto `master` before merging. Unfortunately, GitHub does not have a
one-click way to do this (the "Rebase and merge" option does a
fast-forward merge, which is not what we want). See
[isaacs/github#1143](https://github.com/isaacs/github/issues/1143) and
@ -254,15 +254,15 @@ git remote set-url origin git@github.com:ddclient/ddclient.git
# Add a remote for the fork used in the PR
git remote add "${PR_USER:?}" git@github.com:"${PR_USER:?}"/ddclient
# Fetch the latest commits for the PR and ddclient main
# Fetch the latest commits for the PR and ddclient master
git remote update -p
# Switch to the pull request branch
git checkout -b "${PR_USER:?}-${PR_BRANCH:?}" "${PR_USER:?}/${PR_BRANCH:?}"
# Rebase the commits (optionally using -i to clean up history) onto
# the current ddclient main branch
git rebase origin/main
# the current ddclient master branch
git rebase origin/master
# Force update the contributor's fork. This will only work if the
# contributor has checked the "Allow edits by maintainers" box in the
@ -276,19 +276,19 @@ git push -f
# "Allow edits by maintainers", or if you prefer to merge manually,
# continue with the next steps.
# Switch to the local main branch
git checkout main
# Switch to the local master branch
git checkout master
# Make sure the local main branch is up to date
git merge --ff-only origin/main
# Make sure the local master branch is up to date
git merge --ff-only origin/master
# Merge in the rebased pull request branch **WITHOUT DOING A
# FAST-FORWARD MERGE**
git merge --no-ff "${PR_USER:?}-${PR_BRANCH:?}"
# Review the commits before pushing
git log --graph --oneline --decorate origin/main..
git log --graph --oneline --decorate origin/master..
# Push to ddclient main
git push origin main
# Push to ddclient master
git push origin master
```

View file

@ -1,224 +1,9 @@
# ChangeLog
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/master).
## 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)
* Invalid command-line options or values are now fatal errors (instead of
discarded with a warning).
[#733](https://github.com/ddclient/ddclient/pull/733)
* All log messages are now written to STDERR, not a mix of STDOUT and STDERR.
[#676](https://github.com/ddclient/ddclient/pull/676)
* For `--protocol=freedns` and `--protocol=nfsn`, the core module
`Digest::SHA` is now required. Previously, `Digest::SHA1` was used (if
available) as an alternative to `Digest::SHA`.
[#685](https://github.com/ddclient/ddclient/pull/685)
* The `he` built-in web IP discovery service (`--webv4=he`, `--webv6=he`, and
`--web=he`) was renamed to `he.net` for consistency with the new `he.net`
protocol. The old name is still accepted but is deprecated and will be
removed in a future version of ddclient.
[#682](https://github.com/ddclient/ddclient/pull/682)
* Deprecated built-in web IP discovery services are not listed in the output
of `--list-web-services`.
[#682](https://github.com/ddclient/ddclient/pull/682)
* `dyndns2`: Support for "wait" response lines has been removed. The Dyn
documentation does not mention such responses, and the code to handle them,
untouched since at least 2006, is believed to be obsolete.
[#709](https://github.com/ddclient/ddclient/pull/709)
* `dyndns2`: The obsolete `static` and `custom` options have been removed.
Setting the options may produce a warning.
[#709](https://github.com/ddclient/ddclient/pull/709)
* The diagnostic `--geturl` command-line argument was removed.
[#712](https://github.com/ddclient/ddclient/pull/712)
* `easydns`: The default value for `min-interval` was increased from 5m to 10m
to match easyDNS documentation.
[#713](https://github.com/ddclient/ddclient/pull/713)
* `woima`: The dyn.woima.fi service appears to be defunct so support was
removed. [#716](https://github.com/ddclient/ddclient/pull/716)
* `googledomains`: Support was removed because the service shut down.
[#716](https://github.com/ddclient/ddclient/pull/716)
* The `--retry` option was removed.
[#732](https://github.com/ddclient/ddclient/pull/732)
### 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`
([#604](https://github.com/ddclient/ddclient/pull/604)), `noip`
([#603](https://github.com/ddclient/ddclient/pull/603)), `mythicdyn`
([#616](https://github.com/ddclient/ddclient/pull/616)), `godaddy`
([#560](https://github.com/ddclient/ddclient/pull/560)).
* `porkbun`: Added support for subdomains.
[#624](https://github.com/ddclient/ddclient/pull/624)
* `gandi`: Added support for personal access tokens.
[#636](https://github.com/ddclient/ddclient/pull/636)
* Comments after the `\` line continuation character are now supported.
[3c522a7a](https://github.com/ddclient/ddclient/commit/3c522a7aa235f63ae0439e5674e7406e20c90956)
* Minor improvements to `--help` output.
[#659](https://github.com/ddclient/ddclient/pull/659),
[#665](https://github.com/ddclient/ddclient/pull/665)
* Improved formatting of ddclient's version number.
[#639](https://github.com/ddclient/ddclient/pull/639)
* Updated sample systemd service unit file to improve logging in the systemd
journal. [#669](https://github.com/ddclient/ddclient/pull/669)
* The second and subsequent lines in a multi-line log message now have a
different prefix to distinguish them from separate log messages.
[#676](https://github.com/ddclient/ddclient/pull/676)
[#719](https://github.com/ddclient/ddclient/pull/719)
* Log messages now include context, making it easier to troubleshoot issues.
[#725](https://github.com/ddclient/ddclient/pull/725)
* `emailonly`: New `protocol` option that simply emails you when your IP
address changes. [#654](https://github.com/ddclient/ddclient/pull/654)
* `he.net`: Added support for updating Hurricane Electric records.
[#682](https://github.com/ddclient/ddclient/pull/682)
* `dyndns2`, `domeneshop`, `dnsmadeeasy`, `keysystems`: The `server` option
can now include `http://` or `https://` to control the use of TLS. If
omitted, the value of the `ssl` option is used to determine the scheme.
[#703](https://github.com/ddclient/ddclient/pull/703)
* `ddns.fm`: New `protocol` option for updating [DDNS.FM](https://ddns.fm/)
records. [#695](https://github.com/ddclient/ddclient/pull/695)
* `inwx`: New `protocol` option for updating [INWX](https://www.inwx.com/)
records. [#690](https://github.com/ddclient/ddclient/pull/690)
* `domeneshop`: Add IPv6 support.
[#719](https://github.com/ddclient/ddclient/pull/719)
* `duckdns`: Multiple hosts with the same IP address are now updated together.
[#719](https://github.com/ddclient/ddclient/pull/719)
* `directnic`: Added support for updatng Directnic records.
[#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
* Fixed numerous bugs in cache file (recap) handling.
[#740](https://github.com/ddclient/ddclient/pull/740)
* Fixed numerous bugs in command-line option and configuration file
processing. [#733](https://github.com/ddclient/ddclient/pull/733)
* `noip`: Fixed failure to honor IP discovery settings in some circumstances.
[#591](https://github.com/ddclient/ddclient/pull/591)
* Fixed `--usev6` with providers that have not yet been updated to use the new
separate IPv4/IPv6 logic.
[ad854ab7](https://github.com/ddclient/ddclient/commit/ad854ab716922f5f25742421ebd4c27646b86619)
* HTTP redirects (301, 302) are now followed.
[#592](https://github.com/ddclient/ddclient/pull/592)
* `keysystems`: Fixed update URL.
[#629](https://github.com/ddclient/ddclient/pull/629)
* `dondominio`: Fixed response parsing.
[#646](https://github.com/ddclient/ddclient/pull/646)
* Fixed `--web-ssl-validate` and `--fw-ssl-validate` options, which were
ignored in some cases (defaulting to validate).
[#661](https://github.com/ddclient/ddclient/pull/661)
* Explicitly setting `--web-skip`, `--webv4-skip`, `--webv6-skip`,
`--fw-skip`, `--fwv4-skip`, and `--fwv6-skip` to the empty string now
disables any built-in default skip. Before, setting to the empty string had
no effect. [#662](https://github.com/ddclient/ddclient/pull/662)
* `--use=disabled` now works.
[#665](https://github.com/ddclient/ddclient/pull/665)
* `--retry` and `--daemon` are incompatible with each other; ddclient now
errors out if both are provided.
[#666](https://github.com/ddclient/ddclient/pull/666)
* `--usev4=cisco` and `--usev4=cisco-asa` now work.
[#664](https://github.com/ddclient/ddclient/pull/664)
* Fixed "Scalar value better written as" Perl warning.
[#667](https://github.com/ddclient/ddclient/pull/667)
* Fixed "Invalid Value for keyword 'wtime' = ''" warning.
[#734](https://github.com/ddclient/ddclient/pull/734)
* Fixed unnecessary repeated updates for some services.
[#670](https://github.com/ddclient/ddclient/pull/670)
[#732](https://github.com/ddclient/ddclient/pull/732)
* Fixed DNSExit provider when configured with a zone and non-identical
hostname. [#674](https://github.com/ddclient/ddclient/pull/674)
* `infomaniak`: Fixed frequent forced updates after 25 days (`max-interval`).
[#691](https://github.com/ddclient/ddclient/pull/691)
* `infomaniak`: Fixed incorrect parsing of server response.
[#692](https://github.com/ddclient/ddclient/pull/692)
* `infomaniak`: Fixed incorrect handling of `nochg` responses.
[#723](https://github.com/ddclient/ddclient/pull/723)
* `regfishde`: Fixed IPv6 support.
[#691](https://github.com/ddclient/ddclient/pull/691)
* `easydns`: IPv4 and IPv6 addresses are now updated separately to be
consistent with the easyDNS documentation.
[#713](https://github.com/ddclient/ddclient/pull/713)
* `easydns`: Fixed parsing of result code from server response.
[#713](https://github.com/ddclient/ddclient/pull/713)
* `easydns`: Fixed successful updates treated as failed updates.
[#713](https://github.com/ddclient/ddclient/pull/713)
* Any IP addresses in an HTTP response's headers or in an HTTP error
response's body are now ignored when obtaining the IP address from a
web-based IP discovery service (`--usev4=webv4`, `--usev6=webv6`) or from a
router/firewall device.
[#719](https://github.com/ddclient/ddclient/pull/719)
* `yandex`: Errors are now retried.
[#719](https://github.com/ddclient/ddclient/pull/719)
* `gandi`: Fixed handling of error responses.
[#721](https://github.com/ddclient/ddclient/pull/721)
* `dyndns2`: Fixed handling of responses for multi-host updates.
[#728](https://github.com/ddclient/ddclient/pull/728)
* `porkbun`: The default update URL was updated from `porkbun.com` to
`api.porkbun.com`. [#752](https://github.com/ddclient/ddclient/pull/752)
## 2023-11-23 v3.11.2
### Bug fixes
* Fixed simultaneous IPv4 and IPv6 updates for provider duckdns
* Fixed caching issues for new providers when using the old 'use' config parameter
## 2023-10-25 v3.11.1
### Bug fixes
* Fixed simultaneous IPv4 and IPv6 updates for provider porkbun
* Removed @PACKAGE_VERSION@ placeholder in ddclient.in for now
to allow downstream to adopt the proper build process first.
See [here](https://github.com/ddclient/ddclient/issues/579) for the discussion.
## 20XX-XX-XX v3.11.1 (WIP)
## 2023-10-21 v3.11.0
This version is the same as v3.11.0_1 (except for the updated version number in the code).

View file

@ -1,4 +1,4 @@
ACLOCAL_AMFLAGS = -I build-aux/m4
ACLOCAL_AMFLAGS = -I m4
EXTRA_DIST = \
CONTRIBUTING.md \
COPYING \
@ -16,7 +16,19 @@ EXTRA_DIST = \
sample-get-ip-from-fritzbox
CLEANFILES =
# Command that replaces substitution variables with their values.
subst = sed \
-e 's|@PACKAGE_VERSION[@]|$(PACKAGE_VERSION)|g' \
-e '1 s|^\#\!.*perl$$|\#\!$(PERL)|g' \
-e 's|@localstatedir[@]|$(localstatedir)|g' \
-e 's|@runstatedir[@]|$(runstatedir)|g' \
-e 's|@sysconfdir[@]|$(sysconfdir)|g' \
-e 's|@CURL[@]|$(CURL)|g'
# Files that will be generated by passing their *.in file through
# $(subst).
subst_files = ddclient ddclient.conf
EXTRA_DIST += $(subst_files:=.in)
CLEANFILES += $(subst_files)
@ -24,14 +36,7 @@ $(subst_files): Makefile
rm -f '$@' '$@'.tmp
in='$@'.in; \
test -f "$${in}" || in='$(srcdir)/'$${in}; \
sed \
-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|@CURL[@]|$(CURL)|g' \
"$${in}" >'$@'.tmp && \
$(subst) "$${in}" >'$@'.tmp && \
{ ! test -x "$${in}" || chmod +x '$@'.tmp; }
mv '$@'.tmp '$@'
@ -40,7 +45,7 @@ ddclient.conf: $(srcdir)/ddclient.conf.in
bin_SCRIPTS = ddclient
conf_DATA = ddclient.conf
sysconf_DATA = ddclient.conf
install-data-local:
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
@ -57,36 +62,17 @@ AM_PL_LOG_FLAGS = -Mstrict -w \
-I'$(abs_top_srcdir)'/t/lib \
-MDevel::Autoflush
handwritten_tests = \
t/builtinfw_query.pl \
t/check_value.pl \
t/get_ip_from_if.pl \
t/geturl_connectivity.pl \
t/geturl_response.pl \
t/group_hosts_by.pl \
t/header_ok.pl \
t/interval_expired.pl \
t/is-and-extract-ipv4.pl \
t/is-and-extract-ipv6.pl \
t/is-and-extract-ipv6-global.pl \
t/logmsg.pl \
t/parse_assignments.pl \
t/protocol_directnic.pl \
t/protocol_dnsexit2.pl \
t/protocol_dyndns2.pl \
t/read_recap.pl \
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
t/write_cache.pl
generated_tests = \
t/geturl_connectivity.pl \
t/version.pl
TESTS = $(handwritten_tests) $(generated_tests)
$(TESTS): ddclient
EXTRA_DIST += $(handwritten_tests) \
.autom4te.cfg \
t/lib/Devel/Autoflush.pm \
t/lib/Test/Builder.pm \
t/lib/Test/Builder/Formatter.pm \
@ -157,9 +143,5 @@ 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

137
README.md
View file

@ -3,35 +3,7 @@
`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
```
This is a friendly fork/continuation of https://github.com/ddclient/ddclient
## Alternatives
@ -47,15 +19,12 @@ Dynamic DNS services currently supported include:
* [ChangeIP](https://www.changeip.com)
* [CloudFlare](https://www.cloudflare.com)
* [ClouDNS](https://www.cloudns.net)
* [DDNS.fm](https://www.ddns.fm/)
* [DigitalOcean](https://www.digitalocean.com/)
* [dinahosting](https://dinahosting.com)
* [Directnic](https://directnic.com)
* [DonDominio](https://www.dondominio.com)
* [DNS Made Easy](https://dnsmadeeasy.com)
* [DNSExit](https://dnsexit.com/dns/dns-api)
* [dnsHome.de](https://www.dnshome.de)
* [Domeneshop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
* [domenehsop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
* [DslReports](https://www.dslreports.com)
* [Duck DNS](https://duckdns.org)
* [DynDNS.com](https://account.dyn.com)
@ -65,9 +34,8 @@ Dynamic DNS services currently supported include:
* [Freemyip](https://freemyip.com)
* [Gandi](https://gandi.net)
* [GoDaddy](https://www.godaddy.com)
* [Hurricane Electric](https://dns.he.net)
* [Google](https://domains.google)
* [Infomaniak](https://faq.infomaniak.com/2376)
* [INWX](https://www.inwx.com/)
* [Loopia](https://www.loopia.se)
* [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
* [NameCheap](https://www.namecheap.com)
@ -79,6 +47,7 @@ Dynamic DNS services currently supported include:
* [Porkbun](https://porkbun.com)
* [regfish.de](https://www.regfish.de/domains/dyndns)
* [Sitelutions](https://www.sitelutions.com)
* [woima.fi](https://woima.fi)
* [Yandex](https://dns.yandex.com)
* [Zoneedit](https://www.zoneedit.com)
@ -135,7 +104,7 @@ operating system. See the image to the right for a list of distributions with a
```shell
./configure \
--prefix=/usr \
--sysconfdir=/etc \
--sysconfdir=/etc/ddclient \
--localstatedir=/var
make
make VERBOSE=1 check
@ -156,97 +125,43 @@ start the first time by hand
systemctl start ddclient.service
## Known issues
This is a list for quick referencing of known issues. For further details check out the linked issues and the changelog.
Note that any issues prior to version v3.9.1 will not be listed here.
If a fix is committed but not yet part of any tagged release, the notes here will reference the not-yet-released version number.
### v3.11.2 - v3.9.1: SSL parameter breaks HTTP-only IP acquisition
The `ssl` parameter forces all connections to use HTTPS. While technically
working as expected, this behavior keeps coming up as a pain point when using
HTTP-only IP querying sites such as http://checkip.dyndns.org. Starting with
v4.0.0, the behavior is changed to respect `http://` in a URL. A separate
parameter to disallow all HTTP connections or warn about them may be added
later.
**Fix**: v4.0.0 uses HTTP to connect to URLs starting with `http://`. See
[here](https://github.com/ddclient/ddclient/pull/608) for more info.
**Workaround**: Disable the SSL parameter
### v3.10.0: Chunked encoding not corretly supported in IO::Socket HTTP code
Using the IO::Socket HTTP code will break in various ways whenever the server responds using HTTP 1.1 chunked encoding. Refer to [this issue](https://github.com/ddclient/ddclient/issues/548) for more info.
**Fix**: v3.11.0 - IO::Socket has been deprecated there and curl has been made the standard.
**Workaround**: Use curl for transfers by either setting `-curl` in the command line or by adding `curl=yes` in the config
### v3.10.0: Spammed updates to some providers
This issue arises when using the `use` parameter in the config and using one of these providers:
- Cloudflare
- Hetzner
- Digitalocean
- Infomaniak
**Fix**: v3.11.2
**Workaround**: Use the `usev4`/`usev6` parameters instead of `use`.
## TROUBLESHOOTING
* Enable debugging and verbose messages: `ddclient --daemon=0 --debug --verbose`
1. enable debugging and verbose messages: ``$ ddclient -daemon=0 -debug -verbose -noquiet``
* Do you need to specify a proxy?
If so, just add a `proxy=your.isp.proxy` to the `ddclient.conf` file.
2. Do you need to specify a proxy?
If so, just add a ``proxy=your.isp.proxy`` to the ddclient.conf file.
* Define the IP address of your router with `fwv4=xxx.xxx.xxx.xxx` in
`/etc/ddclient/ddclient.conf` and then try `$ ddclient --daemon=0 --query`
to see if the router status web page can be understood.
3. Define the IP address of your router with ``fw=xxx.xxx.xxx.xxx`` in
``/etc/ddclient/ddclient.conf`` and then try ``$ ddclient -daemon=0 -query`` to see if the router status web page can be understood.
* Need support for another router/firewall?
Define the router yourself with:
4. Need support for another router/firewall?
Define the router status page yourself with: ``fw=url-to-your-router``'s-status-page ``fw-skip=any-string-preceding-your-IP-address``
```
usev4=fwv4
fwv4=url-to-your-router-status-page
fwv4-skip="regular expression matching any string preceding your IP address, if necessary"
```
ddclient does something like this to provide builtin support for common
routers.
ddclient does something like this to provide builtin support for
common routers.
For example, the Linksys routers could have been added with:
```
usev4=fwv4
fwv4=192.168.1.1/Status.htm
fwv4-skip=WAN.*?IP Address
```
fw=192.168.1.1/Status.htm
fw-skip=WAN.*?IP Address
OR [create a new issue](https://github.com/ddclient/ddclient/issues/new)
containing the output from:
OR
Send me the output from:
``$ ddclient -geturl {fw-ip-status-url} [-login login [-password password]]``
and I'll add it to the next release!
```
curl --include --location http://url.of.your.firewall/ip-status-page
```
ie. for my fw/router I used: ``$ ddclient -geturl 192.168.1.254/status.htm``
so that we can add a new firewall definition to a future release of
ddclient.
* Some broadband routers require the use of a password when ddclient accesses
its status page to determine the router's WAN IP address.
5. Some broadband routers require the use of a password when ddclient
accesses its status page to determine the router's WAN IP address.
If this is the case for your router, add
```
fw-login=your-router-login
fw-password=your-router-password
```
to the beginning of your ddclient.conf file.
Note that some routers use either 'root' or 'admin' as their login while
some others accept anything.
Note that some routers use either 'root' or 'admin' as their login
while some others accept anything.
## USING DDCLIENT WITH `ppp`
@ -284,7 +199,7 @@ In my case, it is named dhcpcd-eth0.exe and contains the lines:
#!/bin/sh
PATH=/usr/bin:/root/bin:${PATH}
logger -t dhcpcd IP address changed to $1
ddclient --proxy fasthttp.sympatico.ca --wildcard --ip $1 | logger -t ddclient
ddclient -proxy fasthttp.sympatico.ca -wildcard -ip $1 | logger -t ddclient
exit 0
```

26
autogen
View file

@ -7,16 +7,18 @@ fatal() { error "$@"; exit 1; }
try() { "$@" || fatal "'$@' failed"; }
try cd "${0%/*}"
# aclocal complains if a directory passed to AC_CONFIG_MACRO_DIR doesn't exist.
try mkdir -p build-aux/m4
# autoreconf's '--force' option doesn't affect any of the files installed by the '--install' option.
# Remove the files to truly force them to be updated.
try rm -f \
aclocal.m4 \
build-aux/config.guess \
build-aux/config.sub \
build-aux/install-sh \
build-aux/missing \
build-aux/tap-driver.sh \
;
try mkdir -p m4 build-aux
try autoreconf -fviW all
# Ignore changes to build-aux/tap-driver, but only if we're in a clone
# of the ddclient Git repository. Once CentOS 6 and RHEL 6 reach
# end-of-life we can delete build-aux/tap-driver.sh and this block of
# code. (tap-driver.sh is checked in to this Git repository only
# because we want to support all currently maintained CentOS and RHEL
# releases, and CentoOS 6 and RHEL 6 ship with Automake 1.11 which
# does not come with tap-driver.sh.)
command -v git >/dev/null || exit 0
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
cdup=$(try git rev-parse --show-cdup) || exit 1
[ -z "${cdup}" ] || exit 0
try git update-index --assume-unchanged -- build-aux/tap-driver.sh

651
build-aux/tap-driver.sh Executable file
View file

@ -0,0 +1,651 @@
#! /bin/sh
# Copyright (C) 2011-2020 Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# As a special exception to the GNU General Public License, if you
# distribute this file as part of a program that contains a
# configuration script generated by Autoconf, you may include it under
# the same distribution terms that you use for the rest of that program.
# This file is maintained in Automake, please report
# bugs to <bug-automake@gnu.org> or send patches to
# <automake-patches@gnu.org>.
scriptversion=2013-12-23.17; # UTC
# Make unconditional expansion of undefined variables an error. This
# helps a lot in preventing typo-related bugs.
set -u
me=tap-driver.sh
fatal ()
{
echo "$me: fatal: $*" >&2
exit 1
}
usage_error ()
{
echo "$me: $*" >&2
print_usage >&2
exit 2
}
print_usage ()
{
cat <<END
Usage:
tap-driver.sh --test-name=NAME --log-file=PATH --trs-file=PATH
[--expect-failure={yes|no}] [--color-tests={yes|no}]
[--enable-hard-errors={yes|no}] [--ignore-exit]
[--diagnostic-string=STRING] [--merge|--no-merge]
[--comments|--no-comments] [--] TEST-COMMAND
The '--test-name', '-log-file' and '--trs-file' options are mandatory.
END
}
# TODO: better error handling in option parsing (in particular, ensure
# TODO: $log_file, $trs_file and $test_name are defined).
test_name= # Used for reporting.
log_file= # Where to save the result and output of the test script.
trs_file= # Where to save the metadata of the test run.
expect_failure=0
color_tests=0
merge=0
ignore_exit=0
comments=0
diag_string='#'
while test $# -gt 0; do
case $1 in
--help) print_usage; exit $?;;
--version) echo "$me $scriptversion"; exit $?;;
--test-name) test_name=$2; shift;;
--log-file) log_file=$2; shift;;
--trs-file) trs_file=$2; shift;;
--color-tests) color_tests=$2; shift;;
--expect-failure) expect_failure=$2; shift;;
--enable-hard-errors) shift;; # No-op.
--merge) merge=1;;
--no-merge) merge=0;;
--ignore-exit) ignore_exit=1;;
--comments) comments=1;;
--no-comments) comments=0;;
--diagnostic-string) diag_string=$2; shift;;
--) shift; break;;
-*) usage_error "invalid option: '$1'";;
esac
shift
done
test $# -gt 0 || usage_error "missing test command"
case $expect_failure in
yes) expect_failure=1;;
*) expect_failure=0;;
esac
if test $color_tests = yes; then
init_colors='
color_map["red"]="" # Red.
color_map["grn"]="" # Green.
color_map["lgn"]="" # Light green.
color_map["blu"]="" # Blue.
color_map["mgn"]="" # Magenta.
color_map["std"]="" # No color.
color_for_result["ERROR"] = "mgn"
color_for_result["PASS"] = "grn"
color_for_result["XPASS"] = "red"
color_for_result["FAIL"] = "red"
color_for_result["XFAIL"] = "lgn"
color_for_result["SKIP"] = "blu"'
else
init_colors=''
fi
# :; is there to work around a bug in bash 3.2 (and earlier) which
# does not always set '$?' properly on redirection failure.
# See the Autoconf manual for more details.
:;{
(
# Ignore common signals (in this subshell only!), to avoid potential
# problems with Korn shells. Some Korn shells are known to propagate
# to themselves signals that have killed a child process they were
# waiting for; this is done at least for SIGINT (and usually only for
# it, in truth). Without the `trap' below, such a behaviour could
# cause a premature exit in the current subshell, e.g., in case the
# test command it runs gets terminated by a SIGINT. Thus, the awk
# script we are piping into would never seen the exit status it
# expects on its last input line (which is displayed below by the
# last `echo $?' statement), and would thus die reporting an internal
# error.
# For more information, see the Autoconf manual and the threads:
# <https://lists.gnu.org/archive/html/bug-autoconf/2011-09/msg00004.html>
# <http://mail.opensolaris.org/pipermail/ksh93-integration-discuss/2009-February/004121.html>
trap : 1 3 2 13 15
if test $merge -gt 0; then
exec 2>&1
else
exec 2>&3
fi
"$@"
echo $?
) | LC_ALL=C ${AM_TAP_AWK-awk} \
-v me="$me" \
-v test_script_name="$test_name" \
-v log_file="$log_file" \
-v trs_file="$trs_file" \
-v expect_failure="$expect_failure" \
-v merge="$merge" \
-v ignore_exit="$ignore_exit" \
-v comments="$comments" \
-v diag_string="$diag_string" \
'
# TODO: the usages of "cat >&3" below could be optimized when using
# GNU awk, and/on on systems that supports /dev/fd/.
# Implementation note: in what follows, `result_obj` will be an
# associative array that (partly) simulates a TAP result object
# from the `TAP::Parser` perl module.
## ----------- ##
## FUNCTIONS ##
## ----------- ##
function fatal(msg)
{
print me ": " msg | "cat >&2"
exit 1
}
function abort(where)
{
fatal("internal error " where)
}
# Convert a boolean to a "yes"/"no" string.
function yn(bool)
{
return bool ? "yes" : "no";
}
function add_test_result(result)
{
if (!test_results_index)
test_results_index = 0
test_results_list[test_results_index] = result
test_results_index += 1
test_results_seen[result] = 1;
}
# Whether the test script should be re-run by "make recheck".
function must_recheck()
{
for (k in test_results_seen)
if (k != "XFAIL" && k != "PASS" && k != "SKIP")
return 1
return 0
}
# Whether the content of the log file associated to this test should
# be copied into the "global" test-suite.log.
function copy_in_global_log()
{
for (k in test_results_seen)
if (k != "PASS")
return 1
return 0
}
function get_global_test_result()
{
if ("ERROR" in test_results_seen)
return "ERROR"
if ("FAIL" in test_results_seen || "XPASS" in test_results_seen)
return "FAIL"
all_skipped = 1
for (k in test_results_seen)
if (k != "SKIP")
all_skipped = 0
if (all_skipped)
return "SKIP"
return "PASS";
}
function stringify_result_obj(result_obj)
{
if (result_obj["is_unplanned"] || result_obj["number"] != testno)
return "ERROR"
if (plan_seen == LATE_PLAN)
return "ERROR"
if (result_obj["directive"] == "TODO")
return result_obj["is_ok"] ? "XPASS" : "XFAIL"
if (result_obj["directive"] == "SKIP")
return result_obj["is_ok"] ? "SKIP" : COOKED_FAIL;
if (length(result_obj["directive"]))
abort("in function stringify_result_obj()")
return result_obj["is_ok"] ? COOKED_PASS : COOKED_FAIL
}
function decorate_result(result)
{
color_name = color_for_result[result]
if (color_name)
return color_map[color_name] "" result "" color_map["std"]
# If we are not using colorized output, or if we do not know how
# to colorize the given result, we should return it unchanged.
return result
}
function report(result, details)
{
if (result ~ /^(X?(PASS|FAIL)|SKIP|ERROR)/)
{
msg = ": " test_script_name
add_test_result(result)
}
else if (result == "#")
{
msg = " " test_script_name ":"
}
else
{
abort("in function report()")
}
if (length(details))
msg = msg " " details
# Output on console might be colorized.
print decorate_result(result) msg
# Log the result in the log file too, to help debugging (this is
# especially true when said result is a TAP error or "Bail out!").
print result msg | "cat >&3";
}
function testsuite_error(error_message)
{
report("ERROR", "- " error_message)
}
function handle_tap_result()
{
details = result_obj["number"];
if (length(result_obj["description"]))
details = details " " result_obj["description"]
if (plan_seen == LATE_PLAN)
{
details = details " # AFTER LATE PLAN";
}
else if (result_obj["is_unplanned"])
{
details = details " # UNPLANNED";
}
else if (result_obj["number"] != testno)
{
details = sprintf("%s # OUT-OF-ORDER (expecting %d)",
details, testno);
}
else if (result_obj["directive"])
{
details = details " # " result_obj["directive"];
if (length(result_obj["explanation"]))
details = details " " result_obj["explanation"]
}
report(stringify_result_obj(result_obj), details)
}
# `skip_reason` should be empty whenever planned > 0.
function handle_tap_plan(planned, skip_reason)
{
planned += 0 # Avoid getting confused if, say, `planned` is "00"
if (length(skip_reason) && planned > 0)
abort("in function handle_tap_plan()")
if (plan_seen)
{
# Error, only one plan per stream is acceptable.
testsuite_error("multiple test plans")
return;
}
planned_tests = planned
# The TAP plan can come before or after *all* the TAP results; we speak
# respectively of an "early" or a "late" plan. If we see the plan line
# after at least one TAP result has been seen, assume we have a late
# plan; in this case, any further test result seen after the plan will
# be flagged as an error.
plan_seen = (testno >= 1 ? LATE_PLAN : EARLY_PLAN)
# If testno > 0, we have an error ("too many tests run") that will be
# automatically dealt with later, so do not worry about it here. If
# $plan_seen is true, we have an error due to a repeated plan, and that
# has already been dealt with above. Otherwise, we have a valid "plan
# with SKIP" specification, and should report it as a particular kind
# of SKIP result.
if (planned == 0 && testno == 0)
{
if (length(skip_reason))
skip_reason = "- " skip_reason;
report("SKIP", skip_reason);
}
}
function extract_tap_comment(line)
{
if (index(line, diag_string) == 1)
{
# Strip leading `diag_string` from `line`.
line = substr(line, length(diag_string) + 1)
# And strip any leading and trailing whitespace left.
sub("^[ \t]*", "", line)
sub("[ \t]*$", "", line)
# Return what is left (if any).
return line;
}
return "";
}
# When this function is called, we know that line is a TAP result line,
# so that it matches the (perl) RE "^(not )?ok\b".
function setup_result_obj(line)
{
# Get the result, and remove it from the line.
result_obj["is_ok"] = (substr(line, 1, 2) == "ok" ? 1 : 0)
sub("^(not )?ok[ \t]*", "", line)
# If the result has an explicit number, get it and strip it; otherwise,
# automatically assing the next progresive number to it.
if (line ~ /^[0-9]+$/ || line ~ /^[0-9]+[^a-zA-Z0-9_]/)
{
match(line, "^[0-9]+")
# The final `+ 0` is to normalize numbers with leading zeros.
result_obj["number"] = substr(line, 1, RLENGTH) + 0
line = substr(line, RLENGTH + 1)
}
else
{
result_obj["number"] = testno
}
if (plan_seen == LATE_PLAN)
# No further test results are acceptable after a "late" TAP plan
# has been seen.
result_obj["is_unplanned"] = 1
else if (plan_seen && testno > planned_tests)
result_obj["is_unplanned"] = 1
else
result_obj["is_unplanned"] = 0
# Strip trailing and leading whitespace.
sub("^[ \t]*", "", line)
sub("[ \t]*$", "", line)
# This will have to be corrected if we have a "TODO"/"SKIP" directive.
result_obj["description"] = line
result_obj["directive"] = ""
result_obj["explanation"] = ""
if (index(line, "#") == 0)
return # No possible directive, nothing more to do.
# Directives are case-insensitive.
rx = "[ \t]*#[ \t]*([tT][oO][dD][oO]|[sS][kK][iI][pP])[ \t]*"
# See whether we have the directive, and if yes, where.
pos = match(line, rx "$")
if (!pos)
pos = match(line, rx "[^a-zA-Z0-9_]")
# If there was no TAP directive, we have nothing more to do.
if (!pos)
return
# Let`s now see if the TAP directive has been escaped. For example:
# escaped: ok \# SKIP
# not escaped: ok \\# SKIP
# escaped: ok \\\\\# SKIP
# not escaped: ok \ # SKIP
if (substr(line, pos, 1) == "#")
{
bslash_count = 0
for (i = pos; i > 1 && substr(line, i - 1, 1) == "\\"; i--)
bslash_count += 1
if (bslash_count % 2)
return # Directive was escaped.
}
# Strip the directive and its explanation (if any) from the test
# description.
result_obj["description"] = substr(line, 1, pos - 1)
# Now remove the test description from the line, that has been dealt
# with already.
line = substr(line, pos)
# Strip the directive, and save its value (normalized to upper case).
sub("^[ \t]*#[ \t]*", "", line)
result_obj["directive"] = toupper(substr(line, 1, 4))
line = substr(line, 5)
# Now get the explanation for the directive (if any), with leading
# and trailing whitespace removed.
sub("^[ \t]*", "", line)
sub("[ \t]*$", "", line)
result_obj["explanation"] = line
}
function get_test_exit_message(status)
{
if (status == 0)
return ""
if (status !~ /^[1-9][0-9]*$/)
abort("getting exit status")
if (status < 127)
exit_details = ""
else if (status == 127)
exit_details = " (command not found?)"
else if (status >= 128 && status <= 255)
exit_details = sprintf(" (terminated by signal %d?)", status - 128)
else if (status > 256 && status <= 384)
# We used to report an "abnormal termination" here, but some Korn
# shells, when a child process die due to signal number n, can leave
# in $? an exit status of 256+n instead of the more standard 128+n.
# Apparently, both behaviours are allowed by POSIX (2008), so be
# prepared to handle them both. See also Austing Group report ID
# 0000051 <http://www.austingroupbugs.net/view.php?id=51>
exit_details = sprintf(" (terminated by signal %d?)", status - 256)
else
# Never seen in practice.
exit_details = " (abnormal termination)"
return sprintf("exited with status %d%s", status, exit_details)
}
function write_test_results()
{
print ":global-test-result: " get_global_test_result() > trs_file
print ":recheck: " yn(must_recheck()) > trs_file
print ":copy-in-global-log: " yn(copy_in_global_log()) > trs_file
for (i = 0; i < test_results_index; i += 1)
print ":test-result: " test_results_list[i] > trs_file
close(trs_file);
}
BEGIN {
## ------- ##
## SETUP ##
## ------- ##
'"$init_colors"'
# Properly initialized once the TAP plan is seen.
planned_tests = 0
COOKED_PASS = expect_failure ? "XPASS": "PASS";
COOKED_FAIL = expect_failure ? "XFAIL": "FAIL";
# Enumeration-like constants to remember which kind of plan (if any)
# has been seen. It is important that NO_PLAN evaluates "false" as
# a boolean.
NO_PLAN = 0
EARLY_PLAN = 1
LATE_PLAN = 2
testno = 0 # Number of test results seen so far.
bailed_out = 0 # Whether a "Bail out!" directive has been seen.
# Whether the TAP plan has been seen or not, and if yes, which kind
# it is ("early" is seen before any test result, "late" otherwise).
plan_seen = NO_PLAN
## --------- ##
## PARSING ##
## --------- ##
is_first_read = 1
while (1)
{
# Involutions required so that we are able to read the exit status
# from the last input line.
st = getline
if (st < 0) # I/O error.
fatal("I/O error while reading from input stream")
else if (st == 0) # End-of-input
{
if (is_first_read)
abort("in input loop: only one input line")
break
}
if (is_first_read)
{
is_first_read = 0
nextline = $0
continue
}
else
{
curline = nextline
nextline = $0
$0 = curline
}
# Copy any input line verbatim into the log file.
print | "cat >&3"
# Parsing of TAP input should stop after a "Bail out!" directive.
if (bailed_out)
continue
# TAP test result.
if ($0 ~ /^(not )?ok$/ || $0 ~ /^(not )?ok[^a-zA-Z0-9_]/)
{
testno += 1
setup_result_obj($0)
handle_tap_result()
}
# TAP plan (normal or "SKIP" without explanation).
else if ($0 ~ /^1\.\.[0-9]+[ \t]*$/)
{
# The next two lines will put the number of planned tests in $0.
sub("^1\\.\\.", "")
sub("[^0-9]*$", "")
handle_tap_plan($0, "")
continue
}
# TAP "SKIP" plan, with an explanation.
else if ($0 ~ /^1\.\.0+[ \t]*#/)
{
# The next lines will put the skip explanation in $0, stripping
# any leading and trailing whitespace. This is a little more
# tricky in truth, since we want to also strip a potential leading
# "SKIP" string from the message.
sub("^[^#]*#[ \t]*(SKIP[: \t][ \t]*)?", "")
sub("[ \t]*$", "");
handle_tap_plan(0, $0)
}
# "Bail out!" magic.
# Older versions of prove and TAP::Harness (e.g., 3.17) did not
# recognize a "Bail out!" directive when preceded by leading
# whitespace, but more modern versions (e.g., 3.23) do. So we
# emulate the latter, "more modern" behaviour.
else if ($0 ~ /^[ \t]*Bail out!/)
{
bailed_out = 1
# Get the bailout message (if any), with leading and trailing
# whitespace stripped. The message remains stored in `$0`.
sub("^[ \t]*Bail out![ \t]*", "");
sub("[ \t]*$", "");
# Format the error message for the
bailout_message = "Bail out!"
if (length($0))
bailout_message = bailout_message " " $0
testsuite_error(bailout_message)
}
# Maybe we have too look for dianogtic comments too.
else if (comments != 0)
{
comment = extract_tap_comment($0);
if (length(comment))
report("#", comment);
}
}
## -------- ##
## FINISH ##
## -------- ##
# A "Bail out!" directive should cause us to ignore any following TAP
# error, as well as a non-zero exit status from the TAP producer.
if (!bailed_out)
{
if (!plan_seen)
{
testsuite_error("missing test plan")
}
else if (planned_tests != testno)
{
bad_amount = testno > planned_tests ? "many" : "few"
testsuite_error(sprintf("too %s tests run (expected %d, got %d)",
bad_amount, planned_tests, testno))
}
if (!ignore_exit)
{
# Fetch exit status from the last line.
exit_message = get_test_exit_message(nextline)
if (exit_message)
testsuite_error(exit_message)
}
}
write_test_results()
exit 0
} # End of "BEGIN" block.
'
# TODO: document that we consume the file descriptor 3 :-(
} 3>"$log_file"
test $? -eq 0 || fatal "I/O or internal error"
# Local Variables:
# mode: shell-script
# sh-indentation: 2
# eval: (add-hook 'before-save-hook 'time-stamp)
# time-stamp-start: "scriptversion="
# time-stamp-format: "%:y-%02m-%02d.%02H"
# time-stamp-time-zone: "UTC0"
# time-stamp-end: "; # UTC"
# End:

View file

@ -1,17 +1,8 @@
AC_PREREQ([2.63])
# Get the version from ddclient.in so that the same version string
# doesn't have to be maintained in two places. The m4_dquote macro is
# used instead of quote characters to ensure that the command is only
# run once. The command outputs quote characters to prevent
# incidental expansion (the m4_esyscmd macro does not quote the
# command output itself, so the command output is subject to
# expansion).
AC_INIT([ddclient], m4_dquote(m4_esyscmd([printf '[%s]' "$(./ddclient.in --version=short)"])))
# Needed because of the above invocation of ddclient.in.
AC_SUBST([CONFIGURE_DEPENDENCIES], ['$(top_srcdir)/ddclient.in'])
AC_INIT([ddclient], [3.11.0])
AC_CONFIG_SRCDIR([ddclient.in])
AC_CONFIG_AUX_DIR([build-aux])
AC_CONFIG_MACRO_DIR([build-aux/m4])
AC_CONFIG_MACRO_DIR([m4])
AC_REQUIRE_AUX_FILE([tap-driver.sh])
# If the automake dependency is bumped to v1.12 or newer, remove
# build-aux/tap-driver.sh from the repository. Automake 1.12+ comes
@ -23,18 +14,6 @@ AC_REQUIRE_AUX_FILE([tap-driver.sh])
AM_INIT_AUTOMAKE([1.11 -Wall -Werror foreign subdir-objects parallel-tests])
AM_SILENT_RULES
m4_define([CONFDIR_DEFAULT], [${sysconfdir}/AC_PACKAGE_NAME])
AC_ARG_WITH(
[confdir],
[AS_HELP_STRING(
[--with-confdir=DIR],
m4_expand([[look for ddclient.conf in DIR @<:@default: ]CONFDIR_DEFAULT[@:>@]]))],
[],
# The single quotes are intentional; see:
# https://www.gnu.org/software/automake/manual/html_node/Uniform.html
[with_confdir='CONFDIR_DEFAULT'])
AC_SUBST([confdir], [${with_confdir}])
AC_PROG_MKDIR_P
# The Fedora Docker image doesn't come with the 'findutils' package.
@ -48,18 +27,7 @@ AC_PROG_MKDIR_P
AC_PATH_PROG([FIND], [find])
AS_IF([test -z "${FIND}"], [AC_MSG_ERROR(['find' utility not found])])
AC_ARG_WITH([curl],
[AS_HELP_STRING([[--with-curl[=CURL]]], [use CURL as absolute path to curl executable])],
[],
[with_curl=yes])
AS_CASE([${with_curl}],
[[yes]], [AC_PATH_PROG([CURL], [curl])],
[[no]], [CURL=],
[
AC_MSG_CHECKING([for curl])
CURL=${with_curl}
AC_MSG_RESULT([${CURL}])
]);
AC_PATH_PROG([CURL], [curl])
AS_IF([test -z "${CURL}"], [AC_MSG_ERROR([curl not found])])
AX_WITH_PROG([PERL], perl)
@ -72,7 +40,6 @@ AC_SUBST([PERL])
# package doesn't depend on all of them, so their availability can't
# be assumed.
m4_foreach_w([_m], [
Data::Dumper
File::Basename
File::Path
File::Temp
@ -87,12 +54,9 @@ m4_foreach_w([_m], [
# then some tests will fail. Only prints a warning if not installed.
m4_foreach_w([_m], [
B
Exporter
Data::Dumper
File::Spec::Functions
File::Temp
List::Util
Scalar::Util
re
], [AX_PROG_PERL_MODULES([_m], [],
[AC_MSG_WARN([some tests will fail due to missing module _m])])])
@ -101,23 +65,24 @@ 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
HTTP::Request
HTTP::Response
JSON::PP
Scalar::Util
Test::MockModule
Test::TCP
Test::Warnings
Time::HiRes
URI
parent
], [AX_PROG_PERL_MODULES([_m], [],
[AC_MSG_WARN([some tests may be skipped due to missing module _m])])])
AC_CONFIG_FILES([
Makefile
t/geturl_connectivity.pl
t/version.pl
])
AC_OUTPUT

View file

@ -16,21 +16,15 @@
## are mentioned here.
##
######################################################################
## Use encryption (TLS) when the scheme (either "http://" or "https://") is
## missing from a URL. Defaults to "yes".
#ssl=yes
daemon=300 # check every 300 seconds
syslog=yes # log update msgs to syslog
mail=root # mail all msgs to root
mail-failure=root # mail failed update msgs to root
# 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.
ssl=yes # use ssl-support. Works with
# ssl-library
# postscript=script # run script after updating. The
# new IP is added as argument.
#
#use=watchguard-soho, fw=192.168.111.1:80 # via Watchguard's SOHO FW
#use=netopia-r910, fw=192.168.111.1:80 # via Netopia R910 FW
@ -53,10 +47,6 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
## To obtain an IP address from FW status page (using fw-login, fw-password)
#use=fw, fw=192.168.1.254/status.htm, fw-skip='IP Address' # found after IP Address
#
## To obtain an IP address via UPnP from router
## Requires miniupnpc to be installed on the system.
#use=cmd, cmd=external-ip
#
## To obtain an IP address from Web status page (using the proxy if defined)
## by default, checkip.dyndns.org is used if you use the dyndns protocol.
## Using use=web is enough to get it working.
@ -88,6 +78,26 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
# protocol=dyndns2 \
# your-dynamic-host.dyndns.org
##
## dyndns.org static addresses
##
## (supports variables: wildcard,mx,backupmx)
##
# static=yes, \
# server=members.dyndns.org, \
# protocol=dyndns2 \
# your-static-host.dyndns.org
##
## dyndns.org custom addresses
##
## (supports variables: wildcard,mx,backupmx)
##
# custom=yes, \
# server=members.dyndns.org, \
# protocol=dyndns2 \
# your-domain.top-level,your-other-domain.top-level
##
## ZoneEdit (zoneedit.com)
##
@ -138,9 +148,9 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
##
# protocol = nfsn, \
# zone=example.com, \
# login=member-login, \
# password=api-key \
# password=api-key, \
# zone=example.com \
# example.com,subdomain.example.com
##
@ -161,7 +171,7 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
# ssl=yes, \
# server=dynupdate.no-ip.com, \
# login=your-noip-login, \
# password=your-noip-password \
# password=your-noip-password, \
# your-host.domain.com, your-2nd-host.domain.com
##
@ -187,29 +197,19 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
## Gandi (gandi.net)
##
## Single host update
# protocol=gandi
# zone=example.com
# password=my-gandi-access-token
# use-personal-access-token=yes
# ttl=10800 # optional
# myhost.example.com
##
## GoDaddy (godaddy.com)
##
# protocol=godaddy, \
# password=my-godaddy-api-key, \
# password=my-godaddy-secret, \
# ttl=600 \
# protocol=gandi, \
# zone=example.com, \
# myhost.example.com,nexthost.example.com
# password=my-gandi-api-key, \
# ttl=3h \
# myhost.example.com
##
## Hurricane Electric (dns.he.net)
## Google Domains (www.google.com/domains)
##
# protocol=he.net, \
# password=my-genereated-password \
# myhost.example.com
# protocol=googledomains,
# login=my-auto-generated-username,
# password=my-auto-generated-password
# my.domain.tld, otherhost.domain.tld
##
## Duckdns (http://www.duckdns.org/)
@ -227,14 +227,6 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
# password=my-token
# myhost
##
## DDNS.FM (https://ddns.fm/)
##
#
# protocol=ddns.fm,
# password=my-token
# myhost.example.com
##
## MyOnlinePortal (http://myonlineportal.net)
##
@ -299,9 +291,8 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
# protocol=porkbun
# apikey=APIKey
# secretapikey=SecretAPIKey
# root-domain=example.com
# host.example.com,host2.sub.example.com
# example.com,sub.example.com
# on-root-domain=yes example.com,sub.example.com
##
## ClouDNS (https://www.cloudns.net)
@ -372,14 +363,6 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
#password=api-token \
#example.com,sub.example.com
##
## Directnic (directnic.com)
##
# protocol=directnic,
# urlv4=https://directnic.com/dns/gateway/ipv4_token/
# urlv6=https://directnic.com/dns/gateway/ipv6_token/
# my-domain.com
##
## Infomaniak (www.infomaniak.com)
##
@ -387,35 +370,3 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
# login=ddns_username,
# password=ddns_password
# example.com
#
# N.B. the infomaniak protocol is obsolete. Please use dyndns2 instead:
#
# protocol=dyndns2,
# use=web, web=infomaniak.com/ip.php/
# login=ddns_username,
# password=ddns_password
# redirect=2
# example.com
##
## Email Only
##
# protocol=emailonly
# host.example.com
##
## dnsHome.de
##
# protocol=dyndns2 \
# server=www.dnshome.de \
# login=subdomain.domain.tld \
# password=your_password \
# subdomain.domain.tld
##
## INWX
##
# protocol=inwx \
# login=my-inwx-DynDNS-account-username \
# password=my-inwx-DynDNS-account-password \
# myhost.example.org

File diff suppressed because it is too large Load diff

View file

@ -1,39 +0,0 @@
# Provider implementations
Author: [@LenardHess](https://github.com/LenardHess/)\
Date: 2023-11-23
This document is meant to detail the mechanisms that provider implementation shall use. It differentiates between new and legacy provider implementations. The former are adhering to the IPv6 support updates being done to ddclient, the legacy ones are from before that update.
## New provider Implementation
1. Grab the IP(s) from $config{$host}{'wantipv4'} and/or $config{$host}{'wantipv6'}
2. Optional: Query the provider for the current IP record(s). If they are already good, skip updating IP record(s)
3. Update the IP record(s).
4. If successful (or if the records were already good):
- Set 'status-ipv4' and/or 'status-ipv6' to 'good'
- Set 'ipv4' and/or 'ipv6' to the IP that has been set
- Set 'mtime' to the current time
5. If not successful:
- Set 'status-ipv4' and/or 'status-ipv6' to an error message
- Set 'atime' to the current time
The new provider implementation should not set 'status' nor 'ip'. They're part of the legacy infrastructure and ddclient will take care of setting them correctly.
## Legacy provider implementations
1. Grab the IP from $config{$host}{'wantip'}
2. Optional: Query the provider for the current IP record. If it is already good, skip updating IP record
3. Update the IP record.
4. If successful (or if the record was already good):
- Set 'status' to 'good'
- Set 'ip' to the IP that has been set
- Set 'mtime' to the current time
5. If not successful:
- Set 'status' to an error message
- Set 'atime' to the current time
# ToDo
- Decide/Inquire whether services prefer querying the IP first. Then decide whether to make it mandatory.
- Write guidelines on checking existing records (i.e. check TTL as well?).
- Start a list of providers and their implementation state
- Add more details to this document
- Whether 'wantip*' ought to be deleted when read or not.

View file

@ -10,3 +10,7 @@
## force an update twice a month (only if you are not using daemon-mode)
##
## 30 23 1,15 * * root /usr/bin/ddclient -daemon=0 -syslog -quiet -force
######################################################################
## retry failed updates every hour (only if you are not using daemon-mode)
##
## 0 * * * * root /usr/bin/ddclient -daemon=0 -syslog -quiet retry

View file

@ -4,10 +4,9 @@ Wants=network-online.target
After=network-online.target nss-lookup.target
[Service]
Type=exec
Environment=daemon_interval=5m
ExecStart=/usr/bin/ddclient --daemon ${daemon_interval} --foreground
Restart=on-failure
Type=forking
PIDFile=/run/ddclient.pid
ExecStart=/usr/bin/ddclient
[Install]
WantedBy=multi-user.target

View file

@ -1,169 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
sub setbuiltinfw {
my ($fw) = @_;
no warnings 'once';
$ddclient::builtinfw{$fw->{name}} = $fw;
%ddclient::ip_strategies = ddclient::builtinfw_strategy($fw->{name});
%ddclient::ipv4_strategies = ddclient::builtinfwv4_strategy($fw->{name});
%ddclient::ipv6_strategies = ddclient::builtinfwv6_strategy($fw->{name});
}
my @gotcalls;
my $skip_test_fw = 't/builtinfw_query.pl skip test';
setbuiltinfw({
name => $skip_test_fw,
query => sub { return '192.0.2.1 skip1 192.0.2.2 skip2 192.0.2.3'; },
queryv4 => sub { return '192.0.2.4 skip1 192.0.2.5 skip3 192.0.2.6'; },
queryv6 => sub { return '2001:db8::1 skip1 2001:db8::2 skip4 2001:db8::3'; },
});
my @skip_test_cases = (
{
desc => 'query',
getip => \&ddclient::get_ip,
useopt => 'use',
cfgxtra => {},
want => '192.0.2.2',
},
{
desc => 'queryv4',
getip => \&ddclient::get_ipv4,
useopt => 'usev4',
cfgxtra => {'fwv4-skip' => 'skip3'},
want => '192.0.2.6',
},
{
desc => 'queryv4 with fw-skip fallback',
getip => \&ddclient::get_ipv4,
useopt => 'usev4',
cfgxtra => {},
want => '192.0.2.5',
},
{
desc => 'queryv6',
getip => \&ddclient::get_ipv6,
useopt => 'usev6',
cfgxtra => {'fwv6-skip' => 'skip4'},
want => '2001:db8::3',
},
{
# Support for --usev6=<builtin> wasn't added until after --fwv6-skip was added, so fallback
# to the deprecated --fw-skip option was never needed.
desc => 'queryv6 ignores fw-skip',
getip => \&ddclient::get_ipv6,
useopt => 'usev6',
cfgxtra => {},
want => '2001:db8::1',
},
);
for my $tc (@skip_test_cases) {
my $h = "t/builtinfw_query.pl $tc->{desc}";
$ddclient::config{$h} = {
$tc->{useopt} => $skip_test_fw,
'fw-skip' => 'skip1',
%{$tc->{cfgxtra}},
};
my $got = $tc->{getip}(ddclient::strategy_inputs($tc->{useopt}, $h));
is($got, $tc->{want}, $tc->{desc});
}
my $default_inputs_fw = 't/builtinfw_query.pl default inputs';
setbuiltinfw({
name => $default_inputs_fw,
query => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.1'; },
queryv4 => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.2'; },
queryv6 => sub { my %p = @_; push(@gotcalls, \%p); return '2001:db8::1'; },
});
my @default_inputs_test_cases = (
{
desc => 'use with default inputs',
getip => \&ddclient::get_ip,
useopt => 'use',
want => {use => $default_inputs_fw, fw => 'server', 'fw-skip' => 'skip',
'fw-login' => 'login', 'fw-password' => 'password', 'fw-ssl-validate' => 1},
},
{
desc => 'usev4 with default inputs',
getip => \&ddclient::get_ipv4,
useopt => 'usev4',
want => {usev4 => $default_inputs_fw, fwv4 => 'serverv4', fw => 'server',
'fwv4-skip' => 'skipv4', 'fw-skip' => 'skip', 'fw-login' => 'login',
'fw-password' => 'password', 'fw-ssl-validate' => 1},
},
{
desc => 'usev6 with default inputs',
getip => \&ddclient::get_ipv6,
useopt => 'usev6',
want => {usev6 => $default_inputs_fw, fwv6 => 'serverv6', 'fwv6-skip' => 'skipv6'},
},
);
for my $tc (@default_inputs_test_cases) {
my $h = "t/builtinfw_query.pl $tc->{desc}";
$ddclient::config{$h} = {
$tc->{useopt} => $default_inputs_fw,
'fw' => 'server',
'fwv4' => 'serverv4',
'fwv6' => 'serverv6',
'fw-login' => 'login',
'fw-password' => 'password',
'fw-ssl-validate' => 1,
'fw-skip' => 'skip',
'fwv4-skip' => 'skipv4',
'fwv6-skip' => 'skipv6',
};
@gotcalls = ();
$tc->{getip}(ddclient::strategy_inputs($tc->{useopt}, $h));
is_deeply(\@gotcalls, [$tc->{want}], $tc->{desc});
}
my $custom_inputs_fw = 't/builtinfw_query.pl custom inputs';
setbuiltinfw({
name => $custom_inputs_fw,
query => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.1'; },
inputs => ['if'],
queryv4 => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.2'; },
inputsv4 => ['ifv4'],
queryv6 => sub { my %p = @_; push(@gotcalls, \%p); return '2001:db8::1'; },
inputsv6 => ['ifv6'],
});
my @custom_inputs_test_cases = (
{
desc => 'use with custom inputs',
getip => \&ddclient::get_ip,
useopt => 'use',
want => {use => $custom_inputs_fw, if => 'eth0'},
},
{
desc => 'usev4 with custom inputs',
getip => \&ddclient::get_ipv4,
useopt => 'usev4',
want => {usev4 => $custom_inputs_fw, ifv4 => 'eth4'},
},
{
desc => 'usev6 with custom inputs',
getip => \&ddclient::get_ipv6,
useopt => 'usev6',
want => {usev6 => $custom_inputs_fw, ifv6 => 'eth6'},
},
);
for my $tc (@custom_inputs_test_cases) {
my $h = "t/builtinfw_query.pl $tc->{desc}";
$ddclient::config{$h} = {
$tc->{useopt} => $custom_inputs_fw,
'if' => 'eth0',
'ifv4' => 'eth4',
'ifv6' => 'eth6',
};
@gotcalls = ();
$tc->{getip}(ddclient::strategy_inputs($tc->{useopt}, $h));
is_deeply(\@gotcalls, [$tc->{want}], $tc->{desc});
}
done_testing();

View file

@ -1,53 +0,0 @@
use Test::More;
use strict;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
my @test_cases = (
{
type => ddclient::T_FQDN(),
input => 'example.com',
want => 'example.com',
},
{
type => ddclient::T_FQDN(),
input => 'example',
want_invalid => 1,
},
{
type => ddclient::T_URL(),
input => 'https://www.example.com',
want => 'https://www.example.com',
},
{
type => ddclient::T_URL(),
input => 'https://directnic.com/dns/gateway/ad133/',
want => 'https://directnic.com/dns/gateway/ad133/',
},
{
type => ddclient::T_URL(),
input => 'HTTPS://MixedCase.com/',
want => 'HTTPS://MixedCase.com/',
},
{
type => ddclient::T_URL(),
input => 'ftp://bad.protocol/',
want_invalid => 1,
},
{
type => ddclient::T_URL(),
input => 'bad-url',
want_invalid => 1,
},
);
for my $tc (@test_cases) {
my $got;
my $got_invalid = !(eval {
$got = ddclient::check_value($tc->{input},
ddclient::setv($tc->{type}, 0, 0, undef, undef));
1;
});
is($got_invalid, !!$tc->{want_invalid}, "$tc->{type}: $tc->{input}: validity");
is($got, $tc->{want}, "$tc->{type}: $tc->{input}: normalization") if !$tc->{want_invalid};
}
done_testing();

View file

@ -1,7 +1,12 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
# To aid in debugging, uncomment the following lines. (They are normally left commented to avoid
# accidentally interfering with the Test Anything Protocol messages written by Test::More.)
#STDOUT->autoflush(1);
#$ddclient::globals{'debug'} = 1;
subtest "get_default_interface tests" => sub {
for my $sample (@ddclient::t::routing_samples) {
@ -34,30 +39,23 @@ subtest "get_ip_from_interface tests" => sub {
}
};
subtest "Get default interface and IP for test system (IPv4)" => sub {
subtest "Get default interface and IP for test system" => sub {
my $interface = ddclient::get_default_interface(4);
plan(skip_all => 'no IPv4 interface') if !$interface;
if ($interface) {
isnt($interface, "lo", "Check for loopback 'lo'");
isnt($interface, "lo0", "Check for loopback 'lo0'");
my $ip1 = ddclient::get_ip_from_interface("default", 4);
my $ip2 = ddclient::get_ip_from_interface($interface, 4);
is($ip1, $ip2, "Check IPv4 from default interface");
SKIP: {
skip('default interface does not have an appropriate IPv4 addresses') if !$ip1;
ok(ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)");
}
};
subtest "Get default interface and IP for test system (IPv6)" => sub {
my $interface = ddclient::get_default_interface(6);
plan(skip_all => 'no IPv6 interface') if !$interface;
$interface = ddclient::get_default_interface(6);
if ($interface) {
isnt($interface, "lo", "Check for loopback 'lo'");
isnt($interface, "lo0", "Check for loopback 'lo0'");
my $ip1 = ddclient::get_ip_from_interface("default", 6);
my $ip2 = ddclient::get_ip_from_interface($interface, 6);
is($ip1, $ip2, "Check IPv6 from default interface");
SKIP: {
skip('default interface does not have an appropriate IPv6 addresses') if !$ip1;
ok(ddclient::is_ipv6($ip1), "Valid IPv6 from get_ip_from_interface($interface)");
}
};

View file

@ -1,57 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
use ddclient::t::ip;
httpd_required();
$ddclient::globals{'ssl_ca_file'} = $ca_file;
for my $ipv ('4', '6') {
for my $ssl (0, 1) {
my $httpd = httpd($ipv, $ssl) or next;
$httpd->run(sub {
return [200, ['Content-Type' => 'application/octet-stream'], [$_[0]->as_string()]];
});
}
}
my @test_cases = (
{ipv6_opt => 0, server_ipv => '4', client_ipv => ''},
{ipv6_opt => 0, server_ipv => '4', client_ipv => '4'},
# IPv* client to a non-SSL IPv6 server is not expected to work unless opt('ipv6') is true
{ipv6_opt => 0, server_ipv => '6', client_ipv => '6'},
# Fetch without ssl
{ server_ipv => '4', client_ipv => '' },
{ server_ipv => '4', client_ipv => '4' },
{ server_ipv => '6', client_ipv => '' },
{ server_ipv => '6', client_ipv => '6' },
# Fetch with ssl
{ ssl => 1, server_ipv => '4', client_ipv => '' },
{ ssl => 1, server_ipv => '4', client_ipv => '4' },
{ ssl => 1, server_ipv => '6', client_ipv => '' },
{ ssl => 1, server_ipv => '6', client_ipv => '6' },
);
for my $tc (@test_cases) {
$tc->{ipv6_opt} //= 0;
$tc->{ssl} //= 0;
SKIP: {
skip("IPv6 not supported on this system", 1)
if $tc->{server_ipv} eq '6' && !$ipv6_supported;
skip("HTTP::Daemon too old for IPv6 support", 1)
if $tc->{server_ipv} eq '6' && !$httpd_ipv6_supported;
skip("HTTP::Daemon::SSL not available", 1) if $tc->{ssl} && !$httpd_ssl_supported;
my $uri = httpd($tc->{server_ipv}, $tc->{ssl})->endpoint();
my $name = sprintf("IPv%s client to %s%s",
$tc->{client_ipv} || '*', $uri, $tc->{ipv6_opt} ? ' (-ipv6)' : '');
$ddclient::globals{'ipv6'} = $tc->{ipv6_opt};
my $got = ddclient::geturl(url => $uri, ipversion => $tc->{client_ipv});
isnt($got // '', '', $name);
}
}
done_testing();

View file

@ -0,0 +1,93 @@
use Test::More;
eval { require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@);
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
my $has_http_daemon_ssl = eval { require HTTP::Daemon::SSL; };
my $ipv6_supported = eval {
require IO::Socket::IP;
my $ipv6_socket = IO::Socket::IP->new(
Domain => 'PF_INET6',
LocalHost => '::1',
Listen => 1,
);
defined($ipv6_socket);
};
my $http_daemon_supports_ipv6 = eval {
require HTTP::Daemon;
HTTP::Daemon->VERSION(6.12);
};
# To aid in debugging, uncomment the following lines. (They are normally left commented to avoid
# accidentally interfering with the Test Anything Protocol messages written by Test::More.)
#STDOUT->autoflush(1);
#$ddclient::globals{'verbose'} = 1;
my $certdir = "$ENV{abs_top_srcdir}/t/lib/ddclient/Test/Fake/HTTPD";
$ddclient::globals{'ssl_ca_file'} = "$certdir/dummy-ca-cert.pem";
sub run_httpd {
my ($ipv6, $ssl) = @_;
return undef if $ssl && !$has_http_daemon_ssl;
return undef if $ipv6 && (!$ipv6_supported || !$http_daemon_supports_ipv6);
my $httpd = ddclient::Test::Fake::HTTPD->new(
host => $ipv6 ? '::1' : '127.0.0.1',
scheme => $ssl ? 'https' : 'http',
daemon_args => {
SSL_cert_file => "$certdir/dummy-server-cert.pem",
SSL_key_file => "$certdir/dummy-server-key.pem",
V6Only => 1,
},
);
$httpd->run(sub {
# Echo back the full request.
return [200, ['Content-Type' => 'application/octet-stream'], [$_[0]->as_string()]];
});
diag(sprintf("started IPv%s%s server running at %s",
$ipv6 ? '6' : '4', $ssl ? ' SSL' : '', $httpd->endpoint()));
return $httpd;
}
my %httpd = (
'4' => {'http' => run_httpd(0, 0), 'https' => run_httpd(0, 1)},
'6' => {'http' => run_httpd(1, 0), 'https' => run_httpd(1, 1)},
);
my @test_cases = (
{ipv6_opt => 0, server_ipv => '4', client_ipv => ''},
{ipv6_opt => 0, server_ipv => '4', client_ipv => '4'},
# IPv* client to a non-SSL IPv6 server is not expected to work unless opt('ipv6') is true
{ipv6_opt => 0, server_ipv => '6', client_ipv => '6'},
# Fetch without ssl
{ server_ipv => '4', client_ipv => '' },
{ server_ipv => '4', client_ipv => '4' },
{ server_ipv => '6', client_ipv => '' },
{ server_ipv => '6', client_ipv => '6' },
# Fetch with ssl
{ ssl => 1, server_ipv => '4', client_ipv => '' },
{ ssl => 1, server_ipv => '4', client_ipv => '4' },
{ ssl => 1, server_ipv => '6', client_ipv => '' },
{ ssl => 1, server_ipv => '6', client_ipv => '6' },
);
for my $tc (@test_cases) {
$tc->{ipv6_opt} //= 0;
$tc->{ssl} //= 0;
SKIP: {
skip("IPv6 not supported on this system", 1)
if $tc->{server_ipv} eq '6' && !$ipv6_supported;
skip("HTTP::Daemon too old for IPv6 support", 1)
if $tc->{server_ipv} eq '6' && !$http_daemon_supports_ipv6;
skip("HTTP::Daemon::SSL not available", 1) if $tc->{ssl} && !$has_http_daemon_ssl;
my $uri = $httpd{$tc->{server_ipv}}{$tc->{ssl} ? 'https' : 'http'}->endpoint();
my $name = sprintf("IPv%s client to %s%s",
$tc->{client_ipv} || '*', $uri, $tc->{ipv6_opt} ? ' (-ipv6)' : '');
$ddclient::globals{'ipv6'} = $tc->{ipv6_opt};
my $got = ddclient::geturl(url => $uri, ipversion => $tc->{client_ipv});
isnt($got // '', '', $name);
}
}
done_testing();

View file

@ -1,27 +0,0 @@
use Test::More;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
# Fake curl. Use the printf utility, which can process escapes. This allows Perl to drive the fake
# curl with plain ASCII and get arbitrary bytes back, avoiding problems caused by any encoding that
# might be done by Perl (e.g., "use open ':encoding(UTF-8)';").
my @fakecurl = ('sh', '-c', 'printf %b "$1"', '--');
my @test_cases = (
{
desc => 'binary body',
# Body is UTF-8 encoded ✨ (U+2728 Sparkles) followed by a 0xff byte (invalid UTF-8).
printf => join('\r\n', ('HTTP/1.1 200 OK', '', '\0342\0234\0250\0377')),
# The raw bytes should come through as equally valued codepoints. They must not be decoded.
want => "HTTP/1.1 200 OK\n\n\xe2\x9c\xa8\xff",
},
);
for my $tc (@test_cases) {
@ddclient::curl = (@fakecurl, $tc->{printf});
$ddclient::curl if 0; # suppress spurious warning "Name used only once: possible typo"
my $got = ddclient::geturl(url => 'http://ignored');
is($got, $tc->{want}, $tc->{desc});
}
done_testing();

View file

@ -1,113 +0,0 @@
use Test::More;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
eval { require Data::Dumper; } or skip($@, 1);
Data::Dumper->import();
my $h1 = 'h1';
my $h2 = 'h2';
my $h3 = 'h3';
$ddclient::config{$h1} = {
common => 'common',
h1h2 => 'h1 and h2',
unique => 'h1',
falsy => 0,
maybeunset => 'unique',
};
$ddclient::config{$h2} = {
common => 'common',
h1h2 => 'h1 and h2',
unique => 'h2',
falsy => '',
maybeunset => undef, # should not be grouped with unset
};
$ddclient::config{$h3} = {
common => 'common',
h1h2 => 'unique',
unique => 'h3',
falsy => undef,
# maybeunset is intentionally not set
};
my @test_cases = (
{
desc => 'empty attribute set yields single group with all hosts',
groupby => [qw()],
want => [{cfg => {}, hosts => [$h1, $h2, $h3]}],
},
{
desc => 'common attribute yields single group with all hosts',
groupby => [qw(common)],
want => [{cfg => {common => 'common'}, hosts => [$h1, $h2, $h3]}],
},
{
desc => 'subset share a value',
groupby => [qw(h1h2)],
want => [
{cfg => {h1h2 => 'h1 and h2'}, hosts => [$h1, $h2]},
{cfg => {h1h2 => 'unique'}, hosts => [$h3]},
],
},
{
desc => 'all unique',
groupby => [qw(unique)],
want => [
{cfg => {unique => 'h1'}, hosts => [$h1]},
{cfg => {unique => 'h2'}, hosts => [$h2]},
{cfg => {unique => 'h3'}, hosts => [$h3]},
],
},
{
desc => 'combination',
groupby => [qw(common h1h2)],
want => [
{cfg => {common => 'common', h1h2 => 'h1 and h2'}, hosts => [$h1, $h2]},
{cfg => {common => 'common', h1h2 => 'unique'}, hosts => [$h3]},
],
},
{
desc => 'falsy values',
groupby => [qw(falsy)],
want => [
{cfg => {falsy => 0}, hosts => [$h1]},
{cfg => {falsy => ''}, hosts => [$h2]},
# undef intentionally becomes unset because undef always means "fall back to global or
# default".
{cfg => {}, hosts => [$h3]},
],
},
{
desc => 'set, unset, undef',
groupby => [qw(maybeunset)],
want => [
{cfg => {maybeunset => 'unique'}, hosts => [$h1]},
# undef intentionally becomes unset because undef always means "fall back to global or
# default".
{cfg => {}, hosts => [$h2, $h3]},
],
},
{
desc => 'missing attribute',
groupby => [qw(thisdoesnotexist)],
want => [{cfg => {}, hosts => [$h1, $h2, $h3]}],
},
);
for my $tc (@test_cases) {
my @got = ddclient::group_hosts_by([$h1, $h2, $h3], @{$tc->{groupby}});
# @got is used as a set of sets. Sort everything to make comparison easier.
$_->{hosts} = [sort(@{$_->{hosts}})] for @got;
@got = sort({
for (my $i = 0; $i < @{$a->{hosts}} && $i < @{$b->{hosts}}; ++$i) {
my $x = $a->{hosts}[$i] cmp $b->{hosts}[$i];
return $x if $x != 0;
}
return @{$a->{hosts}} <=> @{$b->{hosts}};
} @got);
is_deeply(\@got, $tc->{want}, $tc->{desc})
or diag(Data::Dumper->new([\@got, $tc->{want}],
[qw(got want)])->Sortkeys(1)->Useqq(1)->Dump());
}
done_testing();

View file

@ -1,74 +0,0 @@
use Test::More;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
my $have_mock = eval { require Test::MockModule; };
my $failmsg;
my $module;
if ($have_mock) {
$module = Test::MockModule->new('ddclient');
# Note: 'mock' is used instead of 'redefine' because 'redefine' is not available in the versions
# of Test::MockModule distributed with old Debian and Ubuntu releases.
$module->mock('failed', sub { $failmsg //= ''; $failmsg .= sprintf(shift, @_) . "\n"; });
}
my @test_cases = (
{
desc => 'malformed not OK',
input => 'malformed',
want => 0,
wantmsg => qr/unexpected/,
},
{
desc => 'HTTP/1.1 200 OK',
input => 'HTTP/1.1 200 OK',
want => 1,
},
{
desc => 'HTTP/2 200 OK',
input => 'HTTP/2 200 OK',
want => 1,
},
{
desc => 'HTTP/3 200 OK',
input => 'HTTP/3 200 OK',
want => 1,
},
{
desc => '401 not OK, fallback message',
input => 'HTTP/1.1 401 ',
want => 0,
wantmsg => qr/authentication failed/,
},
{
desc => '403 not OK, fallback message',
input => 'HTTP/1.1 403 ',
want => 0,
wantmsg => qr/not authorized/,
},
{
desc => 'other 4xx not OK',
input => 'HTTP/1.1 456 bad',
want => 0,
wantmsg => qr/bad/,
},
{
desc => 'only first line is logged on error',
input => "HTTP/1.1 404 not found\n\nbody",
want => 0,
wantmsg => qr/(?!body)/,
},
);
for my $tc (@test_cases) {
subtest $tc->{desc} => sub {
$failmsg = '';
is(ddclient::header_ok($tc->{input}), $tc->{want}, 'return value matches');
SKIP: {
skip('Test::MockModule not available') if !$have_mock;
like($failmsg, $tc->{wantmsg} // qr/^$/, 'fail message matches');
}
};
}
done_testing();

View file

@ -1,51 +0,0 @@
use Test::More;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
my $h = 't/interval_expired.pl';
my $default_now = 1000000000;
my @test_cases = (
{
interval => 'inf',
want => 0,
},
{
now => 'inf',
interval => 'inf',
want => 0,
},
{
cache => '-inf',
interval => 'inf',
want => 0,
},
{
cache => undef, # Falsy cache value.
interval => 'inf',
want => 0,
},
{
now => 0,
cache => 0, # Different kind of falsy cache value.
interval => 'inf',
want => 0,
},
);
for my $tc (@test_cases) {
$tc->{now} //= $default_now;
# For convenience, $tc->{cache} is an offset from $tc->{now}, not an absolute time..
my $cachetime = $tc->{now} + $tc->{cache} if defined($tc->{cache});
$ddclient::config{$h} = {'interval' => $tc->{interval}};
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
$ddclient::cache{$h} = {'cached-time' => $cachetime} if defined($cachetime);
%ddclient::cache if 0; # suppress spurious warning "Name used only once: possible typo"
$ddclient::now = $tc->{now};
$ddclient::now if 0; # suppress spurious warning "Name used only once: possible typo"
my $desc = "now=$tc->{now}, cache=${\($cachetime // 'undef')}, interval=$tc->{interval}";
is(ddclient::interval_expired($h, 'cached-time', 'interval'), $tc->{want}, $desc);
}
done_testing();

View file

@ -1,11 +1,8 @@
# Copied from https://metacpan.org/release/MASAKI/Test-Fake-HTTPD-0.09/source/lib/Test/Fake/HTTPD.pm
# Copied from https://metacpan.org/pod/release/MASAKI/Test-Fake-HTTPD-0.08/lib/Test/Fake/HTTPD.pm
# and modified as follows:
# * Added this comment block.
# * Patched with https://github.com/masaki/Test-Fake-HTTPD/pull/6 to fix server exit if TLS
# negotiation fails.
# * Patched with https://github.com/masaki/Test-Fake-HTTPD/pull/4 to add IPv6 support.
# * Changed package name to ddclient::Test::Fake::HTTPD.
#
# Copyright: 2011-2020 NAKAGAWA Masaki <masaki@cpan.org>
# License: This library is free software; you can redistribute it and/or modify it under the same
# terms as Perl itself.
@ -23,7 +20,7 @@ use Scalar::Util qw(blessed weaken);
use Carp qw(croak);
use Exporter qw(import);
our $VERSION = '0.09';
our $VERSION = '0.08';
$VERSION = eval $VERSION;
our @EXPORT = qw(
@ -104,10 +101,9 @@ sub run {
$self->port || '<default>',
$@ eq '' ? '' : ": $@")) unless $d;
while (1) {
# accept can return undef if TLS handshake fails (e.g., port test or client rejects
# cert).
my $c = $d->accept or next;
$d->accept; # wait for port check from parent process
while (my $c = $d->accept) {
while (my $req = $c->get_request) {
my $res = $self->_to_http_res($app->($req));
$c->send_response($res);
@ -147,7 +143,7 @@ sub endpoint {
my $self = shift;
my $uri = URI->new($self->scheme . ':');
my $host = $self->host;
$host = 'localhost' if !defined($host) || $host eq '' || $host eq '0.0.0.0' || $host eq '::';
$host = 'localhost' if !defined($host) || $host eq '0.0.0.0' || $host eq '::';
$uri->host($host);
$uri->port($self->port);
return $uri;

View file

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

View file

@ -560,5 +560,3 @@ EOF
want_ipv6_if => "en0",
},
);
1;

View file

@ -1,161 +0,0 @@
package ddclient::t::HTTPD;
use v5.10.1;
use strict;
use warnings;
use parent qw(ddclient::Test::Fake::HTTPD);
use Exporter qw(import);
use Test::More;
BEGIN { require 'ddclient'; }
use ddclient::t::ip;
our @EXPORT = qw(
httpd
httpd_ok httpd_required $httpd_supported $httpd_support_error
httpd_ipv6_ok httpd_ipv6_required $httpd_ipv6_supported $httpd_ipv6_support_error
httpd_ssl_ok httpd_ssl_required $httpd_ssl_supported $httpd_ssl_support_error
$ca_file $certdir $other_ca_file
$textplain
);
our $httpd_supported;
our $httpd_support_error;
BEGIN {
$httpd_supported = eval {
require parent; parent->import(qw(ddclient::Test::Fake::HTTPD));
require JSON::PP; JSON::PP->import();
1;
} or $httpd_support_error = $@;
}
sub httpd_ok {
ok($httpd_supported, "HTTPD is supported") or diag($httpd_support_error);
}
sub httpd_required {
plan(skip_all => $httpd_support_error) if !$httpd_supported;
}
our $httpd_ssl_supported = $httpd_supported;
our $httpd_ssl_support_error = $httpd_support_error;
$httpd_ssl_supported = eval { require HTTP::Daemon::SSL; 1; }
or $httpd_ssl_support_error = $@
if $httpd_ssl_supported;
sub httpd_ssl_ok {
ok($httpd_ssl_supported, "SSL is supported") or diag($httpd_ssl_support_error);
}
sub httpd_ssl_required {
plan(skip_all => $httpd_ssl_support_error) if !$httpd_ssl_supported;
}
our $httpd_ipv6_supported = $httpd_supported;
our $httpd_ipv6_support_error = $httpd_support_error;
$httpd_ipv6_supported = $ipv6_supported
or $httpd_ipv6_support_error = $ipv6_support_error
if $httpd_ipv6_supported;
$httpd_ipv6_supported = eval { require HTTP::Daemon; HTTP::Daemon->VERSION(6.12); }
or $httpd_ipv6_support_error = $@
if $httpd_ipv6_supported;
sub httpd_ipv6_ok {
ok($httpd_ipv6_supported, "test HTTP server supports IPv6") or diag($httpd_ipv6_support_error);
}
sub httpd_ipv6_required {
plan(skip_all => $httpd_ipv6_support_error) if !$httpd_ipv6_supported;
}
our $textplain = ['content-type' => 'text/plain; charset=utf-8'];
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
$self->{_requests} = []; # Log of received requests.
$self->{_responses} = []; # Script of responses to play back.
return $self;
}
sub run {
my ($self, $app) = @_;
$self->SUPER::run(sub {
my ($req) = @_;
push(@{$self->{_requests}}, $req);
my $res = $app->($req) if defined($app);
return $res if defined($res);
if ($req->uri()->path() eq '/control') {
pop(@{$self->{_requests}});
if ($req->method() eq 'PUT') {
return [400, $textplain, ['content must be json']]
if $req->headers()->content_type() ne 'application/json';
eval { @{$self->{_responses}} = @{decode_json($req->content())}; 1; }
or return [400, $textplain, ['content is not valid json']];
@{$self->{_requests}} = ();
return [200, $textplain, ["successfully reset request log and response script"]];
} elsif ($req->method() eq 'GET') {
my @reqs = map($_->as_string(), @{$self->{_requests}});
return [200, ['content-type' => 'application/json'], [encode_json(\@reqs)]];
} else {
return [405, $textplain, ['unsupported method: ' . $req->method()]];
}
}
return shift(@{$self->{_responses}}) // [500, $textplain, ["no more scripted responses"]];
});
diag("started server running at " . $self->endpoint());
return $self;
}
sub reset {
my $self = shift;
my $ep = $self->endpoint();
my $got = ddclient::geturl(url => "$ep/control");
diag("http response:\n$got");
ddclient::header_ok($got)
or BAIL_OUT("failed to get log of requests from test http server at $ep");
$got =~ s/^.*?\n\n//s;
my @got = map(HTTP::Request->parse($_), @{decode_json($got)});
ddclient::header_ok(ddclient::geturl(
url => "$ep/control",
method => 'PUT',
headers => ['content-type: application/json'],
data => encode_json(\@_),
)) or BAIL_OUT("failed to reset the test http server at $ep");
return @got;
}
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;
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})) {
my $host
= $ipv eq '4' ? '127.0.0.1'
: $ipv eq '6' ? '::1'
: $httpd_ipv6_supported ? '::1'
: '127.0.0.1';
$daemons{$ipv}{$ssl} = __PACKAGE__->new(
host => $host,
scheme => $ssl ? 'https' : 'http',
daemon_args => {
(V6Only => $ipv eq '6' ? 1 : 0) x ($host eq '::1'),
(SSL_cert_file => "$certdir/dummy-server-cert.pem",
SSL_key_file => "$certdir/dummy-server-key.pem") x $ssl,
},
);
}
return $daemons{$ipv}{$ssl};
}
1;

View file

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

View file

@ -1,30 +0,0 @@
package ddclient::t::ip;
use v5.10.1;
use strict;
use warnings;
use Exporter qw(import);
use Test::More;
our @EXPORT = qw(ipv6_ok ipv6_required $ipv6_supported $ipv6_support_error);
our $ipv6_support_error;
our $ipv6_supported = eval {
require IO::Socket::IP;
my $ipv6_socket = IO::Socket::IP->new(
Domain => 'PF_INET6',
LocalHost => '::1',
Listen => 1,
);
defined($ipv6_socket);
} or $ipv6_support_error = $@;
sub ipv6_ok {
ok($ipv6_supported, "system supports IPv6") or diag($ipv6_support_error);
}
sub ipv6_required {
plan(skip_all => $ipv6_support_error) if !$ipv6_supported;
}
1;

View file

@ -1,167 +0,0 @@
use Test::More;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
my @test_cases = (
{
desc => 'adds a newline',
args => ['xyz'],
want => "xyz\n",
},
{
desc => 'removes one trailing newline (before adding a newline)',
args => ["xyz \n\t\n\n"],
want => "xyz \n\t\n\n",
},
{
desc => 'accepts msg keyword parameter',
args => [msg => 'xyz'],
want => "xyz\n",
},
{
desc => 'msg keyword parameter trumps message parameter',
args => [msg => 'kw', 'pos'],
want => "kw\n",
},
{
desc => 'msg keyword parameter trumps message parameter',
args => [msg => 'kw', 'pos'],
want => "kw\n",
},
{
desc => 'email appends to email body',
args => [email => 1, 'foo'],
init_email => "preexisting message\n",
want_email => "preexisting message\nfoo\n",
want => "foo\n",
},
{
desc => 'single-line label',
args => [label => 'LBL', 'foo'],
want => "LBL: > foo\n",
},
{
desc => 'multi-line label',
args => [label => 'LBL', "foo\nbar"],
want => ("LBL: > foo\n" .
"LBL: bar\n"),
},
{
desc => 'single-line long label',
args => [label => 'VERY LONG LABEL', 'foo'],
want => "VERY LONG LABEL: > foo\n",
},
{
desc => 'multi-line long label',
args => [label => 'VERY LONG LABEL', "foo\nbar"],
want => ("VERY LONG LABEL: > foo\n" .
"VERY LONG LABEL: bar\n"),
},
{
desc => 'single line, no label, single context',
args => ['foo'],
ctxs => ['only context'],
want => "[only context]> foo\n",
},
{
desc => 'single line, no label, two contexts',
args => ['foo'],
ctxs => ['context one', 'context two'],
want => "[context one][context two]> foo\n",
},
{
desc => 'single line, label, two contexts',
args => [label => 'LBL', 'foo'],
ctxs => ['context one', 'context two'],
want => "LBL: [context one][context two]> foo\n",
},
{
desc => 'multiple lines, label, two contexts',
args => [label => 'LBL', "foo\nbar"],
ctxs => ['context one', 'context two'],
want => ("LBL: [context one][context two]> foo\n" .
"LBL: [context one][context two] bar\n"),
},
{
desc => 'string ctx arg',
args => [label => 'LBL', ctx => 'three', "foo\nbar"],
ctxs => ['one', 'two'],
want => ("LBL: [one][two][three]> foo\n" .
"LBL: [one][two][three] bar\n"),
},
{
desc => 'arrayref ctx arg',
args => [label => 'LBL', ctx => ['three', 'four'], "foo\nbar"],
ctxs => ['one', 'two'],
want => ("LBL: [one][two][three][four]> foo\n" .
"LBL: [one][two][three][four] bar\n"),
},
{
desc => 'undef ctx',
args => [label => 'LBL', "foo"],
ctxs => ['one', undef],
want => "LBL: [one]> foo\n",
},
{
desc => 'arrayref ctx',
args => [label => 'LBL', "foo"],
ctxs => ['one', ['two', 'three']],
want => "LBL: [one][two][three]> foo\n",
},
);
for my $tc (@test_cases) {
subtest $tc->{desc} => sub {
$tc->{wantemail} //= '';
my $output;
open(my $fh, '>', \$output);
local $ddclient::emailbody = $tc->{init_email} // '';
local $ddclient::_l = $ddclient::_l;
$ddclient::_l = ddclient::pushlogctx($_) for @{$tc->{ctxs} // []};
{
local *STDERR = $fh;
ddclient::logmsg(@{$tc->{args}});
}
close($fh);
is($output, $tc->{want}, 'output text matches');
is($ddclient::emailbody, $tc->{want_email} // '', 'email content matches');
}
}
my @logfmt_test_cases = (
{
desc => 'single argument is printed directly, not via sprintf',
args => ['%%'],
want => "DEBUG: > %%\n",
},
{
desc => 'multiple arguments are formatted via sprintf',
args => ['%s', 'foo'],
want => "DEBUG: > foo\n",
},
{
desc => 'single argument with context',
args => [ctx => 'context', '%%'],
want => "DEBUG: [context]> %%\n",
},
{
desc => 'multiple arguments with context',
args => [ctx => 'context', '%s', 'foo'],
want => "DEBUG: [context]> foo\n",
},
);
for my $tc (@logfmt_test_cases) {
my $got;
open(my $fh, '>', \$got);
local $ddclient::globals{debug} = 1;
%ddclient::globals if 0;
{
local *STDERR = $fh;
ddclient::debug(@{$tc->{args}});
}
close($fh);
is($got, $tc->{want}, $tc->{desc});
}
done_testing();

View file

@ -44,20 +44,8 @@ my @test_cases = (
tc('unquoted escaped backslash', "a=\\\\", { a => "\\" }, ""),
tc('squoted escaped squote', "a='\\''", { a => "'" }, ""),
tc('dquoted escaped dquote', "a=\"\\\"\"", { a => '"' }, ""),
tc('env: empty', "a_env=", {}, ""),
tc('env: unset', "a_env=UNSET", {}, ""),
tc('env: set', "a_env=TEST", { a => 'val' }, ""),
tc('env: single quoted', "a_env='TEST'", { a => 'val' }, ""),
tc('newline: quoted value', "a='1\n2'", { a => "1\n2" }, ""),
tc('newline: escaped value', "a=1\\\n2", { a => "1\n2" }, ""),
tc('newline: between vars', "a=1 \n b=2", { a => '1' }, "\n b=2"),
tc('newline: terminating', "a=1 \n", { a => '1' }, "\n"),
);
delete($ENV{''});
delete($ENV{UNSET});
$ENV{TEST} = 'val';
for my $tc (@test_cases) {
my ($got_rest, %got_vars) = ddclient::parse_assignments($tc->{input});
subtest $tc->{name} => sub {

View file

@ -1,169 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
use ddclient::t::Logger;
httpd_required();
ddclient::load_json_support('directnic');
httpd()->run(sub {
my ($req) = @_;
diag('==============================================================================');
diag("Test server received request:\n" . $req->as_string());
my $headers = ['content-type' => 'text/plain; charset=utf-8'];
if ($req->uri->as_string =~ m/\/dns\/gateway\/(abc|def)\/\?data=([^&]*)/) {
return [200, ['Content-Type' => 'application/json'], [encode_json({
result => 'success',
message => "Your record was updated to $2",
})]];
} elsif ($req->uri->as_string =~ m/\/dns\/gateway\/bad_token\/\?data=([^&]*)/) {
return [200, ['Content-Type' => 'application/json'], [encode_json({
result => 'error',
message => "There was an error updating your record.",
})]];
} elsif ($req->uri->as_string =~ m/\/bad\/path\/\?data=([^&]*)/) {
return [200, ['Content-Type' => 'application/json'], ['unexpected response body']];
}
return [400, $headers, ['unexpected request: ' . $req->uri()]]
});
my $hostname = httpd()->endpoint();
my @test_cases = (
{
desc => 'IPv4, good',
cfg => {h1 => {urlv4 => "$hostname/dns/gateway/abc/", wantipv4 => '192.0.2.1'}},
wantrecap => {
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
},
wantlogs => [
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
],
},
{
desc => 'IPv4, failed',
cfg => {h1 => {urlv4 => "$hostname/dns/gateway/bad_token/", wantipv4 => '192.0.2.1'}},
wantrecap => {
h1 => {'status-ipv4' => 'failed'},
},
wantlogs => [
{label => 'FAILED', ctx => ['h1'], msg => qr/There was an error updating your record/},
],
},
{
desc => 'IPv4, bad',
cfg => {h1 => {urlv4 => "$hostname/bad/path/", wantipv4 => '192.0.2.1'}},
wantrecap => {
h1 => {'status-ipv4' => 'bad'},
},
wantlogs => [
{label => 'FAILED', ctx => ['h1'], msg => qr/response is not a JSON object:\nunexpected response body/},
],
},
{
desc => 'IPv4, unexpected response',
cfg => {h1 => {urlv4 => "$hostname/unexpected/path/", wantipv4 => '192.0.2.1'}},
wantrecap => {},
wantlogs => [
{label => 'FAILED', ctx => ['h1'], msg => qr/400 Bad Request/},
],
},
{
desc => 'IPv4, no urlv4',
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
wantrecap => {},
wantlogs => [
{label => 'FAILED', ctx => ['h1'], msg => qr/missing urlv4 option/},
],
},
{
desc => 'IPv6, good',
cfg => {h1 => {urlv6 => "$hostname/dns/gateway/abc/", wantipv6 => '2001:db8::1'}},
wantrecap => {
h1 => {'status-ipv6' => 'good', 'ipv6' => '2001:db8::1', 'mtime' => $ddclient::now},
},
wantlogs => [
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
],
},
{
desc => 'IPv4 and IPv6, good',
cfg => {h1 => {
urlv4 => "$hostname/dns/gateway/abc/",
urlv6 => "$hostname/dns/gateway/def/",
wantipv4 => '192.0.2.1',
wantipv6 => '2001:db8::1',
}},
wantrecap => {
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1',
'status-ipv6' => 'good', 'ipv6' => '2001:db8::1',
'mtime' => $ddclient::now},
},
wantlogs => [
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
],
},
{
desc => 'IPv4 and IPv6, mixed success',
cfg => {h1 => {
urlv4 => "$hostname/dns/gateway/bad_token/",
urlv6 => "$hostname/dns/gateway/def/",
wantipv4 => '192.0.2.1',
wantipv6 => '2001:db8::1',
}},
wantips => {h1 => {wantipv4 => '192.0.2.1', wantipv6 => '2001:db8::1'}},
wantrecap => {
h1 => {'status-ipv4' => 'failed',
'status-ipv6' => 'good', 'ipv6' => '2001:db8::1',
'mtime' => $ddclient::now},
},
wantlogs => [
{label => 'FAILED', ctx => ['h1'], msg => qr/There was an error updating your record/},
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
],
},
);
for my $tc (@test_cases) {
diag('==============================================================================');
diag("Starting test: $tc->{desc}");
diag('==============================================================================');
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
local %ddclient::config = %{$tc->{cfg}};
local %ddclient::recap;
{
local $ddclient::_l = $l;
ddclient::nic_directnic_update(undef, sort(keys(%{$tc->{cfg}})));
}
is_deeply(\%ddclient::recap, $tc->{wantrecap}, "$tc->{desc}: recap")
or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{wantrecap}],
Names => ['*got', '*want']));
$tc->{wantlogs} //= [];
subtest("$tc->{desc}: logs" => sub {
my @got = @{$l->{logs}};
my @want = @{$tc->{wantlogs}};
for my $i (0..$#want) {
last if $i >= @got;
my $got = $got[$i];
my $want = $want[$i];
subtest("log $i" => sub {
is($got->{label}, $want->{label}, "label matches");
is_deeply($got->{ctx}, $want->{ctx}, "context matches");
like($got->{msg}, $want->{msg}, "message matches");
}) or diag(ddclient::repr(Values => [$got, $want], Names => ['*got', '*want']));
}
my @unexpected = @got[@want..$#got];
ok(@unexpected == 0, "no unexpected logs")
or diag(ddclient::repr(\@unexpected, Names => ['*unexpected']));
my @missing = @want[@got..$#want];
ok(@missing == 0, "no missing logs")
or diag(ddclient::repr(\@missing, Names => ['*missing']));
});
}
done_testing();

View file

@ -1,242 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
use ddclient::t::Logger;
httpd_required();
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
ddclient::load_json_support('dnsexit2');
httpd()->run(sub {
my ($req) = @_;
return undef if $req->uri()->path() eq '/control';
return [200, ['Content-Type' => 'application/json'], [encode_json({
code => 0,
message => 'Success'
})]];
});
sub cmp_update {
my ($a, $b) = @_;
return $a->{name} cmp $b->{name} || $a->{type} cmp $b->{type};
}
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 => [
{
apikey => 'key',
domain => 'host1.example.com',
update => [
{
content => '192.0.2.1',
name => '',
ttl => 5,
type => 'A',
},
],
},
{
apikey => 'key',
domain => 'example.com',
update => [
{
content => '2001:db8::1',
name => 'host2',
ttl => 10,
type => 'AAAA',
},
],
},
],
},
{
desc => 'two hosts, same zone',
cfg => {
'host1.example.com' => {
ttl => 5,
wantipv4 => '192.0.2.1',
zone => 'example.com',
},
'host2.example.com' => {
ttl => 10,
wantipv6 => '2001:db8::1',
zone => 'example.com',
},
},
want => [
{
apikey => 'key',
domain => 'example.com',
update => [
{
content => '192.0.2.1',
name => 'host1',
ttl => 5,
type => 'A',
},
{
content => '2001:db8::1',
name => 'host2',
ttl => 10,
type => 'AAAA',
},
],
},
],
},
{
desc => 'host outside of zone',
cfg => {
'host.example' => {
wantipv4 => '192.0.2.1',
zone => 'example.com',
},
},
want_fatal => qr{hostname does not end with the zone: example.com},
},
);
for my $tc (@test_cases) {
subtest($tc->{desc} => sub {
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
local %ddclient::config = ();
my @hosts = keys(%{$tc->{cfg}});
for my $h (@hosts) {
$ddclient::config{$h} = {
password => 'key',
path => '/update',
server => httpd()->endpoint(),
%{$tc->{cfg}{$h}},
};
}
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^FATAL$/);
my $err = do {
local $ddclient::_l = $l;
local $@;
(eval { ddclient::nic_dnsexit2_update(undef, @hosts); 1; })
? undef : ($@ // 'unknown error');
};
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,279 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
use MIME::Base64;
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
use ddclient::t::Logger;
httpd_required();
httpd()->run(sub {
my ($req) = @_;
diag('==============================================================================');
diag("Test server received request:\n" . $req->as_string());
return undef if $req->uri()->path() eq '/control';
my $wantauthn = 'Basic ' . encode_base64('username:password', '');
return [401, [@$textplain, 'www-authenticate' => 'Basic realm="realm", charset="UTF-8"'],
['authentication required']] if ($req->header('authorization') // '') ne $wantauthn;
return [400, $textplain, ['invalid method: ' . $req->method()]] if $req->method() ne 'GET';
return undef;
});
my @test_cases = (
{
desc => 'IPv4, single host, good',
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
resp => ['good'],
wantquery => 'hostname=h1&myip=192.0.2.1',
wantrecap => {
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
},
wantlogs => [
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
],
},
{
desc => 'IPv4, single host, nochg',
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
resp => ['nochg'],
wantquery => 'hostname=h1&myip=192.0.2.1',
wantrecap => {
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
},
wantlogs => [
{label => 'WARNING', ctx => ['h1'], msg => qr/nochg/},
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
],
},
{
desc => 'IPv4, single host, bad',
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
resp => ['nohost'],
wantquery => 'hostname=h1&myip=192.0.2.1',
wantrecap => {
h1 => {'status-ipv4' => 'nohost'},
},
wantlogs => [
{label => 'FAILED', ctx => ['h1'], msg => qr/nohost/},
],
},
{
desc => 'IPv4, single host, unexpected',
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
resp => ['WAT'],
wantquery => 'hostname=h1&myip=192.0.2.1',
wantrecap => {
h1 => {'status-ipv4' => 'WAT'},
},
wantlogs => [
{label => 'FAILED', ctx => ['h1'], msg => qr/unexpected.*WAT/},
],
},
{
desc => 'IPv4, multiple hosts, multiple good',
cfg => {
h1 => {wantipv4 => '192.0.2.1'},
h2 => {wantipv4 => '192.0.2.1'},
},
resp => [
'good 192.0.2.1',
'good',
],
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
wantrecap => {
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
},
wantlogs => [
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
],
},
{
desc => 'IPv4, multiple hosts, mixed success',
cfg => {
h1 => {wantipv4 => '192.0.2.1'},
h2 => {wantipv4 => '192.0.2.1'},
h3 => {wantipv4 => '192.0.2.1'},
},
resp => [
'good',
'nochg',
'dnserr',
],
wantquery => 'hostname=h1,h2,h3&myip=192.0.2.1',
wantrecap => {
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
h3 => {'status-ipv4' => 'dnserr'},
},
wantlogs => [
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
{label => 'WARNING', ctx => ['h2'], msg => qr/nochg/},
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
{label => 'FAILED', ctx => ['h3'], msg => qr/dnserr/},
],
},
{
desc => 'IPv6, single host, good',
cfg => {h1 => {wantipv6 => '2001:db8::1'}},
resp => ['good'],
wantquery => 'hostname=h1&myip=2001:db8::1',
wantrecap => {
h1 => {'status-ipv6' => 'good', 'ipv6' => '2001:db8::1', 'mtime' => $ddclient::now},
},
wantlogs => [
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
],
},
{
desc => 'IPv4 and IPv6, single host, good',
cfg => {h1 => {wantipv4 => '192.0.2.1', wantipv6 => '2001:db8::1'}},
resp => ['good'],
wantquery => 'hostname=h1&myip=192.0.2.1,2001:db8::1',
wantrecap => {
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1',
'status-ipv6' => 'good', 'ipv6' => '2001:db8::1',
'mtime' => $ddclient::now},
},
wantlogs => [
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
],
},
{
desc => 'excess status line',
cfg => {
h1 => {wantipv4 => '192.0.2.1'},
h2 => {wantipv4 => '192.0.2.1'},
},
resp => [
'good',
'good',
'WAT',
],
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
wantrecap => {
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
},
wantlogs => [
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
{label => 'WARNING', ctx => ['h1,h2'], msg => qr/unexpected.*\nWAT$/},
],
},
{
desc => 'multiple hosts, single failure',
cfg => {
h1 => {wantipv4 => '192.0.2.1'},
h2 => {wantipv4 => '192.0.2.1'},
},
resp => ['abuse'],
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
wantrecap => {
h1 => {'status-ipv4' => 'abuse'},
h2 => {'status-ipv4' => 'abuse'},
},
wantlogs => [
{label => 'FAILED', ctx => ['h1'], msg => qr/abuse/},
{label => 'FAILED', ctx => ['h2'], msg => qr/abuse/},
],
},
{
desc => 'multiple hosts, single success',
cfg => {
h1 => {wantipv4 => '192.0.2.1'},
h2 => {wantipv4 => '192.0.2.1'},
},
resp => ['good'],
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
wantrecap => {
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
},
wantlogs => [
{label => 'WARNING', ctx => ['h1,h2'], msg => qr//},
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
],
},
{
desc => 'multiple hosts, fewer results',
cfg => {
h1 => {wantipv4 => '192.0.2.1'},
h2 => {wantipv4 => '192.0.2.1'},
h3 => {wantipv4 => '192.0.2.1'},
},
resp => [
'good',
'nochg',
],
wantquery => 'hostname=h1,h2,h3&myip=192.0.2.1',
wantrecap => {
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
h3 => {'status-ipv4' => 'unknown'},
},
wantlogs => [
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
{label => 'WARNING', ctx => ['h2'], msg => qr/nochg/},
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
{label => 'FAILED', ctx => ['h3'], msg => qr/assuming failure/},
],
},
);
for my $tc (@test_cases) {
diag('==============================================================================');
diag("Starting test: $tc->{desc}");
diag('==============================================================================');
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
local %ddclient::config;
local %ddclient::recap;
$ddclient::config{$_} = {
login => 'username',
password => 'password',
server => httpd()->endpoint(),
script => '/nic/update',
%{$tc->{cfg}{$_}},
} for keys(%{$tc->{cfg}});
httpd()->reset([200, $textplain, [map("$_\n", @{$tc->{resp}})]]);
{
local $ddclient::_l = $l;
ddclient::nic_dyndns2_update(undef, sort(keys(%{$tc->{cfg}})));
}
my @requests = httpd()->reset();
is(scalar(@requests), 1, "$tc->{desc}: single update request");
my $req = shift(@requests);
is($req->uri()->path(), '/nic/update', "$tc->{desc}: request path");
is($req->uri()->query(), $tc->{wantquery}, "$tc->{desc}: request query");
is_deeply(\%ddclient::recap, $tc->{wantrecap}, "$tc->{desc}: recap")
or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{wantrecap}],
Names => ['*got', '*want']));
$tc->{wantlogs} //= [];
subtest("$tc->{desc}: logs" => sub {
my @got = @{$l->{logs}};
my @want = @{$tc->{wantlogs}};
for my $i (0..$#want) {
last if $i >= @got;
my $got = $got[$i];
my $want = $want[$i];
subtest("log $i" => sub {
is($got->{label}, $want->{label}, "label matches");
is_deeply($got->{ctx}, $want->{ctx}, "context matches");
like($got->{msg}, $want->{msg}, "message matches");
}) or diag(ddclient::repr(Values => [$got, $want], Names => ['*got', '*want']));
}
my @unexpected = @got[@want..$#got];
ok(@unexpected == 0, "no unexpected logs")
or diag(ddclient::repr(\@unexpected, Names => ['*unexpected']));
my @missing = @want[@got..$#want];
ok(@missing == 0, "no missing logs")
or diag(ddclient::repr(\@missing, Names => ['*missing']));
});
}
done_testing();

View file

@ -1,107 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
use File::Temp;
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
local %ddclient::protocols = (
protocol_a => ddclient::Protocol->new(
recapvars => {
host => ddclient::T_STRING(),
var_a => ddclient::T_BOOL(),
},
),
protocol_b => ddclient::Protocol->new(
recapvars => {
host => ddclient::T_STRING(),
var_b => ddclient::T_NUMBER(),
},
cfgvars => {
var_b_non_recap => {type => ddclient::T_ANY()},
},
),
);
local %ddclient::cfgvars = (merged => {map({ %{$ddclient::protocols{$_}{cfgvars} // {}}; }
sort(keys(%ddclient::protocols)))});
my @test_cases = (
{
desc => "ok value",
cachefile_lines => ["var_a=yes host_a"],
want => {host_a => {host => 'host_a', var_a => 1}},
},
{
desc => "unknown host",
cachefile_lines => ["var_a=yes host_c"],
want => {},
},
{
desc => "unknown var",
cachefile_lines => ["var_b=123 host_a"],
want => {host_a => {host => 'host_a'}},
},
{
desc => "invalid value",
cachefile_lines => ["var_a=wat host_a"],
want => {host_a => {host => 'host_a'}},
},
{
desc => "multiple entries",
cachefile_lines => [
"var_a=yes host_a",
"var_b=123 host_b",
],
want => {
host_a => {host => 'host_a', var_a => 1},
host_b => {host => 'host_b', var_b => 123},
},
},
{
desc => "non-recap vars are not loaded to %recap",
cachefile_lines => ["var_b_non_recap=foo host_b"],
want => {host_b => {host => 'host_b'}},
},
{
desc => "non-recap vars are scrubbed from %recap",
cachefile_lines => ["var_b_non_recap=foo host_b"],
recap => {host_b => {host => 'host_b', var_b_non_recap => 'foo'}},
want => {host_b => {host => 'host_b'}},
},
{
desc => "unknown hosts are scrubbed from %recap",
cachefile_lines => ["host_a", "host_c"],
recap => {host_a => {host => 'host_a'}, host_c => {host => 'host_c'}},
want => {host_a => {host => 'host_a'}},
},
);
for my $tc (@test_cases) {
my $cachef = File::Temp->new();
print($cachef join('', map("$_\n", "## $ddclient::program-$ddclient::version",
@{$tc->{cachefile_lines}})));
$cachef->close();
local $ddclient::globals{cache} = "$cachef";
local %ddclient::recap = %{$tc->{recap} // {}};
my %want_config = (
host_a => {protocol => 'protocol_a'},
host_b => {protocol => 'protocol_b'},
);
# Deep clone %want_config so we can check for changes.
local %ddclient::config;
$ddclient::config{$_} = {%{$want_config{$_}}} for keys(%want_config);
ddclient::read_recap($cachef->filename());
TODO: {
local $TODO = $tc->{want_TODO};
is_deeply(\%ddclient::recap, $tc->{want}, "$tc->{desc}: %recap")
or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{want}],
Names => ['*got', '*want']));
}
is_deeply(\%ddclient::config, \%want_config, "$tc->{desc}: %config")
or diag(ddclient::repr(Values => [\%ddclient::config, \%want_config],
Names => ['*got', '*want']));
}
done_testing();

150
t/skip.pl
View file

@ -1,150 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
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(
sub { return [200, ['Content-Type' => 'text/plain'], ['::1 skip ::2']]; })
if httpd('6');
my $builtinwebv4 = 't/skip.pl webv4';
my $builtinwebv6 = 't/skip.pl webv6';
my $builtinfw = 't/skip.pl fw';
$ddclient::builtinweb{$builtinwebv4} = {'url' => httpd('4')->endpoint(), 'skip' => 'skip'};
$ddclient::builtinweb{$builtinwebv6} = {'url' => httpd('6')->endpoint(), 'skip' => 'skip'}
if httpd('6');
$ddclient::builtinfw{$builtinfw} = {name => 'test', skip => 'skip'};
%ddclient::builtinfw if 0; # suppress spurious warning "Name used only once: possible typo"
%ddclient::ip_strategies = (%ddclient::ip_strategies, ddclient::builtinfw_strategy($builtinfw));
%ddclient::ipv4_strategies =
(%ddclient::ipv4_strategies, ddclient::builtinfwv4_strategy($builtinfw));
%ddclient::ipv6_strategies =
(%ddclient::ipv6_strategies, ddclient::builtinfwv6_strategy($builtinfw));
sub run_test_case {
my %tc = @_;
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;
my $h = 't/skip.pl';
$ddclient::config{$h} = $tc{cfg};
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
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});
}
}
subtest "use=web web='$builtinwebv4'" => sub {
run_test_case(
desc => "web-skip='' cancels built-in skip",
cfg => {
'use' => 'web',
'web' => $builtinwebv4,
'web-skip' => '',
},
want => '127.0.0.1',
);
run_test_case(
desc => 'web-skip=undef uses built-in skip',
cfg => {
'use' => 'web',
'web' => $builtinwebv4,
'web-skip' => undef,
},
want => '127.0.0.2',
);
};
subtest "usev4=webv4 webv4='$builtinwebv4'" => sub {
run_test_case(
desc => "webv4-skip='' cancels built-in skip",
cfg => {
'usev4' => 'webv4',
'webv4' => $builtinwebv4,
'webv4-skip' => '',
},
want => '127.0.0.1',
);
run_test_case(
desc => 'webv4-skip=undef uses built-in skip',
cfg => {
'usev4' => 'webv4',
'webv4' => $builtinwebv4,
'webv4-skip' => undef,
},
want => '127.0.0.2',
);
};
subtest "usev6=webv6 webv6='$builtinwebv6'" => sub {
run_test_case(
desc => "webv6-skip='' cancels built-in skip",
cfg => {
'usev6' => 'webv6',
'webv6' => $builtinwebv6,
'webv6-skip' => '',
},
ipv6 => 1,
want => '::1',
);
run_test_case(
desc => 'webv6-skip=undef uses built-in skip',
cfg => {
'usev6' => 'webv6',
'webv6' => $builtinwebv6,
'webv6-skip' => undef,
},
ipv6 => 1,
want => '::2',
);
};
subtest "use='$builtinfw'" => sub {
run_test_case(
desc => "fw-skip='' cancels built-in skip",
cfg => {
'fw' => httpd('4')->endpoint(),
'fw-skip' => '',
'use' => $builtinfw,
},
want => '127.0.0.1',
);
run_test_case(
desc => 'fw-skip=undef uses built-in skip',
cfg => {
'fw' => httpd('4')->endpoint(),
'fw-skip' => undef,
'use' => $builtinfw,
},
want => '127.0.0.2',
);
};
subtest "usev4='$builtinfw'" => sub {
run_test_case(
desc => "fwv4-skip='' cancels built-in skip",
cfg => {
'fwv4' => httpd('4')->endpoint(),
'fwv4-skip' => '',
'usev4' => $builtinfw,
},
want => '127.0.0.1',
);
run_test_case(
desc => 'fwv4-skip=undef uses built-in skip',
cfg => {
'fwv4' => httpd('4')->endpoint(),
'fwv4-skip' => undef,
'usev4' => $builtinfw,
},
want => '127.0.0.2',
);
};
done_testing();

View file

@ -1,94 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
use ddclient::t::ip;
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
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);
my $h = 't/ssl-validate.pl';
my %ep = (
'4' => httpd('4', 1)->endpoint(),
'6' => httpd('6', 1) ? httpd('6', 1)->endpoint() : undef,
);
my @test_cases = (
{
desc => 'usev4=webv4 web-ssl-validate=no',
cfg => {'usev4' => 'webv4', 'web-ssl-validate' => 0, 'webv4' => $ep{'4'}},
want => '127.0.0.1',
},
{
desc => 'usev4=webv4 web-ssl-validate=yes',
cfg => {'usev4' => 'webv4', 'web-ssl-validate' => 1, 'webv4' => $ep{'4'}},
want => undef,
},
{
desc => 'usev6=webv6 web-ssl-validate=no',
cfg => {'usev6' => 'webv6', 'web-ssl-validate' => 0, 'webv6' => $ep{'6'}},
ipv6 => 1,
want => '::1',
},
{
desc => 'usev6=webv6 web-ssl-validate=yes',
cfg => {'usev6' => 'webv6', 'web-ssl-validate' => 1, 'webv6' => $ep{'6'}},
ipv6 => 1,
want => undef,
},
{
desc => 'usev4=cisco-asa fw-ssl-validate=no',
cfg => {'usev4' => 'cisco-asa', 'fw-ssl-validate' => 0,
# cisco-asa adds https:// to the URL. :-/
'fwv4' => substr($ep{'4'}, length('https://'))},
want => '127.0.0.1',
},
{
desc => 'usev4=cisco-asa fw-ssl-validate=yes',
cfg => {'usev4' => 'cisco-asa', 'fw-ssl-validate' => 1,
# cisco-asa adds https:// to the URL. :-/
'fwv4' => substr($ep{'4'}, length('https://'))},
want => undef,
},
{
desc => 'usev4=fwv4 fw-ssl-validate=no',
cfg => {'usev4' => 'fwv4', 'fw-ssl-validate' => 0, 'fwv4' => $ep{'4'}},
want => '127.0.0.1',
},
{
desc => 'usev4=fwv4 fw-ssl-validate=yes',
cfg => {'usev4' => 'fwv4', 'fw-ssl-validate' => 1, 'fwv4' => $ep{'4'}},
want => undef,
},
);
for my $tc (@test_cases) {
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
SKIP: {
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{ipv6} && !$httpd_ipv6_supported;
# $ddclient::globals{'ssl_ca_file'} is intentionally NOT set to $ca_file so that we can
# test what happens when certificate validation fails. However, if curl can't find any CA
# certificates (which may be the case in some minimal test environments, such as Docker
# images and Debian package builder chroots), it will immediately close the connection
# after it sends the TLS client hello and before it receives the server hello (in Debian
# sid as of 2025-01-08, anyway). This confuses IO::Socket::SSL (used by
# Test::Fake::HTTPD), causing it to hang in the middle of the TLS handshake waiting for
# input that will never arrive. To work around this, the CA certificate file is explicitly
# set to an unrelated certificate so that curl has something to read.
local $ddclient::globals{'ssl_ca_file'} = $other_ca_file;
local $ddclient::config{$h} = $tc->{cfg};
%ddclient::config 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});
is(ddclient::get_ipv6(ddclient::strategy_inputs('usev6', $h)), $tc->{want}, $tc->{desc})
if ($tc->{cfg}{usev6});
}
}
done_testing();

View file

@ -1,395 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
use File::Temp;
BEGIN { eval { require HTTP::Request; 1; } or plan(skip_all => $@); }
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
use List::Util qw(max);
use Scalar::Util qw(refaddr);
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
use ddclient::t::ip;
httpd_required();
httpd('4')->run();
httpd('6')->run() if httpd('6');
local %ddclient::builtinweb = (
v4 => {url => "" . httpd('4')->endpoint()},
defined(httpd('6')) ? (v6 => {url => "" . httpd('6')->endpoint()}) : (),
);
# Sentinel value used by `mergecfg` that means "this hash entry should be deleted if it exists."
my $DOES_NOT_EXIST = [];
sub mergecfg {
my %ret;
for my $cfg (@_) {
next if !defined($cfg);
for my $h (keys(%$cfg)) {
if (refaddr($cfg->{$h}) == refaddr($DOES_NOT_EXIST)) {
delete($ret{$h});
next;
}
$ret{$h} = {%{$ret{$h} // {}}, %{$cfg->{$h}}};
for my $k (keys(%{$ret{$h}})) {
my $a = refaddr($ret{$h}{$k});
delete($ret{$h}{$k}) if defined($a) && $a == refaddr($DOES_NOT_EXIST);
}
}
}
return \%ret;
}
local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1;
local $ddclient::now = 1000;
our @updates;
local %ddclient::protocols = (
# The `legacy` protocol reads the legacy `wantip` property and sets the legacy `ip` and `status`
# properties. (Modern protocol implementations read `wantipv4` and `wantipv6` and set `ipv4`,
# `ipv6`, `status-ipv4`, and `status-ipv6`.) It always succeeds.
legacy => ddclient::LegacyProtocol->new(
update => sub {
my $self = shift;
ddclient::debug('in update');
push(@updates, [@_]);
for my $h (@_) {
local $ddclient::_l = ddclient::pushlogctx($h);
ddclient::debug('updating host');
$ddclient::recap{$h}{status} = 'good';
$ddclient::recap{$h}{ip} = delete($ddclient::config{$h}{wantip});
$ddclient::recap{$h}{mtime} = $ddclient::now;
}
ddclient::debug('returning from update');
},
),
);
my @test_cases = (
map({
my %cfg = %{delete($_->{cfg})};
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{
desc => "legacy, fresh, $desc",
cfg => {host => {
'protocol' => 'legacy',
%cfg,
}},
want_reqs_webv4 => 1,
want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now,
'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now,
'status-ipv4' => 'good',
}},
%$_,
};
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
{
desc => 'legacy, fresh, use=web (IPv6)',
ipv6 => 1,
cfg => {host => {
'protocol' => 'legacy',
'use' => 'web',
'web' => 'v6',
}},
want_reqs_webv6 => 1,
want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now,
'ipv6' => '2001:db8::1',
'mtime' => $ddclient::now,
'status-ipv6' => 'good',
}},
},
{
desc => 'legacy, fresh, usev6=webv6',
ipv6 => 1,
cfg => {host => {
'protocol' => 'legacy',
'usev6' => 'webv6',
}},
want_reqs_webv6 => 1,
want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now,
'ipv6' => '2001:db8::1',
'mtime' => $ddclient::now,
'status-ipv6' => 'good',
}},
},
{
desc => 'legacy, fresh, usev4=webv4 usev6=webv6',
ipv6 => 1,
cfg => {host => {
'protocol' => 'legacy',
'usev4' => 'webv4',
'usev6' => 'webv6',
}},
want_reqs_webv4 => 1,
want_reqs_webv6 => 1,
want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now,
'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now,
'status-ipv4' => 'good',
}},
},
map({
my %cfg = %{delete($_->{cfg})};
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{
desc => "legacy, no change, not yet time, $desc",
recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-interval'),
'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now - ddclient::opt('min-interval'),
'status-ipv4' => 'good',
}},
cfg => {host => {
'protocol' => 'legacy',
%cfg,
}},
want_reqs_webv4 => 1,
%$_,
};
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
map({
my %cfg = %{delete($_->{cfg})};
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{
desc => "legacy, min-interval elapsed but no change, $desc",
recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-interval') - 1,
'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now - ddclient::opt('min-interval') - 1,
'status-ipv4' => 'good',
}},
cfg => {host => {
'protocol' => 'legacy',
%cfg,
}},
want_reqs_webv4 => 1,
%$_,
};
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
map({
my %cfg = %{delete($_->{cfg})};
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{
desc => "legacy, needs update, not yet time, $desc",
recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-interval'),
'ipv4' => '192.0.2.2',
'mtime' => $ddclient::now - ddclient::opt('min-interval'),
'status-ipv4' => 'good',
}},
cfg => {host => {
'protocol' => 'legacy',
%cfg,
}},
want_reqs_webv4 => 1,
want_recap_changes => {host => {
'warned-min-interval' => $ddclient::now,
}},
%$_,
};
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
map({
my %cfg = %{delete($_->{cfg})};
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{
desc => "legacy, min-interval elapsed, needs update, $desc",
recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-interval') - 1,
'ipv4' => '192.0.2.2',
'mtime' => $ddclient::now - ddclient::opt('min-interval') - 1,
'status-ipv4' => 'good',
}},
cfg => {host => {
'protocol' => 'legacy',
%cfg,
}},
want_reqs_webv4 => 1,
want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now,
'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now,
}},
%$_,
};
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
map({
my %cfg = %{delete($_->{cfg})};
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{
desc => "legacy, previous failed update, not yet time to retry, $desc",
recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-error-interval'),
'ipv4' => '192.0.2.2',
'mtime' => $ddclient::now - max(ddclient::opt('min-error-interval'),
ddclient::opt('min-interval')) - 1,
'status-ipv4' => 'failed',
}},
cfg => {host => {
'protocol' => 'legacy',
%cfg,
}},
want_reqs_webv4 => 1,
want_recap_changes => {host => {
'warned-min-error-interval' => $ddclient::now,
}},
%$_,
};
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
map({
my %cfg = %{delete($_->{cfg})};
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{
desc => "legacy, previous failed update, time to retry, $desc",
recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-error-interval') - 1,
'ipv4' => '192.0.2.2',
'mtime' => $ddclient::now - ddclient::opt('min-error-interval') - 2,
'status-ipv4' => 'failed',
}},
cfg => {host => {
'protocol' => 'legacy',
%cfg,
}},
want_reqs_webv4 => 1,
want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now,
'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now,
'status-ipv4' => 'good',
}},
%$_,
};
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
map({
my %cfg = %{delete($_->{cfg})};
my $desc = join(' ', map("$_=$cfg{$_}", sort(keys(%cfg))));
{
desc => "deduplicates identical IP discovery, $desc",
cfg => {
hosta => {protocol => 'legacy', %cfg},
hostb => {protocol => 'legacy', %cfg},
},
want_reqs_webv4 => 1,
want_updates => [['hosta', 'hostb']],
want_recap_changes => {
hosta => {
'atime' => $ddclient::now,
'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now,
'status-ipv4' => 'good',
},
hostb => {
'atime' => $ddclient::now,
'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now,
'status-ipv4' => 'good',
},
},
%$_,
};
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
{
desc => "deduplicates identical IP discovery, usev6=webv6",
ipv6 => 1,
cfg => {
hosta => {protocol => 'legacy', usev6 => 'webv6'},
hostb => {protocol => 'legacy', usev6 => 'webv6'},
},
want_reqs_webv6 => 1,
want_updates => [['hosta', 'hostb']],
want_recap_changes => {
hosta => {
'atime' => $ddclient::now,
'ipv6' => '2001:db8::1',
'mtime' => $ddclient::now,
'status-ipv6' => 'good',
},
hostb => {
'atime' => $ddclient::now,
'ipv6' => '2001:db8::1',
'mtime' => $ddclient::now,
'status-ipv6' => 'good',
},
},
},
);
for my $tc (@test_cases) {
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;
subtest($tc->{desc} => sub {
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
for my $ipv ('4', '6') {
$tc->{"want_reqs_webv$ipv"} //= 0;
my $want = $tc->{"want_reqs_webv$ipv"};
next if !defined(httpd($ipv)) && $want == 0;
local $ddclient::_l = ddclient::pushlogctx("IPv$ipv");
my $ip = $ipv eq '4' ? '192.0.2.1' : '2001:db8::1';
httpd($ipv)->reset(([200, $textplain, [$ip]]) x $want);
}
$tc->{recap}{$_}{host} //= $_ for keys(%{$tc->{recap} // {}});
# Deep copy `%{$tc->{recap}}` so that updates to `%ddclient::recap` don't mutate it.
local %ddclient::recap = %{mergecfg($tc->{recap})};
my $cachef = File::Temp->new();
# $cachef is an object that stringifies to a filename.
local $ddclient::globals{cache} = "$cachef";
$tc->{cfg} = {map({
($_ => {
host => $_,
web => 'v4',
webv4 => 'v4',
webv6 => 'v6',
%{$tc->{cfg}{$_}},
});
} keys(%{$tc->{cfg} // {}}))};
# Deep copy `%{$tc->{cfg}}` so that updates to `%ddclient::config` don't mutate it.
local %ddclient::config = %{mergecfg($tc->{cfg})};
local @updates;
ddclient::update_nics();
for my $ipv ('4', '6') {
next if !defined(httpd($ipv));
local $ddclient::_l = ddclient::pushlogctx("IPv$ipv");
my @gotreqs = httpd($ipv)->reset();
my $got = @gotreqs;
my $want = $tc->{"want_reqs_webv$ipv"};
is($got, $want, "number of requests to webv$ipv service");
}
TODO: {
local $TODO = $tc->{want_updates_TODO};
is_deeply(\@updates, $tc->{want_updates} // [], 'got expected updates')
or diag(ddclient::repr(Values => [\@updates, $tc->{want_updates}],
Names => ['*got', '*want']));
}
my %want_recap = %{mergecfg($tc->{recap}, $tc->{want_recap_changes})};
TODO: {
local $TODO = $tc->{want_recap_changes_TODO};
is_deeply(\%ddclient::recap, \%want_recap, 'recap matches')
or diag(ddclient::repr(Values => [\%ddclient::recap, \%want_recap],
Names => ['*got', '*want']));
}
my %want_cfg = %{mergecfg($tc->{cfg}, $tc->{want_cfg_changes})};
TODO: {
local $TODO = $tc->{want_cfg_changes_TODO};
is_deeply(\%ddclient::config, \%want_cfg, 'config matches')
or diag(ddclient::repr(Values => [\%ddclient::config, \%want_cfg],
Names => ['*got', '*want']));
}
});
}
}
done_testing();

View file

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

View file

@ -1,87 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t::HTTPD;
use ddclient::t::ip;
httpd_required();
my $builtinweb = 't/use_web.pl builtinweb';
my $h = 't/use_web.pl hostname';
my $headers = [
@$textplain,
'this-ipv4-should-be-ignored' => 'skip skip2 192.0.2.255',
'this-ipv6-should-be-ignored' => 'skip skip2 2001:db8::ff',
];
httpd('4')->run(sub { return [200, $headers, ['192.0.2.1 skip 192.0.2.2 skip2 192.0.2.3']]; });
httpd('6')->run(sub { return [200, $headers, ['2001:db8::1 skip 2001:db8::2 skip2 2001:db8::3']]; })
if httpd('6');
my %ep = (
'4' => httpd('4')->endpoint(),
'6' => httpd('6') ? httpd('6')->endpoint() : undef,
);
my @test_cases;
for my $ipv ('4', '6') {
my $ipv4 = $ipv eq '4';
for my $sfx ('', "v$ipv") {
push(
@test_cases,
{
desc => "use$sfx=web$sfx web$sfx=<url> IPv$ipv",
ipv6 => !$ipv4,
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $ep{$ipv}},
want => $ipv4 ? '192.0.2.1' : '2001:db8::1',
},
{
desc => "use$sfx=web$sfx web$sfx=<url> web$sfx-skip=skip IPv$ipv",
ipv6 => !$ipv4,
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $ep{$ipv}, "web$sfx-skip" => 'skip'},
# Note that "skip" should skip past the first "skip" and not past "skip2".
want => $ipv4 ? '192.0.2.2' : '2001:db8::2',
},
{
desc => "use$sfx=web$sfx web$sfx=<builtinweb> IPv$ipv",
ipv6 => !$ipv4,
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $builtinweb},
biw => {url => $ep{$ipv}},
want => $ipv4 ? '192.0.2.1' : '2001:db8::1',
},
{
desc => "use$sfx=web$sfx web$sfx=<builtinweb w/skip> IPv$ipv",
ipv6 => !$ipv4,
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $builtinweb},
biw => {url => $ep{$ipv}, skip => 'skip'},
# Note that "skip" should skip past the first "skip" and not past "skip2".
want => $ipv4 ? '192.0.2.2' : '2001:db8::2',
},
{
desc => "use$sfx=web$sfx web$sfx=<builtinweb w/skip> web$sfx-skip=skip2 IPv$ipv",
ipv6 => !$ipv4,
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $builtinweb, "web$sfx-skip" => 'skip2'},
biw => {url => $ep{$ipv}, skip => 'skip'},
want => $ipv4 ? '192.0.2.3' : '2001:db8::3',
},
);
}
}
for my $tc (@test_cases) {
local $ddclient::builtinweb{$builtinweb} = $tc->{biw};
$ddclient::builtinweb if 0;
local $ddclient::config{$h} = $tc->{cfg};
$ddclient::config if 0;
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;
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,100 +0,0 @@
use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use re qw(is_regexp);
my %variable_collections = (
map({ ($_ => $ddclient::cfgvars{$_}) } grep($_ ne 'merged', keys(%ddclient::cfgvars))),
map({ ("protocol=$_" => $ddclient::protocols{$_}{cfgvars}); } keys(%ddclient::protocols)),
);
my %seen;
my @test_cases = (
map({
my $vcn = $_;
my $vc = $variable_collections{$_};
map({
my $def = $vc->{$_};
my $seen = exists($seen{$def});
$seen{$def} = undef;
({desc => "$vcn $_", def => $vc->{$_}}) x !$seen;
} sort(keys(%$vc)));
} sort(keys(%variable_collections))),
);
for my $tc (@test_cases) {
if ($tc->{def}{required}) {
is($tc->{def}{default}, undef, "'$tc->{desc}' (required) has no default");
} else {
# Preserve all existing variables in $cfgvars{merged} so that variables with dynamic
# defaults can reference them.
local %ddclient::cfgvars = (merged => {
%{$ddclient::cfgvars{merged}},
'var for test' => $tc->{def},
});
# Variables with dynamic defaults will need their own unit tests, but we can still check the
# clean-slate hostless default.
local %ddclient::config;
local %ddclient::opt;
local %ddclient::globals;
my $norm;
my $default = ddclient::default('var for test');
diag("'$tc->{desc}' default: " . ($default // '<undefined>'));
is($default, $tc->{def}{default}, "'$tc->{desc}' default() return value matches default")
if ref($tc->{def}{default}) ne 'CODE';
my $valid = eval { $norm = ddclient::check_value($default, $tc->{def}); 1; } or diag($@);
ok($valid, "'$tc->{desc}' (optional) has a valid default");
is($norm, $default, "'$tc->{desc}' default normalizes to itself") if $valid;
}
}
my @use_test_cases = (
{
desc => 'clean slate hostless default',
want => 'ip',
},
{
desc => 'usage string',
host => '<usage>',
want => qr/disabled.*ip|ip.*disabled/,
},
{
desc => 'usev4 disables use by default',
host => 'host',
cfg => {usev4 => 'webv4'},
want => 'disabled',
},
{
desc => 'usev6 disables use by default',
host => 'host',
cfg => {usev4 => 'webv4'},
want => 'disabled',
},
{
desc => 'explicitly setting use re-enables it',
host => 'host',
cfg => {use => 'web', usev4 => 'webv4'},
want => 'web',
},
);
for my $tc (@use_test_cases) {
my $desc = "'use' dynamic default: $tc->{desc}";
local %ddclient::protocols = (protocol => ddclient::Protocol->new());
local %ddclient::cfgvars = (merged => {
'protocol' => $ddclient::cfgvars{'merged'}{'protocol'},
'use' => $ddclient::cfgvars{'protocol-common-defaults'}{'use'},
'usev4' => $ddclient::cfgvars{'merged'}{'usev4'},
'usev6' => $ddclient::cfgvars{'merged'}{'usev6'},
});
local %ddclient::config = (host => {protocol => 'protocol', %{$tc->{cfg} // {}}});
local %ddclient::opt;
local %ddclient::globals;
my $got = ddclient::opt('use', $tc->{host});
if (is_regexp($tc->{want})) {
like($got, $tc->{want}, $desc);
} else {
is($got, $tc->{want}, $desc);
}
}
done_testing();

View file

@ -4,59 +4,6 @@ use version;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
ok(ddclient::parse_version($ddclient::VERSION),
"module's Perl version string is in opinionated form");
my $n = qr/0|[1-9]\d{0,2}/;
like($ddclient::version, qr/^$n\.$n\.$n(?:-alpha|-beta\.$n|-rc\.$n|\+r\.$n)?$/,
"human-readable version is in opinionated form");
my @tcs = (
['v1.0_0', '1-alpha'],
['v1.0.0_0', '1.0-alpha'],
['v1.2.3.0_0', '1.2.3-alpha'],
['v1.2.3.4.0_0', '1.2.3.4-alpha'],
['v1.0_1', '1-beta.1'],
['v1.0.0_1', '1.0-beta.1'],
['v1.2.3.0_1', '1.2.3-beta.1'],
['v1.2.3.4.0_1', '1.2.3.4-beta.1'],
['v1.2.3.0_899', '1.2.3-beta.899'],
['v1.0_901', '1-rc.1'],
['v1.0.0_901', '1.0-rc.1'],
['v1.2.3.0_901', '1.2.3-rc.1'],
['v1.2.3.4.0_901', '1.2.3.4-rc.1'],
['v1.2.3.0_998', '1.2.3-rc.98'],
['v1.999', '1'],
['v1.0.999', '1.0'],
['v1.2.3.999', '1.2.3'],
['v1.2.3.4.999', '1.2.3.4'],
['v1.999.1', '1+r.1'],
['v1.0.999.1', '1.0+r.1'],
['v1.2.3.999.1', '1.2.3+r.1'],
['v1.2.3.4.999.1', '1.2.3.4+r.1'],
['v1.2.3.999.999', '1.2.3+r.999'],
[$ddclient::VERSION, $ddclient::version],
);
subtest 'humanize_version' => sub {
for my $tc (@tcs) {
my ($pv, $want) = @$tc;
is(ddclient::humanize_version($pv), $want, "$pv -> $want");
}
};
subtest 'human-readable version can be translated back to Perl version' => sub {
for my $tc (@tcs) {
my ($want, $hv) = @$tc;
my $pv = "v$hv";
$pv =~ s/^(?!.*-)(.*?)(?:\+r\.(\d+))?$/"$1.999" . (defined($2) ? ".$2" : "")/e;
$pv =~ s/-alpha$/.0_0/;
$pv =~ s/-beta\.(\d+)$/.0_$1/;
$pv =~ s/-rc\.(\d+)$/'.0_' . (900 + $1)/e;
is($pv, $want, "$hv -> $want");
}
};
is($ddclient::version, '@PACKAGE_VERSION@', "version matches version in Autoconf");
is(ddclient->VERSION(), version->parse('v@PACKAGE_VERSION@'), "version matches Autoconf config");
done_testing();

View file

@ -35,7 +35,7 @@ my @test_cases = (
for my $tc (@test_cases) {
$warning = undef;
ddclient::write_recap($tc->{f});
ddclient::write_cache($tc->{f});
subtest $tc->{name} => sub {
if (defined($tc->{warning_regex})) {
like($warning, $tc->{warning_regex}, "expected warning message");