# $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-<org> 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 <LDAP server> in place of <test LDAP server>
# 09/13/98 - jed: filter program installed on <news server>
#---------------------------------------------------------------------------
#
# 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 = ("<LDAP server>","127.0.0.1","<LDAP server #3>");
  $news_server_host = "<news server>";
  $bindDN = "cn=<news server>,ou=Network Hosts,ou=<location>,o=<org>,c=US";
  $ldapcmd = "/usr/local/bin/ldapsearch -h";
  $newsbase = "cn=<newsgroup>,cn=Network News,ou=Services,ou=<location>,o=<org>,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 <userid@fromhost>
#          "user name" <userid@fromhost>
#          <userid@fromhost>
#          <userid@fromhost> (user name)
#
  ($from_userid,$from_host) = split(/@/,$hdr{"From"});
  if ($from_userid =~ /</) {
    ($dontcare,$from_userid) = split(/</,$from_userid)}
##  $result = `$logger "_from_userid:" "[$from_userid]"`;

  if ($from_host =~ /</) {($from_host,$dontcare) = split(/</,$from_host)}
  if ($from_host =~ />/) {($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="<certifiedAuthor>
#        search starts at 1 rather than 0 since [0] = <DN>
#        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-<org> 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>" newsgroup      # 
##################################################################
  if ($hdr{"Newsgroups"} eq "<newsgroup>") {
##    $result = `$logger _newsgroup = <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-<org> 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=<org>,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";
  }
}
