#include "subscriptionController.h" #include #include #include #include #include #include #include #include #include #include #include "core/configurators/openVpnConfigurator.h" #include "core/configurators/wireguardConfigurator.h" #include "core/utils/containerEnum.h" #include "core/utils/containers/containerUtils.h" #include "core/utils/protocolEnum.h" #include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" #include "core/utils/api/apiUtils.h" #include "core/controllers/gatewayController.h" #include "core/utils/protocolEnum.h" #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" #include "core/utils/constants/protocolConstants.h" #include "version.h" #include "core/models/containerConfig.h" #include "core/models/api/apiConfig.h" #if defined(Q_OS_IOS) || defined(MACOS_NE) #include "platforms/ios/ios_controller.h" #include #endif using namespace amnezia; namespace { QString getSubscriptionStatusForRenewal(const ApiConfig &apiConfig) { if (apiConfig.subscriptionExpiredByServer) { return QStringLiteral("expired"); } if (!apiConfig.subscription.endDate.isEmpty()) { if (apiUtils::isSubscriptionExpired(apiConfig.subscription.endDate)) { return QStringLiteral("expired"); } if (apiUtils::isSubscriptionExpiringSoon(apiConfig.subscription.endDate)) { return QStringLiteral("expire_soon"); } } return QStringLiteral("active"); } } SubscriptionController::SubscriptionController(SecureServersRepository* serversRepository, SecureAppSettingsRepository* appSettingsRepository) : m_serversRepository(serversRepository), m_appSettingsRepository(appSettingsRepository) { } QJsonObject SubscriptionController::GatewayRequestData::toJsonObject() const { QJsonObject obj; if (!osVersion.isEmpty()) { obj[apiDefs::key::osVersion] = osVersion; } if (!appVersion.isEmpty()) { obj[apiDefs::key::appVersion] = appVersion; } if (!appLanguage.isEmpty()) { obj[apiDefs::key::appLanguage] = appLanguage; } if (!installationUuid.isEmpty()) { obj[apiDefs::key::uuid] = installationUuid; } if (!userCountryCode.isEmpty()) { obj[apiDefs::key::userCountryCode] = userCountryCode; } if (!serverCountryCode.isEmpty()) { obj[apiDefs::key::serverCountryCode] = serverCountryCode; } if (!serviceType.isEmpty()) { obj[apiDefs::key::serviceType] = serviceType; } if (!serviceProtocol.isEmpty()) { obj[apiDefs::key::serviceProtocol] = serviceProtocol; } if (!authData.isEmpty()) { obj[apiDefs::key::authData] = authData; } return obj; } SubscriptionController::ProtocolData SubscriptionController::generateProtocolData(const QString &protocol) { ProtocolData protocolData; if (protocol == configKey::awg) { auto connData = WireguardConfigurator::genClientKeys(); protocolData.wireGuardClientPubKey = connData.clientPubKey; protocolData.wireGuardClientPrivKey = connData.clientPrivKey; } else if (protocol == configKey::vless) { protocolData.xrayUuid = QUuid::createUuid().toString(QUuid::WithoutBraces); } return protocolData; } void SubscriptionController::appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload) { if (protocol == configKey::awg) { apiPayload[apiDefs::key::publicKey] = protocolData.wireGuardClientPubKey; } else if (protocol == configKey::vless) { apiPayload[apiDefs::key::publicKey] = protocolData.xrayUuid; } } ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol, const ProtocolData &protocolData, QJsonObject &serverConfigJson) { QString data = QJsonDocument::fromJson(apiResponseBody).object().value(configKey::config).toString(); data.replace("vpn://", ""); QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); if (ba.isEmpty()) { qDebug() << "empty vpn key"; return ErrorCode::ApiConfigEmptyError; } QByteArray ba_uncompressed = qUncompress(ba); if (!ba_uncompressed.isEmpty()) { ba = ba_uncompressed; } QString configStr = ba; if (protocol == configKey::awg) { configStr.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey); auto newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); auto containers = newServerConfig.value(configKey::containers).toArray(); if (containers.isEmpty()) { qDebug() << "missing containers field"; return ErrorCode::ApiConfigEmptyError; } auto container = containers.at(0).toObject(); QString containerName = ContainerUtils::containerTypeToString(DockerContainer::Awg); auto serverProtocolConfig = container.value(containerName).toObject(); auto clientProtocolConfig = QJsonDocument::fromJson(serverProtocolConfig.value(configKey::lastConfig).toString().toUtf8()).object(); // TODO looks like this block can be removed after v1 configs EOL serverProtocolConfig[configKey::junkPacketCount] = clientProtocolConfig.value(configKey::junkPacketCount); serverProtocolConfig[configKey::junkPacketMinSize] = clientProtocolConfig.value(configKey::junkPacketMinSize); serverProtocolConfig[configKey::junkPacketMaxSize] = clientProtocolConfig.value(configKey::junkPacketMaxSize); serverProtocolConfig[configKey::initPacketJunkSize] = clientProtocolConfig.value(configKey::initPacketJunkSize); serverProtocolConfig[configKey::responsePacketJunkSize] = clientProtocolConfig.value(configKey::responsePacketJunkSize); serverProtocolConfig[configKey::initPacketMagicHeader] = clientProtocolConfig.value(configKey::initPacketMagicHeader); serverProtocolConfig[configKey::responsePacketMagicHeader] = clientProtocolConfig.value(configKey::responsePacketMagicHeader); serverProtocolConfig[configKey::underloadPacketMagicHeader] = clientProtocolConfig.value(configKey::underloadPacketMagicHeader); serverProtocolConfig[configKey::transportPacketMagicHeader] = clientProtocolConfig.value(configKey::transportPacketMagicHeader); serverProtocolConfig[configKey::cookieReplyPacketJunkSize] = clientProtocolConfig.value(configKey::cookieReplyPacketJunkSize); serverProtocolConfig[configKey::transportPacketJunkSize] = clientProtocolConfig.value(configKey::transportPacketJunkSize); serverProtocolConfig[configKey::specialJunk1] = clientProtocolConfig.value(configKey::specialJunk1); serverProtocolConfig[configKey::specialJunk2] = clientProtocolConfig.value(configKey::specialJunk2); serverProtocolConfig[configKey::specialJunk3] = clientProtocolConfig.value(configKey::specialJunk3); serverProtocolConfig[configKey::specialJunk4] = clientProtocolConfig.value(configKey::specialJunk4); serverProtocolConfig[configKey::specialJunk5] = clientProtocolConfig.value(configKey::specialJunk5); // container[containerName] = serverProtocolConfig; containers.replace(0, container); newServerConfig[configKey::containers] = containers; configStr = QString(QJsonDocument(newServerConfig).toJson()); } serverConfigJson = QJsonDocument::fromJson(configStr.toUtf8()).object(); return ErrorCode::NoError; } void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson, const QString &serviceType, const QString &serviceProtocol, const QString &userCountryCode, const QByteArray &apiResponseBody) { QJsonObject apiConfig = serverConfigJson.value(apiDefs::key::apiConfig).toObject(); apiConfig[apiDefs::key::serviceType] = serviceType; apiConfig[apiDefs::key::serviceProtocol] = serviceProtocol; apiConfig[apiDefs::key::userCountryCode] = userCountryCode; if (serverConfigJson.value(configKey::configVersion).toInt() == serverConfigUtils::ConfigSource::AmneziaGateway) { QJsonObject responseObj = QJsonDocument::fromJson(apiResponseBody).object(); if (responseObj.contains(apiDefs::key::supportedProtocols)) { apiConfig.insert(apiDefs::key::supportedProtocols, responseObj.value(apiDefs::key::supportedProtocols).toArray()); } if (responseObj.contains(apiDefs::key::serviceInfo)) { apiConfig.insert(apiDefs::key::serviceInfo, responseObj.value(apiDefs::key::serviceInfo).toObject()); } } serverConfigJson[apiDefs::key::apiConfig] = apiConfig; } ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase) { GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase), m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), apiDefs::requestTimeoutMsecs, m_appSettingsRepository->isStrictKillSwitchEnabled()); return gatewayController.post(endpoint, apiPayload, responseBody); } ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol, const ProtocolData &protocolData) { GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), m_appSettingsRepository->getAppLanguage().name().split("_").first(), m_appSettingsRepository->getInstallationUuid(true), userCountryCode, "", serviceType, serviceProtocol, QJsonObject() }; QJsonObject apiPayload = gatewayRequestData.toJsonObject(); appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload); QByteArray responseBody; ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody); if (errorCode != ErrorCode::NoError) { return errorCode; } QJsonObject serverConfigJson; errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson); if (errorCode != ErrorCode::NoError) { return errorCode; } updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody); if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) { return ErrorCode::InternalError; } ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverConfigJson); m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(), serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson())); return ErrorCode::NoError; } ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol, const QString &email) { const QString trimmedEmail = email.trimmed(); if (trimmedEmail.isEmpty()) { return ErrorCode::ApiConfigEmptyError; } GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), m_appSettingsRepository->getAppLanguage().name().split("_").first(), m_appSettingsRepository->getInstallationUuid(true), userCountryCode, "", serviceType, serviceProtocol, QJsonObject() }; ProtocolData protocolData = generateProtocolData(serviceProtocol); QJsonObject apiPayload = gatewayRequestData.toJsonObject(); appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload); apiPayload.insert(apiDefs::key::email, trimmedEmail); QByteArray responseBody; ErrorCode errorCode = executeRequest(QString("%1v1/trial"), apiPayload, responseBody); if (errorCode != ErrorCode::NoError) { return errorCode; } QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object(); QString key = responseObject.value(apiDefs::key::config).toString(); if (key.isEmpty()) { return ErrorCode::ApiConfigEmptyError; } key.replace(QStringLiteral("vpn://"), QString()); QByteArray configBytes = QByteArray::fromBase64(key.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); QByteArray uncompressed = qUncompress(configBytes); if (!uncompressed.isEmpty()) { configBytes = uncompressed; } if (configBytes.isEmpty()) { return ErrorCode::ApiConfigEmptyError; } QJsonObject configObject = QJsonDocument::fromJson(configBytes).object(); if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) { return ErrorCode::InternalError; } ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(configObject); m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(), serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson())); return ErrorCode::NoError; } ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol, const ProtocolData &protocolData, const QString &transactionId, bool isTestPurchase, int *duplicateServerIndex) { GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), m_appSettingsRepository->getAppLanguage().name().split("_").first(), m_appSettingsRepository->getInstallationUuid(true), userCountryCode, "", serviceType, serviceProtocol, QJsonObject() }; QJsonObject apiPayload = gatewayRequestData.toJsonObject(); appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload); apiPayload[apiDefs::key::transactionId] = transactionId; QByteArray responseBody; ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase); if (errorCode != ErrorCode::NoError) { return errorCode; } // Parse the subscription response QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object(); QString key = responseObject.value(QStringLiteral("key")).toString(); if (key.isEmpty()) { qWarning().noquote() << "[IAP] Subscription response does not contain a key field"; return ErrorCode::ApiPurchaseError; } QString normalizedKey = key; normalizedKey.replace(QStringLiteral("vpn://"), QString()); // Check if server with this VPN key already exists for (int i = 0; i < m_serversRepository->serversCount(); ++i) { const auto apiV2 = m_serversRepository->apiV2Config(m_serversRepository->serverIdAt(i)); QString existingVpnKey = apiV2.has_value() ? apiV2->vpnKey() : QString(); existingVpnKey.replace(QStringLiteral("vpn://"), QString()); if (!existingVpnKey.isEmpty() && existingVpnKey == normalizedKey) { if (duplicateServerIndex) { *duplicateServerIndex = i; } qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists"; return ErrorCode::ApiConfigAlreadyAdded; } } QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); QByteArray configUncompressed = qUncompress(configString); if (!configUncompressed.isEmpty()) { configString = configUncompressed; } if (configString.isEmpty()) { qWarning().noquote() << "[IAP] Subscription response config payload is empty"; return ErrorCode::ApiPurchaseError; } QJsonObject configObject = QJsonDocument::fromJson(configString).object(); quint16 crc = qChecksum(QJsonDocument(configObject).toJson()); if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) { return ErrorCode::InternalError; } ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(configObject); ApiV2ServerConfig* apiV2 = &apiV2ServerConfig; apiV2->apiConfig.vpnKey = normalizedKey; apiV2->apiConfig.isTestPurchase = isTestPurchase; apiV2->apiConfig.isInAppPurchase = true; apiV2->apiConfig.subscriptionExpiredByServer = false; apiV2->crc = crc; m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(), serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson())); return ErrorCode::NoError; } ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent) { auto apiV2 = m_serversRepository->apiV2Config(serverId); if (!apiV2.has_value()) { return ErrorCode::InternalError; } const bool isTestPurchase = apiV2->apiConfig.isTestPurchase; QString serviceProtocol = apiV2->serviceProtocol(); ProtocolData protocolData = generateProtocolData(serviceProtocol); QJsonObject authDataJson = apiV2->authData.toJson(); GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), m_appSettingsRepository->getAppLanguage().name().split("_").first(), m_appSettingsRepository->getInstallationUuid(true), apiV2->apiConfig.userCountryCode, newCountryCode, apiV2->serviceType(), serviceProtocol, authDataJson }; QJsonObject apiPayload = gatewayRequestData.toJsonObject(); appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload); if (isConnectEvent) { apiPayload[apiDefs::key::isConnectEvent] = true; } QByteArray responseBody; ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase); if (errorCode != ErrorCode::NoError) { if (errorCode == ErrorCode::ApiSubscriptionExpiredError && !apiV2->apiConfig.isInAppPurchase) { ApiV2ServerConfig expiredApiV2 = *apiV2; expiredApiV2.apiConfig.subscriptionExpiredByServer = true; m_serversRepository->editServer(serverId, expiredApiV2.toJson(), serverConfigUtils::configTypeFromJson(expiredApiV2.toJson())); } return errorCode; } QJsonObject serverConfigJson; errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson); if (errorCode != ErrorCode::NoError) { return errorCode; } updateApiConfigInJson(serverConfigJson, apiV2->apiConfig.serviceType, serviceProtocol, apiV2->apiConfig.userCountryCode, responseBody); if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) { return ErrorCode::InternalError; } ApiV2ServerConfig newApiV2Config = ApiV2ServerConfig::fromJson(serverConfigJson); ApiV2ServerConfig* newApiV2 = &newApiV2Config; newApiV2->apiConfig.vpnKey = apiV2->apiConfig.vpnKey; newApiV2->apiConfig.isTestPurchase = apiV2->apiConfig.isTestPurchase; newApiV2->apiConfig.isInAppPurchase = apiV2->apiConfig.isInAppPurchase; newApiV2->apiConfig.subscriptionExpiredByServer = false; newApiV2->authData = apiV2->authData; newApiV2->crc = apiV2->crc; if (apiV2->nameOverriddenByUser) { newApiV2->name = apiV2->name; newApiV2->nameOverriddenByUser = true; } m_serversRepository->editServer(serverId, newApiV2Config.toJson(), serverConfigUtils::configTypeFromJson(newApiV2Config.toJson())); return ErrorCode::NoError; } ErrorCode SubscriptionController::deactivateDevice(const QString &serverId) { auto apiV2 = m_serversRepository->apiV2Config(serverId); if (!apiV2.has_value()) { return ErrorCode::NoError; } if (!apiV2->isPremium() && !apiV2->isExternalPremium()) { return ErrorCode::NoError; } QJsonObject authDataJson = apiV2->authData.toJson(); GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), m_appSettingsRepository->getAppLanguage().name().split("_").first(), m_appSettingsRepository->getInstallationUuid(true), apiV2->apiConfig.userCountryCode, apiV2->apiConfig.serverCountryCode, apiV2->serviceType(), "", authDataJson }; QJsonObject apiPayload = gatewayRequestData.toJsonObject(); const bool isTestPurchase = apiV2->apiConfig.isTestPurchase; QByteArray responseBody; ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody, isTestPurchase); if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) { return errorCode; } apiV2->containers.clear(); m_serversRepository->editServer(serverId, apiV2->toJson(), serverConfigUtils::configTypeFromJson(apiV2->toJson())); return ErrorCode::NoError; } ErrorCode SubscriptionController::deactivateExternalDevice(const QString &serverId, const QString &uuid, const QString &serverCountryCode) { auto apiV2 = m_serversRepository->apiV2Config(serverId); if (!apiV2.has_value()) { return ErrorCode::NoError; } if (!apiV2->isPremium() && !apiV2->isExternalPremium()) { return ErrorCode::NoError; } QJsonObject authDataJson = apiV2->authData.toJson(); GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), m_appSettingsRepository->getAppLanguage().name().split("_").first(), uuid, apiV2->apiConfig.userCountryCode, serverCountryCode, apiV2->serviceType(), "", authDataJson }; QJsonObject apiPayload = gatewayRequestData.toJsonObject(); const bool isTestPurchase = apiV2->apiConfig.isTestPurchase; QByteArray responseBody; ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody, isTestPurchase); if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) { return errorCode; } if (uuid == m_appSettingsRepository->getInstallationUuid(true)) { apiV2->containers.clear(); m_serversRepository->editServer(serverId, apiV2->toJson(), serverConfigUtils::configTypeFromJson(apiV2->toJson())); } return ErrorCode::NoError; } ErrorCode SubscriptionController::exportNativeConfig(const QString &serverId, const QString &serverCountryCode, QString &nativeConfig) { auto apiV2 = m_serversRepository->apiV2Config(serverId); if (!apiV2.has_value()) { return ErrorCode::InternalError; } const bool isTestPurchase = apiV2->apiConfig.isTestPurchase; QString protocol = configKey::awg; ProtocolData protocolData = generateProtocolData(protocol); QJsonObject authDataJson = apiV2->authData.toJson(); GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), m_appSettingsRepository->getAppLanguage().name().split("_").first(), m_appSettingsRepository->getInstallationUuid(true), apiV2->apiConfig.userCountryCode, serverCountryCode, apiV2->serviceType(), protocol, authDataJson }; QJsonObject apiPayload = gatewayRequestData.toJsonObject(); appendProtocolDataToApiPayload(protocol, protocolData, apiPayload); QByteArray responseBody; ErrorCode errorCode = executeRequest(QString("%1v1/native_config"), apiPayload, responseBody, isTestPurchase); if (errorCode != ErrorCode::NoError) { return errorCode; } QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object(); nativeConfig = jsonConfig.value(configKey::config).toString(); nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey); return ErrorCode::NoError; } ErrorCode SubscriptionController::revokeNativeConfig(const QString &serverId, const QString &serverCountryCode) { auto apiV2 = m_serversRepository->apiV2Config(serverId); if (!apiV2.has_value()) { return ErrorCode::InternalError; } const bool isTestPurchase = apiV2->apiConfig.isTestPurchase; QString protocol = configKey::awg; QJsonObject authDataJson = apiV2->authData.toJson(); GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), m_appSettingsRepository->getAppLanguage().name().split("_").first(), m_appSettingsRepository->getInstallationUuid(true), apiV2->apiConfig.userCountryCode, serverCountryCode, apiV2->serviceType(), protocol, authDataJson }; QJsonObject apiPayload = gatewayRequestData.toJsonObject(); QByteArray responseBody; ErrorCode errorCode = executeRequest(QString("%1v1/revoke_native_config"), apiPayload, responseBody, isTestPurchase); if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) { return errorCode; } return ErrorCode::NoError; } ErrorCode SubscriptionController::prepareVpnKeyExport(const QString &serverId, QString &vpnKey) { auto apiV2 = m_serversRepository->apiV2Config(serverId); if (!apiV2.has_value()) { return ErrorCode::ApiConfigEmptyError; } vpnKey = apiV2->vpnKey(); if (vpnKey.isEmpty()) { vpnKey = apiUtils::getPremiumV2VpnKey(apiV2->toJson()); if (vpnKey.isEmpty()) { return ErrorCode::ApiConfigEmptyError; } apiV2->apiConfig.vpnKey = vpnKey; m_serversRepository->editServer(serverId, apiV2->toJson(), serverConfigUtils::configTypeFromJson(apiV2->toJson())); } return ErrorCode::NoError; } ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers) { if (!m_serversRepository->apiV2Config(serverId).has_value()) { return ErrorCode::NoError; } if (!hasInstalledContainers) { return updateServiceFromGateway(serverId, "", true); } if (isApiKeyExpired(serverId)) { qDebug() << "attempt to update api config by expires_at event"; return updateServiceFromGateway(serverId, "", true); } return ErrorCode::NoError; } void SubscriptionController::removeApiConfig(const QString &serverId) { auto apiV2 = m_serversRepository->apiV2Config(serverId); if (!apiV2.has_value()) { return; } #if defined(Q_OS_IOS) || defined(MACOS_NE) QString description = apiV2->description; QString hostName = apiV2->hostName; QString vpncName = QString("%1 (%2) %3") .arg(description) .arg(hostName) .arg(""); AmneziaVPN::removeVPNC(vpncName.toStdString()); #endif apiV2->dns1.clear(); apiV2->dns2.clear(); apiV2->containers.clear(); apiV2->hostName.clear(); apiV2->defaultContainer = DockerContainer::None; apiV2->apiConfig.publicKey = ApiConfig::PublicKeyInfo{}; m_serversRepository->editServer(serverId, apiV2->toJson(), serverConfigUtils::configTypeFromJson(apiV2->toJson())); } bool SubscriptionController::removeServer(const QString &serverId) { if (serverId.isEmpty()) { return false; } if (!m_serversRepository->apiV2Config(serverId).has_value()) { qWarning().noquote() << "SubscriptionController::removeServer: not an Api V2 server, id" << serverId; return false; } const ErrorCode revokeError = deactivateDevice(serverId); if (revokeError != ErrorCode::NoError && revokeError != ErrorCode::ApiNotFoundError) { qWarning().noquote() << "SubscriptionController::removeServer: deactivateDevice failed (error" << static_cast(revokeError) << "); removing locally anyway."; } m_serversRepository->removeServer(serverId); return true; } bool SubscriptionController::isApiKeyExpired(const QString &serverId) const { auto apiV2 = m_serversRepository->apiV2Config(serverId); if (!apiV2.has_value()) { return false; } const QString expiresAt = apiV2->apiConfig.publicKey.expiresAt; if (expiresAt.isEmpty()) { return false; } auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC(); if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) { return true; } return false; } void SubscriptionController::setCurrentProtocol(const QString &serverId, const QString &protocolName) { auto apiV2 = m_serversRepository->apiV2Config(serverId); if (apiV2.has_value()) { apiV2->apiConfig.serviceProtocol = protocolName; m_serversRepository->editServer(serverId, apiV2->toJson(), serverConfigUtils::configTypeFromJson(apiV2->toJson())); } } bool SubscriptionController::isVlessProtocol(const QString &serverId) const { auto apiV2 = m_serversRepository->apiV2Config(serverId); return apiV2.has_value() && apiV2->serviceProtocol() == "vless"; } ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol, const QString &productId, int *duplicateServerIndex) { #if defined(Q_OS_IOS) || defined(MACOS_NE) bool purchaseOk = false; QString originalTransactionId; QString storeTransactionId; QString storeProductId; QString purchaseError; QEventLoop waitPurchase; IosController::Instance()->purchaseProduct(productId, [&](bool success, const QString &txId, const QString &purchasedProductId, const QString &originalTxId, const QString &errorString) { purchaseOk = success; originalTransactionId = originalTxId; storeTransactionId = txId; storeProductId = purchasedProductId; purchaseError = errorString; waitPurchase.quit(); }); waitPurchase.exec(); if (!purchaseOk || originalTransactionId.isEmpty()) { qDebug() << "IAP purchase failed:" << purchaseError; return ErrorCode::ApiPurchaseError; } qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId << "originalTransactionId =" << originalTransactionId << "productId =" << storeProductId; bool isTestPurchase = IosController::Instance()->isTestFlight(); ProtocolData protocolData = generateProtocolData(serviceProtocol); return importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData, originalTransactionId, isTestPurchase, duplicateServerIndex); #else Q_UNUSED(userCountryCode); Q_UNUSED(serviceType); Q_UNUSED(serviceProtocol); Q_UNUSED(productId); return ErrorCode::ApiPurchaseError; #endif } SubscriptionController::AppStoreRestoreResult SubscriptionController::processAppStoreRestore(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol) { AppStoreRestoreResult result; #if defined(Q_OS_IOS) || defined(MACOS_NE) bool restoreSuccess = false; QList restoredTransactions; QString restoreError; QEventLoop waitRestore; IosController::Instance()->restorePurchases([&](bool success, const QList &transactions, const QString &errorString) { restoreSuccess = success; restoredTransactions = transactions; restoreError = errorString; waitRestore.quit(); }); waitRestore.exec(); if (!restoreSuccess) { qWarning().noquote() << "[IAP] Restore failed:" << restoreError; result.errorCode = ErrorCode::ApiPurchaseError; return result; } if (restoredTransactions.isEmpty()) { qInfo().noquote() << "[IAP] Restore completed, but no transactions were returned"; result.errorCode = ErrorCode::ApiNoPurchasedSubscriptionsError; return result; } bool isTestPurchase = IosController::Instance()->isTestFlight(); QSet processedTransactions; for (const QVariantMap &transaction : restoredTransactions) { const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString(); const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString(); const QString transactionProductId = transaction.value(QStringLiteral("productId")).toString(); if (originalTransactionId.isEmpty()) { qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId" << transactionId; continue; } if (processedTransactions.contains(originalTransactionId)) { result.duplicateCount++; continue; } processedTransactions.insert(originalTransactionId); qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId << "originalTransactionId =" << originalTransactionId << "productId =" << transactionProductId; ProtocolData protocolData = generateProtocolData(serviceProtocol); int currentDuplicateServerIndex = -1; ErrorCode errorCode = importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData, originalTransactionId, isTestPurchase, ¤tDuplicateServerIndex); if (errorCode == ErrorCode::ApiConfigAlreadyAdded) { result.duplicateConfigAlreadyPresent = true; if (result.duplicateServerIndex < 0) { result.duplicateServerIndex = currentDuplicateServerIndex; } qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId << "because subscription config with the same vpn_key already exists"; } else if (errorCode != ErrorCode::NoError) { qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId; result.errorCode = errorCode; } else { result.hasInstalledConfig = true; } } if (!result.hasInstalledConfig) { result.errorCode = result.duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError; } return result; #else Q_UNUSED(userCountryCode); Q_UNUSED(serviceType); Q_UNUSED(serviceProtocol); result.errorCode = ErrorCode::ApiPurchaseError; return result; #endif } ErrorCode SubscriptionController::getAccountInfo(const QString &serverId, QJsonObject &accountInfo) { auto apiV2 = m_serversRepository->apiV2Config(serverId); if (!apiV2.has_value()) { return ErrorCode::InternalError; } bool isTestPurchase = apiV2->apiConfig.isTestPurchase; QJsonObject authDataJson = apiV2->authData.toJson(); GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), m_appSettingsRepository->getAppLanguage().name().split("_").first(), m_appSettingsRepository->getInstallationUuid(true), apiV2->apiConfig.userCountryCode, "", apiV2->serviceType(), "", authDataJson }; QJsonObject apiPayload = gatewayRequestData.toJsonObject(); apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION); apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(apiV2->apiConfig); QByteArray responseBody; ErrorCode errorCode = executeRequest(QString("%1v1/account_info"), apiPayload, responseBody, isTestPurchase); if (errorCode != ErrorCode::NoError) { return errorCode; } accountInfo = QJsonDocument::fromJson(responseBody).object(); return ErrorCode::NoError; } QFuture> SubscriptionController::getRenewalLink(const QString &serverId) { auto promise = QSharedPointer>>::create(); promise->start(); auto apiV2 = m_serversRepository->apiV2Config(serverId); if (!apiV2.has_value()) { promise->addResult(qMakePair(ErrorCode::InternalError, QString())); promise->finish(); return promise->future(); } bool isTestPurchase = apiV2->apiConfig.isTestPurchase; QJsonObject authDataJson = apiV2->authData.toJson(); GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), m_appSettingsRepository->getAppLanguage().name().split("_").first(), m_appSettingsRepository->getInstallationUuid(true), apiV2->apiConfig.userCountryCode, "", apiV2->serviceType(), "", authDataJson }; QJsonObject apiPayload = gatewayRequestData.toJsonObject(); apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION); apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(apiV2->apiConfig); auto gatewayController = QSharedPointer::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase), m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), apiDefs::requestTimeoutMsecs, m_appSettingsRepository->isStrictKillSwitchEnabled()); auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload); auto *watcher = new QFutureWatcher>(); QObject::connect(watcher, &QFutureWatcher>::finished, [promise, watcher, gatewayController]() { const auto [errorCode, responseBody] = watcher->result(); watcher->deleteLater(); if (errorCode != ErrorCode::NoError) { promise->addResult(qMakePair(errorCode, QString())); promise->finish(); return; } QJsonObject responseJson = QJsonDocument::fromJson(responseBody).object(); const QString url = responseJson.value("renewal_url").toString(); promise->addResult(qMakePair(ErrorCode::NoError, url)); promise->finish(); }); watcher->setFuture(postFuture); return promise->future(); }