Merge branch 'ddclient:master' into develop

This commit is contained in:
Awalon 2023-08-09 04:17:43 +02:00 committed by GitHub
commit 13eb3de604
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 344 additions and 772 deletions

View file

@ -9,7 +9,7 @@ jobs:
matrix: matrix:
image: image:
- ubuntu:latest - ubuntu:latest
- ubuntu:16.04 - ubuntu:20.04
- debian:testing - debian:testing
- debian:stable - debian:stable
- debian:oldstable - debian:oldstable
@ -24,10 +24,9 @@ jobs:
automake \ automake \
ca-certificates \ ca-certificates \
git \ git \
curl \
libhttp-daemon-perl \ libhttp-daemon-perl \
libhttp-daemon-ssl-perl \ libhttp-daemon-ssl-perl \
libio-socket-inet6-perl \
libio-socket-ip-perl \
libplack-perl \ libplack-perl \
libtest-mockmodule-perl \ libtest-mockmodule-perl \
libtest-tcp-perl \ libtest-tcp-perl \
@ -48,28 +47,6 @@ jobs:
- name: distribution tarball is complete - name: distribution tarball is complete
run: ./.github/workflows/scripts/dist-tarball-check run: ./.github/workflows/scripts/dist-tarball-check
#test-centos6:
# runs-on: ubuntu-latest
# container: centos:6
# steps:
# - uses: actions/checkout@v1
# - name: install dependencies
# run: |
# yum install -y \
# automake \
# perl-IO-Socket-INET6 \
# perl-core \
# perl-libwww-perl \
# ;
# - 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-centos8: #test-centos8:
# runs-on: ubuntu-latest # runs-on: ubuntu-latest
# container: centos:8 # container: centos:8
@ -105,6 +82,7 @@ jobs:
automake \ automake \
findutils \ findutils \
make \ make \
curl \
perl \ perl \
perl-HTTP-Daemon \ perl-HTTP-Daemon \
perl-HTTP-Daemon-SSL \ perl-HTTP-Daemon-SSL \

View file

@ -3,29 +3,42 @@
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/master). repository history](https://github.com/ddclient/ddclient/commits/master).
## 2023-XX-XX v3.11.0 ## 2023-XX-XX v3.11.0_1
### Breaking changes ### Breaking changes
* ddclient now requires curl.
* ddclient no longer ships any example files for init systems that use `/etc/init.d`. * ddclient no longer ships any example files for init systems that use `/etc/init.d`.
This was done because those files where effectively unmaintained, untested by the developers and only updated by downstream distros. This was done because those files where effectively unmaintained, untested by the developers and only updated by downstream distros.
If you where relying on those files, please copy them into your packaging. If you where relying on those files, please copy them into your packaging.
* The defunct `dnsexit` protocol is removed (replaced by `dnsexit2`).
### New features ### New features
* Added support for domaindiscount24.com * Introduced `usev4` and `usev6` for separate IPv4/IPv6 configuration. These will replace the legacy `use` eventually.
* Added support for domeneshop.no * Added support for moving secrets out of the configuration through environment variables
* Added support for Enom * Extended postscript mechanism
* Added support for Mythic Beasts Dynamic DNS * sample-get-ip-from-fritzbox: Added environment variable to override hostname
* Added support for njal.la * Warn about hosts where no IP could be determined - and skip the (bogus) update.
* Added support for Porkbun
* Added support for IPv6 to the EasyDNS and DuckDNS provider ### Provider updates:
* Added regfish
* Added domeneshop.no
* Added Mythic Beasts
* Added Porkbun
* Added Enom
* Added DigitalOcean
* Added Infomaniak
* Added DNSExit API v2
* Removed old DNSExit API
* Extended EasyDNS to support IPv6
* Extended duckdns to support IPv6
### Bug fixes ### Bug fixes
* DynDNS2 now uses the newer ipv4/ipv6 syntax's * Fixed various issues with caching
* Fixed issues with Hetzner zones
* The OVH provider now ignores extra data returned * The OVH provider now ignores extra data returned
* Allow to define usev4 and usev6 options per hostname
* Merge multiple configs for the same hostname instead of use the last * Merge multiple configs for the same hostname instead of use the last
## 2022-10-20 v3.10.0 ## 2022-10-20 v3.10.0

View file

@ -63,7 +63,6 @@ AM_PL_LOG_FLAGS = -Mstrict -w \
-MDevel::Autoflush -MDevel::Autoflush
handwritten_tests = \ handwritten_tests = \
t/get_ip_from_if.pl \ t/get_ip_from_if.pl \
t/geturl_ssl.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 \

103
README.md
View file

@ -1,70 +1,68 @@
# Unmaintained
ddclient is unmaintained and no further changes will be done nor will issues or pull requests of any kind be accepted.
As alternatives consider <https://github.com/troglobit/inadyn> or <https://github.com/lopsided98/dnsupdate>.
There will be no support for migrating of ddclient and your current provider might not be supported by those alternatives.
See https://github.com/ddclient/ddclient/issues/528 and https://github.com/ddclient/ddclient/issues/380 for more details.
---
# DDCLIENT # DDCLIENT
`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. on many dynamic DNS services. It uses `curl` for internet access.
This is a friendly fork/continuation of https://github.com/ddclient/ddclient
## Alternatives
You might also want to consider using one of the following, if they support
your dynamic DNS provider(s): <https://github.com/troglobit/inadyn> or
<https://github.com/lopsided98/dnsupdate>.
## Supported services ## Supported services
Dynamic DNS services currently supported include: Dynamic DNS services currently supported include:
DynDNS.com - See http://www.dyndns.com for details on obtaining a free account. * [1984.is](https://www.1984.is/product/freedns)
Zoneedit - See http://www.zoneedit.com for details. * [ChangeIP](https://www.changeip.com)
EasyDNS - See http://www.easydns.com for details. * [CloudFlare](https://www.cloudflare.com)
NameCheap - See http://www.namecheap.com for details * [ClouDNS](https://www.cloudns.net)
DslReports - See http://www.dslreports.com for details * [dinahosting](https://dinahosting.com)
Sitelutions - See http://www.sitelutions.com for details * [DonDominio](https://www.dondominio.com)
Loopia - See http://www.loopia.se for details * [DNS Made Easy](https://dnsmadeeasy.com)
Noip - See http://www.noip.com/ for details * [DNSExit](https://dnsexit.com/dns/dns-api)
Freedns - See http://freedns.afraid.org/ for details * [domenehsop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
ChangeIP - See http://www.changeip.com/ for details * [DslReports](https://www.dslreports.com)
nsupdate - See nsupdate(1) and ddns-confgen(8) for details * [Duck DNS](https://duckdns.org)
CloudFlare - See https://www.cloudflare.com/ for details * [DynDNS.com](https://account.dyn.com)
GoDaddy - See https://www.godaddy.com/ for details * [EasyDNS](https://www.easydns.com )
Google - See http://www.google.com/domains for details * [Enom](https://www.enom.com)
Duckdns - See https://duckdns.org/ for details * [Freedns](https://freedns.afraid.org)
Freemyip - See https://freemyip.com for details * [Freemyip](https://freemyip.com)
woima.fi - See https://woima.fi/ for details * [Gandi](https://gandi.net)
Yandex - See https://domain.yandex.com/ for details * [GoDaddy](https://www.godaddy.com)
DNS Made Easy - See https://dnsmadeeasy.com/ for details * [Google](https://domains.google)
DonDominio - See https://www.dondominio.com for details * [Infomaniak](https://faq.infomaniak.com/2376)
NearlyFreeSpeech.net - See https://www.nearlyfreespeech.net/services/dns for details * [Loopia](https://www.loopia.se)
OVH - See https://www.ovh.com for details * [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
Porkbun - See https://porkbun.com/ * [NameCheap](https://www.namecheap.com)
ClouDNS - See https://www.cloudns.net * [NearlyFreeSpeech.net](https://www.nearlyfreespeech.net/services/dns)
dinahosting - See https://dinahosting.com * [Njalla](https://njal.la/docs/ddns)
Gandi - See https://gandi.net * [Noip](https://www.noip.com)
dnsexit - See https://dnsexit.com/ for details * nsupdate - see nsupdate(1) and ddns-confgen(8)
1984.is - See https://www.1984.is/product/freedns/ for details * [OVH](https://www.ovhcloud.com)
Njal.la - See https://njal.la/docs/ddns/ * [Porkbun](https://porkbun.com)
regfish.de - See https://www.regfish.de/domains/dyndns/ for details * [regfish.de](https://www.regfish.de/domains/dyndns)
domenehsop - See https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get * [Sitelutions](https://www.sitelutions.com)
Mythic Beasts - See https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns for details * [woima.fi](https://woima.fi)
Enom - See https://www.enom.com for details * [Yandex](https://dns.yandex.com)
Infomaniak - See https://faq.infomaniak.com/2376 for details * [Zoneedit](https://www.zoneedit.com)
`ddclient` now supports many cable and DSL broadband routers. `ddclient` supports finding your IP address from many cable and DSL
broadband routers.
Comments, suggestions and requests: use the issues on https://github.com/ddclient/ddclient/issues/new Comments, suggestions and requests: please file an issue at
https://github.com/ddclient/ddclient/issues/new
The code was originally written by Paul Burry and is now hosted and maintained The code was originally written by Paul Burry and is now hosted and
through github.com. Please check out http://ddclient.net maintained through github.com. Please check out https://ddclient.net
## REQUIREMENTS ## REQUIREMENTS
* An account from a supported dynamic DNS service provider * An account from a supported dynamic DNS service provider
* Perl v5.10.1 or later * Perl v5.10.1 or later
* `IO::Socket::SSL` perl library for ssl-support
* `JSON::PP` perl library for JSON support * `JSON::PP` perl library for JSON support
* Linux, macOS, or any other Unix-ish system * Linux, macOS, or any other Unix-ish system
* An implementation of `make` (such as [GNU * An implementation of `make` (such as [GNU
@ -85,8 +83,7 @@ See https://github.com/ddclient/ddclient/releases
<img src="https://repology.org/badge/vertical-allrepos/ddclient.svg" alt="Packaging status" align="right"> <img src="https://repology.org/badge/vertical-allrepos/ddclient.svg" alt="Packaging status" align="right">
</a> </a>
The easiest way to install ddclient is to install a package offered by your The easiest way to install ddclient is to install a package offered by your
operating system. See the image to the right for a list of distributions with a operating system. See the image to the right for a list of distributions with a ddclient package.
ddclient package.
### Manual Installation ### Manual Installation

View file

@ -28,6 +28,7 @@ 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_PATH_PROG([CURL], [curl]) AC_PATH_PROG([CURL], [curl])
AS_IF([test -z "${CURL}"], [AC_MSG_ERROR([curl not found])])
AX_WITH_PROG([PERL], perl) AX_WITH_PROG([PERL], perl)
AX_PROG_PERL_VERSION([5.10.1], [], AX_PROG_PERL_VERSION([5.10.1], [],
@ -43,7 +44,6 @@ m4_foreach_w([_m], [
File::Path File::Path
File::Temp File::Temp
Getopt::Long Getopt::Long
IO::Socket::IP
Socket Socket
Sys::Hostname Sys::Hostname
version=0.77 version=0.77
@ -71,7 +71,6 @@ m4_foreach_w([_m], [
HTTP::Message::PSGI HTTP::Message::PSGI
HTTP::Request HTTP::Request
HTTP::Response HTTP::Response
IO::Socket::SSL
Scalar::Util Scalar::Util
Test::MockModule Test::MockModule
Test::TCP Test::TCP

View file

@ -327,6 +327,13 @@ ssl=yes # use ssl-support. Works with
#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)
##
#protocol=dnsexit2
#password=MyAPIKey
#subdomain-1.domain.com,subdomain-2.domain.com
## ##
## domeneshop (www.domeneshop.no) ## domeneshop (www.domeneshop.no)
## ##

View file

@ -3,19 +3,12 @@
# #
# DDCLIENT - a Perl client for updating DynDNS information # DDCLIENT - a Perl client for updating DynDNS information
# #
# Author: Paul Burry (paul+ddclient@burry.ca) # Original Author: Paul Burry (paul+ddclient@burry.ca)
# ddclient developers: see https://github.com/orgs/ddclient/people # Current maintainers:
# # Reuben Thomas <rrt@sc3d.org>
# website: https://ddclient.net # Lenard Heß <lenard@rrhess.de>
#
# Support for multiple IP numbers added by
# Astaro AG, Ingo Schwarze <ischwarze-OOs/4mkCeqbQT0dZR+AlfA@public.gmane.org> September 16, 2008
#
# Support for multiple domain support for Namecheap by Robert Ian Hawdon 2010-09-03: https://robertianhawdon.me.uk/
#
# Initial Cloudflare support by Ian Pye, updated by Robert Ian Hawdon 2012-07-16
# Further updates by Peter Roberts to support the new API 2013-09-26, 2014-06-22: http://blog.peter-r.co.uk/
# #
# website: https://github.com/ddclient/ddclient
# #
###################################################################### ######################################################################
package ddclient; package ddclient;
@ -26,8 +19,6 @@ use File::Basename;
use File::Path qw(make_path); use File::Path qw(make_path);
use File::Temp; use File::Temp;
use Getopt::Long; use Getopt::Long;
use IO::Socket::IP;
use Socket qw(AF_INET AF_INET6 PF_INET PF_INET6);
use Sys::Hostname; use Sys::Hostname;
use version 0.77; our $VERSION = version->declare('@PACKAGE_VERSION@'); use version 0.77; our $VERSION = version->declare('@PACKAGE_VERSION@');
@ -464,7 +455,6 @@ my %variables = (
'retry' => setv(T_BOOL, 0, 0, 0, undef), 'retry' => setv(T_BOOL, 0, 0, 0, undef),
'force' => setv(T_BOOL, 0, 0, 0, undef), 'force' => setv(T_BOOL, 0, 0, 0, undef),
'ssl' => setv(T_BOOL, 0, 0, 0, undef), 'ssl' => setv(T_BOOL, 0, 0, 0, undef),
'curl' => setv(T_BOOL, 0, 0, 0, undef),
'syslog' => setv(T_BOOL, 0, 0, 0, undef), 'syslog' => setv(T_BOOL, 0, 0, 0, undef),
'facility' => setv(T_STRING,0, 0, 'daemon', undef), 'facility' => setv(T_STRING,0, 0, 'daemon', undef),
'priority' => setv(T_STRING,0, 0, 'notice', undef), 'priority' => setv(T_STRING,0, 0, 'notice', undef),
@ -543,11 +533,12 @@ my %variables = (
'server' => setv(T_FQDNP, 1, 0, 'dynamicdns.key-systems.net', undef), 'server' => setv(T_FQDNP, 1, 0, 'dynamicdns.key-systems.net', undef),
'login' => setv(T_LOGIN, 0, 0, 0, 'unused', undef), 'login' => setv(T_LOGIN, 0, 0, 0, 'unused', undef),
}, },
'dnsexit-common-defaults' => { 'dnsexit2-common-defaults' => {
'ssl' => setv(T_BOOL, 0, 0, 1, undef), 'ssl' => setv(T_BOOL, 0, 0, 1, undef),
'server' => setv(T_FQDNP, 1, 0, 'update.dnsexit.com', undef), 'server' => setv(T_FQDNP, 1, 0, 'api.dnsexit.com', undef),
'script' => setv(T_STRING, 0, 1, '/RemoteUpdate.sv', undef), 'path' => setv(T_STRING, 0, 1, '/dns/', undef),
'min-error-interval' => setv(T_DELAY, 0, 0, interval('8m'), 0), 'record-type' => setv(T_STRING, 1, 0, 'A', undef),
'ttl' => setv(T_NUMBER, 1, 0, 5, 0),
}, },
'regfishde-common-defaults' => { 'regfishde-common-defaults' => {
'server' => setv(T_FQDNP, 1, 0, 'dyndns.regfish.de', undef), 'server' => setv(T_FQDNP, 1, 0, 'dyndns.regfish.de', undef),
@ -951,14 +942,18 @@ my %services = (
$variables{'service-common-defaults'}, $variables{'service-common-defaults'},
), ),
}, },
'dnsexit' => { 'dnsexit2' => {
'updateable' => undef, 'updateable' => undef,
'update' => \&nic_dnsexit_update, 'update' => \&nic_dnsexit2_update,
'examples' => \&nic_dnsexit_examples, 'examples' => \&nic_dnsexit2_examples,
'variables' => merge( 'variables' => {
$variables{'dnsexit-common-defaults'}, %{$variables{'service-common-defaults'}},
$variables{'service-common-defaults'}, %{$variables{'dnsexit2-common-defaults'}},
), # nic_updateable() assumes that every service uses a username/login but that is
# not true for the DNSExit API. Silence warnings by redefining the username variable
# as non-required with value unused.
'login' => setv(T_STRING, 0, 0, 'unused', undef),
},
}, },
'regfishde' => { 'regfishde' => {
'updateable' => undef, 'updateable' => undef,
@ -1080,7 +1075,6 @@ my @opt = (
["ssl_ca_file", "=s", "-ssl_ca_file <file> : look at <file> for certificates of trusted certificate authorities (default: auto-detect)"], ["ssl_ca_file", "=s", "-ssl_ca_file <file> : look at <file> for certificates of trusted certificate authorities (default: auto-detect)"],
["fw-ssl-validate", "!", "-{no}fw-ssl-validate : Validate SSL certificate when retrieving IP address from firewall"], ["fw-ssl-validate", "!", "-{no}fw-ssl-validate : Validate SSL certificate when retrieving IP address from firewall"],
["web-ssl-validate", "!","-{no}web-ssl-validate : Validate SSL certificate when retrieving IP address from web"], ["web-ssl-validate", "!","-{no}web-ssl-validate : Validate SSL certificate when retrieving IP address from web"],
["curl", "!", "-{no}curl : use curl for network connections"],
["retry", "!", "-{no}retry : retry failed updates"], ["retry", "!", "-{no}retry : retry failed updates"],
["force", "!", "-{no}force : force an update even if the update may be unnecessary"], ["force", "!", "-{no}force : force an update even if the update may be unnecessary"],
["timeout", "=i", "-timeout <max> : when fetching a URL, wait at most <max> seconds for a response"], ["timeout", "=i", "-timeout <max> : when fetching a URL, wait at most <max> seconds for a response"],
@ -1335,6 +1329,12 @@ sub update_nics {
# But we will set 'wantip' to the IPv4 so old functions continue to work until we update them all # But we will set 'wantip' to the IPv4 so old functions continue to work until we update them all
$config{$h}{'wantip'} = $ipv4 if (!$ip && $ipv4); $config{$h}{'wantip'} = $ipv4 if (!$ip && $ipv4);
if (!$ip && !$ipv4 && !$ipv6)
{
warning("Could not determine an IP for %s", $h);
next;
}
next if !nic_updateable($h, $updateable); next if !nic_updateable($h, $updateable);
push @hosts, $h; push @hosts, $h;
@ -1344,6 +1344,18 @@ sub update_nics {
if (@hosts) { if (@hosts) {
$0 = sprintf("%s - updating %s", $program, join(',', @hosts)); $0 = sprintf("%s - updating %s", $program, join(',', @hosts));
&$update(@hosts); &$update(@hosts);
# Backwards compatibility:
# If we only have 'use', we set 'wantipv4' or 'wantipv6' depending on the IP type of
# 'wantip'. Newer provider implementations such as cloudflare only check 'wantipv*'
# and set 'status-ipv*' accordingly, ignoring 'wantip' and 'status'.
# For these we then load back the 'status' from 'status-ipv*' to ensure correct
# caching and updating behaviour.
foreach my $h (@hosts) {
$config{$h}{'status'} //= $config{$h}{'status-ipv4'};
$config{$h}{'status'} //= $config{$h}{'status-ipv6'};
}
runpostscript(join ' ', keys %ipsv4, keys %ipsv6); runpostscript(join ' ', keys %ipsv4, keys %ipsv6);
} }
} }
@ -1595,7 +1607,7 @@ sub _read_config {
$content .= "$_\n" unless /^#/; $content .= "$_\n" unless /^#/;
## parsing passwords is special ## parsing passwords is special
if (/^([^#]*\s)?([^#]*?password\S*?)\s*=\s*('.*'|[^']\S*)(.*)/) { if (/^([^#]*\s)?([^#]*?password)\s*=\s*('.*'|[^']\S*)(.*)/) {
my ($head, $key, $value, $tail) = ($1 // '', $2, $3, $4); my ($head, $key, $value, $tail) = ($1 // '', $2, $3, $4);
$value = $1 if $value =~ /^'(.*)'$/; $value = $1 if $value =~ /^'(.*)'$/;
$passwords{$key} = $value; $passwords{$key} = $value;
@ -1626,6 +1638,25 @@ sub _read_config {
## verify that keywords are valid...and check the value ## verify that keywords are valid...and check the value
foreach my $k (keys %locals) { foreach my $k (keys %locals) {
# Handle '_env' keyword suffix
if ($k =~ /(.*)_env$/)
{
debug("Loading value for $1 from environment variable $locals{$k}.");
if (exists($ENV{$locals{$k}}))
{
# Set the value to the value of the environment variable
$locals{$1} = $ENV{$locals{$k}};
# Remove the '_env' suffix from the key
$k = $1;
}
else
{
warning("Environment variable '$locals{$k}' not set for keyword '$k' (ignored)");
delete $locals{$k};
next;
}
}
$locals{$k} = $passwords{$k} if defined $passwords{$k}; $locals{$k} = $passwords{$k} if defined $passwords{$k};
if (!exists $variables{'merged'}{$k}) { if (!exists $variables{'merged'}{$k}) {
warning("unrecognized keyword '%s' (ignored)", $k); warning("unrecognized keyword '%s' (ignored)", $k);
@ -1816,7 +1847,7 @@ sub init_config {
$proto = opt('protocol') if !defined($proto); $proto = opt('protocol') if !defined($proto);
load_sha1_support($proto) if (grep (/^$proto$/, ("freedns", "nfsn"))); load_sha1_support($proto) if (grep (/^$proto$/, ("freedns", "nfsn")));
load_json_support($proto) if (grep (/^$proto$/, ("1984", "cloudflare", "digitalocean", "gandi", "godaddy", "hetzner", "yandex", "nfsn", "njalla", "porkbun"))); load_json_support($proto) if (grep (/^$proto$/, ("1984", "cloudflare", "digitalocean", "gandi", "godaddy", "hetzner", "yandex", "nfsn", "njalla", "porkbun", "dnsexit2")));
if (!exists($services{$proto})) { if (!exists($services{$proto})) {
warning("skipping host: %s: unrecognized protocol '%s'", $h, $proto); warning("skipping host: %s: unrecognized protocol '%s'", $h, $proto);
@ -2431,22 +2462,6 @@ sub encode_base64 ($;$) {
$res =~ s/.{$padding}$/'=' x $padding/e if $padding; $res =~ s/.{$padding}$/'=' x $padding/e if $padding;
$res; $res;
} }
######################################################################
## load_ssl_support
######################################################################
sub load_ssl_support {
my $ssl_loaded = eval { require IO::Socket::SSL };
unless ($ssl_loaded) {
fatal("%s", <<"EOM");
Error loading the Perl module IO::Socket::SSL needed for SSL connect.
On Debian, the package libio-socket-ssl-perl must be installed.
On Red Hat, the package perl-IO-Socket-SSL must be installed.
On Alpine, the package perl-io-socket-ssl must be installed.
EOM
}
import IO::Socket::SSL;
{ no warnings; $IO::Socket::SSL::DEBUG = 0; }
}
###################################################################### ######################################################################
## load_sha1_support ## load_sha1_support
@ -2481,180 +2496,6 @@ EOM
import JSON::PP (qw/decode_json encode_json/); import JSON::PP (qw/decode_json encode_json/);
} }
######################################################################
## geturl
######################################################################
sub geturl {
return opt('curl') ? fetch_via_curl(@_) : fetch_via_socket_io(@_);
}
sub fetch_via_socket_io {
my %params = @_;
my $proxy = $params{proxy};
my $url = $params{url};
my $login = $params{login};
my $password = $params{password};
my $ipversion = $params{ipversion} // '';
my $headers = $params{headers} // '';
my $method = $params{method} // 'GET';
my $data = $params{data} // '';
my ($peer, $server, $port, $default_port, $use_ssl);
my ($sd, $request, $reply);
## canonify proxy and url
my $force_ssl;
$force_ssl = 1 if ($url =~ /^https:/);
$proxy =~ s%^https?://%%i if defined($proxy);
$url =~ s%^https?://%%i;
$server = $url;
$server =~ s%[?/].*%%;
$url =~ s%^[^?/]*/?%%;
if ($force_ssl || ($globals{'ssl'} && !($params{ignore_ssl_option} // 0))) {
$use_ssl = 1;
$default_port = '443';
} else {
$use_ssl = 0;
$default_port = '80';
}
debug("proxy = %s", $proxy // '<undefined>');
debug("protocol = %s", $use_ssl ? "https" : "http");
debug("server = %s", $server);
(my $_url = $url) =~ s%\?.*%?<redacted>%; #redact ALL parameters passed on URL, including possible passwords
debug("url = %s", $_url);
debug("ip ver = %s", $ipversion);
## determine peer and port to use.
$peer = $proxy // $server;
$peer =~ s%[?/].*%%;
if ($peer =~ /^\[([^]]+)\](?::(\d+))?$/ || $peer =~ /^([^:]+)(?::(\d+))?/) {
$peer = $1;
$port = $2 // $default_port;
} else {
failed("unable to extract host and port from %s", $peer);
return undef;
}
$request = "$method ";
if (!$use_ssl) {
$request .= "http://$server" if defined($proxy);
} else {
$request .= "https://$server" if defined($proxy);
}
$request .= "/$url HTTP/1.1\n";
$request .= "Host: $server\n";
if (defined($login) || defined($password)) {
my $auth = encode_base64(($login // '') . ':' . ($password // ''), '');
$request .= "Authorization: Basic $auth\n";
}
$request .= "User-Agent: ${program}/${version}\n";
if ($data) {
$request .= "Content-Type: application/x-www-form-urlencoded\n" if $headers !~ /^Content-Type:/mi;
$request .= "Content-Length: " . length($data) . "\n";
}
$request .= "Connection: close\n";
$headers .= "\n" if $headers ne '' && substr($headers, -1) ne "\n";
$request .= $headers;
$request .= "\n";
# RFC 7230 says that all lines before the body must end with <cr><lf>.
(my $rq = $request) =~ s/(?<!\r)\n/\r\n/g;
$request .= $data;
$rq .= $data;
my %socket_args = (
PeerAddr => $peer,
PeerPort => $port,
Proto => 'tcp',
Timeout => opt('timeout'),
);
my $socket_class = 'IO::Socket::IP';
if ($use_ssl) {
# IO::Socket::SSL will load IPv6 support if available on the system.
load_ssl_support;
$socket_class = 'IO::Socket::SSL';
$socket_args{SSL_ca_file} = opt('ssl_ca_file') if defined(opt('ssl_ca_file'));
$socket_args{SSL_ca_path} = opt('ssl_ca_dir') if defined(opt('ssl_ca_dir'));
$socket_args{SSL_verify_mode} = ($params{ssl_validate} // 1)
? IO::Socket::SSL->SSL_VERIFY_PEER
: IO::Socket::SSL->SSL_VERIFY_NONE;
}
if (defined($params{_testonly_socket_class})) {
$socket_args{original_socket_class} = $socket_class;
$socket_class = $params{_testonly_socket_class};
}
if ($ipversion eq '4') {
$socket_args{Domain} = PF_INET;
$socket_args{Family} = AF_INET;
} elsif ($ipversion eq '6') {
$socket_args{Domain} = PF_INET6;
$socket_args{Family} = AF_INET6;
} elsif ($ipversion ne '') {
fatal("geturl passed unsupported 'ipversion' value %s", $ipversion);
}
my $ipv = $ipversion eq '' ? '' : sprintf(" (IPv%s)", $ipversion);
my $peer_port_ipv = sprintf("%s:%s%s", $peer, $port, $ipv);
my $to = sprintf("%s%s%s", $server, defined($proxy) ? " via proxy $peer:$port" : "", $ipv);
verbose("CONNECT:", "%s", $to);
$0 = sprintf("%s - connecting to %s", $program, $peer_port_ipv);
if (opt('exec')) {
$sd = $socket_class->new(%socket_args);
defined($sd) or warning("cannot connect to %s socket: %s%s", $peer_port_ipv, $@,
$use_ssl ? ' ' . IO::Socket::SSL::errstr() : '');
} else {
debug("skipped network connection");
verbose("SENDING:", "%s", $request);
}
if (defined $sd) {
## send the request to the http server
verbose("CONNECTED: ", $use_ssl ? 'using SSL' : 'using HTTP');
verbose("SENDING:", "%s", $request);
$0 = sprintf("%s - sending to %s", $program, $peer_port_ipv);
my $result = syswrite $sd, $rq;
if ($result != length($rq)) {
warning("cannot send to %s (%s).", $peer_port_ipv, $!);
} else {
$0 = sprintf("%s - reading from %s", $program, $peer_port_ipv);
eval {
local $SIG{'ALRM'} = sub { die "timeout"; };
alarm(opt('timeout')) if opt('timeout') > 0;
while ($_ = <$sd>) {
$0 = sprintf("%s - read from %s", $program, $peer_port_ipv);
verbose("RECEIVE:", "%s", $_ // "<undefined>");
$reply .= $_ // '';
}
if (opt('timeout') > 0) {
alarm(0);
}
};
close($sd);
if ($@ and $@ =~ /timeout/) {
warning("TIMEOUT: %s after %s seconds", $to, opt('timeout'));
$reply = '';
}
$reply //= '';
}
}
$0 = sprintf("%s - closed %s", $program, $peer_port_ipv);
## during testing simulate reading the URL
if (opt('test')) {
my $filename = "$server/$url";
$filename =~ s|/|%2F|g;
if (opt('exec')) {
$reply = save_file("$savedir/$filename", $reply, 'unique');
} else {
$reply = load_file("$savedir/$filename");
}
}
$reply =~ s/\r//g if defined $reply;
return $reply;
}
###################################################################### ######################################################################
## curl_cmd() function to execute system curl command ## curl_cmd() function to execute system curl command
###################################################################### ######################################################################
@ -2680,7 +2521,7 @@ sub curl_cmd {
67 => "The user name, password, or similar was not accepted and curl failed to log in.", 67 => "The user name, password, or similar was not accepted and curl failed to log in.",
77 => "Problem with reading the SSL CA cert (path? access rights?).", 77 => "Problem with reading the SSL CA cert (path? access rights?).",
78 => "The resource referenced in the URL does not exist.", 78 => "The resource referenced in the URL does not exist.",
127 => "You requested network access with curl but $system_curl was not found", 127 => "$system_curl was not found",
); );
debug("CURL: %s", $system_curl); debug("CURL: %s", $system_curl);
@ -2724,10 +2565,7 @@ sub escape_curl_param {
return $str; return $str;
} }
###################################################################### sub geturl {
## fetch_via_curl() is used for geturl() when global curl option set
######################################################################
sub fetch_via_curl {
my %params = @_; my %params = @_;
my $proxy = $params{proxy}; my $proxy = $params{proxy};
my $url = $params{url}; my $url = $params{url};
@ -2770,59 +2608,6 @@ sub fetch_via_curl {
debug("skipped network connection"); debug("skipped network connection");
verbose("SENDING:", "%s", "${server}/${url}"); verbose("SENDING:", "%s", "${server}/${url}");
} else { } else {
my $curl_loaded = eval { require WWW::Curl::Easy };
if ($curl_loaded) {
# System has the WWW::Curl::Easy module so use that
import WWW::Curl::Easy;
my $curl = WWW::Curl::Easy->new;
$curl->setopt(WWW::Curl::Easy->CURLOPT_HEADER, 1); ## Include HTTP response for compatibility
$curl->setopt(WWW::Curl::Easy->CURLOPT_SSL_VERIFYPEER, ($params{ssl_validate} // 1) ? 1 : 0 );
$curl->setopt(WWW::Curl::Easy->CURLOPT_SSL_VERIFYHOST, ($params{ssl_validate} // 1) ? 1 : 0 );
$curl->setopt(WWW::Curl::Easy->CURLOPT_CAINFO, opt('ssl_ca_file')) if defined(opt('ssl_ca_file'));
$curl->setopt(WWW::Curl::Easy->CURLOPT_CAPATH, opt('ssl_ca_dir')) if defined(opt('ssl_ca_dir'));
$curl->setopt(WWW::Curl::Easy->CURLOPT_IPRESOLVE,
($ipversion == 4) ? WWW::Curl::Easy->CURL_IPRESOLVE_V4 :
($ipversion == 6) ? WWW::Curl::Easy->CURL_IPRESOLVE_V6 :
WWW::Curl::Easy->CURL_IPRESOLVE_WHATEVER);
$curl->setopt(WWW::Curl::Easy->CURLOPT_USERAGENT, "${program}/${version}");
$curl->setopt(WWW::Curl::Easy->CURLOPT_CONNECTTIMEOUT, $timeout);
$curl->setopt(WWW::Curl::Easy->CURLOPT_TIMEOUT, $timeout);
$curl->setopt(WWW::Curl::Easy->CURLOPT_POST, 1) if ($method eq 'POST');
$curl->setopt(WWW::Curl::Easy->CURLOPT_PUT, 1) if ($method eq 'PUT');
$curl->setopt(WWW::Curl::Easy->CURLOPT_CUSTOMREQUEST, $method) if ($method ne 'GET'); ## for PATCH
$curl->setopt(WWW::Curl::Easy->CURLOPT_USERPWD, "${login}:${password}") if (defined($login) && defined($password));
$curl->setopt(WWW::Curl::Easy->CURLOPT_PROXY, "${protocol}://${proxy}") if defined($proxy);
$curl->setopt(WWW::Curl::Easy->CURLOPT_URL, "${protocol}://${server}/${url}");
# Add header lines if any was provided
if ($headers) {
@header_lines = split('\n', $headers);
$curl->setopt(WWW::Curl::Easy->CURLOPT_HTTPHEADER, \@header_lines);
}
# Add in the data if any was provided (for POST/PATCH)
if (my $datalen = length($data)) {
$curl->setopt(WWW::Curl::Easy->CURLOPT_POSTFIELDS, ${data});
$curl->setopt(WWW::Curl::Easy->CURLOPT_POSTFIELDSIZE, $datalen);
}
$curl->setopt(WWW::Curl::Easy->CURLOPT_WRITEDATA,\$reply);
# don't include ${url} as that might expose login credentials
$0 = sprintf("%s - WWW::Curl::Easy sending to %s", $program, "${protocol}://${server}");
verbose("SENDING:", "WWW::Curl::Easy to %s", "${protocol}://${server}");
verbose("SENDING:", "%s", $headers) if ($headers);
verbose("SENDING:", "%s", $data) if ($data);
my $rc = $curl->perform;
if ($rc != 0) {
warning("CURL error (%d) %s", $rc, $curl->strerror($rc));
debug($curl->errbuf);
}
} else {
# System does not have the WWW::Curl::Easy module so attempt with system Curl command
push(@curlopt, "silent"); push(@curlopt, "silent");
push(@curlopt, "include"); ## Include HTTP response for compatibility push(@curlopt, "include"); ## Include HTTP response for compatibility
push(@curlopt, "insecure") if ($use_ssl && !($params{ssl_validate} // 1)); push(@curlopt, "insecure") if ($use_ssl && !($params{ssl_validate} // 1));
@ -2852,7 +2637,6 @@ sub fetch_via_curl {
verbose("SENDING:", "%s", $_) foreach (@curlopt); verbose("SENDING:", "%s", $_) foreach (@curlopt);
$reply = curl_cmd(@curlopt); $reply = curl_cmd(@curlopt);
}
verbose("RECEIVE:", "%s", $reply // "<undefined>"); verbose("RECEIVE:", "%s", $reply // "<undefined>");
if (!$reply) { if (!$reply) {
# don't include ${url} as that might expose login credentials # don't include ${url} as that might expose login credentials
@ -3919,6 +3703,25 @@ sub header_ok {
} }
return $ok; return $ok;
} }
######################################################################
## DDNS providers
# A DDNS provider consists of an example function, the update
# function, and an optional updateable function.
#
# The example function simply returns a string for the help message,
# explaining how to configure the provider
#
# The update function performs the actual record update.
# It receives an array of hosts as its argument.
#
# The updateable function allows a provider implementation to force
# an update even if ddclient has itself determined no update is
# necessary. The function shall return 1 if an update should be
# performed, else 0.
######################################################################
###################################################################### ######################################################################
## nic_dyndns1_examples ## nic_dyndns1_examples
###################################################################### ######################################################################
@ -4242,113 +4045,169 @@ sub nic_dyndns2_update {
} }
###################################################################### ######################################################################
## nic_dnsexit_examples ## nic_dnsexit2_examples
###################################################################### ######################################################################
sub nic_dnsexit_examples { sub nic_dnsexit2_examples {
return <<"EoEXAMPLE"; return <<"EoEXAMPLE";
o 'dnsexit' o 'dnsexit2'
The 'dnsexit' protocol is the protocol used by the dynamic hostname services The 'dnsexit2' protocol is the new API protocol used by the dynamic hostname services
of the 'DnsExit' dns services. This is currently used by the free of the 'DNSExit' dns services. This is currently used by the free
dynamic DNS service offered by www.dnsexit.com. dynamic DNS service offered by www.dnsexit.com.
Configuration variables applicable to the 'dnsexit' protocol are: Configuration variables applicable to the 'dnsexit2' protocol are:
ssl=no ## turn off ssl protocol=dnsexit2 ##
protocol=dnsexit ## password=YourAPIKey ## API Key of your account.
server=update.dnsexit.com ## defaults to update.dnsexit.com server=api.dnsexit.com ## defaults to api.dnsexit.com.
use=web ## defaults to web path=/dns/ ## defaults to /dns/.
web=update.dnsexit.com ## defaults to update.dnsexit.com record-type=A ## defaults to A record.
script=/RemoteUpdate.sv ## defaults to /RemoteUpdate.sv ttl=5 ## defaults to 5 minutes.
login=service-userid ## userid registered with the service
password=service-password ## password registered with the service
fully.qualified.host ## the host registered with the service. fully.qualified.host ## the host registered with the service.
Example ${program}.conf file entries: Example ${program}.conf file entries:
## single host update ## single host update
protocol=dnsexit \\ protocol=dnsexit2
login=service-userid \\ password=YourAPIKey
password=service-password \\
fully.qualified.host fully.qualified.host
EoEXAMPLE EoEXAMPLE
} }
###################################################################### ######################################################################
## nic_dnsexit_update ## nic_dnsexit2_update
## ##
## written by Gonzalo Pérez de Olaguer Córdoba <salo@gpoc.es> ## by @jortkoopmans
## ## based on https://dnsexit.com/dns/dns-api/
## based on https://www.dnsexit.com/Direct.sv?cmd=ipClients
## fetches this URL to update:
## https://update.dnsexit.com/RemoteUpdate.sv?login=yourlogin&password=yourpassword&
## host=yourhost.yourdomain.com&myip=xxx.xx.xx.xxx
## ##
###################################################################### ######################################################################
sub nic_dnsexit_update { sub nic_dnsexit2_update {
debug("\nnic_dnsexit_update -------------------"); debug("\nnic_dnsexit2_update -------------------");
my %status = ( ## Update each configured host
'0' => [ 'good', 'Success' ],
'1' => [ 'nochg', 'IP is the same as the IP on the system' ],
'2' => [ 'badauth', 'Invalid password' ],
'3' => [ 'badauth', 'User not found' ],
'4' => [ 'nochg', 'IP not changed. To save our system resources, please don\'t post updates unless the IP got changed.' ],
'10' => [ 'error', 'Hostname is not specified' ],
'11' => [ 'nohost', 'fail to find the domain' ],
'13' => [ 'error', 'parameter validation error' ],
);
## update each configured host
foreach my $h (@_) { foreach my $h (@_) {
# All the known status
my %status = (
'0' => [ 'good', 'Success! Actions got executed successfully.' ],
'1' => [ 'warning', 'Some execution problems. May not indicate actions failures. Some action may got executed fine and some may have problems.' ],
'2' => [ 'badauth', 'API Key Authentication Error. The API Key is missing or wrong.' ],
'3' => [ 'error', 'Missing Required Definitions. Your JSON file may missing some required definitions.' ],
'4' => [ 'error', 'JSON Data Syntax Error. Your JSON file has syntax error.' ],
'5' => [ 'error', 'JSON Defined Record Type not Supported. Your JSON may try to update some record type not supported by our system.' ],
'6' => [ 'error', 'System Error. Our system problem. May not be your problem. Contact our support if you got such error.' ],
'7' => [ 'error', 'Error getting post data. Our server has problem to receive your JSON posting.' ],
);
my $ip = delete $config{$h}{'wantip'}; my $ip = delete $config{$h}{'wantip'};
info("setting IP address to %s for %s", $ip, $h); info("Going to update IP address to %s for %s.", $ip, $h);
verbose("UPDATE:","updating %s", $h); # Set the URL of the API endpoint
my $url = "https://$config{$h}{'server'}$config{$h}{'path'}";
# Set the URL that we're going to update # Set JSON payload
my $url; my $data = encode_json({
$url = "https://$config{$h}{'server'}$config{$h}{'script'}"; apikey => $config{$h}{'password'},
$url .= "?login=$config{$h}{'login'}"; domain => $h,
$url .= "&password=$config{$h}{'password'}"; update => {
$url .= "&host=$h"; type => $config{$h}{'record-type'},
$url .= "&myip="; name => $h,
$url .= $ip if $ip; content => $ip,
ttl => $config{$h}{'ttl'}},
});
# Try to get URL # Set additional headers
my $header = "Content-Type: application/json\n";
$header .= "Accept: application/json";
# Make the call
my $reply = geturl( my $reply = geturl(
proxy => opt('proxy'), proxy => opt('proxy'),
url => $url url => $url,
headers => $header,
method => 'POST',
data => $data,
); );
# No response, declare as failed # No reply, declare as failed
if (!defined($reply) || !$reply) { if (!defined($reply) || !$reply) {
failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'}); failed("updating %s: Could not connect to %s%s.", $h, $config{$h}{'server'}, $config{$h}{'path'});
$config{$h}{'status'} = 'failed';
last;
};
# Reply found
debug("%s", $reply);
# $ok is mandatory?
my $ok = header_ok($h, $reply);
# Extract the HTTP response code
(my $http_status) = ($reply =~ m%^s*HTTP/.*\s+(\d+)%i);
debug("HTTP response code: %s", $http_status);
# If not 200, bail
if ( $http_status != "200"){
failed("Failed to update Host\n%s to IP:%s", $h, $ip);
failed("HTTP response code\n%s", $http_status);
failed("Full reply\n%s", $reply) unless opt('verbose');
$config{$h}{'status'} = 'failed';
last; last;
} }
last if !header_ok($h, $reply);
# Response found # Strip HTTP response headers
if ($reply =~ /(\d+)=(.+)/) { (my $strip_status) = ($reply =~ s/^[\s\S]*?(?=\{"code":)//);
my ($statuscode, $statusmsg) = ($1, $2); debug("strip_status");
if (exists $status{$statuscode}) { debug("%s", $strip_status);
my ($status, $message) = @{ $status{$statuscode} }; if ($strip_status) {
if ($status =~ m'^(good|nochg)$') { debug("HTTP headers are stripped.");
}
else {
warning("Unexpected: no HTTP headers stripped!");
}
# Decode the remaining reply, it should be JSON.
my $response = decode_json($reply);
# It should at least have a 'code' and 'message'.
if (defined($response->{'code'}) and defined($response->{'message'})) {
if (exists $status{$response->{'code'}}) {
# Add the server response data to the applicable array
push( @{ $status {$response->{'code'} } }, $response->{'message'});
if (defined($response->{'details'})) {
push ( @{ $status {$response->{'code'} } }, $response->{'details'}[0]);
} else {
# Keep it symmetrical for simplicity
push ( @{ $status {$response->{'code'} } }, "no details received");
}
# Set data from array
my ($status, $message, $srv_message, $srv_details) = @{ $status {$response->{'code'} } };
info("Status: %s -- Message: %s", $status, $message);
info("Server Message: %s -- Server Details: %s", $srv_message, $srv_details);
$config{$h}{'status'} = $status;
# Handle statuses
if ($status eq 'good') {
$config{$h}{'ip'} = $ip; $config{$h}{'ip'} = $ip;
$config{$h}{'mtime'} = $now; $config{$h}{'mtime'} = $now;
} $config{$h}{'status'} = 'good';
$config{$h}{'status'} = $status; success("%s", $message);
if ($status eq 'good') { success("Updated %s successfully to IP address %s at time %s", $h, $ip, prettytime($config{$h}{'mtime'}));
success("updating %s: good: IP address set to %s", $h, $ip); } elsif ($status eq 'warning') {
} else { warning("%s", $message);
warning("updating %s: %s: %s", $h, $status, $message); warning("Server response: %s", $srv_message);
} } elsif ($status =~ m'^(badauth|error)$') {
} else { failed("%s", $message);
failed("Server response: %s", $srv_message);
$config{$h}{'status'} = 'failed';
} else {
failed("This should not be possible");
$config{$h}{'status'} = 'failed'; $config{$h}{'status'} = 'failed';
failed("updating %s: failed: unrecognized status code (%s)", $h, $statuscode);
} }
} else { } else {
failed("Status code %s is unknown!", $response->{'code'});
$config{$h}{'status'} = 'failed';
}
} else {
failed("Did not receive expected \"code\" and \"message\" keys in server response.");
failed("Response:");
failed("%s", $response);
$config{$h}{'status'} = 'failed'; $config{$h}{'status'} = 'failed';
warning("SENT: %s", $url) unless opt('verbose');
warning("REPLIED: %s", $reply);
failed("updating %s: unrecognized reply.", $h);
} }
} }
} }
@ -4895,7 +4754,8 @@ sub nic_easydns_update {
my ($status) = $line =~ /^(\S*)\b.*/; my ($status) = $line =~ /^(\S*)\b.*/;
my $h = shift @hosts; my $h = shift @hosts;
$config{$h}{'status'} = $status; $config{$h}{'status-ipv4'} = $status if $ipv4;
$config{$h}{'status-ipv6'} = $status if $ipv6;
if ($status eq 'NOERROR') { if ($status eq 'NOERROR') {
$config{$h}{'ipv4'} = $ipv4; $config{$h}{'ipv4'} = $ipv4;
$config{$h}{'ipv6'} = $ipv6; $config{$h}{'ipv6'} = $ipv6;
@ -7235,12 +7095,12 @@ sub nic_porkbun_update {
); );
# No response, declare as failed # No response, declare as failed
if (!defined($reply) || !$reply) { if (!defined($reply) || !$reply) {
$config{$host}{'status'} = "bad"; $config{$host}{'status-ipv4'} = "bad";
failed("updating %s: Could not connect to porkbun.com.", $host); failed("updating %s: Could not connect to porkbun.com.", $host);
next; next;
} }
if (!header_ok($host, $reply)) { if (!header_ok($host, $reply)) {
$config{$host}{'status'} = "bad"; $config{$host}{'status-ipv4'} = "bad";
failed("updating %s: failed (%s)", $host, $reply); failed("updating %s: failed (%s)", $host, $reply);
next; next;
} }
@ -7249,12 +7109,12 @@ sub nic_porkbun_update {
$reply =~ qr/{(?:[^{}]*|(?R))*}/mp; $reply =~ qr/{(?:[^{}]*|(?R))*}/mp;
my $response = eval { decode_json(${^MATCH}) }; my $response = eval { decode_json(${^MATCH}) };
if (!defined($response)) { if (!defined($response)) {
$config{$host}{'status'} = "bad"; $config{$host}{'status-ipv4'} = "bad";
failed("%s -- Unexpected service response.", $host); failed("%s -- Unexpected service response.", $host);
next; next;
} }
if ($response->{status} ne 'SUCCESS') { if ($response->{status} ne 'SUCCESS') {
$config{$host}{'status'} = "bad"; $config{$host}{'status-ipv4'} = "bad";
failed("%s -- Unexpected status. (status = %s)", $host, $response->{status}); failed("%s -- Unexpected status. (status = %s)", $host, $response->{status});
next; next;
} }
@ -7266,7 +7126,7 @@ sub nic_porkbun_update {
} }
my $current_content = $records->[0]->{'content'}; my $current_content = $records->[0]->{'content'};
if ($current_content eq $ipv4) { if ($current_content eq $ipv4) {
$config{$host}{'status'} = "good"; $config{$host}{'status-ipv4'} = "good";
success("updating %s: skipped: IPv4 address was already set to %s.", $host, $ipv4); success("updating %s: skipped: IPv4 address was already set to %s.", $host, $ipv4);
next; next;
} }
@ -7298,11 +7158,11 @@ sub nic_porkbun_update {
failed("updating %s: failed (%s)", $host, $reply); failed("updating %s: failed (%s)", $host, $reply);
next; next;
} }
$config{$host}{'status'} = "good"; $config{$host}{'status-ipv4'} = "good";
success("updating %s: good: IPv4 address set to %s", $host, $ipv4); success("updating %s: good: IPv4 address set to %s", $host, $ipv4);
next; next;
} else { } else {
$config{$host}{'status'} = "bad"; $config{$host}{'status-ipv4'} = "bad";
failed("updating %s: No applicable existing records.", $host); failed("updating %s: No applicable existing records.", $host);
next; next;
} }
@ -7328,12 +7188,12 @@ sub nic_porkbun_update {
); );
# No response, declare as failed # No response, declare as failed
if (!defined($reply) || !$reply) { if (!defined($reply) || !$reply) {
$config{$host}{'status'} = "bad"; $config{$host}{'status-ipv6'} = "bad";
failed("updating %s: Could not connect to porkbun.com.", $host); failed("updating %s: Could not connect to porkbun.com.", $host);
next; next;
} }
if (!header_ok($host, $reply)) { if (!header_ok($host, $reply)) {
$config{$host}{'status'} = "bad"; $config{$host}{'status-ipv6'} = "bad";
failed("updating %s: failed (%s)", $host, $reply); failed("updating %s: failed (%s)", $host, $reply);
next; next;
} }
@ -7342,12 +7202,12 @@ sub nic_porkbun_update {
$reply =~ qr/{(?:[^{}]*|(?R))*}/mp; $reply =~ qr/{(?:[^{}]*|(?R))*}/mp;
my $response = eval { decode_json(${^MATCH}) }; my $response = eval { decode_json(${^MATCH}) };
if (!defined($response)) { if (!defined($response)) {
$config{$host}{'status'} = "bad"; $config{$host}{'status-ipv6'} = "bad";
failed("%s -- Unexpected service response.", $host); failed("%s -- Unexpected service response.", $host);
next; next;
} }
if ($response->{status} ne 'SUCCESS') { if ($response->{status} ne 'SUCCESS') {
$config{$host}{'status'} = "bad"; $config{$host}{'status-ipv6'} = "bad";
failed("%s -- Unexpected status. (status = %s)", $host, $response->{status}); failed("%s -- Unexpected status. (status = %s)", $host, $response->{status});
next; next;
} }
@ -7359,7 +7219,7 @@ sub nic_porkbun_update {
} }
my $current_content = $records->[0]->{'content'}; my $current_content = $records->[0]->{'content'};
if ($current_content eq $ipv6) { if ($current_content eq $ipv6) {
$config{$host}{'status'} = "good"; $config{$host}{'status-ipv6'} = "good";
success("updating %s: skipped: IPv6 address was already set to %s.", $host, $ipv6); success("updating %s: skipped: IPv6 address was already set to %s.", $host, $ipv6);
next; next;
} }
@ -7391,11 +7251,11 @@ sub nic_porkbun_update {
failed("updating %s: failed (%s)", $host, $reply); failed("updating %s: failed (%s)", $host, $reply);
next; next;
} }
$config{$host}{'status'} = "good"; $config{$host}{'status-ipv6'} = "good";
success("updating %s: good: IPv6 address set to %s", $host, $ipv4); success("updating %s: good: IPv6 address set to %s", $host, $ipv4);
next; next;
} else { } else {
$config{$host}{'status'} = "bad"; $config{$host}{'status-ipv6'} = "bad";
failed("updating %s: No applicable existing records.", $host); failed("updating %s: No applicable existing records.", $host);
next; next;
} }

View file

@ -12,7 +12,6 @@ my $ipv6_supported = eval {
); );
defined($ipv6_socket); defined($ipv6_socket);
}; };
my $has_curl = qx{ @CURL@ --version 2>/dev/null; } && $? == 0;
my $http_daemon_supports_ipv6 = eval { my $http_daemon_supports_ipv6 = eval {
require HTTP::Daemon; require HTTP::Daemon;
@ -55,54 +54,37 @@ my %httpd = (
); );
my @test_cases = ( my @test_cases = (
# Fetch via IO::Socket::IP
{ipv6_opt => 0, server_ipv => '4', client_ipv => ''}, {ipv6_opt => 0, server_ipv => '4', client_ipv => ''},
{ipv6_opt => 0, server_ipv => '4', client_ipv => '4'}, {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 # 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'}, {ipv6_opt => 0, server_ipv => '6', client_ipv => '6'},
# Fetch via IO::Socket::IP # Fetch without ssl
{ipv6_opt => 1, server_ipv => '4', client_ipv => ''}, { server_ipv => '4', client_ipv => '' },
{ipv6_opt => 1, server_ipv => '4', client_ipv => '4'}, { server_ipv => '4', client_ipv => '4' },
{ipv6_opt => 1, server_ipv => '6', client_ipv => ''}, { server_ipv => '6', client_ipv => '' },
{ipv6_opt => 1, server_ipv => '6', client_ipv => '6'}, { server_ipv => '6', client_ipv => '6' },
# Fetch via IO::Socket::SSL # Fetch with ssl
{ ssl => 1, server_ipv => '4', client_ipv => '' }, { ssl => 1, server_ipv => '4', client_ipv => '' },
{ ssl => 1, server_ipv => '4', client_ipv => '4' }, { ssl => 1, server_ipv => '4', client_ipv => '4' },
{ ssl => 1, server_ipv => '6', client_ipv => '' }, { ssl => 1, server_ipv => '6', client_ipv => '' },
{ ssl => 1, server_ipv => '6', client_ipv => '6' }, { ssl => 1, server_ipv => '6', client_ipv => '6' },
# Fetch with curl
{ curl => 1, server_ipv => '4', client_ipv => '' },
{ curl => 1, server_ipv => '4', client_ipv => '4' },
{ curl => 1, server_ipv => '6', client_ipv => '' },
{ curl => 1, server_ipv => '6', client_ipv => '6' },
# Fetch with curl and ssl
{ curl => 1, ssl => 1, server_ipv => '4', client_ipv => '' },
{ curl => 1, ssl => 1, server_ipv => '4', client_ipv => '4' },
{ curl => 1, ssl => 1, server_ipv => '6', client_ipv => '' },
{ curl => 1, ssl => 1, server_ipv => '6', client_ipv => '6' },
); );
for my $tc (@test_cases) { for my $tc (@test_cases) {
$tc->{ipv6_opt} //= 0; $tc->{ipv6_opt} //= 0;
$tc->{ssl} //= 0; $tc->{ssl} //= 0;
$tc->{curl} //= 0;
SKIP: { SKIP: {
skip("IPv6 not supported on this system", 1) skip("IPv6 not supported on this system", 1)
if $tc->{server_ipv} eq '6' && !$ipv6_supported; if $tc->{server_ipv} eq '6' && !$ipv6_supported;
skip("HTTP::Daemon too old for IPv6 support", 1) skip("HTTP::Daemon too old for IPv6 support", 1)
if $tc->{server_ipv} eq '6' && !$http_daemon_supports_ipv6; if $tc->{server_ipv} eq '6' && !$http_daemon_supports_ipv6;
skip("HTTP::Daemon::SSL not available", 1) if $tc->{ssl} && !$has_http_daemon_ssl; skip("HTTP::Daemon::SSL not available", 1) if $tc->{ssl} && !$has_http_daemon_ssl;
skip("Curl not available on this system", 1) if $tc->{curl} && !$has_curl;
my $uri = $httpd{$tc->{server_ipv}}{$tc->{ssl} ? 'https' : 'http'}->endpoint(); my $uri = $httpd{$tc->{server_ipv}}{$tc->{ssl} ? 'https' : 'http'}->endpoint();
my $name = sprintf("IPv%s client to %s%s%s", my $name = sprintf("IPv%s client to %s%s",
$tc->{client_ipv} || '*', $uri, $tc->{ipv6_opt} ? ' (-ipv6)' : '', $tc->{client_ipv} || '*', $uri, $tc->{ipv6_opt} ? ' (-ipv6)' : '');
$tc->{curl} ? ' (curl)' : '');
$ddclient::globals{'ipv6'} = $tc->{ipv6_opt}; $ddclient::globals{'ipv6'} = $tc->{ipv6_opt};
$ddclient::globals{'curl'} = $tc->{curl};
my $got = ddclient::geturl(url => $uri, ipversion => $tc->{client_ipv}); my $got = ddclient::geturl(url => $uri, ipversion => $tc->{client_ipv});
isnt($got // '', '', $name); isnt($got // '', '', $name);
} }

View file

@ -1,263 +0,0 @@
use Test::More;
use Data::Dumper;
eval {
require HTTP::Request;
require HTTP::Response;
require IO::Socket::IP;
require IO::Socket::SSL;
require ddclient::Test::Fake::HTTPD;
} or plan(skip_all => $@);
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
$Data::Dumper::Sortkeys = 1;
my $httpd = ddclient::Test::Fake::HTTPD->new();
$httpd->run(sub {
my $req = shift;
# Echo back the full request.
my $resp = [ 200, [ 'Content-Type' => 'application/octet-stream' ], [ $req->as_string() ] ];
if ($req->method() ne 'GET') {
# TODO: Add support for CONNECT to test https via proxy.
$resp->[0] = 501; # 501 == Not Implemented
}
return $resp;
});
my $args;
{
package InterceptSocket;
require base;
base->import(qw(IO::Socket::IP));
sub new {
my ($class, %args) = @_;
$args = \%args;
return $class->SUPER::new(%args, PeerAddr => $httpd->host(), PeerPort => $httpd->port());
}
}
# Keys:
# * name: Display name.
# * params: Parameters to pass to geturl.
# * opt_ssl: Value to return from opt('ssl'). Defaults to 0.
# * opt_ssl_ca_dir: Value to return from opt('ssl_ca_dir'). Defaults to undef.
# * opt_ssl_ca_file: Value to return from opt('ssl_ca_file'). Defaults to undef.
# * want_args: Args that should be passed to the socket constructor minus Proto,
# Timeout, and original_socket_class.
# * want_req_method: The HTTP method geturl is expected to use. Defaults to 'GET'.
# * want_req_uri: URI that geturl is expected to request.
# * todo: If defined, mark this test as expected to fail.
my @test_cases = (
{
name => 'https',
params => {
url => 'https://hostname',
},
want_args => {
PeerAddr => 'hostname',
PeerPort => '443',
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
},
want_req_uri => '/',
},
{
name => 'http with ssl=true',
params => {
url => 'http://hostname',
},
opt_ssl => 1,
want_args => {
PeerAddr => 'hostname',
PeerPort => '443',
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
},
want_req_uri => '/',
},
{
name => 'https with port',
params => {
url => 'https://hostname:123',
},
want_args => {
PeerAddr => 'hostname',
PeerPort => '123',
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
},
want_req_uri => '/',
},
{
name => 'http with port and ssl=true',
params => {
url => 'https://hostname:123',
},
opt_ssl => 1,
want_args => {
PeerAddr => 'hostname',
PeerPort => '123',
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
},
want_req_uri => '/',
},
{
name => 'https proxy, http URL',
params => {
proxy => 'https://proxy',
url => 'http://hostname',
},
want_args => {
PeerAddr => 'proxy',
PeerPort => '443',
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
},
want_req_uri => 'http://hostname/',
todo => "broken",
},
{
name => 'http proxy, https URL',
params => {
proxy => 'http://proxy',
url => 'https://hostname',
},
want_args => {
PeerAddr => 'proxy',
PeerPort => '80',
SSL_startHandshake => 0,
},
want_req_method => 'CONNECT',
want_req_uri => 'hostname:443',
todo => "not yet supported; silently fails",
},
{
name => 'https proxy, https URL',
params => {
proxy => 'https://proxy',
url => 'https://hostname',
},
want_args => {
PeerAddr => 'proxy',
PeerPort => '443',
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
},
want_req_method => 'CONNECT',
want_req_uri => 'hostname:443',
todo => "not yet supported; silently fails",
},
{
name => 'http proxy, http URL, ssl=true',
params => {
proxy => 'http://proxy',
url => 'http://hostname',
},
opt_ssl => 1,
want_args => {
PeerAddr => 'proxy',
PeerPort => '443',
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
},
want_req_method => 'CONNECT',
want_req_uri => 'hostname:443',
todo => "not yet supported; silently fails",
},
{
name => 'https proxy with port, http URL with port',
params => {
proxy => 'https://proxy:123',
url => 'http://hostname:456',
},
want_args => {
PeerAddr => 'proxy',
PeerPort => '123',
},
want_req_uri => 'http://hostname:456/',
todo => "broken",
},
{
name => 'http proxy with port, https URL with port',
params => {
proxy => 'http://proxy:123',
url => 'https://hostname:456',
},
want_args => {
PeerAddr => 'proxy',
PeerPort => '123',
SSL_startHandshake => 0,
},
want_req_method => 'CONNECT',
want_req_uri => 'hostname:456',
todo => "not yet supported; silently fails",
},
{
name => 'CA dir',
params => {
url => 'https://hostname',
},
opt_ssl_ca_dir => '/ca/dir',
want_args => {
PeerAddr => 'hostname',
PeerPort => '443',
SSL_ca_path => '/ca/dir',
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
},
want_req_uri => '/',
},
{
name => 'CA file',
params => {
url => 'https://hostname',
},
opt_ssl_ca_file => '/ca/file',
want_args => {
PeerAddr => 'hostname',
PeerPort => '443',
SSL_ca_file => '/ca/file',
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
},
want_req_uri => '/',
},
{
name => 'CA dir and file',
params => {
url => 'https://hostname',
},
opt_ssl_ca_dir => '/ca/dir',
opt_ssl_ca_file => '/ca/file',
want_args => {
PeerAddr => 'hostname',
PeerPort => '443',
SSL_ca_file => '/ca/file',
SSL_ca_path => '/ca/dir',
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
},
want_req_uri => '/',
},
);
for my $tc (@test_cases) {
$args = undef;
$ddclient::globals{'ssl'} = $tc->{opt_ssl} // 0;
$ddclient::globals{'ssl_ca_dir'} = $tc->{opt_ssl_ca_dir};
$ddclient::globals{'ssl_ca_file'} = $tc->{opt_ssl_ca_file};
my $resp_str = ddclient::geturl(_testonly_socket_class => 'InterceptSocket', %{$tc->{params}});
TODO: {
local $TODO = $tc->{todo};
subtest $tc->{name} => sub {
my %want_args = (
Proto => 'tcp',
Timeout => ddclient::opt('timeout'),
original_socket_class => 'IO::Socket::SSL',
%{$tc->{want_args}},
);
is(Dumper($args), Dumper(\%want_args), "socket constructor args");
ok(defined($resp_str), "response is defined") or return;
ok(my $resp = HTTP::Response->parse($resp_str), "parse response") or return;
ok(my $req_str = $resp->decoded_content(), "decode request from response") or return;
ok(my $req = HTTP::Request->parse($req_str), "parse request") or return;
is($req->method(), $tc->{want_req_method} // 'GET', "request method");
is($req->uri(), $tc->{want_req_uri}, "request URI");
};
}
}
done_testing();