Keep desktop config projection separate from core settings
This commit is contained in:
1 parent
f6f3ca8da7
commit
9fa58b4f83
7 files changed
+412
-547
No files matched your search
@@ -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)
|
||||
|
||||
|
||||
@@ -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_<channel_id>`), рассылка онлайн-пирам через etcp_send.
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "desktop_integration.h"
|
||||
#include "gui_log.h"
|
||||
#include "config_parser.h"
|
||||
#include "../../../src/config_updater.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cerrno>
|
||||
@@ -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();
|
||||
|
||||
@@ -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<QComboBox*>("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);
|
||||
}
|
||||
|
||||
@@ -5,19 +5,191 @@
|
||||
#include <QTemporaryDir>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <sys/resource.h>
|
||||
#include <csignal>
|
||||
#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<utun_config, decltype(&free_config)>;
|
||||
|
||||
class NodeConfigTest : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void identityFailureKeepsPreviousValues_data() {
|
||||
QTest::addColumn<bool>("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<bool> 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<QString>("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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <QFileInfo>
|
||||
#include <QHostInfo>
|
||||
#include <utility>
|
||||
|
||||
extern "C" {
|
||||
#include <openssl/evp.h>
|
||||
}
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QFileInfo>
|
||||
#include <QSet>
|
||||
|
||||
// =====================================================================
|
||||
// Whitelists — union of chatgui + uTun config_parser keys
|
||||
// =====================================================================
|
||||
|
||||
static const QSet<QString> 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<QString> SERVER_KEYS = {
|
||||
"addr", "so_mark", "fib", "netif", "type", "mtu", "only_local", "transport"
|
||||
};
|
||||
|
||||
static const QSet<QString> CLIENT_KEYS = {
|
||||
"peer_public_key", "link", "keepalive",
|
||||
"reality", "server_name", "short_id", "public_key", "version", "fingerprint"
|
||||
};
|
||||
|
||||
static const QSet<QString> CONTROL_KEYS = {
|
||||
"ip", "control_ip", "port", "control_port", "allow", "control_allow"
|
||||
};
|
||||
|
||||
static const QSet<QString> 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<QString> ALLOWED_KEYS_KEYS = {
|
||||
"allow_all", "key"
|
||||
};
|
||||
|
||||
static const QSet<QString> NAT_KEYS = {
|
||||
"enabled", "tun_ifname", "tun_ip", "nat_via", "port_start", "port_end", "forward"
|
||||
};
|
||||
|
||||
static const QSet<QString> FIREWALL_KEYS = {
|
||||
"allow"
|
||||
};
|
||||
|
||||
static const QSet<QString> ROUTING_KEYS = {
|
||||
"route_subnet", "my_subnet"
|
||||
};
|
||||
|
||||
static const QSet<QString> 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<QString> TCP_PROXY_SERVER_KEYS = {
|
||||
"enabled", "tcp_recv_buf"
|
||||
};
|
||||
|
||||
static const QSet<QString> NETWORK_KEYS = {
|
||||
"id", "pubkey", "signing_key"
|
||||
};
|
||||
|
||||
static const QSet<QString> 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<QString>* 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<const char*>(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<const char*>(keys.public_key), SC_PUBKEY_SIZE).toHex();
|
||||
m_privKey = QByteArray(reinterpret_cast<const char*>(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<QString>* 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<quint16>(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);
|
||||
}
|
||||
@@ -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 <cstdint>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
|
||||
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<QPair<QString,QString>> 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<NodeClient> 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<QPair<QString,QString>> m_servers;
|
||||
QList<NodeClient> 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
|
||||
Reference in new issue
Block a user