MRCI/src/cmd_proc.cpp
Maurice O'Neal c8f53d1e5c Slimmed down and simplified host administering
- I decided to remove the entire concept of a root user.
  Instead, the host initializes as a blank slate and it
  will be up to the host admin to create a rank 1 user via
  the new command line option "-add_admin" to do initial
  setup with.

- There is no longer such a concept as a protected user.
  Meaning even the last rank 1 user in the host database
  is allowed to delete or modify the rank of their own
  account. To prevent permanent "admin lock out" in this
  scenario the "-elevate" command line option was created.

- Host settings are no longer stored in the database.
  Instead, host settings are now stored in a conf.json file
  in /etc/mrci/conf.json if running on a linux based OS or
  in %Programdata%\mrci\conf.json if running on Windows.

- Email templates are no longer stored in the database.
  Instead, the templates can be any file formatted in UTF-8
  text stored in the host file system. The files they point
  to can be modified in the conf.json file.

- The conf file also replaced all use env variables so
  MRCI_DB_PATH, MRCI_WORK_DIR, MRCI_PRIV_KEY and
  MRCI_PUB_KEY are no longer in use. SSL/TLS cert paths can
  be modified in the conf file.

- Removed email template cmds set_email_template and
  preview_email.

- Also removed cmds close_host, host_config and
  restart_host. The actions these commands could do is best
  left to the host system command line.

- The database class will now explicitly check for write
  permissions to the database and throw an appropriate
  error message if the check fails. "DROP TABLE" SQL
  abilities were added to make this happen.

- Removed async cmds exit(3), maxses(5) and restart(11).
2020-11-10 14:47:00 -05:00

712 lines
20 KiB
C++

#include "cmd_proc.h"
// This file is part of MRCI.
// MRCI 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 3 of the License, or
// (at your option) any later version.
// MRCI 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 MRCI under the LICENSE.md file. If not, see
// <http://www.gnu.org/licenses/>.
ModProcess::ModProcess(const QString &app, const QString &memSes, const QString &memHos, const QString &pipe, QObject *parent) : QProcess(parent)
{
flags = 0;
ipcTypeId = 0;
ipcDataSize = 0;
hostRank = 0;
modCmdNames = nullptr;
cmdUniqueNames = nullptr;
cmdRealNames = nullptr;
cmdAppById = nullptr;
cmdIds = nullptr;
ipcSocket = nullptr;
ipcServ = new QLocalServer(this);
idleTimer = new IdleTimer(this);
sesMemKey = memSes;
hostMemKey = memHos;
pipeName = pipe;
ipcServ->setMaxPendingConnections(1);
connect(this, &QProcess::readyReadStandardError, this, &ModProcess::rdFromStdErr);
connect(this, &QProcess::readyReadStandardOutput, this, &ModProcess::rdFromStdOut);
connect(this, &QProcess::errorOccurred, this, &ModProcess::err);
connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(onFinished(int,QProcess::ExitStatus)));
connect(ipcServ, &QLocalServer::newConnection, this, &ModProcess::newIPCLink);
connect(idleTimer, &IdleTimer::timeout, this, &ModProcess::killProc);
setProgram(app);
}
void ModProcess::rdFromStdErr()
{
emit dataToClient(toCmdId32(ASYNC_SYS_MSG, 0), readAllStandardError(), ERR);
}
void ModProcess::rdFromStdOut()
{
emit dataToClient(toCmdId32(ASYNC_SYS_MSG, 0), readAllStandardOutput(), TEXT);
}
quint16 ModProcess::genCmdId()
{
quint16 ret = 256;
if (flags & SESSION_PARAMS_SET)
{
while(cmdIds->contains(ret)) ret++;
}
return ret;
}
QString ModProcess::makeCmdUnique(const QString &name)
{
QString strNum;
QStringList names = cmdUniqueNames->values();
for (int i = 1; names.contains(name + strNum); ++i)
{
strNum = "_" + QString::number(i);
}
return QString(name + strNum).toLower();
}
bool ModProcess::isCmdLoaded(const QString &name)
{
bool ret = false;
if (modCmdNames->contains(program()))
{
if (modCmdNames->value(program()).contains(name))
{
ret = true;
}
}
return ret;
}
bool ModProcess::allowCmdLoad(const QString &cmdName)
{
bool ret = false;
if (validCommandName(cmdName))
{
if (flags & (LOADING_PUB_CMDS | LOADING_EXEMPT_CMDS))
{
ret = true;
}
else if (!cmdRanks.contains(cmdName))
{
ret = (hostRank == 1);
}
else
{
ret = (cmdRanks[cmdName] >= hostRank);
}
}
return ret;
}
void ModProcess::onDataFromProc(quint8 typeId, const QByteArray &data)
{
if ((typeId == NEW_CMD) && (flags & SESSION_PARAMS_SET))
{
if (data.size() >= 131)
{
// a valid NEW_CMD must have a minimum of 131 bytes.
auto cmdName = QString::fromUtf8(data.mid(3, 64)).trimmed().toLower();
if (isCmdLoaded(cmdName))
{
if (!allowCmdLoad(cmdName))
{
auto cmdId = cmdRealNames->key(cmdName);
cmdIds->removeOne(cmdId);
cmdRealNames->remove(cmdId);
cmdUniqueNames->remove(cmdId);
cmdAppById->remove(cmdId);
if (modCmdNames->contains(program()))
{
modCmdNames->operator[](program()).removeOne(cmdName);
}
emit cmdUnloaded(cmdId);
emit dataToClient(toCmdId32(ASYNC_RM_CMD, 0), wrInt(cmdId, 16), CMD_ID);
}
}
else if (allowCmdLoad(cmdName))
{
auto cmdId = genCmdId();
auto cmdIdBa = wrInt(cmdId, 16);
auto unique = makeCmdUnique(cmdName);
cmdIds->append(cmdId);
cmdRealNames->insert(cmdId, cmdName);
cmdUniqueNames->insert(cmdId, unique);
cmdAppById->insert(cmdId, program());
if (modCmdNames->contains(program()))
{
modCmdNames->operator[](program()).append(cmdName);
}
else
{
auto list = QStringList() << cmdName;
modCmdNames->insert(program(), list);
}
auto frame = cmdIdBa + data.mid(2, 1) + toFixedTEXT(unique, 64) + data.mid(67);
emit dataToClient(toCmdId32(ASYNC_ADD_CMD, 0), frame, NEW_CMD);
}
}
}
else if (typeId == ERR)
{
qDebug() << QString::fromUtf8(data);
}
}
void ModProcess::rdFromIPC()
{
if (flags & FRAME_RDY)
{
if (ipcSocket->bytesAvailable() >= ipcDataSize)
{
onDataFromProc(ipcTypeId, ipcSocket->read(ipcDataSize));
flags ^= FRAME_RDY;
rdFromIPC();
}
}
else if (ipcSocket->bytesAvailable() >= (FRAME_HEADER_SIZE - 4))
{
QByteArray header = ipcSocket->read(FRAME_HEADER_SIZE - 4);
ipcTypeId = static_cast<quint8>(header[0]);
ipcDataSize = static_cast<quint32>(rdInt(header.mid(1, 3)));
flags |= FRAME_RDY;
rdFromIPC();
}
}
void ModProcess::ipcDisconnected()
{
if (ipcSocket != nullptr)
{
ipcSocket->deleteLater();
}
ipcSocket = nullptr;
}
void ModProcess::newIPCLink()
{
if (ipcSocket != nullptr)
{
ipcServ->nextPendingConnection()->deleteLater();
}
else
{
ipcSocket = ipcServ->nextPendingConnection();
connect(ipcSocket, &QLocalSocket::readyRead, this, &ModProcess::rdFromIPC);
connect(ipcSocket, &QLocalSocket::disconnected, this, &ModProcess::ipcDisconnected);
onReady();
}
}
void ModProcess::setSessionParams(QHash<quint16, QString> *uniqueNames,
QHash<quint16, QString> *realNames,
QHash<quint16, QString> *appById,
QHash<QString, QStringList> *namesForMod,
QList<quint16> *ids,
quint32 rnk)
{
flags |= SESSION_PARAMS_SET;
modCmdNames = namesForMod;
cmdUniqueNames = uniqueNames;
cmdRealNames = realNames;
cmdAppById = appById;
cmdIds = ids;
hostRank = rnk;
Query db(this);
db.setType(Query::PULL, TABLE_CMD_RANKS);
db.addColumn(COLUMN_HOST_RANK);
db.addColumn(COLUMN_COMMAND);
db.addCondition(COLUMN_MOD_MAIN, program());
db.exec();
for (int i = 0; i < db.rows(); ++i)
{
cmdRanks.insert(db.getData(COLUMN_COMMAND, i).toString(), db.getData(COLUMN_HOST_RANK, i).toUInt());
}
}
void ModProcess::onFailToStart()
{
emit dataToClient(toCmdId32(ASYNC_SYS_MSG, 0), "\nerr: A module failed to start so some commands may not have loaded. detailed error information was logged for admin review.\n", ERR);
emit modProcFinished();
deleteLater();
}
void ModProcess::err(QProcess::ProcessError error)
{
if (error == QProcess::FailedToStart)
{
qDebug() << "err: Module process: " << program() << " failed to start. reason: " << errorString();
onFailToStart();
}
}
bool ModProcess::openPipe()
{
bool ret = ipcServ->listen(pipeName);
fullPipe = ipcServ->fullServerName();
if (!ipcServ->isListening())
{
QFile::remove(fullPipe);
ret = ipcServ->listen(pipeName);
}
return ret;
}
bool ModProcess::startProc(const QStringList &args)
{
bool ret = false;
if (openPipe())
{
fullPipe = ipcServ->fullServerName();
setArguments(QStringList() << "-pipe_name" << fullPipe << "-mem_ses" << sesMemKey << "-mem_host" << hostMemKey << args << additionalArgs);
start();
}
else
{
setErrorString("Unable to open pipe: " + fullPipe + " " + ipcServ->errorString());
emit errorOccurred(QProcess::FailedToStart);
}
return ret;
}
void ModProcess::addArgs(const QString &cmdLine)
{
additionalArgs = parseArgs(cmdLine.toUtf8(), -1);
}
bool ModProcess::loadPublicCmds()
{
flags |= LOADING_PUB_CMDS;
return startProc(QStringList() << "-public_cmds");
}
bool ModProcess::loadUserCmds()
{
flags |= LOADING_USER_CMDS;
return startProc(QStringList() << "-user_cmds");
}
bool ModProcess::loadExemptCmds()
{
flags |= LOADING_EXEMPT_CMDS;
return startProc(QStringList() << "-exempt_cmds");
}
void ModProcess::cleanupPipe()
{
ipcServ->close();
if (QFile::exists(fullPipe))
{
QFile::remove(fullPipe);
}
ipcDisconnected();
}
void ModProcess::onReady()
{
idleTimer->attach(ipcSocket, 5000); // 5sec idle timeout
auto hostVer = QCoreApplication::applicationVersion().split('.');
QByteArray verFrame;
verFrame.append(wrInt(hostVer[0].toULongLong(), 16));
verFrame.append(wrInt(hostVer[1].toULongLong(), 16));
verFrame.append(wrInt(hostVer[2].toULongLong(), 16));
wrIpcFrame(HOST_VER, verFrame);
}
void ModProcess::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
Q_UNUSED(exitCode)
Q_UNUSED(exitStatus)
emit modProcFinished();
cleanupPipe();
deleteLater();
}
void ModProcess::wrIpcFrame(quint8 typeId, const QByteArray &data)
{
if (ipcSocket != nullptr)
{
ipcSocket->write(wrInt(typeId, 8) + wrInt(data.size(), MAX_FRAME_BITS) + data);
}
}
void ModProcess::killProc()
{
wrIpcFrame(KILL_CMD, QByteArray());
QTimer::singleShot(3000, this, SLOT(kill()));
}
CmdProcess::CmdProcess(quint32 id, const QString &cmd, const QString &modApp, const QString &memSes, const QString &memHos, const QString &pipe, QObject *parent) : ModProcess(modApp, memSes, memHos, pipe, parent)
{
cmdId = id;
cmdName = cmd;
cmdIdle = false;
}
void CmdProcess::setSessionParams(QSharedMemory *mem, char *sesId, char *wrableSubChs, quint32 *hookCmd)
{
hook = hookCmd;
sesMem = mem;
sessionId = sesId;
openWritableSubChs = wrableSubChs;
}
void CmdProcess::killCmd16(quint16 id16)
{
if (toCmdId16(cmdId) == id16)
{
killProc();
}
}
void CmdProcess::killCmd32(quint32 id32)
{
if (cmdId == id32)
{
killProc();
}
}
void CmdProcess::onReady()
{
idleTimer->attach(ipcSocket, 120000); // 2min idle timeout
emit cmdProcReady(cmdId);
}
void CmdProcess::onFailToStart()
{
emit dataToClient(cmdId, "err: The command failed to start. error details were logged for admin review.\n", ERR);
emit dataToClient(cmdId, wrInt(FAILED_TO_START, 16), IDLE);
emit cmdProcFinished(cmdId);
deleteLater();
}
void CmdProcess::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
Q_UNUSED(exitCode)
Q_UNUSED(exitStatus)
if (!cmdIdle)
{
emit dataToClient(cmdId, "err: The command has stopped unexpectedly or it has failed to send an IDLE frame before exiting.\n", ERR);
emit dataToClient(cmdId, wrInt(CRASH, 16), IDLE);
}
emit cmdProcFinished(cmdId);
cleanupPipe();
deleteLater();
}
void CmdProcess::rdFromStdErr()
{
emit dataToClient(cmdId, readAllStandardError(), ERR);
}
void CmdProcess::rdFromStdOut()
{
emit dataToClient(cmdId, readAllStandardOutput(), TEXT);
}
void CmdProcess::dataFromSession(quint32 id, const QByteArray &data, quint8 dType)
{
if (id == cmdId)
{
cmdIdle = false;
wrIpcFrame(dType, data);
}
}
bool CmdProcess::validAsync(quint16 async, const QByteArray &data, QTextStream &errMsg)
{
auto ret = true;
if ((async == ASYNC_USER_DELETED) || (async == ASYNC_RW_MY_INFO) || (async == ASYNC_USER_LOGIN))
{
if (data.size() != BLKSIZE_USER_ID)
{
ret = false; errMsg << "the 256bit user id is not " << BLKSIZE_USER_ID << " bytes long.";
}
}
else if (async == ASYNC_USER_RENAMED)
{
if (data.size() != (BLKSIZE_USER_ID + BLKSIZE_USER_NAME))
{
ret = false; errMsg << "expected data containing the user id and name to be " << (BLKSIZE_USER_ID + BLKSIZE_USER_NAME) << " bytes long.";
}
}
else if (async == ASYNC_DISP_RENAMED)
{
if (data.size() != (BLKSIZE_USER_ID + BLKSIZE_DISP_NAME))
{
ret = false; errMsg << "expected data containing the user id and display name to be " << (BLKSIZE_USER_ID + BLKSIZE_DISP_NAME) << " bytes long.";
}
}
else if (async == ASYNC_USER_RANK_CHANGED)
{
if (data.size() != (BLKSIZE_USER_ID + BLKSIZE_HOST_RANK))
{
ret = false; errMsg << "expected data containing the user id and host rank to be " << (BLKSIZE_USER_ID + BLKSIZE_HOST_RANK) << " bytes long.";
}
}
else if ((async == ASYNC_CAST) || (async == ASYNC_LIMITED_CAST))
{
auto payloadOffs = (MAX_OPEN_SUB_CHANNELS * BLKSIZE_SUB_CHANNEL) + 1;
sesMem->lock();
if (data.size() < payloadOffs)
{
ret = false; errMsg << "the cast header is not at least " << payloadOffs << " bytes long.";
}
else if (!fullMatchChs(openWritableSubChs, data.data()))
{
ret = false; errMsg << "the sub-channels header contain a sub-channel that is not actually open for writing.";
}
else if (rd8BitFromBlock(data.data() + (payloadOffs - 1)) == PING_PEERS)
{
// casting PING_PEERS directly is blocked. command processes should use
// ASYNC_PING_PEERS instead.
ret = false; errMsg << "attempted to cast PING_PEERS which is forbidden for module commands.";
}
sesMem->unlock();
}
else if (async == ASYNC_P2P)
{
auto payloadOffs = ((BLKSIZE_SESSION_ID * 2) + 1);
if (data.size() < payloadOffs)
{
ret = false; errMsg << "p2p header is not at least " << payloadOffs << " bytes long.";
}
else if (memcmp(data.data() + BLKSIZE_SESSION_ID, sessionId, BLKSIZE_SESSION_ID) != 0)
{
// make sure P2P async commands source session id is the actual the local
// session. fraudulent P2P async's are blocked.
ret = false; errMsg << "the source session id does not match the actual session id.";
}
}
else if (async == ASYNC_CLOSE_P2P)
{
if (data.size() < BLKSIZE_SESSION_ID)
{
ret = false; errMsg << "p2p header is not at least " << BLKSIZE_SESSION_ID << " bytes long.";
}
else if (memcmp(data.data(), sessionId, BLKSIZE_SESSION_ID) != 0)
{
// make sure P2P async commands source session id is the actual the local
// session. fraudulent P2P async's are blocked.
ret = false; errMsg << "the source session id does not match the actual session id.";
}
}
else if ((async == ASYNC_OPEN_SUBCH) || (async == ASYNC_CLOSE_SUBCH))
{
if (data.size() != BLKSIZE_SUB_CHANNEL)
{
ret = false; errMsg << "the 72bit sub-channel id is not " << BLKSIZE_SUB_CHANNEL << " bytes long.";
}
}
else if ((async == ASYNC_RM_SUB_CH) || (async == ASYNC_SUB_CH_LEVEL_CHG) ||
(async == ASYNC_RM_RDONLY) || (async == ASYNC_ADD_RDONLY) ||
(async == ASYNC_CH_ACT_FLAG) || (async == ASYNC_NEW_SUB_CH) ||
(async == ASYNC_RENAME_SUB_CH))
{
if (data.size() < BLKSIZE_SUB_CHANNEL)
{
ret = false; errMsg << "a 72bit sub-channel id header is not present.";
}
}
else if ((async == ASYNC_NEW_CH_MEMBER) || (async == ASYNC_INVITED_TO_CH) ||
(async == ASYNC_INVITE_ACCEPTED) || (async == ASYNC_RM_CH_MEMBER) ||
(async == ASYNC_MEM_LEVEL_CHANGED))
{
if (data.size() < (BLKSIZE_USER_ID + BLKSIZE_CHANNEL_ID))
{
ret = false; errMsg << "the channel member info header is not at least " << (BLKSIZE_USER_ID + BLKSIZE_CHANNEL_ID) << "bytes long.";
}
}
else if ((async == ASYNC_RENAME_CH) || (async == ASYNC_DEL_CH))
{
if (data.size() < BLKSIZE_CHANNEL_ID)
{
ret = false; errMsg << "a 64bit channel id header was not found.";
}
}
else if ((async == ASYNC_RDY) || (async == ASYNC_SYS_MSG) || (async == ASYNC_TO_PEER) ||
(async == ASYNC_ADD_CMD) || (async == ASYNC_RM_CMD))
{
ret = false; errMsg << "all modules are not allowed to send this async command directly.";
}
else if ((async < 1) || (async > 46))
{
ret = false; errMsg << "undefined async command id.";
}
return ret;
}
void CmdProcess::asyncDirector(quint16 id, const QByteArray &payload)
{
if ((id == ASYNC_KEEP_ALIVE) || (id == ASYNC_DEBUG_TEXT) || (id == ASYNC_LOGOUT) || (id == ASYNC_SET_DIR) ||
(id == ASYNC_END_SESSION) || (id == ASYNC_USER_LOGIN) || (id == ASYNC_OPEN_SUBCH) || (id == ASYNC_CLOSE_SUBCH))
{
emit privIPC(id, payload);
}
else if ((id == ASYNC_CAST) || (id == ASYNC_LIMITED_CAST) || (id == ASYNC_P2P) || (id == ASYNC_CLOSE_P2P) ||
(id == ASYNC_PING_PEERS))
{
emit pubIPC(id, payload);
}
else if ((id == ASYNC_USER_DELETED) || (id == ASYNC_DISP_RENAMED) || (id == ASYNC_USER_RANK_CHANGED) || (id == ASYNC_CMD_RANKS_CHANGED) ||
(id == ASYNC_ENABLE_MOD) || (id == ASYNC_DISABLE_MOD) || (id == ASYNC_RW_MY_INFO) || (id == ASYNC_NEW_CH_MEMBER) ||
(id == ASYNC_DEL_CH) || (id == ASYNC_RENAME_CH) || (id == ASYNC_CH_ACT_FLAG) || (id == ASYNC_NEW_SUB_CH) ||
(id == ASYNC_RM_SUB_CH) || (id == ASYNC_RENAME_SUB_CH) || (id == ASYNC_INVITED_TO_CH) || (id == ASYNC_RM_CH_MEMBER) ||
(id == ASYNC_INVITE_ACCEPTED) || (id == ASYNC_MEM_LEVEL_CHANGED) || (id == ASYNC_SUB_CH_LEVEL_CHG) || (id == ASYNC_ADD_RDONLY) ||
(id == ASYNC_RM_RDONLY) || (id == ASYNC_USER_RENAMED))
{
emit pubIPCWithFeedBack(id, payload);
}
}
void CmdProcess::onDataFromProc(quint8 typeId, const QByteArray &data)
{
if (typeId == ASYNC_PAYLOAD)
{
if (data.size() >= 2)
{
auto async = rd16BitFromBlock(data.data());
// ASYNC_KEEP_ALIVE is blocked but not considered an error. it has already done
// it's job by getting transffered so it doesn't need to go any further.
if (async != ASYNC_KEEP_ALIVE)
{
auto payload = rdFromBlock(data.data() + 2, static_cast<quint32>(data.size() - 2));
QString errMsg;
QTextStream errTxt(&errMsg);
if (validAsync(async, payload, errTxt))
{
if (async == ASYNC_HOOK_INPUT)
{
*hook = cmdId;
}
else if (async == ASYNC_UNHOOK)
{
*hook = 0;
}
else
{
asyncDirector(async, payload);
}
}
else
{
qDebug() << "async id: " << async << " from command id: " << toCmdId16(cmdId) << " blocked. reason: " << errMsg;
}
}
}
}
else
{
if (typeId == IDLE)
{
cmdIdle = true;
if (*hook == cmdId)
{
*hook = 0;
}
if (data.isEmpty())
{
emit dataToClient(cmdId, wrInt(NO_ERRORS, 16), typeId);
}
else
{
emit dataToClient(cmdId, data, typeId);
}
}
else
{
emit dataToClient(cmdId, data, typeId);
}
}
}
bool CmdProcess::startCmdProc()
{
return startProc(QStringList() << "-run_cmd" << cmdName);
}