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

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


#include <sys/types.h>
#include <time.h>
#include <sys/stat.h>
#include <unistd.h>

#include <stdlib.h>
#include <errno.h>
#include <limits.h>

#if defined(__linux__)

#include "samhain.h"
#include "sh_unix.h"

#define SH_SUBUID_FILE _("/etc/subuid")
#define SH_SUBGID_FILE _("/etc/subgid")

struct subuid_t {
  char name[32];
  unsigned long first;
  unsigned long last;
  struct subuid_t * next;
};

static time_t last_subuid = 0;
static time_t last_subgid = 0;

struct subuid_t * list_subuid = NULL;
struct subuid_t * list_subgid = NULL;

/* Check whether we need to re-read the subuid/subgid file
 */
static int needs_reread (char * file, time_t * last)
{
  int retval = S_FALSE;
  struct stat   buf;
  int status = retry_lstat (FIL__, __LINE__, file, &buf);

  if (status == 0)
    {
      if ((buf.st_mtime - *last) > 1)
	{
	  *last = buf.st_mtime;
	  retval = S_TRUE;
	}
    }
  else if (status && errno == ENOENT)
    {
      /* If there was a file make sure we attempt to re-read
       * to zero out the list. 
       */
      if (*last > 0) retval = S_TRUE;
      *last = 0;
    }
  return retval;
}

/* free the whole list
 */
static void free_subordinate(struct subuid_t * head)
{
  struct subuid_t * prev;
  struct subuid_t * curr = head;
  
  while (curr)
    {
      prev = curr;
      curr = curr->next;
      SH_FREE(prev);
    }
  return;
}

#define NFIELDS_SUBUID 3

static int get_ulong(char * str, unsigned long * result)
{
  char * endptr;

  errno = 0;
  *result = strtoul(str, &endptr, 0);
  if (*str != '\0' && *endptr == '\0' && errno != ERANGE)
    return S_TRUE;
  return S_FALSE;
}

/* Parse a single line into name / startuid / lastuid 
 */
static struct subuid_t * parse_subordinate(char * line)
{
  unsigned int      nfields = NFIELDS_SUBUID;
  size_t            lengths[NFIELDS_SUBUID];
  unsigned long     start, count;
  struct subuid_t * new;
  
  char ** array = split_array(line, &nfields, ':', lengths);

  if (nfields != NFIELDS_SUBUID)
    { SH_FREE(array); return NULL; }

  if (S_TRUE != get_ulong(array[1], &start))
    { SH_FREE(array); return NULL; }
  if ((S_TRUE != get_ulong(array[2], &count)) || (count == 0))
    { SH_FREE(array); return NULL; }
  if (lengths[0] == 0)
    { SH_FREE(array); return NULL; }

  /* we have checked that count != 0 */
  --count;

  if (start > (ULONG_MAX - count))
    { SH_FREE(array); return NULL; }
    
  new = SH_ALLOC(sizeof(struct subuid_t));
  sl_strlcpy(new->name, array[0], 32);
  new->first = start;
  new->last  = start + count; /* start+count-1, but we already did --count */
  new->next  = NULL;

  SH_FREE(array);
  return new;
}

/* (re-)read the subuid/subgid file
 */
static void reread_subordinate (char * file, struct subuid_t ** head_ref)
{
  SL_TICKET fd = (-1);
  char      line[1024];
  
  if (*head_ref) { free_subordinate(*head_ref); *head_ref = NULL; }

  fd = sl_open_read (FIL__, __LINE__, file, SL_YESPRIV);
  if (!SL_ISERROR(fd))
    {
      while ( sh_unix_getline(fd, line, sizeof(line)) > 0 ) 
        {
	  /* for invalid lines, NULL will be returned 
	   */
	  struct subuid_t * new = parse_subordinate(line);

	  if (new)
	    {
	      new->next = *head_ref;
	      *head_ref = new;
	    }
        }
      sl_close(fd);
    }
  return;
}

/* Return the username for a given subuid/subgid 
 */
static char * get_name4id (unsigned long id, struct subuid_t * head)
{
  struct subuid_t * cur = head;
  
  while (cur)
    {
      if (id >= cur->first && id <= cur->last)
	return cur->name;
      cur = cur->next;
    }
  return NULL;
}

/***********************************************
 *
 * Public functions
 *
 ***********************************************/

/* Returns username or NULL for a subuid
 */
char * sh_get_subuid (unsigned long subuid)
{
  static int  init = 0;
  static char file[256];

  if (!init) { sl_strlcpy(file, SH_SUBUID_FILE, sizeof(file)); init = 1; }
  
  if (S_TRUE == needs_reread(file, &last_subuid))
    reread_subordinate(file, &list_subuid); 
  
  return get_name4id (subuid, list_subuid);
}

/* Returns group name or NULL for subgid
 */
char * sh_get_subgid (unsigned long subgid)
{
  static int  init = 0;
  static char file[256];

  if (!init) { sl_strlcpy(file, SH_SUBGID_FILE, sizeof(file)); init = 1; }
  
  if (S_TRUE == needs_reread(file, &last_subgid))
    reread_subordinate(file, &list_subgid); 
  
  return get_name4id (subgid, list_subgid);
}

/* Not Linux, hence no sub(u|g)id
 */
#else

char * sh_get_subuid (unsigned long subuid)
{
  (void) subuid;
  return NULL;
}

char * sh_get_subgid (unsigned long subgid)
{
  (void) subgid;
  return NULL;
}

#endif
