]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/NNTP.pm
127603942e6fafc233a852e3671e9b6c4dd091c0
[public-inbox.git] / lib / PublicInbox / NNTP.pm
1 # Copyright (C) 2015 all contributors <meta@public-inbox.org>
2 # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3 package PublicInbox::NNTP;
4 use strict;
5 use warnings;
6 use base qw(Danga::Socket);
7 use fields qw(nntpd article rbuf ng long_res);
8 use PublicInbox::Msgmap;
9 use PublicInbox::GitCatFile;
10 use PublicInbox::MID qw(mid2path);
11 use Email::MIME;
12 use Data::Dumper qw(Dumper);
13 use POSIX qw(strftime);
14 use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
15 use constant {
16         r501 => '501 command syntax error',
17         r221 => '221 Header follows',
18         r224 => '224 Overview information follows (multi-line)',
19         r225 => '225 Headers follow (multi-line)',
20         r430 => '430 No article with that message-id',
21         long_response_limit => 0xffffffff,
22 };
23
24 sub now () { clock_gettime(CLOCK_MONOTONIC) };
25
26 my @OVERVIEW = qw(Subject From Date Message-ID References Bytes Lines);
27 my $OVERVIEW_FMT = join(":\r\n", @OVERVIEW) . ":\r\n";
28 my $LIST_HEADERS = join("\r\n", qw(Subject From Date Message-ID References
29                                   :bytes :lines Xref To Cc)) . "\r\n";
30
31 # disable commands with easy DoS potential:
32 # LISTGROUP could get pretty bad, too...
33 my %DISABLED; # = map { $_ => 1 } qw(xover list_overview_fmt newnews xhdr);
34
35 sub new ($$$) {
36         my ($class, $sock, $nntpd) = @_;
37         my $self = fields::new($class);
38         binmode $sock, ':utf8'; # RFC 3977
39         $self->SUPER::new($sock);
40         $self->{nntpd} = $nntpd;
41         res($self, '201 server ready - post via email');
42         $self->{rbuf} = '';
43         $self->watch_read(1);
44         $self;
45 }
46
47 sub args_ok ($$) {
48         my ($cb, $argc) = @_;
49         my $tot = prototype $cb;
50         my ($nreq, undef) = split(';', $tot);
51         $nreq = ($nreq =~ tr/$//) - 1;
52         $tot = ($tot =~ tr/$//) - 1;
53         ($argc <= $tot && $argc >= $nreq);
54 }
55
56 # returns 1 if we can continue, 0 if not due to buffered writes or disconnect
57 sub process_line ($$) {
58         my ($self, $l) = @_;
59         my ($req, @args) = split(/\s+/, $l);
60         $req = lc($req);
61         $req = eval {
62                 no strict 'refs';
63                 $req = $DISABLED{$req} ? undef : *{'cmd_'.$req}{CODE};
64         };
65         return res($self, '500 command not recognized') unless $req;
66         return res($self, r501) unless args_ok($req, scalar @args);
67
68         my $res = eval { $req->($self, @args) };
69         my $err = $@;
70         if ($err && !$self->{closed}) {
71                 chomp($l = Dumper(\$l));
72                 err($self, "error from: $l $err");
73                 $res = '503 program fault - command not performed';
74         }
75         return 0 unless defined $res;
76         res($self, $res);
77 }
78
79 sub cmd_mode ($$) {
80         my ($self, $arg) = @_;
81         $arg = uc $arg;
82         return r501 unless $arg eq 'READER';
83         '201 Posting prohibited';
84 }
85
86 sub cmd_slave ($) { '202 slave status noted' }
87
88 sub cmd_xgtitle ($;$) {
89         my ($self, $wildmat) = @_;
90         more($self, '282 list of groups and descriptions follows');
91         list_newsgroups($self, $wildmat);
92         '.'
93 }
94
95 sub list_overview_fmt ($) {
96         my ($self) = @_;
97         do_more($self, $OVERVIEW_FMT);
98 }
99
100 sub list_headers ($;$) {
101         my ($self) = @_;
102         do_more($self, $LIST_HEADERS);
103 }
104
105 sub list_active ($;$) {
106         my ($self, $wildmat) = @_;
107         wildmat2re($wildmat);
108         foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
109                 $ng->{name} =~ $wildmat or next;
110                 group_line($self, $ng);
111         }
112 }
113
114 sub list_active_times ($;$) {
115         my ($self, $wildmat) = @_;
116         wildmat2re($wildmat);
117         foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
118                 $ng->{name} =~ $wildmat or next;
119                 my $c = eval { $ng->mm->created_at } || time;
120                 more($self, "$ng->{name} $c $ng->{address}");
121         }
122 }
123
124 sub list_newsgroups ($;$) {
125         my ($self, $wildmat) = @_;
126         wildmat2re($wildmat);
127         foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
128                 $ng->{name} =~ $wildmat or next;
129                 my $d = $ng->description;
130                 more($self, "$ng->{name} $d");
131         }
132 }
133
134 # LIST SUBSCRIPTIONS, DISTRIB.PATS are not supported
135 sub cmd_list ($;$$) {
136         my ($self, @args) = @_;
137         if (scalar @args) {
138                 my $arg = shift @args;
139                 $arg =~ tr/A-Z./a-z_/;
140                 $arg = "list_$arg";
141                 return r501 if $DISABLED{$arg};
142
143                 $arg = eval {
144                         no strict 'refs';
145                         *{$arg}{CODE};
146                 };
147                 return r501 unless $arg && args_ok($arg, scalar @args);
148                 more($self, '215 information follows');
149                 $arg->($self, @args);
150         } else {
151                 more($self, '215 list of newsgroups follows');
152                 foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
153                         group_line($self, $ng);
154                 }
155         }
156         '.'
157 }
158
159 sub cmd_listgroup ($;$) {
160         my ($self, $group) = @_;
161         if (defined $group) {
162                 my $res = cmd_group($self, $group);
163                 return $res if ($res !~ /\A211 /);
164                 more($self, $res);
165         }
166
167         $self->{ng} or return '412 no newsgroup selected';
168         $self->long_response(0, long_response_limit, sub {
169                 my ($i) = @_;
170                 my $nr = $self->{ng}->mm->id_batch($$i, sub {
171                         my ($ary) = @_;
172                         more($self, join("\r\n", @$ary));
173                 });
174
175                 # -1 to adjust for implicit increment in long_response
176                 $$i = $nr ? $$i + $nr - 1 : long_response_limit;
177         });
178 }
179
180 sub parse_time ($$;$) {
181         my ($date, $time, $gmt) = @_;
182         use Time::Local qw();
183         my ($hh, $mm, $ss) = unpack('A2A2A2', $time);
184         if (defined $gmt) {
185                 $gmt =~ /\A(?:UTC|GMT)\z/i or die "GM invalid: $gmt\n";
186                 $gmt = 1;
187         }
188         my @now = $gmt ? gmtime : localtime;
189         my ($YYYY, $MM, $DD);
190         if (length($date) == 8) { # RFC 3977 allows YYYYMMDD
191                 ($YYYY, $MM, $DD) = unpack('A4A2A2', $date);
192         } else { # legacy clients send YYMMDD
193                 ($YYYY, $MM, $DD) = unpack('A2A2A2', $date);
194                 if ($YYYY > strftime('%y', @now)) {
195                         my $cur_year = $now[5] + 1900;
196                         $YYYY += int($cur_year / 1000) * 1000 - 100;
197                 }
198         }
199         if ($gmt) {
200                 Time::Local::timegm($ss, $mm, $hh, $DD, $MM - 1, $YYYY);
201         } else {
202                 Time::Local::timelocal($ss, $mm, $hh, $DD, $MM - 1, $YYYY);
203         }
204 }
205
206 sub group_line ($$) {
207         my ($self, $ng) = @_;
208         my ($min, $max) = $ng->mm->minmax;
209         more($self, "$ng->{name} $max $min n") if defined $min && defined $max;
210 }
211
212 sub cmd_newgroups ($$$;$$) {
213         my ($self, $date, $time, $gmt, $dists) = @_;
214         my $ts = eval { parse_time($date, $time, $gmt) };
215         return r501 if $@;
216
217         # TODO dists
218         more($self, '231 list of new newsgroups follows');
219         foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
220                 my $c = eval { $ng->mm->created_at } || 0;
221                 next unless $c > $ts;
222                 group_line($self, $ng);
223         }
224         '.'
225 }
226
227 sub wildmat2re (;$) {
228         return $_[0] = qr/.*/ if (!defined $_[0] || $_[0] eq '*');
229         my %keep;
230         my $salt = rand;
231         use Digest::SHA qw(sha1_hex);
232         my $tmp = $_[0];
233
234         $tmp =~ s#(?<!\\)\[(.+)(?<!\\)\]#
235                 my $orig = $1;
236                 my $key = sha1_hex($orig . $salt);
237                 $orig =~ s/([^\w\-])+/\Q$1/g;
238                 $keep{$key} = $orig;
239                 $key
240                 #gex;
241         my %map = ('*' => '.*', '?' => '.' );
242         $tmp =~ s#(?<!\\)([^\w\\])#$map{$1} || "\Q$1"#ge;
243         if (scalar %keep) {
244                 $tmp =~ s#([a-f0-9]{40})#
245                         my $orig = $keep{$1};
246                         defined $orig ? $orig : $1;
247                         #ge;
248         }
249         $_[0] = qr/\A$tmp\z/;
250 }
251
252 sub ngpat2re (;$) {
253         return $_[0] = qr/\A\z/ unless defined $_[0];
254         my %map = ('*' => '.*', ',' => '|');
255         $_[0] =~ s!(.)!$map{$1} || "\Q$1"!ge;
256         $_[0] = qr/\A(?:$_[0])\z/;
257 }
258
259 sub cmd_newnews ($$$$;$$) {
260         my ($self, $newsgroups, $date, $time, $gmt, $dists) = @_;
261         my $ts = eval { parse_time($date, $time, $gmt) };
262         return r501 if $@;
263         more($self, '230 list of new articles by message-id follows');
264         my ($keep, $skip) = split('!', $newsgroups, 2);
265         ngpat2re($keep);
266         ngpat2re($skip);
267         my @srch;
268         foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
269                 $ng->{name} =~ $keep or next;
270                 $ng->{name} =~ $skip and next;
271                 my $srch = $ng->search or next;
272                 push @srch, $srch;
273         };
274         return '.' unless @srch;
275
276         $ts .= '..';
277         my $opts = { asc => 1, limit => 1000, offset => 0 };
278         $self->long_response(0, long_response_limit, sub {
279                 my ($i) = @_;
280                 my $srch = $srch[0];
281                 my $res = $srch->query($ts, $opts);
282                 my $msgs = $res->{msgs};
283                 if (my $nr = scalar @$msgs) {
284                         more($self, '<' .
285                                 join(">\r\n<", map { $_->mid } @$msgs ).
286                                 '>');
287                         $opts->{offset} += $nr;
288                 } else {
289                         shift @srch;
290                         if (@srch) { # continue onto next newsgroup
291                                 $opts->{offset} = 0;
292                         } else { # break out of the long response.
293                                 $$i = long_response_limit;
294                         }
295                 }
296         });
297 }
298
299 sub cmd_group ($$) {
300         my ($self, $group) = @_;
301         my $no_such = '411 no such news group';
302         my $ng = $self->{nntpd}->{groups}->{$group} or return $no_such;
303
304         $self->{ng} = $ng;
305         my ($min, $max) = $ng->mm->minmax;
306         $min ||= 0;
307         $max ||= 0;
308         $self->{article} = $min;
309         my $est_size = $max - $min;
310         "211 $est_size $min $max $group";
311 }
312
313 sub article_adj ($$) {
314         my ($self, $off) = @_;
315         my $ng = $self->{ng} or return '412 no newsgroup selected';
316
317         my $n = $self->{article};
318         defined $n or return '420 no current article has been selected';
319
320         $n += $off;
321         my $mid = $ng->mm->mid_for($n);
322         unless ($mid) {
323                 $n = $off > 0 ? 'next' : 'previous';
324                 return "421 no $n article in this group";
325         }
326         $self->{article} = $n;
327         "223 $n <$mid> article retrieved - request text separately";
328 }
329
330 sub cmd_next ($) { article_adj($_[0], 1) }
331 sub cmd_last ($) { article_adj($_[0], -1) }
332
333 # We want to encourage using email and CC-ing everybody involved to avoid
334 # the single-point-of-failure a single server provides.
335 sub cmd_post ($) {
336         my ($self) = @_;
337         my $ng = $self->{ng};
338         $ng ? "440 mailto:$ng->{address} to post" : '440 posting not allowed'
339 }
340
341 sub cmd_quit ($) {
342         my ($self) = @_;
343         res($self, '205 closing connection - goodbye!');
344         $self->close;
345         undef;
346 }
347
348 sub art_lookup ($$$) {
349         my ($self, $art, $set_headers) = @_;
350         my $ng = $self->{ng};
351         my ($n, $mid);
352         my $err;
353         if (defined $art) {
354                 if ($art =~ /\A\d+\z/o) {
355                         $err = '423 no such article number in this group';
356                         $n = int($art);
357                         goto find_mid;
358                 } elsif ($art =~ /\A<([^>]+)>\z/) {
359                         $mid = $1;
360                         $err = r430;
361                         $n = $ng->mm->num_for($mid) if $ng;
362                         goto found if defined $n;
363                         foreach my $g (values %{$self->{nntpd}->{groups}}) {
364                                 $n = $g->mm->num_for($mid);
365                                 if (defined $n) {
366                                         $ng = $g;
367                                         goto found;
368                                 }
369                         }
370                         return $err;
371                 } else {
372                         return r501;
373                 }
374         } else {
375                 $err = '420 no current article has been selected';
376                 $n = $self->{article};
377                 defined $n or return $err;
378 find_mid:
379                 $ng or return '412 no newsgroup has been selected';
380                 $mid = $ng->mm->mid_for($n);
381                 defined $mid or return $err;
382         }
383 found:
384         my $o = 'HEAD:' . mid2path($mid);
385         my $bytes;
386         my $s = eval { Email::MIME->new($ng->gcf->cat_file($o, \$bytes)) };
387         return $err unless $s;
388         my $lines;
389         if ($set_headers) {
390                 $s->header_set('Newsgroups', $ng->{name});
391                 $s->header_set('Xref', xref($ng, $n));
392                 $lines = $s->body =~ tr!\n!\n!;
393
394                 # must be last
395                 $s->body_set('') if ($set_headers == 2);
396         }
397         [ $n, $mid, $s, $bytes, $lines, $ng ];
398 }
399
400 sub simple_body_write ($$) {
401         my ($self, $s) = @_;
402         my $body = $s->body;
403         $s->body_set('');
404         $body =~ s/^\./../smg;
405         $body =~ s/(?<!\r)\n/\r\n/sg;
406         do_more($self, $body);
407         do_more($self, "\r\n") unless $body =~ /\r\n\z/s;
408         '.'
409 }
410
411 sub set_art {
412         my ($self, $art) = @_;
413         $self->{article} = $art if defined $art && $art =~ /\A\d+\z/;
414 }
415
416 sub cmd_article ($;$) {
417         my ($self, $art) = @_;
418         my $r = $self->art_lookup($art, 1);
419         return $r unless ref $r;
420         my ($n, $mid, $s) = @$r;
421         set_art($self, $art);
422         more($self, "220 $n <$mid> article retrieved - head and body follow");
423         do_more($self, $s->header_obj->as_string);
424         do_more($self, "\r\n");
425         simple_body_write($self, $s);
426 }
427
428 sub cmd_head ($;$) {
429         my ($self, $art) = @_;
430         my $r = $self->art_lookup($art, 2);
431         return $r unless ref $r;
432         my ($n, $mid, $s) = @$r;
433         set_art($self, $art);
434         more($self, "221 $n <$mid> article retrieved - head follows");
435         do_more($self, $s->header_obj->as_string);
436         '.'
437 }
438
439 sub cmd_body ($;$) {
440         my ($self, $art) = @_;
441         my $r = $self->art_lookup($art, 0);
442         return $r unless ref $r;
443         my ($n, $mid, $s) = @$r;
444         set_art($self, $art);
445         more($self, "222 $n <$mid> article retrieved - body follows");
446         simple_body_write($self, $s);
447 }
448
449 sub cmd_stat ($;$) {
450         my ($self, $art) = @_;
451         my $r = $self->art_lookup($art, 0);
452         return $r unless ref $r;
453         my ($n, $mid, undef) = @$r;
454         set_art($self, $art);
455         "223 $n <$mid> article retrieved - request text separately";
456 }
457
458 sub cmd_ihave ($) { '435 article not wanted - do not send it' }
459
460 sub cmd_date ($) { '111 '.strftime('%Y%m%d%H%M%S', gmtime(time)) }
461
462 sub cmd_help ($) {
463         my ($self) = @_;
464         more($self, '100 help text follows');
465         '.'
466 }
467
468 sub get_range ($$) {
469         my ($self, $range) = @_;
470         my $ng = $self->{ng} or return '412 no news group has been selected';
471         defined $range or return '420 No article(s) selected';
472         my ($beg, $end);
473         my ($min, $max) = $ng->mm->minmax;
474         if ($range =~ /\A(\d+)\z/) {
475                 $beg = $end = $1;
476         } elsif ($range =~ /\A(\d+)-\z/) {
477                 ($beg, $end) = ($1, $max);
478         } elsif ($range =~ /\A(\d+)-(\d+)\z/) {
479                 ($beg, $end) = ($1, $2);
480         } else {
481                 return r501;
482         }
483         $beg = $min if ($beg < $min);
484         $end = $max if ($end > $max);
485         return '420 No article(s) selected' if ($beg > $end);
486         [ $beg, $end ];
487 }
488
489 sub hdr_val ($$) {
490         my ($r, $header) = @_;
491         return $r->[3] if $header =~ /\A:?bytes\z/i;
492         return $r->[4] if $header =~ /\A:?lines\z/i;
493         $r = $r->[2]->header_obj->header($header);
494         defined $r or return;
495         $r =~ s/[\r\n\t]+/ /sg;
496         $r;
497 }
498
499 sub long_response ($$$$) {
500         my ($self, $beg, $end, $cb) = @_;
501         die "BUG: nested long response" if $self->{long_res};
502
503         my $fd = $self->{fd};
504         defined $fd or return;
505         # make sure we disable reading during a long response,
506         # clients should not be sending us stuff and making us do more
507         # work while we are stream a response to them
508         $self->watch_read(0);
509         my $t0 = now();
510         $self->{long_res} = sub {
511                 # limit our own running time for fairness with other
512                 # clients and to avoid buffering too much:
513                 my $lim = 100;
514
515                 my $err;
516                 do {
517                         eval { $cb->(\$beg) };
518                 } until (($err = $@) || $self->{closed} ||
519                          ++$beg > $end || !--$lim || $self->{write_buf_size});
520
521                 if ($err || $self->{closed}) {
522                         $self->{long_res} = undef;
523
524                         if ($err) {
525                                 err($self,
526                                     "$err during long response[$fd] - %0.6f",
527                                         now() - $t0);
528                         }
529                         if ($self->{closed}) {
530                                 out($self, " deferred[$fd] aborted - %0.6f",
531                                            now() - $t0);
532                         } else {
533                                 $self->watch_read(1);
534                         }
535                 } elsif (!$lim || $self->{write_buf_size}) {
536                         # no recursion, schedule another call ASAP
537                         # but only after all pending writes are done
538                         Danga::Socket->AddTimer(0, sub {
539                                 $self->write($self->{long_res});
540                         });
541                 } else { # all done!
542                         $self->{long_res} = undef;
543                         $self->watch_read(1);
544                         res($self, '.');
545                         out($self, " deferred[$fd] done - %0.6f", now() - $t0);
546                 }
547         };
548         $self->{long_res}->(); # kick off!
549         undef;
550 }
551
552 sub hdr_message_id ($$$) { # optimize XHDR Message-ID [range] for slrnpull.
553         my ($self, $xhdr, $range) = @_;
554
555         if (defined $range && $range =~ /\A<(.+)>\z/) { # Message-ID
556                 my ($ng, $n) = mid_lookup($self, $1);
557                 return r430 unless $n;
558                 hdr_mid_response($self, $xhdr, $ng, $n, $range, $range);
559         } else { # numeric range
560                 $range = $self->{article} unless defined $range;
561                 my $r = get_range($self, $range);
562                 return $r unless ref $r;
563                 my $mm = $self->{ng}->mm;
564                 my ($beg, $end) = @$r;
565                 more($self, $xhdr ? r221 : r225);
566                 $self->long_response($beg, $end, sub {
567                         my ($i) = @_;
568                         my $mid = $mm->mid_for($$i);
569                         more($self, "$$i <$mid>") if defined $mid;
570                 });
571         }
572 }
573
574 sub xref ($$) {
575         my ($ng, $n) = @_;
576         "$ng->{domain} $ng->{name}:$n"
577 }
578
579 sub mid_lookup ($$) {
580         my ($self, $mid) = @_;
581         my $self_ng = $self->{ng};
582         if ($self_ng) {
583                 my $n = $self_ng->mm->num_for($mid);
584                 return ($self_ng, $n) if defined $n;
585         }
586         foreach my $ng (values %{$self->{nntpd}->{groups}}) {
587                 next if defined $self_ng && $ng eq $self_ng;
588                 my $n = $ng->mm->num_for($mid);
589                 return ($ng, $n) if defined $n;
590         }
591         (undef, undef);
592 }
593
594 sub hdr_xref ($$$) { # optimize XHDR Xref [range] for rtin
595         my ($self, $xhdr, $range) = @_;
596
597         if (defined $range && $range =~ /\A<(.+)>\z/) { # Message-ID
598                 my ($ng, $n) = mid_lookup($self, $1);
599                 return r430 unless $n;
600                 hdr_mid_response($self, $xhdr, $ng, $n, $range, xref($ng, $n));
601         } else { # numeric range
602                 $range = $self->{article} unless defined $range;
603                 my $r = get_range($self, $range);
604                 return $r unless ref $r;
605                 my $ng = $self->{ng};
606                 my $mm = $ng->mm;
607                 my ($beg, $end) = @$r;
608                 more($self, $xhdr ? r221 : r225);
609                 $self->long_response($beg, $end, sub {
610                         my ($i) = @_;
611                         my $mid = $mm->mid_for($$i);
612                         more($self, "$$i ".xref($ng, $$i)) if defined $mid;
613                 });
614         }
615 }
616
617 sub header_obj_for {
618         my ($srch, $mid) = @_;
619         eval {
620                 my $smsg = $srch->lookup_message($mid);
621                 $smsg = PublicInbox::SearchMsg->load_doc($smsg->{doc});
622                 $smsg->mini_mime->header_obj;
623         };
624 };
625
626 sub hdr_searchmsg ($$$$) {
627         my ($self, $xhdr, $hdr, $range) = @_;
628         my $filter;
629         if ($hdr eq 'date') {
630                 $hdr = 'X-PI-TS';
631                 $filter = sub ($) {
632                         strftime('%a, %d %b %Y %T %z', gmtime($_[0]));
633                 };
634         }
635
636         if (defined $range && $range =~ /\A<(.+)>\z/) { # Message-ID
637                 my ($ng, $n) = mid_lookup($self, $1);
638                 return r430 unless $n;
639                 if (my $srch = $ng->search) {
640                         my $m = header_obj_for($srch, $range);
641                         my $v = $m->header($hdr);
642                         $v = $filter->($v) if defined $v && $filter;
643                         hdr_mid_response($self, $xhdr, $ng, $n, $range, $v);
644                 } else {
645                         hdr_slow($self, $xhdr, $hdr, $range);
646                 }
647         } else { # numeric range
648                 $range = $self->{article} unless defined $range;
649                 my $srch = $self->{ng}->search or
650                                 return hdr_slow($self, $xhdr, $hdr, $range);
651                 my $mm = $self->{ng}->mm;
652                 my $r = get_range($self, $range);
653                 return $r unless ref $r;
654                 my ($beg, $end) = @$r;
655                 more($self, $xhdr ? r221 : r225);
656                 $self->long_response($beg, $end, sub {
657                         my ($i) = @_;
658                         my $mid = $mm->mid_for($$i) or return;
659                         my $m = header_obj_for($srch, $mid) or return;
660                         my $v = $m->header($hdr);
661                         defined $v or return;
662                         $v = $filter->($v) if $filter;
663                         more($self, "$$i $v");
664                 });
665         }
666 }
667
668 sub do_hdr ($$$;$) {
669         my ($self, $xhdr, $header, $range) = @_;
670         my $sub = lc $header;
671         if ($sub eq 'message-id') {
672                 hdr_message_id($self, $xhdr, $range);
673         } elsif ($sub eq 'xref') {
674                 hdr_xref($self, $xhdr, $range);
675         } elsif ($sub =~ /\A(subject|references|date)\z/) {
676                 hdr_searchmsg($self, $xhdr, $sub, $range);
677         } else {
678                 hdr_slow($self, $xhdr, $header, $range);
679         }
680 }
681
682 # RFC 3977
683 sub cmd_hdr ($$;$) {
684         my ($self, $header, $range) = @_;
685         do_hdr($self, 0, $header, $range);
686 }
687
688 # RFC 2980
689 sub cmd_xhdr ($$;$) {
690         my ($self, $header, $range) = @_;
691         do_hdr($self, 1, $header, $range);
692 }
693
694 sub hdr_mid_prefix ($$$$$) {
695         my ($self, $xhdr, $ng, $n, $mid) = @_;
696         return $mid if $xhdr;
697
698         # HDR for RFC 3977 users
699         if (my $self_ng = $self->{ng}) {
700                 ($self_ng eq $ng) ? $n : '0';
701         } else {
702                 '0';
703         }
704 }
705
706 sub hdr_mid_response ($$$$$$) {
707         my ($self, $xhdr, $ng, $n, $mid, $v) = @_; # r: art_lookup result
708         my $res = '';
709         if ($xhdr) {
710                 $res .= r221 . "\r\n";
711                 $res .= "$mid $v\r\n" if defined $v;
712         } else {
713                 $res .= r225 . "\r\n";
714                 if (defined $v) {
715                         my $pfx = hdr_mid_prefix($self, $xhdr, $ng, $n, $mid);
716                         $res .= "$pfx $v\r\n";
717                 }
718         }
719         res($self, $res .= '.');
720         undef;
721 }
722
723 sub hdr_slow ($$$$) {
724         my ($self, $xhdr, $header, $range) = @_;
725
726         if (defined $range && $range =~ /\A<.+>\z/) { # Message-ID
727                 my $r = $self->art_lookup($range, 2);
728                 return $r unless ref $r;
729                 my ($n, $ng) = ($r->[0], $r->[5]);
730                 my $v = hdr_val($r, $header);
731                 hdr_mid_response($self, $xhdr, $ng, $n, $range, $v);
732         } else { # numeric range
733                 $range = $self->{article} unless defined $range;
734                 my $r = get_range($self, $range);
735                 return $r unless ref $r;
736                 my ($beg, $end) = @$r;
737                 more($self, $xhdr ? r221 : r225);
738                 $self->long_response($beg, $end, sub {
739                         my ($i) = @_;
740                         $r = $self->art_lookup($$i, 2);
741                         return unless ref $r;
742                         defined($r = hdr_val($r, $header)) or return;
743                         more($self, "$$i $r");
744                 });
745         }
746 }
747
748 sub cmd_xrover ($;$) {
749         my ($self, $range) = @_;
750         my $ng = $self->{ng} or return '412 no newsgroup selected';
751         (defined $range && $range =~ /[<>]/) and
752                 return '420 No article(s) selected'; # no message IDs
753
754         $range = $self->{article} unless defined $range;
755         my $r = get_range($self, $range);
756         return $r unless ref $r;
757         my ($beg, $end) = @$r;
758         my $mm = $ng->mm;
759         my $srch = $ng->search;
760         more($self, '224 Overview information follows');
761         $self->long_response($beg, $end, sub {
762                 my ($i) = @_;
763                 my $mid = $mm->mid_for($$i) or return;
764                 my $m = header_obj_for($srch, $mid) or return;
765                 my $h = $m->header('references');
766                 more($self, "$$i $h") if defined $h;
767         });
768 }
769
770 sub over_line ($$) {
771         my ($self, $r) = @_;
772
773         more($self, join("\t", $r->[0], map {
774                                 my $h = hdr_val($r, $_);
775                                 defined $h ? $h : '';
776                         } @OVERVIEW ));
777 }
778
779 sub cmd_over ($;$) {
780         my ($self, $range) = @_;
781         if ($range && $range =~ /\A<.+>\z/) {
782                 my $r = $self->art_lookup($range, 2);
783                 return '430 No article with that message-id' unless ref $r;
784                 more($self, '224 Overview information follows (multi-line)');
785
786                 # Only set article number column if it's the current group
787                 my $ng = $self->{ng};
788                 $r->[0] = 0 if (!$ng || $ng ne $r->[5]);
789                 over_line($self, $r);
790                 '.';
791         } else {
792                 cmd_xover($self, $range);
793         }
794 }
795
796 sub cmd_xover ($;$) {
797         my ($self, $range) = @_;
798         $range = $self->{article} unless defined $range;
799         my $r = get_range($self, $range);
800         return $r unless ref $r;
801         my ($beg, $end) = @$r;
802         more($self, "224 Overview information follows for $beg to $end");
803         $self->long_response($beg, $end, sub {
804                 my ($i) = @_;
805                 my $r = $self->art_lookup($$i, 2);
806                 return unless ref $r;
807                 over_line($self, $r);
808         });
809 }
810
811 sub cmd_xpath ($$) {
812         my ($self, $mid) = @_;
813         return r501 unless $mid =~ /\A<(.+)>\z/;
814         $mid = $1;
815         my @paths;
816         foreach my $ng (values %{$self->{nntpd}->{groups}}) {
817                 my $n = $ng->mm->num_for($mid);
818                 push @paths, "$ng->{name}/$n" if defined $n;
819         }
820         return '430 no such article on server' unless @paths;
821         '223 '.join(' ', @paths);
822 }
823
824 sub res ($$) {
825         my ($self, $line) = @_;
826         do_write($self, $line . "\r\n");
827 }
828
829 sub more ($$) {
830         my ($self, $line) = @_;
831         do_more($self, $line . "\r\n");
832 }
833
834 sub do_write ($$) {
835         my ($self, $data) = @_;
836         my $done = $self->write($data);
837         die if $self->{closed};
838
839         # Do not watch for readability if we have data in the queue,
840         # instead re-enable watching for readability when we can
841         $self->watch_read(0) if (!$done || $self->{long_res});
842
843         $done;
844 }
845
846 sub err ($$;@) {
847         my ($self, $fmt, @args) = @_;
848         printf { $self->{nntpd}->{err} } $fmt."\n", @args;
849 }
850
851 sub out ($$;@) {
852         my ($self, $fmt, @args) = @_;
853         printf { $self->{nntpd}->{out} } $fmt."\n", @args;
854 }
855
856 use constant MSG_MORE => ($^O eq 'linux') ? 0x8000 : 0;
857
858 sub do_more ($$) {
859         my ($self, $data) = @_;
860         if (MSG_MORE && !$self->{write_buf_size}) {
861                 my $n = send($self->{sock}, $data, MSG_MORE);
862                 if (defined $n) {
863                         my $dlen = length($data);
864                         return 1 if $n == $dlen; # all done!
865                         $data = substr($data, $n, $dlen - $n);
866                 }
867         }
868         $self->do_write($data);
869 }
870
871 # callbacks for by Danga::Socket
872
873 sub event_hup { $_[0]->close }
874 sub event_err { $_[0]->close }
875
876 sub event_write {
877         my ($self) = @_;
878         # only continue watching for readability when we are done writing:
879         if ($self->write(undef) == 1 && !$self->{long_res}) {
880                 $self->watch_read(1);
881         }
882 }
883
884 sub event_read {
885         my ($self) = @_;
886         use constant LINE_MAX => 512; # RFC 977 section 2.3
887         my $r = 1;
888
889         my $buf = $self->read(LINE_MAX) or return $self->close;
890         $self->{rbuf} .= $$buf;
891         while ($r > 0 && $self->{rbuf} =~ s/\A\s*([^\r\n]+)\r?\n//) {
892                 my $line = $1;
893                 my $t0 = now();
894                 my $fd = $self->{fd};
895                 $r = eval { $self->process_line($line) };
896                 my $d = $self->{long_res} ?
897                         " deferred[$fd]" : '';
898                 out($self, "[$fd] $line - %0.6f$d", now() - $t0);
899         }
900
901         return $self->close if $r < 0;
902         my $len = length($self->{rbuf});
903         return $self->close if ($len >= LINE_MAX);
904 }
905
906 sub watch_read {
907         my ($self, $bool) = @_;
908         my $rv = $self->SUPER::watch_read($bool);
909         if ($bool && $self->{rbuf} ne '') {
910                 # Force another read if there is a pipelined request.
911                 # We don't know if the socket has anything for us to read,
912                 # and we must double-check again by the time the timer fires
913                 # in case we really did dispatch a read event and started
914                 # another long response.
915                 Danga::Socket->AddTimer(0, sub {
916                         if (&Danga::Socket::POLLIN & $self->{event_watch}) {
917                                 $self->event_read;
918                         }
919                 });
920         }
921         $rv;
922 }
923
924 sub busy () {
925         my ($self) = @_;
926         ($self->{rbuf} ne '' || $self->{long_res} || $self->{write_buf_size});
927 }
928
929 1;