Files
amnezia-client/client/core/controllers/gatewayController.cpp
T

359 lines
14 KiB
C++
Raw Normal View History

#include "gatewayController.h"
2026-04-28 17:51:59 +03:00
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
2026-04-28 17:51:59 +03:00
#include <QMutexLocker>
#include <QSharedPointer>
2026-04-16 00:19:03 +03:00
#include <QThread>
2026-02-05 06:46:18 +03:00
#include <QtConcurrent>
#include "QBlockCipher.h"
#include "QRsa.h"
#include "amnezia_application.h"
2026-04-28 17:51:59 +03:00
#include "core/transport/dnsGatewayTransport.h"
#include "core/transport/httpGatewayTransport.h"
#include "utilities.h"
2025-05-02 23:54:36 -07:00
namespace
{
namespace configKey
{
constexpr char aesKey[] = "aes_key";
constexpr char aesIv[] = "aes_iv";
constexpr char aesSalt[] = "aes_salt";
constexpr char apiPayload[] = "api_payload";
constexpr char keyPayload[] = "key_payload";
}
2025-03-07 10:39:12 +07:00
2026-04-28 17:51:59 +03:00
amnezia::transport::dns::DnsProtocol dnsProtocolFromPrimary(PrimaryTransport p)
{
switch (p) {
case PrimaryTransport::DnsUdp: return amnezia::transport::dns::DnsProtocol::Udp;
case PrimaryTransport::DnsTcp: return amnezia::transport::dns::DnsProtocol::Tcp;
case PrimaryTransport::DnsDot: return amnezia::transport::dns::DnsProtocol::Tls;
case PrimaryTransport::DnsDoh: return amnezia::transport::dns::DnsProtocol::Https;
case PrimaryTransport::DnsDoq: return amnezia::transport::dns::DnsProtocol::Quic;
default: return amnezia::transport::dns::DnsProtocol::Udp;
}
}
} // namespace
2026-02-05 06:46:18 +03:00
TransportsConfig TransportsConfig::fromJson(const QJsonObject &json)
{
2026-04-28 17:51:59 +03:00
using amnezia::transport::dns::DnsProtocol;
2026-02-05 06:46:18 +03:00
TransportsConfig config;
2026-04-28 17:51:59 +03:00
2026-02-05 06:46:18 +03:00
QString primaryStr = json.value("primary").toString("http").toLower();
if (primaryStr == "http") {
config.primary = PrimaryTransport::Http;
} else if (primaryStr == "dns_udp" || primaryStr == "udp") {
config.primary = PrimaryTransport::DnsUdp;
} else if (primaryStr == "dns_tcp" || primaryStr == "tcp") {
config.primary = PrimaryTransport::DnsTcp;
} else if (primaryStr == "dns_dot" || primaryStr == "dot") {
config.primary = PrimaryTransport::DnsDot;
} else if (primaryStr == "dns_doh" || primaryStr == "doh") {
config.primary = PrimaryTransport::DnsDoh;
} else if (primaryStr == "dns_doq" || primaryStr == "doq") {
config.primary = PrimaryTransport::DnsDoq;
}
2026-04-28 17:51:59 +03:00
2026-02-05 06:46:18 +03:00
config.retryCount = json.value("retry_count").toInt(3);
config.timeoutMs = json.value("timeout_ms").toInt(10000);
2026-04-28 17:51:59 +03:00
2026-02-05 06:46:18 +03:00
if (json.contains("http")) {
QJsonObject httpObj = json["http"].toObject();
config.httpEnabled = httpObj.value("enabled").toBool(true);
config.httpEndpoint = httpObj.value("endpoint").toString();
}
2026-04-28 17:51:59 +03:00
2026-02-05 06:46:18 +03:00
if (json.contains("dns_transports")) {
QJsonArray transportsArray = json["dns_transports"].toArray();
for (const auto &transportVal : transportsArray) {
QJsonObject transportObj = transportVal.toObject();
DnsTransportEntry entry;
2026-04-28 17:51:59 +03:00
2026-02-05 06:46:18 +03:00
entry.server = transportObj.value("server").toString();
entry.domain = transportObj.value("domain").toString();
entry.port = static_cast<quint16>(transportObj.value("port").toInt(15353));
entry.dohPath = transportObj.value("path").toString("/dns-query");
2026-04-28 17:51:59 +03:00
2026-02-05 06:46:18 +03:00
QString typeStr = transportObj.value("type").toString().toLower();
if (typeStr == "udp") {
2026-04-28 17:51:59 +03:00
entry.type = DnsProtocol::Udp;
2026-02-05 06:46:18 +03:00
} else if (typeStr == "tcp") {
2026-04-28 17:51:59 +03:00
entry.type = DnsProtocol::Tcp;
2026-02-05 06:46:18 +03:00
} else if (typeStr == "dot" || typeStr == "tls") {
2026-04-28 17:51:59 +03:00
entry.type = DnsProtocol::Tls;
if (!transportObj.contains("port")) entry.port = 8853;
2026-02-05 06:46:18 +03:00
} else if (typeStr == "doh" || typeStr == "https") {
2026-04-28 17:51:59 +03:00
entry.type = DnsProtocol::Https;
if (!transportObj.contains("port")) entry.port = 443;
2026-02-05 06:46:18 +03:00
} else if (typeStr == "doq" || typeStr == "quic") {
2026-04-28 17:51:59 +03:00
entry.type = DnsProtocol::Quic;
if (!transportObj.contains("port")) entry.port = 8853;
2026-02-05 06:46:18 +03:00
} else {
2026-04-28 17:51:59 +03:00
continue;
2026-02-05 06:46:18 +03:00
}
2026-04-28 17:51:59 +03:00
2026-02-05 06:46:18 +03:00
if (entry.isValid()) {
config.dnsTransports.append(entry);
}
}
}
2026-04-28 17:51:59 +03:00
2026-02-05 06:46:18 +03:00
return config;
}
2026-04-28 17:51:59 +03:00
GatewayController::GatewayController(const QString &gatewayEndpoint,
const bool isDevEnvironment,
const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled,
QObject *parent)
: QObject(parent),
2026-04-28 17:51:59 +03:00
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
{
2026-04-28 17:51:59 +03:00
auto httpTransport = std::make_shared<amnezia::transport::HttpGatewayTransport>(
m_gatewayEndpoint, m_isDevEnvironment, m_requestTimeoutMsecs, m_isStrictKillSwitchEnabled);
{
QMutexLocker lock(&m_transportMutex);
m_transport = std::move(httpTransport);
}
}
2026-04-28 17:51:59 +03:00
std::shared_ptr<amnezia::transport::IGatewayTransport> GatewayController::buildTransport(
const TransportsConfig &config, int requestTimeoutMsecs, bool isDevEnvironment, bool isStrictKillSwitchEnabled)
2026-01-20 17:37:22 +03:00
{
2026-04-28 17:51:59 +03:00
using namespace amnezia::transport;
auto makeHttp = [&](const QString &httpEndpoint) {
return std::make_shared<HttpGatewayTransport>(
httpEndpoint, isDevEnvironment, requestTimeoutMsecs, isStrictKillSwitchEnabled);
};
if (config.primary == PrimaryTransport::Http) {
return makeHttp(config.httpEndpoint);
}
const auto wantedProtocol = dnsProtocolFromPrimary(config.primary);
for (const auto &entry : config.dnsTransports) {
if (entry.type == wantedProtocol && entry.isValid()) {
return std::make_shared<DnsGatewayTransport>(
entry.type, entry.server, entry.domain, entry.port,
requestTimeoutMsecs, isStrictKillSwitchEnabled, entry.dohPath);
}
2026-01-20 17:37:22 +03:00
}
2026-04-28 17:51:59 +03:00
return makeHttp(config.httpEndpoint);
2026-01-20 17:37:22 +03:00
}
2026-02-05 06:46:18 +03:00
void GatewayController::setTransportsConfig(const TransportsConfig &config)
{
2026-04-28 17:51:59 +03:00
if (config.timeoutMs > 0) {
m_requestTimeoutMsecs = config.timeoutMs;
}
2026-02-05 06:46:18 +03:00
if (!config.httpEndpoint.isEmpty()) {
m_gatewayEndpoint = config.httpEndpoint;
}
2026-04-28 17:51:59 +03:00
TransportsConfig effective = config;
if (effective.httpEndpoint.isEmpty()) {
effective.httpEndpoint = m_gatewayEndpoint;
}
auto newTransport = buildTransport(effective, m_requestTimeoutMsecs, m_isDevEnvironment, m_isStrictKillSwitchEnabled);
QString activeName;
{
QMutexLocker lock(&m_transportMutex);
m_transport = std::move(newTransport);
activeName = m_transport ? m_transport->name() : QStringLiteral("none");
2026-02-05 06:46:18 +03:00
}
2026-04-28 17:51:59 +03:00
qDebug() << "[Transport] Active transport set to" << activeName;
2026-02-05 06:46:18 +03:00
}
2026-04-17 13:56:06 +03:00
TransportsConfig GatewayController::buildTransportsConfig()
2026-02-05 06:46:18 +03:00
{
2026-04-28 17:51:59 +03:00
using amnezia::transport::dns::DnsProtocol;
2026-04-17 13:56:06 +03:00
TransportsConfig config;
2026-04-28 17:51:59 +03:00
QString server = QString(AGW_DNS_SERVER).trimmed();
QString domain = QString(AGW_DNS_DOMAIN).trimmed();
2026-04-17 13:56:06 +03:00
if (server.isEmpty() || domain.isEmpty()) {
qDebug() << "[Transport] DNS server/domain not configured, HTTP only";
return config;
2026-02-05 06:46:18 +03:00
}
2026-04-17 13:56:06 +03:00
2026-04-28 17:51:59 +03:00
QString primaryStr = QString(AGW_DNS_PRIMARY).trimmed().toLower();
2026-04-17 13:56:06 +03:00
if (primaryStr == "udp" || primaryStr == "dns_udp") {
config.primary = PrimaryTransport::DnsUdp;
} else if (primaryStr == "tcp" || primaryStr == "dns_tcp") {
config.primary = PrimaryTransport::DnsTcp;
} else if (primaryStr == "dot" || primaryStr == "dns_dot") {
config.primary = PrimaryTransport::DnsDot;
} else if (primaryStr == "doh" || primaryStr == "dns_doh") {
config.primary = PrimaryTransport::DnsDoh;
} else if (primaryStr == "doq" || primaryStr == "dns_doq") {
config.primary = PrimaryTransport::DnsDoq;
} else {
config.primary = PrimaryTransport::Http;
2026-02-05 06:46:18 +03:00
}
2026-04-17 13:56:06 +03:00
2026-04-28 17:51:59 +03:00
int retryCount = QString(AGW_DNS_RETRY_COUNT).trimmed().toInt();
2026-04-17 13:56:06 +03:00
config.retryCount = (retryCount > 0) ? retryCount : 3;
2026-04-28 17:51:59 +03:00
int timeoutMs = QString(AGW_DNS_TIMEOUT_MS).trimmed().toInt();
2026-04-17 13:56:06 +03:00
config.timeoutMs = (timeoutMs > 0) ? timeoutMs : 10000;
config.httpEnabled = true;
2026-04-28 17:51:59 +03:00
auto addTransport = [&](DnsProtocol type, const char *portDefine, quint16 defaultPort,
2026-04-17 13:56:06 +03:00
const QString &dohPath = QString()) {
DnsTransportEntry entry;
entry.type = type;
entry.server = server;
entry.domain = domain;
2026-04-28 17:51:59 +03:00
quint16 port = QString(portDefine).trimmed().toUShort();
2026-04-17 13:56:06 +03:00
entry.port = (port > 0) ? port : defaultPort;
if (!dohPath.isEmpty()) entry.dohPath = dohPath;
config.dnsTransports.append(entry);
};
2026-04-28 17:51:59 +03:00
addTransport(DnsProtocol::Udp, AGW_DNS_PORT_UDP, 5353);
addTransport(DnsProtocol::Tcp, AGW_DNS_PORT_UDP, 5353);
addTransport(DnsProtocol::Tls, AGW_DNS_PORT_DOT, 853);
2026-04-17 13:56:06 +03:00
2026-04-28 17:51:59 +03:00
QString dohPath = QString(AGW_DNS_DOH_PATH).trimmed();
2026-04-17 13:56:06 +03:00
if (dohPath.isEmpty()) dohPath = "/dns-query";
2026-04-28 17:51:59 +03:00
addTransport(DnsProtocol::Https, AGW_DNS_PORT_DOH, 443, dohPath);
2026-04-17 13:56:06 +03:00
2026-04-28 17:51:59 +03:00
addTransport(DnsProtocol::Quic, AGW_DNS_PORT_DOQ, 8853);
2026-04-17 13:56:06 +03:00
qDebug() << "[Transport] Built config from env: server=" << server << "domain=" << domain
<< "transports=" << config.dnsTransports.size() << "primary=" << static_cast<int>(config.primary);
return config;
2026-02-05 06:46:18 +03:00
}
2026-04-28 17:51:59 +03:00
GatewayController::EncryptedRequest GatewayController::encryptRequest(const QJsonObject &apiPayload)
{
2026-04-28 17:51:59 +03:00
EncryptedRequest result;
result.errorCode = amnezia::ErrorCode::NoError;
2025-05-02 23:54:36 -07:00
QSimpleCrypto::QBlockCipher blockCipher;
2026-04-28 17:51:59 +03:00
result.key = blockCipher.generatePrivateSalt(32);
result.iv = blockCipher.generatePrivateSalt(16);
result.salt = blockCipher.generatePrivateSalt(8);
QJsonObject keyPayload;
2026-04-28 17:51:59 +03:00
keyPayload[configKey::aesKey] = QString(result.key.toBase64());
keyPayload[configKey::aesIv] = QString(result.iv.toBase64());
keyPayload[configKey::aesSalt] = QString(result.salt.toBase64());
QByteArray encryptedKeyPayload;
QByteArray encryptedApiPayload;
try {
QSimpleCrypto::QRsa rsa;
EVP_PKEY *publicKey = nullptr;
try {
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
2026-04-28 17:51:59 +03:00
rsaKey = rsaKey.trimmed();
2026-04-16 00:19:03 +03:00
rsaKey.replace("\\n", "\n");
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
} catch (...) {
Utils::logException();
qCritical() << "error loading public key from environment variables";
2026-04-28 17:51:59 +03:00
result.errorCode = amnezia::ErrorCode::ApiMissingAgwPublicKey;
return result;
}
2026-04-28 17:51:59 +03:00
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(QJsonDocument::Compact),
publicKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(publicKey);
2026-04-28 17:51:59 +03:00
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(QJsonDocument::Compact),
result.key, result.iv, "", result.salt);
} catch (...) {
Utils::logException();
qCritical() << "error when encrypting the request body";
2026-04-28 17:51:59 +03:00
result.errorCode = amnezia::ErrorCode::ApiConfigDecryptionError;
return result;
}
QJsonObject requestBody;
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64());
2026-04-28 17:51:59 +03:00
result.body = QJsonDocument(requestBody).toJson(QJsonDocument::Compact);
return result;
}
2026-04-28 17:51:59 +03:00
amnezia::transport::DecryptionResult GatewayController::decryptResponse(const QByteArray &encryptedResponseBody,
const QByteArray &key,
const QByteArray &iv,
const QByteArray &salt) const
2025-12-29 19:18:03 +08:00
{
2026-04-28 17:51:59 +03:00
amnezia::transport::DecryptionResult result;
result.decrypted = encryptedResponseBody;
result.isOk = false;
if (encryptedResponseBody.isEmpty()) {
return result;
}
2025-12-29 19:18:03 +08:00
try {
QSimpleCrypto::QBlockCipher blockCipher;
2026-04-28 17:51:59 +03:00
result.decrypted = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
result.isOk = true;
2025-12-29 19:18:03 +08:00
} catch (...) {
2026-04-28 17:51:59 +03:00
result.decrypted = encryptedResponseBody;
result.isOk = false;
2025-12-29 19:18:03 +08:00
}
return result;
}
2026-04-28 17:51:59 +03:00
std::shared_ptr<amnezia::transport::IGatewayTransport> GatewayController::currentTransport() const
{
2026-04-28 17:51:59 +03:00
QMutexLocker lock(&m_transportMutex);
return m_transport;
}
2026-04-28 17:51:59 +03:00
amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
2026-04-28 17:51:59 +03:00
EncryptedRequest enc = encryptRequest(apiPayload);
if (enc.errorCode != amnezia::ErrorCode::NoError) {
return enc.errorCode;
2025-12-29 19:18:03 +08:00
}
2026-04-28 17:51:59 +03:00
auto transport = currentTransport();
if (!transport) {
return amnezia::ErrorCode::AmneziaServiceConnectionFailed;
}
2026-04-28 17:51:59 +03:00
auto decryptionHook = [this, key = enc.key, iv = enc.iv, salt = enc.salt](const QByteArray &encrypted) {
return decryptResponse(encrypted, key, iv, salt);
2025-08-20 13:00:35 +08:00
};
2026-04-28 17:51:59 +03:00
return transport->send(endpoint, enc.body, responseBody, decryptionHook);
2025-11-18 00:21:02 +08:00
}
2026-04-28 17:51:59 +03:00
QFuture<QPair<amnezia::ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
2025-11-18 00:21:02 +08:00
{
2026-04-28 17:51:59 +03:00
return QtConcurrent::run([this, endpoint, apiPayload]() {
QByteArray responseBody;
amnezia::ErrorCode errorCode = post(endpoint, apiPayload, responseBody);
return qMakePair(errorCode, responseBody);
2025-11-18 00:21:02 +08:00
});
}