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();
69 A list of functions that can be exported. You can delete this section if
70 you don't export anything, such as for a purely object-oriented module.
74 our $article_re = qr/\<[^\s\>]+\@[^\s\>]+\>/; # rfc 1036 2.1.5
75 our $crlf = "\015\012"; # avoid local interpretation of \n
79 =head1 STATE FUNCTIONS
81 =head1 STORAGE FUNCTIONS
83 =head1 INTERNAL FUNCTIONS
85 =head1 Session Administration Commands
87 These methods implement commands from section 5 of RFC 3977 and
88 corresponding commands from other RFCs.
94 Handled internally by a coderef. Returns the contents of %capabilities.
98 Handles 'mode reader' (RFC 3977 5.3)
105 return $s->response(501) unless @_ > 1;
107 if ($s->syntax($arg, '(?i)reader')) { # RFC 4644-2.3
108 return $s->response(200,undef,$s->pathhost);
111 if ($s->syntax($arg, '(?i)stream')) { # RFC 4644-2.3
112 return $s->response(203);
125 return $s->response(501, 'too many arguments') if @_ > 1;
132 $s->server_quit($code, response);
137 my ($s, $code, @args) = @_;
138 $s->response($code, @args);
139 die 'server quitting';
142 =head1 Article Posting and Retrieval
146 $s->group('news.software.nntp');
148 Implements RFC 3977 6.1.1
156 return $s->response(501) unless @_ == 2;
158 if (my ($estimate, $low, $high, $group) = @row = $s->groupinfo($g)) {
159 $s->pointer(@row[3,1]);
160 $s->article_number(undef) unless $estimate;
161 $s->response(211,undef,@row);
167 =head2 parse_grouprange
169 takes a range spec and gets a low and high, as against a given group
171 returns an empty list if the range spec doesn't parse. The highwater
172 returned will be undef if the given group is invalid.
176 sub parse_grouprange {
177 my ($s, $range, $group, $lowwater, $highwater) = @_;
179 (undef, $lowwater, $highwater) = $s->groupinfo($group);
181 return ($group, $lowwater, $highwater) if @_ == 1;
183 my ($low, $r, $high) = $range =~ /(\d+)(-)?(\d+)?/;
185 return ($group, $low, $high);
186 } elsif (defined $r) {
187 return ($group, $low, $highwater);
188 } elsif (defined $low) {
189 return ($group, $low, $low);
201 my ($g, $range, @extraargs) = split(/\s+/, $arg);
202 return $s->response(501) if @extraargs;
204 $range = '1-' unless defined $range;
205 $g = $s->selected_group unless defined $g;
206 return $s->response(412) unless defined $g;
208 my @grouprange = $s->parse_grouprange($range, $g);
209 return $s->response(501) unless @grouprange;
211 my @gi = $s->changegroup($g) if @grouprange;
212 return $s->response(411) unless @gi;
214 my @articles = $s->fetch_grouplist(@grouprange);
216 $s->response(211, undef, @gi);
217 $s->sendresults(@articles,'.');
227 return $s->response(501) if @_ > 1;
229 return $s->response(412) unless $s->selected_group;
230 return $s->response(420) unless $s->article_number;
232 my ($n,$id) = $s->prev_article();
235 $s->article_number($n);
236 $s->response(223, undef, $n, $id);
244 Implements NNTP next (RFC 3977 6.1.4). Moves the article pointer to the next
249 If the currently selected newsgroup is valid, the current article number MUST
250 be set to the next article in that newsgroup (that is, the lowest existing
251 article number greater than the current article number). If successful, a
252 response indicating the new current article number and the message-id of that
253 article MUST be returned. No article text is sent in response to this command.
255 If the current article number is already the last article of the newsgroup, a
256 421 response MUST be returned. In all other aspects (apart, of course, from
257 the lack of 422 response), this command is identical to the LAST command
267 return $s->response(501,'too many arguments') if @_ > 1;
269 return $s->response(412) unless $s->selected_group;
270 return $s->response(420) unless $s->article_number;
272 my ($n,$id) = $s->next_article();
275 $s->article_number($n);
276 $s->response(223, undef, $n, $id);
290 my ($a, $g, $n, $id);
292 if (($id) = $s->syntax("@args", "($article_re)")) {
293 ($a) = $s->fetch_article($id);
294 return $s->response(430) unless defined $a;
296 } elsif (($n) = $s->syntax("@args", "(\\d+)")) {
297 $g = $s->selected_group;
298 return $s->response(412) unless defined $g;
299 $s->log(4, "fetching $g ($n)");
300 ($a, $id) = $s->fetch_article($g,$n);
301 return $s->response(423) unless defined $a;
302 $s->article_number($n);
304 ($g, $n) = $s->pointer;
305 return $s->response(412) unless defined $g;
306 return $s->response(420) unless defined $n;
307 $s->log(4, "fetching ($g $n)");
308 ($a,$id) = $s->fetch_article($g,$n);
309 return $s->response(420) unless defined $a;
311 return $s->response(501);
314 $s->response(220,undef,$n,$id);
325 my ($a, $g, $n, $id);
327 if (($id) = ("@args" =~ "($article_re)")) {
328 ($a) = $s->fetch_head($id);
329 return $s->response(430) unless defined $a;
331 } elsif (($n) = $s->matches("@args", "(\\d+)")) {
332 $g = $s->selected_group;
333 return $s->response(412) unless defined $g;
334 $s->log(4, "fetching $g ($n)");
335 ($a, $id) = $s->fetch_head($g,$n);
336 return $s->response(423) unless defined $a;
337 $s->article_number($n);
339 ($g, $n) = $s->pointer;
340 $s->log(4, "fetching ($g $n)");
341 return $s->response(412) unless defined $g;
342 return $s->response(420) unless defined $n;
343 ($a, $id) = $s->fetch_head($g,$n);
344 return $s->response(420) unless defined $a;
346 return $s->response(501);
349 $s->response(221,undef,$n,$id);
360 my ($a, $g, $n, $id);
362 if (($id) = $s->syntax("@args", "($article_re)")) {
363 ($a) = $s->fetch_body($id);
364 return $s->response(430) unless defined $a;
366 } elsif (($n) = $s->syntax("@args", "(\\d+)")) {
367 $g = $s->selected_group;
368 return $s->response(412) unless defined $g;
369 $s->log(4, "fetching $g ($n)");
370 ($a, $id) = $s->fetch_body($g,$n);
371 return $s->response(423) unless defined $a;
372 $s->article_number($n);
374 ($g, $n) = $s->pointer;
375 $s->log(4, "fetching ($g $n)");
376 return $s->response(412) unless defined $g;
377 return $s->response(420) unless defined $n;
378 ($a,$id) = $s->fetch_body($g,$n);
379 return $s->response(420) unless defined $a;
381 return $s->response(501);
384 $s->response(222,undef,$n,$id);
395 my ($a, $g, $n, $id);
397 if (($id) = $s->syntax("@args", "($article_re)")) {
398 $id = $s->fetch_stat($id);
399 return $s->response(430) unless defined $id;
401 } elsif (($n) = $s->syntax("@args", "(\\d+)")) {
402 $g = $s->selected_group;
403 return $s->response(412) unless defined $g;
404 $s->log(4, "fetching $g ($n)");
405 $id = $s->fetch_stat($g,$n);
406 return $s->response(423) unless defined $id;
407 $s->article_number($n);
409 ($g, $n) = $s->pointer;
410 $s->log(4, "fetching ($g $n)");
411 return $s->response(412) unless defined $g;
412 return $s->response(420) unless defined $n;
413 $id = $s->fetch_stat($g,$n);
414 return $s->response(420) unless defined $id;
416 return $s->response(501);
419 $s->response(223,undef,$n,$id);
434 return $s->response(440) unless $s->permit_posting;
436 my $rid = sprintf('<%s@%s>', Data::UUID->new()->create_str(),$s->pathhost);
438 $s->response(340, 'Ok, recommended ID %s', $rid);
440 my $a = $s->receive();
441 return $s->response(441) unless $a;
444 if (!defined($a->header('From'))
445 or !defined($a->header('Newsgroups'))
446 or !defined($a->header('Subject'))
447 or defined($a->header('Injection-Info'))
448 or defined($a->header('Xref'))
449 or $a->header('Path') =~ /POSTED/
450 or !$s->valid_syntax($a)
452 # must reject 5537-3.5-2
453 return $s->response(441);
456 # TODO 5537-3.5-2 SHOULD reject any proto-article that contains a
457 # header field deprecated for Netnews
458 # TODO deprecated fields:
460 # TODO policy reject NNTP-Posting-Host
463 $a->ensure_header('Date', $s->system_ts());
464 $a->ensure_header('Message-ID', $rid);
466 #$a->ensure_header('Lines',$a->bodylines);
468 # 5537-3.5-8 5537-3.5-9
469 # store method will prepend the pathhost
470 $a->ensure_header('Path','not-for-mail');
473 $a->header('Injection-Info', sprintf(q{posting-host = "%s"},
474 $s->{nntp}{peername}));
479 $posted = $s->store($a);
482 return $s->response(441);
485 return $s->response(240,
486 'article received ok, Message-ID %s', $a->messageid);
488 return $s->response(441);
492 =head2 ihave (RFC 3977 6.3.2)
500 return $s->response(501) unless $id =~ /($article_re)/;
501 return $s->response(430) if $s->fetch_stat($id);
502 return $s->response(436) unless $s->permit_posting;
506 my $a = $s->receive();
508 return $s->response(436) unless $a;
514 return $s->response(436);
516 $s->response($ok ? 235 : 437);
524 my ($s, $g, $n) = (@_,undef,undef);
527 $s->{nntp}{newsgroup} = $g;
528 $s->{nntp}{number} = $n;
530 return wantarray ? ($s->{nntp}{newsgroup},$s->{nntp}{number}) : $s->{nntp}{newsgroup};
534 my ($s, $g) = (@_, undef);
536 $s->{nntp}{newsgroup} = $g;
538 return $s->{nntp}{newsgroup};
542 my ($s, $n) = (@_, undef);
544 $s->{nntp}{number} = $n;
546 return $s->{nntp}{number};
549 our %capabilities = (
556 'LIST ACTIVE NEWSGROUPS OVERVIEW.FMT ACTIVE.TIMES HEADERS' => '',
562 Subject => 'subject',
563 'Message-ID' => 'msgid',
566 References => 'references',
568 Newsgroups => 'newsgroups',
570 ':lines' => 'actuallines',
571 'Xref' => 'local article numbers',
573 our @over = qw(Subject From Date Message-ID References :bytes :lines Xref);
582 return sprintf('%04d%02d%02d%02d%02d%02d', Date::Calc::System_Clock());
585 our $keyword_re = '^[a-zA-Z][a-zA-Z0-9\.\-]{2}';
588 article => \&article, # reader 6.2.1
589 authinfo => \&unsupported, # rfc 4643
590 starttls => \&unsupported, # rfc 4642, IO::Socket::SSL->start
591 body => \&body, # reader 6.2.3
592 check => \&check, # rfc 4644 2.4
593 takethis => \&takethis, # rfc 4644 2.5
594 capabilities => sub { # mandatory 5.2
596 if (@_ > 1 && $arg !~ /^$keyword_re$/) {
601 $s->sendresults('VERSION 2', keys %capabilities,'.');
603 date => sub { # reader 7.1
605 $s->response(111,undef, $s->servertime);
607 group => \&group, # reader 6.1.1
608 hdr => \&hdr, # hdr 8.5
609 xhdr => \&unimplemented,
610 head => \&head, # mandatory 6.2.2
611 help => sub { # mandatory 7.2
614 $s->sendresults('The following commands are implemented',
615 sort grep { $cmd{$_} != \&unimplemented
616 && $cmd{$_} != \&unsupported}
619 ihave => \&ihave, # ihave 6.3.2
620 'last' => \&last, # reader 6.1.3
621 list => \&list, # list 7.6.[13456], over 8.4
622 listgroup => \&listgroup, # reader 6.1.2
623 mode => \&mode, # mode-reader 5.3, 4644-2.3 mode stream
624 newgroups => \&newgroups, # reader 7.3
625 newnews => \&newnews, # newnews 7.4
626 'next' => \&next, # reader 6.1.4
627 over => \&over, # over 8.3
628 xover => \&over, # we hope this is the same as over (it is, but the overview.fmt listing is different)
629 post => \&post, # post 6.3.1
631 'stat' => \&stat, # mandatory 6.2.4
632 # slave is removed from the protocol
633 # slave => sub {my ($s) = @_; $peer_is_slave = 1; $s->response(202)},
634 'xadmin' => \&xadmin,
642 my ($s, $group) = @_;
644 return () unless $group;
646 my @row = $s->groupinfo($group);
649 $s->pointer($group,$row[1]);
659 delegated to IO::Socket->print()
665 $s->{server}{client}->print(@args);
670 $s->sendresults(@lines);
672 Sends each element of @lines followed by a crlf pair.
673 If an element of @lines is a reference, it is assumed to be
674 an arrayref and the elements thereof are joined with a space
675 and the resulting string is output.
680 my ($s, @lines) = @_;
681 $s->print(ref $_ ? join(' ', @$_) : $_, $crlf) for @lines;
693 my ($wildmat, $date, $time);
695 return $s->response(501) unless ($wildmat, $date, $time) =
696 $s->syntax("@args", '(\S+)\s+(\\d{6}|\\d{8})\s+(\\d{6})(\s+GMT)?');
698 my $ts = $s->parsetime($date,$time);
699 return $s->response(501) unless defined $ts;
701 my $regex = $s->wildmat_to_regex($wildmat);
702 return $s->response(501) unless defined $regex;
704 $s->log(2, "newnews wildmat = $regex");
706 my @article_ids = $s->fetch_newnews($ts, $regex);
709 $s->sendresults(@article_ids,'.');
720 ($subcmd, @args) = split(/\s+/, $arg);
722 $subcmd = 'active' unless defined($subcmd); # 7.6.1.1
723 $subcmd = lc($subcmd);
727 if ($subcmd eq 'active') { # 7.6.3
729 @results = $s->fetch_active(@args);
731 return $s->response(501);
734 elsif ($subcmd eq 'active.times') { # 7.6.4
736 @results = $s->fetch_activetimes(@args);
737 return $s->response(503) unless ref $results[0];
739 return $s->response(501);
742 # don't forget to update capabilities when this is implemented
743 elsif ($subcmd eq 'distrib.pats') { # 7.6.5
744 return $s->response(501) if @args;
745 return $s->response(503);
748 elsif ($subcmd eq 'headers') { # 8.6
749 # TODO ask the storage what it can do
750 return $s->response(501) if @args;
751 @results = keys %hdrs;
753 elsif ($subcmd eq 'newsgroups') { # 7.6.6
755 @results = $s->fetch_activetimes(@args);
756 return $s->response(503) unless ref $results[0];
758 return $s->response(501);
761 elsif ($subcmd eq 'overview.fmt') { # 8.4
762 return $s->response(501) if @args;
763 # TODO use old xover format if it seems warranted
764 @results = $s->fetch_overviewfmt();
765 $s->response(215,'Order of fields in overview database.');
766 $s->sendresults(@results,'.');
773 $s->sendresults(@results,'.');
776 # command prep and check
778 # args => 'max args' or [min,max]
779 # check => [regexes to validate args against, if defined]
780 # fail => what to do if it fails
781 # func => command to pass args on to
783 # sub command_check {
784 # my ($syntax, @args) = @_;
787 # see rfc 3977 7.3.2 for description of format
793 my ($s,$date,$time) = @_;
796 if ($date =~ /^(\d\d)(\d\d)(\d\d)$/) {
797 my $curyear = (localtime)[5]+1900;
798 my $curcent = int ($curyear/100);
799 my $yic = $curyear % 100;
800 my $cent = $1 <= $yic ? $curcent : $curcent - 1;
801 $ts = sprintf('%02d%02d-%02d-%02d', $cent,$1,$2,$3);
802 } elsif ($date =~ /^(\d\d\d\d)(\d\d)(\d\d)$/) {
803 $ts = sprintf('%04d-%02d-%02d', $1,$2,$3);
807 if ($time =~ /^(\d\d)(\d\d)(\d\d)$/) {
808 $ts = sprintf('%s %02d:%02d:%02d',$ts,$1,$2,$3);
823 $wildmat =~ s/\./\\\./g;
824 $wildmat =~ s/\?/\./g;
825 $wildmat =~ s/\*/\.\*/g;
829 =head2 wildmat_to_regex
833 sub wildmat_to_regex {
834 my ($s, $wildmat) = @_;
836 my @pats = split(/,/,$wildmat); # TODO look for escaped commas
839 # TODO special case '*' since it always matches
841 while ($pats[0] =~ /^!/) { shift @pats } # init neg can't match
846 my $like = wildmat_to_re($_);
851 $sql = "(^(?!$like)($sql)\$)";
855 $sql = "^($sql)\$" unless $negated;
864 my ($s, $args, @regex) = @_;
869 next unless defined $re;
870 if ($args[$_] !~ /$re/) {
871 $s->log(2, "Argument invalid: $args[$_] !~ /$re/");
880 Checks a string against a regex and returns the matches.
881 Logs if the syntax fails.
886 my ($s, $cmd, $re) = @_;
889 if (@match = ($cmd =~ /^$re$/)) {
893 $s->log(3, "syntax fail: '$cmd' !~ /$re/");
899 Checks a string against a regex and returns the matches.
904 my ($s, $cmd, $re) = @_;
907 if (@match = ($cmd =~ /^$re$/)) {
922 return $s->response(501) unless ($date, $time) =
923 $s->syntax("@args", '(\\d{6}|\\d{8}) (\\d{6})( GMT)?');
925 my $ts = $s->parsetime($date,$time);
926 return $s->response(501) unless defined $ts;
928 my @results = $s->fetch_newgroups($ts);
931 $s->sendresults(@results,'.');
934 # TODO access control?
936 =head2 permit_posting
949 Calls $s->fetch_overview
954 my ($s, $arg, @extra) = @_;
956 my ($id, $lo, $range, $hi);
958 return $s->response(501) if @extra;
961 # 3977-8.5.1 third form
962 return $s->response(412) unless defined $s->selected_group;
963 return $s->response(420) unless defined $s->article_number;
964 @headers = $s->fetch_overview($s->pointer);
965 return $s->response(420) unless @headers;
966 return $s->response(503) if $headers[0] == undef;
967 } elsif (($id) = $s->syntax($arg, "($article_re)")) {
968 # 3977-8.5.1 first form
969 @headers = $s->fetch_overview($id);
970 return $s->response(430) unless @headers;
971 return $s->response(503) if $headers[0] == undef;
972 } elsif (($lo, $range, $hi) = $s->syntax($arg, '(\d+)(-)?(\d+)?')) {
973 # 3977-8.5.1 second form
974 return $s->response(423) if $hi < $lo;
975 return $s->response(412) unless defined $s->selected_group;
976 my @gi = $s->groupinfo();
977 return $s->response(412) unless @gi;
979 @headers = $s->fetch_overview($gi[0], $lo, $hi);
980 } elsif (defined $range) {
981 @headers = $s->fetch_overview($gi[0], $lo, $gi[2]);
983 @headers = $s->fetch_overview($gi[0], $lo);
985 return $s->response(423) unless @headers;
986 return $s->response(503) if $headers[0] == undef;
988 return $s->response(501);
992 $s->sendresults(@headers, '.');
996 # TODO allow any header?
1000 Implements 3977-8.5.1
1002 Calls $s->fetch_headers.
1007 my ($s, $args) = @_;
1009 my ($field, $arg) = split(/\s+/, $args);
1010 my ($id, $hi, $lo, $range);
1014 # 3977-8.5.1 third form
1015 return $s->response(412) unless defined $s->selected_group;
1016 return $s->response(420) unless defined $s->article_number;
1017 @headers = $s->fetch_headers($field, $s->pointer);
1018 return $s->response(420) unless @headers;
1019 return $s->response(503) if $headers[0] == undef;
1020 } elsif (($id) = $s->syntax($arg, "($article_re)")) {
1021 # 3977-8.5.1 first form
1022 @headers = $s->fetch_headers($field, $id);
1023 return $s->response(430) unless @headers;
1024 return $s->response(503) if $headers[0] == undef;
1025 } elsif (($lo, $range, $hi) = $s->syntax($arg, '(\d+)(-)?(\d+)?')) {
1026 # 3977-8.5.1 second form
1027 return $s->response(423) if $hi < $lo;
1028 return $s->response(412) unless defined $s->selected_group;
1029 my @gi = $s->groupinfo();
1030 return $s->response(412) unless @gi;
1032 @headers = $s->fetch_headers($field, $gi[0], $lo, $hi);
1033 } elsif (defined $range) {
1034 @headers = $s->fetch_headers($field, $gi[0], $lo, $gi[2]);
1036 @headers = $s->fetch_headers($field, $gi[0], $lo);
1038 return $s->response(423) unless @headers;
1039 return $s->response(503) if $headers[0] == undef;
1041 return $s->response(501);
1044 foreach (@headers) {
1045 $_->[1] =~ s/\r?\n//g;
1046 $_->[1] =~ s/\t/ /g;
1050 $s->sendresults(@headers, '.');
1054 100 => 'help text follows',
1055 101 => 'Capability list follows',
1056 111 => '%s server date and time',
1058 200 => 'server %s ready, posting allowed',
1059 201 => 'server %s ready, posting prohibited',
1060 202 => 'slave status noted',
1061 203 => 'Streaming permitted',
1062 205 => 'closing connection',
1063 211 => '%d %d %d %s group selected',
1064 215 => 'list of newsgroups follows',
1065 220 => '%d %s article follows',
1066 221 => '%d %s article headers follows',
1067 222 => '%d %s article body follows',
1068 223 => '%d %s article exists and selected',
1069 224 => 'overview information follows',
1070 225 => 'headers follow',
1071 230 => 'list of new articles follows',
1072 231 => 'list of new newsgroups follows',
1073 235 => 'article transferred ok',
1074 238 => '%s Send article to be transferred',
1075 239 => '%s Article transferred OK',
1076 240 => 'article received ok',
1078 335 => 'send article to be transferred. End with <CR-LF>.<CR-LF>',
1079 340 => 'send article to be posted. End with <CR-LF>.<CR-LF>',
1081 400 => 'service not available or no longer available',
1082 401 => '%s server is in wrong mode; use indicated capability',
1083 403 => 'internal fault preventing action being taken',
1084 411 => 'no such newsgroup',
1085 412 => 'no newsgroup selected',
1086 420 => 'no current article has been selected',
1087 421 => 'no next article in this group',
1088 422 => 'no previous article in this group',
1089 423 => 'no such article number in this group',
1090 430 => 'no such article found',
1091 431 => '%s Transfer not possible; try again later',
1092 435 => 'article not wanted - do not send it',
1093 436 => 'transfer failed - try again later',
1094 437 => 'article rejected - do not try again',
1095 438 => '%s Article not wanted',
1096 439 => '%s Transfer rejected; do not retry',
1097 440 => 'posting not allowed',
1098 441 => 'posting failed',
1100 500 => 'command not recognized',
1101 501 => 'command syntax error',
1102 502 => 'access restriction or permission denied',
1103 503 => 'program fault - command not performed',
1106 =head2 connect_to_storage
1110 sub connect_to_storage {
1113 return $s->{db} if defined $s->{db};
1115 # TODO use a config parameter optionally here
1116 my $dsn = $ENV{'DBI_DSN'};
1117 $dsn = 'dbi:Pg:dbname=news' unless defined $dsn;
1118 $s->log(4, "connecting to $dsn");
1120 $s->{db} = DBI->connect($dsn,'','',{AutoCommit => 0,RaiseError=>1});
1121 $s->{db}->{PrintError} = 0;
1123 $s->{db}->do("set CLIENT_ENCODING to 'SQL_ASCII'");
1128 =head2 Storage Access Functions
1134 Return the next article number in a group, undef if none.
1135 Should return the number in a scalar context, number, articleid in
1147 my ($rec, $ref, $rej, $postp) =
1149 $s->{nntp}{response}{239}
1150 + $s->{nntp}{response}{235}
1151 + $s->{nntp}{response}{240}, # received
1153 $s->{nntp}{response}{435}, # refused
1155 $s->{nntp}{response}{439}
1156 + $s->{nntp}{response}{437}, # rejected
1158 $s->{nntp}{response}{436}, # postponed
1161 $s->log(2, sprintf('%s received %d, refused %d, rejected %d, postponed %d', $s->client, $rec, $ref, $rej, $postp));
1163 $s->{nntp}{response}{$_} = 0 for keys %response;
1172 return $s->{server}{client};
1181 $s->{server}->{peeraddr} . ':' .$s->{server}->{peerport};
1184 =head2 process_request
1191 # we don't have the peeraddr set yet.
1192 #$s->log(2, 'forking for connection from %s', $s->{server}{client});
1197 sub post_accept_hook {
1200 # net server seems to log connections
1201 #$s->log(2, 'accepted connection from %s', $s->{server}{peeraddr});
1206 sub request_denied_hook {
1209 $s->log(2, 'denied connection from %s', $s->{server}{peeraddr});
1212 sub process_request {
1214 #$s->log(2,'%s: connected, log_level %d, server_type (%s)',$s->client, $s->{server}{log_level}, join(', ', @{$s->{server}{server_type}}));
1216 $s->connect_to_storage();
1218 $s->{nntp}{connecttime} = time;
1219 $s->{nntp}{response}{$_} = 0 for keys %response;
1221 my $peername = undef;
1222 # five seconds max to do reverse lookup, otherwise skip it
1223 # TODO i think Net::Server will do the reverse
1225 local $SIG{ALRM} = sub { die "Timed Out!\n" };
1227 ($peername) = gethostbyaddr(Socket::inet_aton($s->{server}->{peeraddr}), Socket::AF_INET());
1230 $s->log(1, "Can't get peername for ".$s->{server}->{peeraddr} . ": $@");
1231 $peername = $s->{server}->{peeraddr};
1233 $s->{nntp}{peername} = $peername;
1235 # parent will kill us with a term
1236 $SIG{TERM} = sub { $s->log_stats();exit 0 };
1239 local $SIG{ALRM} = sub { die "Timed Out!\n" };
1240 alarm($s->{nntp}{first_timeout});
1241 $s->response($s->permit_posting()?200:201,undef,$s->pathhost);
1242 # TODO use a variable so subclassers can use not STDIN
1246 $s->log(3, '%s -> %s', $s->client, $_);
1247 my ($cmd, @args) = split(/\s+/, $_, 2);
1248 # TODO enforce maximum length?
1250 if (exists($cmd{$cmd})) {
1251 $s->{command} = $cmd;
1252 $cmd{$cmd}->($s, @args);
1254 $s->log(4, "command not recognized '%s'", $cmd);
1257 alarm($s->{nntp}{timeout});
1261 if ($@=~/timed out/i) {
1262 $s->log(2, '%s: Timed Out.', $s->client);
1263 } elsif ($@ =~ /client quit/) {
1264 $s->log(2, '%s: client quit', $s->client);
1265 } elsif (defined($@) && length($@)) {
1268 $s->log(2, '%s: disconnecting', $s->client);
1272 =head2 default_values
1276 sub default_values {
1277 ### add a single value option
1278 my $hn = Sys::Hostname::hostname();
1279 my @v = split(/\./, $hn);
1282 $hn = join('.', @v);
1286 log_level => 2, # this is default I think
1289 server_type => [qw(Fork)],
1292 log_file => 'Sys::Syslog',
1293 pid_file => '/var/run/news/newsd.pid',
1294 syslog_facility => 'news',
1295 syslog_ident => 'newsd',
1296 syslog_logopt => 'pid',
1297 conf_file => -r '/etc/newsd.conf' ? '/etc/newsd.conf' : undef,
1298 first_timeout => 30, # seconds to receive first command
1299 timeout => 900, # subsequent commands 15 min
1304 # server text, pathhost
1305 # groupsync 604800 == weekly, use undef for no sync, or no active file
1309 # localgroups text default 'local.*',
1310 # groups text default '*'
1312 # insert into configuration values ('localgroups','local.*');
1321 $s->log(1, 'options called');
1322 $s->{'nntp'} ||= {};
1324 my $opt = $s->{'nntp'};
1326 ### setup options in the parent classes
1327 $s->SUPER::options($oh);
1331 $oh->{'pathhost'} ||= \ $opt->{'pathhost'};
1333 $opt->{'activesync'} ||= 604800;
1334 $oh->{'activesync'} ||= \ $opt->{'activesync'};
1336 $opt->{'activefile'} ||= 'ftp://ftp.isc.org/pub/usenet/CONFIG/active.gz';
1337 $oh->{'activefile'} ||= \ $opt->{'activefile'};
1339 $opt->{'newsgroups'} ||= 'ftp://ftp.isc.org/pub/usenet/CONFIG/newsgroups.gz';
1340 $oh->{'newsgroups'} ||= \ $opt->{'newsgroups'};
1342 $opt->{'first_timeout'} ||= 120;
1343 $oh->{'first_timeout'} ||= \$ opt->{'first_timeout'};
1345 $opt->{'timeout'} ||= 900;
1346 $oh->{'timeout'} ||= \ $opt->{'timeout'};
1348 #$template->{'my_option'} = \ $prop->{'my_option'};
1350 ### add a multi value option
1351 #$prop->{'an_arrayref_item'} ||= [];
1352 #$template->{'an_arrayref_item'} = $prop->{'an_arrayref_item'};
1357 $s->log($fmt, @args);
1359 Overrides the Net::Server log method and always treats the first
1360 argument as a format string. We have to do this because Net::Server
1361 treats the arguments differently depending on whether syslog is used.
1362 Uses Perl's sprintf to do the formatting.
1367 my ($s, $lvl, $fmt, @args) = @_;
1371 $msg = sprintf($fmt, @args);
1376 $s->SUPER::log($lvl, $msg);
1384 my ($s, $code, $msg, @args) = @_;
1386 if (!defined($msg) && exists($response{$code})) {
1387 $msg = $response{$code};
1388 } elsif (!defined($msg)) {
1389 $s->log(1,"no message for response code $code");
1392 my $line = sprintf "$code $msg", @args;
1394 $s->log(3,'%s <- %s', $s->client, $line);
1395 $s->{nntp}{response}{$code}++;
1397 $s->print($line,$crlf);
1401 =head2 unimplemented
1406 my ($s, @args) = @_;
1408 $s->log(2,'%s unimplemented command: %s', $s->client, join(' ', $s->{command}, @args));
1417 my ($s, @args) = @_;
1419 $s->log(2,'%s unsupported command: %s', $s->client, join(' ', @args));
1420 $s->log(2,'%s caller = ', $s->client, caller);
1432 my ($have) = $s->fetch_stat($id);
1434 $s->response(438, undef, $id);
1435 $s->log(3, 'already have article %s, rejecting', $have);
1436 } elsif ($s->{nntp}{'throttled'}) { # TODO some way to enter this state
1437 # TODO maybe a SIGUSR1
1438 $s->response(431, undef, $id);
1440 $s->response(238, undef, $id);
1454 my $a = $s->receive();
1456 return $s->response(501) unless $id =~ /($article_re)/;
1459 return $s->server_quit(400,"error in receiving article $id, failed to read");
1460 } elsif ($id ne $a->messageid()) {
1461 my $rid = $a->messageid();
1462 $s->log(1, "message id mismatch. headers follow\n" . $a->{head});
1463 return $s->server_quit(400,"error in receiving article '$id', id mis-match = '$rid'");
1466 $ok = $s->store($a);
1471 return $s->server_quit(400,"error in storing article $id");
1474 $s->response(239,undef,$id);
1476 return $s->response(439,undef,$id);
1482 my $now = $s->system_ts();
1484 Generates an RFC 5536 3.1.1 and RFC 5322 3.3 compliant date string, uses
1485 GMT/UTC. See L<"Date::Calc"/"Today_and_Now">
1490 my ($y, $m, $d, $hr, $min, $sec) = Date::Calc::Today_and_Now(1);
1492 return sprintf('%02d %s %04d %02d:%02d:%02d +0000', $d,
1493 substr(Date::Calc::Month_to_Text($m),0,3),
1494 $y, $hr, $min, $sec);
1497 # TODO actually check against RFC 5536
1506 my @headerfields = $a->head;
1509 foreach (@headerfields) {
1510 my @headerlines = split(/\r?\n/, $_);
1511 return 0 unless $headerlines[0] =~ /^(\S+):.*\S/;
1513 foreach my $hl (@headerlines) {
1514 return 0 unless $hl =~ /\S/;
1518 for (@counts{qw|approved archive control distribution expires
1519 followup-to injection-date injection-info
1520 lines newsgroups organization path summary
1521 supersedes user-agent xref|}) {
1522 return 0 if (defined && $_ > 1);
1536 $s->{nntp}{pathhost} = $set;
1539 return $s->{nntp}{pathhost};
1542 =head2 read_until_dot
1546 sub read_until_dot {
1550 # TODO figure out why we can't read from $s->{server}{client}
1551 # different buffering?
1552 while (my $line = <>) {
1554 last if $line =~ /^\.\r?\n/;
1566 my $a = Net::Server::NNTP::Article->new;
1568 my $c = $s->read_until_dot($fh);
1570 ($a->{head}, $a->{body}) = split(/\r?\n\r?\n/, $c, 2);
1571 $a->{head} .= "\r\n";
1573 # TODO check article for validity
1579 my $a = $s->receive();
1581 Receives an article in "wire" format (i.e. ending with a . on a line,
1582 and initial . doubled). Adds a path header if there isn't one, and adds
1583 pathhost to the path header.
1589 $s->log(5, 'Starting article receive');
1590 my $a = $s->readarticle($s->{server}{client});
1591 $s->log(5, 'Read article');
1592 $s->log(1, 'unable to read article for receive()') unless $a;
1593 return undef unless $a;
1595 $s->log(5, "got article: head: " . $a->{head});
1596 $s->log(6, "got article: body: " . $a->{body});
1598 $a->ensure_header('Path','not-for-mail');
1599 $a->add_to_path($s->pathhost);
1604 =head2 process_moderated
1608 sub process_moderated {
1609 my ($s, $a, $g) = @_;
1615 sub fetch_moderator {
1620 sub validate_approved {
1628 store should store an article in the article database
1630 arguments are a hashref with a head and body
1632 return false if the article should be rejected, return true if the
1633 article was accepted, die if there is an error
1642 sub process_control {
1644 $s->log(1, q{%s: ignoring control message '%s' (%s)}, $s->client,
1645 $a->control, join(',',$a->newsgroups));
1653 my $id = $a->messageid;
1654 return 0 if $s->fetch_stat($id);
1656 my @groups = $s->check_active($a->newsgroups);
1657 $s->log(3, 'no newsgroups header or no valid groups: %s', $a->newsgroups) unless @groups;
1658 return 0 unless @groups;
1660 $s->log(3, 'Checking for control messages');
1661 if (defined(my $cmsg = $a->control())) {
1662 return $s->process_control($a);
1665 $s->log(3, 'Checking for moderated groups');
1666 if (my $modgroup = $s->moderated_group(@groups)) {
1667 if (!defined($a->approved)) {
1668 return $s->process_moderated($a, $modgroup);
1669 } elsif (!$s->validate_approved($a)) {
1675 return $s->store_article($a);
1678 package Net::Server::NNTP::Article;
1679 use Sys::Hostname qw();
1703 return wantarray ? split($a->{head}, /\r?\n(?!\s)/) : $a->{head};
1712 return wantarray ? split($a->{body}, /\r?\n(?!\s)/) : $a->{body};
1721 return $a->{head} . "\r\n", $a->{body};
1730 return $a->{body} =~ tr/\n/\n/;
1739 return $a->{head} =~ tr/\n/\n/;
1748 return length($a->{head}) + length($a->{body}) + 2;
1756 my ($a,$fh,@trailers) = @_;
1757 print $fh $a-{head};
1758 print $_ for @trailers;
1766 my ($a,$fh,@trailers) = @_;
1767 print $fh $a-{body};
1768 print $_ for @trailers;
1776 my ($a,$fh,@trailers) = @_;
1777 print $fh $a->{head}, "\r\n", $a->{body};
1778 print $_,"\r\n" for @trailers;
1781 =head2 read_until_dot
1785 sub read_until_dot {
1789 while (my $line = <$fh>) {
1790 last if $line =~ /^\.\r?\n/;
1802 $a = $a->new unless ref $a;
1804 my $c = $a->read_until_dot($fh);
1806 ($a->{head}, $a->{body}) = split(/\r?\n\r?\n/, $c, 2);
1807 $a->{head} .= "\r\n";
1818 return map { $a->header($_) } @want;
1821 # looks like headers are case insensitive. see rfc 2822
1827 my ($a, $want, $set) = @_;
1831 $set =~ s/\r?\n?$//;
1833 if ($a->{head} =~ /^((?i)\Q$want\E)\s*:/msx) {
1835 =~ s/^((?i)\Q$want\E)\s*:\s?(.+?)$ \r?\n(?!\s)/$1: $set\r\n/msx;
1837 $a->{head} .= "$want: $set\r\n";
1843 =~ /^((?i)\Q$want\E)\s*:\s?(.+?)$ \r?\n(?!\s)/msx;
1845 return undef unless defined $2;
1848 $set =~ s/\r?\n?$//;
1853 =head3 number(@groups) returns number from the Xref header
1862 my ($a,@groups) = @_;
1864 my $xref = $a->header('Xref');
1865 return unless defined($xref);
1866 my %numbers = split /\S+|:/, $xref;
1867 return @numbers{@groups};
1870 =head2 ensure_header
1877 $a->header($h,$c) unless defined($a->header($h));
1878 return $a->header($h);
1881 # generate an RFC 5536 3.1.1 and RFC 5322 3.3 compliant date string
1887 my ($y, $m, $d, $hr, $min, $sec) = Date::Calc::Today_and_Now(1);
1889 return sprintf('%02d %s %04d %02d:%02d:%02d +0000', $d,
1890 substr(Date::Calc::Month_to_Text($m),0,3),
1891 $y, $hr, $min, $sec);
1900 my ($a, $host) = @_;
1901 $host ||= Sys::Hostname::hostname();
1902 return '<'.Data::UUID->new()->create_str().'@'.$host.'>';
1906 our @required_headers = qw(From Date Newsgroups Subject Message-ID Path);
1907 our @opt_headers = qw(Approved Archive Control Distribution Expires
1908 Followup-To Injection-Date Injection-Info Organization References Summary
1909 Supersedes User-Agent Xref);
1917 $a->header('Message-ID',@args);
1926 my $p = $a->header('Path',@args);
1927 return wantarray ? split(/\!/,$p) : $p;
1930 # TODO could do a bit less work here if a scalar is wanted
1940 $a->header('Newsgroups',join(',',@set));
1942 @set = split(/\s*,\s*/,$a->header('Newsgroups'));
1945 return wantarray ? @set : join(',',@set);
1948 # TODO make sure we ignore the RFC's requirements on approved headers
1949 # If you don't know why, then don't change this
1957 $a->header('Approved',@app);
1966 $a->header('Control',@arg);
1975 $path = Sys::Hostname::hostname() unless defined($path);
1977 $a->header('Path',"$path!". $a->header('Path'));
1982 Nathan Wagner, C<< <nw at hydaspes.if.org> >>
1986 Please report any bugs or feature requests to C<bug-net-server-nntp
1987 at rt.cpan.org>, or through the web interface at
1988 L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net::Server::NNTP>.
1989 I will be notified, and then you'll automatically be notified of
1990 progress on your bug as I make changes.
1994 You can find documentation for this module with the perldoc command.
1996 perldoc Net::Server::NNTP
1999 You can also look for information at:
2003 =item * RT: CPAN's request tracker
2005 L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Net::Server::NNTP>
2007 =item * AnnoCPAN: Annotated CPAN documentation
2009 L<http://annocpan.org/dist/Net::Server::NNTP>
2011 =item * CPAN Ratings
2013 L<http://cpanratings.perl.org/d/Net::Server::NNTP>
2017 L<http://search.cpan.org/dist/Net::Server::NNTP>
2025 L<Net::Server::MultiType>
2027 =head1 ACKNOWLEDGEMENTS
2029 Urs Janssen, maintainer of the tin newsreader, helped with this module by
2030 providing a series of prompt and detailed bug reports on the NNTP
2033 =head1 COPYRIGHT & LICENSE
2035 Written entirely from scratch by Nathan Wagner and released into the
2040 1; # End of Net::Server::NNTP