-the -d option will no longer directly run camera instances. it
 will instead utilize systemd to install camera services.

-added -i, -l and -r options to manage camera services.

-the app will now default work and output directories in the
 /var/opt directory instead of /var/www/html and /tmp, making
 it a better neighbour in the fact that it won't overwrite
 existing websites or tmp files.

-install.sh will now create an unprivileged mow user that will
 be used to run camera services. all in effort to make the app
 a good neighour and follow good security practice.
This commit is contained in:
Zii 2023-07-31 11:16:07 -04:00
parent 25528617d5
commit 367ecc839b
5 changed files with 247 additions and 157 deletions

View File

@ -1,11 +1,50 @@
#!/bin/sh
if [ -f "/opt/mow/uninst" ]; then
mow -u -f
fi
if [ ! -d "/opt/mow" ]; then
mkdir /opt/mow
fi
cp ./.build-mow/mow /opt/mow/bin
if [ ! -d "/var/opt/mow" ]; then
mkdir /var/opt/mow
fi
if [ ! -d "/etc/mow" ]; then
mkdir /etc/mow
fi
if [ ! -d "/var/opt/mow/buf" ]; then
mkdir /var/opt/mow/buf
fi
if [ ! -d "/var/opt/mow/web" ]; then
mkdir /var/opt/mow/web
fi
cp -v ./.build-mow/mow /opt/mow/bin
echo "writing /opt/mow/run"
printf "#!/bin/sh\n" > /opt/mow/run
printf "/opt/mow/bin \$1 \$2 \$3\n" >> /opt/mow/run
chmod +x /opt/mow/run
chmod +x /opt/mow/bin
rm /usr/bin/mow
ln -s /opt/mow/run /usr/bin/mow
echo "writing /opt/mow/uninst"
printf "#!/bin/sh\n" > /opt/mow/uninst
printf "rm -v /opt/mow/bin\n" >> /opt/mow/uninst
printf "rm -v /opt/mow/run\n" >> /opt/mow/uninst
printf "rm -v /opt/mow/uninst\n" >> /opt/mow/uninst
printf "rm -v /usr/bin/mow\n" >> /opt/mow/uninst
printf "rm -rv /opt/mow\n" >> /opt/mow/uninst
printf "rm -r /var/opt/mow/buf\n" >> /opt/mow/uninst
printf "deluser mow\n" >> /opt/mow/uninst
useradd -r mow
chown -R mow:mow /var/opt/mow
chmod -v +x /opt/mow/run
chmod -v +x /opt/mow/bin
chmod -v +x /opt/mow/uninst
ln -sv /opt/mow/run /usr/bin/mow

View File

@ -12,46 +12,55 @@
#include "camera.h"
Camera::Camera(QObject *parent) : QObject(parent)
{
shared.recordUrl.clear();
shared.postCmd.clear();
shared.camName.clear();
shared.retCode = 0;
shared.imgThresh = 8000;
shared.maxEvents = 100;
shared.maxLogSize = 100000;
shared.skipCmd = false;
shared.postSecs = 60;
shared.evMaxSecs = 30;
shared.buffPath = QDir::tempPath();
shared.webRoot = "/var/www/html";
shared.webBg = "#485564";
shared.webTxt = "#dee5ee";
shared.webFont = "courier";
}
Camera::Camera(QObject *parent) : QObject(parent) {}
int Camera::start(const QStringList &args)
{
shared.conf = getParam("-c", args);
if (rdConf(getParam("-c", args), &shared))
{
QDir().mkpath(shared.outDir);
QDir().mkpath(shared.tmpDir);
if (rdConf(&shared))
{
auto thr1 = new QThread(nullptr);
auto thr2 = new QThread(nullptr);
auto thr3 = new QThread(nullptr);
auto thr4 = new QThread(nullptr);
QDir().mkpath(shared.outDir + "/events");
QDir().mkpath(shared.tmpDir + "/live");
QDir().mkpath(shared.tmpDir + "/logs");
QDir().mkpath(shared.tmpDir + "/img");
new RecLoop(&shared, thr1, nullptr);
new Upkeep(&shared, thr2, nullptr);
new EventLoop(&shared, thr3, nullptr);
new DetectLoop(&shared, thr4, nullptr);
touch(shared.tmpDir + "/index.html");
touch(shared.tmpDir + "/stream.html");
touch(shared.tmpDir + "/stream.m3u8");
thr1->start();
thr2->start();
thr3->start();
thr4->start();
QFile::link(shared.tmpDir + "/live", shared.outDir + "/live");
QFile::link(shared.tmpDir + "/logs", shared.outDir + "/logs");
QFile::link(shared.tmpDir + "/img", shared.outDir + "/img");
QFile::link(shared.tmpDir + "/index.html", shared.outDir + "/index.html");
QFile::link(shared.tmpDir + "/stream.html", shared.outDir + "/stream.html");
QFile::link(shared.tmpDir + "/stream.m3u8", shared.outDir + "/stream.m3u8");
QFile::link(shared.outDir + "/events", shared.tmpDir + "/events");
if (!QDir::setCurrent(shared.tmpDir))
{
QTextStream(stderr) << "err: failed to change/create the current working directory to camera folder: '" << shared.outDir << "' does it exists?" << Qt::endl;
shared.retCode = ENOENT;
}
else
{
auto thr1 = new QThread(nullptr);
auto thr2 = new QThread(nullptr);
auto thr3 = new QThread(nullptr);
auto thr4 = new QThread(nullptr);
new RecLoop(&shared, thr1, nullptr);
new Upkeep(&shared, thr2, nullptr);
new EventLoop(&shared, thr3, nullptr);
new DetectLoop(&shared, thr4, nullptr);
thr1->start();
thr2->start();
thr3->start();
thr4->start();
}
}
return shared.retCode;

View File

@ -194,6 +194,24 @@ bool rdConf(const QString &filePath, shared_t *share)
}
else
{
share->recordUrl.clear();
share->postCmd.clear();
share->camName.clear();
share->retCode = 0;
share->imgThresh = 8000;
share->maxEvents = 100;
share->maxLogSize = 100000;
share->skipCmd = false;
share->postSecs = 60;
share->evMaxSecs = 30;
share->conf = filePath;
share->buffPath = "/var/opt/" + QString(APP_BIN) + "/buf";
share->webRoot = "/var/opt/" + QString(APP_BIN) + "/web";
share->webBg = "#485564";
share->webTxt = "#dee5ee";
share->webFont = "courier";
QString line;
do
@ -218,88 +236,47 @@ bool rdConf(const QString &filePath, shared_t *share)
}
} while(!line.isEmpty());
}
return share->retCode == 0;
}
bool rdConf(shared_t *share)
{
if (rdConf(share->conf, share))
{
if (share->camName.isEmpty())
{
share->camName = QFileInfo(share->conf).fileName();
}
share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName;
share->tmpDir = share->buffPath + "/" + APP_BIN + "/" + share->camName;
QDir().mkpath(share->outDir);
QDir().mkpath(share->tmpDir);
QDir().mkpath(share->outDir + "/events");
QDir().mkpath(share->tmpDir + "/live");
QDir().mkpath(share->tmpDir + "/logs");
QDir().mkpath(share->tmpDir + "/img");
touch(share->tmpDir + "/index.html");
touch(share->tmpDir + "/stream.html");
touch(share->tmpDir + "/stream.m3u8");
QFile::link(share->tmpDir + "/live", share->outDir + "/live");
QFile::link(share->tmpDir + "/logs", share->outDir + "/logs");
QFile::link(share->tmpDir + "/img", share->outDir + "/img");
QFile::link(share->tmpDir + "/index.html", share->outDir + "/index.html");
QFile::link(share->tmpDir + "/stream.html", share->outDir + "/stream.html");
QFile::link(share->tmpDir + "/stream.m3u8", share->outDir + "/stream.m3u8");
QFile::link(share->outDir + "/events", share->tmpDir + "/events");
if (!QDir::setCurrent(share->tmpDir))
{
QTextStream(stderr) << "err: failed to change/create the current working directory to camera folder: '" << share->outDir << "' does it exists?" << Qt::endl;
share->retCode = ENOENT;
}
share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName;
share->tmpDir = share->buffPath + "/" + APP_BIN + "/" + share->camName;
share->servPath = QString("/var/opt/") + APP_BIN + "/" + APP_BIN + "." + share->camName + ".service";
}
return share->retCode == 0;
}
MultiInstance::MultiInstance(QObject *parent) : QObject(parent) {}
void MultiInstance::instStdout()
void rmServices()
{
for (auto &&proc : procList)
auto files = lsFilesInDir(QString("/var/opt/") + APP_BIN, ".service");
for (auto &&serv : files)
{
QTextStream(stdout) << proc->readAllStandardOutput();
QProcess::execute("systemctl", {"stop", serv});
QProcess::execute("systemctl", {"disable", serv});
QFile::remove(QString("/lib/systemd/system/") + serv);
QFile::remove(QString("/var/opt/") + APP_BIN + "/" + serv);
}
QProcess::execute("systemctl", {"daemon-reload"});
}
void listServices()
{
auto files = lsFilesInDir(QString("/var/opt/") + APP_BIN, ".service");
for (auto &&serv : files)
{
QTextStream(stdout) << serv << ": "; QProcess::execute("systemctl", {"is-active", serv});
}
}
void MultiInstance::instStderr()
{
for (auto &&proc : procList)
{
QTextStream(stderr) << proc->readAllStandardError();
}
}
void MultiInstance::procChanged(QProcess::ProcessState newState)
{
Q_UNUSED(newState)
for (auto &&proc : procList)
{
if (proc->state() == QProcess::Running)
{
return;
}
}
QCoreApplication::quit();
}
int MultiInstance::start(const QStringList &args)
int loadServices(const QStringList &args)
{
auto ret = ENOENT;
auto path = QDir().cleanPath(getParam("-d", args));
@ -307,35 +284,77 @@ int MultiInstance::start(const QStringList &args)
if (!QDir(path).exists())
{
QTextStream(stderr) << "err: the supplied directory in -d '" << path << "' does not exists or is not a directory.";
QTextStream(stderr) << "err: the supplied directory in -d '" << path << "' does not exists or is not a directory." << Qt::endl;
}
else if (files.isEmpty())
{
QTextStream(stderr) << "err: no config files found in '" << path << "'";
QTextStream(stderr) << "err: no config files found in '" << path << "'" << Qt::endl;
}
else
{
ret = 0;
QTextStream(stdout) << "loading conf files from dir: " << path << Qt::endl;
for (auto &&conf : files)
{
auto proc = new QProcess(this);
shared_t shared;
QStringList subArgs;
if (!rdConf(path + "/" + conf, &shared))
{
ret = shared.retCode; break;
}
else
{
QTextStream(stdout) << conf << " --" << Qt::endl;
subArgs << "-c" << path + "/" + conf;
QFile file(shared.servPath);
connect(proc, &QProcess::readyReadStandardOutput, this, &MultiInstance::instStdout);
connect(proc, &QProcess::readyReadStandardError, this, &MultiInstance::instStderr);
connect(proc, &QProcess::stateChanged, this, &MultiInstance::procChanged);
if (!file.open(QFile::ReadWrite | QFile::Truncate))
{
QTextStream(stderr) << "err: failed to open service file: " << shared.servPath << " for writing. reason: " << file.errorString();
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, proc, &QProcess::terminate);
ret = EACCES; file.close(); break;
}
else
{
file.write("[Unit]\n");
file.write("Description=" + QByteArray(APP_NAME) + " Camera - " + shared.camName.toUtf8() + "\n");
file.write("After=network.target\n\n");
file.write("[Service]\n");
file.write("Type=simple\n");
file.write("User=" + QByteArray(APP_BIN) + "\n");
file.write("Restart=always\n");
file.write("RestartSec=5\n");
file.write("TimeoutStopSec=infinity\n");
file.write("ExecStart=/usr/bin/env " + QByteArray(APP_BIN) + " -c " + shared.conf.toUtf8() + "\n\n");
file.write("[Install]\n");
file.write("WantedBy=multi-user.target");
file.close();
proc->setProgram(APP_BIN);
proc->setArguments(subArgs);
proc->start();
auto servName = QFileInfo(shared.servPath).fileName();
procList.append(proc);
if (!QFile::link(shared.servPath, "/lib/systemd/system/" + servName))
{
ret = EACCES; break;
}
else
{
if (ret == 0) ret = QProcess::execute("systemctl", {"daemon-reload"});
if (ret == 0) ret = QProcess::execute("systemctl", {"enable", servName});
if (ret == 0) ret = QProcess::execute("systemctl", {"start", servName});
if (ret != 0)
{
break;
}
else
{
QTextStream(stdout) << "Successfully loaded camera service: " << servName << Qt::endl;
}
}
}
}
}
}

View File

@ -26,10 +26,11 @@
#include <QTimer>
#include <QStringList>
#include <QMutex>
#include <iostream>
using namespace std;
#define APP_VER "3.1"
#define APP_VER "3.2.t1"
#define APP_NAME "Motion Watch"
#define APP_BIN "mow"
#define REC_LOG_NAME "rec_log_lines.html"
@ -66,6 +67,7 @@ struct shared_t
QString webTxt;
QString webFont;
QString webRoot;
QString servPath;
bool skipCmd;
int evMaxSecs;
int postSecs;
@ -82,7 +84,9 @@ QStringList listFacingFiles(const QString &path, const QString &ext, const QDate
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
bool rdConf(const QString &filePath, shared_t *share);
bool rdConf(shared_t *share);
int loadServices(const QStringList &args);
void listServices();
void rmServices();
void touch(const QString &path);
void rdLine(const QString &param, const QString &line, QString *value);
void rdLine(const QString &param, const QString &line, int *value);
@ -90,25 +94,4 @@ void enforceMaxEvents(shared_t *share);
void enforceMaxImages();
void enforceMaxVids();
class MultiInstance : public QObject
{
Q_OBJECT
private:
QList<QProcess*> procList;
private slots:
void instStdout();
void instStderr();
void procChanged(QProcess::ProcessState newState);
public:
explicit MultiInstance(QObject *parent = nullptr);
int start(const QStringList &args);
};
#endif // COMMON_H

View File

@ -13,6 +13,27 @@
#include "common.h"
#include "camera.h"
void showHelp()
{
QTextStream(stdout) << APP_NAME << " " << APP_VER << Qt::endl << Qt::endl;
QTextStream(stdout) << "Usage: mow <argument>" << Qt::endl << Qt::endl;
QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl;
QTextStream(stdout) << "-c : path to the config file used to run a single camera instance." << Qt::endl;
QTextStream(stdout) << "-d : path to a directory that can contain multiple config files." << Qt::endl;
QTextStream(stdout) << " each file found in the directory will be used to create a " << Qt::endl;
QTextStream(stdout) << " systemd service for the camera. already existing camera" << Qt::endl;
QTextStream(stdout) << " services will be reloaded and restarted. any services without" << Qt::endl;
QTextStream(stdout) << " a config file will be removed." << Qt::endl;
QTextStream(stdout) << "-i : this is the same as -d except a directory does not need to be" << Qt::endl;
QTextStream(stdout) << " provided. the default directory /etc/" << APP_BIN << " will be used." << Qt::endl;
QTextStream(stdout) << "-v : display the current version." << Qt::endl;
QTextStream(stdout) << "-u : uninstall the entire app from your system, including all" << Qt::endl;
QTextStream(stdout) << " camera services." << Qt::endl;
QTextStream(stdout) << "-f : force an action without pausing for user confirmation." << Qt::endl;
QTextStream(stdout) << "-l : list all camera services along with statuses." << Qt::endl;
QTextStream(stdout) << "-r : remove all camera services." << Qt::endl;
}
int main(int argc, char** argv)
{
QCoreApplication app(argc, argv);
@ -25,14 +46,7 @@ int main(int argc, char** argv)
if (args.contains("-h"))
{
QTextStream(stdout) << "Motion Watch " << APP_VER << Qt::endl << Qt::endl;
QTextStream(stdout) << "Usage: mow <argument>" << Qt::endl << Qt::endl;
QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl;
QTextStream(stdout) << "-c : path to the config file used to run a single camera instance." << Qt::endl;
QTextStream(stdout) << "-d : path to a directory that can contain multiple config files." << Qt::endl;
QTextStream(stdout) << " each file found in the directory will be used to run a" << Qt::endl;
QTextStream(stdout) << " camera instance." << Qt::endl;
QTextStream(stdout) << "-v : display the current version." << Qt::endl << Qt::endl;
showHelp();
}
else if (args.contains("-v"))
{
@ -40,14 +54,17 @@ int main(int argc, char** argv)
}
else if (args.contains("-d"))
{
auto *muli = new MultiInstance(&app);
rmServices(); ret = loadServices(args);
}
else if (args.contains("-l"))
{
listServices();
}
else if (args.contains("-i"))
{
args = {"-d", "/etc/" + QString(APP_BIN)};
ret = muli->start(args);
if (ret == 0)
{
ret = QCoreApplication::exec();
}
rmServices(); ret = loadServices(args);
}
else if (args.contains("-c"))
{
@ -60,9 +77,32 @@ int main(int argc, char** argv)
ret = QCoreApplication::exec();
}
}
else if (args.contains("-u"))
{
if (args.contains("-f"))
{
rmServices(); QProcess::execute("/opt/mow/uninst");
}
else
{
char ans;
std::cout << "This will completely uninstall " << APP_NAME << " from your system. continue y/n? ";
std::cin >> ans;
if (ans == 'y' || ans == 'Y')
{
rmServices(); QProcess::execute("/opt/mow/uninst");
}
}
}
else if (args.contains("-r"))
{
rmServices();
}
else
{
QTextStream(stderr) << "err: no config file(s) were given in -c" << Qt::endl;
showHelp();
}
return ret;