-the built in ssl cert generater was deprecated so it needed to be
 updated or removed. I opted to calling openssl via external
 command line execution. doing it this way make it easier to
 update if deprecated again and reduces dependancy on libssl.

-tweaked the documentation a little.

-updated build.py to explicitly get the plugins directoy instead
 of deriving it from the bin directory.
This commit is contained in:
Zii 2023-07-21 19:30:31 -04:00
parent fabc82a0a2
commit 51072f0a59
11 changed files with 130 additions and 272 deletions

View File

@ -23,7 +23,7 @@ Usage: mrci <argument>
-load_ssl : re-load the host SSL certificate without stopping the host instance.
-elevate : elevate any user account to rank 1.
-res_pw : reset a user account password with a randomized one time password.
-add_admin : create a rank 1 account with a randomized password.
-add_admin : create a rank 1 account with a randomized one time password.
Internal module | -public_cmds, -user_cmds, -exempt_cmds, -run_cmd |:
@ -46,9 +46,9 @@ add_admin - this argument takes a single string representing a user name to crea
elevate - this argument takes a single string representing a user name to an account to promote to rank 1.
example: -elevate somebody
run_cmd - this argument is used by the host itself, along side the internal module arguments below to run
the internal command names passed by it. this is not meant to be run directly by human input.
the executable will auto close if it fails to connect to the pipe and/or shared memory segments
run_cmd - this argument is used by the host itself along with the internal module arguments to run the
internal command names passed by it. this is not ment to be run directly by human input. the
executable will auto close if it fails to connect to the pipe and/or shared memory segments
```
The host can be managed via a connected client that supports text input/output so the host application is always listening for clients while running entirely in the background. By default the host listen for clients on address 0.0.0.0 and port 35516, effectively making it reachable on any network interface of the host platform via that specific port.
@ -60,7 +60,7 @@ Typical use for a MRCI host is to run commands on a remote host that clients ask
* Broadcast any type of data to all peers connected to the host.
* Run remote commands on connected peers.
* Host object positioning data for peers (online games do this).
* Send data to/from a peer client directly.
* Send data to/from a peer clients directly.
* Fully featured user account management system.
* Built in permissions and command access management.
* Host limits management (max concurrent users, max failed password attempts, etc...).
@ -103,7 +103,7 @@ Python3
### Build ###
To build this project from source you just need to run the build.py and then the install.py python scripts. While running the build the script, it will try to find the Qt API installed in your machine according to the PATH env variable. If not found, it will ask you to input where it can find the Qt bin folder where the qmake executable exists or you can bypass all of this by passing the -qt_dir option on it's command line.
To build this project from source you just need to run the ```build.py``` and then the ```install.py``` python scripts. While running the build the script, it will try to find the Qt API installed in your machine according to the PATH env variable. If not found, it will ask you to input where it can find the Qt bin folder where the qmake executable exists or you can bypass all of this by passing the -qt_dir option on it's command line.
while running the install script, it will ask you to input 1 of 3 options:

View File

@ -24,6 +24,15 @@ def get_qt_path():
print("A direct call to 'qtpaths' has failed so automatic retrieval of the QT bin folder is not possible.")
return input("Please enter the QT bin path (leave blank to cancel the build): ")
def get_qt_plugin_path():
try:
return str(subprocess.check_output(["qtpaths", "--plugin-dir"]), 'utf-8').strip()
except:
print("A direct call to 'qtpaths' has failed so automatic retrieval of the QT plugins folder is not possible.")
return input("Please enter the QT plugins path (leave blank to cancel the build): ")
def get_qt_from_cli():
for arg in sys.argv:
@ -37,6 +46,19 @@ def get_qt_from_cli():
return ""
return ""
def get_qt_plug_from_cli():
for arg in sys.argv:
if arg == "-qt_plugin":
index = sys.argv.index(arg)
try:
return sys.argv[index + 1]
except:
return ""
return ""
def get_info_header():
current_dir = os.path.dirname(__file__)
@ -130,17 +152,17 @@ def verbose_copy(src, dst):
else:
print("wrn: " + src + " does not exists. skipping.")
def linux_build_app_dir(app_ver, app_name, app_target, qt_bin):
def linux_build_app_dir(app_ver, app_name, app_target, qt_bin, qt_plug):
if not os.path.exists("app_dir/linux/sqldrivers"):
os.makedirs("app_dir/linux/sqldrivers")
if not os.path.exists("app_dir/linux/lib"):
os.makedirs("app_dir/linux/lib")
verbose_copy(qt_bin + "/../plugins/sqldrivers/libqsqlite.so", "app_dir/linux/sqldrivers/libqsqlite.so")
verbose_copy(qt_bin + "/../plugins/sqldrivers/libqsqlodbc.so", "app_dir/linux/sqldrivers/libqsqlodbc.so")
verbose_copy(qt_bin + "/../plugins/sqldrivers/libqsqlpsql.so", "app_dir/linux/sqldrivers/libqsqlpsql.so")
verbose_copy(qt_bin + "/../plugins/sqldrivers/libqsqlmysql.so", "app_dir/linux/sqldrivers/libqsqlmysql.so")
verbose_copy(qt_plug + "/sqldrivers/libqsqlite.so", "app_dir/linux/sqldrivers/libqsqlite.so")
verbose_copy(qt_plug + "/sqldrivers/libqsqlodbc.so", "app_dir/linux/sqldrivers/libqsqlodbc.so")
verbose_copy(qt_plug + "/sqldrivers/libqsqlpsql.so", "app_dir/linux/sqldrivers/libqsqlpsql.so")
verbose_copy(qt_plug + "/sqldrivers/libqsqlmysql.so", "app_dir/linux/sqldrivers/libqsqlmysql.so")
verbose_copy("build/linux/" + app_target, "app_dir/linux/" + app_target)
shutil.copyfile("build/linux/" + app_target, "/tmp/" + app_target)
@ -221,17 +243,26 @@ def main():
app_ver = get_app_ver(text)
app_name = get_app_name(text)
qt_bin = get_qt_from_cli()
qt_plug = get_qt_plug_from_cli()
if qt_bin == "":
qt_bin = get_qt_path()
if qt_plug == "":
if platform.system() == "Linux":
qt_plug = get_qt_plugin_path()
else:
qt_plug = "not-needed"
maker = get_maker(qt_bin)
if qt_bin != "":
if qt_bin != "" and qt_plug != "":
print("app_target = " + app_target)
print("app_version = " + app_ver)
print("app_name = " + app_name)
print("qt_bin = " + qt_bin)
print("qt_plugins = " + qt_plug)
print("maker = " + maker + "\n")
if maker == "":
@ -264,7 +295,7 @@ def main():
info_file.write(app_name + "\n")
if platform.system() == "Linux":
linux_build_app_dir(app_ver, app_name, app_target, qt_bin)
linux_build_app_dir(app_ver, app_name, app_target, qt_bin, qt_plug)
elif platform.system() == "Windows":
windows_build_app_dir(app_ver, app_name, app_target, qt_bin)
@ -274,4 +305,4 @@ def main():
print(" output from platform.system() = " + platform.system())
if __name__ == "__main__":
main()
main()

View File

@ -47,7 +47,7 @@ notes:
* When the session detects that the module successfully established a pipe connection, it will send a [HOST_VER](type_ids.md) frame to the module so the module can decide if it supports the host. If the host is not compatible or the module fails for what ever other reason, the module can send a useful error message [ERR](type_ids.md) and then terminate gracefully. The error message will be added to the host debug log where it can be used by host admins to find out what went wrong. The HOST_VER frame is sent only when the module is called with the -public_cmds, -exempt_cmds or -user_cmds parameter.
* When the module sends a [NEW_CMD](type_ids.md) frame, the 16bit command id is needed but does not need to be valid, it just needs to be there as a place holder. The session will auto fill a valid command id before sending the data to the client. A valid NEW_CMD frame must have a minimum of 259 bytes and a valid command name. the session will ignore all NEW_CMD frames the doesn't meet these requirements. See section [6.3](shared_data.md) for what would be considered a valid command name.
* When the module sends a [NEW_CMD](type_ids.md) frame, the 16bit command id is needed but does not need to be valid, it just needs to be there as a place holder. The session will auto fill a valid command id before sending the data to the client. A valid NEW_CMD frame must have a minimum of 259 bytes and a valid command name. the session will ignore all NEW_CMD frames the doesn't meet these requirements. See section [6.4](shared_data.md) for what would be considered a valid command name.
* When a session starts, it will call all modules with the -public_cmds and it doesn't matter if the command names returned to the session overlap with -exempt_cmd or -user_cmds. When a user is logged in, it will then call 2 instances of each module with the -exempt_cmds and -user_cmds parameters so the command names should not overlap when these parameters are active.
@ -57,4 +57,4 @@ notes:
### 2.4 Module Standard Output/Error ###
The host captures all text written to standard out/err. Although modules can send text data to clients via the [TEXT](type_ids.md) frame, another way to do the same thing is to write to stdout. The host however treats stderr differently, it sends all text written to stderr to the host logging system. On Linux systems, syslog is used. On windows systems, a local log file in %PROGRAMDATA%\mrci\messages.log is used. When logging messages, the host will send a generic error message to the client but the full error details will be sent to the logs along with the a generated message id and the module executable.
The host captures all text written to standard out/err. Although modules can send text data to clients via the [TEXT](type_ids.md) frame, another way to do the same thing is to write to stdout. The host however treats stderr differently, it sends all text written to stderr to the host logging system. On Linux systems, syslog is used. On windows systems, a local log file in %PROGRAMDATA%\mrci\messages.log is used. When logging messages, the host will send a generic error message to the client but the full error details will be sent to the logs along with the a generated message id and the module executable.

View File

@ -42,7 +42,7 @@ notes:
| Data | Type | Vaild Size Range | Forbidden Chars | Must Contain |
| ---------------------- | ------ | ---------------- | ------------------ | ----------------------------------------------------------- |
| Module Executable Path | String | 1-512 | ```|*:\"?<>``` | |
| Module Executable Path | String | 1-512 | ```\|*:"?<>``` | |
| User Name | String | 2-24 | spaces or newlines | |
| Email Address | String | 4-64 | spaces or newlines | ```@``` and ```.``` |
| Password | String | 8-200 | | special chars, numbers, capital letters and common letters. |

View File

@ -80,7 +80,7 @@
#include "mem_share.h"
#define APP_NAME "MRCI"
#define APP_VER "5.1.2.1"
#define APP_VER "5.2.2.1"
#define APP_TARGET "mrci"
#define SERVER_HEADER_TAG "MRCI"
#define HOST_CONTROL_PIPE "MRCI_HOST_CONTROL"
@ -111,8 +111,8 @@
#define DEFAULT_DB_DRIVER "QSQLITE"
#define DEFAULT_DB_FILENAME "data.db"
#define DEFAULT_LOG_FILENAME "messages.log"
#define DEFAULT_CERT_FILENAME "tls_chain.pem"
#define DEFAULT_PRIV_FILENAME "tls_priv.pem"
#define DEFAULT_CERT_FILENAME "tls_chain.crt"
#define DEFAULT_PRIV_FILENAME "tls_priv.key"
#define DEFAULT_RES_PW_FILENAME "res_pw_template.txt"
#define DEFAULT_EVERIFY_FILENAME "email_verify_template.txt"
#define DEFAULT_CONFIRM_SUBJECT "Email Verification"

View File

@ -186,8 +186,6 @@ int main(int argc, char *argv[])
QCoreApplication::setApplicationName(APP_NAME);
QCoreApplication::setApplicationVersion(APP_VER);
// args.append("-ls_sql_drvs"); // debug
if (args.contains("-run_cmd", Qt::CaseInsensitive) ||
args.contains("-public_cmds", Qt::CaseInsensitive) ||
args.contains("-exempt_cmds", Qt::CaseInsensitive) ||

View File

@ -16,217 +16,65 @@
// along with MRCI_Client under the LICENSE.md file. If not, see
// <http://www.gnu.org/licenses/>.
Cert::Cert(QObject *parent) : QObject(parent)
{
pKey = EVP_PKEY_new();
x509 = X509_new();
bne = BN_new();
rsa = RSA_new();
}
void Cert::cleanup()
{
EVP_PKEY_free(pKey);
X509_free(x509);
BN_free(bne);
}
bool genRSAKey(Cert *cert, QTextStream &msg)
{
bool ret = false;
if (cert->pKey && cert->bne && cert->rsa)
{
if (BN_set_word(cert->bne, RSA_F4))
{
if (RSA_generate_key_ex(cert->rsa, 2048, cert->bne, NULL))
{
if (EVP_PKEY_assign_RSA(cert->pKey, cert->rsa))
{
ret = true;
}
else
{
msg << "Failed to assign the generated RSA key to a PKEY object." << Qt::endl;
}
}
else
{
msg << "Failed to generate the RSA private key." << Qt::endl;
}
}
else
{
msg << "Failed to initialize a BIGNUM object needed to generate the RSA key." << Qt::endl;
}
}
else
{
msg << "The x509 object did not initialize correctly." << Qt::endl;
}
return ret;
}
bool genX509(Cert *cert, const QString &outsideAddr, QTextStream &msg)
{
auto ret = false;
auto interfaces = QNetworkInterface::allAddresses();
QList<QByteArray> cnNames;
if (!outsideAddr.isEmpty())
{
msg << "x509 gen_wan_ip: " << outsideAddr << Qt::endl;
cnNames.append(outsideAddr.toUtf8());
}
for (auto&& addr : interfaces)
{
if (addr.isGlobal())
{
msg << "x509 gen_lan_ip: " << addr.toString() << Qt::endl;
cnNames.append(addr.toString().toUtf8());
}
}
if (cert->x509 && cert->pKey && !cnNames.isEmpty())
{
ASN1_INTEGER_set(X509_get_serialNumber(cert->x509), QDateTime::currentDateTime().toSecsSinceEpoch());
X509_gmtime_adj(X509_get_notBefore(cert->x509), 0); // now
X509_gmtime_adj(X509_get_notAfter(cert->x509), 31536000L); // 365 days
X509_set_pubkey(cert->x509, cert->pKey);
// copy the subject name to the issuer name.
auto *name = X509_get_subject_name(cert->x509);
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *) cnNames[0].data(), -1, -1, 0);
X509_set_issuer_name(cert->x509, name);
cnNames.removeAt(0);
QByteArray sanField;
for (int i = 0; i < cnNames.size(); ++i)
{
sanField.append("DNS:" + cnNames[i]);
if (i != cnNames.size() - 1)
{
sanField.append(", ");
}
}
if (!sanField.isEmpty())
{
addExt(cert->x509, NID_subject_alt_name, sanField.data());
}
if (X509_sign(cert->x509, cert->pKey, EVP_sha1()))
{
ret = true;
}
else
{
msg << "Failed to self-sign the generated x509 cert." << Qt::endl;
}
}
else
{
msg << "No usable IP addresses could be found to be used as common names in the self-signed cert." << Qt::endl;
}
return ret;
}
void addExt(X509 *cert, int nid, char *value)
{
X509_EXTENSION *ext = X509V3_EXT_conf_nid(NULL, NULL, nid, value);
if (ext != NULL)
{
X509_add_ext(cert, ext, -1);
X509_EXTENSION_free(ext);
}
}
FILE *openFileForWrite(const char *path, QTextStream &msg)
{
auto file = fopen(path, "wb");
if (!file)
{
msg << "Cannot open file: '" << path << "' for writing. " << strerror(errno);
}
return file;
}
void encodeErr(const char *path, QTextStream &msg)
{
msg << "Failed to encode file '" << path << "' to PEM format." << Qt::endl;
}
bool writePrivateKey(const char *path, Cert* cert, QTextStream &msg)
{
auto ret = false;
FILE *file = openFileForWrite(path, msg);
if (file)
{
if (PEM_write_PrivateKey(file, cert->pKey, NULL, NULL, 0, NULL, NULL))
{
ret = true;
}
else
{
encodeErr(path, msg);
}
}
fclose(file);
return ret;
}
bool writeX509(const char *path, Cert *cert, QTextStream &msg)
{
auto ret = false;
FILE *file = openFileForWrite(path, msg);
if (file)
{
if (PEM_write_X509(file, cert->x509))
{
ret = true;
}
else
{
encodeErr(path, msg);
}
}
fclose(file);
return ret;
}
bool genDefaultSSLFiles(const QString &outsideAddr, QTextStream &msg)
{
auto *cert = new Cert();
auto ret = genRSAKey(cert, msg);
QProcess proc;
QStringList args;
if (ret) ret = genX509(cert, outsideAddr, msg);
if (ret) ret = writePrivateKey(DEFAULT_PRIV_FILENAME, cert, msg);
if (ret) ret = writeX509(DEFAULT_CERT_FILENAME, cert, msg);
args << "req";
args << "-newkey" << "rsa:4096";
args << "-x509";
args << "-sha256";
args << "-days" << "365";
args << "-nodes";
args << "-out" << getLocalFilePath(DEFAULT_CERT_FILENAME);
args << "-keyout" << getLocalFilePath(DEFAULT_PRIV_FILENAME);
args << "-subj" << "/CN=" + outsideAddr;
cert->cleanup();
cert->deleteLater();
proc.setProgram("openssl");
proc.setArguments(args);
proc.start();
msg << "openssl args: " << args.join(' ') << Qt::endl;
auto ret = false;
if (!proc.waitForStarted())
{
msg << "err: openssl failed to start, reason: " << proc.errorString() << Qt::endl;
}
else
{
proc.waitForFinished();
msg << "--openssl-output--" << Qt::endl;
auto outText = proc.readAllStandardOutput();
auto errText = proc.readAllStandardError();
msg << "std-out:" << Qt::endl;
for (auto line : outText.split('\n'))
{
msg << line << Qt::endl;
}
msg << "std-err:" << Qt::endl;
for (auto line: errText.split('\n'))
{
msg << line << Qt::endl;
}
if (proc.exitCode() != 0)
{
msg << "err: openssl failed with return code: " << proc.exitCode() << Qt::endl;
}
else
{
ret = true;
}
}
return ret;
}

View File

@ -17,46 +17,8 @@
// along with MRCI_Client under the LICENSE.md file. If not, see
// <http://www.gnu.org/licenses/>.
#include <cstdio>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <QDateTime>
#include <QCoreApplication>
#include <QIODevice>
#include <QFile>
#include <QSslCertificate>
#include <QList>
#include <QNetworkInterface>
#include <QHostAddress>
#include <QSslKey>
#include "common.h"
class Cert : public QObject
{
Q_OBJECT
public:
EVP_PKEY *pKey;
X509 *x509;
BIGNUM *bne;
RSA *rsa;
void cleanup();
explicit Cert(QObject *parent = nullptr);
};
FILE *openFileForWrite(const char *path, QTextStream &msg);
bool genRSAKey(Cert *cert, QTextStream &msg);
bool genX509(Cert *cert, const QString &outsideAddr, QTextStream &msg);
bool writePrivateKey(const char *path, Cert *cert, QTextStream &msg);
bool writeX509(const char *path, Cert *cert, QTextStream &msg);
bool genDefaultSSLFiles(const QString &outsideAddr, QTextStream &msg);
void addExt(X509 *cert, int nid, char *value);
void encodeErr(const char *path, QTextStream &msg);
bool genDefaultSSLFiles(const QString &outsideAddr, QTextStream &msg);
#endif // MAKE_CERT_H

View File

@ -26,6 +26,8 @@ TCPServer::TCPServer(QObject *parent) : QTcpServer(parent)
controlSocket = nullptr;
flags = 0;
controlPipe->setSocketOptions(QLocalServer::OtherAccessOption);
#ifdef Q_OS_LINUX
setupUnixSignalHandlers();

View File

@ -55,15 +55,23 @@ UnixSignalHandler::UnixSignalHandler(QObject *parent) : QObject(parent)
void UnixSignalHandler::hupSignalHandler(int)
{
char chr = 1;
auto sz = static_cast<uint>(sizeof(chr));
write(sighupFd[0], &chr, sizeof(chr));
if (write(sighupFd[0], &chr, sz) != sz)
{
QTextStream(stderr) << "err: Failed write out HUP signal in the signal handler." << Qt::endl;
}
}
void UnixSignalHandler::termSignalHandler(int)
{
char chr = 1;
auto sz = static_cast<uint>(sizeof(chr));
write(sigtermFd[0], &chr, sizeof(chr));
if (write(sigtermFd[0], &chr, sz) != sz)
{
QTextStream(stderr) << "err: Failed write out TERM signal in the signal handler." << Qt::endl;
}
}
void UnixSignalHandler::handleSigTerm()
@ -71,8 +79,12 @@ void UnixSignalHandler::handleSigTerm()
snTerm->setEnabled(false);
char chr;
auto sz = static_cast<uint>(sizeof(chr));
read(sigtermFd[1], &chr, sizeof(chr));
if (read(sigtermFd[1], &chr, sz) != sz)
{
QTextStream(stderr) << "wrn: Failed to read TERM signal data, however this should not affect signal handler operation." << Qt::endl;
}
emit closeServer();
@ -84,8 +96,12 @@ void UnixSignalHandler::handleSigHup()
snHup->setEnabled(false);
char chr;
auto sz = static_cast<uint>(sizeof(chr));
read(sighupFd[1], &chr, sizeof(chr));
if (read(sighupFd[1], &chr, sz) != sz)
{
QTextStream(stderr) << "wrn: Failed to read HUP signal data, however this should not affect signal handler operation." << Qt::endl;
}
emit closeServer();

View File

@ -22,6 +22,7 @@
#ifdef Q_OS_LINUX
#include <QObject>
#include <QTextStream>
#include <QSocketNotifier>
#include <sys/socket.h>
#include <sys/types.h>