]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/NNTP.pm
nntp: NNTPS and NNTP+STARTTLS working
[public-inbox.git] / lib / PublicInbox / NNTP.pm
index 6a582ea41397412253afb5503f79a23610c78c16..659e44d5501194499eac24634c81243282bd4441 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2015-2018 all contributors <meta@public-inbox.org>
+# Copyright (C) 2015-2019 all contributors <meta@public-inbox.org>
 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
 #
 # Each instance of this represents a NNTP client socket
@@ -24,7 +24,7 @@ use constant {
        r225 => '225 Headers follow (multi-line)',
        r430 => '430 No article with that message-id',
 };
-use PublicInbox::Syscall qw(EPOLLIN EPOLLONESHOT);
+use PublicInbox::Syscall qw(EPOLLOUT EPOLLONESHOT);
 use Errno qw(EAGAIN);
 
 my @OVERVIEW = qw(Subject From Date Message-ID References Xref);
@@ -58,6 +58,11 @@ sub next_tick () {
        }
 }
 
+sub requeue ($) {
+       push @$nextq, $_[0];
+       $nextt ||= PublicInbox::EvCleanup::asap(*next_tick);
+}
+
 sub update_idle_time ($) {
        my ($self) = @_;
        my $sock = $self->{sock} or return;
@@ -93,9 +98,19 @@ sub expire_old () {
 sub new ($$$) {
        my ($class, $sock, $nntpd) = @_;
        my $self = fields::new($class);
-       $self->SUPER::new($sock, EPOLLIN | EPOLLONESHOT);
+       my $ev = EPOLLOUT | EPOLLONESHOT;
+       my $wbuf = [];
+       if (ref($sock) eq 'IO::Socket::SSL' && !$sock->accept_SSL) {
+               $ev = PublicInbox::TLS::epollbit() or return $sock->close;
+               $ev |= EPOLLONESHOT;
+               $wbuf->[0] = \&PublicInbox::DS::accept_tls_step;
+       }
+       $self->SUPER::new($sock, $ev);
        $self->{nntpd} = $nntpd;
-       res($self, '201 ' . $nntpd->{servername} . ' ready - post via email');
+       my $greet = "201 $nntpd->{servername} ready - post via email\r\n";
+       open my $fh, '<:scalar',  \$greet or die "open :scalar: $!";
+       push @$wbuf, $fh;
+       $self->{wbuf} = $wbuf;
        $self->{rbuf} = '';
        update_idle_time($self);
        $expt ||= PublicInbox::EvCleanup::later(*expire_old);
@@ -243,7 +258,7 @@ sub parse_time ($$;$) {
        }
        my @now = $gmt ? gmtime : localtime;
        my ($YYYY, $MM, $DD);
-       if (length($date) == 8) { # RFC 3977 allows YYYYMMDD
+       if (bytes::length($date) == 8) { # RFC 3977 allows YYYYMMDD
                ($YYYY, $MM, $DD) = unpack('A4A2A2', $date);
        } else { # legacy clients send YYMMDD
                ($YYYY, $MM, $DD) = unpack('A2A2A2', $date);
@@ -633,7 +648,7 @@ sub long_response ($$) {
                        }
                        if ($self->{sock}) {
                                update_idle_time($self);
-                               check_read($self);
+                               requeue($self);
                        } else {
                                out($self, " deferred[$fd] aborted - %0.6f",
                                           now() - $t0);
@@ -642,14 +657,12 @@ sub long_response ($$) {
                        # no recursion, schedule another call ASAP
                        # but only after all pending writes are done
                        update_idle_time($self);
-
-                       push @$nextq, $self;
-                       $nextt ||= PublicInbox::EvCleanup::asap(*next_tick);
+                       requeue($self);
                } else { # all done!
                        delete $self->{long_res};
-                       check_read($self);
                        res($self, '.');
                        out($self, " deferred[$fd] done - %0.6f", now() - $t0);
+                       requeue($self);
                }
        };
        $self->{long_res}->(); # kick off!
@@ -895,6 +908,19 @@ sub cmd_xover ($;$) {
        });
 }
 
+sub cmd_starttls ($) {
+       my ($self) = @_;
+       my $sock = $self->{sock} or return;
+       # RFC 4642 2.2.1
+       (ref($sock) eq 'IO::Socket::SSL') and return '502 Command unavailable';
+       my $opt = $self->{nntpd}->{accept_tls} or
+               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);
+       undef;
+}
+
 sub cmd_xpath ($$) {
        my ($self, $mid) = @_;
        return r501 unless $mid =~ /\A<(.+)>\z/;
@@ -930,6 +956,7 @@ sub out ($$;@) {
        printf { $self->{nntpd}->{out} } $fmt."\n", @args;
 }
 
+# callback used by PublicInbox::DS for any (e)poll (in/out/hup/err)
 sub event_step {
        my ($self) = @_;
 
@@ -941,17 +968,12 @@ sub event_step {
 
        use constant LINE_MAX => 512; # RFC 977 section 2.3
        my $rbuf = \($self->{rbuf});
-       my $r;
+       my $r = 1;
 
        if (index($$rbuf, "\n") < 0) {
-               my $off = length($$rbuf);
-               $r = sysread($self->{sock}, $$rbuf, LINE_MAX, $off);
-               unless (defined $r) {
-                       return $! == EAGAIN ? $self->watch_in1 : $self->close;
-               }
-               return $self->close if $r == 0;
+               my $off = bytes::length($$rbuf);
+               $r = $self->do_read($rbuf, LINE_MAX, $off) or return;
        }
-       $r = 1;
        while ($r > 0 && $$rbuf =~ s/\A[ \t\r\n]*([^\r\n]*)\r?\n//) {
                my $line = $1;
                return $self->close if $line =~ /[[:cntrl:]]/s;
@@ -964,30 +986,13 @@ sub event_step {
        }
 
        return $self->close if $r < 0;
-       my $len = length($$rbuf);
+       my $len = bytes::length($$rbuf);
        return $self->close if ($len >= LINE_MAX);
        update_idle_time($self);
 
        # maybe there's more pipelined data, or we'll have
        # to register it for socket-readiness notifications
-       check_read($self) unless ($self->{long_res} || $self->{wbuf});
-}
-
-sub check_read {
-       my ($self) = @_;
-       if (index($self->{rbuf}, "\n") >= 0) {
-               # Force another read if there is a pipelined request.
-               # We don't know if the socket has anything for us to read,
-               # and we must double-check again by the time the timer fires
-               # in case we really did dispatch a read event and started
-               # another long response.
-               push @$nextq, $self;
-               $nextt ||= PublicInbox::EvCleanup::asap(*next_tick);
-       } else {
-               # no pipelined requests available, let the kernel know
-               # to wake us up if there's more
-               $self->watch_in1; # PublicInbox::DS::watch_in1
-       }
+       requeue($self) unless ($self->{long_res} || $self->{wbuf});
 }
 
 sub not_idle_long ($$) {