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:
|
jobs:
|
||||||
test-debian-like:
|
test-debian-like:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
image:
|
image:
|
||||||
- ubuntu:latest
|
- ubuntu:latest
|
||||||
|
@ -33,11 +32,10 @@ jobs:
|
||||||
libtest-tcp-perl \
|
libtest-tcp-perl \
|
||||||
libtest-warnings-perl \
|
libtest-warnings-perl \
|
||||||
liburi-perl \
|
liburi-perl \
|
||||||
libwww-perl \
|
|
||||||
net-tools \
|
net-tools \
|
||||||
make \
|
make \
|
||||||
;
|
;
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: autogen
|
- name: autogen
|
||||||
run: ./autogen
|
run: ./autogen
|
||||||
- name: configure
|
- name: configure
|
||||||
|
@ -48,45 +46,41 @@ jobs:
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
||||||
- name: distribution tarball is complete
|
- name: distribution tarball is complete
|
||||||
run: ./.github/workflows/scripts/dist-tarball-check
|
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:
|
#test-centos8:
|
||||||
strategy:
|
# runs-on: ubuntu-latest
|
||||||
fail-fast: false
|
# container: centos:8
|
||||||
matrix:
|
# steps:
|
||||||
image:
|
# - uses: actions/checkout@v2
|
||||||
- fedora:39
|
# - name: install dependencies
|
||||||
- fedora:latest
|
# run: |
|
||||||
- fedora:rawhide
|
# dnf --refresh --enablerepo=PowerTools install -y \
|
||||||
- almalinux:8
|
# automake \
|
||||||
- almalinux:latest
|
# 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
|
runs-on: ubuntu-latest
|
||||||
container:
|
container: fedora
|
||||||
image: ${{ matrix.image }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- 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
|
|
||||||
- name: install dependencies
|
- 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: |
|
run: |
|
||||||
dnf --refresh install --skip-broken -y \
|
dnf --refresh install -y \
|
||||||
automake \
|
automake \
|
||||||
findutils \
|
findutils \
|
||||||
iproute \
|
|
||||||
make \
|
make \
|
||||||
curl \
|
curl \
|
||||||
perl \
|
perl \
|
||||||
|
@ -97,8 +91,6 @@ jobs:
|
||||||
perl-Test-MockModule \
|
perl-Test-MockModule \
|
||||||
perl-Test-TCP \
|
perl-Test-TCP \
|
||||||
perl-Test-Warnings \
|
perl-Test-Warnings \
|
||||||
perl-core \
|
|
||||||
perl-libwww-perl \
|
|
||||||
net-tools \
|
net-tools \
|
||||||
;
|
;
|
||||||
- name: autogen
|
- name: autogen
|
||||||
|
@ -109,3 +101,29 @@ jobs:
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always check
|
run: make VERBOSE=1 AM_COLOR_TESTS=always check
|
||||||
- name: distcheck
|
- name: distcheck
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always 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
|
/Makefile.in
|
||||||
/aclocal.m4
|
/aclocal.m4
|
||||||
/autom4te.cache/
|
/autom4te.cache/
|
||||||
/build-aux/config.guess
|
|
||||||
/build-aux/config.sub
|
|
||||||
/build-aux/install-sh
|
/build-aux/install-sh
|
||||||
/build-aux/missing
|
/build-aux/missing
|
||||||
/build-aux/tap-driver.sh
|
|
||||||
/config.log
|
/config.log
|
||||||
/config.status
|
/config.status
|
||||||
/configure
|
/configure
|
||||||
|
|
|
@ -80,7 +80,7 @@ perltidy -l=99 -conv -ci=4 -ola -ce -nbbc -kis -pt=2 -b ddclient
|
||||||
|
|
||||||
## Git Hygiene
|
## 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:
|
* Please use `git rebase -i` to make your commits easy to review:
|
||||||
- Put unrelated changes in separate commits
|
- Put unrelated changes in separate commits
|
||||||
- Squash your fixup commits
|
- Squash your fixup commits
|
||||||
|
@ -190,11 +190,11 @@ better to revert the original change then redo it:
|
||||||
|
|
||||||
### Merging Pull Requests
|
### 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:
|
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
|
|\ Merge pull request #142 from rhansen/config-line-format
|
||||||
| * 30180ed rhansen@rhansen.org 2020-05-30 13:09:38 -0400
|
| * 30180ed rhansen@rhansen.org 2020-05-30 13:09:38 -0400
|
||||||
|/ Expand comment documenting config line format
|
|/ Expand comment documenting config line format
|
||||||
|
@ -231,7 +231,7 @@ has value:
|
||||||
change was made) and the merge timestamp (when it went live).
|
change was made) and the merge timestamp (when it went live).
|
||||||
|
|
||||||
To achieve a history like the above, the pull request must be rebased
|
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
|
one-click way to do this (the "Rebase and merge" option does a
|
||||||
fast-forward merge, which is not what we want). See
|
fast-forward merge, which is not what we want). See
|
||||||
[isaacs/github#1143](https://github.com/isaacs/github/issues/1143) and
|
[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
|
# Add a remote for the fork used in the PR
|
||||||
git remote add "${PR_USER:?}" git@github.com:"${PR_USER:?}"/ddclient
|
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
|
git remote update -p
|
||||||
|
|
||||||
# Switch to the pull request branch
|
# Switch to the pull request branch
|
||||||
git checkout -b "${PR_USER:?}-${PR_BRANCH:?}" "${PR_USER:?}/${PR_BRANCH:?}"
|
git checkout -b "${PR_USER:?}-${PR_BRANCH:?}" "${PR_USER:?}/${PR_BRANCH:?}"
|
||||||
|
|
||||||
# Rebase the commits (optionally using -i to clean up history) onto
|
# Rebase the commits (optionally using -i to clean up history) onto
|
||||||
# the current ddclient main branch
|
# the current ddclient master branch
|
||||||
git rebase origin/main
|
git rebase origin/master
|
||||||
|
|
||||||
# Force update the contributor's fork. This will only work if the
|
# Force update the contributor's fork. This will only work if the
|
||||||
# contributor has checked the "Allow edits by maintainers" box in 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,
|
# "Allow edits by maintainers", or if you prefer to merge manually,
|
||||||
# continue with the next steps.
|
# continue with the next steps.
|
||||||
|
|
||||||
# Switch to the local main branch
|
# Switch to the local master branch
|
||||||
git checkout main
|
git checkout master
|
||||||
|
|
||||||
# Make sure the local main branch is up to date
|
# Make sure the local master branch is up to date
|
||||||
git merge --ff-only origin/main
|
git merge --ff-only origin/master
|
||||||
|
|
||||||
# Merge in the rebased pull request branch **WITHOUT DOING A
|
# Merge in the rebased pull request branch **WITHOUT DOING A
|
||||||
# FAST-FORWARD MERGE**
|
# FAST-FORWARD MERGE**
|
||||||
git merge --no-ff "${PR_USER:?}-${PR_BRANCH:?}"
|
git merge --no-ff "${PR_USER:?}-${PR_BRANCH:?}"
|
||||||
|
|
||||||
# Review the commits before pushing
|
# Review the commits before pushing
|
||||||
git log --graph --oneline --decorate origin/main..
|
git log --graph --oneline --decorate origin/master..
|
||||||
|
|
||||||
# Push to ddclient main
|
# Push to ddclient master
|
||||||
git push origin main
|
git push origin master
|
||||||
```
|
```
|
||||||
|
|
205
ChangeLog.md
205
ChangeLog.md
|
@ -1,210 +1,7 @@
|
||||||
# ChangeLog
|
# ChangeLog
|
||||||
|
|
||||||
This document describes notable changes. For details, see the [source code
|
This document describes notable changes. For details, see the [source code
|
||||||
repository history](https://github.com/ddclient/ddclient/commits/main).
|
repository history](https://github.com/ddclient/ddclient/commits/master).
|
||||||
|
|
||||||
## v4.0.1-alpha (unreleased work-in-progress)
|
|
||||||
|
|
||||||
## 2025-01-19 v4.0.0
|
|
||||||
|
|
||||||
### Breaking changes
|
|
||||||
|
|
||||||
* ddclient now looks for `ddclient.conf` in `${sysconfdir}/ddclient` by
|
|
||||||
default instead of `${sysconfdir}`.
|
|
||||||
[#789](https://github.com/ddclient/ddclient/pull/789)
|
|
||||||
|
|
||||||
To retain the previous behavior, pass `'--with-confdir=${sysconfdir}'` to
|
|
||||||
`configure`. For example:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# Before v4.0.0:
|
|
||||||
./configure --sysconfdir=/etc
|
|
||||||
# Equivalent with v4.0.0 and later (the single quotes are intentional):
|
|
||||||
./configure --sysconfdir=/etc --with-confdir='${sysconfdir}'
|
|
||||||
```
|
|
||||||
|
|
||||||
or:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# Before v4.0.0:
|
|
||||||
./configure --sysconfdir=/etc/ddclient
|
|
||||||
# Equivalent with v4.0.0 and later:
|
|
||||||
./configure --sysconfdir=/etc
|
|
||||||
```
|
|
||||||
|
|
||||||
* The `--ssl` option is now enabled by default.
|
|
||||||
[#705](https://github.com/ddclient/ddclient/pull/705)
|
|
||||||
* Unencrypted (plain) HTTP is now used instead of encrypted (TLS) HTTP if the
|
|
||||||
URL uses `http://` instead of `https://`, even if the `--ssl` option is
|
|
||||||
enabled. [#608](https://github.com/ddclient/ddclient/pull/608)
|
|
||||||
* The string argument to `--cmdv4` or `--cmdv6` is now executed as-is by the
|
|
||||||
system's shell, matching the behavior of the deprecated `--cmd` option.
|
|
||||||
This makes it possible to pass command-line arguments, which reduces the
|
|
||||||
need for a custom wrapper script. Beware that the string is also subject to
|
|
||||||
the shell's command substitution, quote handling, variable expansion, field
|
|
||||||
splitting, etc., so you may need to add extra escaping to ensure that any
|
|
||||||
special characters are preserved literally.
|
|
||||||
[#766](https://github.com/ddclient/ddclient/pull/766)
|
|
||||||
* The default web service for `--webv4` and `--webv6` has changed from Google
|
|
||||||
Domains (which has shut down) to ipify.
|
|
||||||
[5b104ad1](https://github.com/ddclient/ddclient/commit/5b104ad116c023c3760129cab6e141f04f72b406)
|
|
||||||
* Invalid command-line options or values are now fatal errors (instead of
|
|
||||||
discarded with a warning).
|
|
||||||
[#733](https://github.com/ddclient/ddclient/pull/733)
|
|
||||||
* All log messages are now written to STDERR, not a mix of STDOUT and STDERR.
|
|
||||||
[#676](https://github.com/ddclient/ddclient/pull/676)
|
|
||||||
* For `--protocol=freedns` and `--protocol=nfsn`, the core module
|
|
||||||
`Digest::SHA` is now required. Previously, `Digest::SHA1` was used (if
|
|
||||||
available) as an alternative to `Digest::SHA`.
|
|
||||||
[#685](https://github.com/ddclient/ddclient/pull/685)
|
|
||||||
* The `he` built-in web IP discovery service (`--webv4=he`, `--webv6=he`, and
|
|
||||||
`--web=he`) was renamed to `he.net` for consistency with the new `he.net`
|
|
||||||
protocol. The old name is still accepted but is deprecated and will be
|
|
||||||
removed in a future version of ddclient.
|
|
||||||
[#682](https://github.com/ddclient/ddclient/pull/682)
|
|
||||||
* Deprecated built-in web IP discovery services are not listed in the output
|
|
||||||
of `--list-web-services`.
|
|
||||||
[#682](https://github.com/ddclient/ddclient/pull/682)
|
|
||||||
* `dyndns2`: Support for "wait" response lines has been removed. The Dyn
|
|
||||||
documentation does not mention such responses, and the code to handle them,
|
|
||||||
untouched since at least 2006, is believed to be obsolete.
|
|
||||||
[#709](https://github.com/ddclient/ddclient/pull/709)
|
|
||||||
* `dyndns2`: The obsolete `static` and `custom` options have been removed.
|
|
||||||
Setting the options may produce a warning.
|
|
||||||
[#709](https://github.com/ddclient/ddclient/pull/709)
|
|
||||||
* The diagnostic `--geturl` command-line argument was removed.
|
|
||||||
[#712](https://github.com/ddclient/ddclient/pull/712)
|
|
||||||
* `easydns`: The default value for `min-interval` was increased from 5m to 10m
|
|
||||||
to match easyDNS documentation.
|
|
||||||
[#713](https://github.com/ddclient/ddclient/pull/713)
|
|
||||||
* `woima`: The dyn.woima.fi service appears to be defunct so support was
|
|
||||||
removed. [#716](https://github.com/ddclient/ddclient/pull/716)
|
|
||||||
* `googledomains`: Support was removed because the service shut down.
|
|
||||||
[#716](https://github.com/ddclient/ddclient/pull/716)
|
|
||||||
* The `--retry` option was removed.
|
|
||||||
[#732](https://github.com/ddclient/ddclient/pull/732)
|
|
||||||
|
|
||||||
### New features
|
|
||||||
|
|
||||||
* New `--mail-from` option to control the "From:" header of email messages.
|
|
||||||
[#565](https://github.com/ddclient/ddclient/pull/565)
|
|
||||||
* Simultaneous/separate updating of IPv4 (A) records and IPv6 (AAAA) records
|
|
||||||
is now supported in the following services: `gandi`
|
|
||||||
([#558](https://github.com/ddclient/ddclient/pull/558)), `nsupdate`
|
|
||||||
([#604](https://github.com/ddclient/ddclient/pull/604)), `noip`
|
|
||||||
([#603](https://github.com/ddclient/ddclient/pull/603)), `mythicdyn`
|
|
||||||
([#616](https://github.com/ddclient/ddclient/pull/616)), `godaddy`
|
|
||||||
([#560](https://github.com/ddclient/ddclient/pull/560)).
|
|
||||||
* `porkbun`: Added support for subdomains.
|
|
||||||
[#624](https://github.com/ddclient/ddclient/pull/624)
|
|
||||||
* `gandi`: Added support for personal access tokens.
|
|
||||||
[#636](https://github.com/ddclient/ddclient/pull/636)
|
|
||||||
* Comments after the `\` line continuation character are now supported.
|
|
||||||
[3c522a7a](https://github.com/ddclient/ddclient/commit/3c522a7aa235f63ae0439e5674e7406e20c90956)
|
|
||||||
* Minor improvements to `--help` output.
|
|
||||||
[#659](https://github.com/ddclient/ddclient/pull/659),
|
|
||||||
[#665](https://github.com/ddclient/ddclient/pull/665)
|
|
||||||
* Improved formatting of ddclient's version number.
|
|
||||||
[#639](https://github.com/ddclient/ddclient/pull/639)
|
|
||||||
* Updated sample systemd service unit file to improve logging in the systemd
|
|
||||||
journal. [#669](https://github.com/ddclient/ddclient/pull/669)
|
|
||||||
* The second and subsequent lines in a multi-line log message now have a
|
|
||||||
different prefix to distinguish them from separate log messages.
|
|
||||||
[#676](https://github.com/ddclient/ddclient/pull/676)
|
|
||||||
[#719](https://github.com/ddclient/ddclient/pull/719)
|
|
||||||
* Log messages now include context, making it easier to troubleshoot issues.
|
|
||||||
[#725](https://github.com/ddclient/ddclient/pull/725)
|
|
||||||
* `emailonly`: New `protocol` option that simply emails you when your IP
|
|
||||||
address changes. [#654](https://github.com/ddclient/ddclient/pull/654)
|
|
||||||
* `he.net`: Added support for updating Hurricane Electric records.
|
|
||||||
[#682](https://github.com/ddclient/ddclient/pull/682)
|
|
||||||
* `dyndns2`, `domeneshop`, `dnsmadeeasy`, `keysystems`: The `server` option
|
|
||||||
can now include `http://` or `https://` to control the use of TLS. If
|
|
||||||
omitted, the value of the `ssl` option is used to determine the scheme.
|
|
||||||
[#703](https://github.com/ddclient/ddclient/pull/703)
|
|
||||||
* `ddns.fm`: New `protocol` option for updating [DDNS.FM](https://ddns.fm/)
|
|
||||||
records. [#695](https://github.com/ddclient/ddclient/pull/695)
|
|
||||||
* `inwx`: New `protocol` option for updating [INWX](https://www.inwx.com/)
|
|
||||||
records. [#690](https://github.com/ddclient/ddclient/pull/690)
|
|
||||||
* `domeneshop`: Add IPv6 support.
|
|
||||||
[#719](https://github.com/ddclient/ddclient/pull/719)
|
|
||||||
* `duckdns`: Multiple hosts with the same IP address are now updated together.
|
|
||||||
[#719](https://github.com/ddclient/ddclient/pull/719)
|
|
||||||
* `directnic`: Added support for updatng Directnic records.
|
|
||||||
[#726](https://github.com/ddclient/ddclient/pull/726)
|
|
||||||
* `porkbun`: The update URL hostname is now configurable via the `server`
|
|
||||||
option. [#752](https://github.com/ddclient/ddclient/pull/752)
|
|
||||||
* `dnsexit2`: Multiple hosts are updated in a single API call when possible.
|
|
||||||
[#684](https://github.com/ddclient/ddclient/pull/684)
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
|
|
||||||
* Fixed numerous bugs in cache file (recap) handling.
|
|
||||||
[#740](https://github.com/ddclient/ddclient/pull/740)
|
|
||||||
* Fixed numerous bugs in command-line option and configuration file
|
|
||||||
processing. [#733](https://github.com/ddclient/ddclient/pull/733)
|
|
||||||
* `noip`: Fixed failure to honor IP discovery settings in some circumstances.
|
|
||||||
[#591](https://github.com/ddclient/ddclient/pull/591)
|
|
||||||
* Fixed `--usev6` with providers that have not yet been updated to use the new
|
|
||||||
separate IPv4/IPv6 logic.
|
|
||||||
[ad854ab7](https://github.com/ddclient/ddclient/commit/ad854ab716922f5f25742421ebd4c27646b86619)
|
|
||||||
* HTTP redirects (301, 302) are now followed.
|
|
||||||
[#592](https://github.com/ddclient/ddclient/pull/592)
|
|
||||||
* `keysystems`: Fixed update URL.
|
|
||||||
[#629](https://github.com/ddclient/ddclient/pull/629)
|
|
||||||
* `dondominio`: Fixed response parsing.
|
|
||||||
[#646](https://github.com/ddclient/ddclient/pull/646)
|
|
||||||
* Fixed `--web-ssl-validate` and `--fw-ssl-validate` options, which were
|
|
||||||
ignored in some cases (defaulting to validate).
|
|
||||||
[#661](https://github.com/ddclient/ddclient/pull/661)
|
|
||||||
* Explicitly setting `--web-skip`, `--webv4-skip`, `--webv6-skip`,
|
|
||||||
`--fw-skip`, `--fwv4-skip`, and `--fwv6-skip` to the empty string now
|
|
||||||
disables any built-in default skip. Before, setting to the empty string had
|
|
||||||
no effect. [#662](https://github.com/ddclient/ddclient/pull/662)
|
|
||||||
* `--use=disabled` now works.
|
|
||||||
[#665](https://github.com/ddclient/ddclient/pull/665)
|
|
||||||
* `--retry` and `--daemon` are incompatible with each other; ddclient now
|
|
||||||
errors out if both are provided.
|
|
||||||
[#666](https://github.com/ddclient/ddclient/pull/666)
|
|
||||||
* `--usev4=cisco` and `--usev4=cisco-asa` now work.
|
|
||||||
[#664](https://github.com/ddclient/ddclient/pull/664)
|
|
||||||
* Fixed "Scalar value better written as" Perl warning.
|
|
||||||
[#667](https://github.com/ddclient/ddclient/pull/667)
|
|
||||||
* Fixed "Invalid Value for keyword 'wtime' = ''" warning.
|
|
||||||
[#734](https://github.com/ddclient/ddclient/pull/734)
|
|
||||||
* Fixed unnecessary repeated updates for some services.
|
|
||||||
[#670](https://github.com/ddclient/ddclient/pull/670)
|
|
||||||
[#732](https://github.com/ddclient/ddclient/pull/732)
|
|
||||||
* Fixed DNSExit provider when configured with a zone and non-identical
|
|
||||||
hostname. [#674](https://github.com/ddclient/ddclient/pull/674)
|
|
||||||
* `infomaniak`: Fixed frequent forced updates after 25 days (`max-interval`).
|
|
||||||
[#691](https://github.com/ddclient/ddclient/pull/691)
|
|
||||||
* `infomaniak`: Fixed incorrect parsing of server response.
|
|
||||||
[#692](https://github.com/ddclient/ddclient/pull/692)
|
|
||||||
* `infomaniak`: Fixed incorrect handling of `nochg` responses.
|
|
||||||
[#723](https://github.com/ddclient/ddclient/pull/723)
|
|
||||||
* `regfishde`: Fixed IPv6 support.
|
|
||||||
[#691](https://github.com/ddclient/ddclient/pull/691)
|
|
||||||
* `easydns`: IPv4 and IPv6 addresses are now updated separately to be
|
|
||||||
consistent with the easyDNS documentation.
|
|
||||||
[#713](https://github.com/ddclient/ddclient/pull/713)
|
|
||||||
* `easydns`: Fixed parsing of result code from server response.
|
|
||||||
[#713](https://github.com/ddclient/ddclient/pull/713)
|
|
||||||
* `easydns`: Fixed successful updates treated as failed updates.
|
|
||||||
[#713](https://github.com/ddclient/ddclient/pull/713)
|
|
||||||
* Any IP addresses in an HTTP response's headers or in an HTTP error
|
|
||||||
response's body are now ignored when obtaining the IP address from a
|
|
||||||
web-based IP discovery service (`--usev4=webv4`, `--usev6=webv6`) or from a
|
|
||||||
router/firewall device.
|
|
||||||
[#719](https://github.com/ddclient/ddclient/pull/719)
|
|
||||||
* `yandex`: Errors are now retried.
|
|
||||||
[#719](https://github.com/ddclient/ddclient/pull/719)
|
|
||||||
* `gandi`: Fixed handling of error responses.
|
|
||||||
[#721](https://github.com/ddclient/ddclient/pull/721)
|
|
||||||
* `dyndns2`: Fixed handling of responses for multi-host updates.
|
|
||||||
[#728](https://github.com/ddclient/ddclient/pull/728)
|
|
||||||
* `porkbun`: The default update URL was updated from `porkbun.com` to
|
|
||||||
`api.porkbun.com`. [#752](https://github.com/ddclient/ddclient/pull/752)
|
|
||||||
|
|
||||||
## 2023-11-23 v3.11.2
|
## 2023-11-23 v3.11.2
|
||||||
|
|
||||||
|
|
52
Makefile.am
52
Makefile.am
|
@ -1,4 +1,4 @@
|
||||||
ACLOCAL_AMFLAGS = -I build-aux/m4
|
ACLOCAL_AMFLAGS = -I m4
|
||||||
EXTRA_DIST = \
|
EXTRA_DIST = \
|
||||||
CONTRIBUTING.md \
|
CONTRIBUTING.md \
|
||||||
COPYING \
|
COPYING \
|
||||||
|
@ -16,7 +16,19 @@ EXTRA_DIST = \
|
||||||
sample-get-ip-from-fritzbox
|
sample-get-ip-from-fritzbox
|
||||||
CLEANFILES =
|
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
|
subst_files = ddclient ddclient.conf
|
||||||
|
|
||||||
EXTRA_DIST += $(subst_files:=.in)
|
EXTRA_DIST += $(subst_files:=.in)
|
||||||
CLEANFILES += $(subst_files)
|
CLEANFILES += $(subst_files)
|
||||||
|
|
||||||
|
@ -24,14 +36,7 @@ $(subst_files): Makefile
|
||||||
rm -f '$@' '$@'.tmp
|
rm -f '$@' '$@'.tmp
|
||||||
in='$@'.in; \
|
in='$@'.in; \
|
||||||
test -f "$${in}" || in='$(srcdir)/'$${in}; \
|
test -f "$${in}" || in='$(srcdir)/'$${in}; \
|
||||||
sed \
|
$(subst) "$${in}" >'$@'.tmp && \
|
||||||
-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 && \
|
|
||||||
{ ! test -x "$${in}" || chmod +x '$@'.tmp; }
|
{ ! test -x "$${in}" || chmod +x '$@'.tmp; }
|
||||||
mv '$@'.tmp '$@'
|
mv '$@'.tmp '$@'
|
||||||
|
|
||||||
|
@ -40,7 +45,7 @@ ddclient.conf: $(srcdir)/ddclient.conf.in
|
||||||
|
|
||||||
bin_SCRIPTS = ddclient
|
bin_SCRIPTS = ddclient
|
||||||
|
|
||||||
conf_DATA = ddclient.conf
|
sysconf_DATA = ddclient.conf
|
||||||
|
|
||||||
install-data-local:
|
install-data-local:
|
||||||
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
|
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
|
||||||
|
@ -57,36 +62,17 @@ AM_PL_LOG_FLAGS = -Mstrict -w \
|
||||||
-I'$(abs_top_srcdir)'/t/lib \
|
-I'$(abs_top_srcdir)'/t/lib \
|
||||||
-MDevel::Autoflush
|
-MDevel::Autoflush
|
||||||
handwritten_tests = \
|
handwritten_tests = \
|
||||||
t/builtinfw_query.pl \
|
|
||||||
t/check_value.pl \
|
|
||||||
t/get_ip_from_if.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-ipv4.pl \
|
||||||
t/is-and-extract-ipv6.pl \
|
t/is-and-extract-ipv6.pl \
|
||||||
t/is-and-extract-ipv6-global.pl \
|
t/is-and-extract-ipv6-global.pl \
|
||||||
t/logmsg.pl \
|
|
||||||
t/parse_assignments.pl \
|
t/parse_assignments.pl \
|
||||||
t/protocol_directnic.pl \
|
t/write_cache.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
|
|
||||||
generated_tests = \
|
generated_tests = \
|
||||||
|
t/geturl_connectivity.pl \
|
||||||
t/version.pl
|
t/version.pl
|
||||||
TESTS = $(handwritten_tests) $(generated_tests)
|
TESTS = $(handwritten_tests) $(generated_tests)
|
||||||
$(TESTS): ddclient
|
|
||||||
EXTRA_DIST += $(handwritten_tests) \
|
EXTRA_DIST += $(handwritten_tests) \
|
||||||
.autom4te.cfg \
|
|
||||||
t/lib/Devel/Autoflush.pm \
|
t/lib/Devel/Autoflush.pm \
|
||||||
t/lib/Test/Builder.pm \
|
t/lib/Test/Builder.pm \
|
||||||
t/lib/Test/Builder/Formatter.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-ca-cert.pem \
|
||||||
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-cert.pem \
|
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-cert.pem \
|
||||||
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-key.pem \
|
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-key.pem \
|
||||||
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem \
|
|
||||||
t/lib/ddclient/t.pm \
|
t/lib/ddclient/t.pm \
|
||||||
t/lib/ddclient/t/HTTPD.pm \
|
|
||||||
t/lib/ddclient/t/Logger.pm \
|
|
||||||
t/lib/ddclient/t/ip.pm \
|
|
||||||
t/lib/ok.pm
|
t/lib/ok.pm
|
||||||
|
|
203
README.md
203
README.md
|
@ -3,35 +3,7 @@
|
||||||
`ddclient` is a Perl client used to update dynamic DNS entries for accounts
|
`ddclient` is a Perl client used to update dynamic DNS entries for accounts
|
||||||
on many dynamic DNS services. It uses `curl` for internet access.
|
on many dynamic DNS services. It uses `curl` for internet access.
|
||||||
|
|
||||||
on docker compose
|
This is a friendly fork/continuation of https://github.com/ddclient/ddclient
|
||||||
```docker-compose
|
|
||||||
services:
|
|
||||||
ddclient:
|
|
||||||
image: lscr.io/linuxserver/ddclient:latest
|
|
||||||
container_name: ddclient
|
|
||||||
environment:
|
|
||||||
- PUID=1000
|
|
||||||
- PGID=1000
|
|
||||||
- TZ=Europe/Rome
|
|
||||||
volumes:
|
|
||||||
- /home/orangepi/dockerfiles/ddclient/config:/config
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
file ddclient.conf per servizio DDNS di dynu.com da mettere nel folder config
|
|
||||||
```file
|
|
||||||
daemon=60 # check every 300 seconds
|
|
||||||
syslog=yes # log update msgs to syslog
|
|
||||||
mail=root # mail all msgs to root#mail-failure=root # mail failed update msgs to root
|
|
||||||
pid=/var/run/ddclient/ddclient.pid # record PID in file.
|
|
||||||
use=web, web=checkip.dynu.com/, web-skip='IP Address'
|
|
||||||
protocol=dyndns2 # default protocol
|
|
||||||
server=api.dynu.com
|
|
||||||
# default login
|
|
||||||
login=FabioMich66 # your default user
|
|
||||||
password=Master66! # your default password
|
|
||||||
wildcard=yes
|
|
||||||
patachina.casacam.net
|
|
||||||
```
|
|
||||||
|
|
||||||
## Alternatives
|
## Alternatives
|
||||||
|
|
||||||
|
@ -43,44 +15,41 @@ your dynamic DNS provider(s): <https://github.com/troglobit/inadyn> or
|
||||||
|
|
||||||
Dynamic DNS services currently supported include:
|
Dynamic DNS services currently supported include:
|
||||||
|
|
||||||
* [1984.is](https://www.1984.is/product/freedns)
|
* [1984.is](https://www.1984.is/product/freedns)
|
||||||
* [ChangeIP](https://www.changeip.com)
|
* [ChangeIP](https://www.changeip.com)
|
||||||
* [CloudFlare](https://www.cloudflare.com)
|
* [CloudFlare](https://www.cloudflare.com)
|
||||||
* [ClouDNS](https://www.cloudns.net)
|
* [ClouDNS](https://www.cloudns.net)
|
||||||
* [DDNS.fm](https://www.ddns.fm/)
|
* [DigitalOcean](https://www.digitalocean.com/)
|
||||||
* [DigitalOcean](https://www.digitalocean.com/)
|
* [dinahosting](https://dinahosting.com)
|
||||||
* [dinahosting](https://dinahosting.com)
|
* [DonDominio](https://www.dondominio.com)
|
||||||
* [Directnic](https://directnic.com)
|
* [DNS Made Easy](https://dnsmadeeasy.com)
|
||||||
* [DonDominio](https://www.dondominio.com)
|
* [DNSExit](https://dnsexit.com/dns/dns-api)
|
||||||
* [DNS Made Easy](https://dnsmadeeasy.com)
|
* [Domeneshop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
|
||||||
* [DNSExit](https://dnsexit.com/dns/dns-api)
|
* [DslReports](https://www.dslreports.com)
|
||||||
* [dnsHome.de](https://www.dnshome.de)
|
* [Duck DNS](https://duckdns.org)
|
||||||
* [Domeneshop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
|
* [DynDNS.com](https://account.dyn.com)
|
||||||
* [DslReports](https://www.dslreports.com)
|
* [EasyDNS](https://www.easydns.com )
|
||||||
* [Duck DNS](https://duckdns.org)
|
* [Enom](https://www.enom.com)
|
||||||
* [DynDNS.com](https://account.dyn.com)
|
* [Freedns](https://freedns.afraid.org)
|
||||||
* [EasyDNS](https://www.easydns.com )
|
* [Freemyip](https://freemyip.com)
|
||||||
* [Enom](https://www.enom.com)
|
* [Gandi](https://gandi.net)
|
||||||
* [Freedns](https://freedns.afraid.org)
|
* [GoDaddy](https://www.godaddy.com)
|
||||||
* [Freemyip](https://freemyip.com)
|
* [Google](https://domains.google)
|
||||||
* [Gandi](https://gandi.net)
|
* [Infomaniak](https://faq.infomaniak.com/2376)
|
||||||
* [GoDaddy](https://www.godaddy.com)
|
* [Loopia](https://www.loopia.se)
|
||||||
* [Hurricane Electric](https://dns.he.net)
|
* [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
|
||||||
* [Infomaniak](https://faq.infomaniak.com/2376)
|
* [NameCheap](https://www.namecheap.com)
|
||||||
* [INWX](https://www.inwx.com/)
|
* [NearlyFreeSpeech.net](https://www.nearlyfreespeech.net/services/dns)
|
||||||
* [Loopia](https://www.loopia.se)
|
* [Njalla](https://njal.la/docs/ddns)
|
||||||
* [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
|
* [Noip](https://www.noip.com)
|
||||||
* [NameCheap](https://www.namecheap.com)
|
* nsupdate - see nsupdate(1) and ddns-confgen(8)
|
||||||
* [NearlyFreeSpeech.net](https://www.nearlyfreespeech.net/services/dns)
|
* [OVH](https://www.ovhcloud.com)
|
||||||
* [Njalla](https://njal.la/docs/ddns)
|
* [Porkbun](https://porkbun.com)
|
||||||
* [Noip](https://www.noip.com)
|
* [regfish.de](https://www.regfish.de/domains/dyndns)
|
||||||
* nsupdate - see nsupdate(1) and ddns-confgen(8)
|
* [Sitelutions](https://www.sitelutions.com)
|
||||||
* [OVH](https://www.ovhcloud.com)
|
* [woima.fi](https://woima.fi)
|
||||||
* [Porkbun](https://porkbun.com)
|
* [Yandex](https://dns.yandex.com)
|
||||||
* [regfish.de](https://www.regfish.de/domains/dyndns)
|
* [Zoneedit](https://www.zoneedit.com)
|
||||||
* [Sitelutions](https://www.sitelutions.com)
|
|
||||||
* [Yandex](https://dns.yandex.com)
|
|
||||||
* [Zoneedit](https://www.zoneedit.com)
|
|
||||||
|
|
||||||
`ddclient` supports finding your IP address from many cable and DSL
|
`ddclient` supports finding your IP address from many cable and DSL
|
||||||
broadband routers.
|
broadband routers.
|
||||||
|
@ -135,7 +104,7 @@ operating system. See the image to the right for a list of distributions with a
|
||||||
```shell
|
```shell
|
||||||
./configure \
|
./configure \
|
||||||
--prefix=/usr \
|
--prefix=/usr \
|
||||||
--sysconfdir=/etc \
|
--sysconfdir=/etc/ddclient \
|
||||||
--localstatedir=/var
|
--localstatedir=/var
|
||||||
make
|
make
|
||||||
make VERBOSE=1 check
|
make VERBOSE=1 check
|
||||||
|
@ -156,97 +125,43 @@ start the first time by hand
|
||||||
|
|
||||||
systemctl start ddclient.service
|
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
|
## 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?
|
2. Do you need to specify a proxy?
|
||||||
If so, just add a `proxy=your.isp.proxy` to the `ddclient.conf` file.
|
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
|
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`
|
``/etc/ddclient/ddclient.conf`` and then try ``$ ddclient -daemon=0 -query`` to see if the router status web page can be understood.
|
||||||
to see if the router status web page can be understood.
|
|
||||||
|
|
||||||
* Need support for another router/firewall?
|
4. Need support for another router/firewall?
|
||||||
Define the router yourself with:
|
Define the router status page yourself with: ``fw=url-to-your-router``'s-status-page ``fw-skip=any-string-preceding-your-IP-address``
|
||||||
|
|
||||||
```
|
ddclient does something like this to provide builtin support for
|
||||||
usev4=fwv4
|
common routers.
|
||||||
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:
|
For example, the Linksys routers could have been added with:
|
||||||
|
|
||||||
```
|
fw=192.168.1.1/Status.htm
|
||||||
usev4=fwv4
|
fw-skip=WAN.*?IP Address
|
||||||
fwv4=192.168.1.1/Status.htm
|
|
||||||
fwv4-skip=WAN.*?IP Address
|
|
||||||
```
|
|
||||||
|
|
||||||
OR [create a new issue](https://github.com/ddclient/ddclient/issues/new)
|
OR
|
||||||
containing the output from:
|
Send me the output from:
|
||||||
|
``$ ddclient -geturl {fw-ip-status-url} [-login login [-password password]]``
|
||||||
|
and I'll add it to the next release!
|
||||||
|
|
||||||
```
|
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
|
|
||||||
```
|
|
||||||
|
|
||||||
so that we can add a new firewall definition to a future release of
|
5. Some broadband routers require the use of a password when ddclient
|
||||||
ddclient.
|
accesses its status page to determine the router's WAN IP address.
|
||||||
|
|
||||||
* 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
|
If this is the case for your router, add
|
||||||
|
|
||||||
```
|
|
||||||
fw-login=your-router-login
|
fw-login=your-router-login
|
||||||
fw-password=your-router-password
|
fw-password=your-router-password
|
||||||
```
|
|
||||||
|
|
||||||
to the beginning of your ddclient.conf file.
|
to the beginning of your ddclient.conf file.
|
||||||
Note that some routers use either 'root' or 'admin' as their login while
|
Note that some routers use either 'root' or 'admin' as their login
|
||||||
some others accept anything.
|
while some others accept anything.
|
||||||
|
|
||||||
## USING DDCLIENT WITH `ppp`
|
## USING DDCLIENT WITH `ppp`
|
||||||
|
|
||||||
|
@ -284,7 +199,7 @@ In my case, it is named dhcpcd-eth0.exe and contains the lines:
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
PATH=/usr/bin:/root/bin:${PATH}
|
PATH=/usr/bin:/root/bin:${PATH}
|
||||||
logger -t dhcpcd IP address changed to $1
|
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
|
exit 0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
26
autogen
26
autogen
|
@ -7,16 +7,18 @@ fatal() { error "$@"; exit 1; }
|
||||||
try() { "$@" || fatal "'$@' failed"; }
|
try() { "$@" || fatal "'$@' failed"; }
|
||||||
|
|
||||||
try cd "${0%/*}"
|
try cd "${0%/*}"
|
||||||
# aclocal complains if a directory passed to AC_CONFIG_MACRO_DIR doesn't exist.
|
try mkdir -p m4 build-aux
|
||||||
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 autoreconf -fviW all
|
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])
|
AC_PREREQ([2.63])
|
||||||
# Get the version from ddclient.in so that the same version string
|
AC_INIT([ddclient], [3.11.2])
|
||||||
# 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_CONFIG_SRCDIR([ddclient.in])
|
AC_CONFIG_SRCDIR([ddclient.in])
|
||||||
AC_CONFIG_AUX_DIR([build-aux])
|
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])
|
AC_REQUIRE_AUX_FILE([tap-driver.sh])
|
||||||
# If the automake dependency is bumped to v1.12 or newer, remove
|
# If the automake dependency is bumped to v1.12 or newer, remove
|
||||||
# build-aux/tap-driver.sh from the repository. Automake 1.12+ comes
|
# 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_INIT_AUTOMAKE([1.11 -Wall -Werror foreign subdir-objects parallel-tests])
|
||||||
AM_SILENT_RULES
|
AM_SILENT_RULES
|
||||||
|
|
||||||
m4_define([CONFDIR_DEFAULT], [${sysconfdir}/AC_PACKAGE_NAME])
|
|
||||||
AC_ARG_WITH(
|
|
||||||
[confdir],
|
|
||||||
[AS_HELP_STRING(
|
|
||||||
[--with-confdir=DIR],
|
|
||||||
m4_expand([[look for ddclient.conf in DIR @<:@default: ]CONFDIR_DEFAULT[@:>@]]))],
|
|
||||||
[],
|
|
||||||
# The single quotes are intentional; see:
|
|
||||||
# https://www.gnu.org/software/automake/manual/html_node/Uniform.html
|
|
||||||
[with_confdir='CONFDIR_DEFAULT'])
|
|
||||||
AC_SUBST([confdir], [${with_confdir}])
|
|
||||||
|
|
||||||
AC_PROG_MKDIR_P
|
AC_PROG_MKDIR_P
|
||||||
|
|
||||||
# The Fedora Docker image doesn't come with the 'findutils' package.
|
# The Fedora Docker image doesn't come with the 'findutils' package.
|
||||||
|
@ -48,18 +27,7 @@ AC_PROG_MKDIR_P
|
||||||
AC_PATH_PROG([FIND], [find])
|
AC_PATH_PROG([FIND], [find])
|
||||||
AS_IF([test -z "${FIND}"], [AC_MSG_ERROR(['find' utility not found])])
|
AS_IF([test -z "${FIND}"], [AC_MSG_ERROR(['find' utility not found])])
|
||||||
|
|
||||||
AC_ARG_WITH([curl],
|
AC_PATH_PROG([CURL], [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}])
|
|
||||||
]);
|
|
||||||
AS_IF([test -z "${CURL}"], [AC_MSG_ERROR([curl not found])])
|
AS_IF([test -z "${CURL}"], [AC_MSG_ERROR([curl not found])])
|
||||||
|
|
||||||
AX_WITH_PROG([PERL], perl)
|
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
|
# package doesn't depend on all of them, so their availability can't
|
||||||
# be assumed.
|
# be assumed.
|
||||||
m4_foreach_w([_m], [
|
m4_foreach_w([_m], [
|
||||||
Data::Dumper
|
|
||||||
File::Basename
|
File::Basename
|
||||||
File::Path
|
File::Path
|
||||||
File::Temp
|
File::Temp
|
||||||
|
@ -87,12 +54,9 @@ m4_foreach_w([_m], [
|
||||||
# then some tests will fail. Only prints a warning if not installed.
|
# then some tests will fail. Only prints a warning if not installed.
|
||||||
m4_foreach_w([_m], [
|
m4_foreach_w([_m], [
|
||||||
B
|
B
|
||||||
Exporter
|
Data::Dumper
|
||||||
File::Spec::Functions
|
File::Spec::Functions
|
||||||
File::Temp
|
File::Temp
|
||||||
List::Util
|
|
||||||
Scalar::Util
|
|
||||||
re
|
|
||||||
], [AX_PROG_PERL_MODULES([_m], [],
|
], [AX_PROG_PERL_MODULES([_m], [],
|
||||||
[AC_MSG_WARN([some tests will fail due to missing module _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.
|
# prints a warning if not installed.
|
||||||
m4_foreach_w([_m], [
|
m4_foreach_w([_m], [
|
||||||
Carp
|
Carp
|
||||||
|
Exporter
|
||||||
HTTP::Daemon=6.12
|
HTTP::Daemon=6.12
|
||||||
HTTP::Daemon::SSL
|
HTTP::Daemon::SSL
|
||||||
HTTP::Message::PSGI
|
HTTP::Message::PSGI
|
||||||
HTTP::Request
|
HTTP::Request
|
||||||
HTTP::Response
|
HTTP::Response
|
||||||
JSON::PP
|
Scalar::Util
|
||||||
Test::MockModule
|
Test::MockModule
|
||||||
Test::TCP
|
Test::TCP
|
||||||
Test::Warnings
|
Test::Warnings
|
||||||
Time::HiRes
|
Time::HiRes
|
||||||
URI
|
URI
|
||||||
parent
|
|
||||||
], [AX_PROG_PERL_MODULES([_m], [],
|
], [AX_PROG_PERL_MODULES([_m], [],
|
||||||
[AC_MSG_WARN([some tests may be skipped due to missing module _m])])])
|
[AC_MSG_WARN([some tests may be skipped due to missing module _m])])])
|
||||||
|
|
||||||
AC_CONFIG_FILES([
|
AC_CONFIG_FILES([
|
||||||
Makefile
|
Makefile
|
||||||
|
t/geturl_connectivity.pl
|
||||||
t/version.pl
|
t/version.pl
|
||||||
])
|
])
|
||||||
AC_OUTPUT
|
AC_OUTPUT
|
||||||
|
|
183
ddclient.conf.in
183
ddclient.conf.in
|
@ -16,21 +16,15 @@
|
||||||
## are mentioned here.
|
## 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
|
daemon=300 # check every 300 seconds
|
||||||
syslog=yes # log update msgs to syslog
|
syslog=yes # log update msgs to syslog
|
||||||
mail=root # mail all msgs to root
|
mail=root # mail all msgs to root
|
||||||
mail-failure=root # mail failed update msgs to root
|
mail-failure=root # mail failed update msgs to root
|
||||||
# mail-from=root # set the email "From:" header to "root". If
|
|
||||||
# unset (the default) or empty, the from address
|
|
||||||
# depends on your system's default behavior.
|
|
||||||
pid=@runstatedir@/ddclient.pid # record PID in file.
|
pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
# postscript=script # run script after updating. The new IP is
|
ssl=yes # use ssl-support. Works with
|
||||||
# added as argument.
|
# 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=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=netopia-r910, fw=192.168.111.1:80 # via Netopia R910 FW
|
||||||
|
@ -53,10 +47,6 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
## To obtain an IP address from FW status page (using fw-login, fw-password)
|
## To obtain an IP address from FW status page (using fw-login, fw-password)
|
||||||
#use=fw, fw=192.168.1.254/status.htm, fw-skip='IP Address' # found after IP Address
|
#use=fw, fw=192.168.1.254/status.htm, fw-skip='IP Address' # found after IP Address
|
||||||
#
|
#
|
||||||
## To obtain an IP address via UPnP from router
|
|
||||||
## Requires miniupnpc to be installed on the system.
|
|
||||||
#use=cmd, cmd=external-ip
|
|
||||||
#
|
|
||||||
## To obtain an IP address from Web status page (using the proxy if defined)
|
## To obtain an IP address from Web status page (using the proxy if defined)
|
||||||
## by default, checkip.dyndns.org is used if you use the dyndns protocol.
|
## by default, checkip.dyndns.org is used if you use the dyndns protocol.
|
||||||
## Using use=web is enough to get it working.
|
## Using use=web is enough to get it working.
|
||||||
|
@ -88,6 +78,26 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
# protocol=dyndns2 \
|
# protocol=dyndns2 \
|
||||||
# your-dynamic-host.dyndns.org
|
# 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)
|
## ZoneEdit (zoneedit.com)
|
||||||
##
|
##
|
||||||
|
@ -137,10 +147,10 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
##
|
##
|
||||||
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
|
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
|
||||||
##
|
##
|
||||||
# protocol=nfsn, \
|
# protocol = nfsn, \
|
||||||
# zone=example.com, \
|
|
||||||
# login=member-login, \
|
# login=member-login, \
|
||||||
# password=api-key \
|
# password=api-key, \
|
||||||
|
# zone=example.com \
|
||||||
# example.com,subdomain.example.com
|
# example.com,subdomain.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -161,7 +171,7 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
# ssl=yes, \
|
# ssl=yes, \
|
||||||
# server=dynupdate.no-ip.com, \
|
# server=dynupdate.no-ip.com, \
|
||||||
# login=your-noip-login, \
|
# login=your-noip-login, \
|
||||||
# password=your-noip-password \
|
# password=your-noip-password, \
|
||||||
# your-host.domain.com, your-2nd-host.domain.com
|
# 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)
|
## CloudFlare (www.cloudflare.com)
|
||||||
##
|
##
|
||||||
# protocol=cloudflare, \
|
#protocol=cloudflare, \
|
||||||
# zone=domain.tld, \
|
#zone=domain.tld, \
|
||||||
# ttl=1, \
|
#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).
|
#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".
|
#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
|
#domain.tld,my.domain.tld
|
||||||
|
|
||||||
##
|
##
|
||||||
## Gandi (gandi.net)
|
## Gandi (gandi.net)
|
||||||
##
|
##
|
||||||
## Single host update
|
## Single host update
|
||||||
# protocol=gandi
|
# 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 \
|
|
||||||
# zone=example.com, \
|
# 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, \
|
# protocol=googledomains,
|
||||||
# password=my-genereated-password \
|
# login=my-auto-generated-username,
|
||||||
# myhost.example.com
|
# password=my-auto-generated-password
|
||||||
|
# my.domain.tld, otherhost.domain.tld
|
||||||
|
|
||||||
##
|
##
|
||||||
## Duckdns (http://www.duckdns.org/)
|
## Duckdns (http://www.duckdns.org/)
|
||||||
|
@ -227,14 +227,6 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
# password=my-token
|
# password=my-token
|
||||||
# myhost
|
# myhost
|
||||||
|
|
||||||
##
|
|
||||||
## DDNS.FM (https://ddns.fm/)
|
|
||||||
##
|
|
||||||
#
|
|
||||||
# protocol=ddns.fm,
|
|
||||||
# password=my-token
|
|
||||||
# myhost.example.com
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## MyOnlinePortal (http://myonlineportal.net)
|
## MyOnlinePortal (http://myonlineportal.net)
|
||||||
##
|
##
|
||||||
|
@ -251,23 +243,23 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
##
|
##
|
||||||
## nsupdate.info IPV4(https://www.nsupdate.info)
|
## nsupdate.info IPV4(https://www.nsupdate.info)
|
||||||
##
|
##
|
||||||
# use=web, web=http://ipv4.nsupdate.info/myip
|
#use=web, web=http://ipv4.nsupdate.info/myip
|
||||||
# protocol=dyndns2
|
#protocol=dyndns2
|
||||||
# server=ipv4.nsupdate.info
|
#server=ipv4.nsupdate.info
|
||||||
# login=domain.nsupdate.info
|
#login=domain.nsupdate.info
|
||||||
# password='123'
|
#password='123'
|
||||||
# domain.nsupdate.info
|
#domain.nsupdate.info
|
||||||
|
|
||||||
##
|
##
|
||||||
## nsupdate.info IPV6 (https://www.nsupdate.info)
|
## nsupdate.info IPV6 (https://www.nsupdate.info)
|
||||||
## ddclient releases <= 3.8.1 do not support IPv6
|
## ddclient releases <= 3.8.1 do not support IPv6
|
||||||
##
|
##
|
||||||
# usev6=if, if=eth0
|
#usev6=if, if=eth0
|
||||||
# protocol=dyndns2
|
#protocol=dyndns2
|
||||||
# server=ipv6.nsupdate.info
|
#server=ipv6.nsupdate.info
|
||||||
# login=domain.nsupdate.info
|
#login=domain.nsupdate.info
|
||||||
# password='123'
|
#password='123'
|
||||||
# domain.nsupdate.info
|
#domain.nsupdate.info
|
||||||
|
|
||||||
##
|
##
|
||||||
## Yandex.Mail for Domain (domain.yandex.com)
|
## Yandex.Mail for Domain (domain.yandex.com)
|
||||||
|
@ -299,9 +291,8 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
# protocol=porkbun
|
# protocol=porkbun
|
||||||
# apikey=APIKey
|
# apikey=APIKey
|
||||||
# secretapikey=SecretAPIKey
|
# secretapikey=SecretAPIKey
|
||||||
# root-domain=example.com
|
|
||||||
# host.example.com,host2.sub.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)
|
## ClouDNS (https://www.cloudns.net)
|
||||||
|
@ -321,17 +312,17 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
##
|
##
|
||||||
## dnsexit (www.dnsexit.com)
|
## dnsexit (www.dnsexit.com)
|
||||||
##
|
##
|
||||||
# protocol=dnsexit, \
|
#protocol=dnsexit, \
|
||||||
# login=myusername, \
|
#login=myusername, \
|
||||||
# password=mypassword, \
|
#password=mypassword, \
|
||||||
# subdomain-1.domain.com,subdomain-2.domain.com
|
#subdomain-1.domain.com,subdomain-2.domain.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## dnsexit2 (API method www.dnsexit.com)
|
## dnsexit2 (API method www.dnsexit.com)
|
||||||
##
|
##
|
||||||
# protocol=dnsexit2
|
#protocol=dnsexit2
|
||||||
# password=MyAPIKey
|
#password=MyAPIKey
|
||||||
# subdomain-1.domain.com,subdomain-2.domain.com
|
#subdomain-1.domain.com,subdomain-2.domain.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## domeneshop (www.domeneshop.no)
|
## domeneshop (www.domeneshop.no)
|
||||||
|
@ -367,18 +358,10 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
##
|
##
|
||||||
## DigitalOcean (www.digitalocean.com)
|
## DigitalOcean (www.digitalocean.com)
|
||||||
##
|
##
|
||||||
# protocol=digitalocean, \
|
#protocol=digitalocean, \
|
||||||
# zone=example.com, \
|
#zone=example.com, \
|
||||||
# password=api-token \
|
#password=api-token \
|
||||||
# example.com,sub.example.com
|
#example.com,sub.example.com
|
||||||
|
|
||||||
##
|
|
||||||
## Directnic (directnic.com)
|
|
||||||
##
|
|
||||||
# protocol=directnic,
|
|
||||||
# urlv4=https://directnic.com/dns/gateway/ipv4_token/
|
|
||||||
# urlv6=https://directnic.com/dns/gateway/ipv6_token/
|
|
||||||
# my-domain.com
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## Infomaniak (www.infomaniak.com)
|
## Infomaniak (www.infomaniak.com)
|
||||||
|
@ -387,35 +370,3 @@ pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
# login=ddns_username,
|
# login=ddns_username,
|
||||||
# password=ddns_password
|
# password=ddns_password
|
||||||
# example.com
|
# 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
|
|
||||||
|
|
6616
ddclient.in
6616
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)
|
## 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
|
## 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
|
After=network-online.target nss-lookup.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=exec
|
Type=forking
|
||||||
Environment=daemon_interval=5m
|
PIDFile=/run/ddclient.pid
|
||||||
ExecStart=/usr/bin/ddclient --daemon ${daemon_interval} --foreground
|
ExecStart=/usr/bin/ddclient
|
||||||
Restart=on-failure
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
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;
|
use Test::More;
|
||||||
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
|
||||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
|
||||||
use ddclient::t;
|
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 {
|
subtest "get_default_interface tests" => sub {
|
||||||
for my $sample (@ddclient::t::routing_samples) {
|
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);
|
my $interface = ddclient::get_default_interface(4);
|
||||||
plan(skip_all => 'no IPv4 interface') if !$interface;
|
if ($interface) {
|
||||||
isnt($interface, "lo", "Check for loopback 'lo'");
|
isnt($interface, "lo", "Check for loopback 'lo'");
|
||||||
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
||||||
my $ip1 = ddclient::get_ip_from_interface("default", 4);
|
my $ip1 = ddclient::get_ip_from_interface("default", 4);
|
||||||
my $ip2 = ddclient::get_ip_from_interface($interface, 4);
|
my $ip2 = ddclient::get_ip_from_interface($interface, 4);
|
||||||
is($ip1, $ip2, "Check IPv4 from default interface");
|
is($ip1, $ip2, "Check IPv4 from default interface");
|
||||||
SKIP: {
|
|
||||||
skip('default interface does not have an appropriate IPv4 addresses') if !$ip1;
|
|
||||||
ok(ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)");
|
ok(ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)");
|
||||||
}
|
}
|
||||||
};
|
$interface = ddclient::get_default_interface(6);
|
||||||
|
if ($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, "lo", "Check for loopback 'lo'");
|
||||||
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
||||||
my $ip1 = ddclient::get_ip_from_interface("default", 6);
|
my $ip1 = ddclient::get_ip_from_interface("default", 6);
|
||||||
my $ip2 = ddclient::get_ip_from_interface($interface, 6);
|
my $ip2 = ddclient::get_ip_from_interface($interface, 6);
|
||||||
is($ip1, $ip2, "Check IPv6 from default interface");
|
is($ip1, $ip2, "Check IPv6 from default interface");
|
||||||
SKIP: {
|
|
||||||
skip('default interface does not have an appropriate IPv6 addresses') if !$ip1;
|
|
||||||
ok(ddclient::is_ipv6($ip1), "Valid IPv6 from get_ip_from_interface($interface)");
|
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:
|
# and modified as follows:
|
||||||
# * Added this comment block.
|
# * Patched with https://github.com/masaki/Test-Fake-HTTPD/pull/4 to add IPv6 support.
|
||||||
# * Patched with https://github.com/masaki/Test-Fake-HTTPD/pull/6 to fix server exit if TLS
|
|
||||||
# negotiation fails.
|
|
||||||
# * Changed package name to ddclient::Test::Fake::HTTPD.
|
# * 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
|
# License: This library is free software; you can redistribute it and/or modify it under the same
|
||||||
# terms as Perl itself.
|
# terms as Perl itself.
|
||||||
|
|
||||||
|
@ -23,7 +20,7 @@ use Scalar::Util qw(blessed weaken);
|
||||||
use Carp qw(croak);
|
use Carp qw(croak);
|
||||||
use Exporter qw(import);
|
use Exporter qw(import);
|
||||||
|
|
||||||
our $VERSION = '0.09';
|
our $VERSION = '0.08';
|
||||||
$VERSION = eval $VERSION;
|
$VERSION = eval $VERSION;
|
||||||
|
|
||||||
our @EXPORT = qw(
|
our @EXPORT = qw(
|
||||||
|
@ -104,10 +101,9 @@ sub run {
|
||||||
$self->port || '<default>',
|
$self->port || '<default>',
|
||||||
$@ eq '' ? '' : ": $@")) unless $d;
|
$@ eq '' ? '' : ": $@")) unless $d;
|
||||||
|
|
||||||
while (1) {
|
$d->accept; # wait for port check from parent process
|
||||||
# accept can return undef if TLS handshake fails (e.g., port test or client rejects
|
|
||||||
# cert).
|
while (my $c = $d->accept) {
|
||||||
my $c = $d->accept or next;
|
|
||||||
while (my $req = $c->get_request) {
|
while (my $req = $c->get_request) {
|
||||||
my $res = $self->_to_http_res($app->($req));
|
my $res = $self->_to_http_res($app->($req));
|
||||||
$c->send_response($res);
|
$c->send_response($res);
|
||||||
|
@ -147,7 +143,7 @@ sub endpoint {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
my $uri = URI->new($self->scheme . ':');
|
my $uri = URI->new($self->scheme . ':');
|
||||||
my $host = $self->host;
|
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->host($host);
|
||||||
$uri->port($self->port);
|
$uri->port($self->port);
|
||||||
return $uri;
|
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",
|
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('unquoted escaped backslash', "a=\\\\", { a => "\\" }, ""),
|
||||||
tc('squoted escaped squote', "a='\\''", { a => "'" }, ""),
|
tc('squoted escaped squote', "a='\\''", { a => "'" }, ""),
|
||||||
tc('dquoted escaped dquote', "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) {
|
for my $tc (@test_cases) {
|
||||||
my ($got_rest, %got_vars) = ddclient::parse_assignments($tc->{input});
|
my ($got_rest, %got_vars) = ddclient::parse_assignments($tc->{input});
|
||||||
subtest $tc->{name} => sub {
|
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); }
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
|
||||||
ok(ddclient::parse_version($ddclient::VERSION),
|
is(ddclient->VERSION(), version->parse('v@PACKAGE_VERSION@'), "version matches Autoconf config");
|
||||||
"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");
|
|
||||||
|
|
||||||
done_testing();
|
done_testing();
|
||||||
|
|
|
@ -35,7 +35,7 @@ my @test_cases = (
|
||||||
|
|
||||||
for my $tc (@test_cases) {
|
for my $tc (@test_cases) {
|
||||||
$warning = undef;
|
$warning = undef;
|
||||||
ddclient::write_recap($tc->{f});
|
ddclient::write_cache($tc->{f});
|
||||||
subtest $tc->{name} => sub {
|
subtest $tc->{name} => sub {
|
||||||
if (defined($tc->{warning_regex})) {
|
if (defined($tc->{warning_regex})) {
|
||||||
like($warning, $tc->{warning_regex}, "expected warning message");
|
like($warning, $tc->{warning_regex}, "expected warning message");
|
Loading…
Reference in a new issue