From 48b4c5b53764877830279496cefb7014a6d2e44e Mon Sep 17 00:00:00 2001 From: Maurice ONeal Date: Tue, 21 Apr 2020 12:04:36 -0400 Subject: [PATCH] Major change to the build system and minor changes to SSL handling - mutiple SSL cert files can now be added to the MRCI_PUB_KEY env variable via colon seperated ':' path strings to complete the cert chain if such a thing is required. It is no longer necessary to merge to multiple certs into one to complete a cert chain. - added -load_ssl command line option so cert data can be re-loaded in real time without the need to stop-start the host. - added more detailed error messages to the SSL loading process for easier debugging. - major changes to the build system include the use of python scripts instead of the linux shell script file. - linux_build.sh was removed since it is no longer needed. - the new build process now run 2 python scripts: build.py and then install.py. - the resulting installer if built no longer uses makeself. the installation and/or self extracting process is now handled entirely by python and the install.py script. The main reason for this change is to lay the ground work for multi- platform support. It is still linux only for now but adding windows support will be much easier in the future thanks to python's cross- platform support. --- .gitignore | 2 + MRCI.pro | 5 +- build.py | 186 ++++++++++++++++++++++++ docs/README.md | 50 ++++--- install.py | 315 +++++++++++++++++++++++++++++++++++++++++ linux_build.sh | 147 ------------------- src/commands/admin.cpp | 3 +- src/common.cpp | 41 ++++++ src/common.h | 3 + src/db.cpp | 15 +- src/db.h | 7 +- src/main.cpp | 13 +- src/make_cert.cpp | 87 ++++++++++-- src/make_cert.h | 17 ++- src/session.cpp | 11 +- src/session.h | 4 +- src/tcp_server.cpp | 153 +++++++++++++++++++- src/tcp_server.h | 12 +- 18 files changed, 843 insertions(+), 228 deletions(-) create mode 100644 build.py create mode 100644 install.py delete mode 100644 linux_build.sh diff --git a/.gitignore b/.gitignore index da45b4b..b658088 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ ui_*.h *.jsc Makefile* *build-* +/build +/app_dir # Qt unit tests target_wrapper.* diff --git a/MRCI.pro b/MRCI.pro index 842e626..1ed15e7 100644 --- a/MRCI.pro +++ b/MRCI.pro @@ -27,7 +27,10 @@ QT += sql CONFIG += console CONFIG -= app_bundle -TARGET = mrci +TARGET = build/mrci +OBJECTS_DIR = build +MOC_DIR = build +RCC_DIR = build win32 { diff --git a/build.py b/build.py new file mode 100644 index 0000000..c4223e5 --- /dev/null +++ b/build.py @@ -0,0 +1,186 @@ +#!/bin/python3 + +import os +import re +import subprocess +import shutil +import platform +import sys + +def get_app_target(text): + return re.search(r'(APP_TARGET) +(\"(.*?)\")', text).group(3) + +def get_app_ver(text): + return re.search(r'(APP_VER) +(\"(.*?)\")', text).group(3) + +def get_app_name(text): + return re.search(r'(APP_NAME) +(\"(.*?)\")', text).group(3) + +def get_qt_path(): + try: + return str(subprocess.check_output(["qtpaths", "--binaries-dir"]), 'utf-8').strip() + + except CalledProcessError: + print("A call to 'qtpaths' to get the QT installation bin folder failed.") + + return raw_input("Please enter the QT bin path (leave blank to cancel the build): ") + +def get_qt_from_cli(): + for arg in sys.argv: + if arg == "-qt_dir": + index = sys.argv.index(arg) + + try: + return sys.argv[index + 1] + + except: + return "" + + return "" + +def get_db_header(): + current_dir = os.path.dirname(__file__) + + if current_dir == "": + return "src" + os.sep + "db.h" + else: + return current_dir + os.sep + "src" + os.sep + "db.h" + +def cd(): + current_dir = os.path.dirname(__file__) + + if current_dir != "": + os.chdir(current_dir) + +def verbose_copy(src, dst): + print("cpy: " + src + " --> " + dst) + + if os.path.isdir(src): + if os.path.exists(dst) and os.path.isdir(dst): + shutil.rmtree(dst) + + shutil.copytree(src, dst) + + else: + shutil.copyfile(src, dst) + +def linux_build_app_dir(app_ver, app_name, app_target, qt_bin): + 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("build/" + app_target, "app_dir/linux/" + app_target) + + shutil.copyfile("build/" + app_target, "/tmp/" + app_target) + # copying the executable file from the build folder to + # temp bypasses any -noexe retrictions a linux file + # system may have. there is a chance temp is also + # restricted in this way but that kind of config is + # rare. ldd will not run correctly with -noexe + # enabled. + + lines = str(subprocess.check_output(["ldd", "/tmp/" + app_target]), 'utf-8').split("\n") + + os.remove("/tmp/" + app_target) + + for line in lines: + if " => " in line: + if ("libQt" in line) or ("libicu" in line) or ("libssl" in line) or ("libcrypto" in line): + if " (0x0" in line: + start_index = line.index("> ") + 2 + end_index = line.index(" (0x0") + src_file = line[start_index:end_index] + file_name = os.path.basename(src_file) + + verbose_copy(src_file, "app_dir/linux/lib/" + file_name) + + with open("app_dir/linux/" + app_target + ".sh", "w") as file: + file.write("#!/bin/sh\n") + file.write("export QTDIR=$install_dir\n") + file.write("export QT_PLUGIN_PATH=$install_dir\n") + file.write("export LD_LIBRARY_PATH=\"$install_dir/lib:\$LD_LIBRARY_PATH\"\n") + file.write("$install_dir/" + app_target + " $1 $2 $3\n") + + with open("app_dir/linux/" + app_target + ".service", "w") as file: + file.write("[Unit]\n") + file.write("Description=" + app_name + " Host Daemon\n") + file.write("After=network.target\n\n") + file.write("[Service]\n") + file.write("Type=simple\n") + file.write("User=" + app_target + "\n") + file.write("Restart=on-failure\n") + file.write("RestartSec=5\n") + file.write("TimeoutStopSec=infinity\n") + file.write("ExecStart=/usr/bin/env " + app_target + " -host\n") + file.write("ExecStop=/usr/bin/env " + app_target + " -stop\n\n") + file.write("[Install]\n") + file.write("WantedBy=multi-user.target\n") + + with open("app_dir/linux/uninstall.sh", "w") as file: + file.write("#!/bin/sh\n") + file.write("systemctl -q stop " + app_target + "\n") + file.write("systemctl -q disable " + app_target + "\n") + file.write("rm -v /etc/systemd/system/" + app_target + ".service\n") + file.write("rm -v /usr/bin/" + app_target + "\n") + file.write("rm -rv $install_dir\n") + file.write("deluser " + app_target + "\n") + + complete(app_ver) + +def windows_build_app_dir(): + print("Windows support is work in progress. Check for an update at a later time.") + # to do: fill out code for windows support here. + +def complete(app_ver): + print("Build complete for version: " + app_ver) + print("You can now run the install.py script to install onto this machine or create an installer.") + +def main(): + with open(get_db_header()) as file: + text = file.read() + + app_target = get_app_target(text) + app_ver = get_app_ver(text) + app_name = get_app_name(text) + qt_bin = get_qt_from_cli() + + if qt_bin is "": + qt_bin = get_qt_path() + + if qt_bin != "": + print("app_target = " + app_target) + print("app_version = " + app_ver) + print("app_name = " + app_name) + print("qt_bin = " + qt_bin) + + cd() + + result = subprocess.run([qt_bin + os.sep + "qmake", "-config", "release"]) + + if result.returncode == 0: + result = subprocess.run(["make"]) + + if result.returncode == 0: + if not os.path.exists("app_dir"): + os.makedirs("app_dir") + + with open("app_dir" + os.sep + "info.txt", "w") as info_file: + info_file.write(app_target + "\n") + info_file.write(app_ver + "\n") + info_file.write(app_name + "\n") + + if platform.system() == "Linux": + linux_build_app_dir(app_ver, app_name, app_target, qt_bin) + + elif platform.system() == "Windows": + windows_build_app_dir() + + else: + print("The platform you are running in is not compatible with the app_dir build out procedure.") + print(" output from platform.system() = " + platform.system()) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index be54ba5..b2517b5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,6 +5,8 @@ ### Usage ### ``` +Usage: mrci + -help : display usage information about this application. @@ -19,8 +21,7 @@ -exempt_cmds : run the internal module to list it's rank exempt commands. for internal use only. -user_cmds : run the internal module to list it's user commands. for internal use only. -run_cmd : run an internal module command. for internal use only. - -add_cert : add/update an SSL certificate for a given common name. - -rm_cert : remove an SSL certificate for a given common name. + -load_ssl : re-load the host SSL certificate without stopping the host instance. Internal module | -public_cmds, -user_cmds, -exempt_cmds, -run_cmd |: @@ -44,7 +45,7 @@ Any one user account registered with the host can be given root privileges which ### More Than Just a Command Interpreter ### -Typical use for a MRCI host is to run commands that clients ask it to run, very similar to what you see in terminal emulators. It however does have a few feasures typically not seen in local terminals: +Typical use for a MRCI host is to run commands on a remote host that clients ask it to run, very similar to what you see in remote terminal emulators. It however does have a few feasures typically not seen in terminals: * Broadcast any type of data to all peers connected to the host. * Run remote commands on connected peers. @@ -72,38 +73,41 @@ Because the host is modular, the things you can customize it to do is almost lim * [6.1 Shared Memory](shared_data.md) * [7.1 Internal Commands](intern_commands.md) -### Development Setup ### +### Build Setup ### -Linux Required Packages: +For Linux you need the following packages to successfully build/install: ``` qtbase5-dev libssl-dev gcc make -makeself +python3 ``` -### Build From Source (Linux) ### - -Linux_build.sh is a custom script designed to build this project from the source code using qmake, make and makeself. You can pass 2 optional arguments: - -1. The path to the QT bin folder in case you want to compile with a QT install not defined in PATH. -2. Path of the output makeself file (usually has a .run extension). If not given, the outfile will be named mrci-x.x.x.run in the source code folder. - -Build: +Windows support is still work in progress but the following applications will must likely need to be installed: ``` -cd /path/to/source/code -sh ./linux_build.sh -``` -Install: -``` -chmod +x ./mrci-x.x.x.run -./mrci-x.x.x.run +OpenSSL +Qt5.12 or newer +Python3 ``` -The makeself installer not only installs the application but also installs it as a service if the target linux system supports systemd. +### Build ### -Start/Stop the service: +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: + +***local machine*** - This option will install the built application onto the local machine without creating an installer. + +***create installer*** - This option creates an installer that can be distributed to other machines to installation. The resulting installer is just a regular .py script file that the target machine can run if it has Python3 insalled. Only Python3 needs to be installed and an internet connection is not required. + +***exit*** - Cancel the installation. + +-local or -installer can be passed as command line options for install.py to explicitly select one of the above options without pausing for user input. + +### Services ### + +If a target linux system supports systemd, the application will be installed as a background daemon that can start/stop with the following commands: ``` sudo systemctl start mrci sudo systemctl stop mrci diff --git a/install.py b/install.py new file mode 100644 index 0000000..a95b45e --- /dev/null +++ b/install.py @@ -0,0 +1,315 @@ +#!/bin/python3 + +import os +import subprocess +import shutil +import platform +import sys +import zipfile +import binascii +import tempfile + +def cd(): + current_dir = os.path.dirname(__file__) + + if current_dir != "": + os.chdir(current_dir) + +def get_default_install_dir(app_target, app_name): + if platform.system() == "Linux": + return "/opt/" + app_target + + else: + return "C:\\Program Files\\" + app_name + +def get_default_installer_path(app_ver, app_name): + return os.path.expanduser("~") + os.sep + app_name + "-" + app_ver + ".py" + +def get_install_dir(app_target, app_name): + path = get_default_install_dir(app_target, app_name) + + print("The default install directory is: " + path) + + while(True): + ans = input("Do you want to change it? (y/n): ") + + if ans == "y" or ans == "Y": + path = input("Enter a new install directory (leave blank to go back to the default): ") + path = os.path.normpath(path) + break + + elif ans == "n" or ans == "N": + break + + if path == "": + return get_default_install_dir(app_target, app_name) + + else: + return path + +def get_installer_path(app_ver, app_name): + path = get_default_installer_path(app_ver, app_name) + + print("The built .py installer will placed here: " + path) + + while(True): + ans = input("Do you want to change the path? (y/n): ") + + if ans == "y" or ans == "Y": + path = input("Enter a new path (leave blank to go back to the default): ") + path = os.path.normpath(path) + break + + elif ans == "n" or ans == "N": + break + + if path == "": + return get_default_installer_path(app_ver, app_name) + + else: + return path + +def make_install_dir(path): + try: + if not os.path.exists(path): + os.makedirs(path) + + except: + print("Failed to create the install directory, please make sure you are runnning this script with admin rights.") + +def replace_text(text, old_text, new_text, offs): + while(True): + try: + index = text.index(old_text, offs) + text = text[:index] + new_text + text[index + len(old_text):] + + except ValueError: + break + + return text + +def sub_copy_file(src, dst, old_text, new_text, offs): + print("cpy: " + src + " --> " + dst) + + text = "" + + with open(src, "r") as rd_file: + text = rd_file.read() + text = replace_text(text, old_text, new_text, offs) + + with open(dst, "w") as wr_file: + wr_file.write(text) + +def verbose_copy(src, dst): + print("cpy: " + src + " --> " + dst) + + if os.path.isdir(src): + if os.path.exists(dst) and os.path.isdir(dst): + shutil.rmtree(dst) + + shutil.copytree(src, dst) + + else: + shutil.copyfile(src, dst) + +def verbose_create_symmlink(src, dst): + print("lnk: " + src + " --> " + dst) + + if os.path.exists(dst): + os.remove(dst) + + os.symlink(src, dst) + +def local_install(app_target, app_name): + if platform.system() == "Linux": + if not os.path.exists("app_dir/linux"): + print("An app_dir for the Linux platform could not be found.") + + else: + install_dir = get_install_dir(app_target, app_name) + + if os.path.exists(install_dir + "/uninstall.sh"): + subprocess.run([install_dir + "/uninstall.sh"]) + + make_install_dir(install_dir) + + if not os.path.exists("/var/opt/" + app_target): + os.makedirs("/var/opt/" + app_target) + + sub_copy_file("app_dir/linux/" + app_target + ".sh", install_dir + "/" + app_target + ".sh", "$install_dir", install_dir, 0) + sub_copy_file("app_dir/linux/uninstall.sh", install_dir + "/uninstall.sh", "$install_dir", install_dir, 0) + + verbose_copy("app_dir/linux/" + app_target, install_dir + "/" + app_target) + verbose_copy("app_dir/linux/lib", install_dir + "/lib") + verbose_copy("app_dir/linux/sqldrivers", install_dir + "/sqldrivers") + verbose_copy("app_dir/linux/" + app_target + ".service", "/etc/systemd/system/" + app_target + ".service") + + verbose_create_symmlink(install_dir + "/" + app_target + ".sh", "/usr/bin/" + app_target) + + subprocess.run(["useradd", "-r", app_target]) + subprocess.run(["chmod", "-R", "755", install_dir]) + subprocess.run(["chmod", "755", "/etc/systemd/system/" + app_target + ".service"]) + subprocess.run(["chown", "-R", app_target + ":" + app_target, "/var/opt/" + app_target]) + subprocess.run(["systemctl", "start", app_target]) + subprocess.run(["systemctl", "enable", app_target]) + + print("Installation finished. If you ever need to uninstall this application, run this command with root rights:") + print(" sh " + install_dir + "/uninstall.sh\n") + + elif platform.system() == "Windows": + print("Windows support is work progress. Check for an update at a later time.") + # to do: fill ot code for windows support here. + + else: + print("The platform you are running in is not compatible.") + print(" output from platform.system() = " + platform.system()) + +def dir_tree(path): + ret = [] + + if os.path.isdir(path): + for entry in os.listdir(path): + full_path = os.path.join(path, entry) + + if os.path.isdir(full_path): + for sub_dir_file in dir_tree(full_path): + ret.append(sub_dir_file) + + else: + ret.append(full_path) + + return ret + +def to_hex(data): + return str(binascii.hexlify(data))[2:-1] + +def from_hex(text_line): + return binascii.unhexlify(text_line) + +def make_install(app_ver, app_name): + path = get_installer_path(app_ver, app_name) + + with zipfile.ZipFile("app_dir.zip", "w", compression=zipfile.ZIP_DEFLATED) as zip_file: + print("Compressing app_dir --") + + for file in dir_tree("app_dir"): + print("adding file: " + file) + zip_file.write(file) + + sub_copy_file(__file__, path, "main(is_sfx=False)", "main(is_sfx=True)\n\n\n", 7700) + + with open(path, "a") as dst_file, open("app_dir.zip", "rb") as src_file: + print("Packing the compressed app_dir into the sfx script file --") + + dst_file.write("# APP_DIR\n") + + stat = os.stat("app_dir.zip") + + while(True): + buffer = src_file.read(4000000) + + if len(buffer) != 0: + dst_file.write("# " + to_hex(buffer) + "\n") + + print(str(src_file.tell()) + "/" + str(stat.st_size)) + + if len(buffer) < 4000000: + break + + os.remove("app_dir.zip") + + print("Finished.") + +def sfx(): + abs_sfx_path = os.path.abspath(__file__) + mark_found = False + + os.chdir(tempfile.gettempdir()) + + with open(abs_sfx_path) as packed_file, open("app_dir.zip", "wb") as zip_file: + stat = os.stat(abs_sfx_path) + + print("Unpacking the app_dir compressed file from the sfx script.") + + while(True): + line = packed_file.readline() + + if not line: + break + + elif mark_found: + zip_file.write(from_hex(line[2:-1])) + + print(str(packed_file.tell()) + "/" + str(stat.st_size)) + + else: + if line == "# APP_DIR\n": + mark_found = True + + print("Done.") + + if not mark_found: + print("The app_dir mark was not found, unable to continue.") + + else: + with zipfile.ZipFile("app_dir.zip", "r", compression=zipfile.ZIP_DEFLATED) as zip_file: + print("De-compressing app_dir --") + + zip_file.extractall() + + print("Preparing for installation.") + + os.remove("app_dir.zip") + + with open("app_dir" + os.sep + "info.txt") as info_file: + list = info_file.read().split("\n") + + local_install(list[0], list[2]) + shutil.rmtree("app_dir") + +def main(is_sfx): + cd() + + app_target = "" + app_ver = "" + app_name = "" + + if not is_sfx: + with open("app_dir" + os.sep + "info.txt") as info_file: + list = info_file.read().split("\n") + + app_target = list[0] + app_ver = list[1] + app_name = list[2] + + if is_sfx: + sfx() + + elif "-local" in sys.argv: + local_install(app_target, app_name) + + elif "-installer" in sys.argv: + make_install(app_ver, app_name) + + else: + print("Do you want to install onto this machine or create an installer?") + print("[1] local machine") + print("[2] create installer") + print("[3] exit") + + while(True): + opt = input("select an option: ") + + if opt == "1": + local_install(app_target, app_name) + break + + elif opt == "2": + make_install(app_ver, app_name) + break + + elif opt == "3": + break + +if __name__ == "__main__": + main(is_sfx=False) \ No newline at end of file diff --git a/linux_build.sh b/linux_build.sh deleted file mode 100644 index 6ee18b7..0000000 --- a/linux_build.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/bin/sh - -qt_dir="$1" -installer_file="$2" - -src_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -bin_name="mrci" -app_version="3.0.0.0" -app_name="MRCI" -install_dir="/opt/$bin_name" -var_dir="/var/opt/$bin_name" -bin_dir="/usr/bin" -tmp_dir="$HOME/.cache/mrci_build" - -if [ ! -d "$qt_dir" ]; then - - echo "a valid path to Qt was not provided, falling back to the default: /usr/lib/x86_64-linux-gnu/qt5/bin" - - qt_dir="/usr/lib/x86_64-linux-gnu/qt5/bin" - -else - - PATH=$qt_dir:$PATH - -fi - -if [ "$installer_file" = "" ]; then - - installer_file="$src_dir/$bin_name-$app_version.run" - -fi - -if [ -d "$tmp_dir" ]; then - - rm -rf $tmp_dir - -fi - -if [ $? -eq 0 -a -d "$qt_dir" ]; then - - mkdir -vp $tmp_dir - cp -r $src_dir/. $tmp_dir - cd $tmp_dir - qmake -config release - - if [ $? -eq 0 ]; then - - make - - if [ $? -eq 0 ]; then - - mkdir -v ./build/ - mkdir -v ./build/sqldrivers - mkdir -v ./build/lib - ldd ./$bin_name | grep "libQt" | awk '{print $3}' | xargs -I '{}' cp -v '{}' ./build/lib - ldd ./$bin_name | grep "libicu" | awk '{print $3}' | xargs -I '{}' cp -v '{}' ./build/lib - ldd ./$bin_name | grep "libssl" | awk '{print $3}' | xargs -I '{}' cp -v '{}' ./build/lib - ldd ./$bin_name | grep "libcrypto" | awk '{print $3}' | xargs -I '{}' cp -v '{}' ./build/lib - mv -v ./$bin_name ./build/$bin_name - cp -v $qt_dir/../plugins/sqldrivers/libqsqlite.so ./build/sqldrivers - - startup_script="./build/$bin_name.sh" - setup_script="./build/setup.sh" - uninstall_script="./build/uninstall.sh" - service_file="./build/$bin_name.service" - - echo "#!/bin/sh" > $startup_script - echo "export QTDIR=$install_dir" >> $startup_script - echo "export QT_PLUGIN_PATH=$install_dir" >> $startup_script - echo "export LD_LIBRARY_PATH=\"$install_dir/lib:\$LD_LIBRARY_PATH\"" >> $startup_script - echo "export MRCI_DB_PATH=$var_dir/data.db" >> $startup_script - echo "$install_dir/$bin_name \$1 \$2 \$3" >> $startup_script - - echo "#!/bin/sh" > $setup_script - echo "if [ -f \"$install_dir/uninstall.sh\" ]; then" >> $setup_script - echo " sh $install_dir/uninstall.sh" >> $setup_script - echo "fi" >> $setup_script - echo "if [ ! -d \"$install_dir\" ]; then" >> $setup_script - echo " sudo mkdir -p $install_dir" >> $setup_script - echo "fi" >> $setup_script - echo "if [ ! -d \"$var_dir\" ]; then" >> $setup_script - echo " sudo mkdir -p $var_dir" >> $setup_script - echo "fi" >> $setup_script - echo "cp -rfv ./lib $install_dir" >> $setup_script - echo "cp -rfv ./sqldrivers $install_dir" >> $setup_script - echo "cp -fv ./$bin_name $install_dir" >> $setup_script - echo "cp -fv ./$bin_name.sh $install_dir" >> $setup_script - echo "cp -fv ./uninstall.sh $install_dir" >> $setup_script - echo "cp -fv ./$bin_name.service /etc/systemd/system/$bin_name.service" >> $setup_script - echo "useradd -r $bin_name" >> $setup_script - echo "chmod 755 $install_dir/$bin_name" >> $setup_script - echo "chmod 755 $install_dir/$bin_name.sh" >> $setup_script - echo "chmod 755 $install_dir/uninstall.sh" >> $setup_script - echo "chmod 755 $install_dir" >> $setup_script - echo "chmod -R 755 $install_dir/lib" >> $setup_script - echo "chmod -R 755 $install_dir/sqldrivers" >> $setup_script - echo "chmod 755 /etc/systemd/system/$bin_name.service" >> $setup_script - echo "chown -R $bin_name:$bin_name $var_dir" >> $setup_script - echo "chmod -R 755 $var_dir" >> $setup_script - echo "ln -sf $install_dir/$bin_name.sh $bin_dir/$bin_name" >> $setup_script - echo "systemctl start $bin_name" >> $setup_script - echo "systemctl enable $bin_name" >> $setup_script - echo "if [ \$? -eq 0 ]; then" >> $setup_script - echo " echo \"\nInstallation finished. If you ever need to uninstall this application, run this command:\n\"" >> $setup_script - echo " echo \" $install_dir/uninstall.sh\n\"" >> $setup_script - echo "fi" >> $setup_script - - echo "[Unit]" > $service_file - echo "Description=$app_name Host Daemon" >> $service_file - echo "After=network.target" >> $service_file - echo "" >> $service_file - echo "[Service]" >> $service_file - echo "Type=simple" >> $service_file - echo "User=$bin_name" >> $service_file - echo "Restart=on-failure" >> $service_file - echo "RestartSec=5" >> $service_file - echo "TimeoutStopSec=infinity" >> $service_file - echo "ExecStart=/usr/bin/env $bin_name -host" >> $service_file - echo "ExecStop=/usr/bin/env $bin_name -stop" >> $service_file - echo "" >> $service_file - echo "[Install]" >> $service_file - echo "WantedBy=multi-user.target" >> $service_file - - echo "#!/bin/sh" > $uninstall_script - echo "systemctl -q stop $bin_name" >> $uninstall_script - echo "systemctl -q disable $bin_name" >> $uninstall_script - echo "rm -v /etc/systemd/system/$bin_name.service" >> $uninstall_script - echo "rm -v $bin_dir/$bin_name" >> $uninstall_script - echo "rm -rv $install_dir" >> $uninstall_script - echo "chown -R root:root $var_dir" >> $uninstall_script - echo "deluser $bin_name" >> $uninstall_script - - chmod +x $setup_script - - makeself ./build $installer_file "$app_name Installation" ./setup.sh - - fi - - fi - -fi - -if [ -d "$tmp_dir" ]; then - - rm -rf $tmp_dir - -fi diff --git a/src/commands/admin.cpp b/src/commands/admin.cpp index a9fb702..538c9eb 100644 --- a/src/commands/admin.cpp +++ b/src/commands/admin.cpp @@ -126,7 +126,8 @@ void ServSettings::printSettings() txtOut << "Maximum Sub-Channels: " << db.getData(COLUMN_MAX_SUB_CH).toUInt() << endl; txtOut << "Initial Host Rank: " << db.getData(COLUMN_INITRANK).toUInt() << endl; txtOut << "Root User: " << getUserName(rootUserId()) << endl; - txtOut << "Database Path: " << sqlDataPath() << endl; + txtOut << "Working Path: " << QDir::currentPath() << endl; + txtOut << "Database: " << sqlDataPath() << endl; txtOut << "Mailer Executable: " << db.getData(COLUMN_MAILERBIN).toString() << endl; txtOut << "Mailer Command: " << db.getData(COLUMN_MAIL_SEND).toString() << endl << endl; diff --git a/src/common.cpp b/src/common.cpp index 938c995..50a31ef 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -16,6 +16,47 @@ // along with MRCI under the LICENSE.md file. If not, see // . +QString sslCertChain() +{ + return expandEnvVariables(qEnvironmentVariable(ENV_PUB_KEY, DEFAULT_PUB_KEY_NAME)); +} + +QString sslPrivKey() +{ + return expandEnvVariables(qEnvironmentVariable(ENV_PRIV_KEY, DEFAULT_PRIV_KEY_NAME)); +} + +QByteArray rdFileContents(const QString &path, QTextStream &msg) +{ + QByteArray ret; + + msg << "Reading file contents: '" << path << "' "; + + QFile file(path); + + if (file.open(QFile::ReadOnly)) + { + ret = file.readAll(); + + if (!ret.isEmpty()) + { + msg << "[pass]" << endl; + } + else + { + msg << "[fail] (0 bytes of data was read from the file, is it empty?)" << endl; + } + } + else + { + msg << "[fail] (" << file.errorString() << ")" << endl; + } + + file.close(); + + return ret; +} + QString boolStr(bool state) { QString ret; diff --git a/src/common.h b/src/common.h index 0c83250..b56b35b 100644 --- a/src/common.h +++ b/src/common.h @@ -234,6 +234,7 @@ class Session; QByteArray toTEXT(const QString &txt); QByteArray fixedToTEXT(const QString &txt, int len); QByteArray nullTermTEXT(const QString &txt); +QByteArray rdFileContents(const QString &path, QTextStream &msg); quint32 toCmdId32(quint16 cmdId, quint16 branchId); quint16 toCmdId16(quint32 id); void serializeThread(QThread *thr); @@ -286,6 +287,8 @@ QString getParam(const QString &key, const QStringList &args); QString escapeChars(const QString &str, const QChar &escapeChr, const QChar &chr); QString genSerialNumber(); QString defaultPw(); +QString sslCertChain(); +QString sslPrivKey(); QStringList parseArgs(const QByteArray &data, int maxArgs, int *pos = nullptr); //--------------------------- diff --git a/src/db.cpp b/src/db.cpp index f7af096..d9c3a1b 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -78,16 +78,7 @@ QByteArray genUniqueHash() QString sqlDataPath() { - QString ret = qEnvironmentVariable(ENV_DB_PATH, DEFAULT_DB_PATH); - - ret = expandEnvVariables(ret); - - QFileInfo info(ret); - QDir dir(info.path()); - - if (!dir.exists()) dir.mkpath(info.path()); - - return ret; + return expandEnvVariables(qEnvironmentVariable(ENV_DB_PATH, DEFAULT_DB_FILE)); } QList genSequence(int min, int max, int len) @@ -395,9 +386,9 @@ QString Query::errDetail() txtOut << " driver error: " << errTxt << endl; txtOut << " query: " << qStr << jStr << wStr << limit << endl; - txtOut << " db path: " << sqlDataPath() << endl; + txtOut << " database: " << sqlDataPath() << endl; - QFileInfo info = QFileInfo(QFileInfo(sqlDataPath()).path()); + auto info = QFileInfo(QFileInfo(sqlDataPath()).path()); if (!info.isReadable()) { diff --git a/src/db.h b/src/db.h index 2bedce7..cb846ad 100644 --- a/src/db.h +++ b/src/db.h @@ -37,21 +37,19 @@ #include "shell.h" #define APP_NAME "MRCI" -#define APP_VER "3.2.1.0" +#define APP_VER "3.3.1.0" #define APP_TARGET "mrci" #ifdef Q_OS_WIN #define DEFAULT_MAILBIN "%COMSPEC%" #define DEFAULT_MAIL_SEND "echo %message_body% | mutt -s %subject% %target_email%" -#define DEFAULT_DB_PATH "%PROGRAMDATA%\\mrci\\data.db" #define DEFAULT_WORK_DIR "%PROGRAMDATA%\\mrci" #else #define DEFAULT_MAILBIN "/bin/sh" #define DEFAULT_MAIL_SEND "-c \"echo %message_body% | mutt -s %subject% %target_email%\"" -#define DEFAULT_DB_PATH "/var/opt/mrci/data.db" #define DEFAULT_WORK_DIR "/var/opt/mrci" #endif @@ -67,6 +65,9 @@ #define TEMP_PW_SUB "%temp_pw%" #define USERNAME_SUB "%user_name%" #define DATE_SUB "%date%" +#define DEFAULT_PUB_KEY_NAME "cert.pem" +#define DEFAULT_PRIV_KEY_NAME "priv.pem" +#define DEFAULT_DB_FILE "data.db" #define DEFAULT_ROOT_USER "root" #define DEFAULT_CONFIRM_SUBJECT "Email Verification" #define DEFAULT_TEMP_PW_SUBJECT "Password Reset" diff --git a/src/main.cpp b/src/main.cpp index 5c07b75..49645e8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -59,8 +59,7 @@ void showHelp() txtOut << " -exempt_cmds : run the internal module to list it's rank exempt commands. for internal use only." << endl; txtOut << " -user_cmds : run the internal module to list it's user commands. for internal use only." << endl; txtOut << " -run_cmd : run an internal module command. for internal use only." << endl; - txtOut << " -add_cert : add/update an SSL certificate for a given common name." << endl; - txtOut << " -rm_cert : remove an SSL certificate for a given common name." << endl << endl; + txtOut << " -load_ssl : re-load the host SSL certificate without stopping the host instance." << endl << endl; txtOut << "Internal module | -public_cmds, -user_cmds, -exempt_cmds, -run_cmd |:" << endl << endl; txtOut << " -pipe : the named pipe used to establish a data connection with the session." << endl; txtOut << " -mem_ses : the shared memory key for the session." << endl; @@ -110,6 +109,10 @@ int main(int argc, char *argv[]) auto args = QCoreApplication::arguments(); auto ret = 0; + QDir dir(workDir); + + if (!dir.exists()) dir.mkpath(workDir); + QDir::setCurrent(workDir); QCoreApplication::setApplicationName(APP_NAME); QCoreApplication::setApplicationVersion(APP_VER); @@ -118,7 +121,7 @@ int main(int argc, char *argv[]) qInstallMessageHandler(msgHandler); - //args.append("-add_cert -name test"); // debug + //args.append("-host"); // debug if (args.contains("-help", Qt::CaseInsensitive) || args.size() == 1) { @@ -131,7 +134,9 @@ int main(int argc, char *argv[]) QTextStream(stdout) << "The program is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE" << endl; QTextStream(stdout) << "WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE." << endl << endl; } - else if (args.contains("-stop", Qt::CaseInsensitive) || args.contains("-status", Qt::CaseInsensitive)) + else if (args.contains("-stop", Qt::CaseInsensitive) || + args.contains("-status", Qt::CaseInsensitive) || + args.contains("-load_ssl", Qt::CaseInsensitive)) { ret = shellToHost(args, app); } diff --git a/src/make_cert.cpp b/src/make_cert.cpp index 4ae6134..1ac349b 100644 --- a/src/make_cert.cpp +++ b/src/make_cert.cpp @@ -31,7 +31,7 @@ void Cert::cleanup() BN_free(bne); } -bool genRSAKey(Cert *cert) +bool genRSAKey(Cert *cert, QTextStream &msg) { bool ret = false; @@ -45,14 +45,30 @@ bool genRSAKey(Cert *cert) { ret = true; } + else + { + msg << "Failed to assign the generated RSA key to a PKEY object." << endl; + } + } + else + { + msg << "Failed to generate the RSA private key." << endl; } } + else + { + msg << "Failed to initialize a BIGNUM object needed to generate the RSA key." << endl; + } + } + else + { + msg << "The x509 object did not initialize correctly." << endl; } return ret; } -bool genX509(Cert *cert, const QString &outsideAddr) +bool genX509(Cert *cert, const QString &outsideAddr, QTextStream &msg) { auto ret = false; auto interfaces = QNetworkInterface::allAddresses(); @@ -61,6 +77,8 @@ bool genX509(Cert *cert, const QString &outsideAddr) if (!outsideAddr.isEmpty()) { + msg << "x509 gen_wan_ip: " << outsideAddr << endl; + cnNames.append(outsideAddr.toUtf8()); } @@ -68,6 +86,8 @@ bool genX509(Cert *cert, const QString &outsideAddr) { if (addr.isGlobal()) { + msg << "x509 gen_lan_ip: " << addr.toString() << endl; + cnNames.append(addr.toString().toUtf8()); } } @@ -111,6 +131,14 @@ bool genX509(Cert *cert, const QString &outsideAddr) { ret = true; } + else + { + msg << "Failed to self-sign the generated x509 cert." << endl; + } + } + else + { + msg << "No usable IP addresses could be found to be used as common names in the self-signed cert." << endl; } return ret; @@ -127,14 +155,38 @@ void addExt(X509 *cert, int nid, char *value) } } -bool writePrivateKey(const char *path, Cert* cert) +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." << endl; +} + +bool writePrivateKey(const char *path, Cert* cert, QTextStream &msg) { auto ret = false; - auto *file = fopen(path, "wb"); + FILE *file = openFileForWrite(path, msg); if (file) { - ret = PEM_write_PrivateKey(file, cert->pKey, NULL, NULL, 0, NULL, NULL); + if (PEM_write_PrivateKey(file, cert->pKey, NULL, NULL, 0, NULL, NULL)) + { + ret = true; + } + else + { + encodeErr(path, msg); + } } fclose(file); @@ -142,14 +194,21 @@ bool writePrivateKey(const char *path, Cert* cert) return ret; } -bool writeX509(const char *path, Cert *cert) +bool writeX509(const char *path, Cert *cert, QTextStream &msg) { auto ret = false; - auto *file = fopen(path, "wb"); + FILE *file = openFileForWrite(path, msg); if (file) { - ret = PEM_write_X509(file, cert->x509); + if (PEM_write_X509(file, cert->x509)) + { + ret = true; + } + else + { + encodeErr(path, msg); + } } fclose(file); @@ -157,15 +216,17 @@ bool writeX509(const char *path, Cert *cert) return ret; } -void genDefaultSSLFiles(const QString &outsideAddr) +bool genDefaultSSLFiles(const QString &outsideAddr, QTextStream &msg) { auto *cert = new Cert(); + auto ret = genRSAKey(cert, msg); - genRSAKey(cert); - genX509(cert, outsideAddr); - writePrivateKey(DEFAULT_PRIV_KEY_NAME, cert); - writeX509(DEFAULT_PUB_KEY_NAME, cert); + if (ret) ret = genX509(cert, outsideAddr, msg); + if (ret) ret = writePrivateKey(DEFAULT_PRIV_KEY_NAME, cert, msg); + if (ret) ret = writeX509(DEFAULT_PUB_KEY_NAME, cert, msg); cert->cleanup(); cert->deleteLater(); + + return ret; } diff --git a/src/make_cert.h b/src/make_cert.h index 26ff1f4..6e5ff13 100644 --- a/src/make_cert.h +++ b/src/make_cert.h @@ -34,9 +34,6 @@ #include "db.h" -#define DEFAULT_PUB_KEY_NAME "cert.pem" -#define DEFAULT_PRIV_KEY_NAME "priv.pem" - class Cert : public QObject { Q_OBJECT @@ -53,11 +50,13 @@ public: explicit Cert(QObject *parent = nullptr); }; -bool genRSAKey(Cert *cert); -bool genX509(Cert *cert, const QString &outsideAddr); -bool writePrivateKey(const char *path, Cert *cert); -bool writeX509(const char *path, Cert *cert); -void genDefaultSSLFiles(const QString &outsideAddr); -void addExt(X509 *cert, int nid, char *value); +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); #endif // MAKE_CERT_H diff --git a/src/session.cpp b/src/session.cpp index 882554d..7b06eef 100644 --- a/src/session.cpp +++ b/src/session.cpp @@ -25,11 +25,13 @@ QByteArray wrFrame(quint32 cmdId, const QByteArray &data, uchar dType) return typeBa + cmdBa + sizeBa + data; } -Session::Session(const QString &hostKey, QSslSocket *tcp, QObject *parent) : MemShare(parent) +Session::Session(const QString &hostKey, QSslSocket *tcp, QSslKey *privKey, QList *chain, QObject *parent) : MemShare(parent) { currentDir = QDir::currentPath(); hostMemKey = hostKey; tcpSocket = tcp; + sslKey = privKey; + sslChain = chain; hookCmdId32 = 0; tcpFrameCmdId = 0; tcpPayloadSize = 0; @@ -374,11 +376,8 @@ void Session::dataFromClient() // likely have to do the same. a ASYNC_RDY async will not // get sent until the handshake is successful. - auto pubKey = expandEnvVariables(qEnvironmentVariable(ENV_PUB_KEY, DEFAULT_PUB_KEY_NAME)); - auto privKey = expandEnvVariables(qEnvironmentVariable(ENV_PRIV_KEY, DEFAULT_PRIV_KEY_NAME)); - - tcpSocket->setLocalCertificate(pubKey); - tcpSocket->setPrivateKey(privKey); + tcpSocket->setLocalCertificateChain(*sslChain); + tcpSocket->setPrivateKey(*sslKey); tcpSocket->write(servHeader); tcpSocket->startServerEncryption(); } diff --git a/src/session.h b/src/session.h index 3106ee1..2b173ce 100644 --- a/src/session.h +++ b/src/session.h @@ -31,6 +31,8 @@ class Session : public MemShare private: QSslSocket *tcpSocket; + QList *sslChain; + QSslKey *sslKey; QString currentDir; QHash modCmdNames; QHash > frameQueue; @@ -100,7 +102,7 @@ private slots: public: - explicit Session(const QString &hostKey, QSslSocket *tcp, QObject *parent = nullptr); + explicit Session(const QString &hostKey, QSslSocket *tcp, QSslKey *privKey, QList *chain, QObject *parent = nullptr); public slots: diff --git a/src/tcp_server.cpp b/src/tcp_server.cpp index 91fb277..67c2c16 100644 --- a/src/tcp_server.cpp +++ b/src/tcp_server.cpp @@ -83,7 +83,9 @@ bool TCPServer::createPipe() void TCPServer::replyFromIpify(QNetworkReply *reply) { - genDefaultSSLFiles(reply->readAll()); + wanIP = reply->readAll(); + + loadSSLData(false); reply->deleteLater(); } @@ -101,8 +103,6 @@ bool TCPServer::start() db.addColumn(COLUMN_MAXSESSIONS); db.exec(); - qNam->get(QNetworkRequest(QUrl("https://api.ipify.org"))); - maxSessions = db.getData(COLUMN_MAXSESSIONS).toUInt(); auto ret = false; @@ -126,6 +126,8 @@ bool TCPServer::start() } else { + qNam->get(QNetworkRequest(QUrl("https://api.ipify.org"))); + ret = true; flags |= ACCEPTING; } @@ -217,6 +219,10 @@ void TCPServer::procPipeIn() controlSocket->write(toTEXT("\n")); } + else if (args.contains("-load_ssl", Qt::CaseInsensitive)) + { + controlSocket->write(toTEXT(loadSSLData(true))); + } else if (args.contains("-status", Qt::CaseInsensitive)) { QString text; @@ -237,7 +243,10 @@ void TCPServer::procPipeIn() txtOut << "Active Port: " << serverPort() << endl; txtOut << "Set Address: " << db.getData(COLUMN_IPADDR).toString() << endl; txtOut << "Set Port: " << db.getData(COLUMN_PORT).toUInt() << endl; - txtOut << "Database Path: " << sqlDataPath() << endl << endl; + txtOut << "Working Path: " << QDir::currentPath() << endl; + txtOut << "Database: " << sqlDataPath() << endl; + txtOut << "SSL Chain: " << sslCertChain() << endl; + txtOut << "SSL Private: " << sslPrivKey() << endl << endl; hostSharedMem->unlock(); controlSocket->write(toTEXT(text)); @@ -277,7 +286,7 @@ void TCPServer::incomingConnection(qintptr socketDescriptor) soc->setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption, buffSize); soc->setSocketOption(QAbstractSocket::SendBufferSizeSocketOption, buffSize); - auto *ses = new Session(hostKey, soc, nullptr); + auto *ses = new Session(hostKey, soc, &sslKey, &sslChain, nullptr); auto *thr = new QThread(nullptr); connect(thr, &QThread::finished, soc, &QSslSocket::deleteLater); @@ -308,3 +317,137 @@ void TCPServer::incomingConnection(qintptr socketDescriptor) hostSharedMem->unlock(); } } + +void TCPServer::applyPrivKey(const QString &path, QTextStream &msg) +{ + auto bytes = rdFileContents(path, msg); + + if (!bytes.isEmpty()) + { + msg << "Attempting to load the private key with RSA. "; + + QSslKey key(bytes, QSsl::Rsa); + + if (key.isNull()) + { + msg << "[fail]" << endl; + msg << "Attempting to load the private key with DSA. "; + + key = QSslKey(bytes, QSsl::Dsa); + } + + if (key.isNull()) + { + msg << "[fail]" << endl; + msg << "Attempting to load the private key with Elliptic Curve. "; + + key = QSslKey(bytes, QSsl::Ec); + } + + if (key.isNull()) + { + msg << "[fail]" << endl; + msg << "Attempting to load the private key with Diffie-Hellman. "; + + key = QSslKey(bytes, QSsl::Dh); + } + + if (key.isNull()) + { + msg << "[fail]" << endl; + msg << "Attempting to load the private key as a black box. "; + + key = QSslKey(bytes, QSsl::Opaque); + } + + if (key.isNull()) + { + msg << "[fail]" << endl << endl; + } + else + { + msg << "[pass]" << endl << endl; + + sslKey = key; + } + } +} + +void TCPServer::applyCerts(const QStringList &list, QTextStream &msg) +{ + sslChain.clear(); + + for (auto file : list) + { + sslChain.append(QSslCertificate(rdFileContents(file, msg))); + } +} + +QString TCPServer::loadSSLData(bool onReload) +{ + QString txtMsg; + QTextStream stream(&txtMsg); + + auto chain = sslCertChain().split(":"); + auto priv = sslPrivKey(); + auto allCertsExists = true; + auto privKeyExists = QFile::exists(priv); + + stream << "Private key: " << priv << endl; + + if (!privKeyExists) + { + stream << " ^(the private key does not exists)" << endl; + } + + for (auto cert : chain) + { + stream << "Cert: " << cert << endl; + + if (!QFile::exists(cert)) + { + stream << " ^(this cert does not exists)" << endl; + + allCertsExists = false; + } + } + + if (chain.isEmpty()) + { + stream << "No cert files are defined in the env." << endl; + + allCertsExists = false; + } + + stream << endl; + + if (allCertsExists && privKeyExists) + { + if (onReload && (priv == DEFAULT_PRIV_KEY_NAME) && (sslCertChain() == DEFAULT_PUB_KEY_NAME)) + { + stream << "Re-generating self-signed cert." << endl; + + if (genDefaultSSLFiles(wanIP, stream)) + { + stream << endl << "complete." << endl << endl; + } + } + + applyPrivKey(priv, stream); + applyCerts(chain, stream); + } + else if ((priv == DEFAULT_PRIV_KEY_NAME) && (sslCertChain() == DEFAULT_PUB_KEY_NAME)) + { + stream << "Generating self-signed cert." << endl; + + if (genDefaultSSLFiles(wanIP, stream)) + { + stream << endl << "The default self-signed cert files are generated successfully." << endl << endl; + + applyPrivKey(priv, stream); + applyCerts(chain, stream); + } + } + + return txtMsg; +} diff --git a/src/tcp_server.h b/src/tcp_server.h index 2ebee6e..25f94cf 100644 --- a/src/tcp_server.h +++ b/src/tcp_server.h @@ -35,14 +35,20 @@ private: QLocalServer *controlPipe; QLocalSocket *controlSocket; char *hostLoad; + QList sslChain; + QSslKey sslKey; QString controlPipePath; QString hostKey; + QString wanIP; quint32 maxSessions; quint32 flags; - bool servOverloaded(); - bool createPipe(); - void incomingConnection(qintptr socketDescriptor); + QString loadSSLData(bool onReload); + bool servOverloaded(); + bool createPipe(); + void applyPrivKey(const QString &path, QTextStream &msg); + void applyCerts(const QStringList &list, QTextStream &msg); + void incomingConnection(qintptr socketDescriptor); private slots: