diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index db4061eb..eb1eab45 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -351,6 +351,9 @@ void AmneziaApplication::initModels() m_sftpConfigModel.reset(new SftpConfigModel(this)); m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get()); + m_socks5ConfigModel.reset(new Socks5ProxyConfigModel(this)); + m_engine->rootContext()->setContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel.get()); + m_clientManagementModel.reset(new ClientManagementModel(m_settings, this)); m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get()); connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked, m_serversModel.get(), diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 5561d7c7..b15d55d7 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -41,6 +41,7 @@ #include "ui/models/protocols_model.h" #include "ui/models/servers_model.h" #include "ui/models/services/sftpConfigModel.h" +#include "ui/models/services/socks5ProxyConfigModel.h" #include "ui/models/sites_model.h" #include "ui/models/clientManagementModel.h" #include "ui/models/appSplitTunnelingModel.h" @@ -114,6 +115,7 @@ private: #endif QScopedPointer m_sftpConfigModel; + QScopedPointer m_socks5ConfigModel; QSharedPointer m_vpnConnection; QThread m_vpnConnectionThread; diff --git a/client/containers/containers_defs.cpp b/client/containers/containers_defs.cpp index 89633f9e..3c2a3861 100644 --- a/client/containers/containers_defs.cpp +++ b/client/containers/containers_defs.cpp @@ -69,6 +69,8 @@ QVector ContainerProps::protocolsForContainer(amnezia::DockerCon case DockerContainer::Sftp: return { Proto::Sftp }; + case DockerContainer::Socks5Proxy: return { Proto::Socks5Proxy }; + default: return { defaultProtocol(container) }; } } @@ -98,7 +100,8 @@ QMap ContainerProps::containerHumanNames() { DockerContainer::TorWebSite, QObject::tr("Website in Tor network") }, { DockerContainer::Dns, QObject::tr("Amnezia DNS") }, - { DockerContainer::Sftp, QObject::tr("Sftp file sharing service") } }; + { DockerContainer::Sftp, QObject::tr("Sftp file sharing service") }, + { DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } }; } QMap ContainerProps::containerDescriptions() @@ -131,7 +134,9 @@ QMap ContainerProps::containerDescriptions() { DockerContainer::Dns, QObject::tr("Replace the current DNS server with your own. This will increase your privacy level.") }, { DockerContainer::Sftp, - QObject::tr("Create a file vault on your server to securely store and transfer files.") } }; + QObject::tr("Create a file vault on your server to securely store and transfer files.") }, + { DockerContainer::Socks5Proxy, + QObject::tr("") } }; } QMap ContainerProps::containerDetailedDescriptions() @@ -240,7 +245,8 @@ QMap ContainerProps::containerDetailedDescriptions() QObject::tr("After installation, Amnezia will create a\n\n file storage on your server. " "You will be able to access it using\n FileZilla or other SFTP clients, " "as well as mount the disk on your device to access\n it directly from your device.\n\n" - "For more detailed information, you can\n find it in the support section under \"Create SFTP file storage.\" ") } + "For more detailed information, you can\n find it in the support section under \"Create SFTP file storage.\" ") }, + { DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } }; } @@ -265,6 +271,7 @@ Proto ContainerProps::defaultProtocol(DockerContainer c) case DockerContainer::TorWebSite: return Proto::TorWebSite; case DockerContainer::Dns: return Proto::Dns; case DockerContainer::Sftp: return Proto::Sftp; + case DockerContainer::Socks5Proxy: return Proto::Socks5Proxy; default: return Proto::Any; } } @@ -367,6 +374,7 @@ bool ContainerProps::isShareable(DockerContainer container) case DockerContainer::TorWebSite: return false; case DockerContainer::Dns: return false; case DockerContainer::Sftp: return false; + case DockerContainer::Socks5Proxy: return false; default: return true; } } diff --git a/client/containers/containers_defs.h b/client/containers/containers_defs.h index f80cc097..a63e217b 100644 --- a/client/containers/containers_defs.h +++ b/client/containers/containers_defs.h @@ -28,7 +28,8 @@ namespace amnezia // non-vpn TorWebSite, Dns, - Sftp + Sftp, + Socks5Proxy }; Q_ENUM_NS(DockerContainer) } // namespace ContainerEnumNS diff --git a/client/core/controllers/serverController.cpp b/client/core/controllers/serverController.cpp index 12ede703..081d86d6 100644 --- a/client/core/controllers/serverController.cpp +++ b/client/core/controllers/serverController.cpp @@ -106,7 +106,7 @@ ErrorCode ServerController::runContainerScript(const ServerCredentials &credenti if (e) return e; - QString runner = QString("sudo docker exec -i $CONTAINER_NAME bash %1 ").arg(fileName); + QString runner = QString("sudo docker exec -i $CONTAINER_NAME sh %1 ").arg(fileName); e = runScript(credentials, replaceVars(runner, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr); QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName); @@ -376,6 +376,10 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c return true; } + if (container == DockerContainer::Socks5Proxy) { + return true; + } + return false; } @@ -516,6 +520,7 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential const QJsonObject &amneziaWireguarConfig = config.value(ProtocolProps::protoToString(Proto::Awg)).toObject(); const QJsonObject &xrayConfig = config.value(ProtocolProps::protoToString(Proto::Xray)).toObject(); const QJsonObject &sftpConfig = config.value(ProtocolProps::protoToString(Proto::Sftp)).toObject(); + const QJsonObject &socks5ProxyConfig = config.value(ProtocolProps::protoToString(Proto::Socks5Proxy)).toObject(); Vars vars; @@ -613,6 +618,14 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential vars.append({ { "$UNDERLOAD_PACKET_MAGIC_HEADER", amneziaWireguarConfig.value(config_key::underloadPacketMagicHeader).toString() } }); vars.append({ { "$TRANSPORT_PACKET_MAGIC_HEADER", amneziaWireguarConfig.value(config_key::transportPacketMagicHeader).toString() } }); + // Socks5 proxy vars + vars.append({ { "$SOCKS5_PROXY_PORT", socks5ProxyConfig.value(config_key::port).toString(protocols::socks5Proxy::defaultPort) } }); + auto username = socks5ProxyConfig.value(config_key:: userName).toString(); + auto password = socks5ProxyConfig.value(config_key::password).toString(); + QString socks5user = (!username.isEmpty() && !password.isEmpty()) ? QString("users %1:CL:%2").arg(username, password) : ""; + vars.append({ { "$SOCKS5_USER", socks5user } }); + vars.append({ { "$SOCKS5_AUTH_TYPE", socks5user.isEmpty() ? "none" : "strong" } }); + QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName); if (!serverIp.isEmpty()) { vars.append({ { "$SERVER_IP_ADDRESS", serverIp } }); diff --git a/client/core/scripts_registry.cpp b/client/core/scripts_registry.cpp index 4e720845..95b5df4a 100644 --- a/client/core/scripts_registry.cpp +++ b/client/core/scripts_registry.cpp @@ -18,6 +18,7 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container) case DockerContainer::TorWebSite: return QLatin1String("website_tor"); case DockerContainer::Dns: return QLatin1String("dns"); case DockerContainer::Sftp: return QLatin1String("sftp"); + case DockerContainer::Socks5Proxy: return QLatin1String("socks5_proxy"); default: return QString(); } } diff --git a/client/protocols/protocols_defs.cpp b/client/protocols/protocols_defs.cpp index 9373c8bb..bcae339c 100644 --- a/client/protocols/protocols_defs.cpp +++ b/client/protocols/protocols_defs.cpp @@ -77,7 +77,8 @@ QMap ProtocolProps::protocolHumanNames() { Proto::TorWebSite, "Website in Tor network" }, { Proto::Dns, "DNS Service" }, - { Proto::Sftp, QObject::tr("Sftp service") } }; + { Proto::Sftp, QObject::tr("Sftp service") }, + { Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } }; } QMap ProtocolProps::protocolDescriptions() @@ -102,6 +103,7 @@ amnezia::ServiceType ProtocolProps::protocolService(Proto p) case Proto::TorWebSite: return ServiceType::Other; case Proto::Dns: return ServiceType::Other; case Proto::Sftp: return ServiceType::Other; + case Proto::Socks5Proxy: return ServiceType::Other; default: return ServiceType::Other; } } @@ -113,6 +115,7 @@ int ProtocolProps::getPortForInstall(Proto p) case WireGuard: case ShadowSocks: case OpenVpn: + case Socks5Proxy: return QRandomGenerator::global()->bounded(30000, 50000); default: return defaultPort(p); @@ -135,6 +138,7 @@ int ProtocolProps::defaultPort(Proto p) case Proto::TorWebSite: return -1; case Proto::Dns: return 53; case Proto::Sftp: return 222; + case Proto::Socks5Proxy: return 38080; default: return -1; } } @@ -154,6 +158,7 @@ bool ProtocolProps::defaultPortChangeable(Proto p) case Proto::TorWebSite: return false; case Proto::Dns: return false; case Proto::Sftp: return true; + case Proto::Socks5Proxy: return true; default: return false; } } @@ -175,6 +180,7 @@ TransportProto ProtocolProps::defaultTransportProto(Proto p) case Proto::TorWebSite: return TransportProto::Tcp; case Proto::Dns: return TransportProto::Udp; case Proto::Sftp: return TransportProto::Tcp; + case Proto::Socks5Proxy: return TransportProto::Tcp; } } @@ -195,6 +201,7 @@ bool ProtocolProps::defaultTransportProtoChangeable(Proto p) case Proto::TorWebSite: return false; case Proto::Dns: return false; case Proto::Sftp: return false; + case Proto::Socks5Proxy: return false; default: return false; } return false; diff --git a/client/protocols/protocols_defs.h b/client/protocols/protocols_defs.h index c98735b0..56be0d7d 100644 --- a/client/protocols/protocols_defs.h +++ b/client/protocols/protocols_defs.h @@ -84,6 +84,7 @@ namespace amnezia constexpr char awg[] = "awg"; constexpr char xray[] = "xray"; constexpr char ssxray[] = "ssxray"; + constexpr char socks5proxy[] = "socks5proxy"; constexpr char configVersion[] = "config_version"; @@ -216,6 +217,14 @@ namespace amnezia constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858"; } + namespace socks5Proxy + { + constexpr char defaultUserName[] = "proxy_user"; + constexpr char defaultPort[] = "38080"; + + constexpr char proxyConfigPath[] = "/usr/local/3proxy/conf/3proxy.cfg"; + } + } // namespace protocols namespace ProtocolEnumNS @@ -244,7 +253,8 @@ namespace amnezia // non-vpn TorWebSite, Dns, - Sftp + Sftp, + Socks5Proxy }; Q_ENUM_NS(Proto) diff --git a/client/resources.qrc b/client/resources.qrc index 49fd66d3..84296462 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -198,7 +198,7 @@ ui/qml/Pages2/PageProtocolOpenVpnSettings.qml ui/qml/Pages2/PageProtocolShadowSocksSettings.qml ui/qml/Pages2/PageProtocolCloakSettings.qml - ui/qml/Pages2/PageProtocolXraySettings.qml + ui/qml/Pages2/PageProtocolXraySettings.qml ui/qml/Pages2/PageProtocolRaw.qml ui/qml/Pages2/PageSettingsLogging.qml ui/qml/Pages2/PageServiceSftpSettings.qml @@ -239,5 +239,10 @@ images/controls/alert-circle.svg images/controls/file-check-2.svg ui/qml/Controls2/WarningType.qml + ui/qml/Pages2/PageServiceSocksProxySettings.qml + server_scripts/socks5_proxy/run_container.sh + server_scripts/socks5_proxy/Dockerfile + server_scripts/socks5_proxy/configure_container.sh + server_scripts/socks5_proxy/start.sh diff --git a/client/server_scripts/socks5_proxy/Dockerfile b/client/server_scripts/socks5_proxy/Dockerfile new file mode 100644 index 00000000..7a38682f --- /dev/null +++ b/client/server_scripts/socks5_proxy/Dockerfile @@ -0,0 +1,10 @@ +FROM 3proxy/3proxy:latest + +LABEL maintainer="AmneziaVPN" + +RUN mkdir -p /opt/amnezia +RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh +RUN chmod a+x /opt/amnezia/start.sh + +ENTRYPOINT [ "/bin/sh", "/opt/amnezia/start.sh" ] +CMD [ "" ] \ No newline at end of file diff --git a/client/server_scripts/socks5_proxy/configure_container.sh b/client/server_scripts/socks5_proxy/configure_container.sh new file mode 100644 index 00000000..d271b65e --- /dev/null +++ b/client/server_scripts/socks5_proxy/configure_container.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +echo -e "#!/bin/3proxy" > /usr/local/3proxy/conf/3proxy.cfg +echo -e "config /usr/local/3proxy/conf/3proxy.cfg" >> /usr/local/3proxy/conf/3proxy.cfg +echo -e "timeouts 1 5 30 60 180 1800 15 60" >> /usr/local/3proxy/conf/3proxy.cfg + +echo -e "$SOCKS5_USER" >> /usr/local/3proxy/conf/3proxy.cfg + +echo -e "log /usr/local/3proxy/logs/3proxy.log" >> /usr/local/3proxy/conf/3proxy.cfg +echo -e "logformat \"-\\\"\"+_G{\"\"time_unix\"\":%t, \"\"proxy\"\":{\"\"type:\"\":\"\"%N\"\", \"\"port\"\":%p}, \"\"error\"\":{\"\"code\"\":\"\"%E\"\"}, \"\"auth\"\":{\"\"user\"\":\"\"%U\"\"}, \"\"client\"\":{\"\"ip\"\":\"\"%C\"\", \"\"port\"\":%c}, \"\"server\"\":{\"\"ip\"\":\"\"%R\"\", \"\"port\"\":%r}, \"\"bytes\"\":{\"\"sent\"\":%O, \"\"received\"\":%I}, \"\"request\"\":{\"\"hostname\"\":\"\"%n\"\"}, \"\"message\"\":\"\"%T\"\"}\"" >> /usr/local/3proxy/conf/3proxy.cfg +echo -e "auth $SOCKS5_AUTH_TYPE" >> /usr/local/3proxy/conf/3proxy.cfg +echo -e "socks -p$SOCKS5_PROXY_PORT" >> /usr/local/3proxy/conf/3proxy.cfg \ No newline at end of file diff --git a/client/server_scripts/socks5_proxy/run_container.sh b/client/server_scripts/socks5_proxy/run_container.sh new file mode 100644 index 00000000..38ff863a --- /dev/null +++ b/client/server_scripts/socks5_proxy/run_container.sh @@ -0,0 +1,5 @@ +sudo docker run -d \ +--restart always \ +-p $SOCKS5_PROXY_PORT:$SOCKS5_PROXY_PORT/tcp \ +--name $CONTAINER_NAME \ +$CONTAINER_NAME diff --git a/client/server_scripts/socks5_proxy/start.sh b/client/server_scripts/socks5_proxy/start.sh new file mode 100644 index 00000000..98555d4e --- /dev/null +++ b/client/server_scripts/socks5_proxy/start.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# This scripts copied from Amnezia client to Docker container to /opt/amnezia and launched every time container starts + +echo "Container startup" + +/bin/3proxy /usr/local/3proxy/conf/3proxy.cfg \ No newline at end of file diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index e743d22c..514091d4 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -123,6 +123,9 @@ void InstallController::install(DockerContainer container, int port, TransportPr } else if (container == DockerContainer::Sftp) { containerConfig.insert(config_key::userName, protocols::sftp::defaultUserName); containerConfig.insert(config_key::password, Utils::getRandomString(10)); + } else if (container == DockerContainer::Socks5Proxy) { + containerConfig.insert(config_key::userName, protocols::socks5Proxy::defaultUserName); + containerConfig.insert(config_key::password, Utils::getRandomString(10)); } config.insert(config_key::container, ContainerProps::containerToString(container)); @@ -362,7 +365,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia if (containerInfo.isEmpty()) { continue; } - const static QRegularExpression containerAndPortRegExp("(amnezia[-a-z]*).*?:([0-9]*)->[0-9]*/(udp|tcp).*"); + const static QRegularExpression containerAndPortRegExp("(amnezia[-a-z0-9]*).*?:([0-9]*)->[0-9]*/(udp|tcp).*"); QRegularExpressionMatch containerAndPortMatch = containerAndPortRegExp.match(containerInfo); if (containerAndPortMatch.hasMatch()) { QString name = containerAndPortMatch.captured(1); @@ -427,6 +430,20 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia containerConfig.insert(config_key::userName, userName); containerConfig.insert(config_key::password, password); + } else if (protocol == Proto::Socks5Proxy) { + QString proxyConfig = serverController->getTextFileFromContainer(container, credentials, + protocols::socks5Proxy::proxyConfigPath, errorCode); + + const static QRegularExpression usernameAndPasswordRegExp("users (\\w+):CL:(\\w+)"); + QRegularExpressionMatch usernameAndPasswordMatch = usernameAndPasswordRegExp.match(proxyConfig); + + if (usernameAndPasswordMatch.hasMatch()) { + QString userName = usernameAndPasswordMatch.captured(1); + QString password = usernameAndPasswordMatch.captured(2); + + containerConfig.insert(config_key::userName, userName); + containerConfig.insert(config_key::password, password); + } } config.insert(config_key::container, ContainerProps::containerToString(container)); @@ -603,6 +620,10 @@ void InstallController::clearCachedProfile(QSharedPointer serv int serverIndex = m_serversModel->getProcessedServerIndex(); DockerContainer container = static_cast(m_containersModel->getProcessedContainerIndex()); + if (ContainerProps::containerService(container) == ServiceType::Other) { + return; + } + QJsonObject containerConfig = m_containersModel->getContainerConfig(container); ServerCredentials serverCredentials = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole)); diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 1fdb1e81..c9d655ba 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -35,6 +35,7 @@ namespace PageLoader PageServiceSftpSettings, PageServiceTorWebsiteSettings, PageServiceDnsSettings, + PageServiceSocksProxySettings, PageSetupWizardStart, PageSetupWizardCredentials, diff --git a/client/ui/models/protocols_model.cpp b/client/ui/models/protocols_model.cpp index b2838ce3..32447cd4 100644 --- a/client/ui/models/protocols_model.cpp +++ b/client/ui/models/protocols_model.cpp @@ -86,6 +86,7 @@ PageLoader::PageEnum ProtocolsModel::protocolPage(Proto protocol) const case Proto::TorWebSite: return PageLoader::PageEnum::PageServiceTorWebsiteSettings; case Proto::Dns: return PageLoader::PageEnum::PageServiceDnsSettings; case Proto::Sftp: return PageLoader::PageEnum::PageServiceSftpSettings; + case Proto::Socks5Proxy: return PageLoader::PageEnum::PageServiceSocksProxySettings; default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings; } } diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index 17535e5a..3f167029 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -548,6 +548,8 @@ QStringList ServersModel::getAllInstalledServicesName(const int serverIndex) servicesName.append("SFTP"); } else if (container == DockerContainer::TorWebSite) { servicesName.append("TOR"); + } else if (container == DockerContainer::Socks5Proxy) { + servicesName.append("SOCKS5"); } } } diff --git a/client/ui/models/services/socks5ProxyConfigModel.cpp b/client/ui/models/services/socks5ProxyConfigModel.cpp new file mode 100644 index 00000000..f68670df --- /dev/null +++ b/client/ui/models/services/socks5ProxyConfigModel.cpp @@ -0,0 +1,80 @@ +#include "socks5ProxyConfigModel.h" + +#include "protocols/protocols_defs.h" + +Socks5ProxyConfigModel::Socks5ProxyConfigModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +int Socks5ProxyConfigModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return 1; +} + +bool Socks5ProxyConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.row() < 0 || index.row() >= ContainerProps::allContainers().size()) { + return false; + } + + switch (role) { + case Roles::PortRole: m_protocolConfig.insert(config_key::port, value.toString()); break; + case Roles::UserNameRole: m_protocolConfig.insert(config_key::userName, value.toString()); break; + case Roles::PasswordRole: m_protocolConfig.insert(config_key::password, value.toString()); break; + } + + emit dataChanged(index, index, QList { role }); + return true; +} + +QVariant Socks5ProxyConfigModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) { + return false; + } + + switch (role) { + case Roles::PortRole: return m_protocolConfig.value(config_key::port).toString(); + case Roles::UserNameRole: + return m_protocolConfig.value(config_key::userName).toString(); + case Roles::PasswordRole: return m_protocolConfig.value(config_key::password).toString(); + } + + return QVariant(); +} + +void Socks5ProxyConfigModel::updateModel(const QJsonObject &config) +{ + beginResetModel(); + m_container = ContainerProps::containerFromString(config.value(config_key::container).toString()); + + m_fullConfig = config; + QJsonObject protocolConfig = config.value(config_key::socks5proxy).toObject(); + + m_protocolConfig.insert(config_key::userName, + protocolConfig.value(config_key::userName).toString()); + + m_protocolConfig.insert(config_key::password, protocolConfig.value(config_key::password).toString()); + + m_protocolConfig.insert(config_key::port, protocolConfig.value(config_key::port).toString()); + + endResetModel(); +} + +QJsonObject Socks5ProxyConfigModel::getConfig() +{ + m_fullConfig.insert(config_key::socks5proxy, m_protocolConfig); + return m_fullConfig; +} + +QHash Socks5ProxyConfigModel::roleNames() const +{ + QHash roles; + + roles[PortRole] = "port"; + roles[UserNameRole] = "username"; + roles[PasswordRole] = "password"; + + return roles; +} diff --git a/client/ui/models/services/socks5ProxyConfigModel.h b/client/ui/models/services/socks5ProxyConfigModel.h new file mode 100644 index 00000000..fc6f2fd4 --- /dev/null +++ b/client/ui/models/services/socks5ProxyConfigModel.h @@ -0,0 +1,40 @@ +#ifndef SOCKS5PROXYCONFIGMODEL_H +#define SOCKS5PROXYCONFIGMODEL_H + +#include +#include + +#include "containers/containers_defs.h" + +class Socks5ProxyConfigModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + PortRole = Qt::UserRole + 1, + UserNameRole, + PasswordRole + }; + + explicit Socks5ProxyConfigModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +public slots: + void updateModel(const QJsonObject &config); + QJsonObject getConfig(); + +protected: + QHash roleNames() const override; + +private: + DockerContainer m_container; + QJsonObject m_protocolConfig; + QJsonObject m_fullConfig; +}; + +#endif // SOCKS5PROXYCONFIGMODEL_H diff --git a/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml b/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml new file mode 100644 index 00000000..95343f63 --- /dev/null +++ b/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml @@ -0,0 +1,385 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import ContainerProps 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + defaultActiveFocusItem: listview + + Connections { + target: InstallController + + function onUpdateContainerFinished() { + PageController.showNotificationMessage(qsTr("Settings updated successfully")) + } + } + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + + ColumnLayout { + id: backButtonLayout + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.topMargin: 20 + + BackButtonType { + id: backButton + KeyNavigation.tab: listview + } + } + + FlickableType { + id: fl + anchors.top: backButtonLayout.bottom + anchors.bottom: parent.bottom + contentHeight: listview.implicitHeight + + ListView { + id: listview + + width: parent.width + height: listview.contentItem.height + + clip: true + interactive: false + + model: Socks5ProxyConfigModel + + onFocusChanged: { + if (focus) { + listview.currentItem.focusItemId.forceActiveFocus() + } + } + + delegate: Item { + implicitWidth: listview.width + implicitHeight: content.implicitHeight + + property alias focusItemId: hostLabel.rightButton + + ColumnLayout { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + spacing: 0 + + HeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + headerText: qsTr("SOCKS5 settings") + } + + LabelWithButtonType { + id: hostLabel + Layout.fillWidth: true + Layout.topMargin: 32 + + parentFlickable: fl + KeyNavigation.tab: portLabel.rightButton + + text: qsTr("Host") + descriptionText: ServersModel.getProcessedServerData("hostName") + + descriptionOnTop: true + + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: "#D7D8DB" + + clickedFunction: function() { + GC.copyToClipBoard(descriptionText) + PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } + } + } + + LabelWithButtonType { + id: portLabel + Layout.fillWidth: true + + text: qsTr("Port") + descriptionText: port + + descriptionOnTop: true + + parentFlickable: fl + KeyNavigation.tab: usernameLabel.rightButton + + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: "#D7D8DB" + + clickedFunction: function() { + GC.copyToClipBoard(descriptionText) + PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } + } + } + + LabelWithButtonType { + id: usernameLabel + Layout.fillWidth: true + + text: qsTr("User name") + descriptionText: username + + descriptionOnTop: true + + parentFlickable: fl + KeyNavigation.tab: passwordLabel.eyeButton + + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: "#D7D8DB" + + clickedFunction: function() { + GC.copyToClipBoard(descriptionText) + PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } + } + } + + LabelWithButtonType { + id: passwordLabel + Layout.fillWidth: true + + text: qsTr("Password") + descriptionText: password + + descriptionOnTop: true + + parentFlickable: fl + eyeButton.KeyNavigation.tab: passwordLabel.rightButton + rightButton.KeyNavigation.tab: changeSettingsButton + + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: "#D7D8DB" + + buttonImageSource: hideDescription ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg" + + clickedFunction: function() { + GC.copyToClipBoard(descriptionText) + PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } + } + } + + DrawerType2 { + id: changeSettingsDrawer + parent: root + + anchors.fill: parent + expandedHeight: root.height * 0.9 + + onClosed: { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } + } + + expandedContent: ColumnLayout { + property string tempPort: port + property string tempUsername: username + property string tempPassword: password + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 32 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + spacing: 0 + + Connections { + target: changeSettingsDrawer + function onOpened() { + if (!GC.isMobile()) { + drawerFocusItem.forceActiveFocus() + } + tempPort = port + tempUsername = username + tempPassword = password + } + function onClosed() { + port = tempPort + username = tempUsername + password = tempPassword + portTextField.textFieldText = port + usernameTextField.textFieldText = username + passwordTextField.textFieldText = password + } + } + + Item { + id: drawerFocusItem + KeyNavigation.tab: portTextField.textField + } + + HeaderType { + Layout.fillWidth: true + + headerText: qsTr("SOCKS5 settings") + } + + TextFieldWithHeaderType { + id: portTextField + + Layout.fillWidth: true + Layout.topMargin: 40 + parentFlickable: fl + + headerText: qsTr("Port") + textFieldText: port + textField.maximumLength: 5 + textField.validator: IntValidator { bottom: 1; top: 65535 } + + textField.onEditingFinished: { + textFieldText = textField.text.replace(/^\s+|\s+$/g, '') + if (textFieldText !== port) { + port = textFieldText + } + } + + KeyNavigation.tab: usernameTextField.textField + } + + TextFieldWithHeaderType { + id: usernameTextField + + Layout.fillWidth: true + Layout.topMargin: 16 + parentFlickable: fl + + headerText: qsTr("Username") + textFieldPlaceholderText: "username" + textFieldText: username + textField.maximumLength: 32 + + textField.onEditingFinished: { + textFieldText = textField.text.replace(/^\s+|\s+$/g, '') + if (textFieldText !== username) { + username = textFieldText + } + } + + KeyNavigation.tab: passwordTextField.textField + } + + TextFieldWithHeaderType { + id: passwordTextField + + property bool hidePassword: true + + Layout.fillWidth: true + Layout.topMargin: 16 + parentFlickable: fl + + headerText: qsTr("Password") + textFieldPlaceholderText: "password" + textFieldText: password + textField.maximumLength: 32 + + textField.echoMode: hidePassword ? TextInput.Password : TextInput.Normal + buttonImageSource: textFieldText !== "" ? (hidePassword ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") + : "" + + clickedFunc: function() { + hidePassword = !hidePassword + } + + textField.onFocusChanged: { + textFieldText = textField.text.replace(/^\s+|\s+$/g, '') + if (textFieldText !== password) { + password = textFieldText + } + } + + KeyNavigation.tab: saveButton + } + + BasicButtonType { + id: saveButton + + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.bottomMargin: 24 + + text: qsTr("Change connection settings") + Keys.onTabPressed: lastItemTabClicked(drawerFocusItem) + + clickedFunc: function() { + forceActiveFocus() + + if (!portTextField.textField.acceptableInput) { + portTextField.errorText = qsTr("The port must be in the range of 1 to 65535") + return + } + if (usernameTextField.textFieldText && passwordTextField.textFieldText === "") { + passwordTextField.errorText = qsTr("Password cannot be empty") + return + } else if (usernameTextField.textFieldText === "" && passwordTextField.textFieldText) { + usernameTextField.errorText = qsTr("Username cannot be empty") + return + } + + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(Socks5ProxyConfigModel.getConfig()) + tempPort = portTextField.textFieldText + tempUsername = usernameTextField.textFieldText + tempPassword = passwordTextField.textFieldText + changeSettingsDrawer.close() + } + } + } + } + + BasicButtonType { + id: changeSettingsButton + + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.bottomMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Change connection settings") + Keys.onTabPressed: lastItemTabClicked(focusItem) + + clickedFunc: function() { + forceActiveFocus() + changeSettingsDrawer.open() + } + } + } + } + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml index 97288733..a0c668be 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml @@ -18,6 +18,8 @@ import "../Components" PageType { id: root + property bool isClearCacheVisible: ServersModel.isProcessedServerHasWriteAccess() && !ContainersModel.isServiceContainer(ContainersModel.getProcessedContainerIndex()) + defaultActiveFocusItem: focusItem Item { @@ -103,6 +105,7 @@ PageType { case ProtocolEnum.Awg: AwgConfigModel.updateModel(ProtocolsModel.getConfig()); break; case ProtocolEnum.Xray: XrayConfigModel.updateModel(ProtocolsModel.getConfig()); break; case ProtocolEnum.Ipsec: Ikev2ConfigModel.updateModel(ProtocolsModel.getConfig()); break; + case ProtocolEnum.Socks5Proxy: Socks5ProxyConfigModel.updateModel(ProtocolsModel.getConfig()); break; } PageController.goToPage(protocolPage); } @@ -124,7 +127,7 @@ PageType { Layout.fillWidth: true - visible: ServersModel.isProcessedServerHasWriteAccess() + visible: root.isClearCacheVisible KeyNavigation.tab: removeButton text: qsTr("Clear %1 profile").arg(ContainersModel.getProcessedContainerName()) @@ -167,7 +170,7 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 - visible: ServersModel.isProcessedServerHasWriteAccess() + visible: root.isClearCacheVisible } LabelWithButtonType { diff --git a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml index b694dda0..f27873c6 100644 --- a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml +++ b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml @@ -261,6 +261,11 @@ PageType { Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { + if (!port.textField.acceptableInput) { + port.errorText = qsTr("The port must be in the range of 1 to 65535") + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.install(dockerContainer, port.textFieldText, transportProtoSelector.currentIndex) }