| #!perl -w |
| # Copyright (C) all contributors <meta@public-inbox.org> |
| # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> |
| use v5.12; |
| use Socket qw(IPPROTO_TCP SOL_SOCKET); |
| use PublicInbox::TestCommon; |
| # IO::Poll is part of the standard library, but distros may split it off... |
| require_mods(qw(-imapd IO::Socket::SSL Mail::IMAPClient IO::Poll)); |
| my $imap_client = 'Mail::IMAPClient'; |
| $imap_client->can('starttls') or |
| plan skip_all => 'Mail::IMAPClient does not support TLS'; |
| my $can_compress = $imap_client->can('compress'); |
| if ($can_compress) { # hope this gets fixed upstream, soon |
| require PublicInbox::IMAPClient; |
| $imap_client = 'PublicInbox::IMAPClient'; |
| } |
| |
| my $cert = 'certs/server-cert.pem'; |
| my $key = 'certs/server-key.pem'; |
| unless (-r $key && -r $cert) { |
| plan skip_all => |
| "certs/ missing for $0, run $^X ./create-certs.perl in certs/"; |
| } |
| use_ok 'PublicInbox::TLS'; |
| use_ok 'IO::Socket::SSL'; |
| my $version = 1; # v2 needs newer git |
| require_git('2.6') if $version >= 2; |
| my ($tmpdir, $for_destroy) = tmpdir(); |
| my $err = "$tmpdir/stderr.log"; |
| my $out = "$tmpdir/stdout.log"; |
| my $pi_config; |
| my $group = 'test-imapd-tls'; |
| my $addr = $group . '@example.com'; |
| my $starttls = tcp_server(); |
| my $imaps = tcp_server(); |
| my $ibx = create_inbox 'imapd-tls', version => $version, |
| -primary_address => $addr, indexlevel => 'basic', sub { |
| my ($im, $ibx) = @_; |
| $im->add(eml_load('t/data/0001.patch')) or BAIL_OUT '->add'; |
| $pi_config = "$ibx->{inboxdir}/pi_config"; |
| open my $fh, '>', $pi_config or BAIL_OUT "open: $!"; |
| print $fh <<EOF or BAIL_OUT "print: $!"; |
| [publicinbox "imapd-tls"] |
| inboxdir = $ibx->{inboxdir} |
| address = $addr |
| indexlevel = basic |
| newsgroup = $group |
| EOF |
| close $fh or BAIL_OUT "close: $!\n"; |
| }; |
| $pi_config //= "$ibx->{inboxdir}/pi_config"; |
| |
| my $imaps_addr = tcp_host_port($imaps); |
| my $starttls_addr = tcp_host_port($starttls); |
| my $env = { PI_CONFIG => $pi_config }; |
| my $td; |
| |
| for my $args ( |
| [ "--cert=$cert", "--key=$key", |
| "-limaps://$imaps_addr", |
| "-limap://$starttls_addr" ], |
| ) { |
| for ($out, $err) { |
| open my $fh, '>', $_ or BAIL_OUT "truncate: $!"; |
| } |
| my $cmd = [ '-imapd', '-W0', @$args, "--stdout=$out", "--stderr=$err" ]; |
| $td = start_script($cmd, $env, { 3 => $starttls, 4 => $imaps }); |
| my %o = ( |
| SSL_hostname => 'server.local', |
| SSL_verifycn_name => 'server.local', |
| SSL_verify_mode => SSL_VERIFY_PEER(), |
| SSL_ca_file => 'certs/test-ca.pem', |
| ); |
| # start negotiating a slow TLS connection |
| my $slow = tcp_connect($imaps, Blocking => 0); |
| $slow = IO::Socket::SSL->start_SSL($slow, SSL_startHandshake => 0, %o); |
| my $slow_done = $slow->connect_SSL; |
| my @poll; |
| if ($slow_done) { |
| diag('W: connect_SSL early OK, slow client test invalid'); |
| use PublicInbox::Syscall qw(EPOLLIN EPOLLOUT); |
| @poll = (fileno($slow), EPOLLIN | EPOLLOUT); |
| } else { |
| @poll = (fileno($slow), PublicInbox::TLS::epollbit()); |
| } |
| # we should call connect_SSL much later... |
| my %imaps_opt = (User => 'a', Password => 'b', |
| Server => $imaps->sockhost, |
| Port => $imaps->sockport); |
| # IMAPS |
| my $c = $imap_client->new(%imaps_opt, Ssl => [ %o ]); |
| ok($c && $c->IsAuthenticated, 'authenticated'); |
| ok($c->select($group), 'SELECT works'); |
| ok(!(scalar $c->has_capability('STARTTLS')), |
| 'starttls not advertised with IMAPS'); |
| ok(!$c->starttls, "starttls fails"); |
| ok($c->has_capability('COMPRESS') || |
| $c->has_capability('COMPRESS=DEFLATE'), |
| 'compress advertised'); |
| ok($c->compress, 'compression enabled with IMAPS'); |
| ok(!$c->starttls, 'starttls still fails'); |
| ok($c->noop, 'noop succeeds'); |
| ok($c->logout, 'logout succeeds'); |
| |
| # STARTTLS |
| my %imap_opt = (Server => $starttls->sockhost, |
| Port => $starttls->sockport); |
| $c = $imap_client->new(%imap_opt); |
| ok(scalar $c->has_capability('STARTTLS'), |
| 'starttls advertised'); |
| ok($c->Starttls([ %o ]), 'set starttls options'); |
| ok($c->starttls, '->starttls works'); |
| ok(!(scalar($c->has_capability('STARTTLS'))), |
| 'starttls not advertised'); |
| ok(!$c->starttls, '->starttls again fails'); |
| ok(!(scalar($c->has_capability('STARTTLS'))), |
| 'starttls still not advertised'); |
| ok($c->examine($group), 'EXAMINE works'); |
| ok($c->noop, 'NOOP works'); |
| ok($c->compress, 'compression enabled with IMAPS'); |
| ok($c->noop, 'NOOP works after compress'); |
| ok($c->logout, 'logout succeeds after compress'); |
| |
| # STARTTLS with bad hostname |
| $o{SSL_hostname} = $o{SSL_verifycn_name} = 'server.invalid'; |
| $c = $imap_client->new(%imap_opt); |
| ok(scalar $c->has_capability('STARTTLS'), 'starttls advertised'); |
| ok($c->Starttls([ %o ]), 'set starttls options'); |
| ok(!$c->starttls, '->starttls fails with bad hostname'); |
| |
| $c = $imap_client->new(%imap_opt); |
| ok($c->noop, 'NOOP still works from plain IMAP'); |
| |
| # IMAPS with bad hostname |
| $c = $imap_client->new(%imaps_opt, Ssl => [ %o ]); |
| is($c, undef, 'IMAPS fails with bad hostname'); |
| |
| # make hostname valid |
| $o{SSL_hostname} = $o{SSL_verifycn_name} = 'server.local'; |
| $c = $imap_client->new(%imaps_opt, Ssl => [ %o ]); |
| ok($c, 'IMAPS succeeds again with valid hostname'); |
| |
| # slow TLS connection did not block the other fast clients while |
| # connecting, finish it off: |
| until ($slow_done) { |
| IO::Poll::_poll(-1, @poll); |
| $slow_done = $slow->connect_SSL and last; |
| @poll = (fileno($slow), PublicInbox::TLS::epollbit()); |
| } |
| $slow->blocking(1); |
| ok(sysread($slow, my $greet, 4096) > 0, 'slow got a greeting'); |
| like($greet, qr/\A\* OK \[CAPABILITY IMAP4rev1 /, 'got greeting'); |
| is(syswrite($slow, "1 LOGOUT\r\n"), 10, 'slow wrote LOGOUT'); |
| ok(sysread($slow, my $end, 4096) > 0, 'got end'); |
| is(sysread($slow, my $eof, 4096), 0, 'got EOF'); |
| |
| test_lei(sub { |
| lei_ok qw(ls-mail-source), "imap://$starttls_addr", |
| \'STARTTLS not used by default'; |
| my $plain_out = $lei_out; |
| ok(!lei(qw(ls-mail-source -c imap.starttls), |
| "imap://$starttls_addr"), 'STARTTLS verify fails'); |
| unlike $lei_err, qr!W: imap\.starttls= .*? is not boolean!i, |
| 'no non-boolean warning'; |
| lei_ok qw(-c imap.starttls -c imap.sslVerify= ls-mail-source), |
| "imap://$starttls_addr", |
| \'disabling imap.sslVerify works w/ STARTTLS'; |
| is $lei_out, $plain_out, 'sslVerify=false w/ STARTTLS output'; |
| lei_ok qw(ls-mail-source -c imap.sslVerify=false), |
| "imaps://$imaps_addr", |
| \'disabling imap.sslVerify works w/ imaps://'; |
| is $lei_out, $plain_out, 'sslVerify=false w/ IMAPS output'; |
| }); |
| |
| SKIP: { |
| skip 'TCP_DEFER_ACCEPT is Linux-only', 2 if $^O ne 'linux'; |
| my $var = eval { Socket::TCP_DEFER_ACCEPT() } // 9; |
| defined(my $x = getsockopt($imaps, IPPROTO_TCP, $var)) or die; |
| ok(unpack('i', $x) > 0, 'TCP_DEFER_ACCEPT set on IMAPS'); |
| defined($x = getsockopt($starttls, IPPROTO_TCP, $var)) or die; |
| is(unpack('i', $x), 0, 'TCP_DEFER_ACCEPT is 0 on plain IMAP'); |
| }; |
| SKIP: { |
| require_mods '+accf_data'; |
| require PublicInbox::Daemon; |
| my $x = getsockopt($imaps, SOL_SOCKET, |
| $PublicInbox::Daemon::SO_ACCEPTFILTER); |
| like($x, qr/\Adataready\0+\z/, 'got dataready accf for IMAPS'); |
| $x = getsockopt($starttls, IPPROTO_TCP, |
| $PublicInbox::Daemon::SO_ACCEPTFILTER); |
| is($x, undef, 'no BSD accept filter for plain IMAP'); |
| }; |
| |
| $c = undef; |
| $td->kill; |
| $td->join; |
| is($?, 0, 'no error in exited process'); |
| open my $fh, '<', $err or BAIL_OUT "open $err failed: $!"; |
| my $eout = do { local $/; <$fh> }; |
| unlike($eout, qr/wide/i, 'no Wide character warnings'); |
| } |
| |
| done_testing; |