Mailing List Archive

SPF validator
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@v2.listbox.com