]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/NNTP.pm
Merge remote-tracking branch 'origin/danga-bundle'
[public-inbox.git] / lib / PublicInbox / NNTP.pm
index ff6d89587756fa70bd8708f5a1c716a91b6d11f7..5c5df7b0dcbe548c0284504c84ec64f782de8d18 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;
@@ -23,18 +23,16 @@ use constant {
        r224 => '224 Overview information follows (multi-line)',
        r225 => '225 Headers follow (multi-line)',
        r430 => '430 No article with that message-id',
-       long_response_limit => 0xffffffff,
 };
 
 sub now () { clock_gettime(CLOCK_MONOTONIC) };
 
-my @OVERVIEW = qw(Subject From Date Message-ID References);
+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,
                        qw(:bytes :lines Xref To Cc)) . "\r\n";
 
 # disable commands with easy DoS potential:
-# LISTGROUP could get pretty bad, too...
 my %DISABLED; # = map { $_ => 1 } qw(xover list_overview_fmt newnews xhdr);
 
 my $EXPMAP; # fd -> [ idle_time, $self ]
@@ -52,8 +50,16 @@ sub next_tick () {
                # before finishing reading:
                if (my $long_cb = $nntp->{long_res}) {
                        $nntp->write($long_cb);
-               } elsif (&Danga::Socket::POLLIN & $nntp->{event_watch}) {
+               } else {
+                       # pipelined request, we bypassed socket-readiness
+                       # checks to get here:
                        event_read($nntp);
+
+                       # maybe there's more pipelined data, or we'll have
+                       # to register it for socket-readiness notifications
+                       if (!$nntp->{long_res} && !$nntp->{closed}) {
+                               check_read($nntp);
+                       }
                }
        }
 }
@@ -95,7 +101,7 @@ sub new ($$$) {
        my $self = fields::new($class);
        $self->SUPER::new($sock);
        $self->{nntpd} = $nntpd;
-       res($self, '201 server ready - post via email');
+       res($self, '201 ' . $nntpd->{servername} . ' ready - post via email');
        $self->{rbuf} = '';
        $self->watch_read(1);
        update_idle_time($self);
@@ -116,6 +122,7 @@ sub args_ok ($$) {
 sub process_line ($$) {
        my ($self, $l) = @_;
        my ($req, @args) = split(/\s+/, $l);
+       return 1 unless defined($req); # skip blank line
        $req = lc($req);
        $req = eval {
                no strict 'refs';
@@ -225,15 +232,12 @@ sub cmd_listgroup ($;$) {
        }
 
        $self->{ng} or return '412 no newsgroup selected';
-       long_response($self, 0, long_response_limit, sub {
-               my ($i) = @_;
-               my $nr = $self->{ng}->mm->id_batch($$i, sub {
-                       my ($ary) = @_;
-                       more($self, join("\r\n", @$ary));
-               });
-
-               # -1 to adjust for implicit increment in long_response
-               $$i = $nr ? $$i + $nr - 1 : long_response_limit;
+       my $n = 0;
+       long_response($self, sub {
+               my $ary = $self->{ng}->mm->ids_after(\$n);
+               scalar @$ary or return;
+               more($self, join("\r\n", @$ary));
+               1;
        });
 }
 
@@ -332,8 +336,7 @@ sub cmd_newnews ($$$$;$$) {
        return '.' unless @srch;
 
        my $prev = 0;
-       long_response($self, 0, long_response_limit, sub {
-               my ($i) = @_;
+       long_response($self, sub {
                my $srch = $srch[0];
                my $msgs = $srch->query_ts($ts, $prev);
                if (scalar @$msgs) {
@@ -345,8 +348,9 @@ sub cmd_newnews ($$$$;$$) {
                        shift @srch;
                        if (@srch) { # continue onto next newsgroup
                                $prev = 0;
+                               return 1;
                        } else { # break out of the long response.
-                               $$i = long_response_limit;
+                               return;
                        }
                }
        });
@@ -411,12 +415,30 @@ sub header_append ($$$) {
        $hdr->header_set($k, @v, $v);
 }
 
-sub set_nntp_headers {
-       my ($hdr, $ng, $n, $mid) = @_;
+sub xref ($$$$) {
+       my ($self, $ng, $n, $mid) = @_;
+       my $ret = $self->{nntpd}->{servername} . " $ng->{newsgroup}:$n";
+
+       # num_for is pretty cheap and sometimes we'll lookup the existence
+       # of an article without getting even the OVER info.  In other words,
+       # I'm not sure if its worth optimizing by scanning To:/Cc: and
+       # PublicInbox::ExtMsg on the PSGI end is just as expensive
+       foreach my $other (@{$self->{nntpd}->{grouplist}}) {
+               next if $ng eq $other;
+               my $num = eval { $other->mm->num_for($mid) } or next;
+               $ret .= " $other->{newsgroup}:$num";
+       }
+       $ret;
+}
+
+sub set_nntp_headers ($$$$$) {
+       my ($self, $hdr, $ng, $n, $mid) = @_;
 
        # clobber some
-       $hdr->header_set('Newsgroups', $ng->{newsgroup});
-       $hdr->header_set('Xref', xref($ng, $n));
+       my $xref = xref($self, $ng, $n, $mid);
+       $hdr->header_set('Xref', $xref);
+       $xref =~ s/:\d+//g;
+       $hdr->header_set('Newsgroups', (split(/ /, $xref, 2))[1]);
        header_append($hdr, 'List-Post', "<mailto:$ng->{-primary_address}>");
        if (my $url = $ng->base_url) {
                $mid = mid_escape($mid);
@@ -465,7 +487,7 @@ found:
        my $msg = $ng->msg_by_smsg($smsg) or return $err;
        my $s = Email::Simple->new($msg);
        if ($set_headers) {
-               set_nntp_headers($s->header_obj, $ng, $n, $mid);
+               set_nntp_headers($self, $s->header_obj, $ng, $n, $mid);
 
                # must be last
                $s->body_set('') if ($set_headers == 2);
@@ -492,6 +514,7 @@ sub set_art {
 sub _header ($) {
        my $hdr = $_[0]->header_obj->as_string;
        utf8::encode($hdr);
+       $hdr =~ s/(?<!\r)\n/\r\n/sg;
        $hdr
 }
 
@@ -568,8 +591,8 @@ sub get_range ($$) {
        [ $beg, $end ];
 }
 
-sub long_response ($$$$) {
-       my ($self, $beg, $end, $cb) = @_;
+sub long_response ($$) {
+       my ($self, $cb) = @_;
        die "BUG: nested long response" if $self->{long_res};
 
        my $fd = $self->{fd};
@@ -580,32 +603,23 @@ sub long_response ($$$$) {
        $self->watch_read(0);
        my $t0 = now();
        $self->{long_res} = sub {
-               # limit our own running time for fairness with other
-               # clients and to avoid buffering too much:
-               my $lim = $end == long_response_limit ? 1 : 100;
-
-               my $err;
-               do {
-                       eval { $cb->(\$beg) };
-               } until (($err = $@) || $self->{closed} ||
-                        ++$beg > $end || !--$lim || $self->{write_buf_size});
-
-               if ($err || $self->{closed}) {
+               my $more = eval { $cb->() };
+               if ($@ || $self->{closed}) {
                        $self->{long_res} = undef;
 
-                       if ($err) {
+                       if ($@) {
                                err($self,
                                    "%s during long response[$fd] - %0.6f",
-                                   $err, now() - $t0);
+                                   $@, now() - $t0);
                        }
                        if ($self->{closed}) {
                                out($self, " deferred[$fd] aborted - %0.6f",
                                           now() - $t0);
                        } else {
                                update_idle_time($self);
-                               $self->watch_read(1);
+                               check_read($self);
                        }
-               } elsif (!$lim || $self->{write_buf_size}) {
+               } elsif ($more) { # $self->{write_buf_size}:
                        # no recursion, schedule another call ASAP
                        # but only after all pending writes are done
                        update_idle_time($self);
@@ -614,7 +628,7 @@ sub long_response ($$$$) {
                        $nextt ||= PublicInbox::EvCleanup::asap(*next_tick);
                } else { # all done!
                        $self->{long_res} = undef;
-                       $self->watch_read(1);
+                       check_read($self);
                        res($self, '.');
                        out($self, " deferred[$fd] done - %0.6f", now() - $t0);
                }
@@ -637,19 +651,17 @@ sub hdr_message_id ($$$) { # optimize XHDR Message-ID [range] for slrnpull.
                my $mm = $self->{ng}->mm;
                my ($beg, $end) = @$r;
                more($self, $xhdr ? r221 : r225);
-               long_response($self, $beg, $end, sub {
-                       my ($i) = @_;
-                       my $mid = $mm->mid_for($$i);
-                       more($self, "$$i <$mid>") if defined $mid;
+               long_response($self, sub {
+                       my $r = $mm->msg_range(\$beg, $end);
+                       @$r or return;
+                       more($self, join("\r\n", map {
+                               "$_->[0] <$_->[1]>"
+                       } @$r));
+                       1;
                });
        }
 }
 
-sub xref ($$) {
-       my ($ng, $n) = @_;
-       "$ng->{domain} $ng->{newsgroup}:$n"
-}
-
 sub mid_lookup ($$) {
        my ($self, $mid) = @_;
        my $self_ng = $self->{ng};
@@ -669,9 +681,11 @@ sub hdr_xref ($$$) { # optimize XHDR Xref [range] for rtin
        my ($self, $xhdr, $range) = @_;
 
        if (defined $range && $range =~ /\A<(.+)>\z/) { # Message-ID
-               my ($ng, $n) = mid_lookup($self, $1);
+               my $mid = $1;
+               my ($ng, $n) = mid_lookup($self, $mid);
                return r430 unless $n;
-               hdr_mid_response($self, $xhdr, $ng, $n, $range, xref($ng, $n));
+               hdr_mid_response($self, $xhdr, $ng, $n, $range,
+                               xref($self, $ng, $n, $mid));
        } else { # numeric range
                $range = $self->{article} unless defined $range;
                my $r = get_range($self, $range);
@@ -680,10 +694,14 @@ sub hdr_xref ($$$) { # optimize XHDR Xref [range] for rtin
                my $mm = $ng->mm;
                my ($beg, $end) = @$r;
                more($self, $xhdr ? r221 : r225);
-               long_response($self, $beg, $end, sub {
-                       my ($i) = @_;
-                       my $mid = $mm->mid_for($$i);
-                       more($self, "$$i ".xref($ng, $$i)) if defined $mid;
+               long_response($self, sub {
+                       my $r = $mm->msg_range(\$beg, $end);
+                       @$r or return;
+                       more($self, join("\r\n", map {
+                               my $num = $_->[0];
+                               "$num ".xref($self, $ng, $num, $_->[1]);
+                       } @$r));
+                       1;
                });
        }
 }
@@ -711,14 +729,12 @@ sub hdr_searchmsg ($$$$) {
                my ($beg, $end) = @$r;
                more($self, $xhdr ? r221 : r225);
                my $cur = $beg;
-               long_response($self, 0, long_response_limit, sub {
-                       my ($i) = @_;
+               long_response($self, sub {
                        my $msgs = $srch->query_xover($cur, $end);
-                       my $nr = scalar @$msgs or
-                                       return ($$i = long_response_limit);
+                       my $nr = scalar @$msgs or return;
                        my $tmp = '';
                        foreach my $s (@$msgs) {
-                               $tmp .= $s->num . ' ' . $s->$field . "\r\n";
+                               $tmp .= $s->{num} . ' ' . $s->$field . "\r\n";
                        }
                        utf8::encode($tmp);
                        do_more($self, $tmp);
@@ -796,17 +812,16 @@ sub cmd_xrover ($;$) {
        my $mm = $ng->mm;
        my $srch = $ng->search;
        more($self, '224 Overview information follows');
-       long_response($self, $beg, $end, sub {
-               my ($i) = @_;
-               my $num = $$i;
-               my $h = search_header_for($srch, $num, 'references');
-               defined $h or return;
-               more($self, "$num $h");
+
+       long_response($self, sub {
+               my $h = search_header_for($srch, $beg, 'references');
+               more($self, "$beg $h") if defined($h);
+               $beg++ < $end;
        });
 }
 
-sub over_line ($$) {
-       my ($num, $smsg) = @_;
+sub over_line ($$$$) {
+       my ($self, $ng, $num, $smsg) = @_;
        # n.b. field access and procedural calls can be
        # 10%-15% faster than OO method calls:
        my $s = join("\t", $num,
@@ -816,7 +831,8 @@ sub over_line ($$) {
                "<$smsg->{mid}>",
                $smsg->{references},
                $smsg->{bytes},
-               $smsg->{lines});
+               $smsg->{lines},
+               "Xref: " . xref($self, $ng, $num, $smsg->{mid}));
        utf8::encode($s);
        $s
 }
@@ -832,7 +848,7 @@ sub cmd_over ($;$) {
                # Only set article number column if it's the current group
                my $self_ng = $self->{ng};
                $n = 0 if (!$self_ng || $self_ng ne $ng);
-               more($self, over_line($n, $smsg));
+               more($self, over_line($self, $ng, $n, $smsg));
                '.';
        } else {
                cmd_xover($self, $range);
@@ -848,17 +864,15 @@ sub cmd_xover ($;$) {
        more($self, "224 Overview information follows for $beg to $end");
        my $srch = $self->{ng}->search;
        my $cur = $beg;
-       long_response($self, 0, long_response_limit, sub {
-               my ($i) = @_;
+       long_response($self, sub {
                my $msgs = $srch->query_xover($cur, $end);
-               my $nr = scalar @$msgs or return ($$i = long_response_limit);
+               my $nr = scalar @$msgs or return;
 
                # OVERVIEW.FMT
                more($self, join("\r\n", map {
-                       over_line($_->{num}, $_);
+                       over_line($self, $self->{ng}, $_->{num}, $_);
                        } @$msgs));
                $cur = $msgs->[-1]->{num} + 1;
-               1;
        });
 }
 
@@ -888,7 +902,7 @@ sub more ($$) {
 sub do_write ($$) {
        my ($self, $data) = @_;
        my $done = $self->write($data);
-       return if $self->{closed};
+       return if $self->{closed};
 
        # Do not watch for readability if we have data in the queue,
        # instead re-enable watching for readability when we can
@@ -922,7 +936,7 @@ sub do_more ($$) {
        do_write($self, $data);
 }
 
-# callbacks for Danga::Socket
+# callbacks for PublicInbox::DS
 
 sub event_hup { $_[0]->close }
 sub event_err { $_[0]->close }
@@ -939,11 +953,13 @@ sub event_write {
 sub event_read {
        my ($self) = @_;
        use constant LINE_MAX => 512; # RFC 977 section 2.3
-       my $r = 1;
 
-       my $buf = $self->read(LINE_MAX) or return $self->close;
-       $self->{rbuf} .= $$buf;
-       while ($r > 0 && $self->{rbuf} =~ s/\A\s*([^\r\n]+)\r?\n//) {
+       if (index($self->{rbuf}, "\n") < 0) {
+               my $buf = $self->read(LINE_MAX) or return $self->close;
+               $self->{rbuf} .= $$buf;
+       }
+       my $r = 1;
+       while ($r > 0 && $self->{rbuf} =~ s/\A\s*([^\r\n]*)\r?\n//) {
                my $line = $1;
                return $self->close if $line =~ /[[:cntrl:]]/s;
                my $t0 = now();
@@ -960,10 +976,9 @@ sub event_read {
        update_idle_time($self);
 }
 
-sub watch_read {
-       my ($self, $bool) = @_;
-       my $rv = $self->SUPER::watch_read($bool);
-       if ($bool && $self->{rbuf} ne '') {
+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
@@ -971,8 +986,11 @@ sub watch_read {
                # 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_read(1); # PublicInbox::DS::watch_read
        }
-       $rv;
 }
 
 sub not_idle_long ($$) {