#include "qv2ray/v2/utils/HTTPRequestHelper.hpp" #include "db/Database.hpp" #include "db/ProfileFilter.hpp" #include "fmt/includes.h" #include "fmt/Preset.hpp" #include "GroupUpdater.hpp" #include #include #ifndef NKR_NO_EXTERNAL #include #endif #define FIRST_OR_SECOND(a, b) a.isEmpty() ? b : a namespace NekoRay::sub { GroupUpdater *groupUpdater = new GroupUpdater; void RawUpdater::update(const QString &str) { // Base64 encoded subscription if (auto str2 = DecodeB64IfValid(str);!str2.isEmpty()) { update(str2); return; } // Clash if (str.contains("proxies:")) { updateClash(str); return; } // Multi line if (str.count("\n") > 0) { auto list = str.split("\n"); for (const auto &str2: list) { update(str2.trimmed()); } return; } QSharedPointer ent; // Nekoray format if (str.startsWith("nekoray://")) { auto link = QUrl(str); if (!link.isValid()) return; ent = 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.toUtf8()); showLog("nekoray format: " + ent->bean->DisplayTypeAndName()); } // SOCKS if (str.startsWith("socks5://") || str.startsWith("socks4://") || str.startsWith("socks4a://") || str.startsWith("socks://")) { ent = ProfileManager::NewProxyEntity("socks"); auto ok = ent->SocksHTTPBean()->TryParseLink(str); if (!ok) return; } // HTTP if (str.startsWith("http://") || str.startsWith("https://")) { ent = ProfileManager::NewProxyEntity("http"); auto ok = ent->SocksHTTPBean()->TryParseLink(str); if (!ok) return; } // ShadowSocks if (str.startsWith("ss://")) { ent = ProfileManager::NewProxyEntity("shadowsocks"); auto ok = ent->ShadowSocksBean()->TryParseLink(str); if (!ok) return; } // VMess if (str.startsWith("vmess://")) { ent = ProfileManager::NewProxyEntity("vmess"); auto ok = ent->VMessBean()->TryParseLink(str); if (!ok) return; } // VMess if (str.startsWith("vless://")) { ent = ProfileManager::NewProxyEntity("vless"); auto ok = ent->TrojanVLESSBean()->TryParseLink(str); if (!ok) return; } // Trojan if (str.startsWith("trojan://")) { ent = ProfileManager::NewProxyEntity("trojan"); auto ok = ent->TrojanVLESSBean()->TryParseLink(str); if (!ok) return; } // Naive if (str.startsWith("naive+")) { ent = ProfileManager::NewProxyEntity("naive"); auto ok = ent->NaiveBean()->TryParseLink(str); if (!ok) return; } // Hysteria if (str.startsWith("hysteria://")) { // https://github.com/HyNetwork/hysteria/wiki/URI-Scheme ent = ProfileManager::NewProxyEntity("custom"); auto bean = ent->CustomBean(); auto url = QUrl(str); auto query = QUrlQuery(url.query()); if (url.host().isEmpty() || url.port() == -1 || !query.hasQueryItem("upmbps")) return; bean->name = url.fragment(); bean->serverAddress = url.host(); bean->serverPort = url.port(); bean->core = "hysteria"; bean->command = QString(Preset::Hysteria::command).split(" "); auto result = QString2QJsonObject(Preset::Hysteria::config); result["obfs"] = query.queryItemValue("obfsParam"); result["insecure"] = query.queryItemValue("insecure") == "1"; result["up_mbps"] = query.queryItemValue("upmbps").toInt(); result["down_mbps"] = query.queryItemValue("downmbps").toInt(); result["protocol"] = query.hasQueryItem("protocol") ? query.queryItemValue("protocol") : "udp"; if (query.hasQueryItem("auth")) result["auth_str"] = query.queryItemValue("auth"); if (query.hasQueryItem("alpn")) result["alpn"] = query.queryItemValue("alpn"); if (query.hasQueryItem("peer")) result["server_name"] = query.queryItemValue("peer"); bean->config_simple = QJsonObject2QString(result, false); } // End if (ent == nullptr) return; profileManager->AddProfile(ent, gid_add_to); update_counter++; } #ifndef NKR_NO_EXTERNAL QString Node2QString(const YAML::Node &n, const QString &def = "") { try { return n.as().c_str(); } catch (const YAML::Exception &ex) { return def; } } int Node2Int(const YAML::Node &n, const int &def = 0) { try { return n.as(); } catch (const YAML::Exception &ex) { return def; } } bool Node2Bool(const YAML::Node &n, const bool &def = false) { try { return n.as(); } catch (const YAML::Exception &ex) { 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 // https://github.com/Dreamacro/clash/wiki/configuration void RawUpdater::updateClash(const QString &str) { #ifndef NKR_NO_EXTERNAL try { auto proxies = YAML::Load(str.toStdString())["proxies"]; for (auto proxy: proxies) { auto type = Node2QString(proxy["type"]); if (type == "ss" || type == "ssr") type = "shadowsocks"; if (type == "socks5") type = "socks"; auto ent = ProfileManager::NewProxyEntity(type); if (ent->bean->version == -114514) continue; // common ent->bean->name = Node2QString(proxy["name"]); ent->bean->serverAddress = Node2QString(proxy["server"]); ent->bean->serverPort = Node2Int(proxy["port"]); if (type == "shadowsocks") { auto bean = ent->ShadowSocksBean(); bean->method = Node2QString(proxy["cipher"]).replace("dummy", "none"); bean->password = Node2QString(proxy["password"]); auto plugin_n = proxy["plugin"]; auto pluginOpts_n = proxy["plugin-opts"]; if (plugin_n.IsDefined() && pluginOpts_n.IsDefined()) { if (Node2QString(plugin_n) == "obfs") { bean->plugin = "obfs-local;obfs=" + Node2QString(pluginOpts_n["mode"]) + ";obfs-host=" + Node2QString(pluginOpts_n["host"]); } } auto protocol_n = proxy["protocol"]; if (protocol_n.IsDefined()) { continue; // SSR } } else if (type == "socks" || type == "http") { auto bean = ent->SocksHTTPBean(); bean->password = Node2QString(proxy["username"]); bean->password = Node2QString(proxy["password"]); if (Node2Bool(proxy["tls"])) bean->stream->security = "tls"; if (Node2Bool(proxy["skip-cert-verify"])) bean->stream->allow_insecure = true; } else if (type == "trojan") { auto bean = ent->TrojanVLESSBean(); bean->password = Node2QString(proxy["password"]); bean->stream->security = "tls"; bean->stream->network = Node2QString(proxy["network"], "tcp"); bean->stream->sni = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"])); if (Node2Bool(proxy["skip-cert-verify"])) bean->stream->allow_insecure = true; } else if (type == "vmess") { auto bean = ent->VMessBean(); bean->uuid = Node2QString(proxy["uuid"]); bean->aid = Node2Int(proxy["alterId"]); bean->security = Node2QString(proxy["cipher"]); bean->stream->network = Node2QString(proxy["network"], "tcp"); bean->stream->sni = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"])); if (Node2Bool(proxy["tls"])) bean->stream->security = "tls"; if (Node2Bool(proxy["skip-cert-verify"])) bean->stream->allow_insecure = true; auto ws = NodeChild(proxy, {"ws-opts", "ws-opt"}); if (ws.IsMap()) { auto headers = ws["headers"]; for (auto header: headers) { if (Node2QString(header.first).toLower() == "host") { bean->stream->host = Node2QString(header.second); } } bean->stream->path = Node2QString(ws["path"]); } auto grpc = NodeChild(proxy, {"grpc-opts", "grpc-opt"}); if (grpc.IsMap()) { bean->stream->path = Node2QString(grpc["grpc-service-name"]); } auto h2 = NodeChild(proxy, {"h2-opts", "h2-opt"}); if (h2.IsMap()) { auto hosts = ws["host"]; for (auto host: hosts) { bean->stream->host = Node2QString(host); break; } bean->stream->path = Node2QString(h2["path"]); } auto tcp_http = NodeChild(proxy, {"http-opts", "http-opt"}); if (tcp_http.IsMap()) { bean->stream->network = "tcp"; bean->stream->header_type = "http"; auto headers = tcp_http["headers"]; for (auto header: headers) { if (Node2QString(header.first).toLower() == "host") { bean->stream->host = Node2QString(header.second[0]); } break; } auto paths = tcp_http["path"]; for (auto path: paths) { bean->stream->path = Node2QString(path); break; } } } else { continue; } profileManager->AddProfile(ent, gid_add_to); update_counter++; } } catch (const YAML::Exception &ex) { runOnUiThread([=] { MessageBoxWarning("YAML Exception", ex.what()); }); } #endif } // 在新的 thread 运行 void GroupUpdater::AsyncUpdate(const QString &str, int _sub_gid, const std::function &finish) { auto content = str.trimmed(); bool asURL = false; if (_sub_gid < 0 && (content.startsWith("http://") || content.startsWith("https://"))) { auto items = QStringList{QObject::tr("As Subscription"), 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) == 0) asURL = true; } runOnNewThread([=] { Update(str, _sub_gid, asURL); emit asyncUpdateCallback(_sub_gid); if (finish != nullptr) finish(); }); } void GroupUpdater::Update(const QString &_str, int _sub_gid, bool _not_sub_as_url) { // 创建 rawUpdater NekoRay::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 = profileManager->GetGroup(_sub_gid); if (group != nullptr && group->archive) return; // 网络请求 if (asURL) { auto groupName = group == nullptr ? content : group->name; showLog(">>>>>>>> " + QObject::tr("Requesting subscription: %1").arg(groupName)); auto resp = NetworkRequestHelper::HttpGet(content); if (!resp.error.isEmpty()) { showLog("<<<<<<<< " + 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"); } QList> in; // 更新前 QList> out_all; // 更新前 + 更新后 QList> out; // 更新后 QList> only_in; // 只在更新前有的 QList> only_out; // 只在更新后有的 QList> update_del; // 更新前后都有的,删除更新后多余的 // 订阅解析前 if (group != nullptr) { in = group->Profiles(); group->last_update = QDateTime::currentSecsSinceEpoch(); group->info = sub_user_info; group->order.clear(); group->Save(); } // 解析并添加 profile rawUpdater->update(content); if (group != nullptr) { out_all = group->Profiles(); ProfileFilter::OnlyInSrc_ByPointer(out_all, in, out); ProfileFilter::OnlyInSrc(in, out, only_in); ProfileFilter::OnlyInSrc(out, in, only_out); ProfileFilter::Common(in, out, update_del, false, true); update_del += only_in; for (const auto &ent: update_del) { profileManager->DeleteProfile(ent->id); } QString notice_added; for (const auto &ent: only_out) { notice_added += ent->bean->DisplayTypeAndName() + "\n"; } QString notice_deleted; for (const auto &ent: only_in) { notice_deleted += ent->bean->DisplayTypeAndName() + "\n"; } runOnUiThread([=] { auto change = "\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 = QObject::tr("Nothing"); showLog("<<<<<<<< " + QObject::tr("Change of %1:").arg(group->name) + " " + change); dialog_message("SubUpdater", "finish-dingyue"); }); } else { NekoRay::dataStore->imported_count = rawUpdater->update_counter; runOnUiThread([=] { dialog_message("SubUpdater", "finish"); }); } } }