spf-discuss
[Top] [All Lists]

SPF validator

2004-09-08 08:15:20
All,

I have just written an SPF record validator, the source of which follows
below this message. It is online at

        http://spf.sonologic.nl/

If you want to refer to this validator service on your page, simply copy 
the form below somewhere appropriate (edit to suit your needs):

<FORM METHOD="GET" ACTION="validate.php">
<CENTER>
  <INPUT TYPE="TEXT" SIZE="80" NAME="record">

  <INPUT TYPE="SUBMIT" VALUE="Validate">
</CENTER>
</FORM>

Please do send me feedback on what is wrong with it!

Koen

------------------------------------------------------------------------
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<HTML>
  <HEAD>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <link REL="SHORTCUT ICON" HREF="favicon.ico">
    <TITLE>SPF record validator</TITLE>
  </HEAD>
 <BODY>
<?php

/*
 *      SPF record validator, validates SPF records against the draft
 *      Copyright (C) 2004, Sonologic
 *
 *      This program is free software; you can redistribute it and/or modify 
 *      it under the terms of the GNU General Public License as published by 
 *      the Free Software Foundation; either version 2 of the License, or (at
 *      your option) any later version.
 *
 *      This program is distributed in the hope that it will be useful, but 
 *      WITHOUT ANY WARRANTY; without even the implied warranty of 
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 *      General Public License for more details.
 *
 *      You should have received a copy of the GNU General Public License 
 *      along with this program; if not, write to the Free Software Foundation,
 *      Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *          
 */


function is_macro_string($str) {

/*
    macro-string = *( macro-char / VCHAR )
    macro-char   = ( "%{" ALPHA transformer *delimiter "}" )
                   / "%%" / "%_" / "%-"
    transformer  = [ *DIGIT ] [ "r" ]
*/
  $macrochar="(%\{[a-zA-Z][0-9]*r?\})|(%%)|(%_)|(%-)";
  $vchar="[\x21-\x7E]";
  $exp="/^(($macrochar)|($vchar))*$/";
  
  return preg_match($exp,$str);
}


function is_domain_name($str) {
/*
<label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]

<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>

<let-dig-hyp> ::= <let-dig> | "-"

<let-dig> ::= <letter> | <digit>

<letter> ::= any one of the 52 alphabetic characters A through Z in
upper case and a through z in lower case

<digit> ::= any one of the ten digits 0 through 9

*/

  $prt="[a-zA-Z_](([a-zA-Z0-9]|-|_)*[a-zA-Z0-9])?";
  $rexp="/^$prt(\.$prt)*\.?$/";

  return preg_match($rexp,$str);
}

function is_domain_spec($str) {
  return is_macro_string($str) || is_domain_name($str);
}


function is_ip6($str) {
        // ipv6 regexp due to 'nico at kamensek dot de' in the php documentation

  $pattern1 = '([A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}';
  $pattern2 = '[A-Fa-f0-9]{1,4}::([A-Fa-f0-9]{1,4}:){0,5}[A-Fa-f0-9]{1,4}';
  $pattern3 = '([A-Fa-f0-9]{1,4}:){2}:([A-Fa-f0-9]{1,4}:){0,4}[A-Fa-f0-9]{1,4}';
  $pattern4 = '([A-Fa-f0-9]{1,4}:){3}:([A-Fa-f0-9]{1,4}:){0,3}[A-Fa-f0-9]{1,4}';
  $pattern5 = '([A-Fa-f0-9]{1,4}:){4}:([A-Fa-f0-9]{1,4}:){0,2}[A-Fa-f0-9]{1,4}';
  $pattern6 = '([A-Fa-f0-9]{1,4}:){5}:([A-Fa-f0-9]{1,4}:){0,1}[A-Fa-f0-9]{1,4}';
  $pattern7 = '([A-Fa-f0-9]{1,4}:){6}:[A-Fa-f0-9]{1,4}';

  $full = 
"/^($pattern1)$|^($pattern2)$|^($pattern3)$|^($pattern4)$|^($pattern5)$|^($pattern6)$|^($pattern7)$/";

  return preg_match($full,$str);
}

function is_ip4($str) {
  return 
preg_match("/^([0-9]{1,2}|[01][0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]{1,2}|[01][0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]{1,2}|[01][0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]{1,2}|[01][0-9]{2}|2[0-4][0-9]|25[0-5])$/",$str);
}

function is_ip($str) {
  return is_ip4($str) || is_ip6($str);
}

if(isset($_POST['record']) || isset($_GET['record'])) {
  $numerrors=0;
  $numwarnings=0;

  if(!isset($_POST['record'])) {
    $_POST['record']=$_GET['record'];
  }

  system("echo -n \"".$_POST['record']."\" >> 
/usr/local/www/sites/sonologic/spf/log/spf-validate");

  $record=explode(' ',$_POST['record']);
  if(preg_match("/^v=(.*)$/",$record[0],$version)) {
    if($version[1]=="spf2.0/pra") {
      $warning[$numwarnings]="The record uses 'spf2.0/pra' as a version string. 
Although this is part of the SenderID standard, there is no implenentation that 
checks these records yet and you are encouraged to use 'spf1' for the time 
being.";
      $warningpos[$numwarnings++]=2;
    } else if($version[1]!="spf1") {
      $error[$numerrors]="This is not a valid version string. Use 'spf1' 
instead.";
      $errorpos[$numerrors++]=2;
    } 

    for($i=1;$i<count($record);$i++) {
//      echo "check ".$record[$i]."<BR>";
      if(preg_match("/^(\+|-|~|\?)(.*)$/",$record[$i],$match)) {
        $prefix=$match[1]; $directive=$match[2];
      } else {
        $prefix=''; $directive=$record[$i];
      }

//      echo "prefix [$prefix] dir [$directive]<BR>\n";

      $pos=0;
      for($j=0;$j<$i;$j++) {
        $pos+=strlen($record[$j])+1;
      }

      
if(preg_match("/^(.*?)(=|:)(.*)$/",$directive,$match,PREG_OFFSET_CAPTURE)) {
        $lhs=$match[1][0]; $rhs=$match[3][0];
        $conn=$match[2][0];
        $lhspos=$pos+$match[1][1]+strlen($prefix); 
$rhspos=$pos+$match[3][1]+strlen($prefix);
        $connpos=$pos+$match[2][1]+strlen($prefix);
      } else {
        $lhs=$directive; $conn=$rhs='';
        $connpos=$rhspos=-1;
        $lhspos=$pos+strlen($prefix);
      }

//      echo "lhs [$lhs] ($lhspos) conn [$conn] $connpos rhs [$rhs] 
($rhspos)<BR>";

                // do checking

      if($conn=='=') {
                // modifiers
        if($prefix!='') {
          $error[$numerrors]="Modifiers don't take a prefix";
          $errorpos[$numerrors++]=$pos;
        }
        if($lhs!="redirect" && $lhs!="exp") {
          $warning[$numwarnings]="Unknown modifier";
          $warningpos[$numwarnings++]=$lhspos;
        }
      } else {
                // mechanisms
        switch(strtolower($lhs)) {
          case 'all':
            if($conn!='') {
              $error[$numerrors]="all does not take any arguments";
              $errorpos[$numerrors++]=$connpos;
            }
            break;
          case 'include':
            if($conn=='' || $rhs=='') {
              $error[$numerrors]="include needs an argument, eg. 
include:somedomain.com";
              $errorpos[$numerrors++]=$lhspos+7;
            } else {
              if(!is_domain_spec($rhs)) {
                $error[$numerrors]="invalid domain name or macro";
                $errorpos[$numerrors++]=$rhspos;
              }
            }
            break;
          case 'a':
            if(is_ip($rhs)) {
              $error[$numerrors]="$rhs looks like an ip to me, while the a 
mechanism needs a domain name, perhaps you meant to use ip4 or ip6 here?";
              $errorpos[$numerrors++]=$rhspos;
            } else if($conn!='' && $rhs=='') {
              $error[$numerrors]="no argument specified";
              $errorpos[$numerrors++]=$rhspos;
            } else if($conn!='') {
              if(preg_match("/^(.*?)\/(.*)$/",$rhs,$cidrmatch)) {
                $spec=$cidrmatch[1]; $cidr=$cidrmatch[2];
              } else {
                $spec=$rhs; $cidr='';
              }
              if(!is_domain_spec($spec)) {
                $error[$numerrors]="invalid domain name or macro";
                $errorpos[$numerrors++]=$rhspos;
              }
              if($cidr!='') {
//                  dual-cidr-length = [ ip4-cidr-length ] [ "/" 
ip6-cidr-length ]
                if(!preg_match("/^([0-9]{1,})(\/[0-9]{1,})?$/",$cidr)) {
                  $error[$numerrors]="invalid cidr-length specification";
                  $errorpos[$numerrors++]=$rhspos+strlen($spec);
                }
              }
            }
            break;
          case 'mx':
            if(is_ip($rhs)) {
              $error[$numerrors]="$rhs looks like an ip to me, while the a 
mechanism needs a domain name, perhaps you meant to use ip4 or ip6 here?";
              $errorpos[$numerrors++]=$rhspos;
            } else if($conn!='' && $rhs=='') {
              $error[$numerrors]="no argument specified";
              $errorpos[$numerrors++]=$rhspos;
            } else if($conn!='') {
              if(preg_match("/^(.*?)\/(.*)$/",$rhs,$cidrmatch)) {
                $spec=$cidrmatch[1]; $cidr=$cidrmatch[2];
              } else {
                $spec=$rhs; $cidr='';
              }
              if(!is_domain_spec($spec)) {
                $error[$numerrors]="invalid domain name or macro";
                $errorpos[$numerrors++]=$rhspos;
              }
              if($cidr!='') {
//                  dual-cidr-length = [ ip4-cidr-length ] [ "/" 
ip6-cidr-length ]
                if(!preg_match("/^([0-9]{1,})(\/[0-9]{1,})?$/",$cidr)) {
                  $error[$numerrors]="invalid cidr-length specification";
                  $errorpos[$numerrors++]=$rhspos+strlen($spec);
                }
              }
            }
            break;
          case 'ptr':
            if(is_ip($rhs)) {
              $error[$numerrors]="$rhs looks like an ip to me, while the a 
mechanism needs a domain name, perhaps you meant to use ip4 or ip6 here?";
              $errorpos[$numerrors++]=$rhspos;
            } else if($conn!='' && $rhs=='') {
              $error[$numerrors]="no argument specified";
              $errorpos[$numerrors++]=$rhspos;
            } else if($conn!='') {
              if(!is_domain_spec($rhs)) {
                $error[$numerrors]="invalid domain name or macro";
                $errorpos[$numerrors++]=$rhspos;
              }
            }
            break;
          case 'ip4':
            if(preg_match("/^(.*?)\/(.*)$/",$rhs,$ipmatch)) {
              $ip=explode('.',$ipmatch[1]); $cidr=$ipmatch[2]; 
            } else {
              $ip=explode('.',$rhs); $cidr='';
            }
            if($cidr=='' && count($ip)!=4) {
                $error[$numerrors]="no cidr-length given, specify all four of 
the numbers of the dot-quad ip number";
                $errorpos[$numerrors++]=$rhspos;
            }
            for($k=0;$k<count($ip);$k++) {
                if(!preg_match("/^[0-9]+$/",$ip[$k])) {
                  $error[$numerrors]="invalid ip address";
                  $errorpos[$numerrors++]=$rhspos;
                  $k=count($ip);
                }
            }
            if($cidr!='') {
              if(preg_match("/^[0-9]+/",$cidr)) {
                $nomatch=0;
                if(($cidr+0)>24 && count($ip)<4) $nomatch=1;
                if(($cidr+0)>16 && count($ip)<3) $nomatch=1;
                if(($cidr+0)>8 && count($ip)<2) $nomatch=1;
                if(($cidr+0)>0 && count($ip)<1) $nomatch=1;
                if($nomatch) {
                  $error[$numerrors]="not enough numbers specified for the 
given cidr length";
                  $errorpos[$numerrors++]=$rhspos;
                }
              } else {
                $error[$numerrors]="invalid cidr-length specification";
                $errorpos[$numerrors++]=$rhspos+strlen($rhs)-strlen($cidr);
              }
            }
            break;
          case 'ipv4':
            $error[$numerrors]="ipv4 is not a known mechanism, did you mean 
ip4?";
            $errorpos[$numerrors++]=$lhspos;
            break;
          case 'ip6':
            if(preg_match("/^(.*?)\/(.*)$/",$rhs,$ipmatch)) {
              $ip=explode(':',$ipmatch[1]); $cidr=$ipmatch[2];
            } else {
              $ip=explode(':',$rhs); $cidr='';
            }
            $empty=0;
            for($k=0;$k<count($ip);$k++) {
              if($ip[$k]=='') {
                $empty++;
                if($empty>1 && $k!=1) {
                  $error[$numerrors]="only one :: may occur in an ipv6 address";
                  $errorpos[$numerrors++]=$rhspos;
                  $k=count($ip);
                }
              } else if( ($k==(count($ip)-1)) && is_ip4($ip[$k])) {
                        // ignore, this is ok as per rfc 3513, sec 2.2, ad 3
              } else if(!preg_match("/^[0-9a-fA-F]{1,4}$/",$ip[$k])) {
                $error[$numerrors]="invalid ipv6 address";
                $errorpos[$numerrors++]=$rhspos;
                $k=count($ip);
              }
        
            }
            if($cidr!='' && !preg_match("/^[0-9]+$/",$cidr)) {
                $error[$numerrors]="invalid cidr-length specification";
                $errorpos[$numerrors++]=$rhspos+strlen($rhs)-strlen($cidr);
            }
            break;
          case 'ipv6':
            $error[$numerrors]="ipv6 is not a known mechanism, did you mean 
ip4?";
            $errorpos[$numerrors++]=$lhspos;
            break;
          case 'exists':
            if(!is_domain_spec($rhs)) {
              $error[$numerrors]="invalid domain name or macro";
              $errorpos[$numerrors++]=$rhspos;
            } 
            break;
          default:
            $warning[$numwarnings]="Unknown extension";
            $warningpos[$numwarnings++]=$lhspos;
            break;
        }
      }
                

//      echo "<HR>";
    }
  } else {
    $error[$numerrors]="The record does not start with 'v=spf1' and therefore 
can not be an spf record.";
    $errorpos[$numerrors++]=0;
  }
  
  // now display errors

  echo "<CENTER><H2>";
  if($numerrors==0) {
    echo "Valid!<BR>\n";
    system("echo \" is valid\" >> 
/usr/local/www/sites/sonologic/spf/log/spf-validate");
  } else {
    echo "NOT valid";
    system("echo \" is NOT valid\" >> 
/usr/local/www/sites/sonologic/spf/log/spf-validate");
  }
  echo "</H2></CENTER>\n";
  echo "<HR>\n";

  echo "The SPF record\n<PRE>".$_POST['record']."</PRE>\nwas found to be ";
  if($numerrors==0) {
    echo "valid, congratulations. Note that this does not mean that the ";
    echo "record is <b>correct</b>, i.e. that it indeed describes all the ";
    echo "hosts that you wanted to include. To check whether the record is ";
    echo "correct, use the spfquery tool included in most popular SPF 
implementations (";
    echo 'including but not limited to <A 
HREF="http://www.libspf2.org";>libspf2</A>, ';
    echo '<A HREF="http://www.libspf.org";>libspf</A> and ';
    echo '<A HREF="http://spf.pobox.com/downloads.html";>Mail::SPF::Query</A>.';
  } else {
    echo "invalid. You might want to check the errors and warnings below. If 
you ";
    echo "need more information please check:\n";
    echo "<DL>\n";
    echo ' <DD><A 
HREF="http://spf.pobox.com/mechanisms.html";>Documentation</A></DD>';
    echo ' <DD><A 
HREF="http://spf.pobox.com/rfcs.html";>Specifications</A></DD>';
    echo ' <DD><A HREF="http://spf.pobox.com/faq.html";>Frequently asked 
questions</A></DD>';
    echo '</DL>If all else fails, you might want to ask on ';
    echo '<A HREF="http://spf.pobox.com/mailinglist.html";>the spf-help mailing 
list</A>.';
  }

  echo "<HR>"; 

  for($i=0;$i<strlen($_POST['record']);$i+=50) {
    for($j=0;$j<$numwarnings;$j++) {
      if($warningpos[$j]>=$i && $warningpos[$j]<$i+50) {
        echo "WARNING:";
        echo "<PRE>\n";
        echo substr($_POST['record'],$i,50)."\n";
        echo str_pad("",$warningpos[$j]-$i)."^\n";
        echo "</PRE>\n";
        echo $warning[$j]."<BR><BR>\n";
      }
    }
  }
  for($i=0;$i<strlen($_POST['record']);$i+=50) {
    for($j=0;$j<$numerrors;$j++) {
      if($errorpos[$j]>=$i && $errorpos[$j]<$i+50) {
        echo "ERROR:";
        echo "<PRE>\n";
        echo substr($_POST['record'],$i,50)."\n";
        echo str_pad("",$errorpos[$j]-$i)."^\n";
        echo "</PRE>\n";
        echo $error[$j]."<BR><BR>\n";
      }
    }
  }

} else {
  echo "Usage error!<BR>\n";
}

?>

</BODY></HTML>

------------------------------------------------------------------------


-- 
K.F.J. Martens, Sonologic, http://www.sonologic.nl/
Networking, embedded systems, unix expertise, artificial intelligence.
Public PGP key: http://www.metro.cx/pubkey-gmc.asc
Wondering about the funny attachment your mail program
can't read? Visit http://www.openpgp.org/

-------
Archives at http://archives.listbox.com/spf-help/current/
Donate! http://spf.pobox.com/donations.html
To unsubscribe, change your address, or temporarily deactivate your 
subscription, 
please go to 
http://v2.listbox.com/member/?listname=spf-help(_at_)v2(_dot_)listbox(_dot_)com


<Prev in Thread] Current Thread [Next in Thread>