/*-----------------------------------------------------------------------------

    DNS Whitelist plugin for CommuniGate Pro 4.x, 5.x

    Installation and usage instructions can be found at
    http://kocmuk.ru/2009/11/07/cgp-dns-whitelisting/

    Copyright (C) 2009-2010 kocmuk.ru
    Version 1.0.3

  ---------------------------------------------------------------------------

    Build:

    Linux:      gcc -Wall -pthread dnswl-cgp.c -o dnswl-cgp
    FreeBSD:    gcc -DFREEBSD -pthread dnswl-cgp.c -o dnswl-cgp
    Solaris:    gcc -threads dnswl-cgp.c -o dnswl-cgp -lsocket -lnsl

  ---------------------------------------------------------------------------

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions
  are met:

    1. Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.
    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in
       the documentation and/or other materials provided with the
       distribution.
    3. The name of the author may not be used to endorse or promote
       products derived from this software without specific prior written
       permission. 

  THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
  OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
  OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-----------------------------------------------------------------------------*/

#include <errno.h>
#include <assert.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>                                                         
#include <sys/un.h>
#include <netdb.h>

#ifdef FREEBSD
#include <netinet/in.h>
#endif

#define MAIL_HEADER_LEN     256
#define MAIL_HEADER_NAME    "X-DNSWL-Status"

#define CGP_API_VERSION     2
#define CGP_CMD_LEN         16
#define CGP_CMDARG_LEN      128
#define CGP_ANSWER_LEN      4096
#define CGP_DIR             "/var/CommuniGate"

const char *CGP_PROTOCOLS[] = {"SMTP ", "HTTP ", "HTTPU ", "WEBUSER ", NULL };

volatile sig_atomic_t   killed = 0;

typedef struct req 
{
	unsigned int	seqNum;
	char		cmdArg[CGP_CMDARG_LEN];
	char		buf[MAIL_HEADER_LEN];
	char            ip[MAIL_HEADER_LEN];
	char		dnswl[CGP_CMDARG_LEN];
	char		hname[CGP_CMDARG_LEN];
	int             quiet;
} req_t;

void        	*request_FILE(void *arg);
void	    	chomp (char *string);
void 		killhandler(int arg);
static int  	dnswl_dns_test(req_t *req);
static int  	_getline(const int fd, char *buf, size_t buflen);
static void 	print_usage(void);
static int  	read_args(int argc, char **argv, char *hname, char *dnswl, int *quiet);
static void 	putline(char *f, ...);
int 		reverse_inet_addr(char *ipstr, unsigned int seqNum);

/* ----------------------------------------------------------------- */
int
main(int argc, char **argv)
{
    char            request[CGP_CMDARG_LEN+CGP_CMD_LEN];
    char            cmd[CGP_CMD_LEN], cmdArg[CGP_CMDARG_LEN];
    char	    dnswl[CGP_CMDARG_LEN];
    char	    hname[CGP_CMDARG_LEN] = MAIL_HEADER_NAME;
    unsigned int    seqNum;
    pthread_t       req_thread;
    pthread_attr_t  p_attr;
    req_t           *req;
    int             rc;
    int		    quiet = 0;

    *dnswl = '\0';
    read_args(argc, argv, hname, dnswl, &quiet);

    if ( (strlen(dnswl) == 0)) {
	putline("* dnswl-cgp: -w ?, unable to determine the dns whitelist list");
	return 1;
    }

    pthread_attr_init(&p_attr);
    pthread_attr_setdetachstate(&p_attr, PTHREAD_CREATE_DETACHED);

    signal(SIGINT,  &killhandler);
    signal(SIGTERM, &killhandler);

    while( !killed && fgets(request, sizeof(request), stdin) != NULL ) {
        if( sscanf(request, "%u %s %s\n", &seqNum, cmd, cmdArg) != 3 ) {
            putline("* dnswl-cgp: CGP API error: Can't get command");
            return 1;
        }

        if( strncasecmp(cmd, "FILE", 4) == 0 ) {        /* FILE command  */
            if( (req = (req_t *)calloc(1, sizeof(req_t))) == NULL ) {
		putline("* dnswl-cgp: Can't calloc(): %s", strerror(errno));
                putline("%u FAILURE", seqNum);
                continue;
            }

            strncpy(req->cmdArg, cmdArg, sizeof(cmdArg)-1);
            req->seqNum  = seqNum;
            strncpy(req->hname, hname, sizeof(hname)-1);
 	    strncpy(req->dnswl, dnswl, sizeof(dnswl)-1);
	    req->quiet = quiet;

            req_thread = (pthread_t)NULL;

            if( (rc = pthread_create(&req_thread, &p_attr, request_FILE, (void*)req)) ) {
		putline("* dnswl-cgp: Can't pthread_create(): %s", strerror(errno));
                putline("%u FAILURE", seqNum);
                free(req);
                continue;
            }

        } else if( strncasecmp(cmd, "INTF", 4) == 0 ) { /* INTF command  */

            putline("%u INTF %u", seqNum, CGP_API_VERSION);

        } else {                                        /* Default */

	    putline("* dnswl-cgp: bad command: %s", cmd);
            putline("%u FAILURE", seqNum);

        }
    }

    if( ferror(stdin) && errno != EINTR )
        putline("* dnswl-cgp: %s", strerror(errno));

    if( killed )
        putline("* dnswl-cgp: killed!");

    return 0;
}

/* ----------------------------------------------------------------- */
void *
request_FILE(void *arg)
{
    req_t       *req = (req_t *) arg;
    int         rc;

    /* Block any signals */
    {
        sigset_t sigmask;
        sigemptyset (&sigmask);
        pthread_sigmask (SIG_SETMASK, &sigmask, NULL);
    }

    if ((rc = dnswl_dns_test(req)) < 0 ) {
	 putline("%u FAILURE",  req->seqNum);
    }
    free(req);
    pthread_exit (NULL);
    return NULL;
}

/* ----------------------------------------------------------------- */
static int
dnswl_dns_test(req_t *req)
{
    int         	fh, pos, ac, size, rc, has_ok = 0;
    unsigned int 	cat, score, mscore;
    char        	*p, *s, *q, **pp, *suf, *rbldomain;
    int			instance, hspc;
    char 		*answer;
    struct 		addrinfo *res;

/* --- open file --- */
    if ((fh = open(req->cmdArg, O_RDONLY)) == -1) {
        putline("* dnswl-cgp[%u]: Can't open file(%s): %s", req->seqNum, req->cmdArg, strerror(errno));
	return -1;
    }

/* --- Get IP of source --- */
    instance = 0;
    for(pos = 0, ac = 0; ((rc = _getline(fh, req->buf, sizeof(req->buf))) > 0);) {
        p = req->buf;
        pos += rc;
        if ((*p == '\n') || (--rc < 1))
            break;      /* End of CGP headers */
        s = p + rc;

        if (*s == '\n') {
            *s = '\0';
            if (*(s - 1) == '\r')
                *--s = '\0';
        } else s++;
        if (*p == '\0')
            break;      /* End of CGP headers */

        switch (*p) {
        case 'S':       /* IP of source */
            if (*++p != ' ') continue;
            for (pp = (char **) CGP_PROTOCOLS, p++;
                 ((q = *pp) != NULL) && (strncasecmp(p, q, strlen(q))); pp++);
            if (q == NULL) continue;
            p += strlen(q);
            if ((*p == '[')&&(*--s == ']')) {
                p++; *s = '\0';
            }
            size = s - p;
            if (size <= INET6_ADDRSTRLEN) {
                strcpy(req->ip, p);
                s = req->ip + size;
                *s = '\0';
            } /* Prepare client address */
            continue;
        case 'R':
            instance++;
            continue;
        }
    }
    close(fh);
    chomp(req->ip);

/* --- Skip if cann't find IP --- */
    if (!strlen(req->ip)) {
        putline("* dnswl-cgp[%u]: Internal message. Just skip it.", req->seqNum);
        if (req->quiet)
                putline("%u OK", req->seqNum);
        else
                putline("%u ADDHEADER \"%s: SkipInternal\"", req->seqNum, req->hname);
        return 0;
    }

/* --- reverse ipaddress string for dnsbl query --- */
    if ((reverse_inet_addr(req->ip, req->seqNum)) < 0) {
	return -1;
    }
    s = req->ip + strlen(req->ip) - 1;
	if (*s != '.') {
         *(++s) = '.';
         *(++s) = 0;
    }

/* --- dnswl lookup --- */
    chomp(req->dnswl);

    suf = req->ip + strlen(req->ip);
    hspc = sizeof(req->ip) - strlen(req->ip) - 2;
    rbldomain = req->dnswl;
    while (*rbldomain) {
          s = strchr(rbldomain, ':');
          if (s) *s = 0;
          strncpy (suf, rbldomain, hspc);
          suf[hspc] = '\0';

          if (s) {
            *s = ':';
            rbldomain = s+1;
          } else {
            rbldomain += strlen(rbldomain);
          }

          s = suf + strlen(suf) - 1;
          if (*s != '.') {
            *(++s) = '.';
            *(++s) = 0;
          }

	  res = NULL;
	  if ((rc = getaddrinfo(req->ip, NULL, NULL, &res)) != 0) {
		putline("* dnswl-cgp[%u]: Didn't find DNS A object: %s", req->seqNum, req->ip);
	  } else {
                answer = inet_ntoa(((struct sockaddr_in*)res->ai_addr)->sin_addr);
		chomp(answer);
          	putline("* dnswl-cgp[%u]: Looked up DNS A object: %s -> %s", req->seqNum, req->ip, answer);

	        if ( sscanf(answer,"127.0.%u.%u",&cat, &score) != 2 ) {
       		     putline("* dnswl-cgp[%u]: unknown DNSWL answer: '%s', looking for 127.0.X.Y", req->seqNum, answer);
        	}
		if (has_ok) {
			if (score < mscore) mscore = score;
		 } else {
			mscore = score;
			has_ok = 1;
		}
	 }
	 if (res)
		freeaddrinfo(res);
    }

/*
Trustworthiness / Score (127.0.x.Y):

    * 0 = none - only avoid outright blocking (eg Hotmail, Yahoo mailservers, -0.1)
    * 1 = low - reduce chance of false positives (-1.0)
    * 2 = medium - make sure to avoid false positives but allow override for clear cases (-10.0)
    * 3 = high - avoid override (-100.0).
*/
    if (has_ok) {
	if (mscore == 0)
		putline("%u ADDHEADER \"%s: None\"", req->seqNum, req->hname);
	else if (mscore == 1)
		putline("%u ADDHEADER \"%s: Low\"", req->seqNum, req->hname);
	else if (mscore == 2)
		putline("%u ADDHEADER \"%s: Medium\"", req->seqNum, req->hname);
	else if (mscore == 3)
		putline("%u ADDHEADER \"%s: High\"", req->seqNum, req->hname);
	else 
		putline("%u ADDHEADER \"%s: Unknown\"", req->seqNum, req->hname);
    } else {
	if (req->quiet)
		putline("%u OK", req->seqNum);
	else
		putline("%u ADDHEADER \"%s: NotListed\"", req->seqNum, req->hname);
    }

return 0;
}

/* ------------ Get line from header ------------ */
static int _getline(const int fd, char *buf, size_t buflen) {
  size_t cnt = 0;
  int i;

  while ((i = read(fd, buf, 1)) != 0) {
    if (i < 0) {
        if(errno == EAGAIN || errno == EINTR)
            continue;
        return (-1);
    }
    cnt++;
    if((*buf == '\n') || ((cnt + 1) == buflen))
        break;
    buf++;
  }
  if(*buf != '\n' ) {   /* Skip input  stream until '\n' */
    while (((i = read(fd, buf, 1))!= 0) && (*buf != '\n')) {
      if ((i > 0) || (errno == EAGAIN)||(errno == EINTR))
          continue;
      return (-1);
    }
  }
  *++buf   = '\0';
  return cnt;
}

/* ----------------------------------------------------------------- */
static void
print_usage(void)
{
    printf("Usage: dnswl-cgp [options] -w zone_name1[:zone_name2]\n\n");
    printf("  -w : specify dns whitelist zone name(s) (in DNSBL format)\n");
    printf("  -q : don't add any header on Skip messages\n");
    printf("  -m : mail header name, default=X-DNSWL-Status\n");
    printf("  -h : print this help message\n\n");
}

/* ----------------------------------------------------------------- */
static int
read_args(int argc, char **argv, char *hname, char *dnswl, int *quiet) 
{
    int opt;
    while( (opt = getopt(argc,argv,"?hqw:m:")) != -1 ) {
        switch(opt) {
            case 'm':
		chomp(optarg);
		strcpy(hname, optarg);
                break;
            case 'w':
                chomp(optarg);
                strcpy(dnswl, optarg);
                break;
            case 'q':
                *quiet = 1;
                break;
            case '?':
            case 'h':
                print_usage();
                exit(1);
        }
    }
    return 0;
}

/* ----------------------------------------------------------------- */
static void
putline(char *f, ...)
{
    char    answer[CGP_ANSWER_LEN];
    char    eanswer[CGP_ANSWER_LEN];
    va_list ap;

    va_start(ap, f);
    vsnprintf(answer, sizeof(answer)-1, f, ap);
    va_end(ap);

    snprintf(eanswer, sizeof(eanswer), "%s\n", answer);

    write(STDOUT_FILENO, eanswer, strlen(eanswer));
}

/* ----------------------------------------------------------------- */
void
chomp (char *string)
{
  int len;
  if (string == NULL)
    return;
  len = strlen (string);
  if (len && string[len - 1] == 10)
  {
    string[len - 1] = 0;
    len--;
  }
  if (len && string[len - 1] == 13)
    string[len - 1] = 0;
  return;
}

/*
 * reverse_inet_addr    - reverse ipaddress string for dnsbl query
 *                        e.g. 1.2.3.4 -> 4.3.2.1
 */
int
reverse_inet_addr(char *ipstr, unsigned int seqNum)
{
        unsigned int ipa, tmp;
        int i;
        int ret;
        struct in_addr inaddr;
        const char *ptr;
        char tmpstr[INET_ADDRSTRLEN];
        size_t iplen;

        if ((iplen = strlen(ipstr)) > INET_ADDRSTRLEN) {
		putline("* dnswl-cgp[%u]: invalid ipaddress: %s", seqNum, ipstr);
                return -1;
        }
        ret = inet_pton(AF_INET, ipstr, &inaddr);
        switch (ret) {
        case -1:
		putline("* dnswl-cgp[%u]: reverse_inet_addr: inet_pton() error", seqNum);
                return -1;
                break;
        case 0:
		putline("* dnswl-cgp[%u]: not a valid ip address: %s", seqNum, ipstr);
                return -1;
                break;
        }

        /* case default */
        ipa = inaddr.s_addr;

        tmp = 0;

        for (i = 0; i < 4; i++) {
                tmp = tmp << 8;
                tmp |= ipa & 0xff;
                ipa = ipa >> 8;
        }

        /*
         * this tmpstr hack here is because at least FreeBSD seems to handle
         * buffer lengths differently from Linux and Solaris. Specifically,
         * with inet_ntop(AF_INET, &tmp, ipstr, iplen) one gets a truncated
         * address in ipstr in FreeBSD.
         */
        ptr = inet_ntop(AF_INET, &tmp, tmpstr, INET_ADDRSTRLEN);
        if (!ptr) {
		putline("* dnswl-cgp[%u]: reverse_inet_addr: inet_ntop() error", seqNum);
                return -1;
        }
        assert(strlen(tmpstr) == iplen);
        strncpy(ipstr, tmpstr, iplen);

        return 0;
}

/* ----------------------------------------------------------------- */
void
killhandler(int arg)
{
    killed = 1;
}


