// Copyright 2005-2019 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "murmur_pch.h"

#ifdef Q_OS_WIN
#include "Tray.h"
#include "About.h"
#endif

#include "Server.h"
#include "ServerDB.h"
#include "DBus.h"
#include "Meta.h"
#include "Version.h"
#include "SSL.h"
#include "License.h"
#include "LogEmitter.h"
#include "EnvUtils.h"

#ifdef Q_OS_UNIX
#include "UnixMurmur.h"
#endif

QFile *qfLog = NULL;

static bool bVerbose = false;
#ifdef QT_NO_DEBUG
static bool detach = true;
#else
static bool detach = false;
#endif

#ifdef Q_OS_UNIX
static UnixMurmur *unixMurmur = NULL;
#endif

Meta *meta = NULL;

static LogEmitter le;

static QStringList qlErrors;

static void murmurMessageOutputQString(QtMsgType type, const QString &msg) {
#ifdef Q_OS_UNIX
	if (unixMurmur->logToSyslog) {
		int level;
		switch (type) {
		case QtDebugMsg:
			level = LOG_DEBUG;
			break;
		case QtWarningMsg:
			level = LOG_WARNING;
			break;
		case QtCriticalMsg:
			level = LOG_CRIT;
			break;
		case QtFatalMsg:
		default:
			level = LOG_ALERT;
			break;
		}
		syslog(level, "%s", qPrintable(msg));
		if (type == QtFatalMsg) {
			exit(1);
		}
		return;
	}
#endif

	char c;
	switch (type) {
		case QtDebugMsg:
			if (! bVerbose)
				return;
			c='D';
			break;
		case QtWarningMsg:
			c='W';
			break;
		case QtCriticalMsg:
			c='C';
			break;
		case QtFatalMsg:
			c='F';
			break;
		default:
			c='X';
	}
	QString m= QString::fromLatin1("<%1>%2 %3").arg(QChar::fromLatin1(c)).arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz")).arg(msg);

	if (! qfLog || ! qfLog->isOpen()) {
#ifdef Q_OS_UNIX
		if (! detach)
			fprintf(stderr, "%s\n", qPrintable(m));
		else
			qlErrors << m;
#else
		qlErrors << m;
#ifndef QT_NO_DEBUG
		fprintf(stderr, "%s\n", qPrintable(m));
#endif
#endif
	} else {
		if (! qlErrors.isEmpty()) {
			foreach(const QString &e, qlErrors) {
				qfLog->write(e.toUtf8());
				qfLog->write("\n");
			}
			qlErrors.clear();
		}
		qfLog->write(m.toUtf8());
		qfLog->write("\n");
		qfLog->flush();
	}
	le.addLogEntry(m);
	if (type == QtFatalMsg) {
#ifdef Q_OS_UNIX
		if (detach) {
			if (qlErrors.isEmpty())
				qlErrors << msg;
			qlErrors << QString();
			m = qlErrors.join(QLatin1String("\n"));
			fprintf(stderr, "%s", qPrintable(m));
		}
#else
		::MessageBoxA(NULL, qPrintable(m), "Murmur", MB_OK | MB_ICONWARNING);
#endif
		exit(1);
	}
}

#if QT_VERSION >= 0x050000
static void murmurMessageOutputWithContext(QtMsgType type, const QMessageLogContext &ctx, const QString &msg) {
	Q_UNUSED(ctx);
	murmurMessageOutputQString(type, msg);
}
#else
static void murmurMessageOutput(QtMsgType type, const char *msg) {
	murmurMessageOutputQString(type, QString::fromUtf8(msg));
}
#endif

#ifdef USE_ICE
void IceParse(int &argc, char *argv[]);
void IceStart();
void IceStop();
#endif

#ifdef USE_GRPC
// From MurmurGRPCImpl.cpp.
void GRPCStart();
void GRPCStop();
#endif

int main(int argc, char **argv) {
	// Check for SSE and MMX, but only in the windows binaries
#ifdef Q_OS_WIN
	int cpuinfo[4];
	__cpuid(cpuinfo, 1);

#define MMXSSE 0x02800000
	if ((cpuinfo[3] & MMXSSE) != MMXSSE) {
		::MessageBoxA(NULL, "Mumble requires a SSE capable processor (Pentium 3 / Ahtlon-XP)", "Mumble", MB_OK | MB_ICONERROR);
		exit(0);
	}

	SetDllDirectory(L"");

#endif
	int res;

#ifdef USE_ICE
	IceParse(argc, argv);
#endif

#ifdef Q_OS_WIN
	QApplication a(argc, argv);
	a.setQuitOnLastWindowClosed(false);

	QIcon icon;
	icon.addFile(QLatin1String(":/murmur.16x16.png"));
	icon.addFile(QLatin1String(":/murmur.32x32.png"));
	icon.addFile(QLatin1String(":/murmur.64x64.png"));
	a.setWindowIcon(icon);
#else
#ifndef Q_OS_MAC
	EnvUtils::setenv(QLatin1String("AVAHI_COMPAT_NOWARN"), QLatin1String("1"));
#endif
	QCoreApplication a(argc, argv);
	UnixMurmur unixhandler;
	unixMurmur = &unixhandler;
	unixhandler.initialcap();
#endif
	a.setApplicationName("Murmur");
	a.setOrganizationName("Mumble");
	a.setOrganizationDomain("mumble.sourceforge.net");

	MumbleSSL::initialize();

	QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
#if QT_VERSION < 0x050000
	QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8"));
#endif

#ifdef Q_OS_WIN
	// By default, windbus expects the path to dbus-daemon to be in PATH, and the path
	// should contain bin\\, and the path to the config is hardcoded as ..\etc

	{
		QString path = EnvUtils::getenv(QLatin1String("PATH"));
		if (path.isEmpty()) {
			qWarning() << "Failed to get PATH. Not adding application directory to PATH. DBus bindings may not work.";
		} else {
			QString newPath = QString::fromLatin1("%1;%2").arg(QDir::toNativeSeparators(a.applicationDirPath())).arg(path);
			if (!EnvUtils::setenv(QLatin1String("PATH"), newPath)) {
				qWarning() << "Failed to set PATH. DBus bindings may not work.";
			}
		}
	}
#endif

	QString inifile;
	QString supw;
	bool disableSu = false;
	bool wipeSsl = false;
	bool wipeLogs = false;
	int sunum = 1;
#ifdef Q_OS_UNIX
	bool readPw = false;
#endif

	qsrand(QDateTime::currentDateTime().toTime_t());
#if QT_VERSION >= 0x050000
	qInstallMessageHandler(murmurMessageOutputWithContext);
#else
	qInstallMsgHandler(murmurMessageOutput);
#endif

#ifdef Q_OS_WIN
	Tray tray(NULL, &le);
#endif

	QStringList args = a.arguments();
	for (int i = 1; i < args.size(); i++) {
		bool bLast = false;
		QString arg = args.at(i).toLower();
		if ((arg == "-supw")) {
			detach = false;
			if (i+1 < args.size()) {
				i++;
				supw = args.at(i);
				if (i+1 < args.size()) {
					i++;
					sunum = args.at(i).toInt();
				}
				bLast = true;
			} else {
#ifdef Q_OS_UNIX
				qFatal("-supw expects the password on the command line - maybe you meant -readsupw?");
#else
				qFatal("-supw expects the password on the command line");
#endif
			}
#ifdef Q_OS_UNIX
		} else if ((arg == "-readsupw")) {
			detach = false;
			readPw = true;
			if (i+1 < args.size()) {
				i++;
				sunum = args.at(i).toInt();
			}
			bLast = true;
#endif
		} else if ((arg == "-disablesu")) {
		        detach = false;
		        disableSu = true;
		        if (i+1 < args.size()) {
		                i++;
		                sunum = args.at(i).toInt();
		        }
		        bLast = true;
		} else if ((arg == "-ini") && (i+1 < args.size())) {
			i++;
			inifile = args.at(i);
		} else if ((arg == "-wipessl")) {
			wipeSsl = true;
		} else if ((arg == "-wipelogs")) {
			wipeLogs = true;
		} else if ((arg == "-fg")) {
			detach = false;
		} else if ((arg == "-v")) {
			bVerbose = true;
		} else if ((arg == "-version") || (arg == "--version")) {
			detach = false;
			qInfo("%s -- %s", qPrintable(args.at(0)), MUMBLE_RELEASE);
			return 0;
		} else if (args.at(i) == QLatin1String("-license") || args.at(i) == QLatin1String("--license")) {
#ifdef Q_OS_WIN
			AboutDialog ad(NULL, AboutDialogOptionsShowLicense);
			ad.exec();
			return 0;
#else
			qInfo("%s\n", qPrintable(License::license()));
			return 0;
#endif
		} else if (args.at(i) == QLatin1String("-authors") || args.at(i) == QLatin1String("--authors")) {
#ifdef Q_OS_WIN
			AboutDialog ad(NULL, AboutDialogOptionsShowAuthors);
			ad.exec();
			return 0;
#else
			qInfo("%s\n", qPrintable(License::authors()));
			return 0;
#endif
		} else if (args.at(i) == QLatin1String("-third-party-licenses") || args.at(i) == QLatin1String("--third-party-licenses")) {
#ifdef Q_OS_WIN
			AboutDialog ad(NULL, AboutDialogOptionsShowThirdPartyLicenses);
			ad.exec();
			return 0;
#else
			qInfo("%s", qPrintable(License::printableThirdPartyLicenseInfo()));
			return 0;
#endif
		} else if ((arg == "-h") || (arg == "-help") || (arg == "--help")) {
			detach = false;
			qInfo("Usage: %s [-ini <inifile>] [-supw <password>]\n"
			       "  -ini <inifile>         Specify ini file to use.\n"
			       "  -supw <pw> [srv]       Set password for 'SuperUser' account on server srv.\n"
#ifdef Q_OS_UNIX
			       "  -readsupw [srv]        Reads password for server srv from standard input.\n"
#endif
			       "  -disablesu [srv]       Disable password for 'SuperUser' account on server srv.\n"
#ifdef Q_OS_UNIX
			       "  -limits                Tests and shows how many file descriptors and threads can be created.\n"
			       "                         The purpose of this option is to test how many clients Murmur can handle.\n"
			       "                         Murmur will exit after this test.\n"
#endif
			       "  -v                     Add verbose output.\n"
#ifdef Q_OS_UNIX
			       "  -fg                    Don't detach from console.\n"
#else
			       "  -fg                    Don't write to the log file.\n"
#endif
			       "  -wipessl               Remove SSL certificates from database.\n"
			       "  -wipelogs              Remove all log entries from database.\n"
			       "  -version               Show version information.\n"
			       "\n"
			       "  -license               Show Murmur's license.\n"
			       "  -authors               Show Murmur's authors.\n"
			       "  -third-party-licenses  Show licenses for third-party software used by Murmur.\n"
			       "\n"
			       "If no inifile is provided, murmur will search for one in \n"
			       "default locations.", qPrintable(args.at(0)));
			return 0;
#ifdef Q_OS_UNIX
		} else if (arg == "-limits") {
			detach = false;
			Meta::mp.read(inifile);
			unixhandler.setuid();
			unixhandler.finalcap();
			LimitTest::testLimits(a);
#endif
		} else {
			detach = false;
			qFatal("Unknown argument %s", qPrintable(args.at(i)));
		}
		if (bLast && (i+1 != args.size())) {
			detach = false;
			qFatal("Password arguments must be last.");
		}
	}

	if (QSslSocket::supportsSsl()) {
		qInfo("SSL: OpenSSL version is '%s'", SSLeay_version(SSLEAY_VERSION));
	} else {
		qFatal("SSL: this version of Murmur is built against Qt without SSL Support. Aborting.");
	}

#ifdef Q_OS_UNIX
	inifile = unixhandler.trySystemIniFiles(inifile);
#endif

	Meta::mp.read(inifile);

	// need to open log file early so log dir can be root owned:
	// http://article.gmane.org/gmane.comp.security.oss.general/4404
#ifdef Q_OS_UNIX
	unixhandler.logToSyslog = Meta::mp.qsLogfile == QLatin1String("syslog");
	if (detach && ! Meta::mp.qsLogfile.isEmpty() && !unixhandler.logToSyslog) {
#else
	if (detach && ! Meta::mp.qsLogfile.isEmpty()) {
#endif
		qfLog = new QFile(Meta::mp.qsLogfile);
		if (! qfLog->open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
			delete qfLog;
			qfLog = NULL;
#ifdef Q_OS_UNIX
			fprintf(stderr, "murmurd: failed to open logfile %s: no logging will be done\n", qPrintable(Meta::mp.qsLogfile));
#else
			qWarning("Failed to open logfile %s. No logging will be performed.", qPrintable(Meta::mp.qsLogfile));
#endif
		} else {
			qfLog->setTextModeEnabled(true);
			QFileInfo qfi(*qfLog);
			Meta::mp.qsLogfile = qfi.absoluteFilePath();
#ifdef Q_OS_UNIX
			if (Meta::mp.uiUid != 0 && fchown(qfLog->handle(), Meta::mp.uiUid, Meta::mp.uiGid) == -1) {
				qFatal("can't change log file owner to %d %d:%d - %s", qfLog->handle(), Meta::mp.uiUid, Meta::mp.uiGid, strerror(errno));
			}
#endif
		}
#ifdef Q_OS_UNIX
	} else if (detach && unixhandler.logToSyslog) {
		openlog("murmurd", LOG_PID, LOG_DAEMON);
		syslog(LOG_DEBUG, "murmurd syslog adapter up and running");
#endif
	} else {
		detach = false;
	}

#ifdef Q_OS_UNIX
	unixhandler.setuid();
#endif

	MumbleSSL::addSystemCA();

	ServerDB db;

	meta = new Meta();

#ifdef Q_OS_UNIX
	if (readPw) {
		char password[256];
		char *p;

		printf("Password: ");
		fflush(NULL);
		if (fgets(password, 255, stdin) != password)
			qFatal("No password provided");
		p = strchr(password, '\r');
		if (p)
			*p = 0;
		p = strchr(password, '\n');
		if (p)
			*p = 0;

		supw = QLatin1String(password);
	}
#endif

	if (!supw.isNull()) {
		if (supw.isEmpty()) {
			qFatal("Superuser password can not be empty");
		}
		ServerDB::setSUPW(sunum, supw);
		qInfo("Superuser password set on server %d", sunum);
		return 0;
	}

	if (disableSu) {
	        ServerDB::disableSU(sunum);
	        qInfo("SuperUser password disabled on server %d", sunum);
			return 0;
	}

	if (wipeSsl) {
		qWarning("Removing all per-server SSL certificates from the database.");
		foreach(int sid, ServerDB::getAllServers()) {
			ServerDB::setConf(sid, "key");
			ServerDB::setConf(sid, "certificate");
			ServerDB::setConf(sid, "passphrase");
			ServerDB::setConf(sid, "sslDHParams");
		}
	}

	if (wipeLogs) {
		qWarning("Removing all log entries from the database.");
		ServerDB::wipeLogs();
	}

#ifdef Q_OS_UNIX
	if (detach) {
		if (fork() != 0) {
			_exit(0);
		}
		setsid();
		if (fork() != 0) {
			_exit(0);
		}

		if (! Meta::mp.qsPid.isEmpty()) {
			QFile pid(Meta::mp.qsPid);
			if (pid.open(QIODevice::WriteOnly)) {
				QFileInfo fi(pid);
				Meta::mp.qsPid = fi.absoluteFilePath();

				QTextStream out(&pid);
				out << getpid();
				pid.close();
			}
		}

		if (chdir("/") != 0)
			fprintf(stderr, "Failed to chdir to /");
		int fd;

		fd = open("/dev/null", O_RDONLY);
		dup2(fd, 0);
		close(fd);

		fd = open("/dev/null", O_WRONLY);
		dup2(fd, 1);
		close(fd);

		fd = open("/dev/null", O_WRONLY);
		dup2(fd, 2);
		close(fd);
	}
	unixhandler.finalcap();
#endif

#ifdef USE_DBUS
	MurmurDBus::registerTypes();

	if (! Meta::mp.qsDBus.isEmpty()) {
		if (Meta::mp.qsDBus == "session")
			MurmurDBus::qdbc = new QDBusConnection(QDBusConnection::sessionBus());
		else if (Meta::mp.qsDBus == "system")
			MurmurDBus::qdbc = new QDBusConnection(QDBusConnection::systemBus());
		else {
			// QtDBus is not quite finished yet.
			qWarning("Warning: Peer-to-peer session support is currently nonworking.");
			MurmurDBus::qdbc = new QDBusConnection(QDBusConnection::connectToBus(Meta::mp.qsDBus, "mainbus"));
			if (! MurmurDBus::qdbc->isConnected()) {
				QDBusServer *qdbs = new QDBusServer(Meta::mp.qsDBus, &a);
				qWarning("%s",qPrintable(qdbs->lastError().name()));
				qWarning("%d",qdbs->isConnected());
				qWarning("%s",qPrintable(qdbs->address()));
				MurmurDBus::qdbc = new QDBusConnection(QDBusConnection::connectToBus(Meta::mp.qsDBus, "mainbus"));
			}
		}
		if (! MurmurDBus::qdbc->isConnected()) {
			qWarning("Failed to connect to D-Bus %s",qPrintable(Meta::mp.qsDBus));
		} else {
			new MetaDBus(meta);
			if (MurmurDBus::qdbc->isConnected()) {
				if (! MurmurDBus::qdbc->registerObject("/", meta) || ! MurmurDBus::qdbc->registerService(Meta::mp.qsDBusService)) {
					QDBusError e=MurmurDBus::qdbc->lastError();
					qWarning("Failed to register on DBus: %s %s", qPrintable(e.name()), qPrintable(e.message()));
				} else {
					qWarning("DBus registration succeeded");
				}
			}
		}
	}
#endif

#ifdef USE_ICE
	IceStart();
#endif

#ifdef USE_GRPC
	GRPCStart();
#else
	if (!meta->mp.qsGRPCAddress.isEmpty() || !meta->mp.qsGRPCCert.isEmpty() || !meta->mp.qsGRPCKey.isEmpty()) {
		qWarning("This version of Murmur was built without gRPC support. Ignoring 'grpc' option from configuration file.");
	}
#endif

	meta->getOSInfo();

	int major, minor, patch;
	QString strver;
	meta->getVersion(major, minor, patch, strver);

	qWarning("Murmur %d.%d.%d (%s) running on %s: %s: Booting servers", major, minor, patch, qPrintable(strver), qPrintable(meta->qsOS), qPrintable(meta->qsOSVersion));

	meta->bootAll();

	res=a.exec();

	qWarning("Killing running servers");

	meta->killAll();

	qWarning("Shutting down");

#ifdef USE_DBUS
	delete MurmurDBus::qdbc;
	MurmurDBus::qdbc = NULL;
#endif

#ifdef USE_ICE
	IceStop();
#endif

#ifdef USE_GRPC
	GRPCStop();
#endif

	delete qfLog;
	qfLog = NULL;

	delete meta;

#if QT_VERSION >= 0x050000
	qInstallMessageHandler(NULL);
#else
	qInstallMsgHandler(NULL);
#endif

#ifdef Q_OS_UNIX
	if (! Meta::mp.qsPid.isEmpty()) {
		QFile pid(Meta::mp.qsPid);
		pid.remove();
	}
#endif
	return res;
}
