/*  devfsd.c

    Main file for  devfsd  (devfs daemon for Linux).

    Copyright (C) 1998-1999  Richard Gooch

    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.

    Richard Gooch may be reached by email at  rgooch@atnf.csiro.au
    The postal address is:
      Richard Gooch, c/o ATNF, P. O. Box 76, Epping, N.S.W., 2121, Australia.
*/

/*
    This programme will notify manage the devfs filesystem.


    Written by      Richard Gooch   9-AUG-1998

    Updated by      Richard Gooch   11-AUG-1998

    Updated by      Richard Gooch   10-SEP-1998: Added support for asynchronous
  open and close events.

    Updated by      Richard Gooch   11-SEP-1998: Fixed bug in <read_config>
  where <<line>> pointer was dereferenced before being initialised.

    Updated by      Richard Gooch   9-JUN-1999: Added variable expansion
  support.

    Last updated by Richard Gooch   10-JUN-1999: Added "devname" variable and
  renamed "device" variable name to "devpath".


*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <pwd.h>
#include <grp.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <dirent.h>
#include <fcntl.h>
#include <linux/devfs_fs.h>
#include <syslog.h>
#include <signal.h>
#include <regex.h>
#include <errno.h>
#include <karma.h>


#define VERSION "1.2.1"
#define CONFIG_FILE "/etc/devfsd.conf"
#define MAX_ARGS 6

#define ACTION_PERMISSIONS              0x0000
#define ACTION_EXECUTE_ON_REGISTER      0x0100
#define ACTION_EXECUTE_ON_UNREGISTER    0x0200
#define ACTION_EXECUTE_ON_ASYNC_OPEN    0x0300
#define ACTION_EXECUTE_ON_CLOSE         0x0400
#define ACTION_EXECUTE_MASK             0xff00

struct permissions_type
{
    mode_t mode;
    uid_t uid;
    gid_t gid;
};

struct execute_type
{
    char *argv[MAX_ARGS + 1];
};

struct config_entry
{
    unsigned int action;
    regex_t preg;
    union
    {
	struct permissions_type permissions;
	struct execute_type execute;
    }
    u;
    struct config_entry *next;
};



/*  External functions  */
EXTERN_FUNCTION (flag st_expr_expand,
		 (char *output, unsigned int length, CONST char *input,
		  CONST char *(*get_variable) (CONST char *variable,
					       void *info),
		  void *info, FILE *errfp) );


/*  Private functions  */
static void read_config ();
static void do_servicing (int fd);
static void service_name (const struct devfsd_notify_struct *info);
static void update_permissions (const struct devfsd_notify_struct *info,
				const struct config_entry *entry);
static void execute (const struct devfsd_notify_struct *info,
		     const struct config_entry *entry);
static void free_config ();
static void do_test (int fd);
static uid_t get_uid (const char *string);
static gid_t get_gid (const char *string);
static mode_t get_mode (const char *string);
static void sig_handler (int sig);
static CONST char *get_variable (CONST char *variable, void *info);


/*  Private data  */
static struct config_entry *first_config = NULL;
static struct config_entry *last_config = NULL;
static const char *mount_point = NULL;


/*  Public functions follow  */

int main (int argc, char **argv)
{
    flag print_version = FALSE;
    flag test = FALSE;
    int fd, proto_rev;
    struct sigaction new_action;
    char filename[STRING_LENGTH];
    static char usage[] = "devfsd mntpnt [-vt]";

    if ( (argc < 2) || (argc > 3) )
    {
	fprintf (stderr, "Usage:\t%s\n", usage);
	exit (1);
    }
    if (argc == 3)
    {
	if (strcmp (argv[2], "-v") == 0) print_version = TRUE;
	else if (strcmp (argv[2], "-t") == 0) test = TRUE;
	else
	{
	    fprintf (stderr, "Usage:\t%s\n", usage);
	    exit (1);
	}
    }
    mount_point = argv[1];
    strcpy (filename, argv[1]);
    strcat (filename, "/.devfsd");
    if (print_version)
    {
	fprintf (stderr, "devfsd: Linux device filesystem daemon v%s\n",
		 VERSION);
	fprintf (stderr,
		 "(C) 1998-1999  Richard Gooch <rgooch@atnf.csiro.au>\n\n");
    }
    if (print_version || test)
    {
	fprintf (stderr, "Daemon protocol revision:\t%d\n",
		 DEVFSD_PROTOCOL_REVISION);
    }
    if ( ( fd = open (filename, O_RDONLY, 0) ) < 0 )
    {
	fprintf (stderr, "Error opening file: \"%s\"\t%s\n",
		 filename, ERRSTRING);
	exit (1);
    }
    if (ioctl (fd, DEVFSDIOC_GET_PROTO_REV, &proto_rev) != 0)
    {
	fprintf (stderr, "Error getting protocol revision\t%s\n", ERRSTRING);
	exit (1);
    }
    if ( print_version || test || (DEVFSD_PROTOCOL_REVISION != proto_rev) )
    {
	if (!test) close (fd);
	fprintf (stderr, "Daemon protocol revision:\t%d\n",
		 DEVFSD_PROTOCOL_REVISION);
	fprintf (stderr, "Kernel-side protocol revision:\t%d\n", proto_rev);
	if (DEVFSD_PROTOCOL_REVISION != proto_rev)
	{
	    fprintf (stderr, "Protocol mismatch!\n");
	    exit (1);
	}
	if (!test) exit (0);
    }
    if (test) do_test (fd);
    sigemptyset (&new_action.sa_mask);
    new_action.sa_flags = 0;
    /*  Set up control_c handler  */
    new_action.sa_handler = sig_handler;
    if (sigaction (SIGHUP, &new_action, (struct sigaction *) NULL) != 0)
    {
	fprintf (stderr, "Error setting SIGHUP handler\t%s\n", ERRSTRING);
	exit (1);
    }
    switch ( fork () )
    {
      case 0:
	/*  Child  */
	break;
      case -1:
	/*  Error  */
	perror ("devfsd");
	exit (2);
	/*break;*/
      default:
	/*  Parent  */
	exit (0);
	/*break;*/
    }
    openlog ("devfsd", LOG_PID, LOG_DAEMON);
    close (0);
    close (1);
    close (2);
    while (TRUE)
    {
	read_config ();
	/*  Carry on daemon  */
	do_servicing (fd);
	free_config ();
    }
}   /*  End Function main  */


/*  Private functions follow  */
static void read_config ()
/*  [SUMMARY] Read the configuration file.
    [RETURNS] Nothing.
*/
{
    char *ptr, *line;
    FILE *fp;
    char buf[STRING_LENGTH];

    if ( ( fp = fopen (CONFIG_FILE, "r") ) == NULL )
    {
	syslog (LOG_ERR, "error opening file: \"%s\"\t%s\n",
		 CONFIG_FILE, ERRSTRING);
	exit (1);
    }
    while (fgets (buf, STRING_LENGTH, fp) != NULL)
    {
	int err, num_args, count;
	struct config_entry *new;
	char p[MAX_ARGS][STRING_LENGTH], key[STRING_LENGTH];
	char name[STRING_LENGTH], tmp[STRING_LENGTH];

	buf[strlen (buf) - 1] = '\0';
	/*  Skip whitespace  */
	for (line = buf; isspace (*line); ++line);
	if (line[0] == '\0') continue;
	if (line[0] == '#') continue;
	for (count = 0; count < MAX_ARGS; ++count) p[count][0] = '\0';
	num_args = sscanf (line, "%s %s %s %s %s %s %s %s",
			   key, name, p[0], p[1], p[2], p[3],
			   p[4], p[5]);
	if (num_args < 2)
	{
	    syslog (LOG_ERR, "bad config line: \"%s\"\n", line);
	    syslog (LOG_ERR, "exiting\n");
	    exit (1);
	}
	if ( ( new = malloc (sizeof *new) ) == NULL )
	{
	    syslog (LOG_ERR, "error allocating\n");
	    syslog (LOG_ERR, "exiting\n");
	    exit (1);
	}
	memset (new, 0, sizeof *new);
	if (strcasecmp (key, "PERMISSIONS") == 0)
	    new->action = ACTION_PERMISSIONS;
	else if (strcasecmp (key, "REG_EXE") == 0)
	    new->action = ACTION_EXECUTE_ON_REGISTER;
	else if (strcasecmp (key, "UNREG_EXE") == 0)
	    new->action = ACTION_EXECUTE_ON_UNREGISTER;
	else if (strcasecmp (key, "ASYNC_OPEN_EXE") == 0)
	    new->action = ACTION_EXECUTE_ON_ASYNC_OPEN;
	else if (strcasecmp (key, "CLOSE_EXE") == 0)
	    new->action = ACTION_EXECUTE_ON_CLOSE;
	else
	{
	    syslog (LOG_ERR, "bad config line: \"%s\"\n", line);
	    syslog (LOG_ERR, "exiting\n");
	    exit (1);
	}
	if ( ( err = regcomp (&new->preg, name, 0) ) != 0 )
	{
	    regerror (err, &new->preg, tmp, STRING_LENGTH);
	    syslog (LOG_ERR, "error compiling regexp: \"%s\"\t%s\n", name,tmp);
	    syslog (LOG_ERR, "exiting\n");
	    exit (1);
	}
	switch (new->action)
	{
	  case ACTION_PERMISSIONS:
	    /*  Get user and group  */
	    if ( ( ptr = strchr (p[0], '.') ) == NULL )
	    {
		syslog (LOG_ERR, "missing '.' character in: \"%s\"\n", p[0]);
		syslog (LOG_ERR, "exiting\n");
		exit (1);
	    }
	    *ptr++ = '\0';
	    new->u.permissions.uid = get_uid (p[0]);
	    new->u.permissions.gid = get_gid (ptr);
	    /*  Get mode  */
	    new->u.permissions.mode = get_mode (p[1]);
	    break;
	  case ACTION_EXECUTE_ON_REGISTER:
	  case ACTION_EXECUTE_ON_UNREGISTER:
	  case ACTION_EXECUTE_ON_ASYNC_OPEN:
	  case ACTION_EXECUTE_ON_CLOSE:
	    num_args -= 2;
	    for (count = 0; count < num_args; ++count)
	    {
		err = strlen (p[count]) + 1;
		if ( ( new->u.execute.argv[count] = malloc (err) ) == NULL )
		{
		    syslog (LOG_ERR, "error allocating\n");
		    syslog (LOG_ERR, "exiting\n");
		    exit (1);
		}
		strcpy (new->u.execute.argv[count], p[count]);
	    }
	    new->u.execute.argv[num_args] = NULL;
	    break;
	}
	new->next = NULL;
	if (first_config == NULL) first_config = new;
	else last_config->next = new;
	last_config = new;
    }
    fclose (fp);
    syslog (LOG_INFO, "read config file: \"%s\"\n", CONFIG_FILE);
}   /*  End Function read_config   */

static void do_servicing (int fd)
/*  [SUMMARY] Service devfs changes until a signal is received.
    <fd> The open control file.
    [RETURNS] Nothing.
*/
{
    ssize_t bytes;
    struct devfsd_notify_struct info;

    while ( ( bytes = read (fd, (char *) &info, sizeof info) ) >= 0 )
    {
	if (bytes < 1)
	{
	    syslog (LOG_ERR, "devfs closed file!\n");
	    exit (1);
	}
	service_name (&info);
    }
    if (errno == EINTR) return;
    syslog (LOG_ERR, "Error reading control file\t%s\n", ERRSTRING);
    exit (1);
}   /*  End Function do_servicing  */

static void service_name (const struct devfsd_notify_struct *info)
/*  [SUMMARY] Service a single devfs change.
    <info> The devfs change.
    [RETURNS] Nothing.
*/
{
    regmatch_t mbuf;
    struct config_entry *entry;

    for (entry = first_config; entry != NULL; entry = entry->next)
    {
	if (regexec (&entry->preg, info->devname, 1, &mbuf, 0) != 0) continue;
	/*  We have a match, does the action match the type?  */
	switch (info->type)
	{
	  case DEVFSD_NOTIFY_REGISTERED:
	    switch (entry->action)
	    {
	      case ACTION_PERMISSIONS:
		update_permissions (info, entry);
		break;
	      case ACTION_EXECUTE_ON_REGISTER:
		execute (info, entry);
		break;
	    }
	    break;
	  case DEVFSD_NOTIFY_UNREGISTERED:
	    switch (entry->action)
	    {
	      case ACTION_EXECUTE_ON_UNREGISTER:
		execute (info, entry);
		break;
	    }
	    break;
	  case DEVFSD_NOTIFY_ASYNC_OPEN:
	    switch (entry->action)
	    {
	      case ACTION_EXECUTE_ON_ASYNC_OPEN:
		execute (info, entry);
		break;
	    }
	    break;
	  case DEVFSD_NOTIFY_CLOSE:
	    switch (entry->action)
	    {
	      case ACTION_EXECUTE_ON_CLOSE:
		execute (info, entry);
		break;
	    }
	    break;
	  default:
	    syslog (LOG_ERR, "Unknown event type: %u\n", info->type);
	    exit (1);
	    /*break;*/
	}
    }
}   /*  End Function service_name  */

static void update_permissions (const struct devfsd_notify_struct *info,
				const struct config_entry *entry)
/*  [SUMMARY] Update permissions for a device entry.
    <info> The devfs change.
    <entry> The config file entry.
    [RETURNS] Nothing.
*/
{
    mode_t new_mode;
    char path[STRING_LENGTH];

    strcpy (path, mount_point);
    strcat (path, "/");
    strcat (path, info->devname);
    new_mode = (info->mode & S_IFMT) | (entry->u.permissions.mode & ~S_IFMT);
    if (new_mode != info->mode)
    {
	if (chmod (path, new_mode) != 0)
	{
	    syslog (LOG_ERR, "error changing mode for: \"%s\"\t%s\n",
		    path, ERRSTRING);
	    syslog (LOG_ERR, "exiting\n");
	    exit (1);
	}
    }
    if ( (entry->u.permissions.uid != info->uid) ||
	 (entry->u.permissions.gid != info->gid) )
    {
	if (chown (path, entry->u.permissions.uid, entry->u.permissions.gid)
	    != 0)
	{
	    syslog (LOG_ERR, "error changing mode for: \"%s\"\t%s\n",
		    path, ERRSTRING);
	    syslog (LOG_ERR, "exiting\n");
	    exit (1);
	}
    }
}   /*  End Function update_permissions  */

static void execute (const struct devfsd_notify_struct *info,
		     const struct config_entry *entry)
/*  [SUMMARY] Execute a programme.
    <info> The devfs change.
    <entry> The config file entry.
    [RETURNS] Nothing.
*/
{
    unsigned int count;
    char *argv[MAX_ARGS];
    char largv[MAX_ARGS][STRING_LENGTH];
    char devname[STRING_LENGTH];

    strcpy (devname, mount_point);
    strcat (devname, "/");
    strcat (devname, info->devname);
    argv[MAX_ARGS] = NULL;
    for (count = 0; count < MAX_ARGS; ++count)
    {
	if (entry->u.execute.argv[count] == NULL)
	{
	    argv[count] = NULL;
	    break;
	}
	st_expr_expand (largv[count], STRING_LENGTH,
			entry->u.execute.argv[count], get_variable, devname,
			NULL);
	argv[count] = largv[count];
    }
    switch ( fork () )
    {
      case 0:
	/*  Child  */
	break;
      case -1:
	/*  Error  */
	syslog (LOG_ERR, "error forking\t%s\n", ERRSTRING);
	exit (2);
	/*break;*/
      default:
	/*  Parent  */
	wait (NULL);
	return;
	/*break;*/
    }
    execvp (argv[0], argv);
    syslog (LOG_ERR, "error execing: \"%s\"\t%s\n", argv[0], ERRSTRING);
}   /*  End Function execute  */

static void free_config ()
/*  [SUMMARY] Free the configuration information.
    [RETURNS] Nothing.
*/
{
    struct config_entry *entry, *next;

    for (entry = first_config; entry != NULL; entry = next)
    {
	unsigned int count;

	next = entry->next;
	regfree (&entry->preg);
	if (entry->action & ACTION_EXECUTE_MASK)
	{
	    for (count = 0; count < MAX_ARGS; ++count)
	    {
		if (entry->u.execute.argv[count] == NULL) break;
		free (entry->u.execute.argv[count]);
	    }
	}
	free (entry);
    }
    first_config = NULL;
    last_config = NULL;
}   /*  End Function free_config  */

static void do_test (int fd)
/*  [SUMMARY] Test the devfs protocol.
    <fd> The open control file.
    [RETURNS] Nothing.
*/
{
    ssize_t bytes;
    struct devfsd_notify_struct info;
    char type[STRING_LENGTH];

    fprintf (stderr, "Running in test mode: no actions will be taken...\n");
    while ( ( bytes = read (fd, (char *) &info, sizeof info) ) >= 0 )
    {
	if (bytes < 1)
	{
	    fprintf (stderr, "devfs closed file!\n");
	    exit (1);
	}
	switch (info.type)
	{
	  case DEVFSD_NOTIFY_REGISTERED:
	    strcpy (type, "registered");
	    break;
	  case DEVFSD_NOTIFY_UNREGISTERED:
	    strcpy (type, "unregistered");
	    break;
	  case DEVFSD_NOTIFY_ASYNC_OPEN:
	    strcpy (type, "asynchonously opened");
	    break;
	  case DEVFSD_NOTIFY_CLOSE:
	    strcpy (type, "closed");
	    break;
	  default:
	    fprintf (stderr, "Unknown type: %u\n", info.type);
	    exit (1);
	    /*break;*/
	}
	fprintf (stderr, "Entry: \"%s\" %s mode: %o uid: %d gid: %d\n",
		 info.devname, type, (int) info.mode, (int) info.uid,
		 (int) info.gid);
    }
    fprintf (stderr, "Error reading control file\t%s\n", ERRSTRING);
    exit (1);
}   /*  End Function do_test  */

static uid_t get_uid (const char *string)
/*  [SUMMARY] Convert a string to a UID value.
    <string> The string.
    [RETURNS] The UID value.
*/
{
    struct passwd *pw_ent;

    if ( isdigit (string[0]) ) return atoi (string);
    if ( ( pw_ent = getpwnam (string) ) == NULL )
    {
	syslog (LOG_ERR, "unknown user: \"%s\"\n", string);
	syslog (LOG_ERR, "exiting\n");
	exit (1);
    }
    return (pw_ent->pw_uid);
}   /*  End Function get_uid  */

static gid_t get_gid (const char *string)
/*  [SUMMARY] Convert a string to a GID value.
    <string> The string.
    [RETURNS] The GID value.
*/
{
    struct group *grp_ent;

    if ( isdigit (string[0]) ) return atoi (string);
    if ( ( grp_ent = getgrnam (string) ) == NULL )
    {
	syslog (LOG_ERR, "unknown group: \"%s\"\n", string);
	syslog (LOG_ERR, "exiting\n");
	exit (1);
    }
    return (grp_ent->gr_gid);
}   /*  End Function get_gid  */

static mode_t get_mode (const char *string)
/*  [SUMMARY] Convert a string to a mode value.
    <string> The string.
    [RETURNS] The mode value.
*/
{
    mode_t mode;

    if ( isdigit (string[0]) ) return strtoul (string, NULL, 8);
    if (strlen (string) != 9)
    {
	syslog (LOG_ERR, "bad symbolic mode: \"%s\"\n", string);
	syslog (LOG_ERR, "exiting\n");
	exit (1);
    }
    mode = 0;
    if (string[0] == 'r') mode |= S_IRUSR;
    if (string[1] == 'w') mode |= S_IWUSR;
    if (string[2] == 'x') mode |= S_IXUSR;
    if (string[3] == 'r') mode |= S_IRGRP;
    if (string[4] == 'w') mode |= S_IWGRP;
    if (string[5] == 'x') mode |= S_IXGRP;
    if (string[6] == 'r') mode |= S_IROTH;
    if (string[7] == 'w') mode |= S_IWOTH;
    if (string[8] == 'x') mode |= S_IXOTH;
    return (mode);
}   /*  End Function get_mode  */

static void sig_handler (int sig)
{
    syslog (LOG_INFO, "Caught SIGHUP\n");
}   /*  End Function sig_handler  */

static CONST char *get_variable (CONST char *variable, void *info)
{
    if (strcmp (variable, "devpath") == 0) return (info);
    if (strcmp (variable, "devname") == 0) return strchr (info, '/') + 1;
    return (NULL);
}   /*  End Function get_variable  */

