/* SAMHAIN file system integrity testing                                   */
/* Copyright (C) 2011       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.              */

/***************************************************************************
 *
 * This file provides a module for samhain to use inotify for file checking.
 *
 */

#include "config_xor.h"

#if (defined(SH_WITH_CLIENT) || defined(SH_STANDALONE)) 

#include "samhain.h"
#include "sh_utils.h"
#include "sh_modules.h"
#include "sh_pthread.h"
#include "sh_inotify.h"
#include "sh_unix.h"
#include "sh_hash.h"
#include "sh_files.h"
#include "sh_ignore.h"

#define FIL__  _("sh_fInotify.c")

sh_watches sh_file_watches = SH_INOTIFY_INITIALIZER;

static sh_watches sh_file_missing = SH_INOTIFY_INITIALIZER;

#if defined(HAVE_SYS_INOTIFY_H) 

#include <sys/inotify.h>

/* --- Configuration ------- */

static int ShfInotifyActive = S_FALSE;

static unsigned long ShfInotifyWatches = 0;

static int sh_fInotify_active(const char *s) 
{
  int value;
    
  SL_ENTER(_("sh_fInotify_active"));
  value = sh_util_flagval(s, &ShfInotifyActive);
  if (value == 0 && ShfInotifyActive != S_FALSE)
    {
      sh.flag.inotify |= SH_INOTIFY_USE;
      sh.flag.inotify |= SH_INOTIFY_DOSCAN;
      sh.flag.inotify |= SH_INOTIFY_NEEDINIT;
    }
  if (value == 0 && ShfInotifyActive == S_FALSE)
    {
      sh.flag.inotify = 0;
    }
  SL_RETURN((value), _("sh_fInotify_active"));
}

static int sh_fInotify_watches(const char *s) 
{
  int retval = -1;
  char * foo;
  unsigned long value;
    
  SL_ENTER(_("sh_fInotify_watches"));

  value = strtoul(s, &foo, 0);
  if (*foo == '\0')
    {
      ShfInotifyWatches = (value > 2147483647) ? 2147483647 /* MAX_INT_32 */: value;
      retval = 0;
    }
  SL_RETURN((retval), _("sh_fInotify_watches"));
}
  
  
sh_rconf sh_fInotify_table[] = {
    {
        N_("inotifyactive"),
        sh_fInotify_active,
    },
    {
        N_("inotifywatches"),
        sh_fInotify_watches,
    },
    {
        NULL,
        NULL
    }
};

/* --- End Configuration --- */

static int sh_fInotify_init_internal(void);
static int sh_fInotify_process(struct inotify_event * event);
static int sh_fInotify_report(struct inotify_event * event, char * filename,
			      int class, unsigned long check_mask);

int sh_fInotify_init(struct mod_type * arg)
{
#ifndef HAVE_PTHREAD
  (void) arg;
  return SH_MOD_FAILED;
#else

  if (ShfInotifyActive == S_FALSE)
    return SH_MOD_FAILED;

  if (sh.flag.checkSum == SH_CHECK_INIT)
    return SH_MOD_FAILED;

  if (arg != NULL && arg->initval < 0 &&
      (sh.flag.isdaemon == S_TRUE || sh.flag.loop == S_TRUE))
    {
      /* Init from main thread */
      sh.flag.inotify |= SH_INOTIFY_DOSCAN;
      sh.flag.inotify |= SH_INOTIFY_NEEDINIT;

      if (0 == sh_pthread_create(sh_threaded_module_run, (void *)arg))
	{
	  return SH_MOD_THREAD;
	}
      else
	{
	  sh.flag.inotify = 0;
	  return SH_MOD_FAILED;
	}
    }
  else if (arg != NULL && arg->initval == SH_MOD_THREAD &&
	   (sh.flag.isdaemon == S_TRUE || sh.flag.loop == S_TRUE))
    {
      /* Reconfigure from main thread */
      /* sh_fInotify_init_internal(); */
      sh.flag.inotify |= SH_INOTIFY_DOSCAN;
      sh.flag.inotify |= SH_INOTIFY_NEEDINIT;
      return SH_MOD_THREAD;
    }

  /* Within thread, init */
  return sh_fInotify_init_internal();
#endif
}

int sh_fInotify_run()
{
  ssize_t len = -1;
  char *  buffer = SH_ALLOC(16384);

  if (ShfInotifyActive == S_FALSE)
    return SH_MOD_FAILED;

  if ( (sh.flag.inotify & SH_INOTIFY_DOSCAN) ||
       (sh.flag.inotify & SH_INOTIFY_NEEDINIT))
    {
      if (0 != sh_fInotify_init_internal())
	return SH_MOD_FAILED;
    }

  /* Blocking read from inotify file descriptor.
   */
  len = sh_inotify_read(buffer, 16384);
  
  if (len > 0)
    {
      struct inotify_event *event;
      int i = 0;
      
      while (i < len) 
	{
	  event = (struct inotify_event *) &(buffer[i]);
	  
	  sh_fInotify_process(event);
	  
	  i += sizeof (struct inotify_event) + event->len;
	}

      if ( (sh.flag.inotify & SH_INOTIFY_DOSCAN) ||
	   (sh.flag.inotify & SH_INOTIFY_NEEDINIT))
	{
	  if (0 != sh_fInotify_init_internal())
	   return SH_MOD_FAILED;
	}
 
    }

  /* Re-scan 'dormant' list of sh_file_missing. 
   */ 
  sh_inotify_recheck_watches (&sh_file_watches, &sh_file_missing);

  return 0;
}

/* We block in the read() call on the inotify descriptor,
 * so we always run.
 */
int sh_fInotify_timer(time_t tcurrent)
{
  (void) tcurrent;
  return 1;
}

int sh_fInotify_cleanup()
{
  sh_inotify_purge_dormant(&sh_file_watches);
  sh_inotify_remove(&sh_file_watches);
  sh_inotify_init(&sh_file_watches);
  return 0;
}

int sh_fInotify_reconf()
{
  sh.flag.inotify   = 0;

  ShfInotifyWatches = 0;
  ShfInotifyActive  = 0;

  return sh_fInotify_cleanup();
}

#define PROC_WATCHES_MAX _("/proc/sys/fs/inotify/max_user_watches")

static void sh_fInotify_set_nwatches()
{
  if (ShfInotifyWatches == 0)
    return;

  if (0 == access(PROC_WATCHES_MAX, R_OK|W_OK)) /* flawfinder: ignore */
    {
      FILE * fd;

      if (NULL != (fd = fopen(PROC_WATCHES_MAX, "r+")))
	{
	  char   str[128];
	  char * ret;
	  char * ptr;
	  unsigned long  wn;

	  str[0] = '\0';
	  ret = fgets(str, 128, fd);
	  if (ret && *str != '\0')
	    {
	      wn = strtoul(str, &ptr, 0);
	      if (*ptr == '\0' || *ptr == '\n')
		{
		  if (wn < ShfInotifyWatches)
		    {
		      sl_snprintf(str, sizeof(str), "%lu\n", ShfInotifyWatches);
		      (void) fseek(fd, 0L, SEEK_SET);
		      fputs(str, fd);
		    }
		  sl_fclose(FIL__, __LINE__, fd);
		  return;
		}
	    }
	  sl_fclose(FIL__, __LINE__, fd);
	}
    }
  SH_MUTEX_LOCK(mutex_thread_nolog);
  sh_error_handle((-1), FIL__, __LINE__, 0, MSG_E_SUBGEN, 
		  _("Cannot set max_user_watches"), _("sh_fInotify_set_nwatches"));
  SH_MUTEX_UNLOCK(mutex_thread_nolog);
  return;
}

/* The watch fd is thread specific. To have it in the fInotify thread,
 * the main thread writes a list of files/dirs to watch, and here we
 * now pop files from the list to add watches for them.
 */
static int sh_fInotify_init_internal()
{
  char * filename;
  int    class;
  unsigned long check_mask;
  int    retval;
  int    errnum;

  if (ShfInotifyActive == S_FALSE)
    return SH_MOD_FAILED;

  /* Wait until file scan is finished.
   */
  while((sh.flag.inotify & SH_INOTIFY_DOSCAN) != 0)
    {
      retry_msleep(1,0);

      if (ShfInotifyActive == S_FALSE)
	return SH_MOD_FAILED;
    }

  sh_fInotify_set_nwatches();

  while (NULL != (filename = sh_inotify_pop_dormant(&sh_file_watches, 
						    &class, &check_mask)))
    {
      retval = sh_inotify_add_watch(filename, &sh_file_watches, &errnum,
				    class, check_mask);

      if (retval < 0)
	{
	  char errbuf[SH_ERRBUF_SIZE];

	  sh_error_message(errnum, errbuf, sizeof(errbuf));

	  if ((errnum == ENOENT) || (errnum == EEXIST))
	    {
	      char * epath = sh_util_safe_name (filename);
	      SH_MUTEX_LOCK(mutex_thread_nolog);
	      sh_error_handle( (class == SH_LEVEL_ALLIGNORE) ? 
			       ShDFLevel[class] : 
			       ShDFLevel[SH_ERR_T_FILE], 
			       FIL__, __LINE__, errnum, MSG_E_SUBGPATH, 
			       errbuf, _("sh_fInotify_init_internal"), epath);
	      SH_MUTEX_UNLOCK(mutex_thread_nolog);
	      SH_FREE(epath);
	    }
	  else
	    {
	      SH_MUTEX_LOCK(mutex_thread_nolog);
	      sh_error_handle((-1), FIL__, __LINE__, errnum, MSG_E_SUBGEN, 
			       errbuf, _("sh_fInotify_init_internal"));
	      SH_MUTEX_UNLOCK(mutex_thread_nolog);
	    }
	}
    }

  /* Need this because mod_check() may run after
   * DOSCAN is finished, hence wouldn't call init().
   */
  sh.flag.inotify &= ~SH_INOTIFY_NEEDINIT;

  return 0;
}

static int sh_fInotify_process(struct inotify_event * event)
{
  int class;
  unsigned long check_mask;
  char * filename;

  if (event->wd >= 0)
    {
      filename = sh_inotify_search_item(&sh_file_watches, event->wd, 
					&class, &check_mask);

      if (filename)
	{
	  sh_fInotify_report(event, filename, class, check_mask);

	  SH_FREE(filename);
	}
      else if (sh.flag.inotify & SH_INOTIFY_NEEDINIT)
	{
	  return 1;
	}
      else if ((event->mask & IN_UNMOUNT) == 0 && (event->mask & IN_IGNORED) == 0)
	{
	  /* Remove watch ? Seems reasonable. */
	  sh_inotify_rm_watch(NULL, NULL, event->wd);
	  SH_MUTEX_LOCK(mutex_thread_nolog);
	  sh_error_handle((-1), FIL__, __LINE__, event->wd, MSG_E_SUBGEN, 
			  _("Watch removed: internal error - file path unknown"), 
			  _("sh_fInotify_process"));
	  SH_MUTEX_UNLOCK(mutex_thread_nolog);
	}
    }
  else if ((event->mask & IN_Q_OVERFLOW) != 0)
    {
      sh.flag.inotify |= SH_INOTIFY_DOSCAN;
      sh.flag.inotify |= SH_INOTIFY_NEEDINIT;
      return 1;
    }

  return 0;
}

void sh_fInotify_report_add(char * path, int class, unsigned long check_mask)
{
  if (S_FALSE == sh_ignore_chk_new(path))
    {
      int reported = 0;

      sh_files_clear_file_reported(path);
      
      sh_files_search_file(path, &class, &check_mask, &reported);
      
      sh_files_filecheck (class, check_mask, path, NULL,
			  &reported, 0);
      if (SH_FFLAG_REPORTED_SET(reported))
	sh_files_set_file_reported(path);
    }
  return;
}


static void sh_fInotify_report_miss(char * name, int level)
{
  char * tmp = sh_util_safe_name (name);

  SH_MUTEX_LOCK(mutex_thread_nolog);
  sh_error_handle (level, FIL__, __LINE__, 0, MSG_FI_MISS, tmp);
  SH_MUTEX_UNLOCK(mutex_thread_nolog);

  SH_FREE(tmp);
  return;
}

static int sh_fInotify_report(struct inotify_event * event, char * filename,
			      int class, unsigned long check_mask)
{
  char * fullpath = NULL;
  char * path;
  int    reported;

  if (event->len > 0)
    {
      fullpath = sh_util_strconcat(filename, "/", event->name, NULL);
      path = fullpath;
    }
  else
    {
      path = filename;
    }

  if ( (event->mask & (IN_ACCESS|IN_MODIFY)) != 0)
    {
      sh_files_search_file(path, &class, &check_mask, &reported);

      sh_files_filecheck (class, check_mask, filename,
			  (event->len > 0) ? event->name : NULL,
			  &reported, 0);
    }
  else if ((event->mask & (IN_DELETE|IN_DELETE_SELF|IN_MOVE_SELF|IN_MOVED_FROM)) != 0)
    {
      int isdir = (event->mask & IN_ISDIR);
      int level = (class == SH_LEVEL_ALLIGNORE) ? 
	ShDFLevel[class] : 
	ShDFLevel[(isdir == 0) ? SH_ERR_T_FILE : SH_ERR_T_DIR];

      if (S_FALSE == sh_ignore_chk_del(path))
	{
	  if (0 != hashreport_missing(path, level))
	    {
	      sh_fInotify_report_miss(path, level);
	    }
	}

#ifndef REPLACE_OLD
      sh_hash_set_visited_true(path);
#else
      sh_hash_set_missing(path);
#endif
      if (sh.flag.reportonce == S_TRUE)
	sh_files_set_file_reported(path);

      /* Move to 'dormant' list. 
       */
      sh_inotify_rm_watch(&sh_file_watches, &sh_file_missing, event->wd);
    }
  else if((event->mask & (IN_CREATE|IN_MOVED_TO)) != 0)
    {
      if (S_FALSE == sh_ignore_chk_new(path))
	{
	  sh_files_clear_file_reported(path);
	  
	  sh_files_search_file(path, &class, &check_mask, &reported);
	  
	  sh_files_filecheck (class, check_mask, filename,
			      (event->len > 0) ? event->name : NULL,
			      &reported, 0);
	  if (SH_FFLAG_REPORTED_SET(reported))
	    sh_files_set_file_reported(path);
	}
    }

  return 0;
}


#endif

#endif
