From 367ecc839bcb6159fcd923f24bce6db98a95efd5 Mon Sep 17 00:00:00 2001 From: Zii Date: Mon, 31 Jul 2023 11:16:07 -0400 Subject: [PATCH] v3.2.t1 -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. --- install.sh | 49 ++++++++++++-- src/camera.cpp | 77 +++++++++++---------- src/common.cpp | 177 +++++++++++++++++++++++++++---------------------- src/common.h | 29 ++------ src/main.cpp | 72 +++++++++++++++----- 5 files changed, 247 insertions(+), 157 deletions(-) diff --git a/install.sh b/install.sh index f7eb291..d5eb61f 100644 --- a/install.sh +++ b/install.sh @@ -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 diff --git a/src/camera.cpp b/src/camera.cpp index b555fa8..494ff41 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -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; diff --git a/src/common.cpp b/src/common.cpp index b4aaa1b..1afb09e 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -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; + } + } + } + } } } diff --git a/src/common.h b/src/common.h index 00c33e9..400e067 100644 --- a/src/common.h +++ b/src/common.h @@ -26,10 +26,11 @@ #include #include #include +#include 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 ¶m, const QString &line, QString *value); void rdLine(const QString ¶m, 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 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 diff --git a/src/main.cpp b/src/main.cpp index 78779ee..b7303ba 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 " << 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 " << 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;