# $Id: filter_nnrpd.pl,v 1.4 1999/10/20 jed Exp $ # Note: requires Perl V5 and local installation of LDAP suite, though # (primary) LDAP server may, and probably should, be another host #--------------------------------------------------------------------------- # 04/21/00 - jed: reduce log writing # 02/28/00 - jed: change n504 email filter; add n522 LDAP errors # 12/05/99 - jed: change ldapcmd to OpenLDAP SLAPD;trace LDAP query RC's # 10/20/99 - jed: modify n501 to include check for "In-Reply-To:" # 05/25/99 - jed: comment out n516 for now; add multiple LDAP server queries # 12/21/98 - jed: add first section to reject postings with non- E-mail # return addresses (n515/n516) # 11/24/98 - jed: comment out N502 section; while a true/simple reply # should include References, a reply with a change of # subject may have a Subject line with no "Re: " tag; # while this section is complimentary to the N501 section, # and was intended to insure proper replies with proper # References, it otherwise negated the ability to change # the subject on a previous article # 11/23/98 - jed: add "_posting host" after "_from_host" log output # 11/17/98 - jed: add "spew_headers" subroutine and add to n502/503 # 10/01/98 - jed: updated to use in place of # 09/13/98 - jed: filter program installed on #--------------------------------------------------------------------------- # # This file is loaded when nnrpd starts up. If it defines a sub named # `filter_post', then that function will be called during processing of a # posting. It has access to the headers of the article via the associative # array `%hdr'. If it returns a null string then the article is accepted # for posting. A non-null stringrejects it, and the value returned is used # in the rejection message. # # sub filter_post { my $rval = "" ; # assume we'll accept. # # global defines # $logger = "/bin/logger -i -p news.notice"; $true = 0; $false = 1; # # LDAP global defines # # try the following LDAP servers in left-to-right order @ldaphost = ("","127.0.0.1",""); $news_server_host = ""; $bindDN = "cn=,ou=Network Hosts,ou=,o=,c=US"; $ldapcmd = "/usr/local/bin/ldapsearch -h"; $newsbase = "cn=,cn=Network News,ou=Services,ou=,o=,c=US"; # # ## $result = `$logger running filter_nnrp.pl-----------`; # # n501 - invalid followup/reply: missing the required # "References:" or "In-Reply-To:" header with reference msgid # # insist on a followup that contains: # 1) a Re:(ference) tag in the Subject header # 2) a References: header with reference msgid, -or- # 3) an In-Reply-To: header with reference msgid if ($hdr{"Subject"} =~ /^Re: /o) { if ($hdr{"References"} eq "" and $hdr{"In-Reply-To"} eq "") { $result = spew_headers(); $rval = "n501: Invalid followup/reply posting"; $result = `$logger "_rval:" '"'$rval'"'`; return ($rval); }} # # n502 - invalid followup/reply: missing "Re: " in "Subject:" header # # if ($hdr{"Subject"} !~ /^Re: /o and $hdr{"References"} ne ""){ # $result = spew_headers(); # $rval = "n502: Invalid followup/reply posting"; # $result = `$logger "_rval:" '"'$rval'"'`; return ($rval); # } # # extract userid and hostname from the "From:" header record, making # sure that they are properly extracted from the various possible "From:" # formats: # user name # "user name" # # (user name) # ($from_userid,$from_host) = split(/@/,$hdr{"From"}); if ($from_userid =~ //) {($from_host,$dontcare) = split(/>/,$from_host)} if ($from_host =~ /\(/) {($from_host,$dontcare) = split(/\(/,$from_host)} $from_host = lc($from_host); $duh = $from_host =~ s/ //g; ## $result = `$logger "_from_host:" "[$from_host]"`; ## $result = `$logger "_posting_host: ["'$hdr{"NNTP-Posting-Host"}'"]"`; # # n520 - get list of "valid senders" ('certifiedAuthor') from LDAP database # # if RC = 0 then everything is "hunkey-dorey"; if RC > 0, then an error occured # RC = 256: SLAPD not running or connections not accepted # $filter = "objectclass=*"; $attr = "certifiedAuthor"; $scope = "-s base"; $parm = ""; $base = "-b $newsbase"; @valid_senders = ldap_search(); if ($RC != 0) { $result = `$logger "___LDAPSEARCH for Valid Senders FAILED"`; $rval = "n520: Error with Directory Service"; $result = `$logger "_rval:" '"'$rval'"'`; return ($rval); } $valid_sender_count = scalar @valid_senders; ## $result = `$logger "_valid_sender_count,RC": "$valid_sender_count,$RC"`; # for ($i=0;$i<$#valid_senders+1;$i++) { # $result = `$logger "_valid_senders[$i]:" "[$valid_senders[$i]]"`;} # # n503 - do not allow E-mail return addresses to use any "authorized" News # userid's defined in NNRP.ACCESS on news server and "certifiedAuthor" # attribute in the newsgroup LDAP entry # # LDAPSEARCH result = "certifiedauthor=" # search starts at 1 rather than 0 since [0] = # also note that grep() is case sensitive; "lc" requires Perl 5 # reassign array elements for future testing (ie; "Sender:") # $is_using_certifiedauthorID = $false; $lc_from_userid = lc($from_userid); # $result = `$logger "[lc_from_userid]:" '["'$lc_from_userid'"]'`; for ($i=1;$i<$valid_sender_count;$i++) { ($dontcare,$valid_senderid) = split(/=/,$valid_senders[$i]); $lc_valid_senderid = lc($valid_senderid); # $result = `$logger "[lc_valid_senderid]:" '["'$lc_valid_senderid'"]'`; $valid_senders[$i] = $lc_valid_senderid; if ($lc_from_userid eq $lc_valid_senderid) { $is_using_certifiedauthorID = $true} } ## $result = `$logger "[is_using_certifiedauthorID]: [$is_using_certifiedauthorID]"`; if ($is_using_certifiedauthorID eq $true) { $rval = "n503: Invalid E-mail address"; $result = `$logger "_rval:" '"'$rval'"'`; return ($rval); } # # n515 - reject postings with non- E-mail return addresses # allowing for "...spam..." as a valid pseudo subdomain to let # users protect themselves from automatic inclusion in spam listings # # not completed # } # # n516 - reject postings from users for whom spamming complaints have # been received; this would probably be better as a database # lookup to better handle dynamically changing list and list size # # if ($lc_from_userid eq "steph\@aol.com") { # $result = spew_headers(); # $rval = "n516: Spamming Not Allowed At This Site"; # $result = `$logger "_rval:" '"'$rval'"'`; return ($rval); # } ################################################################## ## do more validation against posts to "" newsgroup # ################################################################## if ($hdr{"Newsgroups"} eq "") { ## $result = `$logger _newsgroup = `; # $result = spew_headers(); # # n504 - LDAP lookup on E-mail address # n522 - LDAP lookup error on E-mail address # # add code to perform an LDAP lookup of the poster's E-mail address # while this is not a foolproof security mechanism, it at least should # help to prevent someone using a non- E-mail address (most likely to # happen with a "spammer" posting) # # Note that while there may be more than one DS "mail" entry that will # match the LDAP lookup, only one is needed to verify the existence of # an valid DN with the appropriate "mail" attribute value; DN's other # than the first one will be in the returned array, but won't be used # Also note that multiple DN's will be seperated by blank lines # # valid mail attributes to search on: "mail", "otherMailbox" # $email_address = "$from_userid\@$from_host"; ## $result = `$logger _email_address: '"'$email_address'"'`; $base = "-b o=,c=US"; $scope = "-s sub"; $filter = "mail=$email_address"; $parm = ""; $attr = "cn"; ## $result = `$logger _filter: '"'$filter'"'`; @ldapvalues = ldap_search(); if ($RC != 0) { $result = `$logger "___LDAPSEARCH for Valid Senders FAILED"`; $rval = "n522: Error with Directory Service"; $result = `$logger "_rval:" '"'$rval'"'`; return ($rval); } chomp @ldapvalues; ## $result = `$logger "_ldapvalues index: [$#ldapvalues]"`; # for ($i=0;$i<$#ldapvalues+1;$i++) { # $result = `$logger "_ldapvalues[$i]:" "[$ldapvalues[$i]]"`;} if ($ldapvalues[0] eq "-1") {$userDN = ""} else { $userDN = $ldapvalues[0]; ($userRDN) = $userDN =~ /^cn=(\w+\s*\w*\s*\w*\s*\w*),/; } ## $result = `$logger "_userRDN(mail)": '"'$userRDN'"'`; if ($userRDN eq "") { $filter = "othermailbox=$email_address"; ## $result = `$logger _filter: '"'$filter'"'`; @ldapvalues = ldap_search(); if ($RC != 0) { $result = `$logger "___LDAPSEARCH for Valid Senders FAILED"`; $rval = "n522: Error with Directory Service"; $result = `$logger "_rval:" '"'$rval'"'`; return ($rval); } chomp @ldapvalues; ## $result = `$logger "_ldapvalues index: [$#ldapvalues]"`; if ($ldapvalues[0] eq "-1") {$userDN = ""} else { $userDN = $ldapvalues[0]; ($userRDN) = $userDN =~ /^cn=(\w+\s*\w*\s*\w*\s*\w*),/; } ## $result = `$logger "_userRDN(otherMail)": '"'$userRDN'"'`; if ($userRDN eq "") { $rval = "n504: Invalid E-mail address"; $result = `$logger "_rval:" '"'$rval'"'`; return ($rval); } } # # n505 - validate Message-ID host against: NNTP-Posting-Host, From hostname, # or News server hostname # # if no match, then Message-ID is considered bogus, implying a # bogus posting - reject # ($dontcare,$MsgID_host) = split(/@/,$hdr{"Message-ID"}); $MsgID_host =~ s/>//; ## $result = `$logger "_MsgID_host:" "$MsgID_host"`; $posting_host = $hdr{"NNTP-Posting-Host"}; ## $result = `$logger "_posting_host:" "$posting_host"`; ## $result = `$logger "_news_server_host:" "$news_server_host"`; $OK_MsgID_host = $false; if ($posting_host eq $MsgID_host) {$OK_MsgID_host = $true} elsif ($from_host eq $MsgID_host) {$OK_MsgID_host = $true} elsif ($news_server_host eq $MsgID_host) {$OK_MsgID_host = $true} ## $result = `$logger "_OK_MsgID_host:" "$OK_MsgID_host"`; if ($OK_MsgID_host eq $false) { $rval = "n505: Invalid News posting host"; $result = `$logger "_rval:" '"'$rval'"'`; return ($rval); } # # See if the posting is a reply/followup or an original post # If it is an original posting (ie; no "References:" header) # then don't do anything more and let the posting go through # Otherwise, verify the authority of the poster - must be from either # authorized host or authorized News userid # if ($hdr{"References"} ne "") { # # check on authorized/authenticated sender first # MUST have a "Sender:" header record or user not authenticated # "Sender:" header enforced above by not allowing a "From" header # record with an authorized userid in it, and by INN configuration # $is_authorized_sender = $false; if ($hdr{"Sender"} ne "") { ($senderID,$senderHOST) = split(/@/,$hdr{"Sender"}); $lc_senderID = lc($senderID); ## $result=`$logger "_lc(senderID): $lc_senderID"`; for ($i=1;$i<$valid_sender_count;$i++) { ## $result=`$logger "_valid_senders[$i]: $valid_senders[$i]"`; if ($lc_senderID eq $valid_senders[$i]) { $is_authorized_sender = $true} } } $result = `$logger _is_authorized_sender: $is_authorized_sender`; # # n521 - if poster is not an "authorized user", see if they are posting from # an "authorized host" - if yes, then accept reply posting # # simply do an LDAP search on the newsgroup entry with the posting host in the # search filter as a possible authorized host # # if NO data is returned, then that means that the search failed and the # specified posting host is NOT an authorized reply posting host # if ($is_authorized_sender eq $false) { $filter = "certifiedhost=$posting_host"; $attr = "certifiedHost"; $scope = "-s base"; $base = "-b $newsbase"; $parm = "-A"; ## $result=`$logger "_certifiedhost filter:" "$filter"`; @valid_host = ldap_search(); if ($RC != 0) { $result = `$logger "___LDAPSEARCH for Valid Hosts FAILED"`; $rval = "n521: Error with Directory Service"; $result = `$logger "_rval:" '"'$rval'"'`; return ($rval); } } ## $result = `$logger "_valid_host[0],RC": '"'$valid_host[0],$RC'"'`; # # n510 - test for both sender and host being invalid # if ($valid_host[0] eq "" && $is_authorized_sender eq $false) { $rval = "n510: You are not authorized to post a followup reply"; } } } ## $result = `$logger "__rval:" '"'$rval'"'`; $rval; ######################################################################### # convert all occurances of quote marks in header values to asterisks and # log all header records # sub spew_headers { foreach $key (sort keys %hdr) { $val = $hdr{$key}; $val =~ s/\'/*/g; $val =~ s/\"/*/g; $result = `$logger $key: '$val'`; } } ######################################################################## # perform LDAP search over range of LDAP servers in ldaphost array # stop searching after first positive response (RC = 0) # trap error messages so users don't see them # sub ldap_search { open(STDERR,">/tmp/LDAP_search_errors"); for ($i=0;$i<$#ldaphost+1;$i++) { ## $result=`$logger "_ldaphost:" "$ldaphost[$i]"`; @result = `$ldapcmd $ldaphost[$i] $parm $scope "$base" "$filter" $attr`; $RC = $?; if ($RC == 0) {close(STDERR); chomp @result; return @result} else {$result=`$logger "__ldapRC:" "$RC"`;} } close(STDERR); return "-1"; } }