Require curl

Use command-line curl, and remove alternative Perl and Curl-via-Perl
implementations of network code.
This commit is contained in:
Reuben Thomas 2023-07-10 00:53:54 +03:00
parent 211404b5d1
commit a9c1e545fb
8 changed files with 45 additions and 596 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,6 +24,7 @@ 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-inet6-perl \
@ -48,28 +49,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 +84,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

@ -7,6 +7,7 @@ repository history](https://github.com/ddclient/ddclient/commits/master).
### 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.
@ -23,7 +24,7 @@ repository history](https://github.com/ddclient/ddclient/commits/master).
### Bug fixes ### Bug fixes
* DynDNS2 now uses the newer ipv4/ipv6 syntax's * DynDNS2 now uses the newer ipv4/ipv6 syntaxes
* 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 * 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

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 \

View file

@ -1,7 +1,7 @@
# 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 This is a friendly fork/continuation of https://github.com/ddclient/ddclient

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], [],

View file

@ -17,8 +17,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@');
@ -454,7 +452,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),
@ -1090,7 +1087,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"],
@ -2460,22 +2456,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
@ -2510,180 +2490,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
###################################################################### ######################################################################
@ -2753,10 +2559,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};
@ -2799,89 +2602,35 @@ 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 }; push(@curlopt, "silent");
if ($curl_loaded) { push(@curlopt, "include"); ## Include HTTP response for compatibility
# System has the WWW::Curl::Easy module so use that push(@curlopt, "insecure") if ($use_ssl && !($params{ssl_validate} // 1));
import WWW::Curl::Easy; push(@curlopt, "cacert=\"".escape_curl_param(opt('ssl_ca_file')).'"') if defined(opt('ssl_ca_file'));
my $curl = WWW::Curl::Easy->new; push(@curlopt, "capath=\"".escape_curl_param(opt('ssl_ca_dir')).'"') if defined(opt('ssl_ca_dir'));
push(@curlopt, "ipv4") if ($ipversion == 4);
push(@curlopt, "ipv6") if ($ipversion == 6);
push(@curlopt, "user-agent=\"".escape_curl_param("${program}/${version}").'"');
push(@curlopt, "connect-timeout=$timeout");
push(@curlopt, "max-time=$timeout");
push(@curlopt, "request=$method");
push(@curlopt, "user=\"".escape_curl_param("${login}:${password}").'"') if (defined($login) && defined($password));
push(@curlopt, "proxy=\"".escape_curl_param("${protocol}://${proxy}").'"') if defined($proxy);
push(@curlopt, "url=\"".escape_curl_param("${protocol}://${server}/${url}").'"');
$curl->setopt(WWW::Curl::Easy->CURLOPT_HEADER, 1); ## Include HTTP response for compatibility # Each header line is added individually
$curl->setopt(WWW::Curl::Easy->CURLOPT_SSL_VERIFYPEER, ($params{ssl_validate} // 1) ? 1 : 0 ); @header_lines = split('\n', $headers);
$curl->setopt(WWW::Curl::Easy->CURLOPT_SSL_VERIFYHOST, ($params{ssl_validate} // 1) ? 1 : 0 ); $_ = "header=\"".escape_curl_param($_).'"' foreach (@header_lines);
$curl->setopt(WWW::Curl::Easy->CURLOPT_CAINFO, opt('ssl_ca_file')) if defined(opt('ssl_ca_file')); push(@curlopt, @header_lines);
$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'); # Add in the data if any was provided (for POST/PATCH)
$curl->setopt(WWW::Curl::Easy->CURLOPT_PUT, 1) if ($method eq 'PUT'); push(@curlopt, "data=\"".escape_curl_param(${data}).'"') if ($data);
$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)); # don't include ${url} as that might expose login credentials
$curl->setopt(WWW::Curl::Easy->CURLOPT_PROXY, "${protocol}://${proxy}") if defined($proxy); $0 = sprintf("%s - Curl system cmd sending to %s", $program, "${protocol}://${server}");
$curl->setopt(WWW::Curl::Easy->CURLOPT_URL, "${protocol}://${server}/${url}"); verbose("SENDING:", "Curl system cmd to %s", "${protocol}://${server}");
verbose("SENDING:", "%s", $_) foreach (@curlopt);
# Add header lines if any was provided $reply = curl_cmd(@curlopt);
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, "include"); ## Include HTTP response for compatibility
push(@curlopt, "insecure") if ($use_ssl && !($params{ssl_validate} // 1));
push(@curlopt, "cacert=\"".escape_curl_param(opt('ssl_ca_file')).'"') if defined(opt('ssl_ca_file'));
push(@curlopt, "capath=\"".escape_curl_param(opt('ssl_ca_dir')).'"') if defined(opt('ssl_ca_dir'));
push(@curlopt, "ipv4") if ($ipversion == 4);
push(@curlopt, "ipv6") if ($ipversion == 6);
push(@curlopt, "user-agent=\"".escape_curl_param("${program}/${version}").'"');
push(@curlopt, "connect-timeout=$timeout");
push(@curlopt, "max-time=$timeout");
push(@curlopt, "request=$method");
push(@curlopt, "user=\"".escape_curl_param("${login}:${password}").'"') if (defined($login) && defined($password));
push(@curlopt, "proxy=\"".escape_curl_param("${protocol}://${proxy}").'"') if defined($proxy);
push(@curlopt, "url=\"".escape_curl_param("${protocol}://${server}/${url}").'"');
# Each header line is added individually
@header_lines = split('\n', $headers);
$_ = "header=\"".escape_curl_param($_).'"' foreach (@header_lines);
push(@curlopt, @header_lines);
# Add in the data if any was provided (for POST/PATCH)
push(@curlopt, "data=\"".escape_curl_param(${data}).'"') if ($data);
# don't include ${url} as that might expose login credentials
$0 = sprintf("%s - Curl system cmd sending to %s", $program, "${protocol}://${server}");
verbose("SENDING:", "Curl system cmd to %s", "${protocol}://${server}");
verbose("SENDING:", "%s", $_) foreach (@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

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();