diff --git a/Makefile.am b/Makefile.am index 75f4dd0..6bf6498 100644 --- a/Makefile.am +++ b/Makefile.am @@ -70,6 +70,7 @@ AM_PL_LOG_FLAGS = -Mstrict -w \ -I'$(abs_top_srcdir)'/t/lib handwritten_tests = \ t/geturl_connectivity.pl \ + t/geturl_ssl.pl \ t/parse_assignments.pl \ t/write_cache.pl generated_tests = \ diff --git a/configure.ac b/configure.ac index 48616e1..33b88fd 100644 --- a/configure.ac +++ b/configure.ac @@ -64,6 +64,8 @@ m4_foreach_w([_m], [ HTTP::Daemon=6.12 HTTP::Daemon::SSL HTTP::Message::PSGI + HTTP::Request + HTTP::Response IO::Socket::IP Scalar::Util Test::TCP diff --git a/ddclient.in b/ddclient.in index 776cb21..8b8a4a9 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2073,6 +2073,10 @@ sub geturl { load_ipv6_support; $socket_class = 'IO::Socket::INET6'; } + 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; diff --git a/t/geturl_ssl.pl b/t/geturl_ssl.pl new file mode 100644 index 0000000..ff67859 --- /dev/null +++ b/t/geturl_ssl.pl @@ -0,0 +1,254 @@ +use Test::More; +use Data::Dumper; +eval { + require HTTP::Request; + require HTTP::Response; + require IO::Socket::IP; + 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 MultiHomed, 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', + }, + want_req_uri => '/', + }, + { + name => 'http with ssl=true', + params => { + url => 'http://hostname', + }, + opt_ssl => 1, + want_args => { + PeerAddr => 'hostname', + PeerPort => '443', + }, + want_req_uri => '/', + }, + { + name => 'https with port', + params => { + url => 'https://hostname:123', + }, + want_args => { + PeerAddr => 'hostname', + PeerPort => '123', + }, + want_req_uri => '/', + }, + { + name => 'http with port and ssl=true', + params => { + url => 'https://hostname:123', + }, + opt_ssl => 1, + want_args => { + PeerAddr => 'hostname', + PeerPort => '123', + }, + want_req_uri => '/', + }, + { + name => 'https proxy, http URL', + params => { + proxy => 'https://proxy', + url => 'http://hostname', + }, + want_args => { + PeerAddr => 'proxy', + PeerPort => '443', + }, + 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', + }, + 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', + }, + 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', + }, + 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', + }, + 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', + }, + 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 = ( + MultiHomed => 1, + 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();