mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-23 02:00:20 +07:00
fixed open Qr QML & add check error code & add test
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user