]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/NNTP.pm
ds: set event flags directly at initialization
[public-inbox.git] / lib / PublicInbox / NNTP.pm
index 13591e5d983c06af9606a1107e151aa201755ad6..eb1679a76b32072a740f4959829cb0b989dcce01 100644 (file)
@@ -5,7 +5,7 @@
 package PublicInbox::NNTP;
 use strict;
 use warnings;
-use base qw(Danga::Socket);
+use base qw(PublicInbox::DS);
 use fields qw(nntpd article rbuf ng long_res);
 use PublicInbox::Search;
 use PublicInbox::Msgmap;
@@ -14,7 +14,7 @@ use PublicInbox::Git;
 require PublicInbox::EvCleanup;
 use Email::Simple;
 use POSIX qw(strftime);
-use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
+PublicInbox::DS->import(qw(now msg_more));
 use Digest::SHA qw(sha1_hex);
 use Time::Local qw(timegm timelocal);
 use constant {
@@ -25,8 +25,6 @@ use constant {
        r430 => '430 No article with that message-id',
 };
 
-sub now () { clock_gettime(CLOCK_MONOTONIC) };
-
 my @OVERVIEW = qw(Subject From Date Message-ID References Xref);
 my $OVERVIEW_FMT = join(":\r\n", @OVERVIEW, qw(Bytes Lines)) . ":\r\n";
 my $LIST_HEADERS = join("\r\n", @OVERVIEW,
@@ -53,11 +51,11 @@ sub next_tick () {
                } else {
                        # pipelined request, we bypassed socket-readiness
                        # checks to get here:
-                       event_read($nntp);
+                       event_step($nntp);
 
                        # maybe there's more pipelined data, or we'll have
                        # to register it for socket-readiness notifications
-                       if (!$nntp->{long_res} && !$nntp->{closed}) {
+                       if (!$nntp->{long_res} && $nntp->{sock}) {
                                check_read($nntp);
                        }
                }
@@ -66,8 +64,8 @@ sub next_tick () {
 
 sub update_idle_time ($) {
        my ($self) = @_;
-       my $fd = $self->{fd};
-       defined $fd and $EXPMAP->{$fd} = [ now(), $self ];
+       my $sock = $self->{sock} or return;
+       $EXPMAP->{fileno($sock)} = [ now(), $self ];
 }
 
 sub expire_old () {
@@ -99,11 +97,10 @@ sub expire_old () {
 sub new ($$$) {
        my ($class, $sock, $nntpd) = @_;
        my $self = fields::new($class);
-       $self->SUPER::new($sock);
+       $self->SUPER::new($sock, PublicInbox::DS::EPOLLIN());
        $self->{nntpd} = $nntpd;
        res($self, '201 ' . $nntpd->{servername} . ' ready - post via email');
        $self->{rbuf} = '';
-       $self->watch_read(1);
        update_idle_time($self);
        $expt ||= PublicInbox::EvCleanup::later(*expire_old);
        $self;
@@ -121,8 +118,8 @@ sub args_ok ($$) {
 # returns 1 if we can continue, 0 if not due to buffered writes or disconnect
 sub process_line ($$) {
        my ($self, $l) = @_;
-       my ($req, @args) = split(/\s+/, $l);
-       return unless defined($req);
+       my ($req, @args) = split(/[ \t]/, $l);
+       return 1 unless defined($req); # skip blank line
        $req = lc($req);
        $req = eval {
                no strict 'refs';
@@ -133,7 +130,7 @@ sub process_line ($$) {
 
        my $res = eval { $req->($self, @args) };
        my $err = $@;
-       if ($err && !$self->{closed}) {
+       if ($err && $self->{sock}) {
                local $/ = "\n";
                chomp($l);
                err($self, 'error from: %s (%s)', $l, $err);
@@ -161,12 +158,12 @@ sub cmd_xgtitle ($;$) {
 
 sub list_overview_fmt ($) {
        my ($self) = @_;
-       do_more($self, $OVERVIEW_FMT);
+       msg_more($self, $OVERVIEW_FMT);
 }
 
 sub list_headers ($;$) {
        my ($self) = @_;
-       do_more($self, $LIST_HEADERS);
+       msg_more($self, $LIST_HEADERS);
 }
 
 sub list_active ($;$) {
@@ -326,27 +323,27 @@ sub cmd_newnews ($$$$;$$) {
        my ($keep, $skip) = split('!', $newsgroups, 2);
        ngpat2re($keep);
        ngpat2re($skip);
-       my @srch;
+       my @over;
        foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
                $ng->{newsgroup} =~ $keep or next;
                $ng->{newsgroup} =~ $skip and next;
-               my $srch = $ng->search or next;
-               push @srch, $srch;
+               my $over = $ng->over or next;
+               push @over, $over;
        };
-       return '.' unless @srch;
+       return '.' unless @over;
 
        my $prev = 0;
        long_response($self, sub {
-               my $srch = $srch[0];
-               my $msgs = $srch->query_ts($ts, $prev);
+               my $over = $over[0];
+               my $msgs = $over->query_ts($ts, $prev);
                if (scalar @$msgs) {
                        more($self, '<' .
                                join(">\r\n<", map { $_->mid } @$msgs ).
                                '>');
                        $prev = $msgs->[-1]->{num};
                } else {
-                       shift @srch;
-                       if (@srch) { # continue onto next newsgroup
+                       shift @over;
+                       if (@over) { # continue onto next newsgroup
                                $prev = 0;
                                return 1;
                        } else { # break out of the long response.
@@ -434,10 +431,30 @@ sub xref ($$$$) {
 sub set_nntp_headers ($$$$$) {
        my ($self, $hdr, $ng, $n, $mid) = @_;
 
+       # why? leafnode requires a Path: header for some inexplicable
+       # reason.  We'll fake the shortest one possible.
+       $hdr->header_set('Path', 'y');
+
+       # leafnode (and maybe other NNTP clients) have trouble dealing
+       # with v2 messages which have multiple Message-IDs (either due
+       # to our own content-based dedupe or buggy git-send-email versions).
+       my @mids = $hdr->header('Message-ID');
+       if (scalar(@mids) > 1) {
+               my $mid0 = "<$mid>";
+               $hdr->header_set('Message-ID', $mid0);
+               my @alt = $hdr->header('X-Alt-Message-ID');
+               my %seen = map { $_ => 1 } (@alt, $mid0);
+               foreach my $m (@mids) {
+                       next if $seen{$m}++;
+                       push @alt, $m;
+               }
+               $hdr->header_set('X-Alt-Message-ID', @alt);
+       }
+
        # clobber some
        my $xref = xref($self, $ng, $n, $mid);
        $hdr->header_set('Xref', $xref);
-       $xref =~ s/:\d+//g;
+       $xref =~ s/:[0-9]+//g;
        $hdr->header_set('Newsgroups', (split(/ /, $xref, 2))[1]);
        header_append($hdr, 'List-Post', "<mailto:$ng->{-primary_address}>");
        if (my $url = $ng->base_url) {
@@ -453,7 +470,7 @@ sub art_lookup ($$$) {
        my ($n, $mid);
        my $err;
        if (defined $art) {
-               if ($art =~ /\A\d+\z/o) {
+               if ($art =~ /\A[0-9]+\z/) {
                        $err = '423 no such article number in this group';
                        $n = int($art);
                        goto find_mid;
@@ -483,7 +500,7 @@ find_mid:
                defined $mid or return $err;
        }
 found:
-       my $smsg = $ng->search->{over_ro}->get_art($n) or return $err;
+       my $smsg = $ng->over->get_art($n) or return $err;
        my $msg = $ng->msg_by_smsg($smsg) or return $err;
        my $s = Email::Simple->new($msg);
        if ($set_headers) {
@@ -501,20 +518,27 @@ sub simple_body_write ($$) {
        $s->body_set('');
        $body =~ s/^\./../smg;
        $body =~ s/(?<!\r)\n/\r\n/sg;
-       do_more($self, $body);
-       do_more($self, "\r\n") unless $body =~ /\r\n\z/s;
+       msg_more($self, $body);
+       msg_more($self, "\r\n") unless $body =~ /\r\n\z/s;
        '.'
 }
 
 sub set_art {
        my ($self, $art) = @_;
-       $self->{article} = $art if defined $art && $art =~ /\A\d+\z/;
+       $self->{article} = $art if defined $art && $art =~ /\A[0-9]+\z/;
 }
 
 sub _header ($) {
        my $hdr = $_[0]->header_obj->as_string;
        utf8::encode($hdr);
        $hdr =~ s/(?<!\r)\n/\r\n/sg;
+
+       # for leafnode compatibility, we need to ensure Message-ID headers
+       # are only a single line.  We can't subclass Email::Simple::Header
+       # and override _default_fold_at in here, either; since that won't
+       # affect messages already in the archive.
+       $hdr =~ s/^(Message-ID:)[ \t]*\r\n[ \t]+([^\r]+)\r\n/$1 $2\r\n/igsm;
+
        $hdr
 }
 
@@ -525,8 +549,8 @@ sub cmd_article ($;$) {
        my ($n, $mid, $s) = @$r;
        set_art($self, $art);
        more($self, "220 $n <$mid> article retrieved - head and body follow");
-       do_more($self, _header($s));
-       do_more($self, "\r\n");
+       msg_more($self, _header($s));
+       msg_more($self, "\r\n");
        simple_body_write($self, $s);
 }
 
@@ -537,7 +561,7 @@ sub cmd_head ($;$) {
        my ($n, $mid, $s) = @$r;
        set_art($self, $art);
        more($self, "221 $n <$mid> article retrieved - head follows");
-       do_more($self, _header($s));
+       msg_more($self, _header($s));
        '.'
 }
 
@@ -576,11 +600,11 @@ sub get_range ($$) {
        defined $range or return '420 No article(s) selected';
        my ($beg, $end);
        my ($min, $max) = $ng->mm->minmax;
-       if ($range =~ /\A(\d+)\z/) {
+       if ($range =~ /\A([0-9]+)\z/) {
                $beg = $end = $1;
-       } elsif ($range =~ /\A(\d+)-\z/) {
+       } elsif ($range =~ /\A([0-9]+)-\z/) {
                ($beg, $end) = ($1, $max);
-       } elsif ($range =~ /\A(\d+)-(\d+)\z/) {
+       } elsif ($range =~ /\A([0-9]+)-([0-9]+)\z/) {
                ($beg, $end) = ($1, $2);
        } else {
                return r501;
@@ -595,7 +619,7 @@ sub long_response ($$) {
        my ($self, $cb) = @_;
        die "BUG: nested long response" if $self->{long_res};
 
-       my $fd = $self->{fd};
+       my $fd = fileno($self->{sock});
        defined $fd or return;
        # make sure we disable reading during a long response,
        # clients should not be sending us stuff and making us do more
@@ -604,7 +628,7 @@ sub long_response ($$) {
        my $t0 = now();
        $self->{long_res} = sub {
                my $more = eval { $cb->() };
-               if ($@ || $self->{closed}) {
+               if ($@ || !$self->{sock}) {
                        $self->{long_res} = undef;
 
                        if ($@) {
@@ -612,14 +636,14 @@ sub long_response ($$) {
                                    "%s during long response[$fd] - %0.6f",
                                    $@, now() - $t0);
                        }
-                       if ($self->{closed}) {
-                               out($self, " deferred[$fd] aborted - %0.6f",
-                                          now() - $t0);
-                       } else {
+                       if ($self->{sock}) {
                                update_idle_time($self);
                                check_read($self);
+                       } else {
+                               out($self, " deferred[$fd] aborted - %0.6f",
+                                          now() - $t0);
                        }
-               } elsif ($more) { # $self->{write_buf_size}:
+               } elsif ($more) { # $self->{wbuf}:
                        # no recursion, schedule another call ASAP
                        # but only after all pending writes are done
                        update_idle_time($self);
@@ -706,9 +730,9 @@ sub hdr_xref ($$$) { # optimize XHDR Xref [range] for rtin
        }
 }
 
-sub search_header_for {
-       my ($srch, $num, $field) = @_;
-       my $smsg = $srch->{over_ro}->get_art($num) or return;
+sub over_header_for {
+       my ($over, $num, $field) = @_;
+       my $smsg = $over->get_art($num) or return;
        return PublicInbox::SearchMsg::date($smsg) if $field eq 'date';
        $smsg->{$field};
 }
@@ -718,11 +742,11 @@ sub hdr_searchmsg ($$$$) {
        if (defined $range && $range =~ /\A<(.+)>\z/) { # Message-ID
                my ($ng, $n) = mid_lookup($self, $1);
                return r430 unless defined $n;
-               my $v = search_header_for($ng->search, $n, $field);
+               my $v = over_header_for($ng->over, $n, $field);
                hdr_mid_response($self, $xhdr, $ng, $n, $range, $v);
        } else { # numeric range
                $range = $self->{article} unless defined $range;
-               my $srch = $self->{ng}->search;
+               my $over = $self->{ng}->over;
                my $mm = $self->{ng}->mm;
                my $r = get_range($self, $range);
                return $r unless ref $r;
@@ -730,14 +754,14 @@ sub hdr_searchmsg ($$$$) {
                more($self, $xhdr ? r221 : r225);
                my $cur = $beg;
                long_response($self, sub {
-                       my $msgs = $srch->query_xover($cur, $end);
+                       my $msgs = $over->query_xover($cur, $end);
                        my $nr = scalar @$msgs or return;
                        my $tmp = '';
                        foreach my $s (@$msgs) {
                                $tmp .= $s->{num} . ' ' . $s->$field . "\r\n";
                        }
                        utf8::encode($tmp);
-                       do_more($self, $tmp);
+                       msg_more($self, $tmp);
                        $cur = $msgs->[-1]->{num} + 1;
                });
        }
@@ -810,11 +834,11 @@ sub cmd_xrover ($;$) {
        return $r unless ref $r;
        my ($beg, $end) = @$r;
        my $mm = $ng->mm;
-       my $srch = $ng->search;
+       my $over = $ng->over;
        more($self, '224 Overview information follows');
 
        long_response($self, sub {
-               my $h = search_header_for($srch, $beg, 'references');
+               my $h = over_header_for($over, $beg, 'references');
                more($self, "$beg $h") if defined($h);
                $beg++ < $end;
        });
@@ -842,7 +866,7 @@ sub cmd_over ($;$) {
        if ($range && $range =~ /\A<(.+)>\z/) {
                my ($ng, $n) = mid_lookup($self, $1);
                defined $n or return r430;
-               my $smsg = $ng->search->{over_ro}->get_art($n) or return r430;
+               my $smsg = $ng->over->get_art($n) or return r430;
                more($self, '224 Overview information follows (multi-line)');
 
                # Only set article number column if it's the current group
@@ -862,10 +886,10 @@ sub cmd_xover ($;$) {
        return $r unless ref $r;
        my ($beg, $end) = @$r;
        more($self, "224 Overview information follows for $beg to $end");
-       my $srch = $self->{ng}->search;
+       my $over = $self->{ng}->over;
        my $cur = $beg;
        long_response($self, sub {
-               my $msgs = $srch->query_xover($cur, $end);
+               my $msgs = $over->query_xover($cur, $end);
                my $nr = scalar @$msgs or return;
 
                # OVERVIEW.FMT
@@ -889,20 +913,14 @@ sub cmd_xpath ($$) {
        '223 '.join(' ', @paths);
 }
 
-sub res ($$) {
-       my ($self, $line) = @_;
-       do_write($self, $line . "\r\n");
-}
+sub res ($$) { do_write($_[0], $_[1] . "\r\n") }
 
-sub more ($$) {
-       my ($self, $line) = @_;
-       do_more($self, $line . "\r\n");
-}
+sub more ($$) { msg_more($_[0], $_[1] . "\r\n") }
 
 sub do_write ($$) {
-       my ($self, $data) = @_;
-       my $done = $self->write($data);
-       return 0 if $self->{closed};
+       my $self = $_[0];
+       my $done = $self->write(\($_[1]));
+       return 0 unless $self->{sock};
 
        # Do not watch for readability if we have data in the queue,
        # instead re-enable watching for readability when we can
@@ -921,49 +939,35 @@ sub out ($$;@) {
        printf { $self->{nntpd}->{out} } $fmt."\n", @args;
 }
 
-use constant MSG_MORE => ($^O eq 'linux') ? 0x8000 : 0;
-
-sub do_more ($$) {
-       my ($self, $data) = @_;
-       if (MSG_MORE && !$self->{write_buf_size}) {
-               my $n = send($self->{sock}, $data, MSG_MORE);
-               if (defined $n) {
-                       my $dlen = length($data);
-                       return 1 if $n == $dlen; # all done!
-                       $data = substr($data, $n, $dlen - $n);
-               }
-       }
-       do_write($self, $data);
-}
-
-# callbacks for Danga::Socket
+sub event_step {
+       my ($self) = @_;
 
-sub event_hup { $_[0]->close }
-sub event_err { $_[0]->close }
+       return unless $self->flush_write && $self->{sock};
+       return if $self->{long_res};
 
-sub event_write {
-       my ($self) = @_;
        update_idle_time($self);
-       # only continue watching for readability when we are done writing:
-       if ($self->write(undef) == 1 && !$self->{long_res}) {
-               $self->watch_read(1);
-       }
-}
+       # only read more requests if we've drained the write buffer,
+       # otherwise we can be buffering infinitely w/o backpressure
 
-sub event_read {
-       my ($self) = @_;
        use constant LINE_MAX => 512; # RFC 977 section 2.3
-
-       if (index($self->{rbuf}, "\n") < 0) {
-               my $buf = $self->read(LINE_MAX) or return $self->close;
-               $self->{rbuf} .= $$buf;
+       my $rbuf = \($self->{rbuf});
+       my $r;
+
+       if (index($$rbuf, "\n") < 0) {
+               my $off = length($$rbuf);
+               $r = sysread($self->{sock}, $$rbuf, LINE_MAX, $off);
+               unless (defined $r) {
+                       return if $!{EAGAIN};
+                       return $self->close;
+               }
+               return $self->close if $r == 0;
        }
-       my $r = 1;
-       while ($r > 0 && $self->{rbuf} =~ s/\A\s*([^\r\n]*)\r?\n//) {
+       $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;
                my $t0 = now();
-               my $fd = $self->{fd};
+               my $fd = fileno($self->{sock});
                $r = eval { process_line($self, $line) };
                my $d = $self->{long_res} ?
                        " deferred[$fd]" : '';
@@ -971,7 +975,7 @@ sub event_read {
        }
 
        return $self->close if $r < 0;
-       my $len = length($self->{rbuf});
+       my $len = length($$rbuf);
        return $self->close if ($len >= LINE_MAX);
        update_idle_time($self);
 }
@@ -989,14 +993,14 @@ sub check_read {
        } else {
                # no pipelined requests available, let the kernel know
                # to wake us up if there's more
-               $self->watch_read(1); # Danga::Socket::watch_read
+               $self->watch_read(1); # PublicInbox::DS::watch_read
        }
 }
 
 sub not_idle_long ($$) {
        my ($self, $now) = @_;
-       defined(my $fd = $self->{fd}) or return;
-       my $ary = $EXPMAP->{$fd} or return;
+       my $sock = $self->{sock} or return;
+       my $ary = $EXPMAP->{fileno($sock)} or return;
        my $exp_at = $ary->[0] + $EXPTIME;
        $exp_at > $now;
 }
@@ -1004,8 +1008,8 @@ sub not_idle_long ($$) {
 # for graceful shutdown in PublicInbox::Daemon:
 sub busy {
        my ($self, $now) = @_;
-       ($self->{rbuf} ne '' || $self->{long_res} || $self->{write_buf_size} ||
-        not_idle_long($self, $now));
+       ($self->{rbuf} ne '' || $self->{long_res} ||
+               $self->{wbuf} || not_idle_long($self, $now));
 }
 
 1;