/*
	xcdrwrap.c
	hopefully secure wrapper to call cdrtools
	12.7.01 tn

	remove all references to glib functions to have no external
	libraries in use - on some platforms suid mode is forbidden then
	23.2.03 tn
*/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include "largefile.h"

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <pwd.h>
#include "xcdroast.h"

#undef DEBUG

/* dont allow any known exploit code to be passed through the cdrtools */
#define ANTI_CDRTOOLS_EXPLOIT


/* copy glib glist stuff we need into this code, so we dont need 
   to link with glib */

/* code ripped from glib 1.2.10 - glib.h and glist.c */

typedef struct _GList	GList;
typedef void* gpointer;
struct _GList
{
  gpointer data;
  GList *next;
  GList *prev;
};

static GList* g_list_last (GList *list)
{
  if (list)
    {
      while (list->next)
	list = list->next;
    }
  
  return list;
}

static GList* g_list_first (GList *list)
{
  if (list)
    {
      while (list->prev)
	list = list->prev;
    }
  
  return list;
}

static GList* g_list_append (GList *list, gpointer data)
{
  GList *new_list;
  GList *last;
  
  new_list = calloc(1, sizeof(GList));
  new_list->data = data;
  
  if (list)
    {
      last = g_list_last (list);
      /* g_assert (last != NULL); */
      last->next = new_list;
      new_list->prev = last;

      return list;
    }
  else
    return new_list;
}

/* end of glib code */


static char sharedir[MAXLINE];
static char prefixdir[MAXLINE];
static char rootconfig[MAXLINE];
static char username[MAXLINE];
static char hostname[MAXLINE];

static int root_users_access;
static int root_hosts_access;
static GList *root_users_lists;
static GList *root_hosts_lists;

static int load_rootconfig(char *cnf);
static void check_access(int verbose);
static void get_spawn_path(char *app, char *ret, int debugout);


/* code duplicated from tools.c and cleared from glib symbols */

static int isroot() {

#ifdef DEBUG
	printf("I am uid: %d\n", getuid());
#endif
        if (getuid() == 0) {
                return 1;
        } else {
                return 0;
        }
}

static int is_directory(char *path) {
struct stat buf;

        if (stat(path,&buf) != 0) {
                return 0;
        }

        if (S_ISDIR(buf.st_mode) == 1) {
                return 1;
        } else {
                return 0;
        }
}

static int is_file(char *path) {
struct stat buf;

        if (stat(path,&buf) != 0) {
                return 0;
        }
        return 1;
}

static char *strip_string(char *str) {
int i,j;
int c1;

        if ( str == NULL) return (NULL);

        /* count how many leading chars to be whitespace */
        for(i=0; i<strlen(str); i++) {
                if (str[i] != ' ' && str[i] != '\t' && str[i] != '\r') 
                        break;
        }

        /* count how many trailing chars to be whitespace */
        for(j=strlen(str)-1; j >= 0; j--) {
                if (str[j] != ' ' && str[j] != '\t' && str[j] != '\n' && str[j] != '\r')
                        break;
        }

        /* string contains only whitespace? */
        if (j<i) {
                str[0] = '\0';
                return(str);
        }

        /* now move the chars to the front */
        for(c1=i; c1 <= j; c1++) {
                str[c1-i] = str[c1]; 
        }
        str[j+1-i] = '\0';      

        return(str);
}

static char *escape_parse(char *str) {
char tmp[MAXLINE];
char c;
int i,j;

        if ( str == NULL) return (NULL);

	if (strlen(str) > MAXLINE) return (NULL);

        j = 0;
        for(i=0; i<strlen(str); i++) {
                c = str[i];
                if (c == '\\') {
                        i++;
                        switch(str[i]) {

                        case 'n':
                                c = '\n';
                                break;
                        
                        case 't':
                                c = '\t';
                                break;
        
                        case 'b':
                                c = '\b';
                                break;

                        default:
                                c = str[i];
                        }
                }       

                tmp[j]=c;
                j++;
        }

        tmp[j] = '\0';

        strcpy(str,tmp);
        return(str);
}

static int parse_config_line(char *iline, char *id, char *value) {
char *p,*p2;
char line[1024];
char tmp[1024];

        strncpy(line,iline, MAXLINE); 
        strcpy(id,"");
        p = strtok(line,"=");
        if (p != NULL) {
                /* got id */
                strcpy(id,p);
                strip_string(id);
        } else {
                return 1;
        }

        strcpy(tmp,"");
        p = strtok(NULL,"");
        if (p != NULL) {
                /* string after = */
                strcpy(tmp,p);
                strip_string(tmp);
        } else { 
                return 1;
        }

        /* now strip quotes from string */
        p = tmp;
        if (*p == '\"') {
                p2 = p+1;
        } else {
                p2 = p;
        }
        if (p[strlen(p)-1] == '\"') {
                p[strlen(p)-1] = '\0';
        }
        strcpy(value,p2);

        /* now reconvert escape-chars */
        escape_parse(value);

        /* all ok */
        return 0;
}

/* done code from tools.c */


/* return the defined callpath for a helper-binary */

static char *cmdstring(char *cmd, char *ret) {
char tmp[MAXLINE];

	if (strncmp(cmd,"CDRECORD",MAXLINE) == 0) {
		strcpy(ret, CDRECORD);
		return ret;
	}
	if (strncmp(cmd,"CDDA2WAV",MAXLINE) == 0) {
		strcpy(ret, CDDA2WAV);
		return ret;
	}
	if (strncmp(cmd,"READCD",MAXLINE) == 0) {
		strcpy(ret, READCD);
		return ret;
	}
	if (strncmp(cmd,"MKISOFS",MAXLINE) == 0) {
		strcpy(ret, MKISOFS);
		return ret;
	}
	if (strncmp(cmd,"WRITETEST",MAXLINE) == 0) {
		strcpy(ret,"WRITETEST");
		return ret;
	}
	if (strncmp(cmd,"-V",MAXLINE) == 0) {
		printf("X-CD-Roast %s\n", XCDROAST_VERSION);
		printf("sharedir: %s\n", sharedir);
		printf("prefixdir: %s\n", prefixdir);

#if !(defined(__MACH__) && defined(__APPLE__)) && (USE_NONROOTMODE == 1)

		if (load_rootconfig(rootconfig)) {
			printf("Warning: rootconfig unreadable\n");
		} else {
			if (!isroot()) 
				check_access(0);
		}
#endif
		/* show found paths of helper apps */
		get_spawn_path(CDRECORD, tmp, 1);
		printf("cdrecord found at: %s\n", tmp);
		get_spawn_path(CDDA2WAV, tmp, 1);
		printf("cdda2wav found at: %s\n", tmp);
		get_spawn_path(READCD, tmp, 1);
		printf("readcd found at: %s\n", tmp);
		get_spawn_path(MKISOFS, tmp, 1);
		printf("mkisofs found at: %s\n", tmp);

		exit(0);
	}
	return NULL;	
}


/* drop any root permissions */

static void drop_root() {

#ifdef  HAVE_SETREUID
        if (setreuid(-1, getuid()) < 0)
#else
#ifdef  HAVE_SETEUID
        if (seteuid(getuid()) < 0)
#else
        if (setuid(getuid()) < 0)
#endif
#endif
        {
                perror("Panic: Cannot set back effective uid.");
                exit(1);
        }
}


/* determine path for helper apps */

static void get_spawn_path(char *app, char *ret, int debugout) {
struct stat buf;

	/* when path is with a leading slash (absolute), do nothing */
	if (app[0] == '/') {
		strncpy(ret,app,MAXLINE);
		return;
	}

	/* otherwise its relative - add sharedir first */
	snprintf(ret,MAXLINE,"%s/%s", sharedir, app);

	/* now check if this file does exist */
	if (stat(ret,&buf) != 0) {
		/* it does not, so try the fallback */
		snprintf(ret,MAXLINE,"%s/%s", prefixdir, app);
	}

	/* additional check if we run with -V */
	if (debugout) {
		/* check the fallback too */
		if (stat(ret,&buf) != 0) {
			strcpy(ret, "- not found -");
		}
		return;
	}

	/* paranoid check */
	if (ret[0] != '/') {
		printf("ERROR: Invalid relative spawnpath %s\nExiting...\n", ret);
		exit(1);
	}

	return;
}


/* print warning when wrong arguments */

static void usagequit() {

	printf("This wrapper should only be called by X-CD-Roast %s\n", 
		XCDROAST_VERSION);
	exit(1);
}


/* read root-user relevant stuff from config file */

static int load_rootconfig(char *cnf) {
FILE *fd;
char line[MAXLINE];
char id[MAXLINE];
char value[MAXLINE];

	if ((fd = fopen(cnf,"r")) == NULL) { 
		/* error opening file */
		return 1;
	}

	for (;;) {
		if (fgets(line,MAXLINE,fd) == NULL)
			break;

		/* skip empty or hashed lines */
		strip_string(line);
		if (*line == '#' || *line == '\0') 
			continue;

                /* parse lines */
		if (parse_config_line(line,id,value) || !value) {
                	fprintf(stderr,"syntax error in config-file\n");
			exit(1);
		}	

		if (strcmp("ROOT_USERS_ACCESS",id) == 0) {
			root_users_access = atoi(value);
		}
		if (strcmp("ROOT_USERS_LISTS",id) == 0) {
			root_users_lists = g_list_append(root_users_lists, strdup(value));	
		}
		if (strcmp("ROOT_HOSTS_ACCESS",id) == 0) {
			root_hosts_access = atoi(value);
		}
		if (strcmp("ROOT_HOSTS_LISTS",id) == 0) {
			root_hosts_lists = g_list_append(root_hosts_lists, strdup(value));	
		}

	}

	if (fclose(fd) != 0) {
		/* error closing file */
		return 1;
	}

	return 0;
}


/* do check if the current user and host is allowed to start xcdroast */
/* return 1 if so, 0 if denied */

static int checkuserhost(char *username, char *hostname) {
int userok, hostok;
int match;
GList *loop;

	match = 0;
	/* user first */
	if (root_users_access == 0) {
		userok = 1;
	} else 
	if (root_users_access == 1) {
		userok = 0;
	} else {
		loop = g_list_first(root_users_lists);
		while (loop) {
#ifdef DEBUG
			printf("trying to match %s to %s\n", username,(char *)loop->data);
#endif
			if (loop->data && strcmp(username,(char *)loop->data) == 0) {
				/* found our login on the list */
				match = 1;
			}		
			loop = loop->next;
		}
		if ((root_users_access == 2 && match) ||
		    (root_users_access == 3 && !match)) {
			userok = 1;
		} else  {
			userok = 0;
		}
	}	

	match = 0;
	/* now check host */
	if (root_hosts_access == 0) {
		hostok = 1;
	} else 
	if (root_hosts_access == 1) {
		hostok = 0;
	} else {
		loop = g_list_first(root_hosts_lists);
		while (loop) {
#ifdef DEBUG
			printf("trying to match %s to %s\n", hostname,(char *)loop->data);
#endif
			if (loop->data && strcmp(hostname,(char *)loop->data) == 0) {
				/* found our login on the list */
				match = 1;
			}		
			loop = loop->next;
		}
		if ((root_hosts_access == 2 && match) ||
		    (root_hosts_access == 3 && !match)) {
			hostok = 1;
		} else  {
			hostok = 0;
		}
	}

	/* only when both the host and the user are allowed, allow access */
	if (userok && hostok) {
		return 1;
	} else {
		return 0;
	}
}


/* try to write a file to the given directory 
   return 0 if ok, or 1 when failed */

static int test_write_perms(char *dir) {
int pidseed;
char tmp[MAXLINE];
int count;
FILE *fd;

	drop_root();

	if (!is_directory(dir))
		return 1;

	/* generate a truely unused unique filename */
	pidseed = (int) getpid();
	count = 0;

	while (1) {
		snprintf(tmp,MAXLINE,"%s/xcdtmp%02d.%d", dir, count, pidseed);
		if (!is_file(tmp))
			break;
		count++;

		/* try max 100 times to find a name */
		if (count > 99) 
			return 1;
	}

	/* tmp does now contain a full filename which is unused for sure */

	/* open that file for writing */
	fd = fopen(tmp,"w");

	if (fd == NULL) {
		/* we failed */
		return 1;
	}	

	if (fclose(fd) != 0) {
		/* error closing file */
		return 1;
	}
	
	/* test finished - remove file */
	unlink(tmp);
	
	return 0;
}


/* access denied */

static void check_access(int verbose) {

	if (!username || !hostname)
		return;

	/* check if the current user on current host may run xcdroast at all */
	if (!checkuserhost(username,hostname)) {
		printf("X-CD-Roast %s\n", "ACCESS DENIED");
		if (verbose) {
			fprintf(stderr,"Unable to run wrapper - permission denied by admin.\n");
			fprintf(stderr,"Aborting...\n");
		}
		exit(1);
	}
}

/* returns the current username */

static char *get_username() {
struct passwd *ent;

	ent = getpwuid(getuid());
	return ent->pw_name;
} 

int main(int argc, char **argv) {
int i, stat;
char **arglist;
char callpath[MAXLINE];
char tmp[MAXLINE];
char *p, *p1;
int seen_device_spec;

	root_users_access = 0;
	root_hosts_access = 0;
	root_users_lists = NULL;
	root_hosts_lists = NULL;
	seen_device_spec = 0;

#ifdef PRE_LIBDIR 
        /* use prefix as sharedir as it came from the makefile-option */
        strncpy(sharedir, PRE_LIBDIR, MAXLINE);
#else
        /* otherwise install our default prefix */
        strncpy(sharedir, LIBDIR, MAXLINE);
#endif

#ifdef CDRTOOLS_PREFIX
        /* use prefix as it came from the makefile-option */
        strncpy(prefixdir, CDRTOOLS_PREFIX, MAXLINE);
#else
# ifdef PREFIX
        /* use prefix as it came from the makefile-option */
        strncpy(prefixdir, PRE_PREFIX, MAXLINE);
# else
        /* otherwise install our default prefix */
        strncpy(prefixdir, PREFIX, MAXLINE);
# endif
#endif  

        snprintf(rootconfig, MAXLINE, "%s/%s", SYSCONFDIR, ROOTCONFIG);

	if (argc < 2) 
		usagequit();


	/* get username and host */
	if (gethostname(hostname,MAXLINE) != 0) {
		strncpy(hostname,"not_available",MAXLINE);
	}
	p1 = get_username();
	if (p1 != NULL) {
		strncpy(username,p1, MAXLINE);
	} else {
		strncpy(username,"not_available",MAXLINE);
	}

#ifdef DEBUG
	printf("User/Host: %s@%s\n", username, hostname);
#endif

	/* check for a known first argument */
	if (cmdstring(argv[1],tmp) == NULL) 
		usagequit();

	
	/* now read rootconfig file - just the user and host allowlists */

#if !(defined(__MACH__) && defined(__APPLE__)) && (USE_NONROOTMODE == 1)

	if (!isroot()) {
		if (load_rootconfig(rootconfig)) {
			fprintf(stderr,"Error reading %s - Aborting...\n", rootconfig);
			exit(1);
		}
		/* check access and quit if not allowed */
		check_access(1);
	}
#endif

	/* note: Its NOT possible to change the path of the binaries
	         via the commandline (-l switch), because this would
		 be a security risk. You have to use the compiled-in
		 paths */


	/* we should check if a directory is writeable? */
	if (strcmp(tmp,"WRITETEST") == 0) {
		if (argc != 3) 
			usagequit();
		stat = test_write_perms(argv[2]);

		if (stat == 0) {
			/* ok, dir is writeable */
			return 0;
		} else {
			return 1;
		}
	}
	
	/* make path absolute */
	get_spawn_path(tmp, callpath, 0);

	/* build new command line */
	arglist = calloc(argc+1, sizeof(char *));
	for (i = 1; i < argc; i++) {
		arglist[i-1] = strdup(argv[i]);
	}

	/* now remove path from first argument */
	strcpy(tmp,callpath);
	p = rindex(tmp,'/');
	if (p != NULL) {
		if (arglist[0]) {
			free(arglist[0]);
		}
		arglist[0] = strdup(p+1);
	}	

#ifdef ANTI_CDRTOOLS_EXPLOIT
	i = 0;
	while(arglist[i]) {

		/* this argument is a device-spec */
		if (strstr(arglist[i], "dev=") ||
		    strstr(arglist[i], "-D") ||
		    strstr(arglist[i], "--dev")) {
			seen_device_spec = i;
		}

		/* this arg or the previous argument a device spec? */
		if (i == seen_device_spec || i == seen_device_spec + 1) {

			/* ok, here is no %x or %n allowed */
			/* these tags show up in exploit code */
			if (strstr(arglist[i], "%x") ||
		 	    strstr(arglist[i], "%n")) {
				printf("Illegal input detected. Go away.\n");
				exit(1);
			}		
		}
		i++;
	}
#endif

#ifdef DEBUG
	printf(":%s:\n",callpath);
	i = 0;
	while(arglist[i]) {
		printf(":%s:\n",arglist[i]);
		i++;
	}
	exit(1);
#endif

	/* now we recreated the original command line */ 
	if (execv(callpath,arglist) < 0) {
		fprintf(stderr,"execv error while calling %s (%s)\n", 
				callpath, strerror(errno));
	}

	return 1;
}

