/*
** rmtd.c            A remote tape device access daemon
**
** Copyright (c) 1993 Peter Eriksson <pen@lysator.liu.se>, Lysator ACS, Sweden
**
** 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, 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 <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <syslog.h>
#include <pwd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <ident.h>
#include "fnmatch.h"


#define MAX_ACLS	256
#define MAX_UMAPS	256

char *path_rmtdconf = "/etc/rmtd.conf";
char *path_rmt      = "/etc/rmt";
char *default_luser = "nobody";
char *chroot_dir    = NULL;

int debug_level     = 0;


struct usermap
{
    int nmaps;
    char *ruser[MAX_UMAPS];
    char *luser[MAX_UMAPS];
};

struct acl
{
    char *address;
    struct usermap *usermap;
} acl_l[MAX_ACLS];
int acl_c = 0;


char *xstrdup(str)
    char *str;
{
    char *rstr;

    rstr = strdup(str);
    if (rstr == NULL)
    {
	syslog(LOG_CRIT, "Resource allocation failuer: strdup(%p): %m",
	       str);
	exit(1);
    }

    return rstr;
}

struct usermap *mk_usermap(mapstr)
    char *mapstr;
{
    struct usermap *up;
    int i;
    char *cp, *cp2;

    if (debug_level)
	syslog(LOG_DEBUG, "Entering: mk_usermap(\"%s\")", mapstr);

    if (mapstr == NULL || (cp = strtok(mapstr, ", \t\n")) == NULL)
    {
	if (debug_level)
	    syslog(LOG_DEBUG, "  Returning NULL (cp = %p)", cp);
	return NULL;
    }
    
    
    up = malloc(sizeof(*up));
    if (up == NULL)
    {
	syslog(LOG_CRIT, "Resource allocation failure: malloc(%d): %m",
	       sizeof(*up));
	exit(1);
    }
    
    up->nmaps = 0;

    do
    {
	if (cp[0] == '-')
	{
	    up->ruser[up->nmaps] = xstrdup(cp+1);
	    up->luser[up->nmaps] = NULL;
	}
	else
	{
	    cp2 = strchr(cp, '=');
	    if (cp2)
	    {
		*cp2++ = '\0';
		up->luser[up->nmaps] = xstrdup(cp2);
	    }
	    else
		up->luser[up->nmaps] = xstrdup(cp);
	    
	    up->ruser[up->nmaps] = xstrdup(cp);
	}
	
	if (debug_level)
	    syslog(LOG_DEBUG, "Adding usermap: %s -> %s",
		   up->ruser[up->nmaps],
		   up->luser[up->nmaps] ? up->luser[up->nmaps] : "<NULL>");
	
	up->nmaps++;
	
	cp = strtok(NULL, ", \t\n");
    } while (cp && up->nmaps < MAX_UMAPS);

    if (debug_level)
	syslog(LOG_DEBUG, "Exiting: mk_usermap()");
    
    return up;
}


add_acl(address, usermap)
    char *address;
    char *usermap;
{
    if (acl_c < MAX_ACLS)
    {
	acl_l[acl_c].address = xstrdup(address);
	acl_l[acl_c].usermap = mk_usermap(usermap);
	
	acl_c++;
    }
    else
    {
	syslog(LOG_ERR, "Access control list overflow, more than %d entries",
	       MAX_ACLS);
	exit(1);
    }
}


load_acl(path)
    char *path;
{
    FILE *fp;
    char buf[10240];
    char *cp;
    int lineno;
    int len;
    

    if (debug_level)
	syslog(LOG_DEBUG, "Entering: load_acl(\"%s\")", path);
    
    acl_c = 0;
    
    fp = fopen(path, "r");
    if (fp == NULL)
    {
	syslog(LOG_ERR, "load_acl: fopen(\"%s\"): %m", path);
	exit(1);
    }

    lineno = 0;
    
    while (fgets(buf, sizeof(buf), fp))
    {
	lineno++;

	if (debug_level > 2)
	    syslog(LOG_DEBUG, "Got line #%d: %s", lineno, buf);
	
	len = strlen(buf);
	if (len == 0)
	    continue;

	/* Handle line continuation character (\) */
	while (sizeof(buf)-(len+1) > 0 &&
	       buf[len-1] == '\n' && buf[len-2] == '\\' &&
	       fgets(buf+(len-2), sizeof(buf)-(len-2), fp))
	{
	    lineno++;
	    len = strlen(buf);
	}

	/* Remove comments */
	cp = strrchr(buf, '#');
	if (cp)
	    *cp = '\0';

	cp = strtok(buf, " \t\n");
	if (!cp)
	    continue;

	if (debug_level)
	    syslog(LOG_DEBUG, "Got keyword '%s'", cp);
	
	if (strcmp(cp, "access") == 0)
	{
	    
	    cp = strtok(NULL, " \t\n");
	    if (!cp)
	    {
		syslog(LOG_ERR,
		       "Malformed 'access' line in config file on line %d",
		       lineno);
		exit(1);
	    }

	    if (debug_level)
		syslog(LOG_DEBUG, "  and '%s' host glob", cp);

	    add_acl(cp, strtok(NULL, " \t\n"));
	}
	else if (strcmp(cp, "rmt-path") == 0)
	{
	    cp = strtok(NULL, " \t\n");
	    if (!cp)
	    {
		syslog(LOG_ERR,
		       "Malformed 'rmt-path' line in config file on line %d",
		       lineno);
		exit(1);
	    }

	    path_rmt = xstrdup(cp);
	}
	else if (strcmp(cp, "default-user") == 0)
	{
	    cp = strtok(NULL, " \t\n");
	    if (!cp)
	    {
		syslog(LOG_ERR,
	       "Malformed 'default-user' line in config file on line %d",
		       lineno);
		exit(1);
	    }

	    default_luser = xstrdup(cp);
	}
	else if (strcmp(cp, "chroot") == 0)
	{
	    cp = strtok(NULL, " \t\n");
	    if (!cp)
	    {
		syslog(LOG_ERR,
	       "Malformed 'chroot' line in config file on line %d",
		       lineno);
		exit(1);
	    }

	    chroot_dir = xstrdup(cp);
	}
	else if (strcmp(cp, "include") == 0)
	{
	    cp = strtok(NULL, " \t\n");
	    if (!cp)
	    {
		syslog(LOG_ERR,
	       "Malformed 'include' line in config file on line %d",
		       lineno);
		exit(1);
	    }

	    load_acl(cp);
	}
	else if (strcmp(cp, "debug-level") == 0)
	{
	    cp = strtok(NULL, " \t\n");
	    if (!cp)
	    {
		syslog(LOG_ERR,
	       "Malformed 'debug-level' line in config file on line %d",
		       lineno);
		exit(1);
	    }

	    debug_level = atoi(cp);
	}
	else
	{
	    syslog(LOG_ERR,
		   "Illegal config file command '%s' on line %d",
		   cp, lineno);
	    exit(1);
	}
    }

    if (debug_level)
	syslog(LOG_DEBUG, "Exiting: load_acl()");
    
    fclose(fp);
}


int check_acl(ruser, addr, luser)
    char *ruser;
    char *addr;
    char **luser;
{
    struct usermap *up;
    int i, j;


    if (debug_level)
	syslog(LOG_DEBUG, "check_acl(\"%s\", \"%s\")", ruser, addr);
    
    for (i = 0; i < acl_c; i++)
    {
	if (fnmatch(acl_l[i].address, addr, FNM_NOESCAPE) == 0)
	{
	    if (debug_level)
		syslog(LOG_DEBUG, "   address match found: %s",
		       acl_l[i].address);
	    
	    up = acl_l[i].usermap;

	    if (up)
	    {
		if (debug_level)
		    syslog(LOG_DEBUG, "   checking usermap");
		
		for (j = 0; j < up->nmaps; j++)
		    if (fnmatch(up->ruser[j], ruser, FNM_NOESCAPE) == 0)
		    {
			if (debug_level)
			    syslog(LOG_DEBUG, "      user match found: %s=%s",
				   up->ruser[j],
				   up->luser[j] ? up->luser[j] : "<null>");
			
			*luser = up->luser[j];
			if (*luser == NULL)
			    return -1;

			if (strcmp(*luser, "*") == 0)
			    *luser = ruser;
			
			return 1;
		    }

		return 0;
	    }
	    else
	    {
		*luser = default_luser;
		return 1;
	    }
	}
    }
    
    return 0;
}


int check_hacl(ruser, hp, luser)
    char *ruser;
    struct hostent *hp;
    char **luser;
{
    int rcode;
    int i;
    
    
    if (rcode = check_acl(ruser, hp->h_name, luser))
	return rcode;
    
    for (i = 0; hp->h_aliases[i]; i++)
	if (rcode = check_acl(ruser, hp->h_aliases[i], luser))
	    return rcode;

    return 0;
}


int main(argc,argv)
    int argc;
    char *argv[];
{
    struct hostent *hp;
    struct passwd *pw;
    struct sockaddr_in sin;
    int sinlen;
    int i;
    char *naddr;
    char *saddr;
    char *ruser;
    char *luser;

    
    openlog("rmtd", LOG_PID, LOG_DAEMON);

    for (i = 1; i < argc && argv[i][0] == '-'; i++)
	switch (argv[i][1])
	{
	  case 'f':
	    path_rmtdconf = argv[i]+2;
	    break;

	  case 'd':
	    debug_level = atoi(argv[i]+2);
	    break;
	    
	  default:
	    syslog(LOG_ERR, "Illegal command line switch: %s", argv[i]);
	    exit(1);
	}
    
    load_acl(path_rmtdconf);
    
    sinlen = sizeof(sin);
    if (getpeername(0, (struct sockaddr *) &sin, &sinlen) < 0)
    {
	syslog(LOG_ERR, "getpeeraddr: %m");
	exit(1);
    }

    naddr = inet_ntoa(sin.sin_addr);
    if (naddr == NULL)
    {
	syslog(LOG_ERR, "inet_ntoa -> NULL addr");
	exit(1);
    }

    hp = gethostbyaddr((char *) &sin.sin_addr,
			  sizeof(sin.sin_addr), AF_INET);
    if (hp == NULL)
    {
	syslog(LOG_ERR, "gethostbyaddr() -> NULL addr");
	exit(1);
    }
    else
	saddr = hp->h_name,
	
    ruser = ident_id(0, 60);
    if (ruser == NULL)
    {
	syslog(LOG_ERR, "ident_id() -> NULL user");
	exit(1);
    }
    
    syslog(LOG_NOTICE, "Connection from: %s@%s (%s)\n", ruser, saddr, naddr);
    
    switch (check_acl(ruser, naddr, &luser))
    {
      case -1:
	syslog(LOG_WARNING, "Access denied for %s@%s (%s)\n",
	       ruser, saddr, naddr);
	exit(2);

      case 0:
	if (check_hacl(ruser, hp, &luser) != 1)
	{
	    syslog(LOG_WARNING, "Access denied for %s@%s (%s)\n",
		   ruser, saddr, naddr);
	    exit(2);
	}
    }

    if (geteuid() == 0 || luser != default_luser)
    {
	pw = getpwnam(luser);
	if (pw == NULL)
	{
	    syslog(LOG_ERR, "getpwnam(\"%s\") -> NULL", luser);
	    exit(1);
	}
	
	if (pw->pw_gid != getgid())
	    if (setgid(pw->pw_gid) < 0)
	    {
		syslog(LOG_ERR, "setgid(%d): %m", pw->pw_gid);
		exit(1);
	    }
	
	if (pw->pw_uid != getuid())
	    if (setuid(pw->pw_uid) < 0)
	    {
		syslog(LOG_ERR, "setuid(%d): %m", pw->pw_uid);
		exit(1);
	    }
    }
    
    if (chroot_dir)
    {
	if (chroot(chroot_dir) < 0)
	{
	    syslog(LOG_ERR, "chroot(\"%s\"): %m", chroot_dir);
	    exit(1);
	}
	if (chdir("/") < 0)
	{
	    syslog(LOG_ERR, "chdir(\"/\") after chroot(): %m", chroot_dir);
	    exit(1);
	}
    }
    
    execl(path_rmt, "rmt", NULL);

    syslog(LOG_ERR, "execl(\"%s\", ..): %m", path_rmt);
    exit(1);
}
