
// Project: webCDwriter - Network CD Writing
// File: Config.cpp
// Author: Jörg Haeger, 25.05.1999

#include <sys/types.h>
#include <grp.h>
#include <pwd.h>
#include "Config.h"
#include "Version.h"

Config::Config()
{
	cdrecordInfo = "cdrecord";
	fileName = getenv("CDWserverConfig");
	if (fileName == NULL)
		fileName = strCat(prefix, "/etc/CDWserver/config");
	char *configDir = strDup(fileName);
	int i;
	for (i = strlen(configDir);
		i > 0 && configDir[i - 1] != '/'; i--);
	configDir[i] = 0;

	firstError = NULL;
	mkisofsInfo = "mkisofs";
	writerInfo = "unknown";

	// values
	burnProof = 0;
	cdrecordArgs = "";
	cdrecordDevArg = "auto";
	cdrecordPath = "cdrecord";
	debug = "";
	defaultFilesDir = strCat(prefix, "/etc/CDWserver/default_files");
	defaultSpeed = 4;
	eMailOnError = NULL;
	group = defaultGroup;
	imageFile = strCat(prefix, "/var/spool/CDWserver/cdimage.iso");
	imageOnTheFly = 1;
	infoFile = strCat(configDir, "greeting");
	laserOn = 1;
	logDir = strCat(prefix, "/var/log/CDWserver");
	logoURL = NULL;
	logRequests = 0;
	logSubProcess = 0;
	logVerbose = 0;
	maxMBytesInSpoolDir = 1600;
	maxMBytesPerSession = 800;
	maxOpenConnections = 10;
	maxOpenSessions = 1;
	maxSpeed = 8;
	mkisofsMArg = "auto";
	mkisofsPath = "mkisofs";
	MP3decoding = 1;
	mpg123Path = "mpg123";
	organization = "unknown";
	portNo = 12411;
	removeSessions = 0;
	reservedMBytes = 10;
	setSchedulerPath = strCat(prefix, "/usr/bin/setScheduler");
	showUsers = 1;
	spoolDir = strCat(prefix, "/var/spool/CDWserver");
	statusPortNo = 12412;
	user = defaultUser;
	waitForCD = 10;
	waitForCDMsgFile = strCat(configDir, "waitForCD");

	try {
		gid = toGid(group);
	}
	catch (Error *error)
	{
		gid = 0;
		group = strDup(toGroup(gid));
	}

	try {
		uid = toUid(user);
	}
	catch (Error *error)
	{
		uid = 0;
		user = strDup(toUser(uid));
	}

	try {
		readConfigFile();
	}
	catch (Error *error)
	{
		firstError = error;
	}
}

void Config::checkExecAsRoot(const char *execName)
{
	struct stat statBuf;
	if (stat(execName, &statBuf) != 0)
		throw new Error("Cannot find %s.", execName);

	if (uid == 0)
		return;

	if (statBuf.st_uid != 0
		|| (statBuf.st_mode & S_ISUID) == 0)
		throw (new Error("Cannot run %s as root.", execName))
			->addHelp("Try one of\n<ul>\n"
				"<li>run<br>[chown root.%s %s<br>\n"
				"chmod 4750 %s]</li>\n"
				"<li>set <b>user=root</b> in %s</li></ul>",
				getGroup(), execName,
				execName,
				fileName);
	else
	if (((statBuf.st_mode & S_IXGRP) == 0
			|| statBuf.st_gid != gid)
		&& (statBuf.st_mode & S_IXOTH) == 0)
		throw (new Error("Cannot run %s as root.", execName))
			->addHelp("Try one of\n<ul>\n"
				"<li>set <b>group=%s</b> in %s</li>\n"
				"<li>set <b>user=root</b> in %s</li></ul>",
				toGroup(statBuf.st_gid), fileName,
				fileName);
}

void Config::evaluate(const char *str1, const char *str2)
{
	if (strcasecmp(str1, "burnProof") == 0)
		burnProof = toBoolean(str2);
	else
	if (strcasecmp(str1, "cdrecordArgs") == 0)
		cdrecordArgs = strDup(str2);
	else
	if (strcasecmp(str1, "cdrecordDevArg") == 0)
	{
		int bus, id, lun;
		if (sscanf(str2, "%d,%d,%d",
			&bus, &id, &lun) == 3)
			cdrecordDevArg = strDup(str2);
		else
		if (strcasecmp(str2, "auto") == 0)
			cdrecordDevArg = "auto";
		else
			throw new Error("invalid value");
	}
	else
	if (strcasecmp(str1, "cdrecordPath") == 0)
		cdrecordPath = strDup(str2);
	else
	if (strcasecmp(str1, "debug") == 0)
		debug = strDup(str2);
	else
	if (strcasecmp(str1, "defaultFilesDir") == 0)
		defaultFilesDir = strDup(str2);
	else
	if (strcasecmp(str1, "defaultSpeed") == 0)
		defaultSpeed = toInteger(str2);
	else
	if (strcasecmp(str1, "eMailOnError") == 0)
		eMailOnError = strDup(str2);
	else
	if (strcasecmp(str1, "group") == 0)
	{
		gid = toGid(str2);
		group = strDup(str2);
	}
	else
	if (strcasecmp(str1, "imageFile") == 0)
		throw new Error("now internal, remove line");
	else
	if (strcasecmp(str1, "imageOnTheFly") == 0)
		imageOnTheFly = toBoolean(str2);
	else
	if (strcasecmp(str1, "laser") == 0)
		laserOn = toBoolean(str2);
	else
	if (strcasecmp(str1, "logDir") == 0)
		logDir = strDup(str2);
	else
	if (strcasecmp(str1, "logoURL") == 0)
		logoURL = strDup(str2);
	else
	if (strcasecmp(str1, "logRequests") == 0)
		logRequests = toBoolean(str2);
	else
	if (strcasecmp(str1, "logSubProcess") == 0)
		logSubProcess = toBoolean(str2);
	else
	if (strcasecmp(str1, "logVerbose") == 0)
		logVerbose = toBoolean(str2);
	else
	if (strcasecmp(str1, "maxMBytesInSpoolDir") == 0)
		maxMBytesInSpoolDir = toInteger(str2);
	else
	if (strcasecmp(str1, "maxMBytesPerSession") == 0)
		maxMBytesPerSession = toInteger(str2);
	else
	if (strcasecmp(str1, "maxOpenConnections") == 0)
		maxOpenConnections = toInteger(str2);
	else
	if (strcasecmp(str1, "maxOpenSessions") == 0)
	{
		if (strcasecmp(str2, "auto") == 0)
			maxOpenSessions = 0;
		else
			maxOpenSessions = toInteger(str2);
	}
	else
	if (strcasecmp(str1, "maxSpeed") == 0)
		maxSpeed = toInteger(str2);
	else
	if (strcasecmp(str1, "maxStoreMBs") == 0)
		throw new Error(
			": use maxMBytesInSpoolDir (see CDWserver/config.eg)");
	else
	if (strcasecmp(str1, "mkisofsMArg") == 0)
		mkisofsMArg = strDup(str2);
	else
	if (strcasecmp(str1, "mkisofsPath") == 0)
		mkisofsPath = strDup(str2);
	else
	if (strcasecmp(str1, "MP3decoding") == 0)
		MP3decoding = toBoolean(str2);
	else
	if (strcasecmp(str1, "mpg123Path") == 0)
		mpg123Path = strDup(str2);
	else
	if (strcasecmp(str1, "organization") == 0)
		organization = strDup(str2);
	else
	if (strcasecmp(str1, "portNo") == 0)
		portNo = toInteger(str2);
	else
	if (strcasecmp(str1, "removeSessions") == 0)
		removeSessions = toBoolean(str2);
	else
	if (strcasecmp(str1, "reservedMBytes") == 0)
	{
		reservedMBytes = toInteger(str2);
		if (reservedMBytes < 10)
			reservedMBytes = 10;
	}
	else
	if (strcasecmp(str1, "showUsers") == 0)
		showUsers = toBoolean(str2);
	else
	if (strcasecmp(str1, "speed") == 0)
		throw new Error("use defaultSpeed and maxSpeed");
	else
	if (strcasecmp(str1, "spoolDir") == 0)
	{
		spoolDir = strDup(str2);
		char str[1024];
		sprintf(str, "%s/cdimage.iso", spoolDir);
		imageFile = strDup(str);
	}
	else
	if (strcasecmp(str1, "statusPortNo") == 0)
		statusPortNo = toInteger(str2);
	else
	if (strcasecmp(str1, "user") == 0)
	{
		uid = toUid(str2);
		user = strDup(str2);
	}
	else
	if (strcasecmp(str1, "waitForCD") == 0)
		waitForCD = toInteger(str2);
	else
		throw new Error("unknown symbol");
}

void Config::parseLine(
	const char *lineStr, char *str1, char *str2)
{
	str1[0] = str2[0] = 0;

	if (lineStr[0] == '#')
		return;

	int i = 0;
	while (lineStr[i] == '\t'
		|| lineStr[i] == ' ')
		i++;

	int j = 0;
	for (; lineStr[i] != 0
		&& lineStr[i] != '\n'
		&& lineStr[i] != '\t'
		&& lineStr[i] != ' '
		&& lineStr[i] != '='; i++)
		str1[j++] = lineStr[i];
	str1[j] = 0;

	while (lineStr[i] == '\t'
		|| lineStr[i] == ' '
		|| lineStr[i] == '=')
		i++;

	j = 0;
	while (lineStr[i] != 0
		&& lineStr[i] != '\n')
		str2[j++] = lineStr[i++];

	// truncate trailing whitespace
	while (j > 0
		&& (str2[j - 1] == '\t'
			|| str2[j - 1] == ' '))
		j--;
	str2[j] = 0;
}

void Config::readConfigFile()
{
	if (!File(fileName).exists()
		|| File(fileName).getSize() == 0)
		writeConfigFile();

	if (!File(infoFile).exists()
		|| File(infoFile).getSize() == 0)
		writeInfoFile();

	if (!File(waitForCDMsgFile).exists()
		|| File(waitForCDMsgFile).getSize() == 0)
		writeWaitForCDMsgFile();

	FILE *inStream = fopen(fileName, "r");
	if (inStream == NULL)
		throw new Error("can't open configFile %s", fileName);

	int lineNo = 0;
	char lineStr[1024];
	while (fgets(lineStr, sizeof lineStr, inStream) != NULL)
	{
		lineNo++;
		char str1[1024], str2[1024];
		parseLine(lineStr, str1, str2);

		try {
			if (strlen(str1) == 0 && strlen(str2) == 0)
				continue;
			else
				evaluate(str1, str2);
		}
		catch (Error *error)
		{
			if (firstError == NULL)
			{
				firstError = new Error(
					"error in %s: line %d: \"%s=%s\": %s",
					fileName, lineNo, str1, str2,
					error->toString());
				log.put(firstError->toString());
			}
			delete error;
		}
	}

	fclose(inStream);

	if (firstError != NULL)
		throw firstError;
}

void Config::reload()
{
	if (firstError != NULL)
	{
		delete firstError;
		firstError = NULL;
	}

	try {
		readConfigFile();

		log.put("Config: port=%d, dev=%s, laser=%s",
			portNo, cdrecordDevArg, laserOn? "on": "off");

		if (uid != geteuid() || gid != getegid())
			throw (new Error("Restart %s as root "
				"to set user %s and group %s!",
				product, user, group));

		if (!File(logDir).exists())
			throw new Error("logDir %s does not exist", logDir);

		if (!File(spoolDir).exists())
			throw new Error("spoolDir %s does not exist", spoolDir);

		cdrecordPath = strDup(File(cdrecordPath).complete());
		checkExecAsRoot(cdrecordPath);
		log.putv("cdrecordPath = %s", cdrecordPath);

		mkisofsPath = strDup(File(mkisofsPath).complete());
		checkExecAsRoot(mkisofsPath);
		log.putv("mkisofsPath = %s", mkisofsPath);

		setSchedulerPath = strDup(File(setSchedulerPath).complete());
		checkExecAsRoot(setSchedulerPath);
		log.putv("setSchedulerPath = %s", setSchedulerPath);

		if (uid != 0)
		if (File(logDir).isReadAndWritable(uid, gid) != 0)
			throw (new Error("logDir %s or a file below it "
			"is not read and writable by user %s / group %s.",
				logDir, user, group))
			->addHelp("Try one of\n<ul>\n"
				"<li>change <b>logDir</b> in %s</li>\n"
				"<li>run<br>[chown %s %s/ -R]</li></ul>",
				fileName, user, logDir);

		if (uid != 0)
		if (File(spoolDir).checkOwner(uid) != 0)
			throw (new Error("spoolDir %s or a file below it "
				"is not owned by user %s.",
				spoolDir, user))
			->addHelp("Try one of\n<ul>\n"
				"<li>change <b>spoolDir</b> in %s</li>\n"
				"<li>run<br>[chown %s %s/ -R]</li></ul>",
				fileName, user, spoolDir);

		if (!config.getImageOnTheFly())
		{
			FILE *outStream = fopen(imageFile, "w");
			if (outStream == NULL)
				throw new Error("can't create imageFile %s",
					imageFile);
			else
				fclose(outStream);
		}

		testMkisofs();
		testCdrecord();
		testCdrecord2();
		testCdrecord3();
		if (MP3decoding)
			testMpg123();
	}
	catch (Error *error)
	{
		firstError = error;
		return;
	}

	if (maxOpenSessions == 0)
	{
		// calculate number of possible parallel sessions
		int n = maxMBytesInSpoolDir;
		if (imageOnTheFly)
		{
			if (n < maxMBytesPerSession)
				maxMBytesPerSession = n;
		}
		else
		{
			if (n < 2 * maxMBytesPerSession)
				maxMBytesPerSession = n / 2;
			n -= maxMBytesPerSession;
		}
		maxOpenSessions = n / maxMBytesPerSession;
	}
}

void Config::setPermissions()
{
	char fileName[4*1024];
	sprintf(fileName, "%s/connects",
		config.getLogDir());
	chown(fileName, uid, gid);
	sprintf(fileName, "%s/%s.log",
		config.getLogDir(), version);
	chown(fileName, uid, gid);
	sprintf(fileName, "%s/%s.vlog",
		config.getLogDir(), version);
	chown(fileName, uid, gid);
	chown(imageFile, uid, gid);
}

void Config::testCdrecord()
{
	Process p(getCdrecordPath());
	p.addArg("-version");
	p.start();
	const char *lineStr = p.readLine();
	if (lineStr == NULL)
		throw new Error("can't run cdrecord");
	if (p.getResult() != 0)
		throw new Error("error running cdrecord -version");
	char str1[strlen(lineStr) + 1], str2[strlen(lineStr) + 1];
	if (sscanf(lineStr, "%s %s", str1, str2) != 2)
		throw new Error("can't get version of cdrecord");
	char *str3 = new char[strlen(str1) + strlen(str2) + 2];
	sprintf(str3, "%s %s", str1, str2);
	cdrecordInfo = str3;
	log.put(lineStr);
}

void Config::testCdrecord2()
{
	Process p(getCdrecordPath());
	p.addArg("-scanbus");
	p.start();

	char CDROMs[20][20], writers[20][20];
	int numOfCDROMs = 0, numOfWriters = 0;
	while (1)
	{
		const char *lineStr = p.readLine();
		if (lineStr == NULL)
			break;
		if (strstr(lineStr, "CD-ROM") == NULL
			&& strstr(lineStr, "WORM") == NULL)
			continue;

		log.put(lineStr);
		int bus, ID, LUN;
		if (sscanf(lineStr, "%d,%d,%d", &bus, &ID, &LUN) != 3)
			continue;

		sprintf(CDROMs[numOfCDROMs++], "%d,%d,%d",
			bus, ID, LUN);
		const char *str = lineStr;
		for (int i = 0; i < 3; i++)
		{
			while (*str != '\'' && *str != 0)
				str++;
			if (*str != 0)
				str++;
		}
		char model[17];
		strncpy(model, str, 16);
		model[16] = 0;

		const char *patterns[] = {
			"CD-R", "CDR", "CR", "CW", "RW",
			"LTR", "MP-7", "PX-R", "PX-W", NULL };
		if (strstr(model, "CDROM") == NULL
			&& strstr(model, "CD-ROM") == NULL)
		{
			const char **p = patterns;
			while (*p != NULL && strstr(model, *p) == NULL)
				p++;
			if (*p != NULL)
				sprintf(writers[numOfWriters++], "%d,%d,%d",
					bus, ID, LUN);
		}
	}

	if (p.getResult() != 0)
		throw new Error("error running cdrecord -scanbus");

	log.put("CDROMs = %d, writers = %d", numOfCDROMs, numOfWriters);
	if (strcasecmp(cdrecordDevArg, "auto") == 0)
		if (numOfWriters == 0)
			if (numOfCDROMs == 0)
				throw new Error(
					"Cannot autodetect cdrecordDevArg");
			else
				cdrecordDevArg = strDup(CDROMs[0]);
		else
			cdrecordDevArg = strDup(writers[0]);

	if (strcasecmp(mkisofsMArg, "auto") == 0)
	{
		int i = 0;
		for (i = 0; i < numOfCDROMs; i++)
			if (strcmp(CDROMs[i], cdrecordDevArg) == 0)
				break;

		if (i < numOfCDROMs)
		{
			char str[1024];
			sprintf(str, "/dev/scd%d", i);
			mkisofsMArg = strDup(str);
		}
	}

	log.put("cdrecordDevArg = %s mkisofsMArg = %s",
		cdrecordDevArg, mkisofsMArg);
}

void Config::testCdrecord3()
{
	Process p(getCdrecordPath());
	char str[4*1024];
	sprintf(str, "dev=%s", getCdrecordDevArg());
	p.addArg(str);
	p.addArg("-inq");
	p.start();

	int errorCode = 0;
	str[0] = 0;
	while (1)
	{
		const char *lineStr = p.readLine();
		if (lineStr == NULL)
			break;

		char a[strlen(lineStr) + 1], b[strlen(lineStr) + 1];
		if (sscanf(lineStr, "%s : '%s", a, b) == 2)
		if (strcmp(a, "Vendor_info") == 0
			|| strcmp(a, "Identifikation") == 0
			|| strcmp(a, "Revision") == 0)
		{
			strcat(str, b);
			strcat(str, " ");
		}
	}

	if (p.getResult() != 0)
		throw new Error("error running cdrecord dev=%s -inq",
			getCdrecordDevArg());

	if (strlen(str) >= 2 && str[strlen(str) - 2] == '\'')
		str[strlen(str) - 2] = 0;
	writerInfo = strDup(str);
}

void Config::testMkisofs()
{
	Process p(getMkisofsPath());
	p.addArg("-version");
	p.start();
	const char *lineStr = p.readLine();
	if (lineStr == NULL)
		throw new Error("can't run mkisofs");
	if (p.getResult() != 0)
		throw new Error("error running mkisofs -version");
	char str1[strlen(lineStr) + 1], str2[strlen(lineStr) + 1];
	if (sscanf(lineStr, "%s %s", str1, str2) != 2)
		throw new Error("can't get version of mkisofs");
	char *str3 = new char[strlen(str1) + strlen(str2) + 2];
	sprintf(str3, "%s %s", str1, str2);
	mkisofsInfo = str3;
	log.put(lineStr);
}

void Config::testMpg123()
{
	Process mpg123(getMpg123Path());
	mpg123.addArg("-s");
	mpg123.addArg(".");
	if (mpg123.run() != 0)
		throw new Error("can't run mpg123");
}

gid_t Config::toGid(const char *groupName)
{
	struct group *group = getgrnam(groupName);
	if (group == NULL)
		throw new Error("unknown group: %s", groupName);
	return group->gr_gid;
}

const char *Config::toGroup(gid_t gid)
{
	struct group *group = getgrgid(gid);
	if (group == NULL)
	{
		static char str[20];
		sprintf(str, "%d", gid);
		return str;
	}
	else
		return group->gr_name;
}

const char *Config::toString(boolean b)
{
	if (b)
		return "on";
	else
		return "off";
}

uid_t Config::toUid(const char *userName)
{
	struct passwd *passwd = getpwnam(userName);
	if (passwd == NULL)
		throw new Error("unknown user: %s", userName);
	return passwd->pw_uid;
}

const char *Config::toUser(uid_t uid)
{
	struct passwd *passwd = getpwuid(uid);
	if (passwd == NULL)
	{
		static char str[20];
		sprintf(str, "%d", uid);
		return str;
	}
	else
		return passwd->pw_name;
}

void Config::write(FILE *outStream)
{
}

void Config::writeConfigFile()
{
	FILE *out = fopen(fileName, "w");
	if (out == NULL)
		throw new Error("can't create configFile %s", fileName);

	fprintf(out, "# Configuration of CDWserver\n");
	fprintf(out, "# See config.html for details\n\n");
	fprintf(out, "# 1. Features\n\n");
	fprintf(out, "burnProof=%s\n", toString(burnProof));
	fprintf(out, "imageOnTheFly=%s\n", toString(imageOnTheFly));
	fprintf(out, "laser=%s\n", toString(laserOn));
	fprintf(out, "MP3decoding=%s\n", toString(MP3decoding));
	fprintf(out, "\n");
	fprintf(out, "# 2. Server\n\n");
	fprintf(out, "group=%s\n", group);
	fprintf(out, "user=%s\n", user);
	fprintf(out, "portNo=%d\n", portNo);
	fprintf(out, "statusPortNo=%d\n", statusPortNo);
	fprintf(out, "maxOpenConnections=%d\n", maxOpenConnections);
	fprintf(out, "\n");
	fprintf(out, "# 3. Directories\n\n");
	fprintf(out, "logDir=%s\n", logDir);
	fprintf(out, "spoolDir=%s\n", spoolDir);
	fprintf(out, "\n");
	fprintf(out, "# 4. Cache\n\n");
	fprintf(out, "maxMBytesInSpoolDir=%d\n", maxMBytesInSpoolDir);
	fprintf(out, "maxMBytesPerSession=%d\n", maxMBytesPerSession);
	fprintf(out, "maxOpenSessions=%s\n", "auto");
	fprintf(out, "reservedMBytes=%d\n", reservedMBytes);
	fprintf(out, "\n");
	fprintf(out, "# 5. CD-writer(s)\n\n");
	fprintf(out, "cdrecordDevArg=%s\n", cdrecordDevArg);
	fprintf(out, "defaultSpeed=%d\n", defaultSpeed);
	fprintf(out, "maxSpeed=%d\n", maxSpeed);
	fprintf(out, "\n");
	fprintf(out, "# 6. Programs\n\n");
	fprintf(out, "cdrecordPath=%s\n", cdrecordPath);
	fprintf(out, "mkisofsPath=%s\n", mkisofsPath);
	fprintf(out, "mpg123Path=%s\n", mpg123Path);
	fprintf(out, "\n");
	fprintf(out, "# 7. Options\n\n");
	fprintf(out, "cdrecordArgs=%s\n", cdrecordArgs);
	fprintf(out, "#debug=Process,Server::noDisc()\n");
	fprintf(out, "#defaultFilesDir=/etc/CDWserver/default_files\n");
	fprintf(out, "#eMailOnError=unknown@unknown.org\n");
	fprintf(out, "#logoURL=http://unknown.org/logo.jpg\n");
	fprintf(out, "logRequests=%s\n", toString(logRequests));
	fprintf(out, "logSubProcess=%s\n", toString(logSubProcess));
	fprintf(out, "logVerbose=%s\n", toString(logVerbose));
	fprintf(out, "organization=%s\n", organization);
	fprintf(out, "removeSessions=%s\n", toString(removeSessions));
	fprintf(out, "showUsers=%s\n", toString(showUsers));
	fprintf(out, "waitForCD=%d\n", waitForCD);

	fclose(out);
}

void Config::writeInfoFile()
{
	FILE *out = fopen(infoFile, "w");
	if (out == NULL)
		throw new Error("can't create %s", infoFile);

	fprintf(out, "Welcome to CDWserver.\n");
	fprintf(out, "\n");
	fprintf(out, "This is the contents of %s.\n", infoFile);
	fclose(out);
}

void Config::writeWaitForCDMsgFile()
{
	FILE *out = fopen(waitForCDMsgFile, "w");
	if (out == NULL)
		throw new Error("can't create %s", waitForCDMsgFile);

	fprintf(out, "The CD-writer was reserved for %%d minutes.\n");
	fprintf(out, "Please insert a CD-R(W)!\n");
	fprintf(out, "\n");
	fprintf(out, "This is the contents of %s.\n", waitForCDMsgFile);
	fclose(out);
}
