/*
 * ASCEND: @(#)filters.c	1.3 (95/07/25 00:55:30)
 *
 *      Copyright (c) 1994 Ascend Communications, Inc.
 *      All rights reserved.
 *
 *	Permission to copy all or part of this material for any purpose is
 *	granted provided that the above copyright notice and this paragraph
 *	are duplicated in all copies.  THIS SOFTWARE IS PROVIDED ``AS IS''
 *	AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT
 *	LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 *	FOR A PARTICULAR PURPOSE.
 */

#if defined( BINARY_FILTERS )

#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <sys/time.h>	/* gettimeofday() */

#include "radius.h"
#include "protos.h"

VALUE_PAIR*	prevRadPair = 0;

#undef DEBUG
#if defined( DEBUG )
#define PRINTF(x)	printf x
#else
#define PRINTF( x )
#endif

#define NO_TOKEN -1

typedef struct {
    char*	name;
    int 	value;
} KeywordStruct;

    /*
     * FilterPortType:
     *
     * Ascii names of some well known tcp/udp services.
     * Used for filtering on a port type.
     *
     */

static KeywordStruct _filterPortType[] = {
    { "ftp-data", 20 },
    { "ftp", 21 },
    { "telnet", 23 },
    { "smtp", 25 },
    { "nameserver", 42 },
    { "domain", 53 },
    { "tftp", 69 },
    { "gopher", 70 },
    { "finger", 79 },
    { "www", 80 },
    { "kerberos", 88 },
    { "hostname", 101 },
    { "nntp", 119 },
    { "ntp", 123 },
    { "exec", 512 },
    { "login", 513 },
    { "cmd", 514 },
    { "talk", 517 },
    {  NULL , NO_TOKEN },
};

typedef enum {
    FILTER_IP_TYPE,
    FILTER_GENERIC_TYPE,
    FILTER_IN,
    FILTER_OUT,
    FILTER_FORWARD,
    FILTER_DROP,
    FILTER_GENERIC_OFFSET,
    FILTER_GENERIC_MASK,
    FILTER_GENERIC_VALUE,
    FILTER_GENERIC_COMPNEQ,
    FILTER_GENERIC_COMPEQ,
    FILTER_MORE,
    FILTER_IP_DST,
    FILTER_IP_SRC,
    FILTER_IP_PROTO,
    FILTER_IP_DST_PORT,
    FILTER_IP_SRC_PORT,
    FILTER_EST
} FilterTokens;


static KeywordStruct _filterKeywords[] = {
    { "ip", 	FILTER_IP_TYPE },
    { "generic",FILTER_GENERIC_TYPE },
    { "in", 	FILTER_IN },
    { "out",	FILTER_OUT },
    { "forward",FILTER_FORWARD },
    { "drop",	FILTER_DROP },
    { "dstip",  FILTER_IP_DST },
    { "srcip",  FILTER_IP_SRC },
    { "dstport",FILTER_IP_DST_PORT },
    { "srcport",FILTER_IP_SRC_PORT },
    { "est",	FILTER_EST },
    { "more",	FILTER_MORE },
    { "!=",	FILTER_GENERIC_COMPNEQ },
    { "==",	FILTER_GENERIC_COMPEQ  },
    {  NULL , NO_TOKEN },
};

#define FILTER_DIRECTION 	0
#define FILTER_DISPOSITION	1
#define IP_FILTER_COMPLETE  	0x3	/* bits shifted by FILTER_DIRECTION */
					/* FILTER_DISPOSITION */
#define GENERIC_FILTER_COMPLETE 0x1c3	/* bits shifted for FILTER_DIRECTION */
					/* FILTER_DISPOSITION, FILTER_GENERIC_OFFSET*/
					/* FILTER_GENERIC_MASK, FILTER_GENERIC_VALUE*/

    /*
     * FilterProtoName:
     *
     * Ascii name of protocols used for filtering.
     *
     */
static KeywordStruct _filterProtoName[] = {
    { "tcp",  6 },
    { "udp",  17 },
    { "ospf", 89 },
    { "icmp", 1 },
    {  NULL , NO_TOKEN },
};

static KeywordStruct _filterCompare[] = {
    { ">", RAD_COMPARE_GREATER },
    { "=", RAD_COMPARE_EQUAL },
    { "<", RAD_COMPARE_LESS },
    { "!=", RAD_COMPARE_NOT_EQUAL },
    {  NULL , NO_TOKEN },
};

static char	_errMsg[512];
static char	_curString[512];

int _findKey P__(( char *string, KeywordStruct *list ));
static int _isAllDigit P__(( char *token ));

    /*
     * _findKey:
     *
     * Given a table of keywords, it will try and match string to an
     * entry. If it does it returns that keyword value. if no NO_TOKEN is
     * returned. A sanity check is made for upper case characters.
     *
     *	string:			Pointer to the token to match.
     *
     *	list:			Point to the list of keywords.
     *
     *	returns:		Keyword value on a match or NO_TOKEN.
     */
int 
_findKey(string, list)
char		*string;
KeywordStruct	*list;
{
    short 	len;
    KeywordStruct*  entry;
    char	buf[80], *ptr;

    len = strlen( (char *) string );
    for( ptr = buf ; len; len--, string++ ) {
	if( isupper( *string ) ) {
	    *ptr++ = tolower( *string );
	} else {
	    *ptr++ = *string;
	}
    }
    *ptr = 0;
    entry = list;
    while( entry->name ) {
   	if( strcmp( (char*) entry->name, (char*) buf ) == 0 ) {
	    break;
	}
	entry++;
    }
    return( entry->value );
}

    /*
     * _isAllDigit:
     *
     * Routine checks a string to make sure all values are digits.
     *
     *	token:			Pointer to sting to check.
     *
     * 	returns:		TRUE if all digits, or FALSE.
     *
     */

static int
_isAllDigit(token)
char	*token;
{
    int i;

    i = strlen( (char *) token );
    while( i-- ) {
	if( isdigit( *token ) ) {
	    token++;
	} else {
	    break;
	}
    }
    if( i > 0 ) {
	return( FALSE );
    } 

    return( TRUE );
}

    /*
     * _a2octet:
     *
     * Converts the ascii mask and value for generic filters into octets.
     * It also does a sanity check to see if the string is greater than
     * MAX_FILTER_LEN. It assumes the sting is hex with NO leading "0x"
     *
     *	tok:			Pointer to the string.
     *
     *  retBuf:			Pointer to place the octets.
     *
     *	returns:		Number of octects or -1 for error.
     * 
     */
static short
_a2octet(tok, retBuf)
char	*tok;
char	*retBuf;
{
    short	rc, len, val, retLen, i;
    char	buf[ RAD_MAX_FILTER_LEN *2 ];
    char	*octet = buf;

    rc = -1;
    retLen = 0;

    if( ( len = strlen( (char*) tok ) ) <= ( RAD_MAX_FILTER_LEN*2 ) ) {
	retLen = len/2;
	if( len % 2 ) {
	    retLen++;
	}
	memset( buf, '\0', RAD_MAX_FILTER_LEN * 2 );
	for( ; len; len-- ) {
	    if( *tok <= '9' && *tok >= '0' ) {
		val = '0';
	        *octet++ = *tok++ - val;
	    } else if( isxdigit( *tok ) ) {
		if( *tok > 'Z' ) {
		    val = 'a';
		} else {
		    val = 'A';
		}
	        *octet++ = ( *tok++ - val ) + 10;
	    } else {
		break;	
	    }
	}
	if( !len ) {
	    /* merge the values */
	    for( i = 0; i < RAD_MAX_FILTER_LEN*2; i+=2 ) {
		*retBuf++ = (buf[i] << 4) | buf[i+1];
	    }
	}
    }

    if( len ) {
	rc = -1;
    } else {
	rc = retLen;
    }
    return( rc );
}



    /*
     * _defaultNetMask:
     *
     *	Given an ip address this routine calculate a default netmask.
     *
     *	address:		Ip address.
     *
     *	returns:		Number of bits for the netmask
     *
     */
static char
_defaultNetmask(address)
unsigned long	address;
{
    char netmask;

    if ( ! address ) {
	netmask = 0;
    } else if (( address & htonl( 0x80000000 ) ) == 0 ) {
	netmask = 8;
    } else if (( address & htonl( 0xc0000000 ) ) == htonl( 0x80000000 ) ) {
	netmask = 16;
    } else if (( address & htonl( 0xe0000000 ) ) == htonl( 0xc0000000 ) ) {
	netmask = 24;
    } else {
	netmask = 32;
    }
    return netmask;
}

		
static char ipAddressDigits[] = "1234567890./";
    /*
     * This functions attempts to convert an IP address in ASCII dot
     * with an optional netmask part to a pair of IpAddress.  Note:
     * An IpAddress is always stored in network byte order.
     *
     * Parameters:
     *
     *  string:		Pointer to a NULL terminated IP address in dot 
     *			notation followed by an optional /nn to indicate
     *			the number leading of bits in the netmask.
     * 
     *  ipAddress:	Pointer to an IpAddress where the converted
     *			address will be stored.
     *
     *	netmask:	Pointer to an IpAddress where the netmask
     *			will be stored.  If no netmask is passed as
     *			as part of the address the default netmask will
     *			be stored here.
     *
     * Returns:
     *	<>		TRUE if valid conversion, FALSE otherwise.
     *
     *	*ipAddress:	If function returns TRUE, the IP address in NBO.
     *	*netmask:	If function returns TRUE, the netmask in NBO.
     */

static int
_ipAddressStringToValue(string, ipAddress, netmask)
char		*string;
unsigned long	*ipAddress;
char		*netmask;
{
    u_char*	dst;
    char*	cp;
    int		numDots;
    int		i;
    long	value;

    if ( ! string ) {
    	return(FALSE);
    }

    /* Allow an IP address to be blanked instead of forcing entry of
       0.0.0.0 -- the user will like it. */

    if ( *string == 0 ) {
	*ipAddress = 0;
	*netmask = 0;
	return TRUE;
    }

    /* First just count the number of dots in the address.  If there
       are more or less than three the address is invalid. */

    cp = string;
    numDots = 0;
    while( *cp ) {
	if( !strchr( ipAddressDigits, *cp) ) {
	    return( FALSE );
	}
	if ( *cp == '.') {
	    ++numDots;
	}
	++cp;
    }
    if ( numDots != 3 ) {
	return( FALSE );
    }

    dst = (u_char *) ipAddress;
    cp = string;

    for ( i = 0; i < sizeof( *ipAddress ); i++ ) {
	value = strtol( cp, (char**) &cp, 10 );
	if (( value < 0 ) || ( value > 255 )) {
	    return( FALSE );
	}
	*dst++ = (u_char) value;
	if ( *cp == '.' ) {
	    cp += 1;
	}
    }

    /* If there is a netmask part, parse it, otherwise figure out the
       default netmask for this class of address. */

    if ( *cp == '/' ) {
	value = strtol( cp + 1, (char**) &cp, 10 );
	if (( *cp != 0 ) || ( value < 0 ) || ( value > 32 )) {
	    return FALSE;
	}
	*netmask = (char) value;
    } else {
	*netmask = _defaultNetmask( *ipAddress );
    }
    return TRUE;
}

    /*
     * _parseIpFilter:
     *
     * This routine parses an IP filter string from a RADIUS
     * reply. The format of the string is:
     *
     *	ip dir action [ dstip n.n.n.n/nn ] [ srcip n.n.n.n/nn ]
     *	    [ proto [ dstport cmp value ] [ srcport cmd value ] [ est ] ] 
     *
     * Fields in [...] are optional.
     *	where:
     *
     *  ip:		Keyword to designate an IP filter. Actually this
     *			has been determined by _parseFilter.
     *
     *	dir:		Filter direction. "IN" or "OUT"
     *
     *	action:		Filter action. "FORWARD" or "DROP"
     *
     *	dstip:		Keyword for destination IP address.
     *			n.n.n.n = IP address. /nn - netmask. 
     *			
     *	srcip:		Keyword for source IP address.
     *			n.n.n.n = IP address. /nn - netmask. 
     *			
     *	proto:		Optional protocol field. Either a name or
     *			number. Known names are in FilterProtoName[].
     *			
     *	dstpost:	Keyword for destination port. Only valid with tcp
     *			or udp. 'cmp' are in FilterPortType[]. 'value' can be
     *			a name or number.
     *
     *	srcpost:	Keyword for source port. Only valid with tcp
     *			or udp. 'cmp' are in FilterPortType[]. 'value' can be
     *			a name or number.
     *			
     *	est:		Keyword for TCP established. Valid only for tcp.
     *			
     * expects:
     *
     *	curEntry:	Pointer to place the filter structure
     *
     *	returns:	-1 for error or 0 for OK
     *	
     */

static int 
_parseIpFilter(curEntry)
RadFilter	*curEntry;
{
 
    unsigned long	elements = 0l;
    int			tok; 
    char*		token;
    RadIpFilter*	ip;

    token = (char *) strtok( NULL, " " ); 

    PRINTF((" in ip  filter \n")); 

    memset( curEntry, '\0', sizeof( RadFilter ) );
    curEntry->type = RAD_FILTER_IP; 
    ip = &curEntry->u.ip;
    ip->established = FALSE;
 
    while( token ) {
	PRINTF((" token %s ", token ));
  	tok = _findKey( token, _filterKeywords );
	switch( tok ) {
	    case FILTER_IN:
	    case FILTER_OUT:
		curEntry->indirection = tok == FILTER_IN ? TRUE: FALSE;
		PRINTF((" got %s ", tok == FILTER_IN?"FILTER_IN":"FILTER_OUT"));
	        elements |= (1 << FILTER_DIRECTION );
		break;
	    case FILTER_FORWARD:
	    case FILTER_DROP:
		PRINTF((" got %s ", tok == FILTER_DROP?
			"FILTER_DROP":"FILTER_FORWARD"));
	        elements |= (1 << FILTER_DISPOSITION );
		if( tok == FILTER_FORWARD ) {
		    curEntry->forward = TRUE;
		} else {
		    curEntry->forward = FALSE;
		}
		break;
	    case FILTER_IP_DST:
	    case FILTER_IP_SRC:
		PRINTF((" got %s ", tok == FILTER_IP_DST?
			"FILTER_IP_DST":"FILTER_IP_SRC"));
		token = (char *) strtok( NULL, " " );
		if ( token ) {
		    if( tok == FILTER_IP_DST ) {
			
		        if( _ipAddressStringToValue( (char*)token, 
				 &ip->dstip, (char *)&ip->dstmask ) ) {
			    PRINTF((" ip %lx netmask %lx \n", ip->dstip, 
				     ip->dstmask ));
			    break;
			}
		    } else {
		        if( _ipAddressStringToValue( (char *)token, 
				&ip->srcip, (char *)&ip->srcmask ) ) {
			    PRINTF((" ip %lx netmask %lx \n", ip->srcip,
				     ip->srcmask ));
			    break;
			}
		    }
		} 

		PRINTF(( "RADIF: IP Filter syntax error %s \n", token ));
		sprintf( _errMsg, 
		  	 "ip filter error: do not recognize %s in %s \n",
			  token, _curString );
		goto doneErr; 
	    case FILTER_IP_DST_PORT:
	    case FILTER_IP_SRC_PORT:
	    {
		RadFilterComparison cmp;
		short		 port;

		PRINTF((" got %s ", tok == FILTER_IP_DST_PORT?
			"FILTER_IP_DST_PORT":"FILTER_IP_SRC_PORT"));
		token = (char *) strtok( NULL, " " );
		if ( token ) {
  		    cmp = _findKey( token, _filterCompare );
		    PRINTF((" cmp value = %d \n", cmp ));
		    if( cmp != NO_TOKEN ) {
			token = (char *) strtok( NULL, " " );
			if ( token ) {
			    if( _isAllDigit( token ) ) {
				port = atoi( (char *) token );
			    } else {
  		    	        port = _findKey( token, _filterPortType );
			    }
			    if( port != (short) NO_TOKEN ) {
		    	    	PRINTF((" port = %d \n", port ));
				if( tok == FILTER_IP_DST_PORT ) {
				    ip->dstPortComp = cmp;
				    ip->dstport = htons( port );
				} else {
				    ip->srcPortComp = cmp;
				    ip->srcport = htons( port );
				}
				break;
			    }
			}
		    }
		}
		sprintf( _errMsg, 
		  	 "ip filter error: do not recognize %s in %s \n",
			  token, _curString );
		PRINTF(( "RADIF: IP Filter syntax error %s \n", token ));
		goto doneErr;
		break;
	    }
	    case FILTER_EST:
		PRINTF((" got est %s ", token ));
		ip->established = TRUE;
		break;
	    default:
		/* no keyword match but may match a protocol list */
		if( _isAllDigit( token ) ) {
		    tok = atoi( (char *) token );
		} else {
		    tok = _findKey( token, _filterProtoName );

		    if( tok == NO_TOKEN ) {
			PRINTF(( "RADIF: IP proto error %s \n", token ));
			sprintf( _errMsg, 
		  	     "ip filter error: do not recognize %s in %s \n",
			     token, _curString );
			goto doneErr;
		    }
		}
		ip->proto = tok;
		PRINTF(("ip proto cmd = %d ", tok));
	}
        token = (char *) strtok( NULL, " " ); 
    } 

    if( elements == IP_FILTER_COMPLETE ) {
	return( 0 );
    }

doneErr:
    PRINTF((" done err \n"));
    log_err( _errMsg );
    return( -1 );
}

    /*
     * _parseGenericFilter:
     *
     * This routine parses a Generic filter string from a RADIUS
     * reply. The format of the string is:
     *
     *	GENERIC dir action offset mask value [== or != ] [more]
     *
     * Fields in [...] are optional.
     *	where:
     *
     * 	generic:	Keyword to indicate a generic filter. This
     *			has been determined by _parseFilter.
     *
     *	dir:		Filter direction. "IN" or "OUT"
     *
     *	action:		Filter action. "FORWARD" or "DROP"
     *
     *	offset:		A Number. Specifies an offset into a frame 
     *			to start comparing.
     *			
     *	mask:		A hexadecimal mask of bits to compare.
     *			
     *	value:		A value to compare with the masked data.
     *
     *	compNeq:	Defines type of comparison. ( "==" or "!=")
     *			Default is "==".
     *			
     *	more:		Optional keyword MORE, to represent the attachment
     *			to the next entry.
     *
     * expects:
     *
     *	curEntry:	Pointer to place the filter structure
     *
     *	returns:	-1 for error or 0 for OK
     *	
     */

static int
_parseGenericFilter(curEntry)
RadFilter	*curEntry;
{
    unsigned long	elements = 0l; 
    int			tok; 
    int			gstate = FILTER_GENERIC_OFFSET;
    char*		token;
    short		valLen, maskLen;
    RadGenericFilter*	gen;

    token = (char *) strtok( NULL, " " ); 

    PRINTF((" in parse generic filter \n")); 

    maskLen = 0;
    memset( (char *)curEntry, '\0', sizeof( RadFilter ) );
    curEntry->type = RAD_FILTER_GENERIC;
    gen = &curEntry->u.generic;
    gen->more = FALSE; 
    gen->compNeq = FALSE;	

    while( token ) {
	PRINTF((" token %s ", token ));
  	tok = _findKey( token, _filterKeywords );
   	PRINTF(("tok %d ", tok));
	switch( tok ) {
	    case FILTER_IN:
	    case FILTER_OUT:
		curEntry->indirection = tok == FILTER_IN ? TRUE: FALSE;
	        elements |= (1 << FILTER_DIRECTION );
		PRINTF((" got %s ", tok == FILTER_IN?"FILTER_IN":"FILTER_OUT"));
		break;
	    case FILTER_FORWARD:
	    case FILTER_DROP:
	        elements |= (1 << FILTER_DISPOSITION );
		PRINTF((" got %s ", tok == FILTER_DROP?
			"FILTER_DROP":"FILTER_FORWARD"));
		if( tok == FILTER_FORWARD ) {
		    curEntry->forward = TRUE;
		} else {
		    curEntry->forward = FALSE;
		}
		break;
	    case FILTER_GENERIC_COMPNEQ:
		gen->compNeq = TRUE;
		PRINTF((" got compare %s ", token));
		break;
	    case FILTER_GENERIC_COMPEQ:
		gen->compNeq = FALSE;
		PRINTF((" got compare %s ", token));
		break;
	    case FILTER_MORE:
		gen->more = htons( TRUE );
		PRINTF((" got more %s ", token ));
		break;
	    default:
	        elements |= ( 1 << gstate );
		switch( gstate ) {
		    case FILTER_GENERIC_OFFSET:
			gstate = FILTER_GENERIC_MASK;
			gen->offset = htons( atoi( (char *) token ) );
			break;
		    case FILTER_GENERIC_MASK:
			gstate = FILTER_GENERIC_VALUE;
			maskLen = _a2octet( token, (char *)gen->mask );
			if( maskLen == (short) -1 ) {
			    sprintf( _errMsg, 
		  		     "filter mask error: %s \n", _curString );
			    goto doneErr;
			}
#ifdef DEBUG
			PRINTF((" octet retlen = %d ", maskLen ));
			for( tok = 0; tok < maskLen; tok++) {
        		    PRINTF(("%2x", gen->mask[tok]));
		        }
			PRINTF(("\n"));
#endif

			break;
		    case FILTER_GENERIC_VALUE:
			gstate ++;
			valLen = _a2octet( token, (char *)gen->value );
			if( valLen != maskLen ) {
			    sprintf( _errMsg, 
    "filter value size is not the same size as the filter mask: %s \n", 
				     _curString );
			    goto doneErr;
			}
			gen->len = htons( valLen );
#ifdef DEBUG
			PRINTF((" octet retlen = %d ", maskLen ));
			for( tok = 0; tok < maskLen; tok++) {
        		    PRINTF(("%2x", gen->value[tok]));
		        }
			PRINTF(("\n"));
#endif

			break;
		    default:
			sprintf( _errMsg, "filter: do not know %s in %s \n",
				 token, _curString );
			PRINTF(( "RADIF: Filter syntax error %s \n", token ));
		       goto doneErr;    
		}
	}
        token = (char *) strtok( NULL, " " ); 
    }

    if( elements == GENERIC_FILTER_COMPLETE ) {
	return( 0 );
    }

doneErr:
    PRINTF((" done err \n"));
    log_err( _errMsg );
    return( -1 );
}
		       
    /*
     * filterBinary:
     *
     * This routine will call routines to parse entries from an ASCII format
     * to a binary format recognized by the Ascend boxes.
     *
     *	pair:			Pointer to value_pair to place return.
     *
     *	valstr:			The string to parse	
     *
     *	return:			-1 for error or 0.
     */
int 
filterBinary(pair, valstr)
VALUE_PAIR	*pair;
char		*valstr;
{

    char*		token;
    unsigned long	tok;
    int			rc;
    RadFilter		radFil, *filt;
    RadGenericFilter*	gen;

    rc = -1;
    strcpy( _curString, valstr );

    token = (char *) strtok( (char *)valstr, " " );
    tok = _findKey( token, _filterKeywords );
    pair->lvalue = sizeof( RadFilter );
    switch( tok ) {
      case FILTER_IP_TYPE:
	rc = _parseIpFilter( &radFil );
	break;
      case FILTER_GENERIC_TYPE:
	rc = _parseGenericFilter( &radFil );
	break;
    }

    /*
     * if more is set then this new entry must exist, be a 
     * FILTER_GENERIC_TYPE, direction and disposition must match for 
     * the previous more to be valid. If any should fail then TURN OFF 
     * previos more
     */
    if( prevRadPair ) {
	filt = ( RadFilter * )prevRadPair->strvalue;
	if(( tok != FILTER_GENERIC_TYPE ) || (rc == -1 ) ||
	   ( prevRadPair->attribute != pair->attribute ) || 
	   ( filt->indirection != radFil.indirection ) || 
	   ( filt->forward != radFil.forward ) ) {
	    gen = &filt->u.generic;
	    gen->more = FALSE;
	    sprintf( _errMsg,
	       "filterBinary:  'more' for previous entry doesn't match: %s.\n",
		     _curString );
	    log_err( _errMsg );
	}
    }
    prevRadPair = NULL;
    if( rc != -1 && tok == FILTER_GENERIC_TYPE ) {
	if( radFil.u.generic.more ) {
	    prevRadPair = pair;
	} 
    }

    if( rc != -1 ) {
	memcpy( pair->strvalue, (char *) &radFil, pair->lvalue );
    }
    return(rc);
}

#endif /* BINARY_FILTERS */

