Files
amnezia-client/client/core/controllers/selfhosted/installController.cpp

1180 lines
46 KiB
C++
Raw Normal View History

refactor: refactor the application to the mvvm architecture (#2009) * refactor: move business logic from servers model * refactor: move containersModel initialization * refactor: added protocol ui controller and removed settings class from protocols model * refactor: moved cli management to separate controller * refactor: moved app split to separate controller * refactor: moved site split to separate controller * refactor: moved allowed dns to separate controller * refactor: moved language logic to separate ui controller * refactor: removed Settings from devices model * refactor: moved configs and services api logit to separate core controller * refactor: added a layer with a repository between the storage and controllers * refactor: use child parent system instead of smart pointers for controllers and models initialization * refactor: moved install functions from server controller to install controller * refactor: install controller refactoring * chore: renamed exportController to exportUiController * refactor: separate export controller * refactor: removed VpnConfigurationsController * chore: renamed ServerController to SshSession * refactor: replaced ServerController to SshSession * chore: moved qml controllers to separate folder * chore: include fixes * chore: moved utils from core root to core/utils * chore: include fixes * chore: rename core/utils files to camelCase foramt * chore: include fixes * chore: moved some utils to api and selfhosted folders * chore: include fixes * chore: remove unused file * chore: moved serialization folder to core/utils * chore: include fixes * chore: moved some files from client root to core/utils * chore: include fixes * chore: moved ui utils to ui/utils folder * chore: include fixes * chore: move utils from root to ui/utils * chore: include fixes * chore: moved configurators to core/configurators * chore: include fixes * refactor: moved iap logic from ui controller to core * refactor: moved remaining core logic from ApiConfigsController to SubscriptionController * chore: rename apiNewsController to apiNewsUiController * refactor: moved core logic from news ui controller to core * chore: renamed apiConfigsController to subscriptionUiController * chore: include fixes * refactor: merge ApiSettingsController with SubscriptionUiController * chore: moved ui selfhosted controllers to separate folder * chore: include fixes * chore: rename connectionController to connectiomUiController * refactor: moved core logic from connectionUiController * chore: rename settingsController to settingsUiController * refactor: move core logic from settingsUiController * refactor: moved core controller signal/slot connections to separate class * fix: newsController fixes after refactoring * chore: rename model to camelCase * chore: include fixes * chore: remove unused code * chore: move selfhosted core to separate folder * chore: include fixes * chore: rename importController to importUiController * refactor: move core logic from importUiController * chore: minor fixes * chore: remove prem v1 migration * refactor: remove openvpn over cloak and openvpn over shadowsocks * refactor: removed protocolsForContainer function * refactor: add core models * refactor: replace json with c++ structs for server config * refactor: move getDnsPair to ServerConfigUtils * feat: add admin selfhosted config export test * feat: add multi import test * refactor: use coreController for tests * feat: add few simple tests * chore: qrepos in all core controllers * feat: add test for settings * refactor: remove repo dependency from configurators * chore: moved protocols to core folder * chore: include fixes * refactor: moved containersDefs, defs, apiDefs, protocolsDefs to different places * chore: include fixes * chore: build fixes * chore: build fixes * refactor: remove q repo and interface repo * feat: add test for ui servers model and controller * chore: renamed to camelCase * chore: include fixes * refactor: moved core logic from sites ui controller * fix: fixed api config processing * fix: fixed processed server index processing * refactor: protocol models now use c++ structs instead of json configs * refactor: servers model now use c++ struct instead of json config * fix: fixed default server index processing * fix: fix logs init * fix: fix secure settings load keys * chore: build fixes * fix: fixed clear settings * fix: fixed restore backup * fix: sshSession usage * fix: fixed export functions signatures * fix: return missing part from buildContainerWorker * fix: fixed server description on page home * refactor: add container config helpers functions * refactor: c++ structs instead of json * chore: add dns protocol config struct * refactor: move config utils functions to config structs * feat: add test for selfhosted server setup * refactor: separate resources.qrc * fix: fixed server rename * chore: return nameOverriddenByUser * fix: build fixes * fix: fixed models init * refactor: cleanup models usage * fix: fixed models init * chore: cleanup connections and functions signatures * chore: cleanup updateModel calls * feat: added cache to servers repo * chore: cleanup unused functions * chore: ssxray processing * chore: remove transportProtoWithDefault and portWithDefault functions * chore: removed proto types any and l2tp * refactor: moved some constants * fix: fixed native configs export * refactor: remove json from processConfigWith functions * fix: fixed processed server index usage * fix: qml warning fixes * chore: merge fixes * chore: update tests * fix: fixed xray config processing * fix: fixed split tunneling processing * chore: rename sites controllers and model * chore: rename fixes * chore: minor fixes * chore: remove ability to load backup from "file with connection settings" button * fix: fixed api device revoke * fix: remove full model update when renaming a user * fix: fixed premium/free server rename * fix: fixed selfhosted new server install * fix: fixed updateContainer function * fix: fixed revoke for external premium configs * feat: add native configs qr processing * chore: codestyle fixes * fix: fixed admin config create * chore: again remove ability to load backup from "file with connection settings" button * chore: minor fixes * fix: fixed variables initialization * fix: fixed qml imports * fix: minor fixes * fix: fix vpnConnection function calls * feat: add buckup error handling * fix: fixed admin config revok * fix: fixed selfhosted awg installation * fix: ad visability * feat: add empty check for primary dns * chore: minor fixes
2026-04-30 14:53:03 +08:00
#include "installController.h"
#include "core/models/protocolConfig.h"
#include <QDebug>
#include <QEventLoop>
#include <QFutureWatcher>
#include <QRegularExpression>
#include <QThread>
#include <QtConcurrent>
#include "core/configurators/configuratorBase.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/installers/awgInstaller.h"
#include "core/installers/installerBase.h"
#include "core/installers/openvpnInstaller.h"
#include "core/installers/sftpInstaller.h"
#include "core/installers/socks5Installer.h"
#include "core/installers/torInstaller.h"
#include "core/installers/wireguardInstaller.h"
#include "core/installers/xrayInstaller.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/api/apiUtils.h"
#include "core/repositories/secureServersRepository.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/selfhosted/scriptsRegistry.h"
#include "core/utils/selfhosted/sshClient.h"
#include "logger.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/models/serverConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/awgProtocolConfig.h"
#include "ui/models/protocols/wireguardConfigModel.h"
#include "core/utils/utilities.h"
#include <QDesktopServices>
#include <QDir>
#include <QProcess>
#include <QStandardPaths>
#include <QUrl>
#include <QSysInfo>
#ifdef Q_OS_WINDOWS
#include <windows.h>
#endif
using namespace amnezia;
using namespace ProtocolUtils;
namespace
{
Logger logger("InstallController");
}
InstallController::InstallController(SecureServersRepository *serversRepository,
SecureAppSettingsRepository* appSettingsRepository,
QObject *parent)
: QObject(parent),
m_serversRepository(serversRepository),
m_appSettingsRepository(appSettingsRepository),
m_cancelInstallation(false)
{
}
InstallController::~InstallController()
{
stopAllSftpMounts();
}
ErrorCode InstallController::setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config,
bool isUpdate)
{
qDebug().noquote() << "InstallController::setupContainer" << ContainerUtils::containerToString(container);
SshSession sshSession(this);
ErrorCode e = ErrorCode::NoError;
e = isUserInSudo(credentials, sshSession);
if (e)
return e;
e = isServerDpkgBusy(credentials, sshSession);
if (e)
return e;
e = installDockerWorker(credentials, container, sshSession);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer installDockerWorker finished";
if (!isUpdate) {
e = isServerPortBusy(credentials, container, config, sshSession);
if (e)
return e;
}
e = prepareHostWorker(credentials, container, sshSession);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished";
sshSession.runScript(credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
amnezia::genBaseVars(credentials, container, QString(), QString())));
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
qDebug().noquote() << "buildContainerWorker start";
e = buildContainerWorker(credentials, container, config, sshSession);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer buildContainerWorker finished";
e = runContainerWorker(credentials, container, config, sshSession);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer runContainerWorker finished";
e = configureContainerWorker(credentials, container, config, sshSession);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer configureContainerWorker finished";
setupServerFirewall(credentials, sshSession);
qDebug().noquote() << "InstallController::setupContainer setupServerFirewall finished";
return startupContainerWorker(credentials, container, config, sshSession);
}
ErrorCode InstallController::updateContainer(int serverIndex, DockerContainer container, const ContainerConfig &oldConfig,
ContainerConfig &newConfig)
{
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
m_serversRepository->setContainerConfig(serverIndex, container, newConfig);
return ErrorCode::NoError;
}
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
SshSession sshSession(this);
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
qDebug() << "InstallController::updateContainer for container" << container << "reinstall required is" << reinstallRequired;
ErrorCode errorCode = ErrorCode::NoError;
if (reinstallRequired) {
errorCode = setupContainer(credentials, container, newConfig, true);
} else {
errorCode = configureContainerWorker(credentials, container, newConfig, sshSession);
if (errorCode == ErrorCode::NoError) {
errorCode = startupContainerWorker(credentials, container, newConfig, sshSession);
}
}
if (errorCode == ErrorCode::NoError) {
clearCachedProfile(serverIndex, container);
m_serversRepository->setContainerConfig(serverIndex, container, newConfig);
}
return errorCode;
}
void InstallController::clearCachedProfile(int serverIndex, DockerContainer container)
{
if (ContainerUtils::containerService(container) == ServiceType::Other) {
return;
}
ContainerConfig containerConfigModel = m_serversRepository->containerConfig(serverIndex, container);
m_serversRepository->clearLastConnectionConfig(serverIndex, container);
emit clientRevocationRequested(serverIndex, containerConfigModel, container);
}
ErrorCode InstallController::validateAndPrepareConfig(int serverIndex)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (serverConfigModel.isApiConfig()) {
return ErrorCode::NoError;
}
DockerContainer container = serverConfigModel.defaultContainer();
if (container == DockerContainer::None) {
return ErrorCode::NoInstalledContainersError;
}
ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, container);
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
SshSession sshSession;
auto isProtocolConfigExists = [](const ContainerConfig &cfg) {
return cfg.protocolConfig.hasClientConfig();
};
if (!isProtocolConfigExists(containerConfig)) {
QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverIndex, clientName);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
m_serversRepository->setContainerConfig(serverIndex, container, containerConfig);
}
return ErrorCode::NoError;
}
void InstallController::validateConfig(int serverIndex)
{
QFuture<ErrorCode> future = QtConcurrent::run([this, serverIndex]() {
return validateAndPrepareConfig(serverIndex);
});
auto *watcher = new QFutureWatcher<ErrorCode>(this);
connect(watcher, &QFutureWatcher<ErrorCode>::finished, this, [this, watcher]() {
ErrorCode errorCode = watcher->result();
watcher->deleteLater();
if (errorCode == ErrorCode::NoError) {
emit configValidated(true);
return;
}
emit validationErrorOccurred(errorCode);
emit configValidated(false);
});
watcher->setFuture(future);
}
ErrorCode InstallController::prepareContainerConfig(DockerContainer container, const ServerCredentials &credentials, ContainerConfig &containerConfig, SshSession &sshSession)
{
if (!ContainerUtils::isSupportedByCurrentPlatform(container)) {
return ErrorCode::NoError;
}
if (ContainerUtils::containerService(container) != ServiceType::Other) {
Proto protocol = ContainerUtils::defaultProtocol(container);
DnsSettings dnsSettings = {
m_appSettingsRepository->primaryDns(),
m_appSettingsRepository->secondaryDns()
};
auto configurator = ConfiguratorBase::create(protocol, &sshSession);
ErrorCode errorCode = ErrorCode::NoError;
ProtocolConfig newProtocolConfig = configurator->createConfig(credentials, container, containerConfig, dnsSettings, errorCode);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
containerConfig.protocolConfig = newProtocolConfig;
}
return ErrorCode::NoError;
}
void InstallController::adminAppendRequested(int serverIndex, DockerContainer container,
const ContainerConfig &containerConfig, const QString &clientName)
{
if (ContainerUtils::containerService(container) == ServiceType::Other
|| !containerConfig.protocolConfig.hasClientConfig()) {
return;
}
QString clientId = containerConfig.protocolConfig.clientId();
if (!clientId.isEmpty()) {
emit clientAppendRequested(serverIndex, clientId, clientName, container);
}
}
ErrorCode InstallController::processContainerForAdmin(DockerContainer container, ContainerConfig &containerConfig,
const ServerCredentials &credentials, SshSession &sshSession,
int serverIndex, const QString &clientName)
{
if (ContainerUtils::isSupportedByCurrentPlatform(container)) {
ErrorCode errorCode = prepareContainerConfig(container, credentials, containerConfig, sshSession);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
}
adminAppendRequested(serverIndex, container, containerConfig, clientName);
return ErrorCode::NoError;
}
ErrorCode InstallController::buildContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
{
amnezia::ScriptVars baseVars = amnezia::genBaseVars(credentials, container, QString(), QString());
QString dockerfilePath = "/opt/amnezia/" + ContainerUtils::containerToString(container) + "/Dockerfile";
QString removeScript = QString("sudo rm %1").arg(dockerfilePath);
ErrorCode errorCode = sshSession.runScript(credentials, sshSession.replaceVars(removeScript, baseVars));
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
errorCode = sshSession.uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, container).toUtf8(), dockerfilePath);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
auto cbReadStdErr = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
amnezia::ScriptVars protocolVars = amnezia::genProtocolVarsForContainer(container, config);
baseVars.append(protocolVars);
ErrorCode error = sshSession.runScript(
credentials, sshSession.replaceVars(amnezia::scriptData(SharedScriptType::build_container), baseVars), cbReadStdOut,
cbReadStdErr);
if (stdOut.contains("doesn't work on cgroups v2"))
return ErrorCode::ServerDockerOnCgroupsV2;
if (stdOut.contains("cgroup mountpoint does not exist"))
return ErrorCode::ServerCgroupMountpoint;
if (stdOut.contains("have reached") && stdOut.contains("pull rate limit"))
return ErrorCode::DockerPullRateLimit;
return error;
}
ErrorCode InstallController::runContainerWorker(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, SshSession &sshSession)
{
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
amnezia::ScriptVars baseVars = amnezia::genBaseVars(credentials, container, QString(), QString());
amnezia::ScriptVars protocolVars = amnezia::genProtocolVarsForContainer(container, config);
baseVars.append(protocolVars);
ErrorCode e = sshSession.runScript(
credentials, sshSession.replaceVars(amnezia::scriptData(ProtocolScriptType::run_container, container), baseVars),
cbReadStdOut);
if (stdOut.contains("address already in use"))
return ErrorCode::ServerPortAlreadyAllocatedError;
if (stdOut.contains("is already in use by container"))
return ErrorCode::ServerPortAlreadyAllocatedError;
if (stdOut.contains("invalid publish"))
return ErrorCode::ServerDockerFailedError;
return e;
}
ErrorCode InstallController::configureContainerWorker(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, SshSession &sshSession)
{
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
auto cbReadStdErr = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
amnezia::ScriptVars baseVars = amnezia::genBaseVars(credentials, container, QString(), QString());
amnezia::ScriptVars protocolVars = amnezia::genProtocolVarsForContainer(container, config);
baseVars.append(protocolVars);
ErrorCode e = sshSession.runContainerScript(
credentials, container,
sshSession.replaceVars(amnezia::scriptData(ProtocolScriptType::configure_container, container), baseVars),
cbReadStdOut, cbReadStdErr);
updateContainerConfigAfterInstallation(container, config, stdOut);
return e;
}
ErrorCode InstallController::startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
{
QString script = amnezia::scriptData(ProtocolScriptType::container_startup, container);
if (script.isEmpty()) {
return ErrorCode::NoError;
}
amnezia::ScriptVars baseVars = amnezia::genBaseVars(credentials, container, QString(), QString());
amnezia::ScriptVars protocolVars = amnezia::genProtocolVarsForContainer(container, config);
baseVars.append(protocolVars);
ErrorCode e = sshSession.uploadTextFileToContainer(container, credentials, sshSession.replaceVars(script, baseVars),
"/opt/amnezia/start.sh");
if (e)
return e;
return sshSession.runScript(
credentials,
sshSession.replaceVars("sudo docker exec -d $CONTAINER_NAME sh -c \"chmod a+x /opt/amnezia/start.sh && "
"/opt/amnezia/start.sh\"",
baseVars));
}
ErrorCode InstallController::isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
{
if (container == DockerContainer::Dns) {
return ErrorCode::NoError;
}
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
auto cbReadStdErr = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
const Proto protocol = ContainerUtils::defaultProtocol(container);
QStringList fixedPorts = ContainerUtils::fixedPortsForContainer(container);
QString port = config.protocolConfig.port();
if (port.isEmpty()) {
port = QString::number(ProtocolUtils::defaultPort(protocol));
}
QString transportProto = config.protocolConfig.transportProto();
if (transportProto.isEmpty()) {
transportProto = ProtocolUtils::transportProtoToString(ProtocolUtils::defaultTransportProto(protocol), protocol);
}
// TODO reimplement with netstat
QString script = QString("which lsof > /dev/null 2>&1 || true && sudo lsof -i -P -n 2>/dev/null | grep -E ':%1 ").arg(port);
for (auto &port : fixedPorts) {
script = script.append("|:%1").arg(port);
}
if (transportProto == "tcpandudp") {
QString tcpProtoScript = script;
QString udpProtoScript = script;
tcpProtoScript.append("' | grep -i tcp");
udpProtoScript.append("' | grep -i udp");
tcpProtoScript.append(" | grep LISTEN");
ErrorCode errorCode = sshSession.runScript(
credentials,
sshSession.replaceVars(tcpProtoScript, amnezia::genBaseVars(credentials, container, QString(), QString())),
cbReadStdOut, cbReadStdErr);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
errorCode = sshSession.runScript(
credentials,
sshSession.replaceVars(udpProtoScript, amnezia::genBaseVars(credentials, container, QString(), QString())),
cbReadStdOut, cbReadStdErr);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
if (!stdOut.isEmpty()) {
return ErrorCode::ServerPortAlreadyAllocatedError;
}
return ErrorCode::NoError;
}
script = script.append("' | grep -i %1").arg(transportProto);
if (transportProto == "tcp") {
script = script.append(" | grep LISTEN");
}
ErrorCode errorCode = sshSession.runScript(
credentials, sshSession.replaceVars(script, amnezia::genBaseVars(credentials, container, QString(), QString())),
cbReadStdOut, cbReadStdErr);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
if (!stdOut.isEmpty()) {
return ErrorCode::ServerPortAlreadyAllocatedError;
}
return ErrorCode::NoError;
}
bool InstallController::isReinstallContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig)
{
if (container == DockerContainer::OpenVpn) {
const auto* oldOvpnConfig = oldConfig.getOpenVpnProtocolConfig();
const auto* newOvpnConfig = newConfig.getOpenVpnProtocolConfig();
if (oldOvpnConfig && newOvpnConfig) {
if (!oldOvpnConfig->serverConfig.hasEqualServerSettings(newOvpnConfig->serverConfig)) {
return true;
}
}
}
if (ContainerUtils::isAwgContainer(container)) {
const auto* oldAwgConfig = oldConfig.getAwgProtocolConfig();
const auto* newAwgConfig = newConfig.getAwgProtocolConfig();
if (oldAwgConfig && newAwgConfig) {
if (!oldAwgConfig->serverConfig.hasEqualServerSettings(newAwgConfig->serverConfig)) {
return true;
}
}
}
if (container == DockerContainer::WireGuard) {
const auto* oldWgConfig = oldConfig.getWireGuardProtocolConfig();
const auto* newWgConfig = newConfig.getWireGuardProtocolConfig();
if (oldWgConfig && newWgConfig) {
if (!oldWgConfig->serverConfig.hasEqualServerSettings(newWgConfig->serverConfig)) {
return true;
}
}
}
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
const auto* oldXrayConfig = oldConfig.getXrayProtocolConfig();
const auto* newXrayConfig = newConfig.getXrayProtocolConfig();
if (oldXrayConfig && newXrayConfig) {
if (oldXrayConfig->serverConfig.port != newXrayConfig->serverConfig.port)
return true;
}
}
if (container == DockerContainer::Socks5Proxy) {
return true;
}
return false;
}
void InstallController::cancelInstallation()
{
m_cancelInstallation = true;
}
ErrorCode InstallController::installDockerWorker(const ServerCredentials &credentials, DockerContainer container, SshSession &sshSession)
{
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &client) {
stdOut += data + "\n";
if (data.contains("Automatically restart Docker daemon?")) {
return client.writeResponse("yes");
}
return ErrorCode::NoError;
};
auto cbReadStdErr = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
ErrorCode error = sshSession.runScript(
credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::install_docker),
amnezia::genBaseVars(credentials, DockerContainer::None, QString(), QString())),
cbReadStdOut, cbReadStdErr);
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
if (container == DockerContainer::Awg2) {
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = regex.match(stdOut);
if (match.hasMatch()) {
int majorVersion = match.captured(1).toInt();
int minorVersion = match.captured(2).toInt();
if (majorVersion < 4 || (majorVersion == 4 && minorVersion < 14)) {
return ErrorCode::ServerLinuxKernelTooOld;
}
}
}
if (stdOut.contains("lock"))
return ErrorCode::ServerPacketManagerError;
if (stdOut.contains("command not found"))
return ErrorCode::ServerDockerFailedError;
return error;
}
ErrorCode InstallController::prepareHostWorker(const ServerCredentials &credentials, DockerContainer container, SshSession &sshSession)
{
// create folder on host
return sshSession.runScript(credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::prepare_host),
amnezia::genBaseVars(credentials, container, QString(), QString())));
}
ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials, SshSession &sshSession)
{
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
auto cbReadStdErr = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
const QString scriptData = amnezia::scriptData(SharedScriptType::check_user_in_sudo);
ErrorCode error = sshSession.runScript(
credentials,
sshSession.replaceVars(scriptData, amnezia::genBaseVars(credentials, DockerContainer::None, QString(), QString())),
cbReadStdOut, cbReadStdErr);
if (credentials.userName != "root" && stdOut.contains("sudo:") && !stdOut.contains("uname:") && stdOut.contains("not found"))
return ErrorCode::ServerSudoPackageIsNotPreinstalled;
if (credentials.userName != "root" && !stdOut.contains("sudo") && !stdOut.contains("wheel"))
return ErrorCode::ServerUserNotInSudo;
if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory"))
return ErrorCode::ServerUserDirectoryNotAccessible;
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
return ErrorCode::ServerUserNotAllowedInSudoers;
if (stdOut.contains("password is required"))
return ErrorCode::ServerUserPasswordRequired;
return error;
}
ErrorCode InstallController::isServerDpkgBusy(const ServerCredentials &credentials, SshSession &sshSession)
{
m_cancelInstallation = false;
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
auto cbReadStdErr = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
QFutureWatcher<ErrorCode> watcher;
QFuture<ErrorCode> future = QtConcurrent::run([this, &stdOut, &cbReadStdOut, &cbReadStdErr, &credentials, &sshSession]() {
// max 100 attempts
for (int i = 0; i < 30; ++i) {
if (m_cancelInstallation) {
return ErrorCode::ServerCancelInstallation;
}
stdOut.clear();
sshSession.runScript(
credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::check_server_is_busy),
amnezia::genBaseVars(credentials, DockerContainer::None, QString(), QString())),
cbReadStdOut, cbReadStdErr);
if (stdOut.contains("Packet manager not found"))
return ErrorCode::ServerPacketManagerError;
if (stdOut.contains("fuser not installed") || stdOut.contains("cat not installed"))
return ErrorCode::NoError;
if (stdOut.isEmpty()) {
return ErrorCode::NoError;
} else {
#ifdef MZ_DEBUG
qDebug().noquote() << stdOut;
#endif
emit serverIsBusy(true);
QThread::msleep(10000);
}
}
return ErrorCode::ServerPacketManagerError;
});
QEventLoop wait;
QObject::connect(&watcher, &QFutureWatcher<ErrorCode>::finished, &wait, &QEventLoop::quit);
watcher.setFuture(future);
wait.exec();
emit serverIsBusy(false);
return future.result();
}
ErrorCode InstallController::setupServerFirewall(const ServerCredentials &credentials, SshSession &sshSession)
{
return sshSession.runScript(
credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::setup_host_firewall),
amnezia::genBaseVars(credentials, DockerContainer::None, QString(), QString())));
}
ErrorCode InstallController::rebootServer(int serverIndex)
{
auto credentials = m_serversRepository->serverCredentials(serverIndex);
SshSession sshSession(this);
QString script = QString("sudo reboot");
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data;
return ErrorCode::NoError;
};
auto cbReadStdErr = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
return sshSession.runScript(credentials, script, cbReadStdOut, cbReadStdErr);
}
ErrorCode InstallController::removeAllContainers(int serverIndex)
{
auto credentials = m_serversRepository->serverCredentials(serverIndex);
SshSession sshSession(this);
ErrorCode errorCode = sshSession.runScript(credentials, amnezia::scriptData(SharedScriptType::remove_all_containers));
if (errorCode == ErrorCode::NoError) {
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
serverConfigModel.visit([](auto& arg) {
arg.containers.clear();
arg.defaultContainer = DockerContainer::None;
});
m_serversRepository->editServer(serverIndex, serverConfigModel);
}
return errorCode;
}
ErrorCode InstallController::removeContainer(int serverIndex, DockerContainer container)
{
auto credentials = m_serversRepository->serverCredentials(serverIndex);
SshSession sshSession(this);
ErrorCode errorCode = sshSession.runScript(
credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
amnezia::genBaseVars(credentials, container, QString(), QString())));
if (errorCode == ErrorCode::NoError) {
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
QMap<DockerContainer, ContainerConfig> containers = serverConfigModel.containers();
containers.remove(container);
DockerContainer defaultContainer = serverConfigModel.defaultContainer();
if (defaultContainer == container) {
if (containers.isEmpty()) {
defaultContainer = DockerContainer::None;
} else {
defaultContainer = containers.begin().key();
}
}
serverConfigModel.visit([&containers, defaultContainer](auto& arg) {
arg.containers = containers;
arg.defaultContainer = defaultContainer;
});
m_serversRepository->editServer(serverIndex, serverConfigModel);
}
return errorCode;
}
QScopedPointer<InstallerBase> InstallController::createInstaller(DockerContainer container)
{
switch (container) {
case DockerContainer::Awg: return QScopedPointer<InstallerBase>(new AwgInstaller(this));
case DockerContainer::Awg2: return QScopedPointer<InstallerBase>(new AwgInstaller(this));
case DockerContainer::WireGuard: return QScopedPointer<InstallerBase>(new WireguardInstaller(this));
case DockerContainer::OpenVpn: return QScopedPointer<InstallerBase>(new OpenVpnInstaller(this));
case DockerContainer::Xray:
case DockerContainer::SSXray: return QScopedPointer<InstallerBase>(new XrayInstaller(this));
case DockerContainer::TorWebSite: return QScopedPointer<InstallerBase>(new TorInstaller(this));
case DockerContainer::Sftp: return QScopedPointer<InstallerBase>(new SftpInstaller(this));
case DockerContainer::Socks5Proxy: return QScopedPointer<InstallerBase>(new Socks5Installer(this));
default: return QScopedPointer<InstallerBase>(new InstallerBase(this));
}
}
ContainerConfig InstallController::generateConfig(DockerContainer container, int port, TransportProto transportProto)
{
auto installer = createInstaller(container);
return installer->generateConfig(container, port, transportProto);
}
ErrorCode InstallController::installContainer(const ServerCredentials &credentials, DockerContainer container, int port,
TransportProto transportProto, ContainerConfig &config)
{
config = generateConfig(container, port, transportProto);
return setupContainer(credentials, container, config, false);
}
bool InstallController::isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig)
{
if (ContainerUtils::isAwgContainer(container)) {
const auto* oldAwgConfig = oldConfig.getAwgProtocolConfig();
const auto* newAwgConfig = newConfig.getAwgProtocolConfig();
if (oldAwgConfig && newAwgConfig) {
if (oldAwgConfig->serverConfig.hasEqualServerSettings(newAwgConfig->serverConfig)) {
return false;
}
}
} else if (container == DockerContainer::WireGuard) {
const auto* oldWgConfig = oldConfig.getWireGuardProtocolConfig();
const auto* newWgConfig = newConfig.getWireGuardProtocolConfig();
if (oldWgConfig && newWgConfig) {
if (oldWgConfig->serverConfig.hasEqualServerSettings(newWgConfig->serverConfig)) {
return false;
}
}
}
return true;
}
ErrorCode InstallController::scanServerForInstalledContainers(int serverIndex)
{
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
SshSession sshSession(this);
QMap<DockerContainer, ContainerConfig> installedContainers;
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
QMap<DockerContainer, ContainerConfig> containers = serverConfigModel.containers();
bool hasNewContainers = false;
QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
for (auto iterator = installedContainers.begin(); iterator != installedContainers.end(); iterator++) {
if (!containers.contains(iterator.key())) {
ContainerConfig containerConfig = iterator.value();
errorCode = processContainerForAdmin(iterator.key(), containerConfig, credentials, sshSession,
serverIndex, clientName);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
containers.insert(iterator.key(), containerConfig);
hasNewContainers = true;
DockerContainer defaultContainer = serverConfigModel.defaultContainer();
if (defaultContainer == DockerContainer::None
&& ContainerUtils::containerService(iterator.key()) != ServiceType::Other
&& ContainerUtils::isSupportedByCurrentPlatform(iterator.key())) {
serverConfigModel.visit([iterator](auto& arg) {
arg.defaultContainer = iterator.key();
});
}
}
}
if (hasNewContainers) {
serverConfigModel.visit([&containers](auto& arg) {
arg.containers = containers;
});
m_serversRepository->editServer(serverIndex, serverConfigModel);
}
return ErrorCode::NoError;
}
ErrorCode InstallController::installServer(const ServerCredentials &credentials, DockerContainer container, int port,
TransportProto transportProto, bool &wasContainerInstalled)
{
SshSession sshSession(this);
QMap<DockerContainer, ContainerConfig> installedContainers;
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
if (errorCode) {
return errorCode;
}
wasContainerInstalled = false;
if (!installedContainers.contains(container)) {
ContainerConfig config;
errorCode = installContainer(credentials, container, port, transportProto, config);
if (errorCode) {
return errorCode;
}
installedContainers.insert(container, config);
wasContainerInstalled = true;
}
QMap<DockerContainer, ContainerConfig> preparedContainers;
for (auto iterator = installedContainers.begin(); iterator != installedContainers.end(); iterator++) {
DockerContainer container = iterator.key();
ContainerConfig containerConfig = iterator.value();
if (ContainerUtils::isSupportedByCurrentPlatform(container)) {
errorCode = prepareContainerConfig(container, credentials, containerConfig, sshSession);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
}
preparedContainers.insert(container, containerConfig);
}
SelfHostedServerConfig serverConfig;
serverConfig.hostName = credentials.hostName;
serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port;
serverConfig.description = m_appSettingsRepository->nextAvailableServerName();
for (auto iterator = preparedContainers.begin(); iterator != preparedContainers.end(); iterator++) {
serverConfig.containers.insert(iterator.key(), iterator.value());
}
serverConfig.defaultContainer = container;
m_serversRepository->addServer(ServerConfig(serverConfig));
int serverIndex = m_serversRepository->serversCount() - 1;
QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
for (auto iterator = preparedContainers.begin(); iterator != preparedContainers.end(); iterator++) {
adminAppendRequested(serverIndex, iterator.key(), iterator.value(), clientName);
}
return ErrorCode::NoError;
}
ErrorCode InstallController::installContainer(int serverIndex, DockerContainer container, int port,
TransportProto transportProto, bool &wasContainerInstalled)
{
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
SshSession sshSession(this);
QMap<DockerContainer, ContainerConfig> installedContainers;
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
if (errorCode) {
return errorCode;
}
wasContainerInstalled = false;
if (!installedContainers.contains(container)) {
ContainerConfig config;
errorCode = installContainer(credentials, container, port, transportProto, config);
if (errorCode) {
return errorCode;
}
installedContainers.insert(container, config);
wasContainerInstalled = true;
}
QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
for (auto iterator = installedContainers.begin(); iterator != installedContainers.end(); iterator++) {
ContainerConfig existingConfigModel = m_serversRepository->containerConfig(serverIndex, iterator.key());
if (existingConfigModel.container == DockerContainer::None) {
ContainerConfig containerConfig = iterator.value();
errorCode = processContainerForAdmin(iterator.key(), containerConfig, credentials, sshSession,
serverIndex, clientName);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
m_serversRepository->setContainerConfig(serverIndex, iterator.key(), containerConfig);
}
}
return ErrorCode::NoError;
}
ErrorCode InstallController::checkSshConnection(const ServerCredentials &credentials, QString &output,
std::function<QString()> passphraseCallback)
{
SshSession sshSession(this);
ErrorCode errorCode = ErrorCode::NoError;
ServerCredentials processedCredentials = credentials;
if (processedCredentials.secretData.contains("BEGIN") && processedCredentials.secretData.contains("PRIVATE KEY")) {
if (!passphraseCallback) {
return ErrorCode::SshPrivateKeyError;
}
QString decryptedPrivateKey;
errorCode = sshSession.getDecryptedPrivateKey(processedCredentials, decryptedPrivateKey, passphraseCallback);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
processedCredentials.secretData = decryptedPrivateKey;
}
output = sshSession.checkSshConnection(processedCredentials, errorCode);
return errorCode;
}
bool InstallController::isServerAlreadyExists(const ServerCredentials &credentials, int &existingServerIndex)
{
int serversCount = m_serversRepository->serversCount();
for (int i = 0; i < serversCount; i++) {
const ServerCredentials existingCredentials = m_serversRepository->serverCredentials(i);
if (credentials.hostName == existingCredentials.hostName && credentials.port == existingCredentials.port) {
existingServerIndex = i;
return true;
}
}
existingServerIndex = -1;
return false;
}
ErrorCode InstallController::mountSftpDrive(const ServerCredentials &credentials, const QString &port, const QString &password,
const QString &username)
{
QString mountPath;
QString cmd;
QString hostname = credentials.hostName;
#ifdef Q_OS_WINDOWS
mountPath = Utils::getNextDriverLetter() + ":";
cmd = "C:\\Program Files\\SSHFS-Win\\bin\\sshfs.exe";
#elif defined AMNEZIA_DESKTOP
mountPath = QString("%1/sftp:%2:%3").arg(QStandardPaths::writableLocation(QStandardPaths::HomeLocation), hostname, port);
QDir dir(mountPath);
if (!dir.exists()) {
dir.mkpath(mountPath);
}
cmd = "/usr/local/bin/sshfs";
QSharedPointer<QProcess> process(new QProcess(this));
process->setProcessChannelMode(QProcess::MergedChannels);
connect(process.get(), &QProcess::readyRead, this, [process, mountPath]() {
QString s = process->readAll();
if (s.contains("The service sshfs has been started")) {
QDesktopServices::openUrl(QUrl("file:///" + mountPath));
}
qDebug() << s;
});
process->setProgram(cmd);
QString args = QString("%1@%2:/ %3 "
"-o port=%4 "
"-f "
"-o reconnect "
"-o rellinks "
"-o fstypename=SSHFS "
"-o ssh_command=/usr/bin/ssh.exe "
"-o UserKnownHostsFile=/dev/null "
"-o StrictHostKeyChecking=no "
"-o password_stdin")
.arg(username, hostname, mountPath, port);
process->setArguments(args.split(" ", Qt::SkipEmptyParts));
process->start();
process->waitForStarted(50);
if (process->state() != QProcess::Running) {
qDebug() << "mountSftpDrive process not started";
qDebug() << args;
return ErrorCode::ServerContainerMissingError;
} else {
process->write((password + "\n").toUtf8());
}
m_sftpMountProcesses.append(process);
#else
Q_UNUSED(mountPath);
Q_UNUSED(cmd);
Q_UNUSED(password);
return ErrorCode::NoError;
#endif
return ErrorCode::NoError;
}
void InstallController::stopAllSftpMounts()
{
#ifdef Q_OS_WINDOWS
for (QSharedPointer<QProcess> process : m_sftpMountProcesses) {
Utils::signalCtrl(process->processId(), CTRL_C_EVENT);
process->kill();
process->waitForFinished();
}
m_sftpMountProcesses.clear();
#endif
}
void InstallController::updateContainerConfigAfterInstallation(DockerContainer container, ContainerConfig &containerConfig, const QString &stdOut)
{
Proto mainProto = ContainerUtils::defaultProtocol(container);
if (container == DockerContainer::TorWebSite) {
if (auto* torProtocolConfig = containerConfig.getTorProtocolConfig()) {
qDebug() << "amnezia-tor onions" << stdOut;
QString onion = stdOut;
onion.replace("\n", "");
torProtocolConfig->serverConfig.site = onion;
}
}
}
ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentials &credentials,
QMap<DockerContainer, ContainerConfig> &installedContainers, SshSession &sshSession)
{
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
auto cbReadStdErr = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
QString script = QString("sudo docker ps --format '{{.Names}} {{.Ports}}'");
ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut, cbReadStdErr);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
const static QRegularExpression containerAndPortRegExp("(amnezia[-a-z0-9]*).*?:([0-9]*)->[0-9]*/(udp|tcp).*");
const static QRegularExpression torOrDnsRegExp("(amnezia-(?:torwebsite|dns)).*?([0-9]*)/(udp|tcp).*");
QStringList containerInfos = stdOut.split("\n");
for (const QString &containerInfo : containerInfos) {
if (containerInfo.isEmpty()) {
continue;
}
QRegularExpressionMatch containerAndPortMatch = containerAndPortRegExp.match(containerInfo);
if (containerAndPortMatch.hasMatch()) {
QString name = containerAndPortMatch.captured(1);
QString portStr = containerAndPortMatch.captured(2);
QString transportProtoStr = containerAndPortMatch.captured(3);
DockerContainer container = ContainerUtils::containerFromString(name);
if (container == DockerContainer::None) {
continue;
}
int port = portStr.toInt();
TransportProto transportProto = ProtocolUtils::transportProtoFromString(transportProtoStr);
auto installer = createInstaller(container);
ContainerConfig config = installer->createBaseConfig(container, port, transportProto);
ErrorCode extractError = installer->extractConfigFromContainer(container, credentials, &sshSession, config);
if (extractError != ErrorCode::NoError && extractError != ErrorCode::ServerContainerMissingError) {
return extractError;
}
installedContainers.insert(container, config);
}
QRegularExpressionMatch torOrDnsRegMatch = torOrDnsRegExp.match(containerInfo);
if (torOrDnsRegMatch.hasMatch()) {
QString name = torOrDnsRegMatch.captured(1);
QString portStr = torOrDnsRegMatch.captured(2);
QString transportProtoStr = torOrDnsRegMatch.captured(3);
DockerContainer container = ContainerUtils::containerFromString(name);
if (container == DockerContainer::None) {
continue;
}
int port = portStr.toInt();
TransportProto transportProto = ProtocolUtils::transportProtoFromString(transportProtoStr);
auto installer = createInstaller(container);
ContainerConfig config = installer->createBaseConfig(container, port, transportProto);
ErrorCode extractError = installer->extractConfigFromContainer(container, credentials, &sshSession, config);
if (extractError != ErrorCode::NoError && extractError != ErrorCode::ServerContainerMissingError) {
return extractError;
}
installedContainers.insert(container, config);
}
}
return ErrorCode::NoError;
}