#include "db/ProfileFilter.hpp" #include "fmt/includes.h" #include "fmt/Preset.hpp" #include "main/HTTPRequestHelper.hpp" #include "GroupUpdater.hpp" #include #include #ifndef NKR_NO_YAML #include #endif namespace NekoGui_sub { GroupUpdater *groupUpdater = new GroupUpdater; void RawUpdater_FixEnt(const std::shared_ptr &ent) { if (ent == nullptr) return; auto stream = NekoGui_fmt::GetStreamSettings(ent->bean.get()); if (stream == nullptr) return; // 1. "security" if (stream->security == "none" || stream->security == "0" || stream->security == "false") { stream->security = ""; } else if (stream->security == "1" || stream->security == "true") { stream->security = "tls"; } // 2. TLS SNI: v2rayN config builder generate sni like this, so set sni here for their format. if (stream->security == "tls" && IsIpAddress(ent->bean->serverAddress) && (!stream->host.isEmpty()) && stream->sni.isEmpty()) { stream->sni = stream->host; } } int JsonEndIdx(const QString &str, int begin) { int sz = str.length(); int counter = 1; for (int i=begin+1;i Disect(const QString &str) { QList res = QList(); int idx=0; int sz = str.size(); while(idx < sz) { if (str[idx] == '\n') { idx++; continue; } if (str[idx] == '{') { int endIdx = JsonEndIdx(str, idx); if (endIdx == -1) return res; res.append(str.mid(idx, endIdx-idx + 1)); idx = endIdx+1; continue; } int nlineIdx = str.indexOf('\n', idx); if (nlineIdx == -1) nlineIdx = sz; res.append(str.mid(idx, nlineIdx-idx)); idx = nlineIdx+1; } return res; } void RawUpdater::update(const QString &str, bool needParse = true) { // Base64 encoded subscription if (auto str2 = DecodeB64IfValid(str); !str2.isEmpty()) { update(str2); return; } // Clash if (str.contains("proxies:")) { runOnUiThread([=] { MessageBoxWarning("Unsupported", "Clash Meta format is no longer supported."); }); return; } // Multi line if (str.count("\n") > 0 && needParse) { auto list = Disect(str); for (const auto &str2: list) { update(str2.trimmed(), false); } return; } // is comment or too short if (str.startsWith("//") || str.startsWith("#") || str.length() < 2) { return; } std::shared_ptr ent; bool needFix = true; // Nekoray format if (str.startsWith("nekoray://")) { needFix = false; auto link = QUrl(str); if (!link.isValid()) return; ent = NekoGui::ProfileManager::NewProxyEntity(link.host()); if (ent->bean->version == -114514) return; auto j = DecodeB64IfValid(link.fragment().toUtf8(), QByteArray::Base64UrlEncoding); if (j.isEmpty()) return; ent->bean->FromJsonBytes(j); } // Json if (str.startsWith('{')) { ent = NekoGui::ProfileManager::NewProxyEntity("custom"); auto bean = ent->CustomBean(); auto obj = QString2QJsonObject(str); if (obj.contains("outbounds")) { bean->core = "internal-full"; bean->config_simple = str; } else if (obj.contains("server")) { bean->core = "internal"; bean->config_simple = str; } else { return; } } // SOCKS if (str.startsWith("socks5://") || str.startsWith("socks4://") || str.startsWith("socks4a://") || str.startsWith("socks://")) { ent = NekoGui::ProfileManager::NewProxyEntity("socks"); auto ok = ent->SocksHTTPBean()->TryParseLink(str); if (!ok) return; } // HTTP if (str.startsWith("http://") || str.startsWith("https://")) { ent = NekoGui::ProfileManager::NewProxyEntity("http"); auto ok = ent->SocksHTTPBean()->TryParseLink(str); if (!ok) return; } // ShadowSocks if (str.startsWith("ss://")) { ent = NekoGui::ProfileManager::NewProxyEntity("shadowsocks"); auto ok = ent->ShadowSocksBean()->TryParseLink(str); if (!ok) return; } // VMess if (str.startsWith("vmess://")) { ent = NekoGui::ProfileManager::NewProxyEntity("vmess"); auto ok = ent->VMessBean()->TryParseLink(str); if (!ok) return; } // VLESS if (str.startsWith("vless://")) { ent = NekoGui::ProfileManager::NewProxyEntity("vless"); auto ok = ent->TrojanVLESSBean()->TryParseLink(str); if (!ok) return; } // Trojan if (str.startsWith("trojan://")) { ent = NekoGui::ProfileManager::NewProxyEntity("trojan"); auto ok = ent->TrojanVLESSBean()->TryParseLink(str); if (!ok) return; } // Naive if (str.startsWith("naive+")) { needFix = false; ent = NekoGui::ProfileManager::NewProxyEntity("naive"); auto ok = ent->NaiveBean()->TryParseLink(str); if (!ok) return; } // Hysteria1 if (str.startsWith("hysteria://")) { needFix = false; ent = NekoGui::ProfileManager::NewProxyEntity("hysteria"); auto ok = ent->QUICBean()->TryParseLink(str); if (!ok) return; } // Hysteria2 if (str.startsWith("hysteria2://") || str.startsWith("hy2://")) { needFix = false; ent = NekoGui::ProfileManager::NewProxyEntity("hysteria2"); auto ok = ent->QUICBean()->TryParseLink(str); if (!ok) return; } // TUIC if (str.startsWith("tuic://")) { needFix = false; ent = NekoGui::ProfileManager::NewProxyEntity("tuic"); auto ok = ent->QUICBean()->TryParseLink(str); if (!ok) return; } // Wireguard if (str.startsWith("wg://")) { needFix = false; ent = NekoGui::ProfileManager::NewProxyEntity("wireguard"); auto ok = ent->WireguardBean()->TryParseLink(str); if (!ok) return; } // SSH if (str.startsWith("ssh://")) { needFix = false; ent = NekoGui::ProfileManager::NewProxyEntity("ssh"); auto ok = ent->SSHBean()->TryParseLink(str); if (!ok) return; } if (ent == nullptr) return; // Fix if (needFix) RawUpdater_FixEnt(ent); // End NekoGui::profileManager->AddProfile(ent, gid_add_to); updated_order += ent; } #ifndef NKR_NO_YAML QString Node2QString(const YAML::Node &n, const QString &def = "") { try { return n.as().c_str(); } catch (const YAML::Exception &ex) { qDebug() << ex.what(); return def; } } QStringList Node2QStringList(const YAML::Node &n) { try { if (n.IsSequence()) { QStringList list; for (auto item: n) { list << item.as().c_str(); } return list; } else { return {}; } } catch (const YAML::Exception &ex) { qDebug() << ex.what(); return {}; } } int Node2Int(const YAML::Node &n, const int &def = 0) { try { return n.as(); } catch (const YAML::Exception &ex) { qDebug() << ex.what(); return def; } } bool Node2Bool(const YAML::Node &n, const bool &def = false) { try { return n.as(); } catch (const YAML::Exception &ex) { try { return n.as(); } catch (const YAML::Exception &ex2) { ex2.what(); } qDebug() << ex.what(); return def; } } // NodeChild returns the first defined children or Null Node YAML::Node NodeChild(const YAML::Node &n, const std::list &keys) { for (const auto &key: keys) { auto child = n[key]; if (child.IsDefined()) return child; } return {}; } #endif // 在新的 thread 运行 void GroupUpdater::AsyncUpdate(const QString &str, int _sub_gid, const std::function &finish) { auto content = str.trimmed(); bool asURL = false; bool createNewGroup = false; if (_sub_gid < 0 && (content.startsWith("http://") || content.startsWith("https://"))) { auto items = QStringList{ QObject::tr("As Subscription (add to this group)"), QObject::tr("As Subscription (create new group)"), QObject::tr("As link"), }; bool ok; auto a = QInputDialog::getItem(nullptr, QObject::tr("url detected"), QObject::tr("%1\nHow to update?").arg(content), items, 0, false, &ok); if (!ok) return; if (items.indexOf(a) <= 1) asURL = true; if (items.indexOf(a) == 1) createNewGroup = true; } runOnNewThread([=] { auto gid = _sub_gid; if (createNewGroup) { auto group = NekoGui::ProfileManager::NewGroup(); group->name = QUrl(str).host(); group->url = str; NekoGui::profileManager->AddGroup(group); gid = group->id; MW_dialog_message("SubUpdater", "NewGroup"); } Update(str, gid, asURL); emit asyncUpdateCallback(gid); if (finish != nullptr) finish(); }); } void GroupUpdater::Update(const QString &_str, int _sub_gid, bool _not_sub_as_url) { // 创建 rawUpdater NekoGui::dataStore->imported_count = 0; auto rawUpdater = std::make_unique(); rawUpdater->gid_add_to = _sub_gid; // 准备 QString sub_user_info; bool asURL = _sub_gid >= 0 || _not_sub_as_url; // 把 _str 当作 url 处理(下载内容) auto content = _str.trimmed(); auto group = NekoGui::profileManager->GetGroup(_sub_gid); if (group != nullptr && group->archive) return; // 网络请求 if (asURL) { auto groupName = group == nullptr ? content : group->name; MW_show_log(">>>>>>>> " + QObject::tr("Requesting subscription: %1").arg(groupName)); auto resp = NetworkRequestHelper::HttpGet(content); if (!resp.error.isEmpty()) { MW_show_log("<<<<<<<< " + QObject::tr("Requesting subscription %1 error: %2").arg(groupName, resp.error + "\n" + resp.data)); return; } content = resp.data; sub_user_info = NetworkRequestHelper::GetHeader(resp.header, "Subscription-UserInfo"); MW_show_log("<<<<<<<< " + QObject::tr("Subscription request fininshed: %1").arg(groupName)); } QList> in; // 更新前 QList> out_all; // 更新前 + 更新后 QList> out; // 更新后 QList> only_in; // 只在更新前有的 QList> only_out; // 只在更新后有的 QList> update_del; // 更新前后都有的,需要删除的新配置 QList> update_keep; // 更新前后都有的,被保留的旧配置 // 订阅解析前 if (group != nullptr) { in = group->Profiles(); group->sub_last_update = QDateTime::currentMSecsSinceEpoch() / 1000; group->info = sub_user_info; group->order.clear(); group->Save(); // if (NekoGui::dataStore->sub_clear) { MW_show_log(QObject::tr("Clearing servers...")); for (const auto &profile: in) { NekoGui::profileManager->DeleteProfile(profile->id); } } } // 解析并添加 profile rawUpdater->update(content); if (group != nullptr) { out_all = group->Profiles(); QString change_text; if (NekoGui::dataStore->sub_clear) { // all is new profile for (const auto &ent: out_all) { change_text += "[+] " + ent->bean->DisplayTypeAndName() + "\n"; } } else { // find and delete not updated profile by ProfileFilter NekoGui::ProfileFilter::OnlyInSrc_ByPointer(out_all, in, out); NekoGui::ProfileFilter::OnlyInSrc(in, out, only_in); NekoGui::ProfileFilter::OnlyInSrc(out, in, only_out); NekoGui::ProfileFilter::Common(in, out, update_keep, update_del, false); QString notice_added; QString notice_deleted; for (const auto &ent: only_out) { notice_added += "[+] " + ent->bean->DisplayTypeAndName() + "\n"; } for (const auto &ent: only_in) { notice_deleted += "[-] " + ent->bean->DisplayTypeAndName() + "\n"; } // sort according to order in remote group->order = {}; for (const auto &ent: rawUpdater->updated_order) { auto deleted_index = update_del.indexOf(ent); if (deleted_index > 0) { if (deleted_index >= update_keep.count()) continue; // should not happen auto ent2 = update_keep[deleted_index]; group->order.append(ent2->id); } else { group->order.append(ent->id); } } group->Save(); // cleanup for (const auto &ent: out_all) { if (!group->order.contains(ent->id)) { NekoGui::profileManager->DeleteProfile(ent->id); } } change_text = "\n" + QObject::tr("Added %1 profiles:\n%2\nDeleted %3 Profiles:\n%4") .arg(only_out.length()) .arg(notice_added) .arg(only_in.length()) .arg(notice_deleted); if (only_out.length() + only_in.length() == 0) change_text = QObject::tr("Nothing"); } MW_show_log("<<<<<<<< " + QObject::tr("Change of %1:").arg(group->name) + "\n" + change_text); MW_dialog_message("SubUpdater", "finish-dingyue"); } else { NekoGui::dataStore->imported_count = rawUpdater->updated_order.count(); MW_dialog_message("SubUpdater", "finish"); } } } // namespace NekoGui_sub bool UI_update_all_groups_Updating = false; #define should_skip_group(g) (g == nullptr || g->url.isEmpty() || g->archive || (onlyAllowed && g->skip_auto_update)) void serialUpdateSubscription(const QList &groupsTabOrder, int _order, bool onlyAllowed) { if (_order >= groupsTabOrder.size()) { UI_update_all_groups_Updating = false; return; } // calculate this group auto group = NekoGui::profileManager->GetGroup(groupsTabOrder[_order]); if (group == nullptr || should_skip_group(group)) { serialUpdateSubscription(groupsTabOrder, _order + 1, onlyAllowed); return; } int nextOrder = _order + 1; while (nextOrder < groupsTabOrder.size()) { auto nextGid = groupsTabOrder[nextOrder]; auto nextGroup = NekoGui::profileManager->GetGroup(nextGid); if (!should_skip_group(nextGroup)) { break; } nextOrder += 1; } // Async update current group UI_update_all_groups_Updating = true; NekoGui_sub::groupUpdater->AsyncUpdate(group->url, group->id, [=] { serialUpdateSubscription(groupsTabOrder, nextOrder, onlyAllowed); }); } void UI_update_all_groups(bool onlyAllowed) { if (UI_update_all_groups_Updating) { MW_show_log("The last subscription update has not exited."); return; } auto groupsTabOrder = NekoGui::profileManager->groupsTabOrder; serialUpdateSubscription(groupsTabOrder, 0, onlyAllowed); }