Compare commits
No commits in common. "main" and "v3.11.2" have entirely different histories.
48 changed files with 4696 additions and 6949 deletions
|
@ -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"
|
92
.github/workflows/ci.yml
vendored
92
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
49
.github/workflows/pr.yml
vendored
49
.github/workflows/pr.yml
vendored
|
@ -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
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
207
ChangeLog.md
207
ChangeLog.md
|
@ -1,210 +1,7 @@
|
|||
# ChangeLog
|
||||
|
||||
This document describes notable changes. For details, see the [source code
|
||||
repository history](https://github.com/ddclient/ddclient/commits/main).
|
||||
|
||||
## v4.0.1-alpha (unreleased work-in-progress)
|
||||
|
||||
## 2025-01-19 v4.0.0
|
||||
|
||||
### 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)
|
||||
repository history](https://github.com/ddclient/ddclient/commits/master).
|
||||
|
||||
## 2023-11-23 v3.11.2
|
||||
|
||||
|
@ -268,7 +65,7 @@ Refer to [v3.11 release plan discussions](https://github.com/ddclient/ddclient/i
|
|||
|
||||
* Added support for domaindiscount24.com
|
||||
* Added support for njal.la
|
||||
|
||||
|
||||
## 2022-05-15 v3.10.0_2
|
||||
|
||||
### Bug fixes
|
||||
|
|
52
Makefile.am
52
Makefile.am
|
@ -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
|
||||
|
|
207
README.md
207
README.md
|
@ -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
|
||||
|
||||
|
@ -43,44 +15,41 @@ your dynamic DNS provider(s): <https://github.com/troglobit/inadyn> or
|
|||
|
||||
Dynamic DNS services currently supported include:
|
||||
|
||||
* [1984.is](https://www.1984.is/product/freedns)
|
||||
* [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)
|
||||
* [DslReports](https://www.dslreports.com)
|
||||
* [Duck DNS](https://duckdns.org)
|
||||
* [DynDNS.com](https://account.dyn.com)
|
||||
* [EasyDNS](https://www.easydns.com )
|
||||
* [Enom](https://www.enom.com)
|
||||
* [Freedns](https://freedns.afraid.org)
|
||||
* [Freemyip](https://freemyip.com)
|
||||
* [Gandi](https://gandi.net)
|
||||
* [GoDaddy](https://www.godaddy.com)
|
||||
* [Hurricane Electric](https://dns.he.net)
|
||||
* [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)
|
||||
* [NearlyFreeSpeech.net](https://www.nearlyfreespeech.net/services/dns)
|
||||
* [Njalla](https://njal.la/docs/ddns)
|
||||
* [Noip](https://www.noip.com)
|
||||
* nsupdate - see nsupdate(1) and ddns-confgen(8)
|
||||
* [OVH](https://www.ovhcloud.com)
|
||||
* [Porkbun](https://porkbun.com)
|
||||
* [regfish.de](https://www.regfish.de/domains/dyndns)
|
||||
* [Sitelutions](https://www.sitelutions.com)
|
||||
* [Yandex](https://dns.yandex.com)
|
||||
* [Zoneedit](https://www.zoneedit.com)
|
||||
* [1984.is](https://www.1984.is/product/freedns)
|
||||
* [ChangeIP](https://www.changeip.com)
|
||||
* [CloudFlare](https://www.cloudflare.com)
|
||||
* [ClouDNS](https://www.cloudns.net)
|
||||
* [DigitalOcean](https://www.digitalocean.com/)
|
||||
* [dinahosting](https://dinahosting.com)
|
||||
* [DonDominio](https://www.dondominio.com)
|
||||
* [DNS Made Easy](https://dnsmadeeasy.com)
|
||||
* [DNSExit](https://dnsexit.com/dns/dns-api)
|
||||
* [Domeneshop](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)
|
||||
* [EasyDNS](https://www.easydns.com )
|
||||
* [Enom](https://www.enom.com)
|
||||
* [Freedns](https://freedns.afraid.org)
|
||||
* [Freemyip](https://freemyip.com)
|
||||
* [Gandi](https://gandi.net)
|
||||
* [GoDaddy](https://www.godaddy.com)
|
||||
* [Google](https://domains.google)
|
||||
* [Infomaniak](https://faq.infomaniak.com/2376)
|
||||
* [Loopia](https://www.loopia.se)
|
||||
* [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
|
||||
* [NameCheap](https://www.namecheap.com)
|
||||
* [NearlyFreeSpeech.net](https://www.nearlyfreespeech.net/services/dns)
|
||||
* [Njalla](https://njal.la/docs/ddns)
|
||||
* [Noip](https://www.noip.com)
|
||||
* nsupdate - see nsupdate(1) and ddns-confgen(8)
|
||||
* [OVH](https://www.ovhcloud.com)
|
||||
* [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)
|
||||
|
||||
`ddclient` supports finding your IP address from many cable and DSL
|
||||
broadband routers.
|
||||
|
@ -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.
|
||||
For example, the Linksys routers could have been added with:
|
||||
|
||||
ddclient does something like this to provide builtin support for common
|
||||
routers.
|
||||
For example, the Linksys routers could have been added with:
|
||||
fw=192.168.1.1/Status.htm
|
||||
fw-skip=WAN.*?IP Address
|
||||
|
||||
```
|
||||
usev4=fwv4
|
||||
fwv4=192.168.1.1/Status.htm
|
||||
fwv4-skip=WAN.*?IP Address
|
||||
```
|
||||
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!
|
||||
|
||||
OR [create a new issue](https://github.com/ddclient/ddclient/issues/new)
|
||||
containing the output from:
|
||||
ie. for my fw/router I used: ``$ ddclient -geturl 192.168.1.254/status.htm``
|
||||
|
||||
```
|
||||
curl --include --location http://url.of.your.firewall/ip-status-page
|
||||
```
|
||||
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
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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
26
autogen
|
@ -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
651
build-aux/tap-driver.sh
Executable 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"]="[0;31m" # Red.
|
||||
color_map["grn"]="[0;32m" # Green.
|
||||
color_map["lgn"]="[1;32m" # Light green.
|
||||
color_map["blu"]="[1;34m" # Blue.
|
||||
color_map["mgn"]="[0;35m" # Magenta.
|
||||
color_map["std"]="[m" # 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:
|
49
configure.ac
49
configure.ac
|
@ -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.2])
|
||||
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
|
||||
|
|
279
ddclient.conf.in
279
ddclient.conf.in
|
@ -1,12 +1,12 @@
|
|||
######################################################################
|
||||
##
|
||||
## Define default global variables with lines like:
|
||||
## var=value [, var=value]*
|
||||
## var=value [, var=value]*
|
||||
## These values will be used for each following host unless overridden
|
||||
## with a local variable definition.
|
||||
##
|
||||
## Define local variables for one or more hosts with:
|
||||
## var=value [, var=value]* host.and.domain[,host2.and.domain...]
|
||||
## var=value [, var=value]* host.and.domain[,host2.and.domain...]
|
||||
##
|
||||
## Lines can be continued on the following line by ending the line
|
||||
## with a \
|
||||
|
@ -16,47 +16,37 @@
|
|||
## 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.
|
||||
daemon=300 # check every 300 seconds
|
||||
syslog=yes # log update msgs to syslog
|
||||
mail=root # mail all msgs to root
|
||||
mail-failure=root # mail failed update msgs to root
|
||||
pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||
ssl=yes # use ssl-support. Works with
|
||||
# ssl-library
|
||||
# postscript=script # run script after updating. The
|
||||
# new IP is added as argument.
|
||||
#
|
||||
#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
|
||||
#use=smc-barricade, fw=192.168.123.254:80 # via SMC's Barricade FW
|
||||
#use=netgear-rt3xx, fw=192.168.0.1:80 # via Netgear's internet FW
|
||||
#use=linksys, fw=192.168.1.1:80 # via Linksys's internet FW
|
||||
#use=maxgate-ugate3x00, fw=192.168.0.1:80 # via MaxGate's UGATE-3x00 FW
|
||||
#use=elsa-lancom-dsl10, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
||||
#use=elsa-lancom-dsl10-ch01, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
||||
#use=elsa-lancom-dsl10-ch02, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
||||
#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
|
||||
#use=smc-barricade, fw=192.168.123.254:80 # via SMC's Barricade FW
|
||||
#use=netgear-rt3xx, fw=192.168.0.1:80 # via Netgear's internet FW
|
||||
#use=linksys, fw=192.168.1.1:80 # via Linksys's internet FW
|
||||
#use=maxgate-ugate3x00, fw=192.168.0.1:80 # via MaxGate's UGATE-3x00 FW
|
||||
#use=elsa-lancom-dsl10, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
||||
#use=elsa-lancom-dsl10-ch01, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
||||
#use=elsa-lancom-dsl10-ch02, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
||||
#use=alcatel-stp, fw=10.0.0.138:80 # via Alcatel Speed Touch Pro
|
||||
#use=xsense-aero, fw=192.168.1.1:80 # via Xsense Aero Router
|
||||
#use=allnet-1298, fw=192.168.1.1:80 # via AllNet 1298 DSL Router
|
||||
#use=3com-oc-remote812, fw=192.168.0.254:80 # via 3com OfficeConnect Remote 812
|
||||
#use=3com-oc-remote812, fw=192.168.0.254:80 # via 3com OfficeConnect Remote 812
|
||||
#use=e-tech, fw=192.168.1.1:80 # via E-tech Router
|
||||
#use=cayman-3220h, fw=192.168.0.1:1080 # via Cayman 3220-H DSL Router
|
||||
#
|
||||
#fw-login=admin, fw-password=XXXXXX # FW login and password
|
||||
#fw-login=admin, fw-password=XXXXXX # FW login and password
|
||||
#
|
||||
## To obtain an IP address from FW status page (using fw-login, fw-password)
|
||||
#use=fw, fw=192.168.1.254/status.htm, fw-skip='IP Address' # found after IP Address
|
||||
#
|
||||
## 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.
|
||||
|
@ -64,55 +54,75 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
|||
## get banned from their service.
|
||||
#use=web, web=checkip.dyndns.org/, web-skip='IP Address' # found after IP Address
|
||||
#
|
||||
#use=ip, ip=127.0.0.1 # via static IP's
|
||||
#use=if, if=eth0 # via interfaces
|
||||
#use=web # via web
|
||||
#use=ip, ip=127.0.0.1 # via static IP's
|
||||
#use=if, if=eth0 # via interfaces
|
||||
#use=web # via web
|
||||
#
|
||||
#protocol=dyndns2 # default protocol
|
||||
#proxy=fasthttp.sympatico.ca:80 # default proxy
|
||||
#server=members.dyndns.org # default server
|
||||
#server=members.dyndns.org:8245 # default server (bypassing proxies)
|
||||
#protocol=dyndns2 # default protocol
|
||||
#proxy=fasthttp.sympatico.ca:80 # default proxy
|
||||
#server=members.dyndns.org # default server
|
||||
#server=members.dyndns.org:8245 # default server (bypassing proxies)
|
||||
|
||||
#login=your-login # default login
|
||||
#password=test # default password
|
||||
#mx=mx.for.your.host # default MX
|
||||
#backupmx=yes|no # host is primary MX?
|
||||
#wildcard=yes|no # add wildcard CNAME?
|
||||
#login=your-login # default login
|
||||
#password=test # default password
|
||||
#mx=mx.for.your.host # default MX
|
||||
#backupmx=yes|no # host is primary MX?
|
||||
#wildcard=yes|no # add wildcard CNAME?
|
||||
|
||||
##
|
||||
## dyndns.org dynamic addresses
|
||||
##
|
||||
## (supports variables: wildcard,mx,backupmx)
|
||||
##
|
||||
# server=members.dyndns.org, \
|
||||
# protocol=dyndns2 \
|
||||
# server=members.dyndns.org, \
|
||||
# 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)
|
||||
##
|
||||
# server=dynamic.zoneedit.com, \
|
||||
# protocol=zoneedit1, \
|
||||
# login=your-zoneedit-login, \
|
||||
# password=your-zoneedit-password \
|
||||
# server=dynamic.zoneedit.com, \
|
||||
# protocol=zoneedit1, \
|
||||
# login=your-zoneedit-login, \
|
||||
# password=your-zoneedit-password \
|
||||
# your.any.domain,your-2nd.any.dom
|
||||
|
||||
##
|
||||
## EasyDNS (easydns.com)
|
||||
##
|
||||
# server=members.easydns.com, \
|
||||
# protocol=easydns, \
|
||||
# login=your-easydns-login, \
|
||||
# password=your-easydns-password \
|
||||
# server=members.easydns.com, \
|
||||
# protocol=easydns, \
|
||||
# login=your-easydns-login, \
|
||||
# password=your-easydns-password \
|
||||
# your.any.domain,your-2nd.any.domain
|
||||
|
||||
##
|
||||
## dslreports.com dynamic-host monitoring
|
||||
##
|
||||
# server=members.dslreports.com \
|
||||
# protocol=dslreports1, \
|
||||
# login=dslreports-login, \
|
||||
# password=dslreports-password \
|
||||
# server=members.dslreports.com \
|
||||
# protocol=dslreports1, \
|
||||
# login=dslreports-login, \
|
||||
# password=dslreports-password \
|
||||
# dslreports-unique-id
|
||||
|
||||
##
|
||||
|
@ -120,16 +130,16 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
|||
##
|
||||
# use=web, web=members.orgdns.org/nic/ip
|
||||
# protocol=dyndns2
|
||||
# server=www.orgdns.org \
|
||||
# login=yourLoginName \
|
||||
# password=yourPassword \
|
||||
# server=www.orgdns.org \
|
||||
# login=yourLoginName \
|
||||
# password=yourPassword \
|
||||
# yourSubdomain.orgdns.org
|
||||
|
||||
##
|
||||
## NameCheap (namecheap.com)
|
||||
##
|
||||
# protocol=namecheap, \
|
||||
# server=dynamicdns.park-your-domain.com, \
|
||||
# server=dynamicdns.park-your-domain.com, \
|
||||
# login=example.com, \
|
||||
# password=example.com-password \
|
||||
# subdomain.example.com
|
||||
|
@ -137,10 +147,10 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
|||
##
|
||||
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
|
||||
##
|
||||
# protocol=nfsn, \
|
||||
# zone=example.com, \
|
||||
# protocol = nfsn, \
|
||||
# login=member-login, \
|
||||
# password=api-key \
|
||||
# password=api-key, \
|
||||
# zone=example.com \
|
||||
# example.com,subdomain.example.com
|
||||
|
||||
##
|
||||
|
@ -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
|
||||
|
||||
##
|
||||
|
@ -176,40 +186,30 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
|||
##
|
||||
## CloudFlare (www.cloudflare.com)
|
||||
##
|
||||
# protocol=cloudflare, \
|
||||
# zone=domain.tld, \
|
||||
# ttl=1, \
|
||||
# login=your-login-email, \ # Only needed if you are using your global API key. If you are using an API token, set it to "token" (without double quotes).
|
||||
# password=APIKey \ # This is either your global API key, or an API token. If you are using an API token, it must have the permissions "Zone - DNS - Edit" and "Zone - Zone - Read". The Zone resources must be "Include - All zones".
|
||||
# domain.tld,my.domain.tld
|
||||
#protocol=cloudflare, \
|
||||
#zone=domain.tld, \
|
||||
#ttl=1, \
|
||||
#login=your-login-email, \ # Only needed if you are using your global API key. If you are using an API token, set it to "token" (without double quotes).
|
||||
#password=APIKey \ # This is either your global API key, or an API token. If you are using an API token, it must have the permissions "Zone - DNS - Edit" and "Zone - Zone - Read". The Zone resources must be "Include - All zones".
|
||||
#domain.tld,my.domain.tld
|
||||
|
||||
##
|
||||
## 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)
|
||||
##
|
||||
|
@ -251,23 +243,23 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
|||
##
|
||||
## nsupdate.info IPV4(https://www.nsupdate.info)
|
||||
##
|
||||
# use=web, web=http://ipv4.nsupdate.info/myip
|
||||
# protocol=dyndns2
|
||||
# server=ipv4.nsupdate.info
|
||||
# login=domain.nsupdate.info
|
||||
# password='123'
|
||||
# domain.nsupdate.info
|
||||
#use=web, web=http://ipv4.nsupdate.info/myip
|
||||
#protocol=dyndns2
|
||||
#server=ipv4.nsupdate.info
|
||||
#login=domain.nsupdate.info
|
||||
#password='123'
|
||||
#domain.nsupdate.info
|
||||
|
||||
##
|
||||
## nsupdate.info IPV6 (https://www.nsupdate.info)
|
||||
## ddclient releases <= 3.8.1 do not support IPv6
|
||||
##
|
||||
# usev6=if, if=eth0
|
||||
# protocol=dyndns2
|
||||
# server=ipv6.nsupdate.info
|
||||
# login=domain.nsupdate.info
|
||||
# password='123'
|
||||
# domain.nsupdate.info
|
||||
#usev6=if, if=eth0
|
||||
#protocol=dyndns2
|
||||
#server=ipv6.nsupdate.info
|
||||
#login=domain.nsupdate.info
|
||||
#password='123'
|
||||
#domain.nsupdate.info
|
||||
|
||||
##
|
||||
## Yandex.Mail for Domain (domain.yandex.com)
|
||||
|
@ -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)
|
||||
|
@ -321,17 +312,17 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
|||
##
|
||||
## dnsexit (www.dnsexit.com)
|
||||
##
|
||||
# protocol=dnsexit, \
|
||||
# login=myusername, \
|
||||
# password=mypassword, \
|
||||
# subdomain-1.domain.com,subdomain-2.domain.com
|
||||
#protocol=dnsexit, \
|
||||
#login=myusername, \
|
||||
#password=mypassword, \
|
||||
#subdomain-1.domain.com,subdomain-2.domain.com
|
||||
|
||||
##
|
||||
## dnsexit2 (API method www.dnsexit.com)
|
||||
##
|
||||
# protocol=dnsexit2
|
||||
# password=MyAPIKey
|
||||
# subdomain-1.domain.com,subdomain-2.domain.com
|
||||
#protocol=dnsexit2
|
||||
#password=MyAPIKey
|
||||
#subdomain-1.domain.com,subdomain-2.domain.com
|
||||
|
||||
##
|
||||
## domeneshop (www.domeneshop.no)
|
||||
|
@ -367,18 +358,10 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
|||
##
|
||||
## DigitalOcean (www.digitalocean.com)
|
||||
##
|
||||
# protocol=digitalocean, \
|
||||
# zone=example.com, \
|
||||
# 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
|
||||
#protocol=digitalocean, \
|
||||
#zone=example.com, \
|
||||
#password=api-token \
|
||||
#example.com,sub.example.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
|
||||
|
|
7074
ddclient.in
7074
ddclient.in
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
|
@ -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();
|
|
@ -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;
|
||||
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;
|
||||
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");
|
||||
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;
|
||||
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;
|
||||
$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");
|
||||
ok(ddclient::is_ipv6($ip1), "Valid IPv6 from get_ip_from_interface($interface)");
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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();
|
93
t/geturl_connectivity.pl.in
Normal file
93
t/geturl_connectivity.pl.in
Normal 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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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;
|
||||
|
|
|
@ -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-----
|
|
@ -560,5 +560,3 @@ EOF
|
|||
want_ipv6_if => "en0",
|
||||
},
|
||||
);
|
||||
|
||||
1;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
167
t/logmsg.pl
167
t/logmsg.pl
|
@ -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();
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
107
t/read_recap.pl
107
t/read_recap.pl
|
@ -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
150
t/skip.pl
|
@ -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();
|
|
@ -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();
|
395
t/update_nics.pl
395
t/update_nics.pl
|
@ -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();
|
41
t/use_cmd.pl
41
t/use_cmd.pl
|
@ -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();
|
87
t/use_web.pl
87
t/use_web.pl
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
Loading…
Reference in a new issue