X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FNNTP.pm;h=9d91544abd309d7186ceae91e338129a42f1e361;hb=94096cab6cd5e00c8a36a4a2667bdb9acf43d01f;hp=bffd773cf9eb087c5d990fed67db0da10e2ff345;hpb=2a717d13f10fcdc69921d80cf94c47a694a175d4;p=public-inbox.git diff --git a/lib/PublicInbox/NNTP.pm b/lib/PublicInbox/NNTP.pm index bffd773c..9d91544a 100644 --- a/lib/PublicInbox/NNTP.pm +++ b/lib/PublicInbox/NNTP.pm @@ -2,17 +2,21 @@ # License: AGPL-3.0+ # # Each instance of this represents a NNTP client socket +# fields: +# nntpd: PublicInbox::NNTPD ref +# article: per-session current article number +# ng: PublicInbox::Inbox ref +# long_cb: long_response private data package PublicInbox::NNTP; use strict; -use warnings; -use base qw(PublicInbox::DS); -use fields qw(nntpd article ng long_cb); +use parent qw(PublicInbox::DS); use PublicInbox::MID qw(mid_escape $MID_EXTRACT); use PublicInbox::Eml; use POSIX qw(strftime); use PublicInbox::DS qw(now); use Digest::SHA qw(sha1_hex); use Time::Local qw(timegm timelocal); +use PublicInbox::GitAsyncCat; use constant { LINE_MAX => 512, # RFC 977 section 2.3 r501 => '501 command syntax error', @@ -44,16 +48,15 @@ sub greet ($) { $_[0]->write($_[0]->{nntpd}->{greet}) }; sub new ($$$) { my ($class, $sock, $nntpd) = @_; - my $self = fields::new($class); + my $self = bless { nntpd => $nntpd }, $class; my $ev = EPOLLIN; my $wbuf; - if (ref($sock) eq 'IO::Socket::SSL' && !$sock->accept_SSL) { + if ($sock->can('accept_SSL') && !$sock->accept_SSL) { return CORE::close($sock) if $! != EAGAIN; $ev = PublicInbox::TLS::epollbit(); $wbuf = [ \&PublicInbox::DS::accept_tls_step, \&greet ]; } $self->SUPER::new($sock, $ev | EPOLLONESHOT); - $self->{nntpd} = $nntpd; if ($wbuf) { $self->{wbuf} = $wbuf; } else { @@ -97,7 +100,7 @@ sub process_line ($$) { sub cmd_capabilities ($;$) { my ($self, undef) = @_; my $res = $CAPABILITIES; - if (ref($self->{sock}) ne 'IO::Socket::SSL' && + if (!$self->{sock}->can('accept_SSL') && $self->{nntpd}->{accept_tls}) { $res .= "STARTTLS\r\n"; } @@ -408,8 +411,9 @@ sub xref ($$$$) { $ret; } -sub set_nntp_headers ($$$$$) { - my ($self, $hdr, $ng, $n, $mid) = @_; +sub set_nntp_headers ($$$) { + my ($self, $hdr, $smsg) = @_; + my ($mid) = $smsg->{mid}; # why? leafnode requires a Path: header for some inexplicable # reason. We'll fake the shortest one possible. @@ -429,7 +433,8 @@ sub set_nntp_headers ($$$$$) { } # clobber some - my $xref = xref($self, $ng, $n, $mid); + my $ng = $self->{ng}; + my $xref = xref($self, $ng, $smsg->{num}, $mid); $hdr->header_set('Xref', $xref); $xref =~ s/:[0-9]+//g; $hdr->header_set('Newsgroups', (split(/ /, $xref, 2))[1]); @@ -441,8 +446,8 @@ sub set_nntp_headers ($$$$$) { } } -sub art_lookup ($$$) { - my ($self, $art, $set_headers) = @_; +sub art_lookup ($$) { + my ($self, $art) = @_; my $ng = $self->{ng}; my ($n, $mid); my $err; @@ -478,13 +483,7 @@ find_mid: } found: my $smsg = $ng->over->get_art($n) or return $err; - my $msg = $ng->msg_by_smsg($smsg) or return $err; - - # PublicInbox::Eml->new will modify $msg in-place, so what's - # left is the body and we won't need to call ->body(), later - my $hdr = PublicInbox::Eml->new($msg)->header_obj; - set_nntp_headers($self, $hdr, $ng, $n, $mid) if $set_headers; - [ $n, $mid, $msg, $hdr ]; + $smsg; } sub msg_body_write ($$) { @@ -495,7 +494,6 @@ sub msg_body_write ($$) { $$msg =~ s/(?msg_more($$msg); - '.' } sub set_art { @@ -504,55 +502,85 @@ sub set_art { } sub msg_hdr_write ($$$) { - my ($self, $hdr, $body_follows) = @_; - $hdr = $hdr->as_string; + my ($self, $eml, $smsg) = @_; + set_nntp_headers($self, $eml, $smsg); + + my $hdr = $eml->{hdr} // \(my $x = ''); # fixup old bug from import (pre-a0c07cba0e5d8b6a) - $hdr =~ s/\A[\r\n]*From [^\r\n]*\r?\n//s; - utf8::encode($hdr); - $hdr =~ s/(?msg_more($hdr); + $$hdr =~ s/^(Message-ID:)[ \t]*\r\n[ \t]+([^\r]+)\r\n/$1 $2\r\n/igsm; + $self->msg_more($$hdr); +} + +sub blob_cb { # called by git->cat_async via git_async_cat + my ($bref, $oid, $type, $size, $smsg) = @_; + my $self = $smsg->{nntp}; + my $code = $smsg->{nntp_code} // 220; + if (!defined($oid)) { + # it's possible to have TOCTOU if an admin runs + # public-inbox-(edit|purge), just move onto the next message + return $self->requeue; + } elsif ($smsg->{blob} ne $oid) { + $self->close; + die "BUG: $smsg->{blob} != $oid"; + } + 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'); + msg_hdr_write($self, $eml, $smsg); + $self->msg_more("\r\n"); + msg_body_write($self, $bref); + } elsif ($code == 221) { + more($self, $r .= 'head follows'); + msg_hdr_write($self, $eml, $smsg); + } elsif ($code == 222) { + more($self, $r .= 'body follows'); + msg_body_write($self, $bref); + } else { + $self->close; + die "BUG: bad code: $r"; + } + $self->write(\".\r\n"); # flushes (includes ->zflush) + $self->requeue; } sub cmd_article ($;$) { my ($self, $art) = @_; - my $r = art_lookup($self, $art, 1); - return $r unless ref $r; - my ($n, $mid, $msg, $hdr) = @$r; + my $smsg = art_lookup($self, $art); + return $smsg unless ref $smsg; set_art($self, $art); - more($self, "220 $n <$mid> article retrieved - head and body follow"); - msg_hdr_write($self, $hdr, 1); - msg_body_write($self, $msg); + $smsg->{nntp} = $self; + ${git_async_cat($self->{ng}->git, $smsg->{blob}, \&blob_cb, $smsg)}; } sub cmd_head ($;$) { my ($self, $art) = @_; - my $r = art_lookup($self, $art, 2); - return $r unless ref $r; - my ($n, $mid, undef, $hdr) = @$r; + my $smsg = art_lookup($self, $art); + return $smsg unless ref $smsg; set_art($self, $art); - more($self, "221 $n <$mid> article retrieved - head follows"); - msg_hdr_write($self, $hdr, 0); - '.' + $smsg->{nntp} = $self; + $smsg->{nntp_code} = 221; + ${git_async_cat($self->{ng}->git, $smsg->{blob}, \&blob_cb, $smsg)}; } sub cmd_body ($;$) { my ($self, $art) = @_; - my $r = art_lookup($self, $art, 0); - return $r unless ref $r; - my ($n, $mid, $msg) = @$r; + my $smsg = art_lookup($self, $art); + return $smsg unless ref $smsg; set_art($self, $art); - more($self, "222 $n <$mid> article retrieved - body follows"); - msg_body_write($self, $msg); + $smsg->{nntp} = $self; + $smsg->{nntp_code} = 222; + ${git_async_cat($self->{ng}->git, $smsg->{blob}, \&blob_cb, $smsg)}; } sub cmd_stat ($;$) { my ($self, $art) = @_; - my $r = art_lookup($self, $art, 0); + my $r = art_lookup($self, $art); return $r unless ref $r; my ($n, $mid) = @$r; set_art($self, $art); @@ -896,7 +924,7 @@ sub cmd_starttls ($) { my ($self) = @_; my $sock = $self->{sock} or return; # RFC 4642 2.2.1 - return r502 if (ref($sock) eq 'IO::Socket::SSL' || $self->compressed); + 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'); @@ -956,38 +984,35 @@ sub out ($$;@) { sub event_step { my ($self) = @_; - return unless $self->flush_write && $self->{sock}; + 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 - my $rbuf = $self->{rbuf} // (\(my $x = '')); - my $r = 1; - - if (index($$rbuf, "\n") < 0) { - my $off = bytes::length($$rbuf); - $r = $self->do_read($rbuf, LINE_MAX, $off) or return; - } - while ($r > 0 && $$rbuf =~ s/\A[ \t]*([^\n]*?)\r?\n//) { - my $line = $1; - return $self->close if $line =~ /[[:cntrl:]]/s; - my $t0 = now(); - my $fd = fileno($self->{sock}); - $r = eval { process_line($self, $line) }; - my $pending = $self->{wbuf} ? ' pending' : ''; - out($self, "[$fd] %s - %0.6f$pending", $line, now() - $t0); + my $rbuf = $self->{rbuf} // \(my $x = ''); + my $line = index($$rbuf, "\n"); + while ($line < 0) { + return $self->close if length($$rbuf) >= LINE_MAX; + $self->do_read($rbuf, LINE_MAX, length($$rbuf)) or return; + $line = index($$rbuf, "\n"); } - + $line = substr($$rbuf, 0, $line + 1, ''); + $line =~ s/\r?\n\z//s; + return $self->close if $line =~ /[[:cntrl:]]/s; + + my $t0 = now(); + my $fd = fileno($self->{sock}); + my $r = eval { process_line($self, $line) }; + my $pending = $self->{wbuf} ? ' pending' : ''; + out($self, "[$fd] %s - %0.6f$pending", $line, now() - $t0); return $self->close if $r < 0; - my $len = bytes::length($$rbuf); - return $self->close if ($len >= LINE_MAX); $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 $self->{wbuf}; + $self->requeue unless $pending; } # for graceful shutdown in PublicInbox::Daemon: