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.
This commit is contained in:
Maurice ONeal 2020-04-21 12:04:36 -04:00
parent 80d493ad16
commit 48b4c5b537
18 changed files with 843 additions and 228 deletions

2
.gitignore vendored
View File

@ -28,6 +28,8 @@ ui_*.h
*.jsc
Makefile*
*build-*
/build
/app_dir
# Qt unit tests
target_wrapper.*

View File

@ -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 {

186
build.py Normal file
View File

@ -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()

View File

@ -5,6 +5,8 @@
### Usage ###
```
Usage: mrci <argument>
<Arguments>
-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

315
install.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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;

View File

@ -16,6 +16,47 @@
// along with MRCI under the LICENSE.md file. If not, see
// <http://www.gnu.org/licenses/>.
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;

View File

@ -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);
//---------------------------

View File

@ -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<int> 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())
{

View File

@ -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"

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
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

View File

@ -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<QSslCertificate> *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();
}

View File

@ -31,6 +31,8 @@ class Session : public MemShare
private:
QSslSocket *tcpSocket;
QList<QSslCertificate> *sslChain;
QSslKey *sslKey;
QString currentDir;
QHash<QString, QStringList> modCmdNames;
QHash<quint32, QList<QByteArray> > 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<QSslCertificate> *chain, QObject *parent = nullptr);
public slots:

View File

@ -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;
}

View File

@ -35,13 +35,19 @@ private:
QLocalServer *controlPipe;
QLocalSocket *controlSocket;
char *hostLoad;
QList<QSslCertificate> sslChain;
QSslKey sslKey;
QString controlPipePath;
QString hostKey;
QString wanIP;
quint32 maxSessions;
quint32 flags;
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: