/*
 * $Id: configuration.c,v 1.24 2002/12/08 14:10:34 alphix Exp $
 *
 * This file is part of Ample.
 *
 * Ample 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.
 *
 * Ample 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 Ample; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*
 * $Id: configuration.c,v 1.24 2002/12/08 14:10:34 alphix Exp $
 *
 * This file parses and prepares configuration data (command line, config
 * file, HTML template, default options etc) and also reading and parsing
 * requests from clients.
 */


#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <libgen.h>
#include <ctype.h>
#if HAVE_GETOPT_H
#include <getopt.h>
#endif
#if HAVE_NETDB_H
#include <netdb.h>
#endif

#include "ample.h"
#include "base64.h"
#include "entries.h"
#include "client.h"
#include "configuration.h"
#include "helper.h"


/* used in readrequest to see if the request timed out before completion */
volatile int timeout;


/*
 * Resizes an array and adds an element to it.
 *
 * Arguments: arrayptr - pointer to the array ptr
 *            element - the element to add
 *
 * Returns: void
 */
static void 
addtoarray(char ***arrayptr, char *element)
{
	int i;

	if(*arrayptr == NULL) {
		*arrayptr = malloc(sizeof(char *));
		if(!(*arrayptr))
			die("malloc()\n");
		(*arrayptr)[0] = NULL;
	}
	
	for(i = 0; (*arrayptr)[i] != NULL; i++);
	
	(*arrayptr) = realloc((*arrayptr), (i + 2) * sizeof(char *));
	if(!(*arrayptr))
		die("realloc()\n");

	(*arrayptr)[i] = element;
	(*arrayptr)[i+1] = NULL;
}


/*
 * This function will deal with leading / and also
 * illegal characters according to RFC 1738:
 * ' ',',','<','>','"','#','%','{','}','|','\','^','~','[',']' and '`'
 * These should be (but aren't always) encoded as %<two character hex code>
 * <hi> would become %3Chi%3E
 *
 * Arguments: url - the string to decode
 *
 * Returns: a decoded string stored in malloc:ed memory
 */
static char *
decodeurl(char *url) 
{
	char *current = url;
	char *ret = (char *)malloc(strlen(url) + 1);
	char *pos = ret;
	int hexval;
	char hexbuf[3];

	hexbuf[2] = '\0';
	if(*current != '/') {
		*pos == '/';
		pos++;
	}

	while(*current != '\0') {
		if(*current != '%') {
			*pos = *current;
			pos++;
			current++;
		} else {
			if(((hexbuf[0] = *(current + 1)) == '\0') ||
			   ((hexbuf[1] = *(current + 2)) == '\0')) {
				strncpy(pos, current, 3);
				pos = pos + 2;
				break;
			} else if ((hexval = (int)strtol(hexbuf, NULL, 16)) == 0) {
				strncpy(pos, current, 3);
				pos = pos + 3;
			} else {
				*pos = (char)hexval;
				pos++;
			}
			current = current + 3;
		}
	}
	
	*pos = '\0';
	debug(2, "Decodeurl parses %s as %s\n", url, ret);
	return ret;
}


/*
 * Signal handler for timeout when reading the request from the client.
 *
 * Arguments: signal - the signal that called this function (SIGALRM)
 *
 * Returns: void
 */
static void 
conntimeout(int signal) 
{
	timeout = TRUE;
}


/*
 * Reads the request from the client and sets configuration options to
 * reflect what the user requested.
 *
 * Arguments: stream - the stream to read client requests from
 *
 * Returns: TRUE if a succesful request was made, FALSE otherwise
 */
bool 
readrequest(struct client_config *cconf)
{
	char line[LINELENGTH];
	int start,end;
	char *tmp, *tmp2;
	struct sigaction sa;

	timeout = FALSE;
	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = conntimeout;
	sigaction(SIGALRM, &sa, NULL);
	alarm(CONN_TIMEOUT);

	while((fgets(line, LINELENGTH, cconf->stream)) != NULL && timeout != TRUE) {
		
		if(!strcmp(line, "\r\n")) {
			/* End of request from client */
			break;
			
		} else if(!strncasecmp(line, "GET", strlen("GET"))) {
			/* Found the GET request */
			debug(3, "GET request was %s\n", line);
			if(strlen(line) < 14)
				continue;
			
			/* If no spaces or just one space is found, continue */
			if(strchr(line, ' ') == NULL || 
			   strchr(line, ' ') == strrchr(line, ' '))
				continue;
			
			/* 
			 * Set start and end indexes so that the URL 
			 * is enclosed between them 
			 */
			for(start = strlen("GET"); 
			    isspace(line[start]); start++);
			for(end = (strrchr(line, ' ') - line); 
			    !isspace(line[end]); end--);
			line[end] = '\0';
			
			/* Decode the URL and set it in the config options */
			cconf->requestpath = decodeurl(&line[start]);
			
			/* Was index.html requested? */
			if((tmp = strrchr(cconf->requestpath, 'i')) != NULL && 
			   (!strcasecmp(tmp, "index.html") || 
			    !strcasecmp(tmp, "index.htm"))) {
				MODE_SET(MODE_INDEX);
				*tmp = '\0';
			}
			
			/* Was info.html requested? */
			if((tmp = strrchr(cconf->requestpath, 'i')) != NULL && 
			   (!strcasecmp(tmp, "info.html") || 
			    !strcasecmp(tmp, "info.htm"))) {
				MODE_SET(MODE_INFO);
				*tmp = '\0';
			}

			/* Was (r)index.m3u requested? */
			if((tmp = strrchr(cconf->requestpath, 'i')) != NULL && 
			   (!strcasecmp(tmp, "index.m3u")) ) {
				*tmp = '\0';
				tmp--;
				if(strlen(cconf->requestpath) > 1 && 
				   *tmp == 'r') {
					MODE_SET(MODE_RM3U);
					gconf.recursive = TRUE;
					*tmp = '\0';
				} else {
					MODE_SET(MODE_M3U);
				}
			}
			
			/* Was a single MP3 file requested? */
			if((tmp = strrchr(cconf->requestpath, '.')) == NULL)
				continue;
			if(!strcasecmp(tmp, ".mp3")) {
				MODE_SET(MODE_SINGLE);
			}
			
		} else if(!strncasecmp(line, "Icy-MetaData:", 
				       strlen("Icy-MetaData:"))) {
			/* Metadata was requested */
			start = strlen("Icy-MetaData:");
			if(line[start] == '\0' || 
			   !(strtol(&line[start], NULL, 10) > 0)) {
				debug(2, "Metadata was requested, but request wasn't valid\n");
			} else if(MODE_ISSET(MODE_SINGLE)) {
				debug(2, "Metadata was requested, but ignored since we are in single mode\n");
			} else {
				MODE_SET(MODE_METADATA);
				debug(2, "Metadata was requested\n");
			}

		} else if (!strncasecmp(line, "Authorization:", strlen("Authorization:"))) {
			/* The user seems to have provided a username and password */
			start = strlen("Authorization:");
			while(isspace(line[start]))
				start++;
			
			if(line[start] == '\0' || strncasecmp(&line[start], "Basic", strlen("Basic"))) {
				debug(2, "Authorization was requested, but request wasn't valid\n");
				continue;
			}
			
			start += strlen("Basic");

			while(isspace(line[start]))
				start++;

			tmp = &line[start];
			while(isbase64(*tmp))
				tmp++;

			*tmp = '\0';
			tmp2 = b64dec(&line[start]);
			if(tmp2 == NULL || strlen(tmp2) < 1 || index(tmp2, ':') == NULL) {
				debug(2, "Authorization was requested, but request wasn't valid\n");
				if(tmp2 != NULL)
					free(tmp2);
				continue;
			}
			
			tmp = index(tmp2, ':');
			*tmp = '\0';
			tmp++;
			if(tmp == '\0' || strlen(tmp) < 1) {
				debug(2, "Authorization was requested, but request wasn't valid\n");
				free(tmp2);
				continue;
			}
			
			cconf->username = strdup(tmp2);
			cconf->password = strdup(tmp);
			free(tmp2);

		} else if (!strncasecmp(line, "Range:", strlen("Range:"))) {
			/* A part of a file was requested */
			if(!MODE_ISSET(MODE_SINGLE)) {
				debug(2, "Range was requested, but ignored since we aren't in single mode\n");
				continue;
			}
			debug(3, "Range request was %s\n", line);
			for(start = strlen("Range:"); 
			    isspace(line[start]); start++);

			if(strncasecmp(&line[start], "bytes", 
				       strlen("bytes"))) {
				debug(2, "Range was requested, but request wasn't valid (1)\n");
				continue;
			}			
			start += strlen("bytes");
			
			for(; isspace(line[start]); start++);
			if(line[start] != '=') {
				debug(2, "Range was requested, but request wasn't valid (2)\n");
				continue;
			}
			
			start++;
			for(; isspace(line[start]); start++);

			if(isdigit(line[start])) {
				cconf->startpos = strtol(&line[start], &tmp, 10);
			} else if(line[start] == '-') {
				cconf->startpos = 0;
				tmp = &line[start];
			} else {
				debug(2, "Range was requested, but request wasn't valid (3)\n");
				continue;
			}
			
			if(*tmp != '-') {
				debug(2, "Range was requested, but request wasn't valid (4)\n");
				continue;
			} else {
				tmp++;
			}

			if(isdigit(*tmp)) {
				cconf->endpos = strtol(tmp, NULL, 10);
			} else {
				cconf->endpos = 0;
			}
			MODE_SET(MODE_PARTIAL);

		} else {
			debug(3, "Client sent line %s\n", line);
		}
	}
	
	sa.sa_handler = SIG_IGN;
	sigaction(SIGALRM, &sa, NULL);

	if(timeout) {
		logmsg("Connection timed out\n");
		return(FALSE);
	} else {
		alarm(0);
	}

	/* Strip trailing slash */
	if(strlen(cconf->requestpath) > 1) {
		tmp = cconf->requestpath;
		tmp += strlen(cconf->requestpath);
		tmp--;
		if(*tmp == '/')
			*tmp = '\0';
	}
	
	return(TRUE);
}


/*
 * Checks if an HTML template file can be found. If so, it is split into 
 * three parts (header, middle and footer) and read into memory. If
 * no file is found, standard header, middle and footer contents is
 * placed in memory.
 *
 * Arguments: none
 *
 * Returns: void
 */
static void 
preparehtml()
{
	char line[LINELENGTH];
	FILE *file;
	int i;
	char *tmp;
	int tmpsize = 2 * LINELENGTH;
	int tmpwritten = 0;

	const struct confoption delimiters[] = {
		{"@BEGIN@", 0, &gconf.htmlheader},
		{"@END@", 0, &gconf.htmlmiddle},
		{NULL, 0, &gconf.htmlfooter},
		{NULL, 0, NULL}
	};

	if((file = fopen(gconf.htmlfile, "r")) != NULL) {
		debug(2, "Using %s as configfile\n", gconf.htmlfile);
	} else {
		debug(2, "No HTML template found\n");
		gconf.htmlheader = strdup(DEF_HTMLHEADER);
		gconf.htmlmiddle = strdup(DEF_HTMLMIDDLE);
		gconf.htmlfooter = strdup(DEF_HTMLFOOTER);
		return;
	}
	
	for(i = 0; delimiters[i].value != NULL; i++) {
		
		tmp = malloc(tmpsize);
		*tmp = '\0';

		while(fgets(line, LINELENGTH, file) != NULL) {
			debug(9, "Template line is %s\n", line);
			if(delimiters[i].name && 
			   !strncmp(line, delimiters[i].name, 
				    strlen(delimiters[i].name)))
				break;
			tmpwritten += strlen(line);
			strcat(tmp, line);
			if((tmpsize - tmpwritten) < LINELENGTH) {
				tmpsize += LINELENGTH;
				realloc(tmp, tmpsize);
			}
		}
		
		realloc(tmp, strlen(tmp) + 1);
		*((char **)delimiters[i].value) = tmp;
	}
} 


/*
 * Prints a usage message and exits.
 *
 * Arguments: error - did we end up here because the user made a mistake
 *                    (like starting Ample with incorrect cmd line options)
 *
 * Returns: void
 */
static void 
usage(bool error)
{
	if(error) {
		fprintf(stderr, "Try `%s --help' for more information.\n", 
			gconf.program_name);
		exit(1);
	} else {
		/* This is several printf's to please ISO C89 */
		printf(USAGE1, gconf.program_name);
		printf(USAGE2, DEF_PORT, DEF_MAX_CLIENTS);
		exit(0);
	}
}


/*
 * Sets default option values for those who hasn't been set by command
 * line options or config file options.
 *
 * Arguments: argc - hopefully no explaination needed?
 *            argv - ditto
 *
 * Returns: void
 */
static void 
setdefopt(int argc, char * argv[])
{
	if(gconf.port == 0) {
#ifdef HAVE_NETDB_H
		struct servent *servent = getservbyname("ample", "tcp");
		gconf.port = (servent ? ntohs(servent->s_port) : DEF_PORT);
#else		
		gconf.port = DEF_PORT;
#endif
	}

	if(gconf.port >= 64*1024)
		die("port number out of range\n");
	if(gconf.order == -1)
		gconf.order = FALSE;
	if(gconf.max_clients == 0)
		gconf.max_clients = DEF_MAX_CLIENTS;
	if(gconf.recursive == -1)
		gconf.recursive = TRUE;
	if(gconf.pathlist == NULL)
		addtoarray(&gconf.pathlist, strdup(DEF_PATH));
	if(gconf.logfile == NULL)
		gconf.logfile = strdup(DEF_LOGFILE);
	if(gconf.servername == NULL)
		gconf.servername = strdup(DEF_SERVERNAME);
	if(gconf.serveraddress == NULL && 
	   !(gconf.serveraddress = getservername()))
		gconf.serveraddress = strdup(DEF_SERVERADDRESS);
	if(gconf.htmlfile == NULL)
		gconf.htmlfile = strdup(DEF_HTMLFILE);
}


/*
 * Takes one line from the config file, parses it and changes 
 * configuration values accordingly.
 *
 * Arguments: tmp - the line to parse
 *            value - a pointer to the value to set
 *
 * Returns: void
 */
static void 
parseconfopt(char *tmp, void *value, int type)
{
	int i;
	float f;
	char *end;
	char *retval;

	while(isspace(*tmp))
		tmp++;
	
	if(*tmp != '=')
		return;
	else
		tmp++;
	
	while(isspace(*tmp))
		tmp++;

	switch(type) {
	case OPT_INT:
		if((i = (int)strtol(tmp, NULL, 10)) > 0)
			*((int *)value) = i;
		break;
	case OPT_FLOAT:
		if((f = (float)strtod(tmp, NULL)) > 0)
			*((float *)value) = f;
		break;
	case OPT_BOOL:
		if(!strncasecmp("yes",tmp,strlen("yes")) || 
		   !strncasecmp("true",tmp,strlen("true")))
			*((bool *)value) = TRUE;
		else if(!strncasecmp("no",tmp,strlen("no")) || 
			!strncasecmp("false",tmp,strlen("false")))
			*((bool *)value) = FALSE;
		break;
	case OPT_STRING:
		end = tmp;
		while(*end != '\n' && *end != '\0' && 
		      *end != '#')
			end++;
		if(end == tmp)
			break;
		retval = malloc(end - tmp + 1);
		if(!retval)
			die("malloc");
		snprintf(retval, (end - tmp + 1), "%.*s", (end - tmp), tmp);

		*(char **)value = retval;
		break;		
	case OPT_MULTISTRING:
		end = tmp;
		while(*end != '\n' && *end != '\0' && 
		      *end != '#' && !isspace(*end))
			end++;
		if(end == tmp)
			break;
		retval = malloc(end - tmp + 1);
		if(!retval)
			die("malloc");
		snprintf(retval, (end - tmp + 1), "%.*s", (end - tmp), tmp);

		addtoarray((char ***)value, retval);
		break;
	default:
		/* We should never end up here */
		debug(2, "Incorrect type passed to parseconfopt()\n");
		break;
	}
}


/*
 * Opens the config file, reads it line by line and sends interesting
 * lines to parseconfopt.
 *
 * Arguments: argc - hopefully no explaination needed?
 *            argv - ditto
 *
 * Returns: void
 */
static void 
setconfopt(int argc, char * argv[]) {

	char line[LINELENGTH];
	FILE *file;
	int i,j;
	const struct confoption options[] = {
		{"clients", OPT_INT, &gconf.max_clients},
		{"filter", OPT_STRING, &gconf.filter},
		{"htmlfile", OPT_STRING, &gconf.htmlfile},
		{"logfile", OPT_STRING, &gconf.logfile},
		{"mp3path", OPT_MULTISTRING, &gconf.pathlist},
		{"order", OPT_BOOL, &gconf.order},
		{"password", OPT_STRING, &gconf.password},
		{"port", OPT_INT, &gconf.port},
		{"recursive", OPT_BOOL, &gconf.recursive},
		{"servername", OPT_STRING, &gconf.servername},
		{"serveraddress", OPT_STRING, &gconf.serveraddress},		
		{"username", OPT_STRING, &gconf.username},
		{NULL, 0, NULL}
	};
	
	if(gconf.conffile == NULL)
		file = fopen(DEF_CONFFILE, "r");
	else
		file = fopen(gconf.conffile, "r");
	
	if (file == NULL)
		return;
	
	while((fgets(line, LINELENGTH, file)) != NULL) {
		if(line[0] == '#')
			continue;
		
		for(i = 0; isspace(line[i]); i++);
		
		for(j = 0; options[j].name != NULL; j++) {
			if(!strncasecmp(&line[i], options[j].name, strlen(options[j].name))) {
				i += strlen(options[j].name);
				parseconfopt(&line[i], options[j].value, options[j].type);
				break;
			}
		}
	}
}


/*
 * Parses command line options and changes configuration accordingly.
 *
 * Arguments: argc - hopefully no explaination needed?
 *            argv - ditto
 *
 * Returns: void
 */
static void 
setcmdopt(int argc, char * argv[])
{
	int c,i;

#ifdef HAVE_GETOPT_H
	const struct option longopts[] = {
		{"port", required_argument, NULL, 'p'},
		{"order", no_argument, NULL, 'o'},
		{"clients", required_argument, NULL, 'c'},
		{"norecursive", no_argument, NULL, 'n'},
		{"conffile", required_argument, NULL, 'f'},
		{"htmlfile", required_argument, NULL, 'm'},
		{"help", no_argument, NULL, 'h'},
		{"debug", optional_argument, NULL, 'd'},
		{"trace", no_argument, NULL, 't'},
		{"version", no_argument, NULL, 'v'},
		{NULL, 0, NULL, 0}
	};

	while((c = getopt_long(argc, argv, "p:oc:nf:m:hd::tv", longopts, &i)) 
	      != -1) {
#else
	while((c = getopt(argc, argv, "p:oc:nf:m:hd::tv")) != -1) {
#endif
		switch(c) {
		case 'p':
			if(strtol(optarg, NULL, 10) > 0)
				gconf.port = (int)strtol(optarg, NULL, 10);
			break;
		case 'o':
			gconf.order = TRUE;
			break;
		case 'c':
			if(strtol(optarg, NULL, 10) > 0)
				gconf.max_clients = strtol(optarg, NULL, 10);
			break;
		case 'n':
			gconf.recursive = FALSE;
			break;
		case 'f':
			gconf.conffile = strdup(optarg);
			break;
		case 'm':
			gconf.htmlfile = strdup(optarg);
			break;
		case 'h':
			usage(FALSE);
			break;
		case 'd':
			if(optarg == NULL || strtol(optarg, NULL, 10) < 0)
				gconf.debuglevel = 1;
			else
				gconf.debuglevel = strtol(optarg, NULL, 10);
			break;
		case 't':
			gconf.trace = TRUE;
			break;
		case 'v':
			printf("Ample version %s\n", AMPLE_VERSION);
			exit(0);
		default:
			usage(TRUE);
		}
	}
	
	i = optind;
	while(i < argc) {
		addtoarray(&gconf.pathlist, strdup(argv[i]));
		i++;
	}
}


/*
 * Prepares the configuration structure and fills it with information from
 * (in order of relevance) command line, config file and default values.
 * Also prepares the user specified/default HTML template for use.
 *
 * Arguments: argc - hopefully no explaination needed?
 *            argv - ditto
 *
 * Returns: void
 */
void 
checkopt(int argc, char * argv[])
{
	memset(&gconf, 0, sizeof(struct global_config));
	gconf.order = -1;
	gconf.recursive = -1;
	gconf.program_name = argv[0];

	setcmdopt(argc, argv);
	setconfopt(argc, argv);
	setdefopt(argc, argv);
	preparehtml();
}
