From 9fa58b4f83fff4602b48df7f3d5453a2fd8eeab1 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 03:14:00 +0100 Subject: [PATCH] Keep desktop config projection separate from core settings --- tools/chatgui/CMakeLists.txt | 4 + tools/chatgui/chatgui_doc.md | 19 +- tools/chatgui/src/main.cpp | 22 +- tools/chatgui/tests/test_gui_language.cpp | 12 +- tools/chatgui/tests/test_node_config.cpp | 182 ++++++- tools/chatgui/transport/node_config.cpp | 634 ++++++---------------- tools/chatgui/transport/node_config.h | 86 ++- 7 files changed, 412 insertions(+), 547 deletions(-) diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index fe3efa9e..e30e36d6 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -599,6 +599,10 @@ add_test(NAME test_config_updater COMMAND test_config_updater) add_executable(test_node_config tests/test_node_config.cpp transport/node_config.cpp transport/config_updater.cpp) target_link_libraries(test_node_config PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun pthread) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_compile_definitions(test_node_config PRIVATE UTUN_TEST_NODE_CONFIG_FAILURES) + target_link_options(test_node_config PRIVATE -Wl,--wrap=sc_generate_keypair -Wl,--wrap=sc_derive_node_id_from_pubkey) +endif() add_test(NAME test_node_config COMMAND test_node_config) set_tests_properties(test_node_config PROPERTIES TIMEOUT 30) diff --git a/tools/chatgui/chatgui_doc.md b/tools/chatgui/chatgui_doc.md index 3f293fd8..efb52785 100644 --- a/tools/chatgui/chatgui_doc.md +++ b/tools/chatgui/chatgui_doc.md @@ -129,17 +129,18 @@ X11 idle time в Wayland-сессии не используется: он не ### 3.2. Транспортный слой **UtunNode** (`transport/utun_node.h`) — C++ обёртка над UTUN_INSTANCE: -- Запускает выделенный std::thread с `runLoop()` — создаёт UTUN_INSTANCE, вызывает `utun_instance_start()`, входит в uasync event loop. -- Приём сообщений: ETCP callback → `QByteArray` → сигнал `messageReceived(uint64_t src, QByteArray data)`. -- Отправка: `send(dstNodeId, data)` через ETCP поверх экземпляра uTun. -- Конфигурация: NodeConfig (INI-файл), генерация X25519/Ed25519 ключей при первом запуске. -- Включает отладку NTP-синхронизации для временных меток сообщений. +- В `runLoop()` создаёт uasync и UTUN_INSTANCE, явно запускает core/chat и проверяет медиапривязки; `[gui] utun_enabled=yes` добавляет UTUN. +- `start()` подтверждает готовность или возвращает причину отказа с откатом ресурсов. Команды и события чата проходят через GuiBridge. +- `stop()` просит выход из poll, ждёт worker; instance освобождает worker, затем GUI освобождает uasync. +- Идентичность и NTP доступны GUI через защищённую копию состояния, без чтения живых структур ядра. +- Перед чтением существующего профиля desktop вызывает общий `config_ensure_keys_and_node_id()`: корректный X25519 private key сохраняется, public key и node_id восстанавливаются из него. **NodeConfig** (`transport/node_config.h`) — управление INI-конфигом uTun-узла: -- Генерация ключей (X25519 + Ed25519), сохранение/загрузка. -- Секции `[server]` и `[client]` для подключения к другим узлам. -- `[gui]` секция: путь к БД, настройки отладки. -- ConfigUpdater — утилита для программного изменения конфиг-файлов (append/replace). +- `create(name)` создаёт новый профиль с X25519-ключами, производным node_id, автосокетами TCP+UDP и control 127.0.0.1:9999. Ошибки генерации/сохранения возвращаются вызывающему. +- `load()` читает собственную GUI-проекцию без DNS и заменяет её целиком; удалённые поля не остаются от предыдущего чтения. Ошибки полей доступны через `hasErrors()/errors()`. +- Сетевые секции, включая `[server:name]` и `[client:name]`, проверяет общий парсер в потоке ядра. GUI не содержит отдельного списка допустимых сетевых опций. +- `save()` обновляет поля идентичности, control и `[gui]`/`[debug]`, сохраняя остальные секции и комментарии. Неиспользуемый режим полной перезаписи из неполной модели удалён. +- ConfigUpdater проверяет UTF-8/физический INI-формат и публикует весь набор правок через QSaveFile; replace обновляет все дубли, append добавляет значение. Отказ записи не усекает исходный файл. **ChatCore** (`transport/chat_core.h`) — центральный API чата в потоке uasync: - `chat_core_submit_message` — отправка сообщения: сохранение в локальную БД (таблица `msg_`), рассылка онлайн-пирам через etcp_send. diff --git a/tools/chatgui/src/main.cpp b/tools/chatgui/src/main.cpp index adf26dc4..c0524d3e 100644 --- a/tools/chatgui/src/main.cpp +++ b/tools/chatgui/src/main.cpp @@ -25,6 +25,7 @@ #include "desktop_integration.h" #include "gui_log.h" #include "config_parser.h" +#include "../../../src/config_updater.h" #include #include #include @@ -199,10 +200,10 @@ int main(int argc, char *argv[]) { QFile::rename(cfgDir + "/chatgui.cfg", cfgPath); NodeConfig cfg(cfgPath); - if (cfg.exists() && !cfg.load()) { - qCritical("Cannot open/read config file: %s", qPrintable(cfgPath)); + if (cfg.exists() && (config_ensure_keys_and_node_id(cfgPath.toUtf8().constData()) != 0 || !cfg.load())) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot prepare/read GUI configuration: %s", qPrintable(cfgPath)); QMessageBox failure(QMessageBox::Critical, "vibechat", - QCoreApplication::translate("ChatGui", "Cannot read configuration:\n%1").arg(cfgPath), QMessageBox::Ok); + QCoreApplication::translate("ChatGui", "Invalid configuration:\n%1").arg(cfgPath), QMessageBox::Ok); instance.setWindow(&failure); failure.exec(); instance.setWindow(nullptr); @@ -227,7 +228,6 @@ int main(int argc, char *argv[]) { }; QString language = resolveGuiLanguage(cfg.language()); if (!loadLanguage(language)) return 1; - bool needSave = false; const bool firstRun = !cfg.exists(); if (firstRun) { @@ -299,7 +299,7 @@ int main(int argc, char *argv[]) { return 0; } cfg.setLanguage(languageCombo->currentData().toString()); - if (!cfg.create(nickname->text().trimmed(), 1333)) { + if (!cfg.create(nickname->text().trimmed())) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "First-run setup: failed to create config=%s", qPrintable(cfgPath)); QMessageBox::critical(nullptr, wizard.windowTitle(), QCoreApplication::translate("ChatGui", "Failed to save your profile. Check that the data folder is writable.")); @@ -317,18 +317,6 @@ int main(int argc, char *argv[]) { instance.setWindow(nullptr); return 1; } - if (cfg.privKey().length() != 64 || cfg.pubKey().length() != 64) { - qWarning("Config: keys missing/invalid, regenerating identity"); - cfg.generateIdentity(); - needSave = true; - } - if (cfg.nodeId().length() != 16) { - qWarning("Config: my_node_id missing/invalid, regenerating identity"); - cfg.generateIdentity(); - needSave = true; - } - if (needSave) - cfg.save(); } QString dbPath = cfg.dbPath(); diff --git a/tools/chatgui/tests/test_gui_language.cpp b/tools/chatgui/tests/test_gui_language.cpp index 9e7fe9e0..61300403 100644 --- a/tools/chatgui/tests/test_gui_language.cpp +++ b/tools/chatgui/tests/test_gui_language.cpp @@ -25,12 +25,12 @@ private slots: void utunPolicyPersistence() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("utun.cfg"); - NodeConfig config(path); QVERIFY(config.create("Chat only", 1333)); + NodeConfig config(path); QVERIFY(config.create("Chat only")); QVERIFY(!config.utunEnabled()); config.setUtunEnabled(true); QVERIFY(config.save()); NodeConfig restored(path); QVERIFY(restored.load()); QVERIFY(!restored.hasErrors() && restored.utunEnabled()); - QVERIFY(restored.saveFull() && restored.load() && restored.utunEnabled()); + QVERIFY(restored.save() && restored.load() && restored.utunEnabled()); QVERIFY(config_update_option(path, "gui", "utun_enabled", "no")); QVERIFY(restored.load() && !restored.utunEnabled()); QVERIFY(config_update_option(path, "gui", "utun_enabled", "invalid")); @@ -109,7 +109,7 @@ private slots: QVERIFY(directory.isValid()); const QString path = directory.filePath("protocols.cfg"); NodeConfig config(path); - QVERIFY(config.create("Protocol test", 1333)); + QVERIFY(config.create("Protocol test")); NetworkSettingsPage page; page.loadFromConfig(path); auto* protocols = page.findChild("autoSocketProtocols"); QVERIFY(protocols && protocols->isEnabled()); @@ -122,7 +122,7 @@ private slots: QCOMPARE(config.autoSocketProtocols(), value); QVERIFY(config.save()); QVERIFY(config.load()); QCOMPARE(config.autoSocketProtocols(), value); - QVERIFY(config.saveFull()); + QVERIFY(config.save()); QVERIFY(config.load()); QCOMPARE(config.autoSocketProtocols(), value); page.loadFromConfig(path); QCOMPARE(protocols->currentData().toString(), value); @@ -147,7 +147,7 @@ private slots: QVERIFY(directory.isValid()); const QString path = directory.filePath("vibechat.cfg"); NodeConfig config(path); - QVERIFY(config.create("Language test", 1333)); + QVERIFY(config.create("Language test")); QCOMPARE(config.language(), QString("system")); for (const QString& language : {QString("ru"), QString("en"), QString("system")}) { QVERIFY(config_update_option(path, "gui", "language", language)); @@ -158,7 +158,7 @@ private slots: NodeConfig reloaded(path); QVERIFY(reloaded.load()); QCOMPARE(reloaded.language(), language); - QVERIFY(reloaded.saveFull()); + QVERIFY(reloaded.save()); QVERIFY(reloaded.load()); QCOMPARE(reloaded.language(), language); } diff --git a/tools/chatgui/tests/test_node_config.cpp b/tools/chatgui/tests/test_node_config.cpp index c0b06078..7fd81293 100644 --- a/tools/chatgui/tests/test_node_config.cpp +++ b/tools/chatgui/tests/test_node_config.cpp @@ -5,19 +5,191 @@ #include #include #include +#ifdef Q_OS_UNIX +#include +#include +#endif extern "C" { #include "config_parser.h" #include "chat/chat_setting.h" #include "socket_compat.h" #include "debug_config.h" +#include "../../../src/config_updater.h" } +#ifdef UTUN_TEST_NODE_CONFIG_FAILURES +static bool failKeyGeneration = false, failNodeId = false; +extern "C" sc_status_t __real_sc_generate_keypair(struct SC_MYKEYS *keys); +extern "C" sc_status_t __wrap_sc_generate_keypair(struct SC_MYKEYS *keys) { + return failKeyGeneration ? SC_ERR_CRYPTO : __real_sc_generate_keypair(keys); +} +extern "C" uint64_t __real_sc_derive_node_id_from_pubkey(const uint8_t *publicKey); +extern "C" uint64_t __wrap_sc_derive_node_id_from_pubkey(const uint8_t *publicKey) { + return failNodeId ? 0 : __real_sc_derive_node_id_from_pubkey(publicKey); +} +#endif + using Config = std::unique_ptr; class NodeConfigTest : public QObject { Q_OBJECT private slots: + void identityFailureKeepsPreviousValues_data() { + QTest::addColumn("nodeIdFailure"); + QTest::newRow("keypair") << false; + QTest::newRow("node-id") << true; + } + void identityFailureKeepsPreviousValues() { +#ifdef UTUN_TEST_NODE_CONFIG_FAILURES + QFETCH(bool, nodeIdFailure); + QTemporaryDir directory; QVERIFY(directory.isValid()); + NodeConfig config(directory.filePath("identity.cfg")); QVERIFY(config.create("Original")); + const QString privateKey = config.privKey(), publicKey = config.pubKey(), id = config.nodeId(); + QScopedValueRollback failure(nodeIdFailure ? failNodeId : failKeyGeneration, true); + QVERIFY(!config.generateIdentity()); + QCOMPARE(config.privKey(), privateKey); QCOMPARE(config.pubKey(), publicKey); QCOMPARE(config.nodeId(), id); + NodeConfig fresh(directory.filePath("failed.cfg")); + QVERIFY(!fresh.create("Failure")); QVERIFY(!fresh.exists()); +#else + QSKIP("Linker fault injection is available on Linux"); +#endif + } + + void repairKeepsExistingPrivateKey() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("repair.cfg"); + NodeConfig config(path); QVERIFY(config.create("Original")); + const QString privateKey = config.privKey(), publicKey = config.pubKey(), id = config.nodeId(); + QVERIFY(config_update_option(path, "global", "my_node_id", "")); + QVERIFY(config_update_option(path, "global", "my_public_key", "")); + QCOMPARE(config_ensure_keys_and_node_id(path.toUtf8().constData()), 0); + QVERIFY(config.load()); QVERIFY(!config.hasErrors()); + QCOMPARE(config.privKey(), privateKey); QCOMPARE(config.pubKey(), publicKey); QCOMPARE(config.nodeId(), id); + } + + void zeroIpv6ControlPortAndAndroidModeRoundTrip() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("control.cfg"); + NodeConfig config(path); QVERIFY(config.create("Control")); + QVERIFY(config_update_option(path, "global", "auto_sockets", "android")); + QVERIFY(config_update_option(path, "control", "control_ip", "::1")); + QVERIFY(config_update_option(path, "control", "control_port", "0")); + QVERIFY(config.load()); QVERIFY(!config.hasErrors()); + QCOMPARE(config.controlAddr(), QString("[::1]:0")); QCOMPARE(config.controlPort(), quint16(0)); + QVERIFY(config.autoSockets()); QVERIFY(config.save()); + Config parsed(parse_config(qPrintable(path)), free_config); QVERIFY(parsed); + QCOMPARE(parsed->global.auto_sockets, CFG_AUTO_SOCKETS_ANDROID); + QCOMPARE(parsed->global.control_sock.ss_family, AF_INET6); + config.setControlPort(9999); QVERIFY(config.save()); QVERIFY(config.load()); + QCOMPARE(config.controlAddr(), QString("[::1]:9999")); + } + + void failedLoadKeepsPreviousProjection() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("missing.cfg"); + NodeConfig config(path); QVERIFY(config.create("Previous")); + QVERIFY(QFile::remove(path)); QVERIFY(!config.load()); + QCOMPARE(config.nodeName(), QString("Previous")); + QVERIFY(config.nodeIdU64() != 0); + } + + void lastEmptyIdentityAndInvalidLanguageReplacePreviousValues() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("last.cfg"); + NodeConfig config(path); QVERIFY(config.create("Previous")); + QFile file(path); QVERIFY(file.open(QIODevice::Append)); + const QByteArray tail("[global]\nmy_node_id=\n[gui]\nlanguage=ru\nlanguage=invalid\n"); + QCOMPARE(file.write(tail), qint64(tail.size())); file.close(); + QVERIFY(config.load()); QVERIFY(config.nodeId().isEmpty()); + QCOMPARE(config.language(), QString("system")); + } + + void failedWriteKeepsOriginalFile() { +#ifdef Q_OS_UNIX + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("write.cfg"); + NodeConfig config(path); QVERIFY(config.create("Previous")); + QFile file(path); QVERIFY(file.open(QIODevice::ReadOnly)); + const auto original = file.readAll(); file.close(); + config.setNodeName("Unsaved"); + bool saved; + { + struct LimitGuard { + struct rlimit previous{}; + using Handler = void (*)(int); + Handler handler = SIG_DFL; + bool active = false; + ~LimitGuard() { if (active) { setrlimit(RLIMIT_FSIZE, &previous); std::signal(SIGXFSZ, handler); } } + } guard; + QVERIFY(getrlimit(RLIMIT_FSIZE, &guard.previous) == 0); + struct rlimit limited = guard.previous; limited.rlim_cur = 64; + guard.handler = std::signal(SIGXFSZ, SIG_IGN); guard.active = true; + QVERIFY(setrlimit(RLIMIT_FSIZE, &limited) == 0); + saved = config.save(); + } + QVERIFY(!saved); + QVERIFY(file.open(QIODevice::ReadOnly)); QCOMPARE(file.readAll(), original); +#else + QSKIP("RLIMIT_FSIZE is available on Unix"); +#endif + } + void reloadResetsRemovedFields() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("reload.cfg"); + NodeConfig config(path); QVERIFY(config.create("Previous profile")); + QVERIFY(config.load()); + QFile file(path); QVERIFY(file.open(QIODevice::WriteOnly)); + const QByteArray minimal("[global]\nauto_socket_protocols=udp\n[gui]\nlanguage=en\n"); + QCOMPARE(file.write(minimal), qint64(minimal.size())); file.close(); + QVERIFY(config.load()); QVERIFY(!config.hasErrors()); + QVERIFY(config.nodeName().isEmpty() && config.nodeId().isEmpty()); + QVERIFY(config.pubKey().isEmpty() && config.privKey().isEmpty()); + QVERIFY(config.controlAddr().isEmpty() && config.dbPath().isEmpty() && config.debugFile().isEmpty()); + QVERIFY(config.debugLevel().isEmpty() && !config.autoSockets()); + QCOMPARE(config.autoSocketProtocols(), QString("udp")); + QCOMPARE(config.language(), QString("en")); + } + + void guiProjectionAcceptsNativeSettingsAndComments() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("native.cfg"); + NodeConfig config(path); QVERIFY(config.create("Native options")); + QFile file(path); QVERIFY(file.open(QIODevice::Append)); + const QByteArray options("[ GLOBAL ]\nclient_type=1\nauto_socket_skip_no_default_route=no\n" + "my_node_name=Commented # note\nauto_sockets=true # enabled\n" + "[reality]\nenabled=no\n[socks]\nenabled=no\n[stun]\nenabled=no\n[log_udp]\nip=127.0.0.1\nport=9\n"); + QCOMPARE(file.write(options), qint64(options.size())); file.close(); + Config parsed(parse_config(qPrintable(path)), free_config); QVERIFY(parsed); + QVERIFY(config.load()); QVERIFY2(!config.hasErrors(), qPrintable(config.errors().join('\n'))); + QCOMPARE(config.nodeName(), QString("Commented")); + QVERIFY(config.autoSockets()); + QVERIFY(config.save()); + QVERIFY(file.open(QIODevice::ReadOnly)); + const auto saved = file.readAll(); + QVERIFY(saved.contains("[reality]\nenabled=no\n[socks]\nenabled=no\n[stun]\nenabled=no")); + QVERIFY(saved.contains("client_type=1\nauto_socket_skip_no_default_route=no")); + } + + void rejectsNonHexIdentity() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("identity.cfg"); + NodeConfig config(path); QVERIFY(config.create("Identity")); + QVERIFY(config_update_option(path, "global", "my_private_key", QString(64, 'z'))); + QVERIFY(config.load()); QVERIFY(config.hasErrors()); + } + + void rejectedEditDoesNotPublishPartialChanges() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("edit.cfg"); + NodeConfig config(path); QVERIFY(config.create("Original")); + QFile file(path); QVERIFY(file.open(QIODevice::ReadOnly)); + const auto original = file.readAll(); file.close(); + config.setNodeName("Injected\n[gui]"); + config.setLanguage("ru"); + QVERIFY(!config.save()); + QVERIFY(file.open(QIODevice::ReadOnly)); QCOMPARE(file.readAll(), original); + } void generatedConfigurations_data() { QTest::addColumn("protocols"); for (const QString& protocols : {QString("tcp"), QString("udp"), QString("tcp+udp")}) @@ -28,12 +200,12 @@ private slots: QFETCH(QString, protocols); QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("vibechat.cfg"); - NodeConfig config(path); QVERIFY(config.create(QString::fromUtf8("Тест конфигурации"), 1333)); + NodeConfig config(path); QVERIFY(config.create(QString::fromUtf8("Тест конфигурации"))); config.setAutoSocketProtocols(protocols); for (bool utun : {false, true}) { config.setUtunEnabled(utun); for (int pass = 0; pass < 3; ++pass) { - QVERIFY(pass == 1 ? config.saveFull() : config.save()); + QVERIFY(config.save()); QVERIFY(config.load()); QVERIFY2(!config.hasErrors(), qPrintable(config.errors().join('\n'))); Config parsed(parse_config(qPrintable(path)), free_config); QVERIFY(parsed); QCOMPARE(QString::fromUtf8(parsed->global.name), config.nodeName()); @@ -51,7 +223,7 @@ private slots: void settingsUpdatesPreserveValidConfiguration() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("settings.cfg"); - NodeConfig config(path); QVERIFY(config.create("Settings test", 1333)); + NodeConfig config(path); QVERIFY(config.create("Settings test")); QVERIFY(config_rewrite_servers(path, {{"tcp", "127.0.0.1:1333", "tcp", "private", ""}, {"udp", "[::1]:1333", "udp", "local", ""}})); QVERIFY(config_update_option(path, "chatserver", "storage_unit_size", "23M")); @@ -73,12 +245,12 @@ private slots: void repeatedLoadDoesNotDuplicateDebugOptions() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("debug.cfg"); - NodeConfig config(path); QVERIFY(config.create("Debug settings", 1333)); + NodeConfig config(path); QVERIFY(config.create("Debug settings")); QVERIFY(config_update_option(path, "debug", "stream", "debug")); for (int attempt = 0; attempt < 3; ++attempt) { QVERIFY(config.load()); QCOMPARE(config.debugCategories(), QString("stream=debug")); - QVERIFY(config.saveFull()); + QVERIFY(config.save()); Config parsed(parse_config(qPrintable(path)), free_config); QVERIFY(parsed); } } diff --git a/tools/chatgui/transport/node_config.cpp b/tools/chatgui/transport/node_config.cpp index 236bb47d..631055be 100644 --- a/tools/chatgui/transport/node_config.cpp +++ b/tools/chatgui/transport/node_config.cpp @@ -1,515 +1,229 @@ -// node_config.cpp — Simplified INI config manager for embedded uTun node +/* GUI-проекция INI; сетевую конфигурацию разбирает ядро при запуске. */ #include "node_config.h" #include "config_updater.h" -#include "../../../lib/debug_config.h" +#include #include +#include extern "C" { -#include -} -#include -#include -#include -#include - -// ===================================================================== -// Whitelists — union of chatgui + uTun config_parser keys -// ===================================================================== - -static const QSet GLOBAL_KEYS = { - "my_node_name", "my_private_key", "my_public_key", "my_node_id", - "tun_enabled", "tun_ifname", "tun_ip", "mtu", - "keepalive_timeout", "keepalive_interval", "keepalive_adaptive", "bbr_max_cwnd", - "auto_sockets", "auto_socket_protocols", - "debug_level", "log_file", "db_path", "db_sync_enabled", "db_sync_ttl", - "enable_timestamp", "enable_function_names", "enable_file_lines", "enable_colors", - "tun_test_mode" -}; - -static const QSet SERVER_KEYS = { - "addr", "so_mark", "fib", "netif", "type", "mtu", "only_local", "transport" -}; - -static const QSet CLIENT_KEYS = { - "peer_public_key", "link", "keepalive", - "reality", "server_name", "short_id", "public_key", "version", "fingerprint" -}; - -static const QSet CONTROL_KEYS = { - "ip", "control_ip", "port", "control_port", "allow", "control_allow" -}; - -static const QSet GUI_KEYS = { - "db_path", "debug_file", "debug_level", "language", "utun_enabled", - "sound_dir", "sound_msg_incoming", "sound_msg_outgoing", - "sound_connect", "sound_disconnect" -}; - -static const QSet ALLOWED_KEYS_KEYS = { - "allow_all", "key" -}; - -static const QSet NAT_KEYS = { - "enabled", "tun_ifname", "tun_ip", "nat_via", "port_start", "port_end", "forward" -}; - -static const QSet FIREWALL_KEYS = { - "allow" -}; - -static const QSet ROUTING_KEYS = { - "route_subnet", "my_subnet" -}; - -static const QSet TCP_PROXY_CLIENT_KEYS = { - "enabled", "tun_name", "tun_ip", "mtu", "via_node", - "forward", "socks_enabled", "socks_addr", "http_proxy_enabled", "http_proxy_addr" -}; - -static const QSet TCP_PROXY_SERVER_KEYS = { - "enabled", "tcp_recv_buf" -}; - -static const QSet NETWORK_KEYS = { - "id", "pubkey", "signing_key" -}; - -static const QSet NTP_KEYS = { - "enabled", "server", "interval" -}; - -static const QStringList VALID_DEBUG_LEVELS = { - "none", "error", "warn", "info", "debug", "trace" -}; - -// Основные debug-категории для подсказки в секции [debug] (комментарием). -// Имена соответствуют lib/debug_config.c (g_debug_categories), уровень — разумный дефолт. -static const struct { const char* name; const char* level; } DEBUG_CATEGORY_HINTS[] = { - {"sys", "error"}, - {"connection", "info"}, - {"etcp", "trace"}, - {"crypto", "error"}, - {"config", "info"}, - {"tun", "error"}, - {"routing", "info"}, - {"timers", "error"}, - {"bgp", "info"}, - {"chat", "info"}, - {"chat_sync", "info"}, - {"member_sync", "info"}, - {"socket", "error"}, - {"control", "info"}, - {"dump", "trace"}, - {"traffic", "trace"}, - {"debug", "trace"}, - {"general", "info"}, - {"nat", "info"}, - {"keepalive", "error"}, - {"media", "info"}, - {"etcp_route", "info"}, - {"etcp_dump", "trace"}, - {"proxy", "info"}, -}; - -// Sections: "" = global, use prefix match for dynamic sections -static bool isSectionValid(const QString& section) { - if (section.isEmpty() || section == "global") return true; - if (section.startsWith("server:")) return true; - if (section.startsWith("client:")) return true; - if (section.startsWith("network:")) return true; - if (section == "control") return true; - if (section == "gui") return true; - if (section == "allowed_keys") return true; - if (section == "nat") return true; - if (section == "firewall") return true; - if (section == "debug") return true; - if (section == "routing" || section == "route") return true; - if (section == "tcp_proxy_client") return true; - if (section == "tcp_proxy_server") return true; - if (section == "ntp") return true; - if (section == "chat" || section == "chatserver") return true; - return false; +#include "../../../src/config_ini.h" +#include "../../../src/transport_layer/secure_channel.h" } -static const QSet* keysForSection(const QString& section) { - if (section.isEmpty() || section == "global") return &GLOBAL_KEYS; - if (section.startsWith("server:")) return &SERVER_KEYS; - if (section.startsWith("client:")) return &CLIENT_KEYS; - if (section.startsWith("network:")) return &NETWORK_KEYS; - if (section == "control") return &CONTROL_KEYS; - if (section == "gui") return &GUI_KEYS; - if (section == "allowed_keys") return &ALLOWED_KEYS_KEYS; - if (section == "nat") return &NAT_KEYS; - if (section == "firewall") return &FIREWALL_KEYS; - if (section == "routing" || section == "route") return &ROUTING_KEYS; - if (section == "tcp_proxy_client") return &TCP_PROXY_CLIENT_KEYS; - if (section == "tcp_proxy_server") return &TCP_PROXY_SERVER_KEYS; - if (section == "ntp") return &NTP_KEYS; - return nullptr; // [debug] — free-form, no key validation +static int boolean_value(const QString& value) { + const QString text = value.toLower(); + if (text == "yes" || text == "true" || text == "1") return 1; + if (text == "no" || text == "false" || text == "0") return 0; + return -1; } -static QString formatKeyHex(const uint8_t* bin, size_t len) { - return QByteArray(reinterpret_cast(bin), len).toHex(); -} - -static bool parseKeyHex(const QString& hex, uint8_t* bin, size_t len) { - QByteArray ba = QByteArray::fromHex(hex.toLatin1()); - if (ba.size() != (int)len) return false; - memcpy(bin, ba.constData(), len); - return true; -} - -// ===================================================================== -// NodeConfig -// ===================================================================== - NodeConfig::NodeConfig(const QString& path) : m_path(path) {} bool NodeConfig::exists() const { return QFileInfo::exists(m_path); } -void NodeConfig::generateIdentity() { - EVP_PKEY* pkey = nullptr; - EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, nullptr); - if (ctx) { - EVP_PKEY_keygen_init(ctx); - EVP_PKEY_generate(ctx, &pkey); - EVP_PKEY_CTX_free(ctx); +/* Публикуем ключи/ID в модели только после успеха обеих криптографических операций. */ +bool NodeConfig::generateIdentity() { + struct SC_MYKEYS keys; + if (sc_generate_keypair(&keys) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI identity generation failed path=%s", qPrintable(m_path)); + return false; } - if (pkey) { - size_t pkLen = 32, skLen = 32; - uint8_t pk[32], sk[32]; - EVP_PKEY_get_raw_public_key(pkey, pk, &pkLen); - EVP_PKEY_get_raw_private_key(pkey, sk, &skLen); - m_pubKey = formatKeyHex(pk, 32); - m_privKey = formatKeyHex(sk, 32); - EVP_PKEY_free(pkey); - - uint8_t hash[32]; unsigned int hashLen = 32; - EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); - EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL); - EVP_DigestUpdate(mdctx, pk, 32); - EVP_DigestFinal_ex(mdctx, hash, &hashLen); - EVP_MD_CTX_free(mdctx); - uint64_t id; memcpy(&id, hash, 8); - id &= 0x7FFFFFFFFFFFFFFFULL; - m_nodeId = QString("%1").arg(id, 16, 16, QChar('0')).right(16); + const uint64_t id = sc_derive_node_id_from_pubkey(keys.public_key); + if (!id) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI identity node ID derivation failed path=%s", qPrintable(m_path)); + return false; } + m_pubKey = QByteArray(reinterpret_cast(keys.public_key), SC_PUBKEY_SIZE).toHex(); + m_privKey = QByteArray(reinterpret_cast(keys.private_key), SC_PRIVKEY_SIZE).toHex(); + m_nodeId = QString("%1").arg(qulonglong(id), 16, 16, QChar('0')); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "GUI identity generated node=%s path=%s", qPrintable(m_nodeId), qPrintable(m_path)); + return true; } -bool NodeConfig::create(const QString& nodeName, quint16 listenPort) { - if (nodeName.isEmpty()) { - QString host = QHostInfo::localHostName(); - m_nodeName = host.isEmpty() ? "vibechat" : host; - } else { - m_nodeName = nodeName; +bool NodeConfig::create(const QString& nodeName) { + if (exists()) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI profile creation rejected existing path=%s", qPrintable(m_path)); + return false; } - if (m_pubKey.isEmpty()) generateIdentity(); - if (m_controlAddr.isEmpty()) - m_controlAddr = "127.0.0.1:9999"; - m_servers.clear(); - m_clients.clear(); - (void)listenPort; /* автосокеты сами создают UDP+TCP на случайных портах */ - m_autoSockets = true; - m_autoSocketProtocols = QStringLiteral("tcp+udp"); - if (m_dbPath.isEmpty()) - m_dbPath = "chat_data"; - if (m_debugFile.isEmpty()) - m_debugFile = "chatgui.log"; + if (!generateIdentity()) return false; + m_nodeName = nodeName.isEmpty() ? QHostInfo::localHostName() : nodeName; + if (m_nodeName.isEmpty()) m_nodeName = "vibechat"; + m_controlAddr = "127.0.0.1:9999"; + m_autoSocketsValue = "yes"; + m_autoSocketProtocols = "tcp+udp"; + m_dbPath = "chat_data"; + m_debugFile = "chatgui.log"; return save(); } +/* Отдельная свежая проекция исключает сохранение удалённых полей после повторного load. */ bool NodeConfig::load() { - QFile f(m_path); - if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return false; - - m_servers.clear(); - m_clients.clear(); - m_errors.clear(); - m_debugCategories.clear(); - m_utunEnabled = false; - m_language = QStringLiteral("system"); - m_autoSocketProtocols = QStringLiteral("tcp+udp"); - - QString section, controlIp, controlPort; - QTextStream in(&f); - int lineNum = 0; - bool sectionValid = true; // global/empty is valid - bool debugSection = false; - - while (!in.atEnd()) { - QString line = in.readLine().trimmed(); - lineNum++; - if (line.isEmpty() || line.startsWith('#')) continue; - - if (line.startsWith('[') && line.endsWith(']')) { - section = line.mid(1, line.length() - 2); - sectionValid = isSectionValid(section); - debugSection = (section == "debug"); - if (!sectionValid) { - m_errors.append(QString("line %1: unknown section [%2]") - .arg(lineNum).arg(section)); - } + QStringList lines; + if (!config_read_lines(m_path, lines)) return false; + NodeConfig next(m_path); + QString section, controlIp = "127.0.0.1", controlPort; + const auto invalid = [&](int line, const QString& field) { + next.m_errors.append(QString("line %1: invalid %2").arg(line).arg(field)); + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI config invalid field=%s path=%s line=%d", + qPrintable(field), qPrintable(m_path), line); + }; + for (int i = 0; i < lines.size(); ++i) { + const QByteArray text = lines[i].trimmed().toUtf8(); + if (text.isEmpty() || text.startsWith('#')) continue; + if (text.startsWith('[')) { + char name[128]; + if (config_ini_section(text.constData(), name, sizeof(name))) { invalid(i + 1, "section header"); section.clear(); } + else section = QString::fromUtf8(name).toLower(); continue; } - - if (!sectionValid) - continue; // skip keys in unknown sections (already reported) - - int eq = line.indexOf('='); - if (eq < 0) continue; - QString key = line.left(eq).trimmed(); - QString val = line.mid(eq + 1).trimmed(); - - // Validate key unless [debug] section (free-form category=level) - if (!debugSection) { - const QSet* validKeys = keysForSection(section); - if (validKeys && !validKeys->contains(key)) { - QString knownList; - for (const auto& k : *validKeys) { - if (!knownList.isEmpty()) knownList += ", "; - knownList += k; - } - m_errors.append(QString("line %1: [%2] unknown option '%3'. Valid: %4") - .arg(lineNum).arg(section.isEmpty() ? "global" : section) - .arg(key).arg(knownList)); - } + char keyBytes[CONFIG_INI_LINE_SIZE], valueBytes[CONFIG_INI_LINE_SIZE]; + if (config_ini_key_value(text.constData(), keyBytes, sizeof(keyBytes), valueBytes, sizeof(valueBytes))) { + invalid(i + 1, "key=value"); + continue; } - - // Value validation for known keys - if (!debugSection && key == "debug_level" && !val.isEmpty()) { - if (!VALID_DEBUG_LEVELS.contains(val.toLower())) { - m_errors.append(QString("line %1: [%2] debug_level='%3' is invalid. Valid: %4") - .arg(lineNum).arg(section.isEmpty() ? "global" : section) - .arg(val).arg(VALID_DEBUG_LEVELS.join(", "))); - } - } - - if ((key == "port" || key == "control_port") && !val.isEmpty()) { - bool ok; - int p = val.toInt(&ok); - if (!ok || p < 1 || p > 65535) { - m_errors.append(QString("line %1: [%2] %3='%4' is invalid (must be 1-65535)") - .arg(lineNum).arg(section).arg(key).arg(val)); - } - } - - if ((key == "my_public_key" || key == "my_private_key" || key == "peer_public_key") - && !val.isEmpty() && val.length() != 64) { - m_errors.append(QString("line %1: [%2] %3 must be 64 hex chars (got %4)") - .arg(lineNum).arg(section.isEmpty() ? "global" : section) - .arg(key).arg(val.length())); - } - - if (key == "my_node_id" && !val.isEmpty() && val.length() != 16) { - m_errors.append(QString("line %1: [%2] my_node_id must be 16 hex chars (got %3)") - .arg(lineNum).arg(section.isEmpty() ? "global" : section) - .arg(val.length())); - } - - // --- store known values (unchanged) --- - if (section == "global" || section.isEmpty()) { - if (key == "my_node_name") m_nodeName = val; - else if (key == "my_node_id") m_nodeId = val; - else if (key == "my_public_key") m_pubKey = val; - else if (key == "my_private_key") m_privKey = val; - else if (key == "auto_sockets") m_autoSockets = (val.compare("yes", Qt::CaseInsensitive) == 0 || val == "1"); - else if (key == "auto_socket_protocols") { - const QString protocols = val.section('#', 0, 0).section(';', 0, 0).trimmed().toLower(); - if (protocols == "tcp" || protocols == "udp" || protocols == "tcp+udp") m_autoSocketProtocols = protocols; - else { - m_errors.append(QString("line %1: invalid auto_socket_protocols='%2'; expected tcp, udp or tcp+udp") - .arg(lineNum).arg(val)); - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "auto_socket_protocols='%s' is invalid path=%s line=%d", - qPrintable(val), qPrintable(m_path), lineNum); - } - } - } else if (section.startsWith("server:")) { - QString srvName = section.mid(7); - if (key == "addr") m_servers.append({srvName, val}); - } else if (section.startsWith("client:")) { - QString cliName = section.mid(7); - if (key == "peer_public_key") { - NodeClient nc; - nc.name = cliName; - nc.pubKeyHex = val; - m_clients.append(nc); - } else if (key == "link") { - if (!m_clients.isEmpty() && m_clients.last().name == cliName) { - int sep1 = val.indexOf(':'); - int sep2 = val.lastIndexOf(':'); - if (sep1 > 0 && sep2 > sep1) { - m_clients.last().serverName = val.left(sep1); - m_clients.last().remoteAddr = val.mid(sep1 + 1); - } - } + const QString key = QString::fromUtf8(keyBytes), value = QString::fromUtf8(valueBytes); + if (section == "global") { + if (key == "my_node_name") { + next.m_nodeName = value; + if (value.toUtf8().size() >= 64) invalid(i + 1, key); + } else if (key == "my_public_key" || key == "my_private_key") { + (key == "my_public_key" ? next.m_pubKey : next.m_privKey) = value; + if (!value.isEmpty() && config_ini_hex(valueBytes, nullptr, SC_PRIVKEY_SIZE)) invalid(i + 1, key); + } else if (key == "my_node_id") { + next.m_nodeId.clear(); + bool valid; + const auto id = value.toULongLong(&valid, 16); + if (!value.isEmpty() && (!valid || value.startsWith('+') || value.startsWith('-'))) { + next.m_nodeId = value; + invalid(i + 1, key); + } else if (!value.isEmpty()) next.m_nodeId = QString("%1").arg(id, 16, 16, QChar('0')); + } else if (key == "auto_sockets") { + const int enabled = boolean_value(value); + if (value.compare("android", Qt::CaseInsensitive) == 0) next.m_autoSocketsValue = "android"; + else if (enabled >= 0) next.m_autoSocketsValue = enabled ? "yes" : "no"; + else invalid(i + 1, key); + } else if (key == "auto_socket_protocols") { + const QString protocols = value.toLower(); + if (protocols == "tcp" || protocols == "udp" || protocols == "tcp+udp") next.m_autoSocketProtocols = protocols; + else invalid(i + 1, key); } } else if (section == "control") { - if (key == "ip" || key == "control_ip") controlIp = val; - else if (key == "port" || key == "control_port") controlPort = val; + if (key == "ip" || key == "control_ip") controlIp = value; + else if (key == "port" || key == "control_port") { + bool valid; + const uint port = value.toUInt(&valid); + if (!valid || port > 65535 || value.startsWith('+') || value.startsWith('-')) invalid(i + 1, key); + else controlPort = QString::number(port); + } } else if (section == "gui") { if (key == "utun_enabled") { - const QString value = val.toLower(); - if (value == "yes" || value == "true" || value == "1") m_utunEnabled = true; - else if (value == "no" || value == "false" || value == "0") m_utunEnabled = false; - else m_errors.append(QString("Line %1: invalid gui utun_enabled '%2'").arg(lineNum).arg(val)); - } else if (key == "db_path") m_dbPath = val; - else if (key == "debug_file") m_debugFile = val; - else if (key == "debug_level") m_debugLevel = val; + const int enabled = boolean_value(value); + if (enabled < 0) invalid(i + 1, key); + else next.m_utunEnabled = enabled != 0; + } else if (key == "db_path") next.m_dbPath = value; + else if (key == "debug_file") next.m_debugFile = value; + else if (key == "debug_level") next.m_debugLevel = value; else if (key == "language") { - if (val == "system" || val == "en" || val == "ru") m_language = val; - else DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "GUI language: invalid preference '%s', using system language", qPrintable(val)); + if (value == "system" || value == "en" || value == "ru") next.m_language = value; + else { + next.m_language = "system"; + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "GUI language: invalid preference, using system language path=%s line=%d", + qPrintable(m_path), i + 1); + } } - } else if (debugSection) { - if (!m_debugCategories.isEmpty()) m_debugCategories += ','; - m_debugCategories += key + '=' + val; + } else if (section == "debug") { + if (!next.m_debugCategories.isEmpty()) next.m_debugCategories += ','; + next.m_debugCategories += key + '=' + value; } } - f.close(); - - if (!controlIp.isEmpty() && !controlPort.isEmpty()) - m_controlAddr = controlIp + ":" + controlPort; - - return true; -} - -bool NodeConfig::saveFull() { - QFile f(m_path); - if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) return false; - - QTextStream out(&f); - out << "[global]\n"; - if (!m_nodeName.isEmpty()) out << "my_node_name=" << m_nodeName << "\n"; - if (!m_nodeId.isEmpty()) out << "my_node_id=" << m_nodeId << "\n"; - if (!m_privKey.isEmpty()) out << "my_private_key=" << m_privKey << "\n"; - if (!m_pubKey.isEmpty()) out << "my_public_key=" << m_pubKey << "\n"; - out << "auto_sockets=" << (m_autoSockets ? "yes" : "no") << "\n"; - out << "auto_socket_protocols=" << m_autoSocketProtocols << "\n"; - out << "\n"; - - for (auto& srv : m_servers) { - out << "[server:" << srv.first << "]\n"; - out << "addr=" << srv.second << "\n"; - out << "type=public\n\n"; - } - - for (auto& cli : m_clients) { - out << "[client:" << cli.name << "]\n"; - out << "peer_public_key=" << cli.pubKeyHex << "\n"; - out << "link=" << cli.serverName << ":" << cli.remoteAddr << "\n\n"; - } - - if (!m_controlAddr.isEmpty()) { - out << "[control]\n"; - int sep = m_controlAddr.lastIndexOf(':'); - if (sep > 0) { - out << "ip=" << m_controlAddr.left(sep) << "\n"; - out << "port=" << m_controlAddr.mid(sep + 1) << "\n\n"; - } - } - - out << "[gui]\n"; - out << "utun_enabled=" << (m_utunEnabled ? "yes" : "no") << "\n"; - out << "db_path=" << (m_dbPath.isEmpty() ? "chat_data" : m_dbPath) << "\n"; - out << "debug_file=" << m_debugFile << "\n"; - out << "language=" << m_language << "\n"; - out << "debug_level=" << (m_debugLevel.isEmpty() ? "info" : m_debugLevel) << "\n\n"; - - out << "[debug]\n"; - for (const auto& h : DEBUG_CATEGORY_HINTS) - out << '#' << h.name << '=' << h.level << '\n'; - for (const QString& d : m_debugCategories.split(',', Qt::SkipEmptyParts)) { - int eq = d.indexOf('='); - if (eq > 0) out << d.left(eq).trimmed() << '=' << d.mid(eq + 1).trimmed() << '\n'; - } - out << '\n'; - - out << "[allowed_keys]\n"; - out << "allow_all=yes\n\n"; - - out << "[ntp]\n"; - out << "enabled=yes\n"; - out << "server=pool.ntp.org\n"; - out << "server=time.google.com\n"; - out << "server=time.cloudflare.com\n"; - out << "server=time.windows.com\n"; - out << "server=ntp1.vniiftri.ru\n"; - out << "server=ntp2.vniiftri.ru\n"; - out << "interval=3600\n\n"; - - f.close(); + if (!controlPort.isEmpty()) { + if (controlIp.contains(':') && !controlIp.startsWith('[')) controlIp = '[' + controlIp + ']'; + next.m_controlAddr = controlIp + ':' + controlPort; + } + *this = std::move(next); + DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "GUI config loaded path=%s errors=%lld auto_sockets=%s protocols=%s language=%s", + qPrintable(m_path), (long long)m_errors.size(), qPrintable(m_autoSocketsValue), + qPrintable(m_autoSocketProtocols), qPrintable(m_language)); return true; } +/* Обновляем всю GUI-проекцию в памяти; ни одно поле не публикуется отдельно. */ bool NodeConfig::save() { - if (!QFileInfo::exists(m_path)) - return saveFull(); - - QFile f(m_path); - if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return false; - QStringList lines; - QTextStream in(&f); - while (!in.atEnd()) { - QString line = in.readLine(0); - if (line.endsWith('\r')) line.chop(1); - lines.append(line); + const struct { const char *field; bool valid; } checks[] = { + {"my_public_key", config_ini_hex(m_pubKey.toLatin1().constData(), nullptr, SC_PUBKEY_SIZE) == 0}, + {"my_private_key", config_ini_hex(m_privKey.toLatin1().constData(), nullptr, SC_PRIVKEY_SIZE) == 0}, + {"my_node_id", config_ini_hex(m_nodeId.toLatin1().constData(), nullptr, sizeof(uint64_t)) == 0}, + {"my_node_name", m_nodeName.toUtf8().size() < 64 && !m_nodeName.contains('#')}, + {"db_path", !m_dbPath.contains('#')}, {"debug_file", !m_debugFile.contains('#')}, + {"auto_socket_protocols", m_autoSocketProtocols == "tcp" || m_autoSocketProtocols == "udp" || m_autoSocketProtocols == "tcp+udp"}, + {"language", m_language == "system" || m_language == "en" || m_language == "ru"} + }; + for (const auto& check : checks) { + if (check.valid) continue; + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI config save rejected invalid field=%s path=%s", check.field, qPrintable(m_path)); + return false; } - f.close(); - - config_update_in_lines(lines, "global", "my_node_name", m_nodeName, true); - config_update_in_lines(lines, "global", "my_node_id", m_nodeId, true); - config_update_in_lines(lines, "global", "my_private_key", m_privKey, true); - config_update_in_lines(lines, "global", "my_public_key", m_pubKey, true); - config_update_in_lines(lines, "global", "auto_sockets", m_autoSockets ? "yes" : "no", true); - config_update_in_lines(lines, "global", "auto_socket_protocols", m_autoSocketProtocols, true); + const bool newFile = !exists(); + QStringList lines; + if (!config_read_lines(m_path, lines, true)) return false; + if (newFile) { + lines = QStringList{"[global]", "", "[control]", "", "[gui]", "", "[debug]", "", + "[allowed_keys]", "allow_all=yes", "", "[ntp]", "enabled=yes", "server=pool.ntp.org", + "server=time.google.com", "server=time.cloudflare.com", "server=time.windows.com", + "server=ntp1.vniiftri.ru", "server=ntp2.vniiftri.ru", "interval=3600", ""}; + } + const struct { const char *section; const char *key; QString value; } fields[] = { + {"global", "my_node_name", m_nodeName}, {"global", "my_node_id", m_nodeId}, + {"global", "my_private_key", m_privKey}, {"global", "my_public_key", m_pubKey}, + {"global", "auto_sockets", m_autoSocketsValue}, {"global", "auto_socket_protocols", m_autoSocketProtocols}, + {"gui", "utun_enabled", m_utunEnabled ? "yes" : "no"}, {"gui", "db_path", m_dbPath.isEmpty() ? "chat_data" : m_dbPath}, + {"gui", "debug_file", m_debugFile}, {"gui", "language", m_language}, + {"gui", "debug_level", m_debugLevel.isEmpty() ? "info" : m_debugLevel} + }; + for (const auto& field : fields) + if (!config_update_in_lines(lines, field.section, field.key, field.value, true)) return false; if (!m_controlAddr.isEmpty()) { - int sep = m_controlAddr.lastIndexOf(':'); - if (sep > 0) { - config_update_in_lines(lines, "control", "ip", m_controlAddr.left(sep), true); - config_update_in_lines(lines, "control", "port", m_controlAddr.mid(sep + 1), true); + const int separator = m_controlAddr.lastIndexOf(':'); + QString host = m_controlAddr.left(separator); + const QString portText = m_controlAddr.mid(separator + 1); + bool valid; + const uint port = portText.toUInt(&valid); + if (separator <= 0 || !valid || port > 65535 || portText.startsWith('+') || portText.startsWith('-') || host.contains('#')) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI config save rejected invalid control address path=%s", qPrintable(m_path)); + return false; } + if (host.startsWith('[') && host.endsWith(']')) host = host.mid(1, host.size() - 2); + if (!config_update_in_lines(lines, "control", "ip", host, true) || + !config_update_in_lines(lines, "control", "port", QString::number(port), true)) return false; } - - config_update_in_lines(lines, "gui", "utun_enabled", m_utunEnabled ? "yes" : "no", true); - config_update_in_lines(lines, "gui", "db_path", m_dbPath.isEmpty() ? "chat_data" : m_dbPath, true); - config_update_in_lines(lines, "gui", "debug_file", m_debugFile, true); - config_update_in_lines(lines, "gui", "language", m_language, true); - config_update_in_lines(lines, "gui", "debug_level", m_debugLevel.isEmpty() ? "info" : m_debugLevel, true); - - /* [debug] section: each category=level as separate line */ - QStringList dbg = m_debugCategories.split(',', Qt::SkipEmptyParts); - for (const QString& d : dbg) { - int eq = d.indexOf('='); - if (eq > 0) - config_update_in_lines(lines, "debug", d.left(eq).trimmed(), d.mid(eq + 1).trimmed(), true); + for (const QString& entry : m_debugCategories.split(',', Qt::SkipEmptyParts)) { + const int equal = entry.indexOf('='); + if (equal <= 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI config save rejected malformed debug setting path=%s", qPrintable(m_path)); + return false; + } + if (!config_update_in_lines(lines, "debug", entry.left(equal).trimmed(), entry.mid(equal + 1).trimmed(), true)) return false; } - - if (!f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) return false; - QTextStream out(&f); - for (int i = 0; i < lines.size(); i++) - out << lines[i] << '\n'; - f.close(); - return true; + return config_write_lines(m_path, lines); } uint64_t NodeConfig::nodeIdU64() const { - if (m_nodeId.length() != 16) return 0; + if (config_ini_hex(m_nodeId.toLatin1().constData(), nullptr, sizeof(uint64_t))) return 0; return m_nodeId.toULongLong(nullptr, 16); } quint16 NodeConfig::controlPort() const { - if (m_controlAddr.isEmpty()) return 0; - int sep = m_controlAddr.lastIndexOf(':'); - if (sep < 0) return 0; - return static_cast(m_controlAddr.mid(sep + 1).toUInt()); + const int separator = m_controlAddr.lastIndexOf(':'); + bool valid; + const uint port = m_controlAddr.mid(separator + 1).toUInt(&valid); + return separator > 0 && valid && port <= 65535 ? quint16(port) : 0; } -void NodeConfig::setControlPort(quint16 p) { - int sep = m_controlAddr.lastIndexOf(':'); - if (sep >= 0) - m_controlAddr = m_controlAddr.left(sep) + ":" + QString::number(p); - else - m_controlAddr = "127.0.0.1:" + QString::number(p); +void NodeConfig::setControlPort(quint16 port) { + const int separator = m_controlAddr.lastIndexOf(':'); + const QString host = separator > 0 ? m_controlAddr.left(separator) : QString("127.0.0.1"); + m_controlAddr = host + ':' + QString::number(port); } diff --git a/tools/chatgui/transport/node_config.h b/tools/chatgui/transport/node_config.h index 5624131f..137b3d70 100644 --- a/tools/chatgui/transport/node_config.h +++ b/tools/chatgui/transport/node_config.h @@ -1,91 +1,77 @@ -/* Desktop INI-конфиг узла: идентичность, listen/client-секции, control и GUI-настройки. - * load читает и проверяет значения; create генерирует конфиг/ключи; save обновляет известные поля - * существующего файла, saveFull перезаписывает файл из модели. Запуск ядра выполняет UtunNode. */ +/* GUI-проекция desktop INI: идентичность, control и параметры интерфейса. + * Все вызовы синхронные, в одном потоке; доступ к файлу сериализует вызывающий. + * load проверяет физический текст и собственные поля GUI без DNS/сетевого I/O. + * Полную схему сетевых опций проверяет parse_config при запуске ядра. + * save обновляет только поля этой модели, сохраняя остальные секции/комментарии; + * публикация через общий config_updater с проверкой записи и commit. */ #ifndef NODE_CONFIG_H #define NODE_CONFIG_H +#include #include #include -#include -#include - -struct NodeClient { - QString name; - QString pubKeyHex; - QString serverName; - QString remoteAddr; -}; class NodeConfig { public: explicit NodeConfig(const QString& path); - bool exists() const; - bool create(const QString& nodeName, quint16 listenPort); - bool load(); - bool save(); - bool saveFull(); + bool exists() const; // существует ли путь; доступность проверяется чтением + bool create(const QString& nodeName); // новый файл: X25519/id, автосокеты TCP+UDP, control 127.0.0.1:9999 + bool load(); // false при I/O/повреждённом тексте, прежняя модель сохраняется; true => проверить hasErrors() + bool save(); // false при неверном поле/I/O; весь набор правок публикуется одной записью QString nodeName() const { return m_nodeName; } - void setNodeName(const QString& n) { m_nodeName = n; } - QString nodeId() const { return m_nodeId; } + void setNodeName(const QString& name) { m_nodeName = name; } + QString nodeId() const { return m_nodeId; } // 16 HEX-символов либо пусто, если не задан QString pubKey() const { return m_pubKey; } - QString privKey() const { return m_privKey; } - uint64_t nodeIdU64() const; + QString privKey() const { return m_privKey; } // секрет; не выводить в диагностику + uint64_t nodeIdU64() const; // 0, если ID отсутствует или неверен - QList> servers() const { return m_servers; } + QString controlAddr() const { return m_controlAddr; } // host:port либо [IPv6]:port; пусто => control не задан + void setControlAddr(const QString& address) { m_controlAddr = address; } + quint16 controlPort() const; // 0 при отсутствии/ошибке; явный нулевой порт также допустим + void setControlPort(quint16 port); // сохраняет host; при пустом адресе использует 127.0.0.1 - QList clients() const { return m_clients; } - - QString controlAddr() const { return m_controlAddr; } - void setControlAddr(const QString& a) { m_controlAddr = a; } - quint16 controlPort() const; - void setControlPort(quint16 p); - - bool autoSockets() const { return m_autoSockets; } - void setAutoSockets(bool v) { m_autoSockets = v; } + bool autoSockets() const { return m_autoSocketsValue != "no"; } + void setAutoSockets(bool enabled) { m_autoSocketsValue = enabled ? "yes" : "no"; } QString autoSocketProtocols() const { return m_autoSocketProtocols; } void setAutoSocketProtocols(const QString& protocols) { m_autoSocketProtocols = protocols; } QString filePath() const { return m_path; } - /* GUI settings (from [gui] section) */ bool utunEnabled() const { return m_utunEnabled; } void setUtunEnabled(bool enabled) { m_utunEnabled = enabled; } QString dbPath() const { return m_dbPath; } - void setDbPath(const QString& p) { m_dbPath = p; } + void setDbPath(const QString& path) { m_dbPath = path; } QString debugFile() const { return m_debugFile; } - void setDebugFile(const QString& p) { m_debugFile = p; } - QString debugLevel() const { return m_debugLevel; } - QString debugCategories() const { return m_debugCategories; } + void setDebugFile(const QString& path) { m_debugFile = path; } + QString debugLevel() const { return m_debugLevel; } // пустой при отсутствии, save использует info + QString debugCategories() const { return m_debugCategories; } // [debug] как category=level через запятую QString language() const { return m_language; } void setLanguage(const QString& language) { m_language = language; } - /* Config validation errors (collected during load()) */ + /* load заменяет модель целиком; удалённые из файла значения не сохраняются. + * Ошибки собственных полей не мешают прочитать остальные; save проверяет текущие значения заново. */ bool hasErrors() const { return !m_errors.isEmpty(); } const QStringList& errors() const { return m_errors; } - /* Identity generation (public for fixing invalid keys) */ - void generateIdentity(); + bool generateIdentity(); // успех заменяет всю идентичность; ошибка сохраняет прежнюю, файл не меняет private: - - QString m_path; + QString m_path; // путь INI, не каталог данных QString m_nodeName; QString m_nodeId; QString m_pubKey; QString m_privKey; - QList> m_servers; - QList m_clients; QString m_controlAddr; - bool m_utunEnabled = false; // дополнительный UTUN-сервис; обычный запуск — core/chat - bool m_autoSockets = true; - QString m_autoSocketProtocols = QStringLiteral("tcp+udp"); // протоколы автоматически создаваемых сокетов - QString m_dbPath; - QString m_debugFile; + bool m_utunEnabled = false; // явно запускаемый дополнительный UTUN-сервис + QString m_autoSocketsValue = QStringLiteral("no"); // yes/no/android, последний режим сохраняется до явного setter + QString m_autoSocketProtocols = QStringLiteral("tcp+udp"); + QString m_dbPath; // относительно каталога конфигурации; пустой save заменяет на chat_data + QString m_debugFile; // путь журнала GUI QString m_debugLevel; QString m_debugCategories; - QString m_language = QStringLiteral("system"); - QStringList m_errors; + QString m_language = QStringLiteral("system"); // system/en/ru + QStringList m_errors; // ошибки последнего успешного чтения, без секретных значений }; #endif // NODE_CONFIG_H