/* SAMHAIN file system integrity testing                                   */
/* Copyright (C) 2009 Rainer Wichmann                                      */
/*                                                                         */
/*  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 "config_xor.h"

#if defined(HAVE_SYS_INOTIFY_H)

#undef  FIL__
#define FIL__  _("sh_inotify.c")

/* printf */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>

#include "samhain.h"
#include "sh_pthread.h"
#include "sh_calls.h"
#include "sh_inotify.h"
#include "sh_mem.h"
#include "sh_utils.h"
#include "slib.h"

/**************************************************
 *
 * Make the inotify fd thread-specific by 
 * encapsulating it in get/set functions:
 * sh_get_inotify_fd() / sh_set_inotify_fd()
 *
 **************************************************/

#if defined(HAVE_PTHREAD)
static pthread_key_t  inotify_key;
static pthread_once_t inotify_key_once = PTHREAD_ONCE_INIT;

static void make_inotify_key()
{
    (void) pthread_key_create(&inotify_key, free);
}

static int sh_get_inotify_fd()
{
  void * ptr;
  int  * fp;

  (void) pthread_once(&inotify_key_once, make_inotify_key);
 
  if ((ptr = pthread_getspecific(inotify_key)) == NULL) 
    {
      ptr = malloc(sizeof(int));
      if (ptr)
	{
	  fp  = (int*) ptr;
	  *fp = -1;
	  (void) pthread_setspecific(inotify_key, ptr);
	}
      else
	{
	  return -1;
	}
    }
  else 
    {
      fp  = (int*) ptr;
    }
  return *fp;
}

static void sh_set_inotify_fd(int fd)
{
  int  * fp;

  fp = (int*) pthread_getspecific(inotify_key);
  if (fp)
    *fp = fd;
  return;
}

/* !defined(HAVE_PTHREAD) */
#else

static int sh_inotify_fd = -1;

static inline int sh_get_inotify_fd()
{
  return sh_inotify_fd;
}

static inline void sh_set_inotify_fd(int fd)
{
  sh_inotify_fd = fd;
}

#endif

/*--- nothing thread-related below this point --- */

#include "zAVLTree.h"

typedef struct 
{
  int    watch;
  int    flag;
  char * file;
} sh_watch;

/**************************************************
 *
 * Get inotify fd, initialize inotify if necessary
 *
 **************************************************/
#define SH_INOTIFY_FAILED -2

static int sh_inotify_getfd()
{
  int ifd = sh_get_inotify_fd();

  if (ifd >= 0)
    {
      return ifd;
    }

  else if (ifd == SH_INOTIFY_FAILED)
    {
      return -1;
    }

  else /* if (ifd == -1) */
    {
#if defined(HAVE_INOTIFY_INIT1)
      ifd = inotify_init1(IN_CLOEXEC);
#else
      ifd = inotify_init();
      if (ifd >= 0)
	{
	  long sflags;

	  sflags = retry_fcntl(FIL__, __LINE__, ifd, F_GETFD, 0);
	  retry_fcntl(FIL__, __LINE__, ifd, F_SETFD, sflags|FD_CLOEXEC);
	}
#endif

      if (ifd < 0)
	{
	  sh_set_inotify_fd(SH_INOTIFY_FAILED);
	  return -1;
	}

      sh_set_inotify_fd(ifd);
      return ifd;
    }
}

/**************************************************
 *
 * Public function:
 *  int sh_inotify_wait_for_change(char * filename,
 *                                 int watch,
 *                                 int * errnum,
 *                                 int   waitsec);
 * Returns: watch, if nonnegative
 *          -1 on error or reopen required
 *             (check errnum != 0)
 *
 * Caller needs to keep track of watch descriptor
 *
 **************************************************/

#define SH_INOTIFY_REOPEN 0
#define SH_INOTIFY_MODIFY 1

static void sh_inotify_init(sh_watches * watches)
{
  watches->list_of_watches = NULL;
  watches->count           = 0;
  watches->max_count       = 0;
  return;
}

static void sh_inotify_free_watch(void * item)
{
  sh_watch * this = (sh_watch *) item;

  if (this->file)
    SH_FREE(this->file);
  SH_FREE(this);
  return;
}

static sh_watch * sh_inotify_create_watch(char * file, int nwatch, int flag)
{
  sh_watch * this = SH_ALLOC(sizeof(sh_watch));

  this->file  = sh_util_strdup(file);
  this->watch = nwatch;
  this->flag  = flag;
  return this;
}

/********** List Handling ******************/

struct sh_inotify_litem
{
  sh_watch * watch;
  struct sh_inotify_litem * next;
};

typedef struct {
  struct sh_inotify_litem *prenode;
  struct sh_inotify_litem *curnode;
} sh_inotify_listCursor;

static sh_watch * sh_inotify_list_first(sh_inotify_listCursor * listcursor, sh_watches * watches)
{
  listcursor->prenode = watches->dormant_watches;
  listcursor->curnode = watches->dormant_watches;

  return listcursor->curnode->watch;
}

static sh_watch * sh_inotify_list_next(sh_inotify_listCursor * listcursor, sh_watches * watches)
{
  (void) watches;

  listcursor->prenode = listcursor->curnode;
  listcursor->curnode = listcursor->curnode->next;

  if (listcursor->curnode)
    return listcursor->curnode->watch;
  return NULL;
}

static void sh_inotify_listitem_destroy(struct sh_inotify_litem * this)
{
  SH_FREE(this);
  return;
}

static sh_watch * sh_inotify_list_del_current(sh_inotify_listCursor * listcursor, sh_watches * watches)
{
  sh_watch * ret = NULL;

  if (listcursor->curnode)
    {
      struct sh_inotify_litem * this = listcursor->curnode;

      if (listcursor->prenode == this)
	{
	  watches->dormant_watches = this->next;

	  listcursor->prenode = watches->dormant_watches;
	  listcursor->curnode = watches->dormant_watches;
	}
      else
	{
	  listcursor->prenode->next = this->next;
	  listcursor->curnode       = this->next;
	}
      ret = listcursor->curnode->watch;
      sh_inotify_listitem_destroy(this);
    }
  return ret;
}

static int sh_inotify_add_dormant(sh_watches * watches, sh_watch * item)
{
  struct sh_inotify_litem * this = SH_ALLOC(sizeof(struct sh_inotify_litem));

  this->watch = item;
  this->next  = (struct sh_inotify_litem *) watches->dormant_watches;
  
  watches->dormant_watches = this;
  return 0;
}

/********** End List Handling **************/

static zAVLKey sh_inotify_getkey(void const *item)
{
  return (&((sh_watch *)item)->watch);
}


/* This function removes all watches from the list,
 * and closes the inode file descriptor in this thread.
 */
void sh_inotify_remove(sh_watches * watches)
{
  int     ifd = sh_inotify_getfd();
  zAVLTree   * all_watches = (zAVLTree *)(watches->list_of_watches);

  if (all_watches)
    zAVLFreeTree(all_watches, sh_inotify_free_watch);

  sh_inotify_init(watches);

  if (ifd >= 0)
    close(ifd);
  sh_set_inotify_fd(-1);

  watches->count = 0;

  return;
}

static int index_watched_file(char * filename, sh_watches * watches)
{
  sh_watch   * item;
  zAVLCursor   avlcursor;
  zAVLTree   * all_watches = (zAVLTree *)(watches->list_of_watches);

  if (all_watches)
    {
      for (item = (sh_watch *) zAVLFirst(&avlcursor, all_watches); item;
	   item = (sh_watch *) zAVLNext(&avlcursor))
	{
	  if (item->file)
	    {
	      if (0 == strcmp(filename, item->file))
		return item->watch;
	    }
	}
    }
  return -1;
}

/* This function is idempotent; it will add the watch only once 
 */
int sh_inotify_add_watch(char * filename, sh_watches * watches, int  * errnum)
{
  *errnum = 0;

  if (filename)
    {
      int nwatch;
      sh_watch   * item;
      int index = index_watched_file(filename, watches);
      
      if (index < 0)
	{
	  int     ifd = sh_inotify_getfd();

	  /*************************************

	  if (watches->count == SH_INOTIFY_MAX)
	    {
#ifdef EMFILE
	      *errnum = EMFILE;
#else
	      *errnum = 24;
#endif
	      return -1;
	    }
	  **************************************/

	  nwatch = inotify_add_watch (ifd, filename, 
				      IN_MODIFY|IN_DELETE_SELF|IN_MOVE_SELF|IN_UNMOUNT);
	  if (nwatch < 0)
	    {
	      *errnum = errno;
	      return -1;
	    }

	  item = sh_inotify_create_watch(filename, nwatch, /* flag */ 0);
	  
	  if (NULL == watches->list_of_watches)
	    watches->list_of_watches = zAVLAllocTree (sh_inotify_getkey, zAVL_KEY_INT);
 
	  if (watches->list_of_watches)
	    {
	      *errnum =  zAVLInsert((zAVLTree *)(watches->list_of_watches), item);
	      if (*errnum != 0)
		{
		  sh_inotify_free_watch(item);
		  return -1;
		}
	    }
	  else
	    {
	      *errnum = -1;
	      return -1;
	    }

	  ++(watches->count);
	}
    }
  return 0;
}

int sh_inotify_wait_for_change(char * filename, sh_watches * watches, 
			       int  * errnum, int waitsec)
{
  sh_watch   * item;
  zAVLTree   * all_watches = (zAVLTree *)(watches->list_of_watches);
  int          ifd = sh_inotify_getfd();
  
  *errnum = 0;

 start_it:

  if (ifd >= 0)
    {
      ssize_t len = -1;
      ssize_t  i  = 0;
      int  flag = 0;
      char buffer[1024];

      sh_inotify_listCursor listcursor;

      /* -- Add watch if required 
       */
      if (filename)
	{
	  if (sh_inotify_add_watch(filename, watches, errnum) < 0)
	    {
	      retry_msleep(waitsec, 0);
	      return -1;
	    }
	}

      /* -- Check dormant watches for reopening.
       */
      for (item = sh_inotify_list_first(&listcursor, watches); item;
	   item = sh_inotify_list_next(&listcursor, watches))
	{
	have_next:
	  if (item->file && item->watch == -1)
	    {
	      item->watch = inotify_add_watch (ifd, item->file, 
					       IN_MODIFY|IN_DELETE_SELF|IN_MOVE_SELF|IN_UNMOUNT);
	      if (item->watch >= 0)
		{
		  zAVLInsert(all_watches, item);
		  item = sh_inotify_list_del_current(&listcursor, watches);
		  goto have_next;
		}
	    }
	}


      /* -- Blocking read on inotify file descriptor
       */
      do {
	len = read (ifd, &buffer, sizeof(buffer));
      } while (len < 0 || errno == EINTR);

      if (len > 0)
	{
	  struct inotify_event *event;

	  i = 0;
	  
	  while (i < len) {

	    event = (struct inotify_event *) &buffer[i];

	    item = zAVLSearch(all_watches, &(event->wd));

	    if (item)
	      {
		if (event->mask & IN_MODIFY)
		  {
		    item->flag |= SH_INOTIFY_MODIFY;
		    flag |= SH_INOTIFY_MODIFY;
		  }
		else if (event->mask & IN_DELETE_SELF || 
			 event->mask & IN_UNMOUNT     || 
			 event->mask & IN_MOVE_SELF   )
		  {
		    item->flag |= SH_INOTIFY_REOPEN;
		    (void) inotify_rm_watch(ifd, item->watch);
		    zAVLDelete(all_watches, item);
		    sh_inotify_add_dormant(watches, item);
		    item->watch    = -1;
		    flag |= SH_INOTIFY_REOPEN;
		  }
	      }
	    
	    i += sizeof (struct inotify_event) + event->len;
	  }
	}
      else if (len == -1)
	{
	  *errnum = errno;
	  retry_msleep(waitsec, 0);

	  return -1;
	}

      if (flag & SH_INOTIFY_REOPEN)
	{
	  if (flag & SH_INOTIFY_MODIFY)
	    return 0;
	  else
	    goto start_it;
	}

      return 0;
    }

  /* Inotify not working, sleep
   */
  retry_msleep(waitsec, 0);

  *errnum = 0;
  return -1;
}

/* !defined(HAVE_SYS_INOTIFY_H) */
#else

#include "sh_calls.h"
#include "sh_inotify.h"

void sh_inotify_remove(sh_watches * watches)
{
  (void) watches;
  return;
}

int sh_inotify_wait_for_change(char * filename, sh_watches * watches,
			       int *  errnum, int waitsec)
{
  (void) filename;
  (void) watches;

  /* Inotify not working, sleep for waitsec seconds
   */
  retry_msleep(waitsec, 0);

  *errnum = 0;
  return -1;
}

int sh_inotify_add_watch(char * filename, sh_watches * watches, int  * errnum)
{
  (void) filename;
  (void) watches;
  *errnum = 0;
  return 0;
}

#endif
