Qv2ray/src/main.cpp

535 lines
21 KiB
C++

#include "common/CommandArgs.hpp"
#include "common/QvHelpers.hpp"
#include "common/QvTranslator.hpp"
#include "core/handler/ConfigHandler.hpp"
#include "core/settings/SettingsBackend.hpp"
#include "ui/w_MainWindow.hpp"
#include <QApplication>
#include <QFileInfo>
#include <QLocale>
#include <QObject>
#include <QStandardPaths>
#include <QStyle>
#include <QStyleFactory>
#include <QTranslator>
#include <csignal>
#include <singleapplication.h>
#ifdef Q_OS_UNIX
// For unix root user check
#include "unistd.h"
#endif
void signalHandler(int signum)
{
cout << "Qv2ray: Interrupt signal (" << signum << ") received." << endl;
qApp->exit(-99);
}
bool verifyConfigAvaliability(QString path, bool checkExistingConfig)
{
// Does not exist.
if (!QDir(path).exists())
return false;
// A temp file used to test file permissions in that folder.
QFile testFile(path + ".qv2ray_file_write_test_file" + QSTRN(QTime::currentTime().msecsSinceStartOfDay()));
bool opened = testFile.open(QFile::OpenModeFlag::ReadWrite);
if (!opened)
{
LOG(MODULE_SETTINGS, "Directory at: " + path + " cannot be used as a valid config file path.")
LOG(MODULE_INIT, "---> Cannot create a new file or openwrite a file.")
return false;
}
else
{
testFile.write("Qv2ray test file, feel free to remove.");
testFile.flush();
testFile.close();
bool removed = testFile.remove();
if (!removed)
{
// This is rare, as we can create a file but failed to remove it.
LOG(MODULE_SETTINGS, "Directory at: " + path + " cannot be used as a valid config file path.")
LOG(MODULE_INIT, "---> Cannot remove a file.")
return false;
}
}
if (checkExistingConfig)
{
// Check if an existing config is found.
QFile configFile(path + "Qv2ray.conf");
// No such config file.
if (!configFile.exists())
return false;
bool opened2 = configFile.open(QIODevice::ReadWrite);
try
{
if (opened2)
{
// Verify if the config can be loaded.
// Failed to parse if we changed the file structure...
// Original:
// -- auto conf =
// StructFromJsonString<Qv2rayConfig>(configFile.readAll());
//
// Verify JSON file format. (only) because this file version may
// not be upgraded and may contain unsupported structure.
auto err = VerifyJsonString(StringFromFile(&configFile));
if (!err.isEmpty())
{
LOG(MODULE_INIT, "Json parse returns: " + err)
return false;
}
else
{
// If the file format is valid.
auto conf = JsonFromString(StringFromFile(&configFile));
LOG(MODULE_SETTINGS,
"Path: " + path + " contains a config file, in version " + conf["config_version"].toVariant().toString())
configFile.close();
return true;
}
}
else
{
LOG(MODULE_SETTINGS, "File: " + configFile.fileName() + " cannot be opened!")
return false;
}
}
catch (...)
{
LOG(MODULE_SETTINGS, "Exception raised when checking config: " + configFile.fileName())
// LOG(INIT, e->what())
QvMessageBoxWarn(nullptr, QObject::tr("Warning"),
QObject::tr("Qv2ray cannot load the config file from here:") + NEWLINE + configFile.fileName());
return false;
}
}
else
return true;
}
bool initialiseQv2ray()
{
LOG(MODULE_INIT, "Application exec path: " + QApplication::applicationDirPath())
const QString currentPathConfig = QApplication::applicationDirPath() + "/config" QV2RAY_CONFIG_DIR_SUFFIX;
const QString configQv2ray = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/qv2ray" QV2RAY_CONFIG_DIR_SUFFIX;
const QString homeQv2ray = QDir::homePath() + "/.qv2ray" QV2RAY_CONFIG_DIR_SUFFIX;
//
//
// Some built-in search paths for Qv2ray to find configs. (load the first
// one if possible).
//
QStringList configFilePaths;
configFilePaths << currentPathConfig;
#ifdef WITH_FLATHUB_CONFIG_PATH
// AppConfigLocation uses 'Q'v2ray instead of `q`v2ray. Keep here as
// backward compatibility.
configFilePaths << QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + QV2RAY_CONFIG_DIR_SUFFIX;
#endif
configFilePaths << configQv2ray;
configFilePaths << homeQv2ray;
//
QString configPath = "";
bool hasExistingConfig = false;
for (auto path : configFilePaths)
{
// Verify the config path, check if the config file exists and in the
// correct JSON format. True means we check for config existance as
// well. --|HERE |
bool isValidConfigPath = verifyConfigAvaliability(path, true);
// If we already found a valid config file. just simply load it...
if (hasExistingConfig)
break;
if (isValidConfigPath)
{
DEBUG(MODULE_INIT, "Path: " + path + " is valid.")
configPath = path;
hasExistingConfig = true;
}
else
{
LOG(MODULE_INIT, "Path: " + path + " does not contain a valid config file.")
}
}
// If there's no existing config.
if (hasExistingConfig)
{
// Use the config path found by the checks above
SetConfigDirPath(configPath);
LOG(MODULE_INIT, "Using " + QV2RAY_CONFIG_DIR + " as the config path.")
}
else
{
//
// Create new config at these dirs, these are default values for each
// platform.
#ifdef Q_OS_WIN
configPath = currentPathConfig;
#else
configPath = configQv2ray;
#endif
bool mkpathResult = QDir().mkpath(configPath);
// Check if the dirs are write-able
if (mkpathResult && verifyConfigAvaliability(configPath, false))
{
// Found a valid config dir, with write permission, but assume no
// config is located in it.
LOG(MODULE_INIT, "Set " + configPath + " as the config path.")
SetConfigDirPath(configPath);
if (QFile::exists(QV2RAY_CONFIG_FILE))
{
// As we already tried to load config from every possible dir.
// This condition branch (!hasExistingConfig check) holds the
// fact that current config dir, should NOT contain any valid
// file (at least in the same name) It usually means that
// QV2RAY_CONFIG_FILE here is corrupted, in JSON format.
// Otherwise Qv2ray would have loaded this config already
// instead of notifying to create a new config in this folder.
LOG(MODULE_INIT, "This should not occur: Qv2ray config exists but failed to load.")
QvMessageBoxWarn(nullptr, QObject::tr("Failed to initialise Qv2ray"),
QObject::tr("Failed to determine the location of config file.") + NEWLINE +
QObject::tr("Qv2ray will now exit.") + NEWLINE + QObject::tr("Please report if you think it's a bug."));
return false;
}
Qv2rayConfig conf;
conf.kernelConfig.KernelPath(QString(QV2RAY_DEFAULT_VASSETS_PATH));
conf.kernelConfig.AssetsPath(QString(QV2RAY_DEFAULT_VCORE_PATH));
conf.logLevel = 3;
//
// Save initial config.
SaveGlobalConfig(conf);
LOG(MODULE_INIT, "Created initial config file.")
}
else
{
// None of the path above can be used as a dir for storing config.
// Even the last folder failed to pass the check.
LOG(MODULE_INIT, "FATAL")
LOG(MODULE_INIT, " ---> CANNOT find a proper place to store Qv2ray config files.")
QString searchPath = configFilePaths.join(NEWLINE);
QvMessageBoxWarn(nullptr, QObject::tr("Cannot Start Qv2ray"),
QObject::tr("Cannot find a place to store config files.") + NEWLINE +
QObject::tr("Qv2ray has searched these paths below:") + NEWLINE + NEWLINE + searchPath + NEWLINE +
QObject::tr("Qv2ray will now exit."));
return false;
}
}
if (!QDir(QV2RAY_GENERATED_DIR).exists())
{
// The dir used to generate final config file, for V2ray interaction.
QDir().mkdir(QV2RAY_GENERATED_DIR);
LOG(MODULE_INIT, "Created config generation dir at: " + QV2RAY_GENERATED_DIR)
}
return true;
}
int main(int argc, char *argv[])
{
#ifndef Q_OS_WIN
// Register signal handlers.
signal(SIGINT, signalHandler);
signal(SIGHUP, signalHandler);
signal(SIGKILL, signalHandler);
signal(SIGTERM, signalHandler);
#endif
//
// parse the command line before starting as a Qt application
{
std::unique_ptr<QCoreApplication> consoleApp(new QCoreApplication(argc, argv));
//
// Install a default translater. From the OS/DE
consoleApp->installTranslator(QvTranslator(QLocale::system().name()).pTranslator.get());
QvCommandArgParser parser;
QString errorMessage;
switch (parser.ParseCommandLine(&errorMessage))
{
case CommandLineOk: break;
case CommandLineError:
cout << errorMessage.toStdString() << endl;
cout << parser.Parser()->helpText().toStdString() << endl;
return 1;
case CommandLineVersionRequested:
LOG("Qv2ray", QV2RAY_VERSION_STRING)
LOG("QV2RAY_BUILD_INFO", QV2RAY_BUILD_INFO)
LOG("QV2RAY_BUILD_EXTRA_INFO", QV2RAY_BUILD_EXTRA_INFO)
return 0;
case CommandLineHelpRequested: cout << parser.Parser()->helpText().toStdString() << endl; return 0;
}
}
#ifdef Q_OS_UNIX
// Unix OS root user check.
// Do not use getuid() here since it's installed as owned by the root,
// someone may accidently setuid to it.
if (!StartupOption.forceRunAsRootUser && geteuid() == 0)
{
LOG("ERROR", QObject::tr("You cannot run Qv2ray as root, please use --I-just-wanna-run-with-root if you REALLY want to do so."))
LOG("ERROR", QObject::tr(" --> USE IT AT YOUR OWN RISK!"))
return 1;
}
#endif
//
// finished: command line parsing
LOG("QV2RAY_BUILD_INFO", QV2RAY_BUILD_INFO)
LOG("QV2RAY_BUILD_EXTRA_INFO", QV2RAY_BUILD_EXTRA_INFO)
LOG(MODULE_INIT,
"Qv2ray " QV2RAY_VERSION_STRING " running on " + QSysInfo::prettyProductName() + " " + QSysInfo::currentCpuArchitecture() + NEWLINE)
//
// This line must be called before any other ones, since we are using these
// values to identify instances.
SingleApplication::setApplicationName("Qv2ray");
SingleApplication::setApplicationVersion(QV2RAY_VERSION_STRING);
SingleApplication::setApplicationDisplayName("Qv2ray");
//
//
#ifdef QT_DEBUG
// ----------------------------> For debug build...
SingleApplication::setApplicationName("Qv2ray - DEBUG");
#endif
SingleApplication _qApp(
argc, argv, false, SingleApplication::Mode::User | SingleApplication::Mode::ExcludeAppPath | SingleApplication::Mode::ExcludeAppVersion);
_qApp.setQuitOnLastWindowClosed(false);
// Early initialisation
//
// Not duplicated.
// Install a default translater. From the OS/DE
auto _lang = QLocale::system().name();
Qv2rayTranslator = std::move(QvTranslator(_lang).pTranslator);
//
// Do not install en-US as it's the default language.
bool _result_ = _qApp.installTranslator(Qv2rayTranslator.get());
LOG(MODULE_UI, "Installing a tranlator from OS: " + _lang + " -- " + (_result_ ? "OK" : "Failed"))
//
LOG("LICENCE", NEWLINE
"This program comes with ABSOLUTELY NO WARRANTY." NEWLINE "This is free software, and you are welcome to redistribute it" NEWLINE
"under certain conditions." NEWLINE NEWLINE "Copyright (c) 2019-2020 Qv2ray Development Group." NEWLINE NEWLINE NEWLINE
"Libraries that have been used in Qv2ray are listed below (Sorted by date added):" NEWLINE
"Copyright (c) 2020 dridk (@dridk): X2Struct (Apache)" NEWLINE "Copyright (c) 2011 SCHUTZ Sacha (@dridk): QJsonModel (MIT)" NEWLINE
"Copyright (c) 2020 Nikolaos Ftylitakis (@ftylitak): QZXing (Apache2)" NEWLINE
"Copyright (c) 2016 Singein (@Singein): ScreenShot (MIT)" NEWLINE
"Copyright (c) 2016 Nikhil Marathe (@nikhilm): QHttpServer (MIT)" NEWLINE
"Copyright (c) 2020 Itay Grudev (@itay-grudev): SingleApplication (MIT)" NEWLINE
"Copyright (c) 2020 paceholder (@paceholder): nodeeditor (QNodeEditor modified by lhy0403) (BSD-3-Clause)" NEWLINE
"Copyright (c) 2019 TheWanderingCoel (@TheWanderingCoel): ShadowClash (launchatlogin) (GPLv3)" NEWLINE
"Copyright (c) 2020 Ram Pani (@DuckSoft): QvRPCBridge (WTFPL)" NEWLINE
"Copyright (c) 2019 ShadowSocks (@shadowsocks): libQtShadowsocks (LGPLv3)" NEWLINE
"Copyright (c) 2015-2020 qBittorrent (Anton Lashkov) (@qBittorrent): speedplotview (GPLv2)" NEWLINE NEWLINE)
//
LOG(MODULE_INIT, "Qv2ray Start Time: " + QSTRN(QTime::currentTime().msecsSinceStartOfDay()))
//
#ifdef QT_DEBUG
cout << "WARNING: ============================== This is a debug build, many features are not stable enough. =============================="
<< endl;
#endif
//
// Load the language translation list.
auto langs = GetFileList(QDir(":/translations"));
if (langs.empty())
{
LOG(MODULE_INIT, "FAILED to find any translations. THIS IS A BUILD ERROR.")
QvMessageBoxWarn(nullptr, QObject::tr("Cannot load languages"),
QObject::tr("Qv2ray will continue running, but you cannot change the UI language."));
}
else
{
for (auto lang : langs)
{
LOG(MODULE_INIT, "Found Translator: " + lang)
}
}
// Qv2ray Initialize, find possible config paths and verify them.
if (!initialiseQv2ray())
{
return -1;
}
// Load the config for upgrade, but do not parse it to the struct.
auto conf = JsonFromString(StringFromFile(QV2RAY_CONFIG_FILE));
auto confVersion = conf["config_version"].toVariant().toString().toInt();
if (confVersion > QV2RAY_CONFIG_VERSION)
{
// Config version is larger than the current version...
// This is rare but it may happen....
QvMessageBoxWarn(nullptr, QObject::tr("Qv2ray Cannot Continue"),
QObject::tr("You are running a lower version of Qv2ray compared to the current config file.") + NEWLINE +
QObject::tr("Please check if there's an issue explaining about it.") + NEWLINE +
QObject::tr("Or submit a new issue if you think this is an error.") + NEWLINE + NEWLINE +
QObject::tr("Qv2ray will now exit."));
return -2;
}
if (confVersion < QV2RAY_CONFIG_VERSION)
{
// That is, config file needs to be upgraded.
conf = Qv2ray::UpgradeSettingsVersion(confVersion, QV2RAY_CONFIG_VERSION, conf);
}
// Load config object from upgraded config QJsonObject
auto confObject = StructFromJsonString<Qv2rayConfig>(JsonToString(conf));
// Remove system translator, for loading custom translations.
qApp->removeTranslator(Qv2rayTranslator.get());
LOG(MODULE_INIT, "Removed system translations")
if (confObject.uiConfig.language.isEmpty())
{
// Prevent empty.
LOG(MODULE_UI, "Setting default UI language to en-US")
confObject.uiConfig.language = "en-US";
}
Qv2rayTranslator = std::move(QvTranslator(confObject.uiConfig.language).pTranslator);
if (qApp->installTranslator(Qv2rayTranslator.get()))
{
LOG(MODULE_INIT, "Successfully installed a translator for " + confObject.uiConfig.language)
}
else
{
// Do not translate these.....
// If a translator fails to load, pop up a message.
QvMessageBoxWarn(nullptr, "Translation Failed",
"Cannot load translation for " + confObject.uiConfig.language + ", English is now used." + NEWLINE + NEWLINE +
"Please go to Preferences Window to change language or open an Issue");
}
// Let's save the config.
SaveGlobalConfig(confObject);
//
// Check OpenSSL version for auto-update and subscriptions
auto osslReqVersion = QSslSocket::sslLibraryBuildVersionString();
auto osslCurVersion = QSslSocket::sslLibraryVersionString();
LOG(MODULE_NETWORK, "Current OpenSSL version: " + osslCurVersion)
if (!QSslSocket::supportsSsl())
{
LOG(MODULE_NETWORK, "Required OpenSSL version: " + osslReqVersion)
LOG(MODULE_NETWORK, "OpenSSL library MISSING, Quitting.")
QvMessageBoxWarn(nullptr, QObject::tr("Dependency Missing"),
QObject::tr("Cannot find openssl libs") + NEWLINE +
QObject::tr("This could be caused by a missing of `openssl` package in your system.") + NEWLINE +
QObject::tr("If you are using an AppImage from Github Action, please report a bug.") + NEWLINE + NEWLINE +
QObject::tr("Technical Details") + NEWLINE + "OSsl.Rq.V=" + osslReqVersion + NEWLINE +
"OSsl.Cr.V=" + osslCurVersion);
return -3;
}
#ifdef Q_OS_WIN
// Set special font in Windows
QFont font;
font.setPointSize(9);
font.setFamily("Microsoft YaHei");
_qApp.setFont(font);
#endif
#ifdef QV2RAY_USE_BUILTIN_DARKTHEME
LOG(MODULE_UI, "Using built-in theme.")
if (confObject.uiConfig.useDarkTheme)
{
LOG(MODULE_UI, " --> Using built-in dark theme.")
// From https://forum.qt.io/topic/101391/windows-10-dark-theme/4
_qApp.setStyle("Fusion");
QPalette darkPalette;
QColor darkColor = QColor(45, 45, 45);
QColor disabledColor = QColor(127, 127, 127);
// See Qv2rayBase.hpp MACRO --> BLACK(obj)
QColor defaultTextColor = QColor(210, 210, 210);
darkPalette.setColor(QPalette::Window, darkColor);
darkPalette.setColor(QPalette::WindowText, defaultTextColor);
darkPalette.setColor(QPalette::Disabled, QPalette::WindowText, disabledColor);
darkPalette.setColor(QPalette::Base, QColor(18, 18, 18));
darkPalette.setColor(QPalette::AlternateBase, darkColor);
darkPalette.setColor(QPalette::ToolTipBase, defaultTextColor);
darkPalette.setColor(QPalette::ToolTipText, defaultTextColor);
darkPalette.setColor(QPalette::Text, defaultTextColor);
darkPalette.setColor(QPalette::Disabled, QPalette::Text, disabledColor);
darkPalette.setColor(QPalette::Button, darkColor);
darkPalette.setColor(QPalette::ButtonText, defaultTextColor);
darkPalette.setColor(QPalette::Disabled, QPalette::ButtonText, disabledColor);
darkPalette.setColor(QPalette::BrightText, Qt::red);
darkPalette.setColor(QPalette::Link, QColor(42, 130, 218));
darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218));
darkPalette.setColor(QPalette::HighlightedText, Qt::black);
darkPalette.setColor(QPalette::Disabled, QPalette::HighlightedText, disabledColor);
qApp->setPalette(darkPalette);
qApp->setStyleSheet("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }");
}
#else
// Set custom themes.
QStringList themes = QStyleFactory::keys();
//_qApp.setDesktopFileName("qv2ray.desktop");
if (themes.contains(confObject.uiConfig.theme))
{
_qApp.setStyle(confObject.uiConfig.theme);
LOG(MODULE_INIT + " " + MODULE_UI, "Setting Qv2ray UI themes: " + confObject.uiConfig.theme)
}
#endif
#ifndef QT_DEBUG
try
{
#endif
//_qApp.setAttribute(Qt::AA_DontUseNativeMenuBar);
// Initialise Connection Handler
ConnectionManager = new QvConfigHandler();
// Show MainWindow
MainWindow w;
QObject::connect(&_qApp, &SingleApplication::instanceStarted, [&]() {
// When a second instance is connected, show the mainwindow.
w.show();
w.raise();
w.activateWindow();
});
// Handler for session logout, shutdown, etc.
// Will not block.
QGuiApplication::setFallbackSessionManagementEnabled(false);
QObject::connect(&_qApp, &QGuiApplication::commitDataRequest, []() { LOG(MODULE_INIT, "Quit triggered by session manager.") });
#ifndef Q_OS_WIN
signal(SIGUSR1, [](int) { emit MainWindow::mwInstance->StartConnection(); });
signal(SIGUSR2, [](int) { emit MainWindow::mwInstance->StopConnection(); });
#endif
auto rcode = _qApp.exec();
delete ConnectionManager;
LOG(MODULE_INIT, "Quitting normally")
return rcode;
#ifndef QT_DEBUG
}
catch (...)
{
QvMessageBoxWarn(nullptr, "ERROR", "There's something wrong happened and Qv2ray will quit now.");
LOG(MODULE_INIT, "EXCEPTION THROWN: " __FILE__)
return -99;
}
#endif
}