9 package Net::Server::NNTP;
11 use base qw(Net::Server::Fork);
15 Net::Server::NNTP - The great new Net::Server::NNTP!
23 our $VERSION = '0.01';
27 Quick summary of what the module does.
29 This module implements NNTP. It is intended to be compliant with RFCs
32 By default, this module doesn't actually store any news articles. It
33 is intended that it will be sub-classed by a module that will implement
34 all of the hook methods to store and retrieve articles.
36 The networking services are handled by Net::Server, which this
37 module sub-classes using the Net::Server::MultiType module.
38 Therefore, the network configuration can be set with the options
39 as listed in Net::Server.
41 Net::Server data is in $server->{server}, as documented in the documentation
42 for Net::Server. This module puts all of its configuration data
45 By default, the server will read F</etc/newsd.conf> at start-up for
46 configuration options.
48 The default Net::Server personality used is Fork, but that can be changed by
49 setting the server_type configuration parameter (q.v. Net::Server::MultiType).
51 NNTP specific parameters are:
53 first_timeout specificies the timeout in seconds to receive an initial
54 command from the server.
56 timeout specifies the timeout in seconds for subsequent commands
58 Perhaps a little code snippet.
60 use Net::Server::NNTP;
62 my $foo = Net::Server::NNTP->new();
67 our $article_re = qr/\<[^\s\>]+\@[^\s\>]+\>/; # rfc 1036 2.1.5
68 our $crlf = "\015\012"; # avoid local interpretation of \n
72 =head1 STATE FUNCTIONS
74 =head1 STORAGE FUNCTIONS
76 =head1 INTERNAL FUNCTIONS
78 =head1 Session Administration Commands
80 These methods implement commands from section 5 of RFC 3977 and
81 corresponding commands from other RFCs.
87 Handled internally by a coderef. Returns the contents of %capabilities.
91 Handles 'mode reader' (RFC 3977 5.3)
98 return $s->response(501) unless @_ > 1;
100 if ($s->syntax($arg, '(?i)reader')) { # RFC 4644-2.3
101 return $s->response(200,undef,$s->pathhost);
104 if ($s->syntax($arg, '(?i)stream')) { # RFC 4644-2.3
105 return $s->response(203);
118 return $s->response(501, 'too many arguments') if @_ > 1;
125 $s->server_quit($code, response);
130 my ($s, $code, @args) = @_;
131 $s->response($code, @args);
132 die 'server quitting';
135 =head1 Article Posting and Retrieval
139 $s->group('news.software.nntp');
141 Implements RFC 3977 6.1.1
149 return $s->response(501) unless @_ == 2;
151 if (my ($estimate, $low, $high, $group) = @row = $s->groupinfo($g)) {
152 $s->pointer(@row[3,1]);
153 $s->article_number(undef) unless $estimate;
154 $s->response(211,undef,@row);
160 =head2 parse_grouprange
162 takes a range spec and gets a low and high, as against a given group
164 returns an empty list if the range spec doesn't parse. The highwater
165 returned will be undef if the given group is invalid.
169 sub parse_grouprange {
170 my ($s, $range, $group, $lowwater, $highwater) = @_;
172 (undef, $lowwater, $highwater) = $s->groupinfo($group);
174 return ($group, $lowwater, $highwater) if @_ == 1;
176 my ($low, $r, $high) = $range =~ /(\d+)(-)?(\d+)?/;
178 return ($group, $low, $high);
179 } elsif (defined $r) {
180 return ($group, $low, $highwater);
181 } elsif (defined $low) {
182 return ($group, $low, $low);
194 my ($g, $range, @extraargs) = split(/\s+/, $arg);
195 return $s->response(501) if @extraargs;
197 $range = '1-' unless defined $range;
198 $g = $s->selected_group unless defined $g;
199 return $s->response(412) unless defined $g;
201 my @grouprange = $s->parse_grouprange($range, $g);
202 return $s->response(501) unless @grouprange;
204 my @gi = $s->changegroup($g) if @grouprange;
205 return $s->response(411) unless @gi;
207 my @articles = $s->fetch_grouplist(@grouprange);
209 $s->response(211, undef, @gi);
210 $s->sendresults(@articles,'.');
220 return $s->response(501) if @_ > 1;
222 return $s->response(412) unless $s->selected_group;
223 return $s->response(420) unless $s->article_number;
225 my ($n,$id) = $s->prev_article();
228 $s->article_number($n);
229 $s->response(223, undef, $n, $id);
237 Implements NNTP next (RFC 3977 6.1.4). Moves the article pointer to the next
242 If the currently selected newsgroup is valid, the current article number MUST
243 be set to the next article in that newsgroup (that is, the lowest existing
244 article number greater than the current article number). If successful, a
245 response indicating the new current article number and the message-id of that
246 article MUST be returned. No article text is sent in response to this command.
248 If the current article number is already the last article of the newsgroup, a
249 421 response MUST be returned. In all other aspects (apart, of course, from
250 the lack of 422 response), this command is identical to the LAST command
260 return $s->response(501,'too many arguments') if @_ > 1;
262 return $s->response(412) unless $s->selected_group;
263 return $s->response(420) unless $s->article_number;
265 my ($n,$id) = $s->next_article();
268 $s->article_number($n);
269 $s->response(223, undef, $n, $id);
283 my ($a, $g, $n, $id);
285 if (($id) = $s->syntax("@args", "($article_re)")) {
286 ($a) = $s->fetch_article($id);
287 return $s->response(430) unless defined $a;
289 } elsif (($n) = $s->syntax("@args", "(\\d+)")) {
290 $g = $s->selected_group;
291 return $s->response(412) unless defined $g;
292 $s->log(4, "fetching $g ($n)");
293 ($a, $id) = $s->fetch_article($g,$n);
294 return $s->response(423) unless defined $a;
295 $s->article_number($n);
297 ($g, $n) = $s->pointer;
298 return $s->response(412) unless defined $g;
299 return $s->response(420) unless defined $n;
300 $s->log(4, "fetching ($g $n)");
301 ($a,$id) = $s->fetch_article($g,$n);
302 return $s->response(420) unless defined $a;
304 return $s->response(501);
307 $s->response(220,undef,$n,$id);
318 my ($a, $g, $n, $id);
320 if (($id) = ("@args" =~ "($article_re)")) {
321 ($a) = $s->fetch_head($id);
322 return $s->response(430) unless defined $a;
324 } elsif (($n) = $s->matches("@args", "(\\d+)")) {
325 $g = $s->selected_group;
326 return $s->response(412) unless defined $g;
327 $s->log(4, "fetching $g ($n)");
328 ($a, $id) = $s->fetch_head($g,$n);
329 return $s->response(423) unless defined $a;
330 $s->article_number($n);
332 ($g, $n) = $s->pointer;
333 $s->log(4, "fetching ($g $n)");
334 return $s->response(412) unless defined $g;
335 return $s->response(420) unless defined $n;
336 ($a, $id) = $s->fetch_head($g,$n);
337 return $s->response(420) unless defined $a;
339 return $s->response(501);
342 $s->response(221,undef,$n,$id);
353 my ($a, $g, $n, $id);
355 if (($id) = $s->syntax("@args", "($article_re)")) {
356 ($a) = $s->fetch_body($id);
357 return $s->response(430) unless defined $a;
359 } elsif (($n) = $s->syntax("@args", "(\\d+)")) {
360 $g = $s->selected_group;
361 return $s->response(412) unless defined $g;
362 $s->log(4, "fetching $g ($n)");
363 ($a, $id) = $s->fetch_body($g,$n);
364 return $s->response(423) unless defined $a;
365 $s->article_number($n);
367 ($g, $n) = $s->pointer;
368 $s->log(4, "fetching ($g $n)");
369 return $s->response(412) unless defined $g;
370 return $s->response(420) unless defined $n;
371 ($a,$id) = $s->fetch_body($g,$n);
372 return $s->response(420) unless defined $a;
374 return $s->response(501);
377 $s->response(222,undef,$n,$id);
388 my ($a, $g, $n, $id);
390 if (($id) = $s->syntax("@args", "($article_re)")) {
391 $id = $s->fetch_stat($id);
392 return $s->response(430) unless defined $id;
394 } elsif (($n) = $s->syntax("@args", "(\\d+)")) {
395 $g = $s->selected_group;
396 return $s->response(412) unless defined $g;
397 $s->log(4, "fetching $g ($n)");
398 $id = $s->fetch_stat($g,$n);
399 return $s->response(423) unless defined $id;
400 $s->article_number($n);
402 ($g, $n) = $s->pointer;
403 return $s->response(412) unless defined $g;
404 return $s->response(420) unless defined $n;
405 $s->log(4, "fetching ($g $n)");
406 $id = $s->fetch_stat($g,$n);
407 return $s->response(420) unless defined $id;
409 return $s->response(501);
412 $s->response(223,undef,$n,$id);
427 return $s->response(440) unless $s->permit_posting;
429 my $rid = sprintf('<%s@%s>', Data::UUID->new()->create_str(),$s->pathhost);
431 $s->response(340, 'Ok, recommended ID %s', $rid);
433 my $a = $s->receive();
434 return $s->response(441) unless $a;
437 if (!defined($a->header('From'))
438 or !defined($a->header('Newsgroups'))
439 or !defined($a->header('Subject'))
440 or defined($a->header('Injection-Info'))
441 or defined($a->header('Xref'))
442 or $a->header('Path') =~ /POSTED/
443 or !$s->valid_syntax($a)
445 # must reject 5537-3.5-2
446 return $s->response(441);
449 # TODO 5537-3.5-2 SHOULD reject any proto-article that contains a
450 # header field deprecated for Netnews
451 # TODO deprecated fields:
453 # TODO policy reject NNTP-Posting-Host
456 $a->ensure_header('Date', $s->system_ts());
457 $a->ensure_header('Message-ID', $rid);
459 #$a->ensure_header('Lines',$a->bodylines);
461 # 5537-3.5-8 5537-3.5-9
462 # store method will prepend the pathhost
463 $a->ensure_header('Path','not-for-mail');
466 $a->header('Injection-Info', sprintf(q{posting-host = "%s"},
467 $s->{nntp}{peername}));
472 $posted = $s->store($a);
475 return $s->response(441);
478 return $s->response(240,
479 'article received ok, Message-ID %s', $a->messageid);
481 return $s->response(441);
485 =head2 ihave (RFC 3977 6.3.2)
493 return $s->response(501) unless $id =~ /($article_re)/;
494 return $s->response(430) if $s->fetch_stat($id);
495 return $s->response(436) unless $s->permit_posting;
499 my $a = $s->receive();
501 return $s->response(436) unless $a;
507 return $s->response(436);
509 $s->response($ok ? 235 : 437);
517 my ($s, $g, $n) = (@_,undef,undef);
520 $s->{nntp}{newsgroup} = $g;
521 $s->{nntp}{number} = $n;
523 return wantarray ? ($s->{nntp}{newsgroup},$s->{nntp}{number}) : $s->{nntp}{newsgroup};
527 my ($s, $g) = (@_, undef);
529 $s->{nntp}{newsgroup} = $g;
531 return $s->{nntp}{newsgroup};
535 my ($s, $n) = (@_, undef);
537 $s->{nntp}{number} = $n;
539 return $s->{nntp}{number};
542 our %capabilities = (
549 'LIST ACTIVE NEWSGROUPS OVERVIEW.FMT ACTIVE.TIMES HEADERS' => '',
555 Subject => 'subject',
556 'Message-ID' => 'msgid',
559 References => 'references',
561 Newsgroups => 'newsgroups',
563 ':lines' => 'actuallines',
564 'Xref' => 'local article numbers',
566 our @over = qw(Subject From Date Message-ID References :bytes :lines Xref);
575 return sprintf('%04d%02d%02d%02d%02d%02d', Date::Calc::System_Clock());
578 our $keyword_re = '^[a-zA-Z][a-zA-Z0-9\.\-]{2}';
581 article => \&article, # reader 6.2.1
582 authinfo => \&unsupported, # rfc 4643
583 starttls => \&unsupported, # rfc 4642, IO::Socket::SSL->start
584 body => \&body, # reader 6.2.3
585 check => \&check, # rfc 4644 2.4
586 takethis => \&takethis, # rfc 4644 2.5
587 capabilities => sub { # mandatory 5.2
589 if (@_ > 1 && $arg !~ /^$keyword_re$/) {
594 $s->sendresults('VERSION 2', keys %capabilities,'.');
596 date => sub { # reader 7.1
598 $s->response(111,undef, $s->servertime);
600 group => \&group, # reader 6.1.1
601 hdr => \&hdr, # hdr 8.5
602 xhdr => \&unimplemented,
603 head => \&head, # mandatory 6.2.2
604 help => sub { # mandatory 7.2
607 $s->sendresults('The following commands are implemented',
608 sort grep { $cmd{$_} != \&unimplemented
609 && $cmd{$_} != \&unsupported}
612 ihave => \&ihave, # ihave 6.3.2
613 'last' => \&last, # reader 6.1.3
614 list => \&list, # list 7.6.[13456], over 8.4
615 listgroup => \&listgroup, # reader 6.1.2
616 mode => \&mode, # mode-reader 5.3, 4644-2.3 mode stream
617 newgroups => \&newgroups, # reader 7.3
618 newnews => \&newnews, # newnews 7.4
619 'next' => \&next, # reader 6.1.4
620 over => \&over, # over 8.3
621 xover => \&over, # we hope this is the same as over (it is, but the overview.fmt listing is different)
622 post => \&post, # post 6.3.1
624 'stat' => \&stat, # mandatory 6.2.4
625 # slave is removed from the protocol
626 # slave => sub {my ($s) = @_; $peer_is_slave = 1; $s->response(202)},
627 'xadmin' => \&xadmin,
635 my ($s, $group) = @_;
637 return () unless $group;
639 my @row = $s->groupinfo($group);
642 $s->pointer($group,$row[1]);
652 delegated to IO::Socket->print()
658 $s->{server}{client}->print(@args);
663 $s->sendresults(@lines);
665 Sends each element of @lines followed by a crlf pair.
666 If an element of @lines is a reference, it is assumed to be
667 an arrayref and the elements thereof are joined with a space
668 and the resulting string is output.
673 my ($s, @lines) = @_;
674 $s->print(ref $_ ? join(' ', @$_) : $_, $crlf) for @lines;
686 my ($wildmat, $date, $time);
688 return $s->response(501) unless ($wildmat, $date, $time) =
689 $s->syntax("@args", '(\S+)\s+(\\d{6}|\\d{8})\s+(\\d{6})(\s+GMT)?');
691 my $ts = $s->parsetime($date,$time);
692 return $s->response(501) unless defined $ts;
694 my $regex = $s->wildmat_to_regex($wildmat);
695 return $s->response(501) unless defined $regex;
697 $s->log(2, "newnews wildmat = $regex");
699 my @article_ids = $s->fetch_newnews($ts, $regex);
702 $s->sendresults(@article_ids,'.');
713 ($subcmd, @args) = split(/\s+/, $arg);
715 $subcmd = 'active' unless defined($subcmd); # 7.6.1.1
716 $subcmd = lc($subcmd);
720 if ($subcmd eq 'active') { # 7.6.3
722 @results = $s->fetch_active(@args);
724 return $s->response(501);
727 elsif ($subcmd eq 'active.times') { # 7.6.4
729 @results = $s->fetch_activetimes(@args);
730 return $s->response(503) unless ref $results[0];
732 return $s->response(501);
735 # don't forget to update capabilities when this is implemented
736 elsif ($subcmd eq 'distrib.pats') { # 7.6.5
737 return $s->response(501) if @args;
738 return $s->response(503);
741 elsif ($subcmd eq 'headers') { # 8.6
742 # TODO ask the storage what it can do
743 return $s->response(501) if @args;
744 @results = keys %hdrs;
746 elsif ($subcmd eq 'newsgroups') { # 7.6.6
748 @results = $s->fetch_activetimes(@args);
749 return $s->response(503) unless ref $results[0];
751 return $s->response(501);
754 elsif ($subcmd eq 'overview.fmt') { # 8.4
755 return $s->response(501) if @args;
756 # TODO use old xover format if it seems warranted
757 @results = $s->fetch_overviewfmt();
758 $s->response(215,'Order of fields in overview database.');
759 $s->sendresults(@results,'.');
766 $s->sendresults(@results,'.');
769 # command prep and check
771 # args => 'max args' or [min,max]
772 # check => [regexes to validate args against, if defined]
773 # fail => what to do if it fails
774 # func => command to pass args on to
776 # sub command_check {
777 # my ($syntax, @args) = @_;
780 # see rfc 3977 7.3.2 for description of format
786 my ($s,$date,$time) = @_;
789 if ($date =~ /^(\d\d)(\d\d)(\d\d)$/) {
790 my $curyear = (localtime)[5]+1900;
791 my $curcent = int ($curyear/100);
792 my $yic = $curyear % 100;
793 my $cent = $1 <= $yic ? $curcent : $curcent - 1;
794 $ts = sprintf('%02d%02d-%02d-%02d', $cent,$1,$2,$3);
795 } elsif ($date =~ /^(\d\d\d\d)(\d\d)(\d\d)$/) {
796 $ts = sprintf('%04d-%02d-%02d', $1,$2,$3);
800 if ($time =~ /^(\d\d)(\d\d)(\d\d)$/) {
801 $ts = sprintf('%s %02d:%02d:%02d',$ts,$1,$2,$3);
816 $wildmat =~ s/\./\\\./g;
817 $wildmat =~ s/\?/\./g;
818 $wildmat =~ s/\*/\.\*/g;
822 =head2 wildmat_to_regex
826 sub wildmat_to_regex {
827 my ($s, $wildmat) = @_;
829 my @pats = split(/,/,$wildmat); # TODO look for escaped commas
832 # TODO special case '*' since it always matches
834 while ($pats[0] =~ /^!/) { shift @pats } # init neg can't match
839 my $like = wildmat_to_re($_);
844 $sql = "(^(?!$like)($sql)\$)";
848 $sql = "^($sql)\$" unless $negated;
857 my ($s, $args, @regex) = @_;
862 next unless defined $re;
863 if ($args[$_] !~ /$re/) {
864 $s->log(2, "Argument invalid: $args[$_] !~ /$re/");
873 Checks a string against a regex and returns the matches.
874 Logs if the syntax fails.
879 my ($s, $cmd, $re) = @_;
882 if (@match = ($cmd =~ /^$re$/)) {
886 $s->log(3, "syntax fail: '$cmd' !~ /$re/");
892 Checks a string against a regex and returns the matches.
897 my ($s, $cmd, $re) = @_;
900 if (@match = ($cmd =~ /^$re$/)) {
915 return $s->response(501) unless ($date, $time) =
916 $s->syntax("@args", '(\\d{6}|\\d{8}) (\\d{6})( GMT)?');
918 my $ts = $s->parsetime($date,$time);
919 return $s->response(501) unless defined $ts;
921 my @results = $s->fetch_newgroups($ts);
924 $s->sendresults(@results,'.');
927 # TODO access control?
929 =head2 permit_posting
942 Calls $s->fetch_overview
947 my ($s, $arg, @extra) = @_;
949 my ($id, $lo, $range, $hi);
951 return $s->response(501) if @extra;
954 # 3977-8.5.1 third form
955 return $s->response(412) unless defined $s->selected_group;
956 return $s->response(420) unless defined $s->article_number;
957 @headers = $s->fetch_overview($s->pointer);
958 return $s->response(420) unless @headers;
959 return $s->response(503) if $headers[0] == undef;
960 } elsif (($id) = $s->syntax($arg, "($article_re)")) {
961 # 3977-8.5.1 first form
962 @headers = $s->fetch_overview($id);
963 return $s->response(430) unless @headers;
964 return $s->response(503) if $headers[0] == undef;
965 } elsif (($lo, $range, $hi) = $s->syntax($arg, '(\d+)(-)?(\d+)?')) {
966 # 3977-8.5.1 second form
967 return $s->response(423) if $hi < $lo;
968 return $s->response(412) unless defined $s->selected_group;
969 my @gi = $s->groupinfo();
970 return $s->response(412) unless @gi;
972 @headers = $s->fetch_overview($gi[0], $lo, $hi);
973 } elsif (defined $range) {
974 @headers = $s->fetch_overview($gi[0], $lo, $gi[2]);
976 @headers = $s->fetch_overview($gi[0], $lo);
978 return $s->response(423) unless @headers;
979 return $s->response(503) if $headers[0] == undef;
981 return $s->response(501);
985 $s->sendresults(@headers, '.');
989 # TODO allow any header?
993 Implements 3977-8.5.1
995 Calls $s->fetch_headers.
1000 my ($s, $args) = @_;
1002 my ($field, $arg) = split(/\s+/, $args);
1003 my ($id, $hi, $lo, $range);
1007 # 3977-8.5.1 third form
1008 return $s->response(412) unless defined $s->selected_group;
1009 return $s->response(420) unless defined $s->article_number;
1010 @headers = $s->fetch_headers($field, $s->pointer);
1011 return $s->response(420) unless @headers;
1012 return $s->response(503) if $headers[0] == undef;
1013 } elsif (($id) = $s->syntax($arg, "($article_re)")) {
1014 # 3977-8.5.1 first form
1015 @headers = $s->fetch_headers($field, $id);
1016 return $s->response(430) unless @headers;
1017 return $s->response(503) if $headers[0] == undef;
1018 } elsif (($lo, $range, $hi) = $s->syntax($arg, '(\d+)(-)?(\d+)?')) {
1019 # 3977-8.5.1 second form
1020 return $s->response(423) if $hi < $lo;
1021 return $s->response(412) unless defined $s->selected_group;
1022 my @gi = $s->groupinfo();
1023 return $s->response(412) unless @gi;
1025 @headers = $s->fetch_headers($field, $gi[0], $lo, $hi);
1026 } elsif (defined $range) {
1027 @headers = $s->fetch_headers($field, $gi[0], $lo, $gi[2]);
1029 @headers = $s->fetch_headers($field, $gi[0], $lo);
1031 return $s->response(423) unless @headers;
1032 return $s->response(503) if $headers[0] == undef;
1034 return $s->response(501);
1037 foreach (@headers) {
1038 $_->[1] =~ s/\r?\n//g;
1039 $_->[1] =~ s/\t/ /g;
1043 $s->sendresults(@headers, '.');
1047 100 => 'help text follows',
1048 101 => 'Capability list follows',
1049 111 => '%s server date and time',
1051 200 => 'server %s ready, posting allowed',
1052 201 => 'server %s ready, posting prohibited',
1053 202 => 'slave status noted',
1054 203 => 'Streaming permitted',
1055 205 => 'closing connection',
1056 211 => '%d %d %d %s group selected',
1057 215 => 'list of newsgroups follows',
1058 220 => '%d %s article follows',
1059 221 => '%d %s article headers follows',
1060 222 => '%d %s article body follows',
1061 223 => '%d %s article exists and selected',
1062 224 => 'overview information follows',
1063 225 => 'headers follow',
1064 230 => 'list of new articles follows',
1065 231 => 'list of new newsgroups follows',
1066 235 => 'article transferred ok',
1067 238 => '%s Send article to be transferred',
1068 239 => '%s Article transferred OK',
1069 240 => 'article received ok',
1071 335 => 'send article to be transferred. End with <CR-LF>.<CR-LF>',
1072 340 => 'send article to be posted. End with <CR-LF>.<CR-LF>',
1074 400 => 'service not available or no longer available',
1075 401 => '%s server is in wrong mode; use indicated capability',
1076 403 => 'internal fault preventing action being taken',
1077 411 => 'no such newsgroup',
1078 412 => 'no newsgroup selected',
1079 420 => 'no current article has been selected',
1080 421 => 'no next article in this group',
1081 422 => 'no previous article in this group',
1082 423 => 'no such article number in this group',
1083 430 => 'no such article found',
1084 431 => '%s Transfer not possible; try again later',
1085 435 => 'article not wanted - do not send it',
1086 436 => 'transfer failed - try again later',
1087 437 => 'article rejected - do not try again',
1088 438 => '%s Article not wanted',
1089 439 => '%s Transfer rejected; do not retry',
1090 440 => 'posting not allowed',
1091 441 => 'posting failed',
1093 500 => 'command not recognized',
1094 501 => 'command syntax error',
1095 502 => 'access restriction or permission denied',
1096 503 => 'program fault - command not performed',
1099 =head2 connect_to_storage
1103 sub connect_to_storage {
1106 return $s->{db} if defined $s->{db};
1108 # TODO use a config parameter optionally here
1109 my $dsn = $ENV{'DBI_DSN'};
1110 $dsn = 'dbi:Pg:dbname=news' unless defined $dsn;
1111 $s->log(4, "connecting to $dsn");
1113 $s->{db} = DBI->connect($dsn,'','',{AutoCommit => 0,RaiseError=>1});
1114 $s->{db}->{PrintError} = 0;
1116 $s->{db}->do("set CLIENT_ENCODING to 'SQL_ASCII'");
1121 =head2 Storage Access Functions
1127 Return the next article number in a group, undef if none.
1128 Should return the number in a scalar context, number, articleid in
1140 my ($rec, $ref, $rej, $postp) =
1142 $s->{nntp}{response}{239}
1143 + $s->{nntp}{response}{235}
1144 + $s->{nntp}{response}{240}, # received
1146 $s->{nntp}{response}{435}, # refused
1148 $s->{nntp}{response}{439}
1149 + $s->{nntp}{response}{437}, # rejected
1151 $s->{nntp}{response}{436}, # postponed
1154 $s->log(2, sprintf('%s received %d, refused %d, rejected %d, postponed %d', $s->client, $rec, $ref, $rej, $postp));
1156 $s->{nntp}{response}{$_} = 0 for keys %response;
1165 return $s->{server}{client};
1174 $s->{server}->{peeraddr} . ':' .$s->{server}->{peerport};
1177 =head2 process_request
1184 # we don't have the peeraddr set yet.
1185 #$s->log(2, 'forking for connection from %s', $s->{server}{client});
1190 sub post_accept_hook {
1193 # net server seems to log connections
1194 #$s->log(2, 'accepted connection from %s', $s->{server}{peeraddr});
1199 sub request_denied_hook {
1202 $s->log(2, 'denied connection from %s', $s->{server}{peeraddr});
1205 sub process_request {
1207 #$s->log(2,'%s: connected, log_level %d, server_type (%s)',$s->client, $s->{server}{log_level}, join(', ', @{$s->{server}{server_type}}));
1209 $s->connect_to_storage();
1211 $s->{nntp}{connecttime} = time;
1212 $s->{nntp}{response}{$_} = 0 for keys %response;
1214 my $peername = undef;
1215 # five seconds max to do reverse lookup, otherwise skip it
1216 # TODO i think Net::Server will do the reverse
1218 local $SIG{ALRM} = sub { die "Timed Out!\n" };
1220 ($peername) = gethostbyaddr(Socket::inet_aton($s->{server}->{peeraddr}), Socket::AF_INET());
1223 $s->log(1, "Can't get peername for ".$s->{server}->{peeraddr} . ": $@");
1224 $peername = $s->{server}->{peeraddr};
1226 $s->{nntp}{peername} = $peername;
1228 # parent will kill us with a term
1229 $SIG{TERM} = sub { $s->log_stats();exit 0 };
1232 local $SIG{ALRM} = sub { die "Timed Out!\n" };
1233 alarm($s->{nntp}{first_timeout});
1234 $s->response($s->permit_posting()?200:201,undef,$s->pathhost);
1235 # TODO use a variable so subclassers can use not STDIN
1239 $s->log(3, '%s -> %s', $s->client, $_);
1240 my ($cmd, @args) = split(/\s+/, $_, 2);
1241 # TODO enforce maximum length?
1243 if (exists($cmd{$cmd})) {
1244 $s->{command} = $cmd;
1245 $cmd{$cmd}->($s, @args);
1247 $s->log(4, "command not recognized '%s'", $cmd);
1250 alarm($s->{nntp}{timeout});
1254 if ($@=~/timed out/i) {
1255 $s->log(2, '%s: Timed Out.', $s->client);
1256 } elsif ($@ =~ /client quit/) {
1257 $s->log(2, '%s: client quit', $s->client);
1258 } elsif (defined($@) && length($@)) {
1261 $s->log(2, '%s: disconnecting', $s->client);
1265 =head2 default_values
1269 sub default_values {
1270 ### add a single value option
1271 my $hn = Sys::Hostname::hostname();
1272 my @v = split(/\./, $hn);
1275 $hn = join('.', @v);
1279 log_level => 2, # this is default I think
1282 server_type => [qw(Fork)],
1285 log_file => 'Sys::Syslog',
1286 pid_file => '/var/run/news/newsd.pid',
1287 syslog_facility => 'news',
1288 syslog_ident => 'newsd',
1289 syslog_logopt => 'pid',
1290 conf_file => -r '/etc/newsd.conf' ? '/etc/newsd.conf' : undef,
1291 first_timeout => 30, # seconds to receive first command
1292 timeout => 900, # subsequent commands 15 min
1297 # server text, pathhost
1298 # groupsync 604800 == weekly, use undef for no sync, or no active file
1302 # localgroups text default 'local.*',
1303 # groups text default '*'
1305 # insert into configuration values ('localgroups','local.*');
1314 $s->log(1, 'options called');
1315 $s->{'nntp'} ||= {};
1317 my $opt = $s->{'nntp'};
1319 ### setup options in the parent classes
1320 $s->SUPER::options($oh);
1324 $oh->{'pathhost'} ||= \ $opt->{'pathhost'};
1326 $opt->{'activesync'} ||= 604800;
1327 $oh->{'activesync'} ||= \ $opt->{'activesync'};
1329 $opt->{'activefile'} ||= 'ftp://ftp.isc.org/pub/usenet/CONFIG/active.gz';
1330 $oh->{'activefile'} ||= \ $opt->{'activefile'};
1332 $opt->{'newsgroups'} ||= 'ftp://ftp.isc.org/pub/usenet/CONFIG/newsgroups.gz';
1333 $oh->{'newsgroups'} ||= \ $opt->{'newsgroups'};
1335 $opt->{'first_timeout'} ||= 120;
1336 $oh->{'first_timeout'} ||= \$ opt->{'first_timeout'};
1338 $opt->{'timeout'} ||= 900;
1339 $oh->{'timeout'} ||= \ $opt->{'timeout'};
1341 #$template->{'my_option'} = \ $prop->{'my_option'};
1343 ### add a multi value option
1344 #$prop->{'an_arrayref_item'} ||= [];
1345 #$template->{'an_arrayref_item'} = $prop->{'an_arrayref_item'};
1350 $s->log($fmt, @args);
1352 Overrides the Net::Server log method and always treats the first
1353 argument as a format string. We have to do this because Net::Server
1354 treats the arguments differently depending on whether syslog is used.
1355 Uses Perl's sprintf to do the formatting.
1360 my ($s, $lvl, $fmt, @args) = @_;
1364 $msg = sprintf($fmt, @args);
1369 $s->SUPER::log($lvl, $msg);
1377 my ($s, $code, $msg, @args) = @_;
1379 if (!defined($msg) && exists($response{$code})) {
1380 $msg = $response{$code};
1381 } elsif (!defined($msg)) {
1382 $s->log(1,"no message for response code $code");
1385 my $line = sprintf "$code $msg", @args;
1387 $s->log(3,'%s <- %s', $s->client, $line);
1388 $s->{nntp}{response}{$code}++;
1390 $s->print($line,$crlf);
1394 =head2 unimplemented
1399 my ($s, @args) = @_;
1401 $s->log(2,'%s unimplemented command: %s', $s->client, join(' ', $s->{command}, @args));
1410 my ($s, @args) = @_;
1412 $s->log(2,'%s unsupported command: %s', $s->client, join(' ', @args));
1413 $s->log(2,'%s caller = ', $s->client, caller);
1425 my ($have) = $s->fetch_stat($id);
1427 $s->response(438, undef, $id);
1428 $s->log(3, 'already have article %s, rejecting', $have);
1429 } elsif ($s->{nntp}{'throttled'}) { # TODO some way to enter this state
1430 # TODO maybe a SIGUSR1
1431 $s->response(431, undef, $id);
1433 $s->response(238, undef, $id);
1447 my $a = $s->receive();
1449 return $s->response(501) unless $id =~ /($article_re)/;
1452 return $s->server_quit(400,"error in receiving article $id, failed to read");
1453 } elsif ($id ne $a->messageid()) {
1454 my $rid = $a->messageid();
1455 $s->log(1, "message id mismatch. headers follow\n" . $a->{head});
1456 return $s->server_quit(400,"error in receiving article '$id', id mis-match = '$rid'");
1459 $ok = $s->store($a);
1464 return $s->server_quit(400,"error in storing article $id");
1467 $s->response(239,undef,$id);
1469 return $s->response(439,undef,$id);
1475 my $now = $s->system_ts();
1477 Generates an RFC 5536 3.1.1 and RFC 5322 3.3 compliant date string, uses
1478 GMT/UTC. See L<"Date::Calc"/"Today_and_Now">
1483 my ($y, $m, $d, $hr, $min, $sec) = Date::Calc::Today_and_Now(1);
1485 return sprintf('%02d %s %04d %02d:%02d:%02d +0000', $d,
1486 substr(Date::Calc::Month_to_Text($m),0,3),
1487 $y, $hr, $min, $sec);
1490 # TODO actually check against RFC 5536
1499 my @headerfields = $a->head;
1502 foreach (@headerfields) {
1503 my @headerlines = split(/\r?\n/, $_);
1504 return 0 unless $headerlines[0] =~ /^(\S+):.*\S/;
1506 foreach my $hl (@headerlines) {
1507 return 0 unless $hl =~ /\S/;
1511 for (@counts{qw|approved archive control distribution expires
1512 followup-to injection-date injection-info
1513 lines newsgroups organization path summary
1514 supersedes user-agent xref|}) {
1515 return 0 if (defined && $_ > 1);
1529 $s->{nntp}{pathhost} = $set;
1532 return $s->{nntp}{pathhost};
1535 =head2 read_until_dot
1539 sub read_until_dot {
1543 # TODO figure out why we can't read from $s->{server}{client}
1544 # different buffering?
1545 while (my $line = <>) {
1547 last if $line =~ /^\.\r?\n/;
1559 my $a = Net::Server::NNTP::Article->new;
1561 my $c = $s->read_until_dot($fh);
1563 ($a->{head}, $a->{body}) = split(/\r?\n\r?\n/, $c, 2);
1564 $a->{head} .= "\r\n";
1566 # TODO check article for validity
1572 my $a = $s->receive();
1574 Receives an article in "wire" format (i.e. ending with a . on a line,
1575 and initial . doubled). Adds a path header if there isn't one, and adds
1576 pathhost to the path header.
1582 $s->log(5, 'Starting article receive');
1583 my $a = $s->readarticle($s->{server}{client});
1584 $s->log(5, 'Read article');
1585 $s->log(1, 'unable to read article for receive()') unless $a;
1586 return undef unless $a;
1588 $s->log(5, "got article: head: " . $a->{head});
1589 $s->log(6, "got article: body: " . $a->{body});
1591 $a->ensure_header('Path','not-for-mail');
1592 $a->add_to_path($s->pathhost);
1597 =head2 process_moderated
1601 sub process_moderated {
1602 my ($s, $a, $g) = @_;
1608 sub fetch_moderator {
1613 sub validate_approved {
1621 store should store an article in the article database
1623 arguments are a hashref with a head and body
1625 return false if the article should be rejected, return true if the
1626 article was accepted, die if there is an error
1635 sub process_control {
1637 $s->log(1, q{%s: ignoring control message '%s' (%s)}, $s->client,
1638 $a->control, join(',',$a->newsgroups));
1646 my $id = $a->messageid;
1647 return 0 if $s->fetch_stat($id);
1649 my @groups = $s->check_active($a->newsgroups);
1650 $s->log(3, 'no newsgroups header or no valid groups: %s', $a->newsgroups) unless @groups;
1651 return 0 unless @groups;
1653 $s->log(3, 'Checking for control messages');
1654 if (defined(my $cmsg = $a->control())) {
1655 return $s->process_control($a);
1658 $s->log(3, 'Checking for moderated groups');
1659 if (my $modgroup = $s->moderated_group(@groups)) {
1660 if (!defined($a->approved)) {
1661 return $s->process_moderated($a, $modgroup);
1662 } elsif (!$s->validate_approved($a)) {
1668 return $s->store_article($a);
1671 package Net::Server::NNTP::Article;
1672 use Sys::Hostname qw();
1696 return wantarray ? split($a->{head}, /\r?\n(?!\s)/) : $a->{head};
1705 return wantarray ? split($a->{body}, /\r?\n(?!\s)/) : $a->{body};
1714 return $a->{head} . "\r\n", $a->{body};
1723 return $a->{body} =~ tr/\n/\n/;
1732 return $a->{head} =~ tr/\n/\n/;
1741 return length($a->{head}) + length($a->{body}) + 2;
1749 my ($a,$fh,@trailers) = @_;
1750 print $fh $a-{head};
1751 print $_ for @trailers;
1759 my ($a,$fh,@trailers) = @_;
1760 print $fh $a-{body};
1761 print $_ for @trailers;
1769 my ($a,$fh,@trailers) = @_;
1770 print $fh $a->{head}, "\r\n", $a->{body};
1771 print $_,"\r\n" for @trailers;
1774 =head2 read_until_dot
1778 sub read_until_dot {
1782 while (my $line = <$fh>) {
1783 last if $line =~ /^\.\r?\n/;
1795 $a = $a->new unless ref $a;
1797 my $c = $a->read_until_dot($fh);
1799 ($a->{head}, $a->{body}) = split(/\r?\n\r?\n/, $c, 2);
1800 $a->{head} .= "\r\n";
1811 return map { $a->header($_) } @want;
1814 # looks like headers are case insensitive. see rfc 2822
1820 my ($a, $want, $set) = @_;
1824 $set =~ s/\r?\n?$//;
1826 if ($a->{head} =~ /^((?i)\Q$want\E)\s*:/msx) {
1828 =~ s/^((?i)\Q$want\E)\s*:\s?(.+?)$ \r?\n(?!\s)/$1: $set\r\n/msx;
1830 $a->{head} .= "$want: $set\r\n";
1836 =~ /^((?i)\Q$want\E)\s*:\s?(.+?)$ \r?\n(?!\s)/msx;
1838 return undef unless defined $2;
1841 $set =~ s/\r?\n?$//;
1846 =head3 number(@groups) returns number from the Xref header
1855 my ($a,@groups) = @_;
1857 my $xref = $a->header('Xref');
1858 return unless defined($xref);
1859 my %numbers = split /\S+|:/, $xref;
1860 return @numbers{@groups};
1863 =head2 ensure_header
1870 $a->header($h,$c) unless defined($a->header($h));
1871 return $a->header($h);
1874 # generate an RFC 5536 3.1.1 and RFC 5322 3.3 compliant date string
1880 my ($y, $m, $d, $hr, $min, $sec) = Date::Calc::Today_and_Now(1);
1882 return sprintf('%02d %s %04d %02d:%02d:%02d +0000', $d,
1883 substr(Date::Calc::Month_to_Text($m),0,3),
1884 $y, $hr, $min, $sec);
1893 my ($a, $host) = @_;
1894 $host ||= Sys::Hostname::hostname();
1895 return '<'.Data::UUID->new()->create_str().'@'.$host.'>';
1899 our @required_headers = qw(From Date Newsgroups Subject Message-ID Path);
1900 our @opt_headers = qw(Approved Archive Control Distribution Expires
1901 Followup-To Injection-Date Injection-Info Organization References Summary
1902 Supersedes User-Agent Xref);
1910 $a->header('Message-ID',@args);
1919 my $p = $a->header('Path',@args);
1920 return wantarray ? split(/\!/,$p) : $p;
1923 # TODO could do a bit less work here if a scalar is wanted
1933 $a->header('Newsgroups',join(',',@set));
1935 @set = split(/\s*,\s*/,$a->header('Newsgroups'));
1938 return wantarray ? @set : join(',',@set);
1941 # TODO make sure we ignore the RFC's requirements on approved headers
1942 # If you don't know why, then don't change this
1950 $a->header('Approved',@app);
1959 $a->header('Control',@arg);
1968 $path = Sys::Hostname::hostname() unless defined($path);
1970 $a->header('Path',"$path!". $a->header('Path'));
1975 Nathan Wagner, C<< <nw at hydaspes.if.org> >>
1979 Please report any bugs or feature requests to C<bug-net-server-nntp
1980 at rt.cpan.org>, or through the web interface at
1981 L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net::Server::NNTP>.
1982 I will be notified, and then you'll automatically be notified of
1983 progress on your bug as I make changes.
1987 You can find documentation for this module with the perldoc command.
1989 perldoc Net::Server::NNTP
1992 You can also look for information at:
1996 =item * RT: CPAN's request tracker
1998 L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Net::Server::NNTP>
2000 =item * AnnoCPAN: Annotated CPAN documentation
2002 L<http://annocpan.org/dist/Net::Server::NNTP>
2004 =item * CPAN Ratings
2006 L<http://cpanratings.perl.org/d/Net::Server::NNTP>
2010 L<http://search.cpan.org/dist/Net::Server::NNTP>
2018 L<Net::Server::MultiType>
2020 =head1 ACKNOWLEDGEMENTS
2022 Urs Janssen, maintainer of the tin newsreader, helped with this module by
2023 providing a series of prompt and detailed bug reports on the NNTP
2026 =head1 COPYRIGHT & LICENSE
2028 Written entirely from scratch by Nathan Wagner and released into the
2033 1; # End of Net::Server::NNTP