X-Git-Url: http://www.git.stargrave.org/?p=public-inbox.git;a=blobdiff_plain;f=lib%2FPublicInbox%2FNNTP.pm;h=8ad7adc1fd0a3ca49a5635dc19c8b4c7c95b6c01;hp=aea04c05464b28b30d5e8d7a713e5da0446c9282;hb=23af251dd607c4e75ab1e68063f2c885c48cc035;hpb=5c8909925072804901e9c3b45bbf25446d379e7b diff --git a/lib/PublicInbox/NNTP.pm b/lib/PublicInbox/NNTP.pm index aea04c05..8ad7adc1 100644 --- a/lib/PublicInbox/NNTP.pm +++ b/lib/PublicInbox/NNTP.pm @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2021 all contributors +# Copyright (C) all contributors # License: AGPL-3.0+ # # Each instance of this represents a NNTP client socket @@ -9,6 +9,7 @@ # long_cb: long_response private data package PublicInbox::NNTP; use strict; +use v5.10.1; use parent qw(PublicInbox::DS); use PublicInbox::MID qw(mid_escape $MID_EXTRACT); use PublicInbox::Eml; @@ -21,21 +22,19 @@ use PublicInbox::Address; use constant { LINE_MAX => 512, # RFC 977 section 2.3 - r501 => '501 command syntax error', - r502 => '502 Command unavailable', - r221 => '221 Header follows', - r224 => '224 Overview information follows (multi-line)', - r225 => '225 Headers follow (multi-line)', - r430 => '430 No article with that message-id', + r501 => "501 command syntax error\r\n", + r502 => "502 Command unavailable\r\n", + r221 => "221 Header follows\r\n", + r225 => "225 Headers follow (multi-line)\r\n", + r430 => "430 No article with that message-id\r\n", }; -use PublicInbox::Syscall qw(EPOLLIN EPOLLONESHOT); use Errno qw(EAGAIN); my $ONE_MSGID = qr/\A$MID_EXTRACT\z/; my @OVERVIEW = qw(Subject From Date Message-ID References); my $OVERVIEW_FMT = join(":\r\n", @OVERVIEW, qw(Bytes Lines), '') . - "Xref:full\r\n."; + "Xref:full\r\n.\r\n"; my $LIST_HEADERS = join("\r\n", @OVERVIEW, - qw(:bytes :lines Xref To Cc)) . "\r\n."; + qw(:bytes :lines Xref To Cc)) . "\r\n.\r\n"; my $CAPABILITIES = <<""; 101 Capability list:\r VERSION 2\r @@ -46,32 +45,17 @@ HDR\r OVER\r COMPRESS DEFLATE\r -sub greet ($) { $_[0]->write($_[0]->{nntpd}->{greet}) }; - -sub new ($$$) { - my ($class, $sock, $nntpd) = @_; - my $self = bless { nntpd => $nntpd }, $class; - my $ev = EPOLLIN; - my $wbuf; - if ($sock->can('accept_SSL') && !$sock->accept_SSL) { - return CORE::close($sock) if $! != EAGAIN; - $ev = PublicInbox::TLS::epollbit() or return CORE::close($sock); - $wbuf = [ \&PublicInbox::DS::accept_tls_step, \&greet ]; - } - $self->SUPER::new($sock, $ev | EPOLLONESHOT); - if ($wbuf) { - $self->{wbuf} = $wbuf; - } else { - greet($self); - } - $self->update_idle_time; - $self; +sub do_greet ($) { $_[0]->write($_[0]->{nntpd}->{greet}) }; + +sub new { + my ($cls, $sock, $nntpd) = @_; + (bless { nntpd => $nntpd }, $cls)->greet($sock) } sub args_ok ($$) { my ($cb, $argc) = @_; my $tot = prototype $cb; - my ($nreq, undef) = split(';', $tot); + my ($nreq, undef) = split(/;/, $tot); $nreq = ($nreq =~ tr/$//) - 1; $tot = ($tot =~ tr/$//) - 1; ($argc <= $tot && $argc >= $nreq); @@ -82,19 +66,18 @@ sub process_line ($$) { my ($self, $l) = @_; my ($req, @args) = split(/[ \t]+/, $l); return 1 unless defined($req); # skip blank line - $req = $self->can('cmd_'.lc($req)); - return res($self, '500 command not recognized') unless $req; - return res($self, r501) unless args_ok($req, scalar @args); - + $req = $self->can('cmd_'.lc($req)) // + return $self->write(\"500 command not recognized\r\n"); + return $self->write(\r501) unless args_ok($req, scalar @args); my $res = eval { $req->($self, @args) }; my $err = $@; if ($err && $self->{sock}) { local $/ = "\n"; chomp($l); err($self, 'error from: %s (%s)', $l, $err); - $res = '503 program fault - command not performed'; + $res = \"503 program fault - command not performed\r\n"; } - defined($res) ? res($self, $res) : 0; + defined($res) ? $self->write($res) : 0; } # The keyword argument is not used (rfc3977 5.2.2) @@ -105,19 +88,19 @@ sub cmd_capabilities ($;$) { $self->{nntpd}->{accept_tls}) { $res .= "STARTTLS\r\n"; } - $res .= '.'; + $res .= ".\r\n"; } sub cmd_mode ($$) { my ($self, $arg) = @_; - uc($arg) eq 'READER' ? '201 Posting prohibited' : r501; + uc($arg) eq 'READER' ? \"201 Posting prohibited\r\n" : \r501; } -sub cmd_slave ($) { '202 slave status noted' } +sub cmd_slave ($) { \"202 slave status noted\r\n" } sub cmd_xgtitle ($;$) { my ($self, $wildmat) = @_; - more($self, '282 list of groups and descriptions follows'); + $self->msg_more("282 list of groups and descriptions follows\r\n"); list_newsgroups($self, $wildmat); } @@ -125,60 +108,63 @@ sub list_overview_fmt ($) { $OVERVIEW_FMT } sub list_headers ($;$) { $LIST_HEADERS } -sub list_active_i { # "LIST ACTIVE" and also just "LIST" (no args) - my ($self, $groupnames) = @_; - my @window = splice(@$groupnames, 0, 100) or return 0; - my $ibx; +sub names2ibx ($;$) { + my ($self, $names) = @_; my $groups = $self->{nntpd}->{pi_cfg}->{-by_newsgroup}; - for my $ngname (@window) { - $ibx = $groups->{$ngname} and group_line($self, $ibx); + if ($names) { # modify arrayref in-place + $_ = $groups->{$_} for @$names; + $names; # now an arrayref of ibx + } else { + my @ret = map { $groups->{$_} } @{$self->{nntpd}->{groupnames}}; + \@ret; } - scalar(@$groupnames); # continue if there's more +} + +sub list_active_i { # "LIST ACTIVE" and also just "LIST" (no args) + my ($self, $ibxs) = @_; + my @window = splice(@$ibxs, 0, 1000); + $self->msg_more(join('', map { group_line($_) } @window)); + scalar @$ibxs; # continue if there's more } sub list_active ($;$) { # called by cmd_list my ($self, $wildmat) = @_; wildmat2re($wildmat); - long_response($self, \&list_active_i, [ - grep(/$wildmat/, @{$self->{nntpd}->{groupnames}}) ]); + my @names = grep(/$wildmat/, @{$self->{nntpd}->{groupnames}}); + $self->long_response(\&list_active_i, names2ibx($self, \@names)); } sub list_active_times_i { - my ($self, $groupnames) = @_; - my @window = splice(@$groupnames, 0, 100) or return 0; - my $groups = $self->{nntpd}->{pi_cfg}->{-by_newsgroup}; - for my $ngname (@window) { - my $ibx = $groups->{$ngname} or next; - my $c = eval { $ibx->uidvalidity } // time; - more($self, "$ngname $c <$ibx->{-primary_address}>"); - } - scalar(@$groupnames); # continue if there's more + my ($self, $ibxs) = @_; + my @window = splice(@$ibxs, 0, 1000); + $self->msg_more(join('', map { + my $c = eval { $_->uidvalidity } // time; + "$_->{newsgroup} $c <$_->{-primary_address}>\r\n"; + } @window)); + scalar @$ibxs; # continue if there's more } sub list_active_times ($;$) { # called by cmd_list my ($self, $wildmat) = @_; wildmat2re($wildmat); - long_response($self, \&list_active_times_i, [ - grep(/$wildmat/, @{$self->{nntpd}->{groupnames}}) ]); + my @names = grep(/$wildmat/, @{$self->{nntpd}->{groupnames}}); + $self->long_response(\&list_active_times_i, names2ibx($self, \@names)); } sub list_newsgroups_i { - my ($self, $groupnames) = @_; - my @window = splice(@$groupnames, 0, 100) or return 0; - my $groups = $self->{nntpd}->{pi_cfg}->{-by_newsgroup}; - my $ibx; - for my $ngname (@window) { - $ibx = $groups->{$ngname} and - more($self, "$ngname ".$ibx->description); - } - scalar(@$groupnames); # continue if there's more + my ($self, $ibxs) = @_; + my @window = splice(@$ibxs, 0, 1000); + $self->msg_more(join('', map { + "$_->{newsgroup} ".$_->description."\r\n" + } @window)); + scalar @$ibxs; # continue if there's more } sub list_newsgroups ($;$) { # called by cmd_list my ($self, $wildmat) = @_; wildmat2re($wildmat); - long_response($self, \&list_newsgroups_i, [ - grep(/$wildmat/, @{$self->{nntpd}->{groupnames}}) ]); + my @names = grep(/$wildmat/, @{$self->{nntpd}->{groupnames}}); + $self->long_response(\&list_newsgroups_i, names2ibx($self, \@names)); } # LIST SUBSCRIPTIONS, DISTRIB.PATS are not supported @@ -191,12 +177,11 @@ sub cmd_list ($;$$) { $arg = "list_$arg"; $arg = $self->can($arg); return r501 unless $arg && args_ok($arg, scalar @args); - more($self, '215 information follows'); + $self->msg_more("215 information follows\r\n"); $arg->($self, @args); } else { - more($self, '215 list of newsgroups follows'); - long_response($self, \&list_active_i, [ # copy array - @{$self->{nntpd}->{groupnames}} ]); + $self->msg_more("215 list of newsgroups follows\r\n"); + $self->long_response(\&list_active_i, names2ibx($self)); } } @@ -204,15 +189,15 @@ sub listgroup_range_i { my ($self, $beg, $end) = @_; my $r = $self->{ibx}->mm(1)->msg_range($beg, $end, 'num'); scalar(@$r) or return; - $self->msg_more(join('', map { "$_->[0]\r\n" } @$r)); + $self->msg_more(join("\r\n", @$r, '')); 1; } sub listgroup_all_i { my ($self, $num) = @_; - my $ary = $self->{ibx}->mm(1)->ids_after($num); + my $ary = $self->{ibx}->over(1)->ids_after($num); scalar(@$ary) or return; - more($self, join("\r\n", @$ary)); + $self->msg_more(join("\r\n", @$ary, '')); 1; } @@ -220,16 +205,16 @@ sub cmd_listgroup ($;$$) { my ($self, $group, $range) = @_; if (defined $group) { my $res = cmd_group($self, $group); - return $res if ($res !~ /\A211 /); - more($self, $res); + return $res if ref($res); # error if const strref + $self->msg_more($res); } - $self->{ibx} or return '412 no newsgroup selected'; + $self->{ibx} or return \"412 no newsgroup selected\r\n"; if (defined $range) { my $r = get_range($self, $range); return $r unless ref $r; - long_response($self, \&listgroup_range_i, @$r); + $self->long_response(\&listgroup_range_i, @$r); } else { # grab every article number - long_response($self, \&listgroup_all_i, \(my $num = 0)); + $self->long_response(\&listgroup_all_i, \(my $num = 0)); } } @@ -259,23 +244,19 @@ sub parse_time ($$;$) { } } -sub group_line ($$) { - my ($self, $ibx) = @_; +sub group_line ($) { + my ($ibx) = @_; my ($min, $max) = $ibx->mm(1)->minmax; - more($self, "$ibx->{newsgroup} $max $min n"); + "$ibx->{newsgroup} $max $min n\r\n"; } sub newgroups_i { - my ($self, $ts, $i, $groupnames) = @_; - my $end = $$i + 100; - my $groups = $self->{nntpd}->{pi_cfg}->{-by_newsgroup}; - while ($$i < $end) { - my $ngname = $groupnames->[$$i++] // return; - my $ibx = $groups->{$ngname} or next; # expired on reload - next unless (eval { $ibx->uidvalidity } // 0) > $ts; - group_line($self, $ibx); - } - 1; + my ($self, $ts, $ibxs) = @_; + my @window = splice(@$ibxs, 0, 1000); + $self->msg_more(join('', map { group_line($_) } grep { + (eval { $_->uidvalidity } // 0) > $ts + } @window)); + scalar @$ibxs; } sub cmd_newgroups ($$$;$$) { @@ -284,9 +265,8 @@ sub cmd_newgroups ($$$;$$) { return r501 if $@; # TODO dists - more($self, '231 list of new newsgroups follows'); - long_response($self, \&newgroups_i, $ts, \(my $i = 0), - $self->{nntpd}->{groupnames}); + $self->msg_more("231 list of new newsgroups follows\r\n"); + $self->long_response(\&newgroups_i, $ts, names2ibx($self)); } sub wildmat2re (;$) { @@ -321,22 +301,19 @@ sub ngpat2re (;$) { } sub newnews_i { - my ($self, $names, $ts, $prev) = @_; - my $ngname = $names->[0]; - if (my $ibx = $self->{nntpd}->{pi_cfg}->{-by_newsgroup}->{$ngname}) { - if (my $over = $ibx->over) { - my $msgs = $over->query_ts($ts, $$prev); - if (scalar @$msgs) { - $self->msg_more(join('', map { - "<$_->{mid}>\r\n"; - } @$msgs)); - $$prev = $msgs->[-1]->{num}; - return 1; # continue on current group - } + my ($self, $ibxs, $ts, $prev) = @_; + if (my $over = $ibxs->[0]->over) { + my $msgs = $over->query_ts($ts, $$prev); + if (scalar @$msgs) { + $self->msg_more(join('', map { + "<$_->{mid}>\r\n"; + } @$msgs)); + $$prev = $msgs->[-1]->{num}; + return 1; # continue on current group } } - shift @$names; - if (@$names) { # continue onto next newsgroup + shift @$ibxs; + if (@$ibxs) { # continue onto next newsgroup $$prev = 0; 1; } else { # all done, break out of the long_response @@ -348,46 +325,45 @@ sub cmd_newnews ($$$$;$$) { my ($self, $newsgroups, $date, $time, $gmt, $dists) = @_; my $ts = eval { parse_time($date, $time, $gmt) }; return r501 if $@; - more($self, '230 list of new articles by message-id follows'); - my ($keep, $skip) = split('!', $newsgroups, 2); + $self->msg_more("230 list of new articles by message-id follows\r\n"); + my ($keep, $skip) = split(/!/, $newsgroups, 2); ngpat2re($keep); ngpat2re($skip); - my @names = grep(!/$skip/, grep(/$keep/, - @{$self->{nntpd}->{groupnames}})); - return '.' unless scalar(@names); + my @names = grep(/$keep/, @{$self->{nntpd}->{groupnames}}); + @names = grep(!/$skip/, @names); + return \".\r\n" unless scalar(@names); my $prev = 0; - long_response($self, \&newnews_i, \@names, $ts, \$prev); + $self->long_response(\&newnews_i, names2ibx($self, \@names), + $ts, \$prev); } sub cmd_group ($$) { my ($self, $group) = @_; my $nntpd = $self->{nntpd}; my $ibx = $nntpd->{pi_cfg}->{-by_newsgroup}->{$group} or - return '411 no such news group'; + return \"411 no such news group\r\n"; $nntpd->idler_start; $self->{ibx} = $ibx; my ($min, $max) = $ibx->mm(1)->minmax; $self->{article} = $min; my $est_size = $max - $min; - "211 $est_size $min $max $group"; + "211 $est_size $min $max $group\r\n"; } sub article_adj ($$) { my ($self, $off) = @_; - my $ibx = $self->{ibx} or return '412 no newsgroup selected'; - - my $n = $self->{article}; - defined $n or return '420 no current article has been selected'; + my $ibx = $self->{ibx} // return \"412 no newsgroup selected\r\n"; + my $n = $self->{article} // + return \"420 no current article has been selected\r\n"; $n += $off; - my $mid = $ibx->mm(1)->mid_for($n); - unless ($mid) { + my $mid = $ibx->mm(1)->mid_for($n) // do { $n = $off > 0 ? 'next' : 'previous'; - return "421 no $n article in this group"; - } + return "421 no $n article in this group\r\n"; + }; $self->{article} = $n; - "223 $n <$mid> article retrieved - request text separately"; + "223 $n <$mid> article retrieved - request text separately\r\n"; } sub cmd_next ($) { article_adj($_[0], 1) } @@ -398,13 +374,13 @@ sub cmd_last ($) { article_adj($_[0], -1) } sub cmd_post ($) { my ($self) = @_; my $ibx = $self->{ibx}; - $ibx ? "440 mailto:$ibx->{-primary_address} to post" - : '440 posting not allowed' + $ibx ? "440 mailto:$ibx->{-primary_address} to post\r\n" + : \"440 posting not allowed\r\n" } sub cmd_quit ($) { my ($self) = @_; - res($self, '205 closing connection - goodbye!'); + $self->write(\"205 closing connection - goodbye!\r\n"); $self->shutdn; undef; } @@ -487,22 +463,22 @@ sub art_lookup ($$$) { my $err; if (defined $art) { if ($art =~ /\A[0-9]+\z/) { - $err = '423 no such article number in this group'; + $err = \"423 no such article number in this group\r\n"; $n = int($art); goto find_ibx; } elsif ($art =~ $ONE_MSGID) { ($ibx, $n) = mid_lookup($self, $1); goto found if $ibx; - return r430; + return \r430; } else { - return r501; + return \r501; } } else { - $err = '420 no current article has been selected'; + $err = \"420 no current article has been selected\r\n"; $n = $self->{article} // return $err; find_ibx: $ibx = $self->{ibx} or - return '412 no newsgroup has been selected'; + return \"412 no newsgroup has been selected\r\n"; } found: my $smsg = $ibx->over(1)->get_art($n) or return $err; @@ -510,7 +486,7 @@ found: if ($code == 223) { # STAT set_art($self, $n); "223 $n <$smsg->{mid}> article retrieved - " . - "request text separately"; + "request text separately\r\n"; } else { # HEAD | BODY | ARTICLE $smsg->{nntp} = $self; $smsg->{nntp_code} = $code; @@ -566,15 +542,15 @@ sub blob_cb { # called by git->cat_async via ibx_async_cat my $r = "$code $smsg->{num} <$smsg->{mid}> article retrieved - "; my $eml = PublicInbox::Eml->new($bref); if ($code == 220) { - more($self, $r .= 'head and body follow'); + $self->msg_more($r .= "head and body follow\r\n"); msg_hdr_write($eml, $smsg); $self->msg_more("\r\n"); msg_body_write($self, $bref); } elsif ($code == 221) { - more($self, $r .= 'head follows'); + $self->msg_more($r .= "head follows\r\n"); msg_hdr_write($eml, $smsg); } elsif ($code == 222) { - more($self, $r .= 'body follows'); + $self->msg_more($r .= "body follows\r\n"); msg_body_write($self, $bref); } else { $self->close; @@ -604,20 +580,18 @@ sub cmd_stat ($;$) { art_lookup($self, $art, 223); # art may be msgid } -sub cmd_ihave ($) { '435 article not wanted - do not send it' } +sub cmd_ihave ($) { \"435 article not wanted - do not send it\r\n" } -sub cmd_date ($) { '111 '.strftime('%Y%m%d%H%M%S', gmtime(time)) } +sub cmd_date ($) { '111 '.strftime('%Y%m%d%H%M%S', gmtime(time))."\r\n" } -sub cmd_help ($) { - my ($self) = @_; - more($self, '100 help text follows'); - '.' -} +sub cmd_help ($) { \"100 help text follows\r\n.\r\n" } +# returns a ref on success sub get_range ($$) { my ($self, $range) = @_; - my $ibx = $self->{ibx} or return '412 no news group has been selected'; - defined $range or return '420 No article(s) selected'; + my $ibx = $self->{ibx} // + return "412 no news group has been selected\r\n"; + $range // return "420 No article(s) selected\r\n"; my ($beg, $end); my ($min, $max) = $ibx->mm(1)->minmax; if ($range =~ /\A([0-9]+)\z/) { @@ -631,61 +605,10 @@ sub get_range ($$) { } $beg = $min if ($beg < $min); $end = $max if ($end > $max); - return '420 No article(s) selected' if ($beg > $end); - [ \$beg, $end ]; + $beg > $end ? "420 No article(s) selected\r\n" : [ \$beg, $end ]; } -sub long_step { - my ($self) = @_; - # wbuf is unset or empty, here; {long} may add to it - my ($fd, $cb, $t0, @args) = @{$self->{long_cb}}; - my $more = eval { $cb->($self, @args) }; - if ($@ || !$self->{sock}) { # something bad happened... - delete $self->{long_cb}; - my $elapsed = now() - $t0; - if ($@) { - err($self, - "%s during long response[$fd] - %0.6f", - $@, $elapsed); - } - out($self, " deferred[$fd] aborted - %0.6f", $elapsed); - $self->close; - } elsif ($more) { # $self->{wbuf}: - $self->update_idle_time; - - # COMPRESS users all share the same DEFLATE context. - # Flush it here to ensure clients don't see - # each other's data - $self->zflush; - - # no recursion, schedule another call ASAP, but only after - # all pending writes are done. autovivify wbuf: - my $new_size = push(@{$self->{wbuf}}, \&long_step); - - # wbuf may be populated by $cb, no need to rearm if so: - $self->requeue if $new_size == 1; - } else { # all done! - delete $self->{long_cb}; - res($self, '.'); - my $elapsed = now() - $t0; - my $fd = fileno($self->{sock}); - out($self, " deferred[$fd] done - %0.6f", $elapsed); - my $wbuf = $self->{wbuf}; # do NOT autovivify - $self->requeue unless $wbuf && @$wbuf; - } -} - -sub long_response ($$;@) { - my ($self, $cb, @args) = @_; # cb returns true if more, false if done - - my $sock = $self->{sock} or return; - # make sure we disable reading during a long response, - # clients should not be sending us stuff and making us do more - # work while we are stream a response to them - $self->{long_cb} = [ fileno($sock), $cb, now(), @args ]; - long_step($self); # kick off! - undef; -} +sub long_response_done { $_[0]->write(\".\r\n") } # overrides superclass sub hdr_msgid_range_i { my ($self, $beg, $end) = @_; @@ -706,8 +629,8 @@ sub hdr_message_id ($$$) { # optimize XHDR Message-ID [range] for slrnpull. $range = $self->{article} unless defined $range; my $r = get_range($self, $range); return $r unless ref $r; - more($self, $xhdr ? r221 : r225); - long_response($self, \&hdr_msgid_range_i, @$r); + $self->msg_more($xhdr ? r221 : r225); + $self->long_response(\&hdr_msgid_range_i, @$r); } } @@ -778,8 +701,8 @@ sub hdr_xref ($$$) { # optimize XHDR Xref [range] for rtin $range = $self->{article} unless defined $range; my $r = get_range($self, $range); return $r unless ref $r; - more($self, $xhdr ? r221 : r225); - long_response($self, \&xref_range_i, @$r); + $self->msg_more($xhdr ? r221 : r225); + $self->long_response(\&xref_range_i, @$r); } } @@ -822,8 +745,8 @@ sub hdr_smsg ($$$$) { $range = $self->{article} unless defined $range; my $r = get_range($self, $range); return $r unless ref $r; - more($self, $xhdr ? r221 : r225); - long_response($self, \&smsg_range_i, @$r, $field); + $self->msg_more($xhdr ? r221 : r225); + $self->long_response(\&smsg_range_i, @$r, $field); } } @@ -840,7 +763,7 @@ sub do_hdr ($$$;$) { } elsif ($sub =~ /\A:(bytes|lines)\z/) { hdr_smsg($self, $xhdr, $1, $range); } else { - $xhdr ? (r221 . "\r\n.") : "503 HDR not permitted on $header"; + $xhdr ? (r221.".\r\n") : "503 HDR not permitted on $header\r\n"; } } @@ -870,37 +793,30 @@ sub hdr_mid_prefix ($$$$$) { sub hdr_mid_response ($$$$$$) { my ($self, $xhdr, $ibx, $n, $mid, $v) = @_; - my $res = ''; - if ($xhdr) { - $res .= r221 . "\r\n"; - $res .= "$mid $v\r\n"; - } else { - $res .= r225 . "\r\n"; - my $pfx = hdr_mid_prefix($self, $xhdr, $ibx, $n, $mid); - $res .= "$pfx $v\r\n"; - } - res($self, $res .= '.'); + $self->write(($xhdr ? r221.$mid : + r225.hdr_mid_prefix($self, $xhdr, $ibx, $n, $mid)) . + " $v\r\n.\r\n"); undef; } sub xrover_i { my ($self, $beg, $end) = @_; my $h = over_header_for($self->{ibx}, $$beg, 'references'); - more($self, "$$beg $h") if defined($h); + $self->msg_more("$$beg $h\r\n") if defined($h); $$beg++ < $end; } sub cmd_xrover ($;$) { my ($self, $range) = @_; - my $ibx = $self->{ibx} or return '412 no newsgroup selected'; + my $ibx = $self->{ibx} or return \"412 no newsgroup selected\r\n"; (defined $range && $range =~ /[<>]/) and - return '420 No article(s) selected'; # no message IDs + return \"420 No article(s) selected\r\n"; # no message IDs $range = $self->{article} unless defined $range; my $r = get_range($self, $range); return $r unless ref $r; - more($self, '224 Overview information follows'); - long_response($self, \&xrover_i, @$r); + $self->msg_more("224 Overview information follows\r\n"); + $self->long_response(\&xrover_i, @$r); } sub over_line ($$$) { @@ -926,7 +842,8 @@ sub cmd_over ($;$) { my ($ibx, $n) = mid_lookup($self, $1); defined $n or return r430; my $smsg = $ibx->over(1)->get_art($n) or return r430; - more($self, '224 Overview information follows (multi-line)'); + $self->msg_more( + "224 Overview information follows (multi-line)\r\n"); # Only set article number column if it's the current group # (RFC 3977 8.3.2) @@ -936,8 +853,7 @@ sub cmd_over ($;$) { $smsg->{-orig_num} = $smsg->{num}; $smsg->{num} = 0; } - $self->msg_more(over_line($self, $ibx, $smsg)); - '.'; + over_line($self, $ibx, $smsg).".\r\n"; } else { cmd_xover($self, $range); } @@ -962,20 +878,19 @@ sub cmd_xover ($;$) { my $r = get_range($self, $range); return $r unless ref $r; my ($beg, $end) = @$r; - more($self, "224 Overview information follows for $$beg to $end"); - long_response($self, \&xover_i, @$r); + $self->msg_more( + "224 Overview information follows for $$beg to $end\r\n"); + $self->long_response(\&xover_i, @$r); } -sub compressed { undef } - sub cmd_starttls ($) { my ($self) = @_; my $sock = $self->{sock} or return; # RFC 4642 2.2.1 return r502 if ($sock->can('accept_SSL') || $self->compressed); my $opt = $self->{nntpd}->{accept_tls} or - return '580 can not initiate TLS negotiation'; - res($self, '382 Continue with TLS negotiation'); + return \"580 can not initiate TLS negotiation\r\n"; + $self->write(\"382 Continue with TLS negotiation\r\n"); $self->{sock} = IO::Socket::SSL->start_SSL($sock, %$opt); $self->requeue if PublicInbox::DS::accept_tls_step($self); undef; @@ -984,15 +899,15 @@ sub cmd_starttls ($) { # RFC 8054 sub cmd_compress ($$) { my ($self, $alg) = @_; - return '503 Only DEFLATE is supported' if uc($alg) ne 'DEFLATE'; + return "503 Only DEFLATE is supported\r\n" if uc($alg) ne 'DEFLATE'; return r502 if $self->compressed; - PublicInbox::NNTPdeflate->enable($self); + PublicInbox::NNTPdeflate->enable($self) or return + \"403 Unable to activate compression\r\n"; + PublicInbox::DS::write($self, \"206 Compression active\r\n"); $self->requeue; undef } -sub zflush {} # overridden by NNTPdeflate - sub cmd_xpath ($$) { my ($self, $mid) = @_; return r501 unless $mid =~ $ONE_MSGID; @@ -1018,20 +933,8 @@ sub cmd_xpath ($$) { push @paths, "$ibx->{newsgroup}/$n"; } } - return '430 no such article on server' unless @paths; - '223 '.join(' ', sort(@paths)); -} - -sub res ($$) { do_write($_[0], $_[1] . "\r\n") } - -sub more ($$) { $_[0]->msg_more($_[1] . "\r\n") } - -sub do_write ($$) { - my $self = $_[0]; - my $done = $self->write(\($_[1])); - return 0 unless $self->{sock}; - - $done; + return \"430 no such article on server\r\n" unless @paths; + '223 '.join(' ', sort(@paths))."\r\n"; } sub err ($$;@) { @@ -1050,7 +953,6 @@ sub event_step { return unless $self->flush_write && $self->{sock} && !$self->{long_cb}; - $self->update_idle_time; # only read more requests if we've drained the write buffer, # otherwise we can be buffering infinitely w/o backpressure @@ -1072,17 +974,19 @@ sub event_step { out($self, "[$fd] %s - %0.6f$pending", $line, now() - $t0); return $self->close if $r < 0; $self->rbuf_idle($rbuf); - $self->update_idle_time; # maybe there's more pipelined data, or we'll have # to register it for socket-readiness notifications $self->requeue unless $pending; } -# for graceful shutdown in PublicInbox::Daemon: -sub busy { - my ($self, $now) = @_; - ($self->{rbuf} || $self->{wbuf} || $self->not_idle_long($now)); +sub busy { # for graceful shutdown in PublicInbox::Daemon: + my ($self) = @_; + defined($self->{rbuf}) || defined($self->{wbuf}) } +package PublicInbox::NNTPdeflate; +use PublicInbox::DSdeflate; +our @ISA = qw(PublicInbox::DSdeflate PublicInbox::NNTP); + 1;