#include "ConfigHandler.hpp" #include "common/HTTPRequestHelper.hpp" #include "common/QvHelpers.hpp" #include "components/plugins/QvPluginHost.hpp" #include "core/connection/Serialization.hpp" #include "core/handler/RouteHandler.hpp" #include "core/settings/SettingsBackend.hpp" namespace Qv2ray::core::handler { QvConfigHandler::QvConfigHandler(QObject *parent) : QObject(parent) { asyncRequestHelper = new NetworkRequestHelper(this); DEBUG(MODULE_CORE_HANDLER, "ConnectionHandler Constructor.") const auto connectionJson = JsonFromString(StringFromFile(QV2RAY_CONFIG_DIR + "connections.json")); const auto groupJson = JsonFromString(StringFromFile(QV2RAY_CONFIG_DIR + "groups.json")); // for (const auto &connectionId : connectionJson.keys()) { connections.insert(ConnectionId{ connectionId }, ConnectionObject::fromJson(connectionJson.value(connectionId).toObject())); } // for (const auto &groupId : groupJson.keys()) { auto groupObject = GroupObject::fromJson(groupJson.value(groupId).toObject()); if (groupObject.displayName.isEmpty()) { groupObject.displayName = tr("Group: %1").arg(GenerateRandomString(5)); } groups.insert(GroupId{ groupId }, groupObject); for (const auto &connId : groupObject.connections) { connections[connId].__qvConnectionRefCount++; } } // for (const auto &id : connections.keys()) { auto const &connectionObject = connections.value(id); if (connectionObject.__qvConnectionRefCount == 0) { QFile connectionFile(QV2RAY_CONNECTIONS_DIR + id.toString() + QV2RAY_CONFIG_FILE_EXTENSION); if (connectionFile.exists()) { if (!connectionFile.remove()) LOG(MODULE_CONNECTION, "Failed to remove connection config file") } connections.remove(id); LOG(MODULE_CORE_HANDLER, "Dropped connection id: " + id.toString() + " since it's not in a group") } else { const auto connectionFilePath = QV2RAY_CONNECTIONS_DIR + id.toString() + QV2RAY_CONFIG_FILE_EXTENSION; connectionRootCache[id] = CONFIGROOT(JsonFromString(StringFromFile(connectionFilePath))); DEBUG(MODULE_CORE_HANDLER, "Loaded connection id: " + id.toString() + " into cache.") } } // Force default group name. if (!groups.contains(DefaultGroupId)) { groups.insert(DefaultGroupId, {}); groups[DefaultGroupId].displayName = tr("Default Group"); groups[DefaultGroupId].isSubscription = false; } // kernelHandler = new KernelInstanceHandler(this); connect(kernelHandler, &KernelInstanceHandler::OnCrashed, this, &QvConfigHandler::OnKernelCrashed_p); connect(kernelHandler, &KernelInstanceHandler::OnStatsDataAvailable, this, &QvConfigHandler::OnStatsDataArrived_p); connect(kernelHandler, &KernelInstanceHandler::OnKernelLogAvailable, this, &QvConfigHandler::OnKernelLogAvailable); connect(kernelHandler, &KernelInstanceHandler::OnConnected, this, &QvConfigHandler::OnConnected); connect(kernelHandler, &KernelInstanceHandler::OnDisconnected, this, &QvConfigHandler::OnDisconnected); // tcpingHelper = new LatencyTestHost(5, this); connect(tcpingHelper, &LatencyTestHost::OnLatencyTestCompleted, this, &QvConfigHandler::OnLatencyDataArrived_p); // // Save per 1 minutes. saveTimerId = startTimer(1 * 60 * 1000); // Do not ping all... pingConnectionTimerId = startTimer(60 * 1000); } void QvConfigHandler::SaveConnectionConfig() { QJsonObject connectionsObject; for (const auto &key : connections.keys()) { connectionsObject[key.toString()] = connections[key].toJson(); } StringToFile(JsonToString(connectionsObject), QV2RAY_CONFIG_DIR + "connections.json"); // QJsonObject groupObject; for (const auto &key : groups.keys()) { groupObject[key.toString()] = groups[key].toJson(); } StringToFile(JsonToString(groupObject), QV2RAY_CONFIG_DIR + "groups.json"); RouteManager->SaveRoutes(); SaveGlobalSettings(); } void QvConfigHandler::timerEvent(QTimerEvent *event) { if (event->timerId() == saveTimerId) { SaveConnectionConfig(); } else if (event->timerId() == pingAllTimerId) { StartLatencyTest(); } else if (event->timerId() == pingConnectionTimerId) { auto id = kernelHandler->CurrentConnection(); if (!id.isEmpty() && GlobalConfig.advancedConfig.testLatencyPeriodcally) { StartLatencyTest(id.connectionId); } } } void QvConfigHandler::StartLatencyTest() { for (const auto &connection : connections.keys()) { StartLatencyTest(connection); } } void QvConfigHandler::StartLatencyTest(const GroupId &id) { for (const auto &connection : groups[id].connections) { StartLatencyTest(connection); } } void QvConfigHandler::StartLatencyTest(const ConnectionId &id) { emit OnLatencyTestStarted(id); tcpingHelper->TestLatency(id, GlobalConfig.networkConfig.latencyTestingMethod); } const QList QvConfigHandler::Subscriptions() const { QList subsList; for (const auto &group : groups.keys()) { if (groups[group].isSubscription) { subsList.push_back(group); } } return subsList; } void QvConfigHandler::ClearGroupUsage(const GroupId &id) { for (const auto &conn : groups[id].connections) { ClearConnectionUsage({ conn, id }); } } void QvConfigHandler::ClearConnectionUsage(const ConnectionGroupPair &id) { CheckValidId(id.connectionId, nothing); connections[id.connectionId].stats.Clear(); emit OnStatsAvailable(id, {}); PluginHost->Send_ConnectionStatsEvent({ GetDisplayName(id.connectionId), 0, 0, 0, 0 }); return; } const QList QvConfigHandler::GetGroupId(const ConnectionId &connId) const { CheckValidId(connId, {}); QList grps; for (const auto &groupId : groups.keys()) { const auto &group = groups[groupId]; if (group.connections.contains(connId)) { grps.push_back(groupId); } } return grps; } const std::optional QvConfigHandler::RenameConnection(const ConnectionId &id, const QString &newName) { CheckValidId(id, {}); OnConnectionRenamed(id, connections[id].displayName, newName); PluginHost->Send_ConnectionEvent({ Events::ConnectionEntry::Renamed, newName, connections[id].displayName }); connections[id].displayName = newName; SaveConnectionConfig(); return {}; } bool QvConfigHandler::RemoveConnectionFromGroup(const ConnectionId &id, const GroupId &gid) { CheckValidId(id, false); LOG(MODULE_CONNECTION, "Removing connection : " + id.toString()) if (groups[gid].connections.contains(id)) { auto removedEntries = groups[gid].connections.removeAll(id); if (removedEntries > 1) { LOG(MODULE_CONNECTION, "Found same connection occured multiple times in a group.") } // Decrease reference count. connections[id].__qvConnectionRefCount -= removedEntries; } if (GlobalConfig.autoStartId == ConnectionGroupPair{ id, gid }) { GlobalConfig.autoStartId.clear(); } // // Emit everything first then clear the connection map. PluginHost->Send_ConnectionEvent({ Events::ConnectionEntry::RemovedFromGroup, GetDisplayName(id), "" }); emit OnConnectionRemovedFromGroup({ id, gid }); // if (connections[id].__qvConnectionRefCount <= 0) { LOG(MODULE_CONNECTION, "Fully removing a connection from cache.") connectionRootCache.remove(id); // QFile connectionFile(QV2RAY_CONNECTIONS_DIR + id.toString() + QV2RAY_CONFIG_FILE_EXTENSION); if (connectionFile.exists()) { if (!connectionFile.remove()) LOG(MODULE_CONNECTION, "Failed to remove connection config file") } connections.remove(id); } return true; } bool QvConfigHandler::LinkConnectionWithGroup(const ConnectionId &id, const GroupId &newGroupId) { CheckValidId(id, false); if (groups[newGroupId].connections.contains(id)) { LOG(MODULE_CONNECTION, "Connection not linked since " + id.toString() + " is already in the group " + newGroupId.toString()) return false; } groups[newGroupId].connections.append(id); connections[id].__qvConnectionRefCount++; PluginHost->Send_ConnectionEvent({ Events::ConnectionEntry::LinkedWithGroup, connections[id].displayName, "" }); emit OnConnectionLinkedWithGroup({ id, newGroupId }); return true; } bool QvConfigHandler::MoveConnectionFromToGroup(const ConnectionId &id, const GroupId &sourceGid, const GroupId &targetGid) { CheckValidId(id, false); CheckValidId(targetGid, false); CheckValidId(sourceGid, false); // if (!groups[sourceGid].connections.contains(id)) { LOG(MODULE_CONNECTION, "Trying to move a connection away from a group it does not belong to.") return false; } if (groups[targetGid].connections.contains(id)) { LOG(MODULE_CONNECTION, "The connection: " + id.toString() + " has already been in the target group: " + targetGid.toString()) auto removedCount = groups[sourceGid].connections.removeAll(id); connections[id].__qvConnectionRefCount -= removedCount; } else { // If the target group does not contain this connection. auto removedCount = groups[sourceGid].connections.removeAll(id); connections[id].__qvConnectionRefCount -= removedCount; // groups[targetGid].connections.append(id); connections[id].__qvConnectionRefCount++; } emit OnConnectionRemovedFromGroup({ id, sourceGid }); emit OnConnectionLinkedWithGroup({ id, targetGid }); return true; } const std::optional QvConfigHandler::DeleteGroup(const GroupId &id) { CheckValidId(id, {}); if (!groups.contains(id) || id == NullGroupId) { return tr("Group does not exist"); } // Copy construct auto list = groups[id].connections; for (const auto &conn : list) { MoveConnectionFromToGroup(conn, id, DefaultGroupId); } // PluginHost->Send_ConnectionEvent({ Events::ConnectionEntry::FullyRemoved, groups[id].displayName, "" }); // groups.remove(id); SaveConnectionConfig(); emit OnGroupDeleted(id, list); if (id == DefaultGroupId) { groups[id].displayName = tr("Default Group"); } return {}; } bool QvConfigHandler::StartConnection(const ConnectionGroupPair &identifier) { CheckValidId(identifier, false); connections[identifier.connectionId].lastConnected = system_clock::to_time_t(system_clock::now()); // CONFIGROOT root = GetConnectionRoot(identifier.connectionId); const auto fullConfig = RouteManager->GenerateFinalConfig(root, groups[identifier.groupId].routeConfigId); // auto errMsg = kernelHandler->StartConnection(identifier, fullConfig); if (errMsg) { QvMessageBoxWarn(nullptr, tr("Failed to start connection"), *errMsg); return false; } GlobalConfig.lastConnectedId = identifier; return true; } void QvConfigHandler::RestartConnection() // const ConnectionId &id { StopConnection(); StartConnection(GlobalConfig.lastConnectedId); } void QvConfigHandler::StopConnection() // const ConnectionId &id { kernelHandler->StopConnection(); SaveConnectionConfig(); } void QvConfigHandler::OnKernelCrashed_p(const ConnectionGroupPair &id, const QString &errMessage) { LOG(MODULE_CORE_HANDLER, "Kernel crashed: " + errMessage) emit OnDisconnected(id); PluginHost->Send_ConnectivityEvent({ GetDisplayName(id.connectionId), {}, Events::Connectivity::Disconnected }); emit OnKernelCrashed(id, errMessage); } QvConfigHandler::~QvConfigHandler() { LOG(MODULE_CORE_HANDLER, "Triggering save settings from destructor") tcpingHelper->StopAllLatencyTest(); delete kernelHandler; SaveConnectionConfig(); } const CONFIGROOT QvConfigHandler::GetConnectionRoot(const ConnectionId &id) const { CheckValidId(id, CONFIGROOT()); return connectionRootCache.value(id); } void QvConfigHandler::OnLatencyDataArrived_p(const ConnectionId &id, const LatencyTestResult &result) { CheckValidId(id, nothing); connections[id].latency = result.avg; emit OnLatencyTestFinished(id, result.avg); } bool QvConfigHandler::UpdateConnection(const ConnectionId &id, const CONFIGROOT &root, bool skipRestart) { CheckValidId(id, false); // auto path = QV2RAY_CONNECTIONS_DIR + "/" + id.toString() + QV2RAY_CONFIG_FILE_EXTENSION; auto content = JsonToString(root); bool result = StringToFile(content, path); // connectionRootCache[id] = root; // emit OnConnectionModified(id); PluginHost->Send_ConnectionEvent({ Events::ConnectionEntry::Edited, connections[id].displayName, "" }); if (!skipRestart && kernelHandler->CurrentConnection().connectionId == id) { emit RestartConnection(); } return result; } const GroupId QvConfigHandler::CreateGroup(const QString &displayName, bool isSubscription) { GroupId id(GenerateRandomString()); groups[id].displayName = displayName; groups[id].isSubscription = isSubscription; groups[id].creationDate = system_clock::to_time_t(system_clock::now()); PluginHost->Send_ConnectionEvent({ Events::ConnectionEntry::Created, displayName, "" }); emit OnGroupCreated(id, displayName); SaveConnectionConfig(); return id; } const GroupRoutingId QvConfigHandler::GetGroupRoutingId(const GroupId &id) { if (groups[id].routeConfigId == NullRoutingId) { groups[id].routeConfigId = GroupRoutingId{ GenerateRandomString() }; } return groups[id].routeConfigId; } const std::optional QvConfigHandler::RenameGroup(const GroupId &id, const QString &newName) { CheckValidId(id, {}); if (!groups.contains(id)) { return tr("Group does not exist"); } OnGroupRenamed(id, groups[id].displayName, newName); PluginHost->Send_ConnectionEvent({ Events::ConnectionEntry::Renamed, newName, groups[id].displayName }); groups[id].displayName = newName; return {}; } bool QvConfigHandler::SetSubscriptionData(const GroupId &id, std::optional isSubscription, const std::optional &address, std::optional updateInterval) { CheckValidId(id, false); if (isSubscription.has_value()) groups[id].isSubscription = ACCESS_OPTIONAL_VALUE(isSubscription); if (address.has_value()) groups[id].subscriptionOption.address = ACCESS_OPTIONAL_VALUE(address); if (updateInterval.has_value()) groups[id].subscriptionOption.updateInterval = ACCESS_OPTIONAL_VALUE(updateInterval); return true; } bool QvConfigHandler::SetSubscriptionIncludeKeywords(const GroupId &id, const QStringList &Keywords) { CheckValidId(id, false); groups[id].subscriptionOption.IncludeKeywords.clear(); for (const auto &keyword : Keywords) { if (!keyword.trimmed().isEmpty()) { groups[id].subscriptionOption.IncludeKeywords.push_back(keyword); } } return true; } bool QvConfigHandler::SetSubscriptionIncludeRelation(const GroupId &id, SubscriptionFilterRelation relation) { CheckValidId(id, false); if (!groups.contains(id)) { return false; } groups[id].subscriptionOption.IncludeRelation = relation; return true; } bool QvConfigHandler::SetSubscriptionExcludeKeywords(const GroupId &id, const QStringList &Keywords) { CheckValidId(id, false); groups[id].subscriptionOption.ExcludeKeywords.clear(); for (const auto &keyword : Keywords) { if (!keyword.trimmed().isEmpty()) { groups[id].subscriptionOption.ExcludeKeywords.push_back(keyword); } } return true; } bool QvConfigHandler::SetSubscriptionExcludeRelation(const GroupId &id, SubscriptionFilterRelation relation) { CheckValidId(id, false); groups[id].subscriptionOption.ExcludeRelation = relation; return true; } void QvConfigHandler::UpdateSubscriptionAsync(const GroupId &id) { CheckValidId(id, nothing); if (!groups[id].isSubscription) return; asyncRequestHelper->AsyncHttpGet(groups[id].subscriptionOption.address, [=](const QByteArray &d) { CHUpdateSubscription_p(id, d); emit OnSubscriptionAsyncUpdateFinished(id); }); } bool QvConfigHandler::UpdateSubscription(const GroupId &id) { if (!groups[id].isSubscription) return false; const auto data = NetworkRequestHelper::HttpGet(groups[id].subscriptionOption.address); return CHUpdateSubscription_p(id, data); } bool QvConfigHandler::CHUpdateSubscription_p(const GroupId &id, const QByteArray &data) { CheckValidId(id, false); if (!groups.contains(id)) { return false; } // // ====================================================================================== Begin reading subscription auto _newConnections = GetConnectionConfigFromSubscription(data, GetDisplayName(id)); if (_newConnections.count() < 5) { LOG(MODULE_SUBSCRIPTION, "Found a subscription with less than 5 connections.") if (QvMessageBoxAsk(nullptr, tr("Update Subscription"), tr("%n entrie(s) have been found from the subscription source, do you want to continue?", "", _newConnections.count())) != QMessageBox::Yes) return false; } // // ====================================================================================== Begin Connection Data Storage // Anyway, we try our best to preserve the connection id. QMultiMap nameMap; QMultiMap, ConnectionId> typeMap; { // Store connection type metadata into map. for (const auto &conn : groups[id].connections) { nameMap.insert(GetDisplayName(conn), conn); const auto &&[protocol, host, port] = GetConnectionInfo(conn); if (port != 0) { typeMap.insert({ protocol, host, port }, conn); } } } // ====================================================================================== End Connection Data Storage // bool hasErrorOccured = false; // Copy construct here. auto originalConnectionIdList = groups[id].connections; groups[id].connections.clear(); // decltype(_newConnections) filteredConnections; // for (const auto &config : _newConnections) { // filter connections const bool isIncludeOperationAND = groups[id].subscriptionOption.IncludeRelation == RELATION_AND; const bool isExcludeOperationOR = groups[id].subscriptionOption.ExcludeRelation == RELATION_OR; // // Initial includeConfig value bool includeconfig = isIncludeOperationAND; { bool hasIncludeItemMatched = false; for (const auto &key : groups[id].subscriptionOption.IncludeKeywords) { if (!key.trimmed().isEmpty()) { hasIncludeItemMatched = true; // WARN: MAGIC, DO NOT TOUCH if (!isIncludeOperationAND == config.first.contains(key.trimmed())) { includeconfig = !isIncludeOperationAND; break; } } } // If includekeywords is empty then include all configs. if (!hasIncludeItemMatched) includeconfig = true; } if (includeconfig) { bool hasExcludeItemMatched = false; includeconfig = isExcludeOperationOR; for (const auto &key : groups[id].subscriptionOption.ExcludeKeywords) { if (!key.trimmed().isEmpty()) { hasExcludeItemMatched = true; // WARN: MAGIC, DO NOT TOUCH if (isExcludeOperationOR == config.first.contains(key.trimmed())) { includeconfig = !isExcludeOperationOR; break; } } } // If excludekeywords is empty then don't exclude any configs. if (!hasExcludeItemMatched) includeconfig = true; } if (includeconfig) { filteredConnections << config; } } LOG(MODULE_SUBSCRIPTION, "Filtered out less than 5 connections.") const auto useFilteredConnections = filteredConnections.count() > 5 || QvMessageBoxAsk(nullptr, tr("Update Subscription"), tr("%1 out of %n entrie(s) have been filtered out, do you want to continue?", "", _newConnections.count()) .arg(filteredConnections.count())) == QMessageBox::Yes; for (const auto &config : useFilteredConnections ? filteredConnections : _newConnections) { const auto &_alias = config.first; // Should not have complex connection we assume. bool canGetOutboundData = false; auto outboundData = GetConnectionInfo(config.second, &canGetOutboundData); // // ====================================================================================== Begin guessing new ConnectionId if (nameMap.contains(_alias)) { // Just go and save the connection... LOG(MODULE_CORE_HANDLER, "Reused connection id from name: " + _alias) const auto _conn = nameMap.take(_alias); groups[id].connections << _conn; UpdateConnection(_conn, config.second, true); // Remove Connection Id from the list. originalConnectionIdList.removeAll(_conn); typeMap.remove(typeMap.key(_conn)); } else if (canGetOutboundData && typeMap.contains(outboundData)) { LOG(MODULE_CORE_HANDLER, "Reused connection id from protocol/host/port pair for connection: " + _alias) const auto _conn = typeMap.take(outboundData); groups[id].connections << _conn; // Update Connection Properties UpdateConnection(_conn, config.second, true); RenameConnection(_conn, _alias); // Remove Connection Id from the list. originalConnectionIdList.removeAll(_conn); nameMap.remove(nameMap.key(_conn)); } else { // New connection id is required since nothing matched found... LOG(MODULE_CORE_HANDLER, "Generated new connection id for connection: " + _alias) CreateConnection(config.second, _alias, id, true); } // ====================================================================================== End guessing new ConnectionId } // Check if anything left behind (not being updated or changed significantly) LOG(MODULE_CORE_HANDLER, "Removed old connections not have been matched.") for (const auto &conn : originalConnectionIdList) { LOG(MODULE_CORE_HANDLER, "Removing connections not in the new subscription: " + conn.toString()) RemoveConnectionFromGroup(conn, id); } // Update the time groups[id].lastUpdatedDate = system_clock::to_time_t(system_clock::now()); return hasErrorOccured; } void QvConfigHandler::OnStatsDataArrived_p(const ConnectionGroupPair &id, const QMap &data) { if (id.isEmpty()) return; const auto &cid = id.connectionId; QMap result; for (const auto t : data.keys()) { const auto &stat = data[t]; connections[cid].stats[t].upLinkData += stat.first; connections[cid].stats[t].downLinkData += stat.second; result[t] = { stat, connections[cid].stats[t].toData() }; } emit OnStatsAvailable(id, result); PluginHost->Send_ConnectionStatsEvent({ GetDisplayName(cid), // result[CurrentStatAPIType].first.first, // result[CurrentStatAPIType].first.second, // result[CurrentStatAPIType].second.first, // result[CurrentStatAPIType].second.second }); } const ConnectionGroupPair QvConfigHandler::CreateConnection(const CONFIGROOT &root, const QString &displayName, const GroupId &groupId, bool skipSaveConfig) { LOG(MODULE_CORE_HANDLER, "Creating new connection: " + displayName) ConnectionId newId(GenerateUuid()); groups[groupId].connections << newId; connections[newId].creationDate = system_clock::to_time_t(system_clock::now()); connections[newId].displayName = displayName; connections[newId].__qvConnectionRefCount = 1; emit OnConnectionCreated({ newId, groupId }, displayName); PluginHost->Send_ConnectionEvent({ Events::ConnectionEntry::Created, displayName, "" }); UpdateConnection(newId, root); if (!skipSaveConfig) { SaveConnectionConfig(); } return { newId, groupId }; } } // namespace Qv2ray::core::handler #undef CheckIdExistance #undef CheckGroupExistanceEx #undef CheckGroupExistance #undef CheckConnectionExistanceEx #undef CheckConnectionExistance