viewvcs: handle exceptions in on_destroy cb
[public-inbox.git] / t / imapd-tls.t
blobb95085a29e4d1b9230fdeedb0ad78e8cc80b9abd
1 #!perl -w
2 # Copyright (C) all contributors <meta@public-inbox.org>
3 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4 use v5.12;
5 use Socket qw(IPPROTO_TCP SOL_SOCKET);
6 use PublicInbox::TestCommon;
7 # IO::Poll is part of the standard library, but distros may split it off...
8 require_mods(qw(-imapd IO::Socket::SSL Mail::IMAPClient IO::Poll));
9 my $imap_client = 'Mail::IMAPClient';
10 $imap_client->can('starttls') or
11         plan skip_all => 'Mail::IMAPClient does not support TLS';
12 my $can_compress = $imap_client->can('compress');
13 if ($can_compress) { # hope this gets fixed upstream, soon
14         require PublicInbox::IMAPClient;
15         $imap_client = 'PublicInbox::IMAPClient';
18 my $cert = 'certs/server-cert.pem';
19 my $key = 'certs/server-key.pem';
20 unless (-r $key && -r $cert) {
21         plan skip_all =>
22                 "certs/ missing for $0, run $^X ./create-certs.perl in certs/";
24 use_ok 'PublicInbox::TLS';
25 use_ok 'IO::Socket::SSL';
26 my $version = 1; # v2 needs newer git
27 require_git('2.6') if $version >= 2;
28 my ($tmpdir, $for_destroy) = tmpdir();
29 my $err = "$tmpdir/stderr.log";
30 my $out = "$tmpdir/stdout.log";
31 my $pi_config;
32 my $group = 'test-imapd-tls';
33 my $addr = $group . '@example.com';
34 my $starttls = tcp_server();
35 my $imaps = tcp_server();
36 my $ibx = create_inbox 'imapd-tls', version => $version,
37                         -primary_address => $addr, indexlevel => 'basic', sub {
38         my ($im, $ibx) = @_;
39         $im->add(eml_load('t/data/0001.patch')) or BAIL_OUT '->add';
40         $pi_config = "$ibx->{inboxdir}/pi_config";
41         open my $fh, '>', $pi_config or BAIL_OUT "open: $!";
42         print $fh <<EOF or BAIL_OUT "print: $!";
43 [publicinbox "imapd-tls"]
44         inboxdir = $ibx->{inboxdir}
45         address = $addr
46         indexlevel = basic
47         newsgroup = $group
48 EOF
49         close $fh or BAIL_OUT "close: $!\n";
51 $pi_config //= "$ibx->{inboxdir}/pi_config";
53 my $imaps_addr = tcp_host_port($imaps);
54 my $starttls_addr = tcp_host_port($starttls);
55 my $env = { PI_CONFIG => $pi_config };
56 my $td;
58 for my $args (
59         [ "--cert=$cert", "--key=$key",
60                 "-limaps://$imaps_addr",
61                 "-limap://$starttls_addr" ],
62 ) {
63         for ($out, $err) {
64                 open my $fh, '>', $_ or BAIL_OUT "truncate: $!";
65         }
66         my $cmd = [ '-imapd', '-W0', @$args, "--stdout=$out", "--stderr=$err" ];
67         $td = start_script($cmd, $env, { 3 => $starttls, 4 => $imaps });
68         my %o = (
69                 SSL_hostname => 'server.local',
70                 SSL_verifycn_name => 'server.local',
71                 SSL_verify_mode => SSL_VERIFY_PEER(),
72                 SSL_ca_file => 'certs/test-ca.pem',
73         );
74         # start negotiating a slow TLS connection
75         my $slow = tcp_connect($imaps, Blocking => 0);
76         $slow = IO::Socket::SSL->start_SSL($slow, SSL_startHandshake => 0, %o);
77         my $slow_done = $slow->connect_SSL;
78         my @poll;
79         if ($slow_done) {
80                 diag('W: connect_SSL early OK, slow client test invalid');
81                 use PublicInbox::Syscall qw(EPOLLIN EPOLLOUT);
82                 @poll = (fileno($slow), EPOLLIN | EPOLLOUT);
83         } else {
84                 @poll = (fileno($slow), PublicInbox::TLS::epollbit());
85         }
86         # we should call connect_SSL much later...
87         my %imaps_opt = (User => 'a', Password => 'b',
88                         Server => $imaps->sockhost,
89                         Port => $imaps->sockport);
90         # IMAPS
91         my $c = $imap_client->new(%imaps_opt, Ssl => [ %o ]);
92         ok($c && $c->IsAuthenticated, 'authenticated');
93         ok($c->select($group), 'SELECT works');
94         ok(!(scalar $c->has_capability('STARTTLS')),
95                 'starttls not advertised with IMAPS');
96         ok(!$c->starttls, "starttls fails");
97         ok($c->has_capability('COMPRESS') ||
98                 $c->has_capability('COMPRESS=DEFLATE'),
99                 'compress advertised');
100         ok($c->compress, 'compression enabled with IMAPS');
101         ok(!$c->starttls, 'starttls still fails');
102         ok($c->noop, 'noop succeeds');
103         ok($c->logout, 'logout succeeds');
105         # STARTTLS
106         my %imap_opt = (Server => $starttls->sockhost,
107                         Port => $starttls->sockport);
108         $c = $imap_client->new(%imap_opt);
109         ok(scalar $c->has_capability('STARTTLS'),
110                 'starttls advertised');
111         ok($c->Starttls([ %o ]), 'set starttls options');
112         ok($c->starttls, '->starttls works');
113         ok(!(scalar($c->has_capability('STARTTLS'))),
114                 'starttls not advertised');
115         ok(!$c->starttls, '->starttls again fails');
116         ok(!(scalar($c->has_capability('STARTTLS'))),
117                 'starttls still not advertised');
118         ok($c->examine($group), 'EXAMINE works');
119         ok($c->noop, 'NOOP works');
120         ok($c->compress, 'compression enabled with IMAPS');
121         ok($c->noop, 'NOOP works after compress');
122         ok($c->logout, 'logout succeeds after compress');
124         # STARTTLS with bad hostname
125         $o{SSL_hostname} = $o{SSL_verifycn_name} = 'server.invalid';
126         $c = $imap_client->new(%imap_opt);
127         ok(scalar $c->has_capability('STARTTLS'), 'starttls advertised');
128         ok($c->Starttls([ %o ]), 'set starttls options');
129         ok(!$c->starttls, '->starttls fails with bad hostname');
131         $c = $imap_client->new(%imap_opt);
132         ok($c->noop, 'NOOP still works from plain IMAP');
134         # IMAPS with bad hostname
135         $c = $imap_client->new(%imaps_opt, Ssl => [ %o ]);
136         is($c, undef, 'IMAPS fails with bad hostname');
138         # make hostname valid
139         $o{SSL_hostname} = $o{SSL_verifycn_name} = 'server.local';
140         $c = $imap_client->new(%imaps_opt, Ssl => [ %o ]);
141         ok($c, 'IMAPS succeeds again with valid hostname');
143         # slow TLS connection did not block the other fast clients while
144         # connecting, finish it off:
145         until ($slow_done) {
146                 IO::Poll::_poll(-1, @poll);
147                 $slow_done = $slow->connect_SSL and last;
148                 @poll = (fileno($slow), PublicInbox::TLS::epollbit());
149         }
150         $slow->blocking(1);
151         ok(sysread($slow, my $greet, 4096) > 0, 'slow got a greeting');
152         like($greet, qr/\A\* OK \[CAPABILITY IMAP4rev1 /, 'got greeting');
153         is(syswrite($slow, "1 LOGOUT\r\n"), 10, 'slow wrote LOGOUT');
154         ok(sysread($slow, my $end, 4096) > 0, 'got end');
155         is(sysread($slow, my $eof, 4096), 0, 'got EOF');
157         test_lei(sub {
158                 lei_ok qw(ls-mail-source), "imap://$starttls_addr",
159                         \'STARTTLS not used by default';
160                 my $plain_out = $lei_out;
161                 ok(!lei(qw(ls-mail-source -c imap.starttls),
162                         "imap://$starttls_addr"), 'STARTTLS verify fails');
163                 unlike $lei_err, qr!W: imap\.starttls= .*? is not boolean!i,
164                         'no non-boolean warning';
165                 lei_ok qw(-c imap.starttls -c imap.sslVerify= ls-mail-source),
166                         "imap://$starttls_addr",
167                         \'disabling imap.sslVerify works w/ STARTTLS';
168                 is $lei_out, $plain_out, 'sslVerify=false w/ STARTTLS output';
169                 lei_ok qw(ls-mail-source -c imap.sslVerify=false),
170                         "imaps://$imaps_addr",
171                         \'disabling imap.sslVerify works w/ imaps://';
172                 is $lei_out, $plain_out, 'sslVerify=false w/ IMAPS output';
173         });
175         SKIP: {
176                 skip 'TCP_DEFER_ACCEPT is Linux-only', 2 if $^O ne 'linux';
177                 my $var = eval { Socket::TCP_DEFER_ACCEPT() } // 9;
178                 defined(my $x = getsockopt($imaps, IPPROTO_TCP, $var)) or die;
179                 ok(unpack('i', $x) > 0, 'TCP_DEFER_ACCEPT set on IMAPS');
180                 defined($x = getsockopt($starttls, IPPROTO_TCP, $var)) or die;
181                 is(unpack('i', $x), 0, 'TCP_DEFER_ACCEPT is 0 on plain IMAP');
182         };
183         SKIP: {
184                 require_mods '+accf_data';
185                 require PublicInbox::Daemon;
186                 my $x = getsockopt($imaps, SOL_SOCKET,
187                                 $PublicInbox::Daemon::SO_ACCEPTFILTER);
188                 like($x, qr/\Adataready\0+\z/, 'got dataready accf for IMAPS');
189                 $x = getsockopt($starttls, IPPROTO_TCP,
190                                 $PublicInbox::Daemon::SO_ACCEPTFILTER);
191                 is($x, undef, 'no BSD accept filter for plain IMAP');
192         };
194         $c = undef;
195         $td->kill;
196         $td->join;
197         is($?, 0, 'no error in exited process');
198         open my $fh, '<', $err or BAIL_OUT "open $err failed: $!";
199         my $eout = do { local $/; <$fh> };
200         unlike($eout, qr/wide/i, 'no Wide character warnings');
203 done_testing;