]> Sergey Matveev's repositories - public-inbox.git/commitdiff
Merge remote-tracking branch 'origin/email-simple-mem' into master
authorEric Wong <e@80x24.org>
Sun, 30 Jun 2019 17:13:30 +0000 (17:13 +0000)
committerEric Wong <e@80x24.org>
Sun, 30 Jun 2019 17:13:30 +0000 (17:13 +0000)
* origin/email-simple-mem:
  nntp: reduce syscalls for ARTICLE and BODY
  mbox: split header and body processing
  mbox: use Email::Simple->new to do in-place modifications
  nntp: rework and simplify art_lookup response

21 files changed:
MANIFEST
examples/public-inbox-httpd@.service
examples/public-inbox-nntpd@.service
examples/unsubscribe-psgi@.service
lib/PublicInbox/DS.pm
lib/PublicInbox/DSKQXS.pm
lib/PublicInbox/Daemon.pm
lib/PublicInbox/EvCleanup.pm
lib/PublicInbox/HTTP.pm
lib/PublicInbox/HTTPD/Async.pm
lib/PublicInbox/Listener.pm
lib/PublicInbox/NNTP.pm
lib/PublicInbox/ParentPipe.pm
lib/PublicInbox/Qspawn.pm
lib/PublicInbox/Syscall.pm
lib/PublicInbox/TLS.pm
lib/PublicInbox/WatchMaildir.pm
t/ds-leak.t
t/httpd-https.t [new file with mode: 0644]
t/nntpd-tls.t
t/nntpd.t

index 29920953e962b30124c87f24f9f2bef973906761..4cb5f38fd5fb96bedd6344a6bef969d0d99f79e8 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -210,6 +210,7 @@ t/hl_mod.t
 t/html_index.t
 t/httpd-corner.psgi
 t/httpd-corner.t
+t/httpd-https.t
 t/httpd-unix.t
 t/httpd.t
 t/hval.t
index 56117ef09368405ac21683d62b33d14edfda0c63..e811da40497a402e88f1b58a614a8519e7980434 100644 (file)
@@ -20,7 +20,12 @@ ExecStartPre = /bin/mkdir -p -m 1777 /tmp/.pub-inline
 ExecStart = /usr/local/bin/public-inbox-httpd \
 -1 /var/log/public-inbox/httpd.out.log
 StandardError = syslog
+
+# NonBlocking is REQUIRED to avoid a race condition if running
+# simultaneous services
+NonBlocking = true
 Sockets = public-inbox-httpd.socket
+
 KillSignal = SIGQUIT
 User = nobody
 Group = nogroup
index 62202c2fa54e18a9f11da0fe0540f2c904a23864..a879841e6dacde6096c89d666547370cd150f4fe 100644 (file)
@@ -20,7 +20,12 @@ ExecStartPre = /bin/mkdir -p -m 1777 /tmp/.pub-inline
 ExecStart = /usr/local/bin/public-inbox-nntpd \
 -1 /var/log/public-inbox/nntpd.out.log
 StandardError = syslog
+
+# NonBlocking is REQUIRED to avoid a race condition if running
+# simultaneous services
+NonBlocking = true
 Sockets = public-inbox-nntpd.socket
+
 KillSignal = SIGQUIT
 User = nobody
 Group = nogroup
index acc29e8e64c648a448ac4433fd587a17da561f78..c8721fbe1d1bee5247ad721e656a5524dace2a72 100644 (file)
@@ -12,7 +12,12 @@ After = unsubscribe-psgi.socket
 # any PSGI server ought to work,
 # but public-inbox-httpd supports socket activation like unsubscribe.milter
 ExecStart = /usr/local/bin/public-inbox-httpd -W0 /etc/unsubscribe.psgi
+
+# NonBlocking is REQUIRED to avoid a race condition if running
+# simultaneous services
+NonBlocking = true
 Sockets = unsubscribe-psgi.socket
+
 # we need to modify the mlmmj spool
 User = mlmmj
 KillMode = process
index a8700bc53e8c417e7be1bf725ed51e7337e852ba..586c47cdc8cef94e05509dfbf0a82136a147f870 100644 (file)
@@ -28,6 +28,7 @@ use 5.010_001;
 use PublicInbox::Syscall qw(:epoll);
 
 use fields ('sock',              # underlying socket
+            'rbuf',              # scalarref, usually undef
             'wbuf',              # arrayref of coderefs or GLOB refs
             'wbuf_off',  # offset into first element of wbuf to start writing at
             );
@@ -36,6 +37,7 @@ use Errno  qw(EAGAIN EINVAL EEXIST);
 use Carp   qw(croak confess carp);
 require File::Spec;
 
+my $nextq = []; # queue for next_tick
 our (
      %DescriptorMap,             # fd (num) -> PublicInbox::DS object
      $Epoll,                     # Global epoll fd (or DSKQXS ref)
@@ -98,12 +100,6 @@ Returns a timer object which you can call C<< $timer->cancel >> on if you need t
 sub AddTimer {
     my ($class, $secs, $coderef) = @_;
 
-    if (!$secs) {
-        my $timer = bless([0, $coderef], 'PublicInbox::DS::Timer');
-        unshift(@Timers, $timer);
-        return $timer;
-    }
-
     my $fire_time = now() + $secs;
 
     my $timer = bless [$fire_time, $coderef], "PublicInbox::DS::Timer";
@@ -173,9 +169,23 @@ sub FirstTimeEventLoop {
 
 sub now () { clock_gettime(CLOCK_MONOTONIC) }
 
+sub next_tick () {
+    my $q = $nextq;
+    $nextq = [];
+    for (@$q) {
+        if (ref($_) eq 'CODE') {
+            $_->();
+        } else {
+            $_->event_step;
+        }
+    }
+}
+
 # runs timers and returns milliseconds for next one, or next event loop
 sub RunTimers {
-    return $LoopTimeout unless @Timers;
+    next_tick();
+
+    return ((@$nextq || @ToClose) ? 0 : $LoopTimeout) unless @Timers;
 
     my $now = now();
 
@@ -185,6 +195,9 @@ sub RunTimers {
         $to_run->[1]->($now) if $to_run->[1];
     }
 
+    # timers may enqueue into nextq:
+    return 0 if (@$nextq || @ToClose);
+
     return $LoopTimeout unless @Timers;
 
     # convert time to an even number of milliseconds, adding 1
@@ -246,17 +259,8 @@ sub PostEventLoop {
     # now we can close sockets that wanted to close during our event processing.
     # (we didn't want to close them during the loop, as we didn't want fd numbers
     #  being reused and confused during the event loop)
-    while (my $sock = shift @ToClose) {
-        my $fd = fileno($sock);
-
-        # close the socket. (not a PublicInbox::DS close)
-        CORE::close($sock);
-
-        # and now we can finally remove the fd from the map.  see
-        # comment above in ->close.
-        delete $DescriptorMap{$fd};
-    }
-
+    delete($DescriptorMap{fileno($_)}) for @ToClose;
+    @ToClose = (); # let refcounting drop everything all at once
 
     # by default we keep running, unless a postloop callback (either per-object
     # or global) cancels it
@@ -317,6 +321,8 @@ sub new {
 ### I N S T A N C E   M E T H O D S
 #####################################################################
 
+sub requeue ($) { push @$nextq, $_[0] }
+
 =head2 C<< $obj->close >>
 
 Close the socket.
@@ -373,6 +379,10 @@ sub psendfile ($$$) {
     $written;
 }
 
+sub epbit ($$) { # (sock, default)
+    ref($_[0]) eq 'IO::Socket::SSL' ? PublicInbox::TLS::epollbit() : $_[1];
+}
+
 # returns 1 if done, 0 if incomplete
 sub flush_write ($) {
     my ($self) = @_;
@@ -391,8 +401,8 @@ next_buf:
                         goto next_buf;
                     }
                 } elsif ($! == EAGAIN) {
+                    epwait($sock, epbit($sock, EPOLLOUT) | EPOLLONESHOT);
                     $self->{wbuf_off} = $off;
-                    watch($self, EPOLLOUT|EPOLLONESHOT);
                     return 0;
                 } else {
                     return $self->close;
@@ -412,17 +422,25 @@ next_buf:
     1; # all done
 }
 
-sub do_read ($$$$) {
+sub rbuf_idle ($$) {
+    my ($self, $rbuf) = @_;
+    if ($$rbuf eq '') { # who knows how long till we can read again
+        delete $self->{rbuf};
+    } else {
+        $self->{rbuf} = $rbuf;
+    }
+}
+
+sub do_read ($$$;$) {
     my ($self, $rbuf, $len, $off) = @_;
-    my $r = sysread($self->{sock}, $$rbuf, $len, $off);
+    my $r = sysread(my $sock = $self->{sock}, $$rbuf, $len, $off // 0);
     return ($r == 0 ? $self->close : $r) if defined $r;
     # common for clients to break connections without warning,
     # would be too noisy to log here:
-    if (ref($self) eq 'IO::Socket::SSL') {
-        my $ev = PublicInbox::TLS::epollbit() or return $self->close;
-        watch($self, $ev | EPOLLONESHOT);
-    } elsif ($! == EAGAIN) {
-        watch($self, EPOLLIN | EPOLLONESHOT);
+    if ($! == EAGAIN) {
+        epwait($sock, epbit($sock, EPOLLIN) | EPOLLONESHOT);
+        rbuf_idle($self, $rbuf);
+        0;
     } else {
         $self->close;
     }
@@ -499,17 +517,20 @@ sub write {
 
         if (defined $written) {
             return 1 if $written == $to_write;
+            requeue($self); # runs: event_step -> flush_write
         } elsif ($! == EAGAIN) {
+            epwait($sock, epbit($sock, EPOLLOUT) | EPOLLONESHOT);
             $written = 0;
         } else {
             return $self->close;
         }
+
+        # deal with EAGAIN or partial write:
         my $tmpio = tmpio($self, $bref, $written) or return 0;
 
         # wbuf may be an empty array if we're being called inside
         # ->flush_write via CODE bref:
         push @{$self->{wbuf} ||= []}, $tmpio;
-        watch($self, EPOLLOUT|EPOLLONESHOT);
         return 0;
     }
 }
@@ -528,46 +549,39 @@ sub msg_more ($$) {
             # queue up the unwritten substring:
             my $tmpio = tmpio($self, \($_[1]), $n) or return 0;
             $self->{wbuf} = [ $tmpio ];
-            watch($self, EPOLLOUT|EPOLLONESHOT);
+            epwait($sock, EPOLLOUT|EPOLLONESHOT);
             return 0;
         }
     }
     $self->write(\($_[1]));
 }
 
-sub watch ($$) {
-    my ($self, $ev) = @_;
-    my $sock = $self->{sock} or return;
+sub epwait ($$) {
+    my ($sock, $ev) = @_;
     epoll_ctl($Epoll, EPOLL_CTL_MOD, fileno($sock), $ev) and
         confess("EPOLL_CTL_MOD $!");
-    0;
 }
 
-sub watch_in1 ($) { watch($_[0], EPOLLIN | EPOLLONESHOT) }
-
 # return true if complete, false if incomplete (or failure)
 sub accept_tls_step ($) {
     my ($self) = @_;
     my $sock = $self->{sock} or return;
     return 1 if $sock->accept_SSL;
     return $self->close if $! != EAGAIN;
-    if (my $ev = PublicInbox::TLS::epollbit()) {
-        unshift @{$self->{wbuf} ||= []}, \&accept_tls_step;
-        return watch($self, $ev | EPOLLONESHOT);
-    }
-    drop($self, 'BUG? EAGAIN but '.PublicInbox::TLS::err());
+    epwait($sock, PublicInbox::TLS::epollbit() | EPOLLONESHOT);
+    unshift @{$self->{wbuf} ||= []}, \&accept_tls_step;
+    0;
 }
 
+# return true if complete, false if incomplete (or failure)
 sub shutdn_tls_step ($) {
     my ($self) = @_;
     my $sock = $self->{sock} or return;
     return $self->close if $sock->stop_SSL(SSL_fast_shutdown => 1);
     return $self->close if $! != EAGAIN;
-    if (my $ev = PublicInbox::TLS::epollbit()) {
-        unshift @{$self->{wbuf} ||= []}, \&shutdn_tls_step;
-        return watch($self, $ev | EPOLLONESHOT);
-    }
-    drop($self, 'BUG? EAGAIN but '.PublicInbox::TLS::err());
+    epwait($sock, PublicInbox::TLS::epollbit() | EPOLLONESHOT);
+    unshift @{$self->{wbuf} ||= []}, \&shutdn_tls_step;
+    0;
 }
 
 # don't bother with shutdown($sock, 2), we don't fork+exec w/o CLOEXEC
@@ -581,7 +595,6 @@ sub shutdn ($) {
        $self->close;
     }
 }
-
 package PublicInbox::DS::Timer;
 # [$abs_float_firetime, $coderef];
 sub cancel {
index 364df3d68cd77838dfe56cb8598ea854e4e12488..1c3b970b7a332d3fd681fd885157005c70fea504 100644 (file)
@@ -16,7 +16,8 @@ use warnings;
 use parent qw(IO::KQueue);
 use parent qw(Exporter);
 use IO::KQueue;
-use PublicInbox::Syscall qw(EPOLLONESHOT EPOLLIN EPOLLOUT EPOLL_CTL_DEL);
+use PublicInbox::Syscall qw(EPOLLONESHOT EPOLLIN EPOLLOUT EPOLLET
+       EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL);
 our @EXPORT_OK = qw(epoll_ctl epoll_wait);
 my $owner_pid = -1; # kqueue is close-on-fork (yes, fork, not exec)
 
@@ -24,10 +25,15 @@ my $owner_pid = -1; # kqueue is close-on-fork (yes, fork, not exec)
 sub kq_flag ($$) {
        my ($bit, $ev) = @_;
        if ($ev & $bit) {
-               my $fl = EV_ADD | EV_ENABLE;
-               ($ev & EPOLLONESHOT) ? ($fl | EV_ONESHOT) : $fl;
+               my $fl = EV_ENABLE;
+               $fl |= EV_CLEAR if $fl & EPOLLET;
+
+               # EV_DISPATCH matches EPOLLONESHOT semantics more closely
+               # than EV_ONESHOT, in that EV_ADD is not required to
+               # re-enable a disabled watch.
+               ($ev & EPOLLONESHOT) ? ($fl | EV_DISPATCH) : $fl;
        } else {
-               EV_ADD | EV_DISABLE;
+               EV_DISABLE;
        }
 }
 
@@ -40,9 +46,15 @@ sub new {
 
 sub epoll_ctl {
        my ($self, $op, $fd, $ev) = @_;
-       if ($op != EPOLL_CTL_DEL) {
+       if ($op == EPOLL_CTL_MOD) {
                $self->EV_SET($fd, EVFILT_READ, kq_flag(EPOLLIN, $ev));
                $self->EV_SET($fd, EVFILT_WRITE, kq_flag(EPOLLOUT, $ev));
+       } elsif ($op == EPOLL_CTL_DEL) {
+               $self->EV_SET($fd, EVFILT_READ, EV_DISABLE);
+               $self->EV_SET($fd, EVFILT_WRITE, EV_DISABLE);
+       } else {
+               $self->EV_SET($fd, EVFILT_READ, EV_ADD|kq_flag(EPOLLIN, $ev));
+               $self->EV_SET($fd, EVFILT_WRITE, EV_ADD|kq_flag(EPOLLOUT, $ev));
        }
        0;
 }
index cf011a2020b560d544766021435b822d4e32ff21..2b7ac266dfabbccfa83c106c0a114d2f1a446faf 100644 (file)
@@ -252,13 +252,11 @@ sub daemonize () {
 
 
 sub worker_quit {
-       my ($reason) = @_;
        # killing again terminates immediately:
        exit unless @listeners;
 
        $_->close foreach @listeners; # call PublicInbox::DS::close
        @listeners = ();
-       $reason->close if ref($reason) eq 'PublicInbox::ParentPipe';
 
        my $proc_name;
        my $warn = 0;
@@ -590,7 +588,7 @@ sub daemon_loop ($$$$) {
        } else {
                reopen_logs();
                $set_user->() if $set_user;
-               $SIG{USR2} = sub { worker_quit('USR2') if upgrade() };
+               $SIG{USR2} = sub { worker_quit() if upgrade() };
                $refresh->();
        }
        $uid = $gid = undef;
index 33b54ebc05fb6ef0c7af0d1cbbba7d1378e805e5..be6672edcdb09d0db6f37f65b1baa7798fe247ce 100644 (file)
@@ -1,80 +1,23 @@
-# Copyright (C) 2016-2018 all contributors <meta@public-inbox.org>
+# Copyright (C) 2016-2019 all contributors <meta@public-inbox.org>
 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
 
-# event cleanups (currently for PublicInbox::DS)
+# event cleanups (for PublicInbox::DS)
 package PublicInbox::EvCleanup;
 use strict;
 use warnings;
-use base qw(PublicInbox::DS);
-use PublicInbox::Syscall qw(EPOLLOUT EPOLLONESHOT);
+require PublicInbox::DS;
 
+# this only runs under public-inbox-{httpd/nntpd}, not generic PSGI servers
 my $ENABLED;
 sub enabled { $ENABLED }
 sub enable { $ENABLED = 1 }
-my $singleton;
-my $asapq = [ [], undef ];
-my $nextq = [ [], undef ];
 my $laterq = [ [], undef ];
 
-sub once_init () {
-       my $self = fields::new('PublicInbox::EvCleanup');
-       my ($r, $w);
-
-       # This is a dummy pipe which is always writable so it can always
-       # fires in the next event loop iteration.
-       pipe($r, $w) or die "pipe: $!";
-       fcntl($w, 1031, 4096) if $^O eq 'linux'; # 1031: F_SETPIPE_SZ
-       $self->SUPER::new($w, 0);
-
-       # always writable, since PublicInbox::EvCleanup::event_step
-       # never drains wbuf.  We can avoid wasting a hash slot by
-       # stuffing the read-end of the pipe into the never-to-be-touched
-       # wbuf
-       $self->{wbuf} = $r;
-       $self;
-}
-
-sub _run_all ($) {
-       my ($q) = @_;
-
-       my $run = $q->[0];
-       $q->[0] = [];
-       $q->[1] = undef;
-       $_->() foreach @$run;
-}
-
-# ensure PublicInbox::DS::ToClose processing after timers fire
-sub _asap_close () { $asapq->[1] ||= _asap_timer() }
-
-# Called by PublicInbox::DS
-sub event_step { _run_all($asapq) }
-
-sub _run_next () {
-       _run_all($nextq);
-       _asap_close();
-}
-
 sub _run_later () {
-       _run_all($laterq);
-       _asap_close();
-}
-
-sub _asap_timer () {
-       $singleton ||= once_init();
-       $singleton->watch(EPOLLOUT|EPOLLONESHOT);
-       1;
-}
-
-sub asap ($) {
-       my ($cb) = @_;
-       push @{$asapq->[0]}, $cb;
-       $asapq->[1] ||= _asap_timer();
-}
-
-sub next_tick ($) {
-       my ($cb) = @_;
-       push @{$nextq->[0]}, $cb;
-       $nextq->[1] ||= PublicInbox::DS->AddTimer(0, *_run_next);
+       my $run = $laterq->[0];
+       $laterq->[0] = [];
+       $laterq->[1] = undef;
+       $_->() foreach @$run;
 }
 
 sub later ($) {
@@ -83,10 +26,5 @@ sub later ($) {
        $laterq->[1] ||= PublicInbox::DS->AddTimer(60, *_run_later);
 }
 
-END {
-       event_step();
-       _run_all($nextq);
-       _run_all($laterq);
-}
-
+END { _run_later() }
 1;
index a1cb4aca9469c236bb090fa92c5deaafa834e009..5546ac465570d123d19498ef285b385b3b1ba913 100644 (file)
@@ -11,7 +11,7 @@ package PublicInbox::HTTP;
 use strict;
 use warnings;
 use base qw(PublicInbox::DS);
-use fields qw(httpd env rbuf input_left remote_addr remote_port forward pull);
+use fields qw(httpd env input_left remote_addr remote_port forward pull);
 use bytes (); # only for bytes::length
 use Fcntl qw(:seek);
 use Plack::HTTPParser qw(parse_http_request); # XS or pure Perl
@@ -30,10 +30,8 @@ use constant {
 use Errno qw(EAGAIN);
 
 my $pipelineq = [];
-my $pipet;
 sub process_pipelineq () {
        my $q = $pipelineq;
-       $pipet = undef;
        $pipelineq = [];
        foreach (@$q) {
                next unless $_->{sock};
@@ -58,9 +56,16 @@ sub http_date () {
 sub new ($$$) {
        my ($class, $sock, $addr, $httpd) = @_;
        my $self = fields::new($class);
-       $self->SUPER::new($sock, EPOLLIN | EPOLLONESHOT);
+       my $ev = EPOLLIN;
+       my $wbuf;
+       if (ref($sock) eq 'IO::Socket::SSL' && !$sock->accept_SSL) {
+               return CORE::close($sock) if $! != EAGAIN;
+               $ev = PublicInbox::TLS::epollbit();
+               $wbuf = [ \&PublicInbox::DS::accept_tls_step ];
+       }
+       $self->SUPER::new($sock, $ev | EPOLLONESHOT);
        $self->{httpd} = $httpd;
-       $self->{rbuf} = '';
+       $self->{wbuf} = $wbuf if $wbuf;
        ($self->{remote_addr}, $self->{remote_port}) =
                PublicInbox::Daemon::host_with_port($addr);
        $self;
@@ -75,31 +80,34 @@ sub event_step { # called by PublicInbox::DS
        # otherwise we can be buffering infinitely w/o backpressure
 
        return read_input($self) if defined $self->{env};
-       my $rbuf = \($self->{rbuf});
-       my $off = bytes::length($$rbuf);
-       $self->do_read($rbuf, 8192, $off) and rbuf_process($self);
+       my $rbuf = $self->{rbuf} // (\(my $x = ''));
+       $self->do_read($rbuf, 8192, bytes::length($$rbuf)) or return;
+       rbuf_process($self, $rbuf);
 }
 
 sub rbuf_process {
-       my ($self) = @_;
+       my ($self, $rbuf) = @_;
+       $rbuf //= $self->{rbuf} // (\(my $x = ''));
 
        my %env = %{$self->{httpd}->{env}}; # full hash copy
-       my $r = parse_http_request($self->{rbuf}, \%env);
+       my $r = parse_http_request($$rbuf, \%env);
 
        # We do not support Trailers in chunked requests, for now
        # (they are rarely-used and git (as of 2.7.2) does not use them)
        if ($r == -1 || $env{HTTP_TRAILER} ||
                        # this length-check is necessary for PURE_PERL=1:
-                       ($r == -2 && bytes::length($self->{rbuf}) > 0x4000)) {
+                       ($r == -2 && bytes::length($$rbuf) > 0x4000)) {
                return quit($self, 400);
        }
-       return $self->watch_in1 if $r < 0; # incomplete
-       $self->{rbuf} = substr($self->{rbuf}, $r);
-
+       if ($r < 0) { # incomplete
+               $self->rbuf_idle($rbuf);
+               return $self->requeue;
+       }
+       $$rbuf = substr($$rbuf, $r);
        my $len = input_prepare($self, \%env);
        defined $len or return write_err($self, undef); # EMFILE/ENFILE
 
-       $len ? read_input($self) : app_dispatch($self);
+       $len ? read_input($self, $rbuf) : app_dispatch($self, undef, $rbuf);
 }
 
 # IO::Handle::write returns boolean, this returns bytes written:
@@ -111,16 +119,15 @@ sub xwrite ($$$) {
        $w;
 }
 
-sub read_input ($) {
-       my ($self) = @_;
+sub read_input ($;$) {
+       my ($self, $rbuf) = @_;
+       $rbuf //= $self->{rbuf} // (\(my $x = ''));
        my $env = $self->{env};
        return if $env->{REMOTE_ADDR}; # in app dispatch
-       return read_input_chunked($self) if env_chunked($env);
+       return read_input_chunked($self, $rbuf) if env_chunked($env);
 
        # env->{CONTENT_LENGTH} (identity)
-       my $sock = $self->{sock};
        my $len = delete $self->{input_left};
-       my $rbuf = \($self->{rbuf});
        my $input = $env->{'psgi.input'};
 
        while ($len > 0) {
@@ -135,15 +142,15 @@ sub read_input ($) {
                        }
                        $$rbuf = '';
                }
-               my $r = sysread($sock, $$rbuf, 8192);
-               return recv_err($self, $r, $len) unless $r;
+               $self->do_read($rbuf, 8192) or return recv_err($self, $len);
                # continue looping if $r > 0;
        }
-       app_dispatch($self, $input);
+       app_dispatch($self, $input, $rbuf);
 }
 
 sub app_dispatch {
-       my ($self, $input) = @_;
+       my ($self, $input, $rbuf) = @_;
+       $self->rbuf_idle($rbuf);
        my $env = $self->{env};
        $env->{REMOTE_ADDR} = $self->{remote_addr};
        $env->{REMOTE_PORT} = $self->{remote_port};
@@ -235,11 +242,12 @@ sub identity_wcb ($) {
 
 sub next_request ($) {
        my ($self) = @_;
-       if ($self->{rbuf} eq '') { # wait for next request
-               $self->watch_in1;
-       } else { # avoid recursion for pipelined requests
+       if ($self->{rbuf}) {
+               # avoid recursion for pipelined requests
+               PublicInbox::DS::requeue(\&process_pipelineq) if !@$pipelineq;
                push @$pipelineq, $self;
-               $pipet ||= PublicInbox::EvCleanup::asap(*process_pipelineq);
+       } else { # wait for next request
+               $self->requeue;
        }
 }
 
@@ -267,7 +275,7 @@ sub getline_cb ($$$) {
                                if ($self->{wbuf}) {
                                        $self->write($next);
                                } else {
-                                       PublicInbox::EvCleanup::asap($next);
+                                       PublicInbox::DS::requeue($next);
                                }
                                return;
                        }
@@ -360,27 +368,25 @@ sub write_err {
 }
 
 sub recv_err {
-       my ($self, $r, $len) = @_;
-       return $self->close if (defined $r && $r == 0);
-       if ($! == EAGAIN) {
+       my ($self, $len) = @_;
+       if ($! == EAGAIN) { # epoll/kevent watch already set by do_read
                $self->{input_left} = $len;
-               return $self->watch_in1;
+       } else {
+               err($self, "error reading input: $! ($len bytes remaining)");
        }
-       err($self, "error reading for input: $! ($len bytes remaining)");
-       quit($self, 500);
 }
 
 sub read_input_chunked { # unlikely...
-       my ($self) = @_;
+       my ($self, $rbuf) = @_;
+       $rbuf //= $self->{rbuf} // (\(my $x = ''));
        my $input = $self->{env}->{'psgi.input'};
-       my $sock = $self->{sock};
        my $len = delete $self->{input_left};
-       my $rbuf = \($self->{rbuf});
 
        while (1) { # chunk start
                if ($len == CHUNK_ZEND) {
                        $$rbuf =~ s/\A\r\n//s and
-                               return app_dispatch($self, $input);
+                               return app_dispatch($self, $input, $rbuf);
+
                        return quit($self, 400) if bytes::length($$rbuf) > 2;
                }
                if ($len == CHUNK_END) {
@@ -403,9 +409,8 @@ sub read_input_chunked { # unlikely...
                }
 
                if ($len < 0) { # chunk header is trickled, read more
-                       my $off = bytes::length($$rbuf);
-                       my $r = sysread($sock, $$rbuf, 8192, $off);
-                       return recv_err($self, $r, $len) unless $r;
+                       $self->do_read($rbuf, 8192, bytes::length($$rbuf)) or
+                               return recv_err($self, $len);
                        # (implicit) goto chunk_start if $r > 0;
                }
                $len = CHUNK_ZEND if $len == 0;
@@ -429,8 +434,8 @@ sub read_input_chunked { # unlikely...
                        }
                        if ($$rbuf eq '') {
                                # read more of current chunk
-                               my $r = sysread($sock, $$rbuf, 8192);
-                               return recv_err($self, $r, $len) unless $r;
+                               $self->do_read($rbuf, 8192) or
+                                       return recv_err($self, $len);
                        }
                }
        }
@@ -459,14 +464,7 @@ sub close {
 # for graceful shutdown in PublicInbox::Daemon:
 sub busy () {
        my ($self) = @_;
-       ($self->{rbuf} ne '' || $self->{env} || $self->{wbuf});
+       ($self->{rbuf} || $self->{env} || $self->{wbuf});
 }
 
-# fires after pending writes are complete:
-sub restart_pass ($) {
-       $_[0]->{forward}->restart_read; # see PublicInbox::HTTPD::Async
-}
-
-sub enqueue_restart_pass ($) { $_[0]->write(\&restart_pass) }
-
 1;
index b46baeb2804f44a88244b35b7211fabda72da16a..584db8d4ba5bd14c2e16408d5e41840fb1b1cad6 100644 (file)
@@ -4,14 +4,15 @@
 # XXX This is a totally unstable API for public-inbox internal use only
 # This is exposed via the 'pi-httpd.async' key in the PSGI env hash.
 # The name of this key is not even stable!
-# Currently is is intended for use with read-only pipes.
+# Currently intended for use with read-only pipes with expensive
+# processes such as git-http-backend(1), cgit(1)
 package PublicInbox::HTTPD::Async;
 use strict;
 use warnings;
 use base qw(PublicInbox::DS);
 use fields qw(cb cleanup);
-require PublicInbox::EvCleanup;
 use Errno qw(EAGAIN);
+use PublicInbox::Syscall qw(EPOLLIN EPOLLET);
 
 sub new {
        my ($class, $io, $cb, $cleanup) = @_;
@@ -19,47 +20,35 @@ sub new {
        # no $io? call $cb at the top of the next event loop to
        # avoid recursion:
        unless (defined($io)) {
-               PublicInbox::EvCleanup::asap($cb) if $cb;
-               PublicInbox::EvCleanup::next_tick($cleanup) if $cleanup;
+               PublicInbox::DS::requeue($cb);
+               die 'cleanup unsupported w/o $io' if $cleanup;
                return;
        }
 
        my $self = fields::new($class);
        IO::Handle::blocking($io, 0);
-       $self->SUPER::new($io, PublicInbox::DS::EPOLLIN());
+       $self->SUPER::new($io, EPOLLIN | EPOLLET);
        $self->{cb} = $cb;
        $self->{cleanup} = $cleanup;
        $self;
 }
 
-sub restart_read ($) { $_[0]->watch(PublicInbox::DS::EPOLLIN()) }
-
-sub main_cb ($$$) {
-       my ($http, $fh, $bref) = @_;
+sub main_cb ($$) {
+       my ($http, $fh) = @_;
        sub {
                my ($self) = @_;
-               my $r = sysread($self->{sock}, $$bref, 8192);
+               my $r = sysread($self->{sock}, my $buf, 65536);
                if ($r) {
-                       $fh->write($$bref); # may call $http->close
-
+                       $fh->write($buf); # may call $http->close
                        if ($http->{sock}) { # !closed
-                               if ($http->{wbuf}) {
-                                       # HTTP client could not keep up, so
-                                       # stop reading and buffering.
-                                       $self->watch(0);
-
-                                       # Tell the HTTP socket to restart us
-                                       # when HTTP client is done draining
-                                       # $http->{wbuf}:
-                                       $http->enqueue_restart_pass;
-                               }
-                               # stay in EPOLLIN, but let other clients
-                               # get some work done, too.
+                               $self->requeue;
+                               # let other clients get some work done, too
                                return;
                        }
-                       # fall through to close below...
-               } elsif (!defined $r) {
-                       return restart_read($self) if $! == EAGAIN;
+
+                       # else: fall through to close below...
+               } elsif (!defined $r && $! == EAGAIN) {
+                       return; # EPOLLET means we'll be notified
                }
 
                # Done! Error handling will happen in $fh->close
@@ -75,10 +64,16 @@ sub async_pass {
        # will automatically close this ($self) object.
        $http->{forward} = $self;
        $fh->write($$bref); # PublicInbox:HTTP::{chunked,identity}_wcb
-       $self->{cb} = main_cb($http, $fh, $bref);
+       $$bref = undef; # we're done with this
+       my $cb = $self->{cb} = main_cb($http, $fh);
+       $cb->($self); # either hit EAGAIN or ->requeue to keep EPOLLET happy
 }
 
-sub event_step { $_[0]->{cb}->(@_) }
+sub event_step {
+       # {cb} may be undef after ->requeue due to $http->close happening
+       my $cb = $_[0]->{cb} or return;
+       $cb->(@_);
+}
 
 sub close {
        my $self = $_[0];
@@ -87,7 +82,7 @@ sub close {
 
        # we defer this to the next timer loop since close is deferred
        if (my $cleanup = delete $self->{cleanup}) {
-               PublicInbox::EvCleanup::next_tick($cleanup);
+               PublicInbox::DS::requeue($cleanup);
        }
 }
 
index 94b2aed4db632c597eac684057e26264f80859eb..594dabb83e38eacd2b2733c952c205e3ffdc2456 100644 (file)
@@ -9,6 +9,7 @@ use base 'PublicInbox::DS';
 use Socket qw(SOL_SOCKET SO_KEEPALIVE IPPROTO_TCP TCP_NODELAY);
 use fields qw(post_accept);
 require IO::Handle;
+use PublicInbox::Syscall qw(EPOLLIN EPOLLEXCLUSIVE EPOLLET);
 
 sub new ($$$) {
        my ($class, $s, $cb) = @_;
@@ -17,15 +18,14 @@ sub new ($$$) {
        listen($s, 1024);
        IO::Handle::blocking($s, 0);
        my $self = fields::new($class);
-       $self->SUPER::new($s, PublicInbox::DS::EPOLLIN()|
-                             PublicInbox::DS::EPOLLEXCLUSIVE());
+       $self->SUPER::new($s, EPOLLIN|EPOLLET|EPOLLEXCLUSIVE);
        $self->{post_accept} = $cb;
        $self
 }
 
 sub event_step {
        my ($self) = @_;
-       my $sock = $self->{sock};
+       my $sock = $self->{sock} or return;
 
        # no loop here, we want to fairly distribute clients
        # between multiple processes sharing the same socket
@@ -35,6 +35,7 @@ sub event_step {
        if (my $addr = accept(my $c, $sock)) {
                IO::Handle::blocking($c, 0); # no accept4 :<
                $self->{post_accept}->($c, $addr, $sock);
+               $self->requeue;
        }
 }
 
index 5a886a3c32be100b0d24f393b8792846d0fcfd24..26bc679f2996b67e0008b8f90bfb1a1d9732521f 100644 (file)
@@ -6,7 +6,7 @@ package PublicInbox::NNTP;
 use strict;
 use warnings;
 use base qw(PublicInbox::DS);
-use fields qw(nntpd article rbuf ng);
+use fields qw(nntpd article ng);
 use PublicInbox::Search;
 use PublicInbox::Msgmap;
 use PublicInbox::MID qw(mid_escape);
@@ -38,20 +38,6 @@ my %DISABLED; # = map { $_ => 1 } qw(xover list_overview_fmt newnews xhdr);
 my $EXPMAP; # fd -> [ idle_time, $self ]
 my $expt;
 our $EXPTIME = 180; # 3 minutes
-my $nextt;
-
-my $nextq = [];
-sub next_tick () {
-       $nextt = undef;
-       my $q = $nextq;
-       $nextq = [];
-       event_step($_) for @$q;
-}
-
-sub requeue ($) {
-       push @$nextq, $_[0];
-       $nextt ||= PublicInbox::EvCleanup::asap(*next_tick);
-}
 
 sub update_idle_time ($) {
        my ($self) = @_;
@@ -64,14 +50,11 @@ sub expire_old () {
        my $exp = $EXPTIME;
        my $old = $now - $exp;
        my $nr = 0;
-       my $closed = 0;
        my %new;
        while (my ($fd, $v) = each %$EXPMAP) {
                my ($idle_time, $nntp) = @$v;
                if ($idle_time < $old) {
-                       if ($nntp->shutdn) {
-                               $closed++;
-                       } else {
+                       if (!$nntp->shutdn) {
                                ++$nr;
                                $new{$fd} = $v;
                        }
@@ -81,14 +64,7 @@ sub expire_old () {
                }
        }
        $EXPMAP = \%new;
-       if ($nr) {
-               $expt = PublicInbox::EvCleanup::later(*expire_old);
-       } else {
-               $expt = undef;
-               # noop to kick outselves out of the loop ASAP so descriptors
-               # really get closed
-               PublicInbox::EvCleanup::asap(sub {}) if $closed;
-       }
+       $expt = PublicInbox::EvCleanup::later(*expire_old) if $nr;
 }
 
 sub greet ($) { $_[0]->write($_[0]->{nntpd}->{greet}) };
@@ -99,7 +75,8 @@ sub new ($$$) {
        my $ev = EPOLLIN;
        my $wbuf;
        if (ref($sock) eq 'IO::Socket::SSL' && !$sock->accept_SSL) {
-               $ev = PublicInbox::TLS::epollbit() or return CORE::close($sock);
+               return CORE::close($sock) if $! != EAGAIN;
+               $ev = PublicInbox::TLS::epollbit();
                $wbuf = [ \&PublicInbox::DS::accept_tls_step, \&greet ];
        }
        $self->SUPER::new($sock, $ev | EPOLLONESHOT);
@@ -654,12 +631,12 @@ sub long_response ($$) {
                        push @$wbuf, $long_cb;
 
                        # wbuf may be populated by $cb, no need to rearm if so:
-                       requeue($self) if scalar(@$wbuf) == 1;
+                       $self->requeue if scalar(@$wbuf) == 1;
                } else { # all done!
                        $long_cb = undef;
                        res($self, '.');
                        out($self, " deferred[$fd] done - %0.6f", now() - $t0);
-                       requeue($self) unless $self->{wbuf};
+                       $self->requeue unless $self->{wbuf};
                }
        };
        $self->write($long_cb); # kick off!
@@ -914,7 +891,7 @@ sub cmd_starttls ($) {
                return '580 can not initiate TLS negotiation';
        res($self, '382 Continue with TLS negotiation');
        $self->{sock} = IO::Socket::SSL->start_SSL($sock, %$opt);
-       requeue($self) if PublicInbox::DS::accept_tls_step($self);
+       $self->requeue if PublicInbox::DS::accept_tls_step($self);
        undef;
 }
 
@@ -984,16 +961,12 @@ sub event_step {
        return $self->close if $r < 0;
        my $len = bytes::length($$rbuf);
        return $self->close if ($len >= LINE_MAX);
-       if ($len) {
-               $self->{rbuf} = $rbuf;
-       } else {
-               delete $self->{rbuf};
-       }
+       $self->rbuf_idle($rbuf);
        update_idle_time($self);
 
        # maybe there's more pipelined data, or we'll have
        # to register it for socket-readiness notifications
-       requeue($self) unless $self->{wbuf};
+       $self->requeue unless $self->{wbuf};
 }
 
 sub not_idle_long ($$) {
index ccc0815ea46d5400ba591d2457aba220a9b2c2f1..2e2abb5fd3172303507fc9990d9e707da6a22111 100644 (file)
@@ -1,20 +1,27 @@
 # Copyright (C) 2016-2018 all contributors <meta@public-inbox.org>
 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
-# only for PublicInbox::Daemon
+
+# only for PublicInbox::Daemon, allows worker processes to be
+# notified if the master process dies.
 package PublicInbox::ParentPipe;
 use strict;
 use warnings;
 use base qw(PublicInbox::DS);
 use fields qw(cb);
+use PublicInbox::Syscall qw(EPOLLIN EPOLLONESHOT);
 
 sub new ($$$) {
-       my ($class, $pipe, $cb) = @_;
+       my ($class, $pipe, $worker_quit) = @_;
        my $self = fields::new($class);
-       $self->SUPER::new($pipe, PublicInbox::DS::EPOLLIN());
-       $self->{cb} = $cb;
+       $self->SUPER::new($pipe, EPOLLIN|EPOLLONESHOT);
+       $self->{cb} = $worker_quit;
        $self;
 }
 
-sub event_step { $_[0]->{cb}->($_[0]) }
+# master process died, time to call worker_quit ourselves
+sub event_step {
+       $_[0]->close; # PublicInbox::DS::close
+       $_[0]->{cb}->();
+}
 
 1;
index f2630a0f0109a624690edf38b849423b9b0af8dd..8f0b9fe240ebc378a81ce86a7207f73d71ec4f63 100644 (file)
@@ -128,7 +128,7 @@ sub psgi_qx {
        my $rpipe; # comes from popen_rd
        my $async = $env->{'pi-httpd.async'};
        my $cb = sub {
-               my $r = sysread($rpipe, my $buf, 8192);
+               my $r = sysread($rpipe, my $buf, 65536);
                if ($async) {
                        $async->async_pass($env->{'psgix.io'}, $qx, \$buf);
                } elsif (defined $r) {
index 500efa67016f80149f3723d08f0e11f5e8af71fa..d7e15c72c003e800c697449aaeea075fd9456c1f 100644 (file)
@@ -22,7 +22,7 @@ use vars qw(@ISA @EXPORT_OK %EXPORT_TAGS $VERSION);
 $VERSION     = "0.25";
 @ISA         = qw(Exporter);
 @EXPORT_OK   = qw(sendfile epoll_ctl epoll_create epoll_wait
-                  EPOLLIN EPOLLOUT
+                  EPOLLIN EPOLLOUT EPOLLET
                   EPOLL_CTL_ADD EPOLL_CTL_DEL EPOLL_CTL_MOD
                   EPOLLONESHOT EPOLLEXCLUSIVE);
 %EXPORT_TAGS = (epoll => [qw(epoll_ctl epoll_create epoll_wait
@@ -39,7 +39,7 @@ use constant EPOLLOUT      => 4;
 # use constant EPOLLRDBAND   => 128;
 use constant EPOLLEXCLUSIVE => (1 << 28);
 use constant EPOLLONESHOT => (1 << 30);
-use constant EPOLLET => (1 << 31);
+use constant EPOLLET => (1 << 31);
 use constant EPOLL_CTL_ADD => 1;
 use constant EPOLL_CTL_DEL => 2;
 use constant EPOLL_CTL_MOD => 3;
index 576c11d7cc0f917c70c646a98254eb3146306827..0b9a55dfd237c22bfd1aa24c3b9bdeef9cd447ff 100644 (file)
@@ -13,12 +13,9 @@ sub err () { $SSL_ERROR }
 
 # returns the EPOLL event bit which matches the existing SSL error
 sub epollbit () {
-       if ($! == EAGAIN) {
-               return EPOLLIN if $SSL_ERROR == SSL_WANT_READ;
-               return EPOLLOUT if $SSL_ERROR == SSL_WANT_WRITE;
-               die "unexpected SSL error: $SSL_ERROR";
-       }
-       0;
+       return EPOLLIN if $SSL_ERROR == SSL_WANT_READ;
+       return EPOLLOUT if $SSL_ERROR == SSL_WANT_WRITE;
+       die "unexpected SSL error: $SSL_ERROR";
 }
 
 1;
index 2d4c6f4340af5d34b01148be0878743be52fc5eb..a76bf06e629db1c5b5381ee04e038fd48b970790 100644 (file)
@@ -150,6 +150,12 @@ sub _try_path {
        if (!ref($inboxes) && $inboxes eq 'watchspam') {
                return _remove_spam($self, $path);
        }
+
+       my $warn_cb = $SIG{__WARN__} || sub { print STDERR @_ };
+       local $SIG{__WARN__} = sub {
+               $warn_cb->("path: $path\n");
+               $warn_cb->(@_);
+       };
        foreach my $ibx (@$inboxes) {
                my $mime = _path_to_mime($path) or next;
                my $im = _importer_for($self, $ibx);
index dd8446dcd1f77e0f9a2d167ab0c057c917a443bb..34ffc125ed49a8dfcc975550b4cc4acc299d3ed4 100644 (file)
@@ -15,9 +15,21 @@ if ('close-on-exec for epoll and kqueue') {
 
        PublicInbox::DS->SetLoopTimeout(0);
        PublicInbox::DS->SetPostLoopCallback(sub { 0 });
+
+       # make sure execve closes if we're using fork()
+       my ($r, $w);
+       pipe($r, $w) or die "pipe: $!";
+
        PublicInbox::DS->AddTimer(0, sub { $pid = spawn([qw(sleep 10)]) });
        PublicInbox::DS->EventLoop;
        ok($pid, 'subprocess spawned');
+
+       # wait for execve, we need to ensure lsof sees sleep(1)
+       # and not the fork of this process:
+       close $w or die "close: $!";
+       my $l = <$r>;
+       is($l, undef, 'cloexec works and sleep(1) is running');
+
        my @of = grep(/$evfd_re/, `lsof -p $pid 2>/dev/null`);
        my $err = $?;
        SKIP: {
diff --git a/t/httpd-https.t b/t/httpd-https.t
new file mode 100644 (file)
index 0000000..f6b9806
--- /dev/null
@@ -0,0 +1,141 @@
+# Copyright (C) 2019 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+use strict;
+use warnings;
+use Test::More;
+use File::Temp qw(tempdir);
+use Socket qw(SOCK_STREAM IPPROTO_TCP SOL_SOCKET);
+# IO::Poll is part of the standard library, but distros may split them off...
+foreach my $mod (qw(IO::Socket::SSL IO::Poll)) {
+       eval "require $mod";
+       plan skip_all => "$mod missing for $0" if $@;
+}
+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 ./create-certs.perl in certs/";
+}
+use_ok 'PublicInbox::TLS';
+use_ok 'IO::Socket::SSL';
+require './t/common.perl';
+my $psgi = "./t/httpd-corner.psgi";
+my $tmpdir = tempdir('pi-httpd-https-XXXXXX', TMPDIR => 1, CLEANUP => 1);
+my $err = "$tmpdir/stderr.log";
+my $out = "$tmpdir/stdout.log";
+my $httpd = 'blib/script/public-inbox-httpd';
+my %opts = (
+       LocalAddr => '127.0.0.1',
+       ReuseAddr => 1,
+       Proto => 'tcp',
+       Type => SOCK_STREAM,
+       Listen => 1024,
+);
+my $https = IO::Socket::INET->new(%opts);
+my ($pid, $tail_pid);
+END {
+       foreach ($pid, $tail_pid) {
+               kill 'TERM', $_ if defined $_;
+       }
+};
+my $https_addr = $https->sockhost . ':' . $https->sockport;
+my %opt = ( Proto => 'tcp', PeerAddr => $https_addr, Type => SOCK_STREAM );
+
+for my $args (
+       [ "-lhttps://$https_addr/?key=$key,cert=$cert" ],
+) {
+       for ($out, $err) {
+               open my $fh, '>', $_ or die "truncate: $!";
+       }
+       if (my $tail_cmd = $ENV{TAIL}) { # don't assume GNU tail
+               $tail_pid = fork;
+               if (defined $tail_pid && $tail_pid == 0) {
+                       exec(split(' ', $tail_cmd), $out, $err);
+               }
+       }
+       my $cmd = [ $httpd, '-W0', @$args,
+                       "--stdout=$out", "--stderr=$err", $psgi ];
+       $pid = spawn_listener(undef, $cmd, [ $https ]);
+       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 = IO::Socket::INET->new(%opt, Blocking => 0);
+       $slow = IO::Socket::SSL->start_SSL($slow, SSL_startHandshake => 0, %o);
+       my @poll = (fileno($slow));
+       my $slow_done = $slow->connect_SSL;
+       if ($slow_done) {
+               diag('W: connect_SSL early OK, slow client test invalid');
+               push @poll, PublicInbox::Syscall::EPOLLOUT();
+       } else {
+               push @poll, PublicInbox::TLS::epollbit();
+       }
+
+       # normal HTTPS
+       my $c = IO::Socket::INET->new(%opt);
+       IO::Socket::SSL->start_SSL($c, %o);
+       ok($c->print("GET /empty HTTP/1.1\r\n\r\nHost: example.com\r\n\r\n"),
+               'wrote HTTP request');
+       my $buf = '';
+       sysread($c, $buf, 2007, length($buf)) until $buf =~ /\r\n\r\n/;
+       like($buf, qr!\AHTTP/1\.1 200!, 'read HTTP response');
+
+       # HTTPS with bad hostname
+       $c = IO::Socket::INET->new(%opt);
+       $o{SSL_hostname} = $o{SSL_verifycn_name} = 'server.fail';
+       $c = IO::Socket::SSL->start_SSL($c, %o);
+       is($c, undef, 'HTTPS fails with bad hostname');
+
+       $o{SSL_hostname} = $o{SSL_verifycn_name} = 'server.local';
+       $c = IO::Socket::INET->new(%opt);
+       IO::Socket::SSL->start_SSL($c, %o);
+       ok($c, 'HTTPS 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($slow->print("GET /empty HTTP/1.1\r\n\r\nHost: example.com\r\n\r\n"),
+               'wrote HTTP request from slow');
+       $buf = '';
+       sysread($slow, $buf, 666, length($buf)) until $buf =~ /\r\n\r\n/;
+       like($buf, qr!\AHTTP/1\.1 200!, 'read HTTP response from slow');
+       $slow = undef;
+
+       SKIP: {
+               skip 'TCP_DEFER_ACCEPT is Linux-only', 2 if $^O ne 'linux';
+               my $var = Socket::TCP_DEFER_ACCEPT();
+               defined(my $x = getsockopt($https, IPPROTO_TCP, $var)) or die;
+               ok(unpack('i', $x) > 0, 'TCP_DEFER_ACCEPT set on https');
+       };
+       SKIP: {
+               skip 'SO_ACCEPTFILTER is FreeBSD-only', 2 if $^O ne 'freebsd';
+               if (system('kldstat -m accf_data >/dev/null')) {
+                       skip 'accf_data not loaded? kldload accf_data', 2;
+               }
+               require PublicInbox::Daemon;
+               my $var = PublicInbox::Daemon::SO_ACCEPTFILTER();
+               my $x = getsockopt($https, SOL_SOCKET, $var);
+               like($x, qr/\Adataready\0+\z/, 'got dataready accf for https');
+       };
+
+       $c = undef;
+       kill('TERM', $pid);
+       is($pid, waitpid($pid, 0), 'httpd exited successfully');
+       is($?, 0, 'no error in exited process');
+       $pid = undef;
+       if (defined $tail_pid) {
+               kill 'TERM', $tail_pid;
+               waitpid($tail_pid, 0);
+               $tail_pid = undef;
+       }
+}
+done_testing();
+1;
index 427d370fc4a69a87b7e7f747cbcc5f21bccd1119..e3ecdd4fa0ecd4d83c4122b17ab31ff17a5e0afe 100644 (file)
@@ -11,6 +11,9 @@ foreach my $mod (qw(DBD::SQLite IO::Socket::SSL Net::NNTP IO::Poll)) {
        eval "require $mod";
        plan skip_all => "$mod missing for $0" if $@;
 }
+Net::NNTP->can('starttls') or
+       plan skip_all => 'Net::NNTP does not support TLS';
+
 my $cert = 'certs/server-cert.pem';
 my $key = 'certs/server-key.pem';
 unless (-r $key && -r $cert) {
index 6cba2be435089207d7ad8a33d76b23f11d417461..bf5bb883f728dcb8f500ff67b831962e8b40c174 100644 (file)
--- a/t/nntpd.t
+++ b/t/nntpd.t
@@ -106,8 +106,13 @@ EOF
        is_deeply($list, { $group => [ qw(1 1 n) ] }, 'LIST works');
        is_deeply([$n->group($group)], [ qw(0 1 1), $group ], 'GROUP works');
        is_deeply($n->listgroup($group), [1], 'listgroup OK');
-       ok(!$n->starttls, 'STARTTLS fails when unconfigured');
-       is($n->code, 580, 'got 580 code on server w/o TLS');
+
+       SKIP: {
+               $n->can('starttls') or
+                       skip('Net::NNTP too old to support STARTTLS', 2);
+               ok(!$n->starttls, 'STARTTLS fails when unconfigured');
+               is($n->code, 580, 'got 580 code on server w/o TLS');
+       };
 
        %opts = (
                PeerAddr => $host_port,