
spf at metro
Sep 8, 2004, 8:15 AM
Post #1 of 1
(857 views)
Permalink
|
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/ ------- To unsubscribe, change your address, or temporarily deactivate your subscription, please go to http://v2.listbox.com/member/?listname=spf-devel [at] v2
|