/****************************************************************************
 *
 * acl.c - a small library for nrpe.c. It adds IPv4 subnets support to ACL in nrpe.
 *
 * License: GPLv2
 * Copyright (c) 2011 Kaspersky Lab ZAO
 *
 * Description:
 *
 * acl.c creates two linked lists. One is for IPv4 hosts and networks, another 
 * is for domain names. All connecting hosts (if allowed_hosts is defined) 
 * are checked in these two lists.
 *
 * Note:
 *  Only ANCII names are supported in ACL.
 *
 * License Notice:
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 ****************************************************************************/

#include "../include/config.h"
#include "../include/common.h"
#include "../include/utils.h"

#include <sys/types.h>
#include <sys/socket.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <netdb.h>
#include <stdarg.h>

#include "../include/acl.h"

extern int debug;

/* This function checks if a char argument from valid char range.
 * Valid range is: ASCII only, a number or a letter, a space, a dot, a slash, a dash, a comma.
 *
 * Returns:
 *      0 - char isn't from valid group
 *  1 - char is a number
 *  2 - char is a letter
 *  3 - char is a space(' ')
 *  4 - char is a dot('.')
 *  5 - char is a slash('/')
 *  6 - char is a dash('-')
 *  7 - char is a comma(',')
 */

int isvalidchar(int c) {
        if (!isascii(c))
                return 0;

        if (isdigit(c))
                return 1;

        if (isalpha(c))
                return 2;

        if (isspace(c))
                return 3;

        switch (c) {
        case '.':
                return 4;
        case '/':
                return 5;
        case '-':
                return 6;
        case ',':
                return 7;
        default:
                return 0;
        }
}

/*
 * Get substring from allowed_hosts from s position to e position.
 */

char * acl_substring(char *string, int s, int e) {
    char *substring;
    int len = e - s;

        if (len < 0)
                return NULL;

    if ( (substring = malloc(len + 1)) == NULL)
        return NULL;

    memmove(substring, string + s, len + 1);
    return substring;
}

/*
 * Add IPv4 host or network to IP ACL. IPv4 format is X.X.X.X[/X].
 * Host will be added to ACL only if it has passed IPv4 format check.
 *
 * Returns:
 * 1 - on success
 * 0 - on failure
 *
 * States for IPv4 format check:
 *  0 - numbers(-> 1), dot(-> -1), slash(-> -1), other(-> -1)
 *  1 - numbers(-> 1), dot(-> 2),  slash(-> -1), other(-> -1)
 *  2 - numbers(-> 3), dot(-> -1), slash(-> -1), other(-> -1)
 *  3 - numbers(-> 3), dot(-> 4),  slash(-> -1), other(-> -1)
 *  4 - numbers(-> 5), dot(-> -1), slash(-> -1), other(-> -1)
 *  5 - numbers(-> 5), dot(-> 6),  slash(-> -1), other(-> -1)
 *  6 - numbers(-> 7), dot(-> -1), slash(-> -1), other(-> -1)
 *  7 - numbers(-> 7), dor(-> -1), slash(-> 8),  other(-> -1)
 *  8 - numbers(-> 9), dor(-> -1), slash(-> -1), other(-> -1)
 *  9 - numbers(-> 9), dot(-> -1), slash(-> -1), other(-> -1)
 *
 *  Good states are 7(IPv4 host) and 9(IPv4 network)
 */

int add_ipv4_to_acl(char *ipv4) {

        int state = 0;
        int octet = 0;
        int index = 0;  /* position in data array */
        int data[5];    /* array to store ip octets and mask */
        int len = strlen(ipv4);
        int i, c;
        unsigned long ip, mask;
        struct ip_acl *ip_acl_curr;

		if(debug == TRUE)
			logit(LOG_INFO, "add_ipv4_to_acl: checking ip-address >%s<", ipv4);

        /* Check for min and max IPv4 valid length */
		if (len < 7 || len > 18) {
			logit(LOG_INFO, "add_ipv4_to_acl: Error, ip-address >%s< incorrect length", ipv4);
			return 0;
		}

        /* default mask for ipv4 */
        data[4] = 32;

        /* Basic IPv4 format check */
        for (i = 0; i < len; i++) {
			/* Return 0 on error state */
			if (state == -1) {
				if(debug == TRUE)
					logit(LOG_INFO, "add_ipv4_to_acl: Error, ip-address >%s< incorrect "
								"format, continue with next check ...", ipv4);
				return 0;
			}

                c = ipv4[i];

                switch (c) {
                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                        octet = octet * 10 + CHAR_TO_NUMBER(c);
                        switch (state) {
                        case 0: case 2: case 4: case 6: case 8:
                                state++;
                                break;
                        }
                        break;
                case '.':
                        switch (state) {
                        case 1: case 3: case 5:
                                data[index++] = octet;
                                octet = 0;
                                state++;
                                break;
                        default:
                                state = -1;
                        }
                        break;
                case '/':
                        switch (state) {
                        case 7:
                                data[index++] = octet;
                                octet = 0;
                                state++;
                                break;
                        default:
                                state = -1;
                        }
                        break;
                default:
                        state = -1;
                }
        }

        /* Exit state handling */
        switch (state) {
        case 7: case 9:
                data[index] = octet;
                break;
        default:
                /* Bad states */
                logit(LOG_INFO, "add_ipv4_to_acl: Error, ip-address >%s< bad state", ipv4);
                return 0;
        }

        /*
         * Final IPv4 format check.
         */
        for (i=0; i < 4; i++) {
                if (data[i] < 0 || data[i] > 255) {
                        logit(LOG_ERR,"Invalid IPv4 address/network format(%s) in allowed_hosts option\n",ipv4);
                        return 0;
                }
        }

        if (data[4] < 0 || data[4] > 32) {
                logit(LOG_ERR,"Invalid IPv4 network mask format(%s) in allowed_hosts option\n",ipv4);
                return 0;
        }

        /* Convert ip and mask to unsigned long */
        ip = htonl((data[0] << 24) + (data[1] << 16) + (data[2] << 8) + data[3]);
        mask =  htonl(-1 << (32 - data[4]));

        /* Wrong network address */
        if ( (ip & mask) != ip) {
                logit(LOG_ERR,"IP address and mask do not match in %s\n",ipv4);
                return 0;
        }

        /* Add addr to ip_acl list */
        if ( (ip_acl_curr = malloc(sizeof(*ip_acl_curr))) == NULL) {
                logit(LOG_ERR,"Can't allocate memory for ACL, malloc error\n");
                return 0;
        }

        /* Save result in ACL ip list */
        ip_acl_curr->family = AF_INET;
        ip_acl_curr->addr.s_addr = ip;
        ip_acl_curr->mask.s_addr = mask;
        ip_acl_curr->next = NULL;

        if (ip_acl_head == NULL) {
                ip_acl_head = ip_acl_curr;
        } else {
                ip_acl_prev->next = ip_acl_curr;
        }
        ip_acl_prev = ip_acl_curr;

        if(debug == TRUE)
          logit(LOG_INFO, "add_ipv4_to_acl: ip-address >%s< correct, adding.", ipv4);

        return 1;
}

/*
 * Add IPv6 host or network to IP ACL. Host will be added to ACL only if 
 * it has passed IPv6 format check.
 *
 */

int add_ipv6_to_acl(char *ipv6) {
	char	*ipv6tmp;
	char	*addr_part, *mask_part;
	struct in6_addr addr;
	struct in6_addr mask;
	int		maskval;
	int		byte, bit;
	int		nbytes = sizeof(mask.s6_addr) / sizeof(mask.s6_addr[0]);
	int		x;
	struct ip_acl	*ip_acl_curr;

	/* Save temporary copy of ipv6 so we can use the original in error 
		messages if needed */
	ipv6tmp = strdup(ipv6);
	if(NULL == ipv6tmp) {
		logit(LOG_ERR, "Memory allocation failed for copy of address: %s\n", 
				ipv6);
		return 0;
		}

	addr_part = ipv6tmp;
	mask_part = strchr(ipv6tmp, '/');
	if (mask_part) {
		*mask_part = '\0';
		++mask_part;
	}

	/* Parse the address itself */
	if(inet_pton(AF_INET6, addr_part, &addr) <= 0) {
		free(ipv6tmp);
		return 0;
		}

	/* Check whether there is a netmask */
	if (mask_part && *mask_part) {
		/* If so, build a netmask */
		/* Get the number of bits in the mask */
		maskval = atoi(mask_part);
		if(maskval < 0 || maskval > 128) {
			free(ipv6tmp);
			return 0;
			}

		/* Initialize to zero */
		for(x = 0; x < nbytes; x++) {
			mask.s6_addr[x] = 0;
			}

		/* Set mask based on mask bits */
		byte = 0;
		bit = 7;
		while(maskval > 0) {
			mask.s6_addr[byte] |= 1 << bit;
			bit -= 1;
			if(bit < 0) {
				bit = 7;
				byte++;
				}
			maskval--;
			}
		}
	else {
		/* Otherwise, this is a single address */
		for(x = 0; x < nbytes; x++) {
			mask.s6_addr[x] = 0xFF;
			}
		}

	/* Add address to ip_acl list */
	ip_acl_curr = malloc(sizeof(*ip_acl_curr));
	if(NULL == ip_acl_curr) {
		logit(LOG_ERR, "Memory allocation failed for ACL: %s\n", ipv6);
		return 0;
		}

	/* Save result in ACL ip list */
	ip_acl_curr->family = AF_INET6;
	for(x = 0; x < nbytes; x++) {
		ip_acl_curr->addr6.s6_addr[x] = 
				addr.s6_addr[x] & mask.s6_addr[x];
		ip_acl_curr->mask6.s6_addr[x] = mask.s6_addr[x];
		}
	ip_acl_curr->next = NULL;

	if(NULL == ip_acl_head) {
		ip_acl_head = ip_acl_curr;
		}
	else {
		ip_acl_prev->next = ip_acl_curr;
		}
	ip_acl_prev = ip_acl_curr;

	free(ipv6tmp);
	return 1;
	}

/*
 * Add domain to DNS ACL list
 * Domain will be added only if it has passed domain name check.
 *
 * In this case domain valid format is:
 * 1) Domain names must use only alphanumeric characters and dashes (-).
 * 2) Domain names mustn't begin or end with dashes (-).
 * 3) Domain names mustn't have more than 63 characters.
 *
 * Return:
 * 1 - for success
 * 0 - for failure
 *
 * 0 - alpha(-> 1), number(-> 1), dot(-> -1), dash(-> -1), all other(-> -1)
 * 1 - alpha(-> 1), number(-> 1), dot(-> 2),  dash(-> 6),  all other(-> -1)
 * 2 - alpha(-> 3), number(-> 1), dot(-> -1), dash(-> -1), all other(-> -1)
 * 3 - alpha(-> 4), number(-> 1), dot(-> 2),  dash(-> 6),  all other(-> -1)
 * 4 - alpha(-> 5), number(-> 1), dot(-> 2),  dash(-> 6),  all other(-> -1)
 * 5 - alpha(-> 1), number(-> 1), dot(-> 2),  dash(-> 6),  all other(-> -1)
 * 6 - alpha(-> 1), number(-> 1), dot(-> 2),  dash(-> 6),  all other(-> -1)

 * For real FQDN only 4 and 5 states are good for exit.
 * I don't check if top domain exists (com, ru and etc.)
 * But in real life NRPE could work in LAN,
 * with local domain zones like .local or with names like 'mars' added to /etc/hosts.
 * So 1 is good state too. And maybe this check is not necessary at all...
 */

int add_domain_to_acl(char *domain) {
        int state = 0;
        int len = strlen(domain);
        int i, c;

        struct dns_acl *dns_acl_curr;

        if (len > 63) {
                logit(LOG_INFO,
					   "ADD_DOMAIN_TO_ACL: Error, did not add >%s< to acl list, too long!",
					   domain);
                return 0;
        }

        for (i = 0; i < len; i++) {
                c = domain[i];
                switch (isvalidchar(c)) {
                case 1:
                        state = 1;
                        break;
                case 2:
                        switch (state) {
                        case 0: case 1: case 5: case 6:
                                state = 1;
                                break;
                        case 2: case 3: case 4:
                                state++;
                                break;
                        }
                        break;

                case 4:
                        switch (state) {
                        case 0: case 2:
                                state = -1;
                                break;
                        default:
                                state = 2;
                        }
                        break;
                case 6:
                        switch (state) {
                        case 0: case 2:
                                state = -1;
                                break;
                        default:
                                state = 6;
                        }
                        break;
                default:
                        logit(LOG_INFO,
							   "ADD_DOMAIN_TO_ACL: Error, did not add >%s< to acl list, "
								"invalid chars!", domain);
					/* Not valid chars */
                        return 0;
                }
        }

        /* Check exit code */
        switch (state) {
        case 1: case 4: case 5:
                /* Add name to domain ACL list */
                if ( (dns_acl_curr = malloc(sizeof(*dns_acl_curr))) == NULL) {
                        logit(LOG_ERR,"Can't allocate memory for ACL, malloc error\n");
                        return 0;
                }
                strcpy(dns_acl_curr->domain, domain);
                dns_acl_curr->next = NULL;

                if (dns_acl_head == NULL)
                        dns_acl_head = dns_acl_curr;
                else
                        dns_acl_prev->next = dns_acl_curr;

                dns_acl_prev = dns_acl_curr;
                if(debug == TRUE)
                     logit(LOG_INFO, "ADD_DOMAIN_TO_ACL: added >%s< to acl list!", domain);
                return 1;
        default:
                logit(LOG_INFO,
					   "ADD_DOMAIN_TO_ACL: ERROR, did not add >%s< to acl list, "
						"check allowed_host in config file!", domain);
                return 0;
        }
}

/* Checks connection host in ACL
 *
 * Returns:
 * 1 - on success
 * 0 - on failure
 */

int is_an_allowed_host(int family, void *host)
{
	struct ip_acl		*ip_acl_curr = ip_acl_head;
	int					nbytes;
	int					x;
	struct dns_acl		*dns_acl_curr = dns_acl_head;
	struct sockaddr_in	*addr;
	struct sockaddr_in6	addr6;
	struct addrinfo		*res, *ai;
	struct in_addr		tmp;

	while (ip_acl_curr != NULL) {
		if(ip_acl_curr->family == family) {
			switch(ip_acl_curr->family) {
			case AF_INET:
				if (debug == TRUE) {
					tmp.s_addr = ((struct in_addr*)host)->s_addr;
					logit(LOG_INFO, "is_an_allowed_host (AF_INET): is host >%s< "
							"an allowed host >%s<\n",
						 inet_ntoa(tmp), inet_ntoa(ip_acl_curr->addr));
				}
				if((((struct in_addr *)host)->s_addr & 
						ip_acl_curr->mask.s_addr) == 
						ip_acl_curr->addr.s_addr) {
					if (debug == TRUE)
						logit(LOG_INFO, "is_an_allowed_host (AF_INET): host is in allowed host list!");
					return 1;
					}
				break;
			case AF_INET6:
				nbytes = sizeof(ip_acl_curr->mask6.s6_addr) / 
						sizeof(ip_acl_curr->mask6.s6_addr[0]);
				for(x = 0; x < nbytes; x++) {
					if((((struct in6_addr *)host)->s6_addr[x] & 
							ip_acl_curr->mask6.s6_addr[x]) != 
							ip_acl_curr->addr6.s6_addr[x]) {
						break;
						}
					}
				if(x == nbytes) { 
					/* All bytes in host's address pass the netmask mask */
					return 1;
					}
				break;
				}
			}
		ip_acl_curr = ip_acl_curr->next;
        }

	while(dns_acl_curr != NULL) {
		if (!getaddrinfo(dns_acl_curr->domain, NULL, NULL, &res)) {

			for (ai = res; ai; ai = ai->ai_next) {
				if (ai->ai_family == family) {
					switch (ai->ai_family) {

						case AF_INET:
							if (debug == TRUE) {
								tmp.s_addr = ((struct in_addr *) host)->s_addr;
								logit(LOG_INFO, "is_an_allowed_host (AF_INET): test match host >%s< "
											  "for allowed host >%s<\n",
									  inet_ntoa(tmp), dns_acl_curr->domain);
							}

							addr = (struct sockaddr_in *) (ai->ai_addr);
							if (addr->sin_addr.s_addr == ((struct in_addr *) host)->s_addr) {
								if (debug == TRUE)
									logit(LOG_INFO, "is_an_allowed_host (AF_INET): "
											"host is in allowed host list!");
								return 1;
							}
							break;

						case AF_INET6:
							if (debug == TRUE) {
								char formattedStr[INET6_ADDRSTRLEN];
								inet_ntop(ai->ai_family, (void *) &(((struct sockaddr_in6 *) (ai->ai_addr))->sin6_addr),
										  formattedStr, INET6_ADDRSTRLEN);
								logit(LOG_INFO, "is_an_allowed_host (AF_INET6): test match host against >%s< "
											  "for allowed host >%s<\n",
									  formattedStr, dns_acl_curr->domain);
							}
							struct in6_addr *resolved = &(((struct sockaddr_in6 *) (ai->ai_addr))->sin6_addr);
							memcpy((char *) &addr6, ai->ai_addr, sizeof(addr6));
							if (!memcmp(&addr6.sin6_addr, host, sizeof(addr6.sin6_addr))) {
								if (debug == TRUE)
									logit(LOG_INFO, "is_an_allowed_host (AF_INET6): "
											"host is in allowed host list!");
								return 1;
							}
							break;
					}
				}
			}
		}

		dns_acl_curr = dns_acl_curr->next;
	}
	return 0;
}

/* The trim() function takes a source string and copies it to the destination string,
 * stripped of leading and training whitespace. The destination string must be 
 * allocated at least as large as the source string.
 */

void trim( char *src, char *dest) {
	char *sptr, *dptr;

	for( sptr = src; isspace( *sptr) && *sptr; sptr++); /* Jump past leading spaces */
	for( dptr = dest; !isspace( *sptr) && *sptr; ) {
		*dptr = *sptr;
		sptr++;
		dptr++;
	}
	*dptr = '\0';
	return;
}

/* This function splits allowed_hosts to substrings with comma(,) as a delimiter.
 * It doesn't check validness of ACL record (add_ipv4_to_acl() and add_domain_to_acl() do),
 * just trims spaces from ACL records.
 * After this it sends ACL records to add_ipv4_to_acl() or add_domain_to_acl().
 */

void parse_allowed_hosts(char *allowed_hosts) {
	char *hosts = strdup( allowed_hosts);	/* Copy since strtok* modifies original */
	char *saveptr;
	char *tok;
	const char *delim = ",";
	char *trimmed_tok;
    int add_to_acl = 0;

	if (debug == TRUE)
		logit(LOG_INFO,
			 "parse_allowed_hosts: parsing the allowed host string >%s< to add to ACL list\n",
			 allowed_hosts);

#ifdef HAVE_STRTOK_R
	tok = strtok_r(hosts, delim, &saveptr);
#else
	if (debug == TRUE)
		logit(LOG_INFO,"parse_allowed_hosts: using strtok, this might lead to "
				"problems in the allowed_hosts string determination!\n");
	tok = strtok(hosts, delim);
#endif
	while( tok) {
		trimmed_tok = malloc(sizeof(char) * (strlen(tok) + 1));
		trim(tok, trimmed_tok);
		if (debug == TRUE)
			logit(LOG_DEBUG, "parse_allowed_hosts: ADDING this record (%s) to ACL list!\n", trimmed_tok);
		if (strlen(trimmed_tok) > 0) {

            /* lets check the type of the address before we try and add it to the acl */

            if (strchr(trimmed_tok, ':') != NULL) {

                /* its an ipv6 address */
                add_to_acl = add_ipv6_to_acl(trimmed_tok);
                
            } else {

                /* its either a fqdn or an ipv4 address
                   unfortunately, i don't want to re-invent the wheel here
                   the logic exists inside of add_ipv4_to_acl() to detect
                   whether or not it is a ip or not */
                add_to_acl = add_ipv4_to_acl(trimmed_tok);
            }

            /* but we only try to add it to a domain if the other tests have failed */
            if (!add_to_acl && !add_domain_to_acl(trimmed_tok)) {
				logit(LOG_ERR,"Can't add to ACL this record (%s). Check allowed_hosts option!\n",trimmed_tok);
			} else if (debug == TRUE)    
				logit(LOG_DEBUG,"parse_allowed_hosts: Record added to ACL list!\n");
		}
		free( trimmed_tok);
#ifdef HAVE_STRTOK_R
		tok = strtok_r(NULL, delim, &saveptr);
#else
		tok = strtok(NULL, delim);
#endif
	}

	free( hosts);
}

/*
 * Converts mask in unsigned long format to two digit prefix
 */

unsigned int prefix_from_mask(struct in_addr mask) {
        int prefix = 0;
        unsigned long bit = 1;
        int i;

        for (i = 0; i < 32; i++) {
                if (mask.s_addr & bit)
                        prefix++;

                bit = bit << 1;
        }
        return (prefix);
}

/*
 * It shows all hosts in ACL lists
 */

void show_acl_lists(void)
{
	struct ip_acl *ip_acl_curr = ip_acl_head;
	struct dns_acl *dns_acl_curr = dns_acl_head;

	logit(LOG_INFO, "Showing ACL lists for both IP and DOMAIN acl's:\n" );

	while (ip_acl_curr != NULL) {
		logit(LOG_INFO, "   IP ACL: %s/%u %u\n", inet_ntoa(ip_acl_curr->addr),
			 prefix_from_mask(ip_acl_curr->mask), ip_acl_curr->addr.s_addr);
		ip_acl_curr = ip_acl_curr->next;
	}

	while (dns_acl_curr != NULL) {
		logit(LOG_INFO, "  DNS ACL: %s\n", dns_acl_curr->domain);
		dns_acl_curr = dns_acl_curr->next;
	}
}
