#ifndef CPPHTTPLIB_OPENSSL_SUPPORT #define CPPHTTPLIB_OPENSSL_SUPPORT #endif #include "Proxy_Ping0_Labeler.h" #include "Async_Blocking.h" #include "Ping0_Html_Parser.h" #include "export.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace { constexpr std::string_view ipv4_query_url_host = "https://api4.ipify.org"; constexpr std::string_view ipv6_query_url_host = "https://api6.ipify.org"; constexpr std::string_view ip_query_url_path = "/?format=json"; constexpr std::string_view ip_check_password = "ip-check"; constexpr long ip_query_timeout_ms = 15000; struct Node_Route { size_t index = 0; std::string username; std::string proxy_name; }; struct Public_Ip_Cache { std::optional ipv4; std::optional ipv6; std::optional ipv4_error; std::optional ipv6_error; bool has_ipv4_state() const { return ipv4.has_value() || ipv4_error.has_value(); } bool has_ipv6_state() const { return ipv6.has_value() || ipv6_error.has_value(); } }; struct Node_Ping0_Result { std::string name; std::string public_ipv4; std::optional public_ipv6; fs::path ip_cache_path; fs::path ping0_html_path; Ping0::Ip_Info info; size_t order = 0; std::optional last_delay_ms; }; std::vector ai_node_infos; std::array public_ip_cache_locks; fs::path path_from_utf8(std::string_view value) { return ::path_from_utf8(value); } std::string path_to_utf8(const fs::path& path) { return ::path_to_utf8(path); } std::string trim_ascii(std::string value) { const auto first = value.find_first_not_of(" \t\r\n"); if (first == std::string::npos) { return {}; } const auto last = value.find_last_not_of(" \t\r\n"); return value.substr(first, last - first + 1); } std::string make_utc_time() { const auto now = std::chrono::system_clock::now(); const auto value = std::chrono::system_clock::to_time_t(now); std::tm utc{}; #ifdef _WIN32 gmtime_s(&utc, &value); #else gmtime_r(&value, &utc); #endif return std::format("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday, utc.tm_hour, utc.tm_min, utc.tm_sec); } std::string make_safe_file_name(std::string_view name) { std::string result; result.reserve(name.size()); for (const unsigned char ch : name) { if (ch < 32 || ch == '"' || ch == '*' || ch == '/' || ch == ':' || ch == '<' || ch == '>' || ch == '?' || ch == '\\' || ch == '|') { result.push_back('_'); } else { result.push_back(static_cast(ch)); } } while (!result.empty() && (result.back() == ' ' || result.back() == '.')) { result.pop_back(); } return result.empty() ? std::string{"_"} : result; } void write_file_atomic(const fs::path& path, std::string_view content) { fs::create_directories(path.parent_path()); auto temporary_path = path; temporary_path += L".tmp"; { std::ofstream stream(temporary_path, std::ios::binary | std::ios::trunc); if (!stream) { throw std::runtime_error(std::format("无法创建文件:{}", path_to_utf8(temporary_path))); } stream.write(content.data(), static_cast(content.size())); } std::error_code error; fs::rename(temporary_path, path, error); if (error) { fs::remove(path, error); error.clear(); fs::rename(temporary_path, path, error); if (error) { fs::remove(temporary_path); throw std::runtime_error(std::format("保存文件失败:{},{}", path_to_utf8(path), error_text_to_utf8(error.message()))); } } } YAML::Node load_yaml_file(const fs::path& path) { std::ifstream stream(path, std::ios::binary); if (!stream) { throw std::runtime_error(std::format("无法打开 YAML:{}", path_to_utf8(path))); } return YAML::Load(stream); } YAML::Node find_yaml_value(const YAML::Node& node, std::string_view key) { if (!node.IsMap()) { return {}; } for (const auto& item : node) { if (item.first.IsScalar() && item.first.as() == key) { return item.second; } } return {}; } std::optional read_optional_string(const YAML::Node& node, std::string_view key) { const auto value = find_yaml_value(node, key); if (!value || !value.IsScalar()) { return std::nullopt; } const auto text = trim_ascii(value.as()); if (text.empty()) { return std::nullopt; } return text; } std::optional read_current_mixed_port(const fs::path& root) { const auto current_config = (root / L".." / L"config" / L"config.yaml").lexically_normal(); if (!fs::is_regular_file(current_config)) { std::cerr << std::format("Ping0 标注跳过未缓存出口 IP:找不到当前 Mihomo 配置 {}\n", path_to_utf8(current_config)); return std::nullopt; } try { const auto config = load_yaml_file(current_config); const auto port = find_yaml_value(config, "mixed-port"); if (!port || !port.IsScalar()) { std::cerr << std::format("Ping0 标注跳过未缓存出口 IP:当前 Mihomo 配置没有 mixed-port:{}\n", path_to_utf8(current_config)); return std::nullopt; } return port.as(); } catch (const std::exception& error) { std::cerr << std::format("Ping0 标注跳过未缓存出口 IP:读取当前 Mihomo 配置失败:{},{}\n", path_to_utf8(current_config), error_text_to_utf8(error.what())); return std::nullopt; } } std::vector make_node_routes(const YAML::Node& proxies) { std::vector routes; routes.reserve(proxies.size()); size_t username_index = 1; for (size_t i = 0; i < proxies.size(); ++i) { const auto name = proxies[i]["name"].as(); if (contain(name, "剩余流量") || contain(name, "套餐到期")) { continue; } routes.push_back({i, std::format("n{:04}", username_index++), name}); } return routes; } fs::path make_ip_cache_path(const fs::path& cache_dir, std::string_view proxy_name) { return cache_dir / path_from_utf8(make_safe_file_name(proxy_name)) / L"public_ip.yaml"; } std::mutex& public_ip_cache_lock(const fs::path& path) { return public_ip_cache_locks[std::hash{}(path_to_utf8(path)) % public_ip_cache_locks.size()]; } Public_Ip_Cache load_cached_public_ips(const fs::path& path) { Public_Ip_Cache result; if (!fs::is_regular_file(path)) { return result; } const auto cache = load_yaml_file(path); result.ipv4 = read_optional_string(cache, "public_ipv4"); if (!result.ipv4) { result.ipv4 = read_optional_string(cache, "public_ip"); } result.ipv6 = read_optional_string(cache, "public_ipv6"); result.ipv4_error = read_optional_string(cache, "public_ipv4_error"); result.ipv6_error = read_optional_string(cache, "public_ipv6_error"); return result; } std::string query_public_ip_by_user(std::string_view username, uint16_t mixed_port, std::string_view host) { httplib::Client client{std::string(host)}; client.set_proxy("127.0.0.1", mixed_port); client.set_proxy_basic_auth(std::string(username), std::string(ip_check_password)); client.set_follow_location(true); client.set_connection_timeout(std::chrono::milliseconds(std::min(ip_query_timeout_ms, 6000L))); client.set_read_timeout(std::chrono::milliseconds(ip_query_timeout_ms)); client.set_write_timeout(std::chrono::milliseconds(ip_query_timeout_ms)); client.set_max_timeout(std::chrono::milliseconds(ip_query_timeout_ms)); httplib::Headers headers{{"User-Agent", "wyc-node-ip-cache/1.0"}, {"Accept-Encoding", ""}}; const auto response = client.Get(std::string(ip_query_url_path), headers); if (!response) { throw std::runtime_error(std::format("ipify 请求失败:{}", httplib::to_string(response.error()))); } if (response->status < 200 || response->status >= 300) { throw std::runtime_error(std::format("ipify 返回 HTTP {}", response->status)); } const auto data = nlohmann::json::parse(response->body); return trim_ascii(data.at("ip").get()); } std::string source_url(std::string_view host) { return std::string(host) + std::string(ip_query_url_path); } void save_public_ip_cache(const fs::path& path, const Node_Route& route, const Public_Ip_Cache& cache, uint16_t mixed_port) { YAML::Node node(YAML::NodeType::Map); node["proxy_name"] = route.proxy_name; node["username"] = route.username; node["mixed_port"] = mixed_port; if (cache.ipv4) { node["public_ip"] = *cache.ipv4; node["public_ipv4"] = *cache.ipv4; } if (cache.ipv6) { node["public_ipv6"] = *cache.ipv6; } if (cache.ipv4_error) { node["public_ipv4_error"] = *cache.ipv4_error; } if (cache.ipv6_error) { node["public_ipv6_error"] = *cache.ipv6_error; } node["cached_at_utc"] = make_utc_time(); node["ipv4_source"] = source_url(ipv4_query_url_host); node["ipv6_source"] = source_url(ipv6_query_url_host); YAML::Emitter emitter; emitter.SetIndent(2); emitter << node; if (!emitter.good()) { throw std::runtime_error(std::format("生成出口 IP 缓存失败:{}", emitter.GetLastError())); } write_file_atomic(path, std::string_view(emitter.c_str(), emitter.size())); } Public_Ip_Cache get_public_ips(const Node_Route& route, const fs::path& cache_dir, std::optional mixed_port) { const auto cache_path = make_ip_cache_path(cache_dir, route.proxy_name); std::lock_guard lock(public_ip_cache_lock(cache_path)); auto cache = load_cached_public_ips(cache_path); bool changed = false; if (!mixed_port) { return cache; } if (!cache.has_ipv4_state()) { try { cache.ipv4 = query_public_ip_by_user(route.username, *mixed_port, ipv4_query_url_host); } catch (const std::exception& error) { cache.ipv4_error = error_text_to_utf8(error.what()); } changed = true; } if (!cache.has_ipv6_state()) { try { cache.ipv6 = query_public_ip_by_user(route.username, *mixed_port, ipv6_query_url_host); } catch (const std::exception& error) { cache.ipv6_error = error_text_to_utf8(error.what()); } changed = true; } if (changed) { save_public_ip_cache(cache_path, route, cache, *mixed_port); } return cache; } fs::path make_ping0_html_file_name(std::string_view ip) { return path_from_utf8(std::format("{}-高精度IP地址归属地查询-IP风控值查询-原生IP查询-IP类型查询-家庭宽带IP查询-全球小鸡监控平台.html", ip)); } std::optional find_ping0_html_path(const fs::path& cache_dir, std::string_view ip) { const auto exact = cache_dir / make_ping0_html_file_name(ip); if (fs::is_regular_file(exact)) { return exact; } if (!fs::is_directory(cache_dir)) { return std::nullopt; } const auto prefix = std::string(ip) + "-"; for (const auto& item : fs::directory_iterator(cache_dir)) { if (!item.is_regular_file()) { continue; } const auto name = path_to_utf8(item.path().filename()); if (name.starts_with(prefix) && path_to_utf8(item.path().extension()) == ".html") { return item.path(); } } return std::nullopt; } int ai_scene_score(const Ping0::Ip_Info& info) { for (const auto& scene : info.scenes) { if (scene.name.find("AI") != std::string::npos || scene.name.find("ai") != std::string::npos) { return scene.score; } } return -1; } std::string ai_scene_advice(const Ping0::Ip_Info& info) { for (const auto& scene : info.scenes) { if (scene.name.find("AI") != std::string::npos || scene.name.find("ai") != std::string::npos) { return scene.advice; } } return {}; } int first_number_or_max(std::string_view value) { int result = 0; bool has_number = false; for (const unsigned char ch : value) { if (ch >= '0' && ch <= '9') { result = result * 10 + ch - '0'; has_number = true; } else if (has_number) { return result; } } return has_number ? result : std::numeric_limits::max(); } int risk_sort_value(int value) { return value >= 0 ? value : std::numeric_limits::max(); } int delay_sort_value(const Node_Ping0_Result& result) { return result.last_delay_ms.value_or(std::numeric_limits::max()); } std::optional read_node_last_delay_ms(const fs::path& node_state_dir, std::string_view name) { const auto path = node_state_dir / path_from_utf8(make_safe_file_name(name) + ".json"); if (!fs::is_regular_file(path)) { return std::nullopt; } std::ifstream stream(path, std::ios::binary); const auto data = nlohmann::json::parse(stream); const auto delay = data.value("last_delay_ms", 0); return delay > 0 ? std::optional{delay} : std::nullopt; } std::string country_from_location(std::string_view location) { const auto first = location.find_first_not_of(" \t\r\n"); if (first == std::string_view::npos) { return "未知"; } const auto last = location.find_first_of(" \t\r\n", first); return std::string(location.substr(first, last == std::string_view::npos ? location.size() - first : last - first)); } std::string compact_text(std::string value) { value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char ch) { return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '(' || ch == ')' || ch == '%' || ch == '/' || ch == '\\' || ch == ':' || ch == '*' || ch == '?' || ch == '"' || ch == '<' || ch == '>' || ch == '|'; }), value.end()); return value; } std::string compact_ip_text(std::string value) { for (char& ch : value) { if (ch == ':') { ch = '-'; } } return compact_text(std::move(value)); } std::string make_proxy_name_suffix(const Node_Ping0_Result& result) { const auto ai_score = ai_scene_score(result.info); std::vector parts; if (ai_score >= 0) { parts.push_back(std::format("AI:{}星", ai_score)); } if (result.info.risk >= 0) { parts.push_back(std::format("风控:{}:{}", result.info.risk, compact_text(result.info.risk_level))); } if (!result.info.shared_users.empty()) { parts.push_back("共享:" + compact_text(result.info.shared_users)); } if (!result.info.ip_type.empty()) { parts.push_back(compact_text(result.info.ip_type)); } if (!result.info.native_type.empty()) { parts.push_back(compact_text(result.info.native_type)); } parts.push_back("ip:" + compact_ip_text(result.public_ipv4)); if (result.public_ipv6) { parts.push_back("_" + compact_ip_text(*result.public_ipv6)); } std::string suffix; for (const auto& part : parts) { if (!part.empty()) { suffix += " " + part; } } return suffix; } void print_ping0_info(const Node_Route& route, const Node_Ping0_Result& result) { const auto ai_score = ai_scene_score(result.info); const auto advice = ai_scene_advice(result.info); std::cout << std::format("Ping0 节点信息:{}\n", route.proxy_name); std::cout << std::format(" 用户: {}\n", route.username); std::cout << std::format(" 出口 IPv4: {}\n", result.public_ipv4); std::cout << std::format(" 出口 IPv6: {}\n", result.public_ipv6.value_or("无")); std::cout << std::format(" HTML 缓存: {}\n", path_to_utf8(result.ping0_html_path)); std::cout << std::format(" 位置: {}\n", result.info.location); std::cout << std::format(" ASN: {} {}\n", result.info.asn, result.info.asn_owner.name); std::cout << std::format(" IP 类型: {}\n", result.info.ip_type); std::cout << std::format(" 原生属性: {}\n", result.info.native_type); std::cout << std::format(" 风控: {}% {}\n", result.info.risk, result.info.risk_level); std::cout << std::format(" 共享人数: {}\n", result.info.shared_users); if (ai_score >= 0) { std::cout << std::format(" AI 应用: {} 星 {}\n", ai_score, advice); } } std::optional load_node_ping0_result(const Node_Route& route, const fs::path& ip_cache_dir, const fs::path& html_cache_dir, std::optional mixed_port) { const auto public_ips = get_public_ips(route, ip_cache_dir, mixed_port); if (!public_ips.ipv4) { std::cerr << std::format("Ping0 标注跳过:节点 {} 没有 IPv4 出口缓存\n", route.proxy_name); return std::nullopt; } const auto html_path = find_ping0_html_path(html_cache_dir, *public_ips.ipv4); if (!html_path) { std::cerr << std::format("【注意添加缓存数据】================================ Ping0 HTML 缓存不存在,节点 {},出口 IPv4 {},目录 {}\n", route.proxy_name, *public_ips.ipv4, path_to_utf8(html_cache_dir)); return std::nullopt; } auto info = Ping0::parse_file(*html_path); return Node_Ping0_Result{route.proxy_name, *public_ips.ipv4, public_ips.ipv6, make_ip_cache_path(ip_cache_dir, route.proxy_name), *html_path, std::move(info), route.index, std::nullopt}; } void record_ai_node_info(Node_Ping0_Result result, const fs::path& node_state_dir) { const auto ai_score = ai_scene_score(result.info); if (ai_score < 3) { return; } result.last_delay_ms = read_node_last_delay_ms(node_state_dir, result.name); ai_node_infos.push_back(std::move(result)); } struct Node_Ping0_Task_Result { Node_Route route; std::optional result; std::string error; }; asio::awaitable load_node_ping0_result_task(asio::thread_pool& pool, std::vector& results, size_t index, fs::path ip_cache_dir, fs::path html_cache_dir, std::optional mixed_port, std::shared_ptr pending, asio::steady_timer& signal) { try { const auto route = results[index].route; results[index].result = co_await WyC::async_blocking(pool, [route, ip_cache_dir, html_cache_dir, mixed_port] { return load_node_ping0_result(route, ip_cache_dir, html_cache_dir, mixed_port); }); } catch (const std::exception& error) { results[index].error = error_text_to_utf8(error.what()); } --*pending; signal.cancel(); } asio::awaitable append_ping0_info_to_proxy_names_async(YAML::Node& proxies) { const fs::path root = path_from_utf8(get_executable_dir()); const auto ip_cache_dir = (root / L".." / L"Node_Exit_Ip_Cache").lexically_normal(); const auto html_cache_dir = (root / L".." / L"Ping0_Html_Cache").lexically_normal(); const auto node_state_dir = (root / L".." / L"serer_state" / L"node_state").lexically_normal(); fs::create_directories(ip_cache_dir); fs::create_directories(html_cache_dir); ai_node_infos.clear(); const auto mixed_port = read_current_mixed_port(root); const auto routes = make_node_routes(proxies); const auto logical_cpu_count = std::max(1u, std::thread::hardware_concurrency()); const auto thread_count = std::min(routes.size(), static_cast(std::min(logical_cpu_count, 16u))); if (routes.empty()) { co_return; } asio::thread_pool pool(thread_count); std::vector results(routes.size()); for (size_t i = 0; i < routes.size(); ++i) { results[i].route = routes[i]; } auto pending = std::make_shared(routes.size()); auto executor = co_await asio::this_coro::executor; asio::steady_timer signal(executor); std::cout << std::format("Ping0 标注需要处理 {} 个节点,使用 {} 个 Asio 池线程\n", routes.size(), thread_count); for (size_t i = 0; i < routes.size(); ++i) { asio::co_spawn(executor, load_node_ping0_result_task(pool, results, i, ip_cache_dir, html_cache_dir, mixed_port, pending, signal), asio::detached); } while (*pending != 0) { signal.expires_after(std::chrono::hours(24)); std::error_code error; co_await signal.async_wait(asio::redirect_error(asio::use_awaitable, error)); } pool.stop(); pool.join(); for (auto& item : results) { if (!item.error.empty()) { std::cerr << std::format("Ping0 标注失败:{},{}\n", item.route.proxy_name, item.error); continue; } if (!item.result) { continue; } print_ping0_info(item.route, *item.result); const auto new_name = item.route.proxy_name + make_proxy_name_suffix(*item.result); proxies[item.route.index]["name"] = new_name; item.result->name = new_name; record_ai_node_info(std::move(*item.result), node_state_dir); } } } void append_ping0_info_to_proxy_names(YAML::Node& proxies) { asio::io_context io(1); std::exception_ptr error; asio::co_spawn(io, append_ping0_info_to_proxy_names_async(proxies), [&](std::exception_ptr current_error) { error = current_error; }); io.run(); if (error) { std::rethrow_exception(error); } } std::vector make_ai_site_proxy_list() { std::map country_count; for (const auto& info : ai_node_infos) { ++country_count[country_from_location(info.info.location)]; } std::vector list = ai_node_infos; std::stable_sort(list.begin(), list.end(), [&](const Node_Ping0_Result& left, const Node_Ping0_Result& right) { const auto left_country = country_from_location(left.info.location); const auto right_country = country_from_location(right.info.location); const auto left_count = country_count[left_country]; const auto right_count = country_count[right_country]; if (left_count != right_count) { return left_count > right_count; } const auto left_ai_score = ai_scene_score(left.info); const auto right_ai_score = ai_scene_score(right.info); if (left_ai_score != right_ai_score) { return left_ai_score > right_ai_score; } const auto left_risk = risk_sort_value(left.info.risk); const auto right_risk = risk_sort_value(right.info.risk); if (left_risk != right_risk) { return left_risk < right_risk; } const auto left_shared_users = first_number_or_max(left.info.shared_users); const auto right_shared_users = first_number_or_max(right.info.shared_users); if (left_shared_users != right_shared_users) { return left_shared_users < right_shared_users; } const auto left_delay = delay_sort_value(left); const auto right_delay = delay_sort_value(right); if (left_delay != right_delay) { return left_delay < right_delay; } return left.order < right.order; }); std::vector result; result.reserve(list.size()); for (const auto& info : list) { result.push_back(info.name); } std::cout << "AI 网站节点:按 AI>=3 星过滤,并按国家节点数量、AI 星级、风控、共享人数、延迟排序\n"; for (const auto& info : list) { const auto country = country_from_location(info.info.location); const auto delay = info.last_delay_ms ? std::format("{}ms", *info.last_delay_ms) : std::string{"未知"}; std::cout << std::format(" {},国家{}({}),AI{}星,风控{},共享{},延迟{}\n", info.name, country, country_count[country], ai_scene_score(info.info), info.info.risk, info.info.shared_users, delay); } return result; }