#include <u.h>
#include <libc.h>
#include <bio.h>
#include <ctype.h>
#include <ip.h>
//check host error codes
enum {
None,
Pass,
Neutral,
Fail,
SoftFail,
TempError,
PermError
};
char *statwords[]={
[None] "None",
[Pass] "Pass",
[Neutral] "Neutral",
[Fail] "Fail",
[SoftFail] "SoftFail",
[TempError] "TempError",
[PermError] "PermError"
};
// lex status
enum {
TERM,
MORE,
DONE,
ERROR
};
enum {
NONE,
PLUS,
MINUS,
QMARK,
TILDE,
DDOT,
UNKNOWN,
VERSION,
SID,
REDIRECT,
EXP,
ALL,
INCLUDE,
A,
MX,
PTR,
IP4,
IP6,
EXISTS,
EXPL,
END,
SPF,
TXT
};
char *Keywords[]={
[NONE] " ",
[PLUS] "+",
[MINUS] "-",
[QMARK] "?",
[TILDE] "~",
[DDOT] ":",
[UNKNOWN] "unknown",
[VERSION] "v=",
[SID] "spf2.0/pra",
[REDIRECT] "redirect",
[EXP] "exp",
[ALL] "all",
[INCLUDE] "include",
[A] "a",
[MX] "mx",
[PTR] "ptr",
[IP4] "ip4",
[IP6] "ip6",
[EXISTS] "exists",
[EXPL] "exp",
[END] "end",
[SPF] "spf",
[TXT] "txt",
};
#define MAXDIGITS 4
#define MAXTOKEN 128
#define MAXTERM 1000
#define MAXVAL 100
#define MAXDEEP 10
#define MAXQUERY 50
typedef struct Symbol Symbol;
typedef struct Val Val;
struct Val {
uchar ip[16];
uchar mask[16];
char s[MAXVAL];
char n[MAXVAL];
};
struct Symbol {
int pos; // position
int len;
int ismacro;
int isdomainspec;
int op; // mechanism
int q; // qualifier
int m; // modifier
int err; // error message in val
int n; // number of val
char t[MAXVAL];
Val *v;
};
Symbol Z={0,0,0,0,0,0,0,0,0,nil};
char *macrovars[11];
int DEBUG=0;
int WHITELIST=0;
int chkdeep;
int check_host(uchar *ip, char *domain, int deep);
/* utils */
void*
emalloc(ulong sz)
{
void *v;
if((v=malloc(sz)) == nil) {
fprint(2, "out of memory allocating %lud\n", sz);
exits("mem");
}
memset(v, 0, sz);
setmalloctag(v, getcallerpc(&sz));
return v;
}
void*
erealloc(void *v, ulong sz)
{
void *nv;
if((nv=realloc(v, sz)) == nil) {
fprint(2, "out of memory allocating %lud\n", sz);
exits("mem");
}
if(v == nil)
setmalloctag(nv, getcallerpc(&v));
setrealloctag(nv, getcallerpc(&v));
return nv;
}
void
debug(char *fmt, ... )
{
va_list arg;
char buf[20000];
if ( ! DEBUG )
return;
va_start(arg, fmt);
vseprint(buf, buf+20000, fmt, arg);
va_end(arg);
fprint(2,"%s\n",buf);
}
/* give to parser the next element
thanks quintile :) */
int
lex(char *src, char **dstp, int *pos, int *len)
{
char *s, *d;
s = src + *pos;
while(isspace(*s))
s++;
if(*s =='\0' || *s == '\n')
return DONE;
d = *dstp;
while(isprint(*s) && !isspace(*s)){
if((d - *dstp) >= MAXTERM-1)
sysfatal("lex: corrupt SPF record - term too long\n");
*d = (isupper(*s))? tolower(*s): *s;
s++;
d++;
}
*d = 0;
*len = d - *dstp;
*pos = s - src;
return MORE;
}
int
match(char *s)
{
for(int i=0;i<END;i++)
if (strncmp(Keywords[i],s,strlen(Keywords[i])) == 0)
return i;
return UNKNOWN;
}
int
expandip(Symbol *pts)
{
char *hascidr;
int v4;
Symbol *S;
uchar ip[16];
uchar mask[6];
S = pts;
debug("expandip(): S->n = %d",S->n);
for(int i=0;i<S->n;i++) {
memset(mask,0xff,16);
if ( strlen(S->v[i].s) == 0 ) {
debug("expandip(): zero length value found: %s",S->v[i].s);
return PermError;
}
hascidr=strchr(S->v[i].s,'/');
v4=parseip(ip,S->v[i].s);
if(hascidr != nil)
parseipmask(mask,hascidr);
if (v4 != 6)
v4tov6(S->v[i].mask,mask);
else
memmove(S->v[i].mask,mask,16);
memmove(S->v[i].ip,ip,16);
}
return None;
}
int
isanip(char *name) {
uchar isip[16];
int len;
if ( name == nil )
return 0;
len=strlen(name);
if ( parseip(isip,name)!=0 && isdigit(name[0]) && isdigit(name[len-1]) )
return 1;
return 0;
}
char*
getaddress(char *str) {
char *p;
int len;
if ( str == nil )
return nil;
len=strlen(str);
if ( len < 3)
return nil;
p=str+len-2; // skipo \0 and \n from str
while ( p != str && !isspace(*p))
p--;
if ( isspace(*p) )
p++;
return strdup(p);
}
/* it will return up to nl lines */
int
ress(char *q, char **lines, int nl) {
int fd;
int i,n;
char buf[1024];
if ( (fd=open("/net/dns", ORDWR)) < 0 )
return -1;
seek(fd, 0, 0);
if(write(fd, q, strlen(q)) < 0) {
snprint(buf,1024,"%r");
close(fd);
if ( strstr(buf,"dns: name does not exist") != 0 )
return None;
else
return -1;
}
seek(fd, 0, 0);
i=0;
while((n=read(fd, buf,sizeof(buf))) > 0 && i<nl) {
buf[n]=0;
if ( n < 10 ) /* truncate the /net/dns buffer */
break;
lines[i] = strdup(buf);
i++;
}
close(fd);
return i;
}
/* code from ndb/dnsquery */
char*
ptrq(char *s) {
int len;
char line[1024];
char buf[1024];
char *p;
char *np;
strncpy(line,s,1024);
strncat(line," ptr",4);
for(p = line; *p; p++)
if(*p == ' '){
*p = '.';
break;
}
np = buf;
len = 0;
while(p >= line){
len++;
p--;
if(*p == '.'){
memmove(np, p+1, len);
np += len;
len = 0;
}
}
memmove(np, p+1, len);
np += len;
strcpy(np, "in-addr.arpa");
strcpy(line, buf);
return strdup(line);
}
int
addip(Symbol *S, char *addr, char *cidr, char *n)
{
S->n++;
debug("addip() S->n == %d",S->n);
if ( S->n == 1 )
S->v = emalloc(sizeof(Val));
else
S->v = erealloc(S->v,sizeof(Val)*S->n);
if (cidr)
snprint(S->v[S->n-1].s,MAXVAL,"%s/%s",addr,cidr);
else
snprint(S->v[S->n-1].s,MAXVAL,"%s",addr);
if (n)
snprint(S->v[S->n-1].n,MAXVAL,"%s",n);
return None;
}
int
symbres(Symbol *S, char *str, int deep)
{
int j,i;
char *query, *hascidr=nil;
char *names[MAXQUERY];
char *addr;
debug("symbress(): deep = %d",deep);
if ( deep > MAXDEEP)
return None;
if ( deep > 0 )
query=smprint("%s ip\n",str);
else {
if ( hascidr=strchr(str,'/') ) {
*hascidr=0;
hascidr++;
}
switch(S->m) {
case A: query=smprint("%s ip\n",str); break;
case MX: query=smprint("%s mx\n",str); break;
case PTR: query=smprint("%s ptr\n",ptrq(str)); break;
case EXISTS:
case NONE: query=smprint("%s ip\n",str); break;
case SPF: query=smprint("%s spf\n",str); break;
case TXT: query=smprint("%s txt\n",str); break;
default:
debug("symbres(): unknown operation %d\n",S->m);
return PermError;
}
}
i=ress(query,names,MAXQUERY);
for(j=0;j<i;j++) {
addr=getaddress(names[j]);
debug("symbres(): names[%d] = %s query = %s",j,addr,query);
if (isanip(addr))
addip(S,addr,hascidr,str);
else
symbres(S,addr,deep+1);
free(addr);
free(names[j]);
}
free(query);
return None;
}
char*
getrules(char *name,int op)
{
char *p,*r;
char *query;
char *s[1];
switch(op) {
case SPF: query=smprint("%s spf\n",name); break;
case TXT: query=smprint("%s txt\n",name); break;
default:
debug("resolve(): unknown operation %d\n",op);
return nil;
}
if ( ress(query,s,1) < 1 ) {
free(query);
return nil;
}
p=s[0];
while (*p != ' ')
p++;
r=strdup(p);
free(s[0]);
free(query);
if ( strstr(r,Keywords[VERSION]) != 0 || strstr(r,Keywords[SID]) != 0 )
return r;
debug("getrules(): rules == %s",r);
return nil;
}
char*
expandmacro(char *str)
{
Fmt fmt;
char *p,*start;
char *delim=nil;
char digits[MAXDIGITS];
int j,n,ntok, var;
int numdigits, isreverse;
char *tok[MAXTOKEN];
char *aux;
if ( str == nil || strlen(str) == 0 )
return nil;
p=str;
fmtstrinit(&fmt);
while(*p != '\0') {
/* literals as rfc says. . . .*/
if (*p == '%' && *(p+1) == '_') {
fmtprint(&fmt," ");
p+=2;
}
if (*p == '%' && *(p+1) == '-') {
fmtprint(&fmt,"%%20");
p+=2;
}
if (*p == '%' && *(p+1) == '%') {
fmtprint(&fmt,"%%");
p+=2;
}
/* macro start */
if (*p == '%' && *(p+1) == 123) { // 123 is the open-curly ascii
p+=2;
start=p;
if (*p == '\0') {
debug("expandmacro(): premature end found");
return nil;
}
/* reset values for each macro */
numdigits=0; var=-1; isreverse=0;
while (*p != 125 && *p != '\0') { // 125 is the closed-curly
if ((p-start) == 0) {
switch(*p){
case 's': var=0; break; /* sender string */
case 'l': var=1; break; /* local-part of sender */
case 'o': var=2; break; /* domain of sender */
case 'd': var=3; break; /* domain */
case 'i': var=4; break; /* ip */
case 'p': var=5; break; /* ip validated domain name */
case 'h': var=6; break; /* HELO/EHLO domain */
case 'c': var=7; break; /* SMTP client IP */
case 'r': var=8; break; /* checker domain name */
case 't': var=9; break; /* current timestamp */
case 'v': var=10; break; /* in-addr or ip6 string */
}
}
/* macro transformer */
/* number of elems to print */
if ((p-start) > 0) {
if ((*p >= '0' && *p <= '9') && numdigits < (MAXDIGITS-1)) {
digits[numdigits]=*p;
numdigits++;
}
if (*p == 'r')
isreverse=1;
}
if ((p-start) > (numdigits+isreverse)) {
switch(*p){
case '.':
case '-':
case '+':
case ',':
case '/':
case '_':
case '=':
delim=smprint("%c",*p);
break;
}
}
p++;
}
/* macro end */
digits[numdigits]='\0';
ntok=atoi(digits);
p++; // skip the closing curly
if (var == -1) {
return nil;
} else {
if (delim == nil)
delim=smprint(".");
aux=smprint("%s",macrovars[var]);
n=getfields(aux, tok, MAXTOKEN, 1, delim);
if (ntok > n || ntok == 0) ntok=n;
if (ntok == 0)
fmtprint(&fmt,"%s",macrovars[var]);
else {
/* If transformers or delimiters are provided, the replacement value for
a macro letter is split into parts. After performing any reversal
operation and/or removal of left-hand parts, the parts are rejoined
using "." and not the original splitting characters. */
if (isreverse) {
for(j=(n-1); j>=(n-ntok); j--)
fmtprint(&fmt,"%s%s",tok[j], (j > (n-ntok)) ? ".": "");
} else {
for(j=0; j<ntok; j++)
fmtprint(&fmt,"%s%s",tok[j], (j == (ntok-1)) ? "" :".");
}
}
free(aux);
}
free(delim);
delim=nil;
}
fmtprint(&fmt,"%c",*p);
p++;
}
return fmtstrflush(&fmt);
}
/* parse term and fill the symbol value */
int
parse(Symbol **pts, char *term)
{
int len, stat;
char *ptr, *aux, *macro;
Symbol *S;
S=*pts;
ptr=term;
debug("parse(): term = %s",ptr);
S->q=match(ptr);
if (S->q <= UNKNOWN) {
ptr++;
S->m=match(ptr);
} else {
/* + is the implicit qualifier */
S->m=S->q;
S->q=PLUS;
}
len = strlen(Keywords[(*pts)->m]);
ptr+=len;
if ( (ptr-term) < 0 )
sysfatal("parse(): error parsing!!!");
aux=nil;
/* if command has : it is specifiying a domain-spec
that should be expanded as a macro */
if ( *ptr == ':' || *ptr == '=' ) {
S->isdomainspec=1;
ptr++;
aux=smprint("%s",ptr);
macro=expandmacro(aux);
if (macro == nil )
return PermError;
else
len=strlen(macro);
} else {
macro=smprint("%s",ptr);
len=strlen(macro);
}
free(aux);
stat=None;
/* mechanism parsing */
debug("parse(): S->m =%s\n",Keywords[S->m]);
switch(S->m){
case VERSION:
case SID:
case ALL:
S->v = emalloc(sizeof(Val));
snprint(S->v[0].s,MAXVAL,"%s",macro);
break;
case IP4:
case IP6:
S->n=1;
S->v = emalloc(sizeof(Val));
snprint(S->v[0].s,MAXVAL,"%s",macro);
stat=expandip(S);
break;
case REDIRECT:
case EXPL:
case EXISTS:
case INCLUDE:
S->v = emalloc(sizeof(Val));
snprint(S->v[0].s,MAXVAL,"%s",macro);
break;
case A:
case MX:
/* minimun len is 3 becouse mx/24 is a valid construct */
if (len<4) {
aux=smprint("%s%s",macrovars[2],macro); /* sender domain */
free(macro);
macro=aux;
}
debug("parse(): mx: aux = %s",macro);
stat=symbres(S,macro,0);
if ( stat != None ) {
debug("parse(): a/mx error received from symbres: %s",macro);
break;
}
stat=expandip(S);
break;
case PTR:
if ( len > 1 )
strncpy(S->t,macro,MAXVAL);
else
strncpy(S->t,macrovars[2],MAXVAL);
stat=symbres(S,macrovars[4],0);
if ( stat != None ) {
debug("parse(): ptr error received from symbres");
break;
}
stat=expandip(S);
break;
default: /* user defined macros */
stat=symbres(S,macro,0);
break;
}
free(macro);
return stat;
}
int
testq(int q)
{
int val;
switch(q) {
case PLUS: val=Pass; break;
case MINUS: val=Fail; break;
case QMARK: val=Neutral; break;
case TILDE: val=SoftFail; break;
default: val=Pass; break;
};
return val;
}
int
eval(Symbol *pts, uchar *ip)
{
uchar mip1[16];
uchar mip2[16];
int q,i, answer;
Symbol *S;
S = pts;
answer=Fail;
q=testq(S->q);
memset(mip1,0,16);
memset(mip2,0,16);
debug("eval(): S->m =%s",Keywords[S->m]);
switch(S->m){
case INCLUDE:
chkdeep++;
answer=check_host(ip,S->v[0].s, chkdeep);
break;
case REDIRECT:
chkdeep++;
answer= check_host(ip,S->v[0].s,chkdeep);
break;
case VERSION:
if (strncmp(S->v[0].s,"pf1",3) != 0 )
answer=PermError;
else
return None;
break;
case SID:
if ( strncmp(S->v[0].s,"spf2.0/pra",10) !=0)
answer=PermError;
else
return None;
break;
case ALL:
return q;
break;
case IP4:
case IP6:
for(int i=0;i<S->n;i++) {
if (WHITELIST)
fprint(2,"%I %M\n",S->v[i].ip,S->v[i].mask);
maskip(S->v[i].ip, S->v[i].mask, mip1);
maskip(ip, S->v[i].mask, mip2);
debug("eval(): mip1 %I mip2 %I",mip1,mip2);
if (equivip6(mip1, mip2)) {
answer=Pass;
break;
} else
answer=Fail;
}
break;
case EXPL:
case EXISTS:
case A:
case MX:
/* all of this mechanism matches if <ip> is one of the
<target-name>'s IP addresses. */
for(i=0;i<S->n;i++) {
if (WHITELIST)
fprint(2,"%I %M\n",S->v[i].ip,S->v[i].mask);
debug("eval(): %I == %I",S->v[i].ip, ip);
if (equivip6(S->v[i].ip, ip)) {
answer=Pass;
break;
} else
answer=Fail;
}
break;
case PTR:
for(i=0;i<S->n;i++) {
if (WHITELIST)
fprint(2,"%I %M\n",S->v[i].ip,S->v[i].mask);
debug("eval(): %I == %I",S->v[i].ip, ip);
if (equivip6(S->v[i].ip, ip)) {
debug("eval(): %s == %s",S->v[i].n, S->t);
if ( strstr(S->v[i].n,S->t) != 0 ) {
answer=Pass;
break;
}
} else
answer=Fail;
}
break;
default:
answer=PermError;
debug("eval(): no valid symbol found S->m %d",S->m);
break;
}
if (answer == q )
return Pass;
return answer;
}
int
check_host(uchar *ip, char *domain, int deep)
{
Symbol *symbtab;
Symbol *ps;
int nsymb;
int tabsize;
int stat, pos, len,lstat;
char *rules;
char *term;
if (deep == MAXDEEP )
return PermError;
debug("check_host(): deep == %d",deep);
rules=getrules(domain,TXT);
if (rules == nil ) {
rules=getrules(domain,SPF);
if (rules == nil ) {
debug("check_host(): unable to find rules");
/* invalid, malformed, or non-existent domains cause SPF checks
to return "None" because no SPF record can be found */
return None;
}
}
debug("check_host(): rules %s",rules);
/* create the symbol table and parse all elements */
tabsize=10;
symbtab=emalloc(sizeof(Symbol)*tabsize);
nsymb=stat=pos=len=0;
while (1) {
if ( nsymb >= tabsize-1) {
tabsize+=5;
symbtab=erealloc(symbtab,sizeof(Symbol)*tabsize);
debug("check_host(): realloc done");
}
symbtab[nsymb]=Z;
ps=&symbtab[nsymb];
term=emalloc(MAXTERM);
lstat=lex(rules, &term,&pos, &len);
if (lstat == DONE)
break;
else if (lstat == MORE) {
ps->pos=pos;
ps->len=len;
} else
return PermError;
stat=parse(&ps,term);
free(term);
if ( stat != None) {
debug("check_host(): parse error received: %d",stat);
return stat;
}
nsymb++;
}
/* evaluate the elements and return the result */
for(int i=0;i<nsymb;i++) {
ps=&symbtab[i];
stat=eval(&symbtab[i],ip);
/* Mechanisms after "all" will never be tested. Any "redirect" modifier
(Section 6.1) has no effect when there is an "all" mechanism. */
if (stat == Pass || ps->m == ALL) {
return stat;
}
if (ps->m == REDIRECT)
return stat;
}
return stat;
}
void
usage(void)
{
print("spf [-w] [-D]-u <user> -d <domain.com> -a <client-ip-addr> -l <local domain name>\n");
exits("usage()");
}
void
main(int argc, char *argv[])
{
int stat;
char *suser, *sdom, *sip, *ldom, *vipdom=nil;
uchar clientip[16];
fmtinstall('I',eipfmt);
fmtinstall('V',eipfmt);
fmtinstall('M',eipfmt);
suser=sdom=sip=ldom=nil;
if (argc < 9)
usage();
DEBUG=0;
ARGBEGIN{
case 'u':
suser=ARGF(); /* local-part of sender */
break;
case 'd':
sdom=ARGF(); /* domain of sender */
break;
case 'a':
sip=ARGF(); /* client ip */
break;
case 'l':
ldom=ARGF(); /* local domain */
break;
case 'D':
DEBUG=1;
break;
case 'w':
WHITELIST=1;
break;
default:
print(" badflag('%c')\n", ARGC());
usage();
} ARGEND;
if (argc != 0|| suser==nil || sdom==nil || ldom==nil || sip==nil )
usage();
//vipdom=getrules(sip,PTR);
if ( vipdom == nil || *vipdom == '!') {
free(vipdom);
vipdom=smprint("unknown");
}
macrovars[0]=smprint("%s@%s",suser,sdom); /* sender string */
macrovars[1]=strdup(suser); /* local-part of sender */
macrovars[2]=strdup(sdom); /* domain of sender */
macrovars[3]=strdup(sdom); /* domain */
macrovars[4]=strdup(sip); /* ip */
macrovars[5]=strdup(vipdom); /* ip validated domain name */
macrovars[6]=strdup(sdom); /* HELO/EHLO domain */
macrovars[7]=strdup(sip); /* SMTP client IP */
macrovars[8]=strdup(ldom); /* checker domain name */
macrovars[9]=smprint("%ld",time(0)); /* current timestamp */
macrovars[10]=smprint("in-addr"); /* in-addr or ip6 string */
parseip(clientip,sip);
chkdeep=0;
stat=check_host(clientip,sdom,chkdeep);
print("%s\n",statwords[stat]);
exits(statwords[stat]);
}
|