fixed open Qr QML & add check error code & add test

This commit is contained in:
dranik
2026-05-07 19:15:28 +03:00
parent 2cb7b30d8a
commit 5583c0a2a9
20 changed files with 884 additions and 76 deletions
@@ -16,11 +16,16 @@ namespace
{
constexpr auto kGenerateQrEndpoint = "%1api/v1/generate_qr";
constexpr auto kScanQrEndpoint = "%1api/v1/scan_qr";
constexpr qsizetype kPairingMaxQrUuidChars = 128;
constexpr qsizetype kPairingMaxVpnConfigChars = 256 * 1024;
constexpr qsizetype kPairingMaxApiKeyChars = 8192;
bool isLocalGatewayHost(const QString &gatewayUrl)
{
return gatewayUrl.contains(QStringLiteral("127.0.0.1"), Qt::CaseInsensitive)
|| gatewayUrl.contains(QStringLiteral("localhost"), Qt::CaseInsensitive);
|| gatewayUrl.contains(QStringLiteral("localhost"), Qt::CaseInsensitive)
|| gatewayUrl.contains(QStringLiteral("[::1]"), Qt::CaseInsensitive)
|| gatewayUrl.contains(QStringLiteral("::1"), Qt::CaseInsensitive);
}
ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
@@ -118,6 +123,20 @@ ErrorCode PairingController::parseScanQrResponseBody(const QByteArray &responseB
return interpretScanQrJson(obj);
}
ErrorCode PairingController::validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey)
{
if (qrUuid.size() > kPairingMaxQrUuidChars) {
return ErrorCode::ApiConfigEmptyError;
}
if (vpnConfig.size() > kPairingMaxVpnConfigChars) {
return ErrorCode::ApiPairingPayloadTooLargeError;
}
if (apiKey.size() > kPairingMaxApiKeyChars) {
return ErrorCode::ApiPairingPayloadTooLargeError;
}
return ErrorCode::NoError;
}
PairingController::PairingController(SecureAppSettingsRepository *appSettingsRepository)
: m_appSettingsRepository(appSettingsRepository)
{
@@ -187,6 +206,11 @@ ErrorCode PairingController::completePairing(const QString &qrUuid, const QStrin
return ErrorCode::ApiConfigEmptyError;
}
const ErrorCode fieldErr = validatePairingScanFields(qrUuid, vpnConfig, apiKey);
if (fieldErr != ErrorCode::NoError) {
return fieldErr;
}
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, m_appSettingsRepository->isStrictKillSwitchEnabled());
@@ -34,6 +34,9 @@ public:
static amnezia::ErrorCode parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload);
static amnezia::ErrorCode parseScanQrResponseBody(const QByteArray &responseBody);
/** Length bounds before `scan_qr` (avoids huge JSON / abuse). */
static amnezia::ErrorCode validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey);
amnezia::ErrorCode startPairing(const QString &qrUuid, QrPairingConfigPayload &outPayload);
amnezia::ErrorCode completePairing(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, const QString &apiKey);
@@ -312,6 +312,83 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::importServerFromQrPairingResponse(const QString &vpnConfigKey, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols,
ServerConfig &serverConfig, int *duplicateServerIndex)
{
if (vpnConfigKey.isEmpty()) {
return ErrorCode::ApiConfigEmptyError;
}
QString normalizedKey = vpnConfigKey;
normalizedKey.replace(QStringLiteral("vpn://"), QString());
for (int i = 0; i < m_serversRepository->serversCount(); ++i) {
ServerConfig existingServerConfig = m_serversRepository->server(i);
QString existingVpnKey;
if (existingServerConfig.isApiV1()) {
const ApiV1ServerConfig *apiV1 = existingServerConfig.as<ApiV1ServerConfig>();
existingVpnKey = apiV1 ? apiV1->vpnKey() : QString();
} else if (existingServerConfig.isApiV2()) {
const ApiV2ServerConfig *apiV2 = existingServerConfig.as<ApiV2ServerConfig>();
existingVpnKey = apiV2 ? apiV2->vpnKey() : QString();
}
existingVpnKey.replace(QStringLiteral("vpn://"), QString());
if (!existingVpnKey.isEmpty() && existingVpnKey == normalizedKey) {
if (duplicateServerIndex) {
*duplicateServerIndex = i;
}
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()) {
return ErrorCode::ApiConfigEmptyError;
}
QJsonObject serverJson = QJsonDocument::fromJson(configString).object();
if (serverJson.isEmpty()) {
return ErrorCode::ApiConfigEmptyError;
}
if (serverJson.value(configKey::configVersion).toInt() != apiDefs::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
QJsonObject apiConfig = serverJson.value(apiDefs::key::apiConfig).toObject();
if (!serviceInfo.isEmpty()) {
apiConfig.insert(apiDefs::key::serviceInfo, serviceInfo);
}
if (!supportedProtocols.isEmpty()) {
apiConfig.insert(apiDefs::key::supportedProtocols, supportedProtocols);
}
serverJson[apiDefs::key::apiConfig] = apiConfig;
ServerConfig serverConfigModel = ServerConfig::fromJson(serverJson);
if (!serverConfigModel.isApiV2()) {
return ErrorCode::InternalError;
}
ApiV2ServerConfig *apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (apiV2 && apiV2->apiConfig.vpnKey.isEmpty()) {
QString fullKey = vpnConfigKey.trimmed();
if (!fullKey.startsWith(QStringLiteral("vpn://"))) {
fullKey = QStringLiteral("vpn://") + fullKey;
}
apiV2->apiConfig.vpnKey = fullKey;
}
m_serversRepository->addServer(serverConfigModel);
serverConfig = serverConfigModel;
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &transactionId, bool isTestPurchase,
@@ -1,6 +1,7 @@
#ifndef SUBSCRIPTIONCONTROLLER_H
#define SUBSCRIPTIONCONTROLLER_H
#include <QJsonArray>
#include <QJsonObject>
#include <QByteArray>
#include <QFuture>
@@ -57,6 +58,11 @@ public:
const QString &serviceProtocol, const QString &email,
ServerConfig &serverConfig);
/** Decode premium API (vpn://) bundle from QR pairing TV response, merge gateway fields, add server. */
ErrorCode importServerFromQrPairingResponse(const QString &vpnConfigKey, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, ServerConfig &serverConfig,
int *duplicateServerIndex = nullptr);
ErrorCode importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &transactionId, bool isTestPurchase,
+39 -1
View File
@@ -86,6 +86,18 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
}
#endif
#ifdef AMNEZIA_LOCAL_GATEWAY
{
const QUrl gatewayUrl(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl);
const QString host = gatewayUrl.host().toLower();
if (host == QLatin1String("localhost") || host == QLatin1String("127.0.0.1") || host == QLatin1String("::1")) {
encRequestData.isPlaintextLocalGateway = true;
encRequestData.requestBody = QJsonDocument(apiPayload).toJson();
return encRequestData;
}
}
#endif
QSimpleCrypto::QBlockCipher blockCipher;
encRequestData.key = blockCipher.generatePrivateSalt(32);
encRequestData.iv = blockCipher.generatePrivateSalt(32);
@@ -176,6 +188,16 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
reply->deleteLater();
if (encRequestData.isPlaintextLocalGateway) {
const auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
if (errorCode) {
return errorCode;
}
responseBody = encryptedResponseBody;
return ErrorCode::NoError;
}
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
@@ -223,7 +245,8 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
return ErrorCode::NoError;
}
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject &apiPayload,
QNetworkReply **activeReplyOut)
{
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
promise->start();
@@ -236,6 +259,9 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
}
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
if (activeReplyOut) {
*activeReplyOut = reply;
}
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
@@ -249,6 +275,18 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
reply->deleteLater();
if (encRequestData.isPlaintextLocalGateway) {
const auto errorCode = apiUtils::checkNetworkReplyErrors(*sslErrors, replyErrorString, replyError, httpStatusCode,
encryptedResponseBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray()));
} else {
promise->addResult(qMakePair(ErrorCode::NoError, encryptedResponseBody));
}
promise->finish();
return;
}
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
+4 -1
View File
@@ -25,7 +25,9 @@ public:
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
/** If \a activeReplyOut is non-null, the underlying QNetworkReply is written for abort/cancel (not owned by caller). */
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject &apiPayload,
QNetworkReply **activeReplyOut = nullptr);
private:
struct EncryptedRequestData
@@ -36,6 +38,7 @@ private:
QByteArray iv;
QByteArray salt;
amnezia::ErrorCode errorCode;
bool isPlaintextLocalGateway = false;
};
struct DecryptionResult