/* GUI-проекция INI; сетевую конфигурацию разбирает ядро при запуске. */ #include "node_config.h" #include "config_updater.h" #include #include #include extern "C" { #include "../../../src/config_ini.h" #include "../../../src/transport_layer/secure_channel.h" } 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; } NodeConfig::NodeConfig(const QString& path) : m_path(path) {} bool NodeConfig::exists() const { return QFileInfo::exists(m_path); } /* Публикуем ключи/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; } 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) { if (exists()) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI profile creation rejected existing path=%s", qPrintable(m_path)); return false; } 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() { 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; } 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; } 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 = 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 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 (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 (section == "debug") { if (!next.m_debugCategories.isEmpty()) next.m_debugCategories += ','; next.m_debugCategories += key + '=' + value; } } 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() { 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; } 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()) { 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; } 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; } return config_write_lines(m_path, lines); } uint64_t NodeConfig::nodeIdU64() const { if (config_ini_hex(m_nodeId.toLatin1().constData(), nullptr, sizeof(uint64_t))) return 0; return m_nodeId.toULongLong(nullptr, 16); } quint16 NodeConfig::controlPort() const { 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 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); }