]> Sergey Matveev's repositories - public-inbox.git/blob - t/pop3d.t
pop3: advertise STLS in CAPA if appropriate
[public-inbox.git] / t / pop3d.t
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 PublicInbox::TestCommon;
6 use Socket qw(IPPROTO_TCP SOL_SOCKET);
7 # Net::POP3 is part of the standard library, but distros may split it off...
8 require_mods(qw(DBD::SQLite Net::POP3 IO::Socket::SSL File::FcntlLock));
9 require_git('2.6'); # for v2
10 use_ok 'IO::Socket::SSL';
11 use_ok 'PublicInbox::TLS';
12 my ($tmpdir, $for_destroy) = tmpdir();
13 mkdir("$tmpdir/p3state") or xbail "mkdir: $!";
14 my $err = "$tmpdir/stderr.log";
15 my $out = "$tmpdir/stdout.log";
16 my $olderr = "$tmpdir/plain.err";
17 my $group = 'test-pop3';
18 my $addr = $group . '@example.com';
19 my $stls = tcp_server();
20 my $plain = tcp_server();
21 my $pop3s = tcp_server();
22 my $patch = eml_load('t/data/0001.patch');
23 my $ibx = create_inbox 'pop3d', version => 2, -primary_address => $addr,
24                         indexlevel => 'basic', sub {
25         my ($im, $ibx) = @_;
26         $im->add(eml_load('t/plack-qp.eml')) or BAIL_OUT '->add';
27         $im->add($patch) or BAIL_OUT '->add';
28 };
29 my $pi_config = "$tmpdir/pi_config";
30 open my $fh, '>', $pi_config or BAIL_OUT "open: $!";
31 print $fh <<EOF or BAIL_OUT "print: $!";
32 [publicinbox]
33         pop3state = $tmpdir/p3state
34 [publicinbox "pop3"]
35         inboxdir = $ibx->{inboxdir}
36         address = $addr
37         indexlevel = basic
38         newsgroup = $group
39 EOF
40 close $fh or BAIL_OUT "close: $!\n";
41
42 my $pop3s_addr = tcp_host_port($pop3s);
43 my $stls_addr = tcp_host_port($stls);
44 my $plain_addr = tcp_host_port($plain);
45 my $env = { PI_CONFIG => $pi_config };
46 my $cert = 'certs/server-cert.pem';
47 my $key = 'certs/server-key.pem';
48
49 unless (-r $key && -r $cert) {
50         plan skip_all =>
51                 "certs/ missing for $0, run $^X ./create-certs.perl in certs/";
52 }
53
54 my $old = start_script(['-pop3d', '-W0',
55         "--stdout=$tmpdir/plain.out", "--stderr=$olderr" ],
56         $env, { 3 => $plain });
57 my @old_args = ($plain->sockhost, Port => $plain->sockport);
58 my $oldc = Net::POP3->new(@old_args);
59 my $locked_mb = ('e'x32)."\@$group";
60 ok($oldc->apop("$locked_mb.0", 'anonymous'), 'APOP to old');
61
62 { # locking within the same process
63         my $x = Net::POP3->new(@old_args);
64         ok(!$x->apop("$locked_mb.0", 'anonymous'), 'APOP lock failure');
65         like($x->message, qr/unable to lock/, 'diagnostic message');
66
67         $x = Net::POP3->new(@old_args);
68         ok($x->apop($locked_mb, 'anonymous'), 'APOP lock acquire');
69
70         my $y = Net::POP3->new(@old_args);
71         ok(!$y->apop($locked_mb, 'anonymous'), 'APOP lock fails once');
72
73         undef $x;
74         $y = Net::POP3->new(@old_args);
75         ok($y->apop($locked_mb, 'anonymous'), 'APOP lock works after release');
76 }
77
78 for my $args (
79         [ "--cert=$cert", "--key=$key",
80                 "-lpop3s://$pop3s_addr",
81                 "-lpop3://$stls_addr" ],
82 ) {
83         for ($out, $err) { open my $fh, '>', $_ or BAIL_OUT "truncate: $!" }
84         my $cmd = [ '-netd', '-W0', @$args, "--stdout=$out", "--stderr=$err" ];
85         my $td = start_script($cmd, $env, { 3 => $stls, 4 => $pop3s });
86
87         my %o = (
88                 SSL_hostname => 'server.local',
89                 SSL_verifycn_name => 'server.local',
90                 SSL_verify_mode => SSL_VERIFY_PEER(),
91                 SSL_ca_file => 'certs/test-ca.pem',
92         );
93         # start negotiating a slow TLS connection
94         my $slow = tcp_connect($pop3s, Blocking => 0);
95         $slow = IO::Socket::SSL->start_SSL($slow, SSL_startHandshake => 0, %o);
96         my $slow_done = $slow->connect_SSL;
97         my @poll;
98         if ($slow_done) {
99                 diag('W: connect_SSL early OK, slow client test invalid');
100                 use PublicInbox::Syscall qw(EPOLLIN EPOLLOUT);
101                 @poll = (fileno($slow), EPOLLIN | EPOLLOUT);
102         } else {
103                 @poll = (fileno($slow), PublicInbox::TLS::epollbit());
104         }
105
106         my @p3s_args = ($pop3s->sockhost,
107                         Port => $pop3s->sockport, SSL => 1, %o);
108         my $p3s = Net::POP3->new(@p3s_args);
109         my $capa = $p3s->capa;
110         ok(!exists $capa->{STLS}, 'no STLS CAPA for POP3S');
111         ok($p3s->quit, 'QUIT works w/POP3S');
112         {
113                 $p3s = Net::POP3->new(@p3s_args);
114                 ok(!$p3s->apop("$locked_mb.0", 'anonymous'),
115                         'APOP lock failure w/ another daemon');
116                 like($p3s->message, qr/unable to lock/, 'diagnostic message');
117         }
118
119         # slow TLS connection did not block the other fast clients while
120         # connecting, finish it off:
121         until ($slow_done) {
122                 IO::Poll::_poll(-1, @poll);
123                 $slow_done = $slow->connect_SSL and last;
124                 @poll = (fileno($slow), PublicInbox::TLS::epollbit());
125         }
126         $slow->blocking(1);
127         ok(sysread($slow, my $greet, 4096) > 0, 'slow got a greeting');
128         my @np3_args = ($stls->sockhost, Port => $stls->sockport);
129         my $np3 = Net::POP3->new(@np3_args);
130         ok($np3->quit, 'plain QUIT works');
131         $np3 = Net::POP3->new(@np3_args, %o);
132         $capa = $np3->capa;
133         ok(exists $capa->{STLS}, 'STLS CAPA advertised before STLS');
134         ok($np3->starttls, 'STLS works');
135         $capa = $np3->capa;
136         ok(!exists $capa->{STLS}, 'STLS CAPA not advertised after STLS');
137         ok($np3->quit, 'QUIT works after STLS');
138
139         for my $mailbox (('x'x32)."\@$group", $group, ('a'x32)."\@z.$group") {
140                 $np3 = Net::POP3->new(@np3_args);
141                 ok(!$np3->user($mailbox), "USER $mailbox reject");
142                 ok($np3->quit, 'QUIT after USER fail');
143
144                 $np3 = Net::POP3->new(@np3_args);
145                 ok(!$np3->apop($mailbox, 'anonymous'), "APOP $mailbox reject");
146                 ok($np3->quit, "QUIT after APOP fail $mailbox");
147         }
148         for my $mailbox ($group, "$group.0") {
149                 my $u = ('f'x32)."\@$mailbox";
150                 $np3 = Net::POP3->new(@np3_args);
151                 ok($np3->user($u), "UUID\@$mailbox accept");
152                 ok($np3->pass('anonymous'), 'pass works');
153
154                 $np3 = Net::POP3->new(@np3_args);
155                 ok($np3->user($u), "UUID\@$mailbox accept");
156                 ok($np3->pass('anonymous'), 'pass works');
157
158                 my $list = $np3->list;
159                 my $uidl = $np3->uidl;
160                 is_deeply([sort keys %$list], [sort keys %$uidl],
161                         'LIST and UIDL keys match');
162                 ok($_ > 0, 'bytes in LIST result') for values %$list;
163                 like($_, qr/\A[a-z0-9]{40,}\z/,
164                         'blob IDs in UIDL result') for values %$uidl;
165
166                 $np3 = Net::POP3->new(@np3_args);
167                 ok(!$np3->apop($u, 'anonumuss'), 'APOP wrong pass reject');
168
169                 $np3 = Net::POP3->new(@np3_args);
170                 ok($np3->apop($u, 'anonymous'), "APOP UUID\@$mailbox");
171                 my @res = $np3->popstat;
172                 is($res[0], 2, 'STAT knows about 2 messages');
173
174                 my $msg = $np3->get(2);
175                 $msg = join('', @$msg);
176                 $msg =~ s/\r\n/\n/g;
177                 is_deeply(PublicInbox::Eml->new($msg), $patch,
178                         't/data/0001.patch round-tripped');
179
180                 ok(!$np3->get(22), 'missing message');
181
182                 $msg = $np3->top(2, 0);
183                 $msg = join('', @$msg);
184                 $msg =~ s/\r\n/\n/g;
185                 is($msg, $patch->header_obj->as_string . "\n",
186                         'TOP numlines=0');
187
188                 ok(!$np3->top(2, -1), 'negative TOP numlines');
189
190                 $msg = $np3->top(2, 1);
191                 $msg = join('', @$msg);
192                 $msg =~ s/\r\n/\n/g;
193                 is($msg, $patch->header_obj->as_string . <<EOF,
194
195 Filenames within a project tend to be reasonably stable within a
196 EOF
197                         'TOP numlines=1');
198
199                 $msg = $np3->top(2, 10000);
200                 $msg = join('', @$msg);
201                 $msg =~ s/\r\n/\n/g;
202                 is_deeply(PublicInbox::Eml->new($msg), $patch,
203                         'TOP numlines=10000 (excess)');
204
205                 $np3 = Net::POP3->new(@np3_args, %o);
206                 ok($np3->starttls, 'STLS works before APOP');
207                 ok($np3->apop($u, 'anonymous'), "APOP UUID\@$mailbox w/ STLS");
208
209                 # undocumented:
210                 ok($np3->_NOOP, 'NOOP works') if $np3->can('_NOOP');
211         }
212
213         SKIP: {
214                 skip 'TCP_DEFER_ACCEPT is Linux-only', 2 if $^O ne 'linux';
215                 my $var = eval { Socket::TCP_DEFER_ACCEPT() } // 9;
216                 my $x = getsockopt($pop3s, IPPROTO_TCP, $var) //
217                         xbail "IPPROTO_TCP: $!";
218                 ok(unpack('i', $x) > 0, 'TCP_DEFER_ACCEPT set on POP3S');
219                 $x = getsockopt($stls, IPPROTO_TCP, $var) //
220                         xbail "IPPROTO_TCP: $!";
221                 is(unpack('i', $x), 0, 'TCP_DEFER_ACCEPT is 0 on plain POP3');
222         };
223         SKIP: {
224                 skip 'SO_ACCEPTFILTER is FreeBSD-only', 2 if $^O ne 'freebsd';
225                 system('kldstat -m accf_data >/dev/null') and
226                         skip 'accf_data not loaded? kldload accf_data', 2;
227                 require PublicInbox::Daemon;
228                 my $x = getsockopt($pop3s, SOL_SOCKET,
229                                 $PublicInbox::Daemon::SO_ACCEPTFILTER);
230                 like($x, qr/\Adataready\0+\z/, 'got dataready accf for pop3s');
231                 $x = getsockopt($stls, IPPROTO_TCP,
232                                 $PublicInbox::Daemon::SO_ACCEPTFILTER);
233                 is($x, undef, 'no BSD accept filter for plain IMAP');
234         };
235
236         $td->kill;
237         $td->join;
238         is($?, 0, 'no error in exited -netd');
239         open my $fh, '<', $err or BAIL_OUT "open $err failed: $!";
240         my $eout = do { local $/; <$fh> };
241         unlike($eout, qr/wide/i, 'no Wide character warnings in -netd');
242 }
243
244 {
245         my $capa = $oldc->capa;
246         ok(defined($capa->{PIPELINING}), 'pipelining supported by CAPA');
247         is($capa->{EXPIRE}, 0, 'EXPIRE 0 set');
248         ok(!exists $capa->{STLS}, 'STLS unset w/o daemon certs');
249
250         # ensure TOP doesn't trigger "EXPIRE 0" like RETR does (cf. RFC2449)
251         my $list = $oldc->list;
252         ok(scalar keys %$list, 'got a listing of messages');
253         ok($oldc->top($_, 1), "TOP $_ 1") for keys %$list;
254         ok($oldc->quit, 'QUIT after TOP');
255
256         # clients which see "EXPIRE 0" can elide DELE requests
257         $oldc = Net::POP3->new(@old_args);
258         ok($oldc->apop("$locked_mb.0", 'anonymous'), 'APOP for RETR');
259         is_deeply($oldc->capa, $capa, 'CAPA unchanged');
260         is_deeply($oldc->list, $list, 'LIST unchanged by previous TOP');
261         ok($oldc->get($_), "RETR $_") for keys %$list;
262         ok($oldc->quit, 'QUIT after RETR');
263
264         $oldc = Net::POP3->new(@old_args);
265         ok($oldc->apop("$locked_mb.0", 'anonymous'), 'APOP reconnect');
266         my $cont = $oldc->list;
267         is_deeply($cont, {}, 'no messages after implicit DELE from EXPIRE 0');
268         ok($oldc->quit, 'QUIT on noop');
269
270         # test w/o checking CAPA to trigger EXPIRE 0
271         $oldc = Net::POP3->new(@old_args);
272         ok($oldc->apop($locked_mb, 'anonymous'), 'APOP on latest slice');
273         my $l2 = $oldc->list;
274         is_deeply($l2, $list, 'different mailbox, different deletes');
275         ok($oldc->get($_), "RETR $_") for keys %$list;
276         ok($oldc->quit, 'QUIT w/o EXPIRE nor DELE');
277
278         $oldc = Net::POP3->new(@old_args);
279         ok($oldc->apop($locked_mb, 'anonymous'), 'APOP again on latest');
280         $l2 = $oldc->list;
281         is_deeply($l2, $list, 'no DELE nor EXPIRE preserves messages');
282         ok($oldc->delete(2), 'explicit DELE on latest');
283         ok($oldc->quit, 'QUIT w/ highest DELE');
284
285         # this is non-standard behavior, but necessary if we expect hundreds
286         # of thousands of users on cheap HW
287         $oldc = Net::POP3->new(@old_args);
288         ok($oldc->apop($locked_mb, 'anonymous'), 'APOP yet again on latest');
289         is_deeply($oldc->list, {}, 'highest DELE deletes older messages, too');
290 }
291
292 # TODO: more tests, but mpop was really helpful in helping me
293 # figure out bugs with larger newsgroups (>50K messages) which
294 # probably isn't suited for this test suite.
295
296 $old->kill;
297 $old->join;
298 is($?, 0, 'no error in exited -pop3d');
299 open $fh, '<', $olderr or BAIL_OUT "open $olderr failed: $!";
300 my $eout = do { local $/; <$fh> };
301 unlike($eout, qr/wide/i, 'no Wide character warnings in -pop3d');
302
303 done_testing;