nekoray_Mahdi-zarei/sub/GroupUpdater.cpp

497 lines
17 KiB
C++

#include "db/ProfileFilter.hpp"
#include "fmt/includes.h"
#include "fmt/Preset.hpp"
#include "main/HTTPRequestHelper.hpp"
#include "GroupUpdater.hpp"
#include <QInputDialog>
#include <QUrlQuery>
#ifndef NKR_NO_YAML
#include <yaml-cpp/yaml.h>
#endif
namespace NekoGui_sub {
GroupUpdater *groupUpdater = new GroupUpdater;
void RawUpdater_FixEnt(const std::shared_ptr<NekoGui::ProxyEntity> &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<sz;i++) {
if (str[i] == '{') counter++;
if (str[i] == '}') counter--;
if (counter==0) return i;
}
return -1;
}
QList<QString> Disect(const QString &str) {
QList<QString> res = QList<QString>();
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<NekoGui::ProxyEntity> 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<std::string>().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<std::string>().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<int>();
} catch (const YAML::Exception &ex) {
qDebug() << ex.what();
return def;
}
}
bool Node2Bool(const YAML::Node &n, const bool &def = false) {
try {
return n.as<bool>();
} catch (const YAML::Exception &ex) {
try {
return n.as<int>();
} 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<std::string> &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<void()> &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>();
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<std::shared_ptr<NekoGui::ProxyEntity>> in; // 更新前
QList<std::shared_ptr<NekoGui::ProxyEntity>> out_all; // 更新前 + 更新后
QList<std::shared_ptr<NekoGui::ProxyEntity>> out; // 更新后
QList<std::shared_ptr<NekoGui::ProxyEntity>> only_in; // 只在更新前有的
QList<std::shared_ptr<NekoGui::ProxyEntity>> only_out; // 只在更新后有的
QList<std::shared_ptr<NekoGui::ProxyEntity>> update_del; // 更新前后都有的,需要删除的新配置
QList<std::shared_ptr<NekoGui::ProxyEntity>> 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<int> &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);
}