-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 #!/bin/sh
if [ -f "/opt/mow/uninst" ]; then
mow -u -f
fi
if [ ! -d "/opt/mow" ]; then if [ ! -d "/opt/mow" ]; then
mkdir /opt/mow mkdir /opt/mow
fi 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 "#!/bin/sh\n" > /opt/mow/run
printf "/opt/mow/bin \$1 \$2 \$3\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 echo "writing /opt/mow/uninst"
rm /usr/bin/mow printf "#!/bin/sh\n" > /opt/mow/uninst
ln -s /opt/mow/run /usr/bin/mow 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,31 +12,39 @@
#include "camera.h" #include "camera.h"
Camera::Camera(QObject *parent) : QObject(parent) 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";
}
int Camera::start(const QStringList &args) 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)) QDir().mkpath(shared.outDir + "/events");
QDir().mkpath(shared.tmpDir + "/live");
QDir().mkpath(shared.tmpDir + "/logs");
QDir().mkpath(shared.tmpDir + "/img");
touch(shared.tmpDir + "/index.html");
touch(shared.tmpDir + "/stream.html");
touch(shared.tmpDir + "/stream.m3u8");
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 thr1 = new QThread(nullptr);
auto thr2 = new QThread(nullptr); auto thr2 = new QThread(nullptr);
@ -53,6 +61,7 @@ int Camera::start(const QStringList &args)
thr3->start(); thr3->start();
thr4->start(); thr4->start();
} }
}
return shared.retCode; return shared.retCode;
} }

View File

@ -194,6 +194,24 @@ bool rdConf(const QString &filePath, shared_t *share)
} }
else 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; QString line;
do do
@ -218,15 +236,7 @@ bool rdConf(const QString &filePath, shared_t *share)
} }
} while(!line.isEmpty()); } while(!line.isEmpty());
}
return share->retCode == 0;
}
bool rdConf(shared_t *share)
{
if (rdConf(share->conf, share))
{
if (share->camName.isEmpty()) if (share->camName.isEmpty())
{ {
share->camName = QFileInfo(share->conf).fileName(); share->camName = QFileInfo(share->conf).fileName();
@ -234,72 +244,39 @@ bool rdConf(shared_t *share)
share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName; share->outDir = QDir().cleanPath(share->webRoot) + "/" + share->camName;
share->tmpDir = share->buffPath + "/" + APP_BIN + "/" + share->camName; share->tmpDir = share->buffPath + "/" + APP_BIN + "/" + share->camName;
share->servPath = QString("/var/opt/") + APP_BIN + "/" + APP_BIN + "." + share->camName + ".service";
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;
}
} }
return share->retCode == 0; return share->retCode == 0;
} }
MultiInstance::MultiInstance(QObject *parent) : QObject(parent) {} void rmServices()
{
auto files = lsFilesInDir(QString("/var/opt/") + APP_BIN, ".service");
void MultiInstance::instStdout() for (auto &&serv : files)
{ {
for (auto &&proc : procList) 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()
{ {
QTextStream(stdout) << proc->readAllStandardOutput(); 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() int loadServices(const QStringList &args)
{
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)
{ {
auto ret = ENOENT; auto ret = ENOENT;
auto path = QDir().cleanPath(getParam("-d", args)); auto path = QDir().cleanPath(getParam("-d", args));
@ -307,35 +284,77 @@ int MultiInstance::start(const QStringList &args)
if (!QDir(path).exists()) 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()) 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 else
{ {
ret = 0; ret = 0;
QTextStream(stdout) << "loading conf files from dir: " << path << Qt::endl;
for (auto &&conf : files) 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); if (!file.open(QFile::ReadWrite | QFile::Truncate))
connect(proc, &QProcess::readyReadStandardError, this, &MultiInstance::instStderr); {
connect(proc, &QProcess::stateChanged, this, &MultiInstance::procChanged); 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); auto servName = QFileInfo(shared.servPath).fileName();
proc->setArguments(subArgs);
proc->start();
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 <QTimer>
#include <QStringList> #include <QStringList>
#include <QMutex> #include <QMutex>
#include <iostream>
using namespace std; using namespace std;
#define APP_VER "3.1" #define APP_VER "3.2.t1"
#define APP_NAME "Motion Watch" #define APP_NAME "Motion Watch"
#define APP_BIN "mow" #define APP_BIN "mow"
#define REC_LOG_NAME "rec_log_lines.html" #define REC_LOG_NAME "rec_log_lines.html"
@ -66,6 +67,7 @@ struct shared_t
QString webTxt; QString webTxt;
QString webFont; QString webFont;
QString webRoot; QString webRoot;
QString servPath;
bool skipCmd; bool skipCmd;
int evMaxSecs; int evMaxSecs;
int postSecs; 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 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); QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
bool rdConf(const QString &filePath, shared_t *share); 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 touch(const QString &path);
void rdLine(const QString &param, const QString &line, QString *value); void rdLine(const QString &param, const QString &line, QString *value);
void rdLine(const QString &param, const QString &line, int *value); void rdLine(const QString &param, const QString &line, int *value);
@ -90,25 +94,4 @@ void enforceMaxEvents(shared_t *share);
void enforceMaxImages(); void enforceMaxImages();
void enforceMaxVids(); 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 #endif // COMMON_H

View File

@ -13,6 +13,27 @@
#include "common.h" #include "common.h"
#include "camera.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) int main(int argc, char** argv)
{ {
QCoreApplication app(argc, argv); QCoreApplication app(argc, argv);
@ -25,14 +46,7 @@ int main(int argc, char** argv)
if (args.contains("-h")) if (args.contains("-h"))
{ {
QTextStream(stdout) << "Motion Watch " << APP_VER << Qt::endl << Qt::endl; showHelp();
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;
} }
else if (args.contains("-v")) else if (args.contains("-v"))
{ {
@ -40,14 +54,17 @@ int main(int argc, char** argv)
} }
else if (args.contains("-d")) else if (args.contains("-d"))
{ {
auto *muli = new MultiInstance(&app); rmServices(); ret = loadServices(args);
ret = muli->start(args);
if (ret == 0)
{
ret = QCoreApplication::exec();
} }
else if (args.contains("-l"))
{
listServices();
}
else if (args.contains("-i"))
{
args = {"-d", "/etc/" + QString(APP_BIN)};
rmServices(); ret = loadServices(args);
} }
else if (args.contains("-c")) else if (args.contains("-c"))
{ {
@ -60,9 +77,32 @@ int main(int argc, char** argv)
ret = QCoreApplication::exec(); ret = QCoreApplication::exec();
} }
} }
else if (args.contains("-u"))
{
if (args.contains("-f"))
{
rmServices(); QProcess::execute("/opt/mow/uninst");
}
else else
{ {
QTextStream(stderr) << "err: no config file(s) were given in -c" << Qt::endl; 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
{
showHelp();
} }
return ret; return ret;