Plan 9 from Bell Labs’s /usr/web/sources/contrib/gabidiaz/root/sys/src/cmd/spf/spf.c

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


#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]);


}

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to [email protected].