Files
tizi/src/base/Proxy_Ping0_Labeler.cpp
T
2026-07-29 20:45:14 +08:00

591 lines
24 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 <algorithm>
#include <array>
#include <chrono>
#include <filesystem>
#include <format>
#include <fstream>
#include <iostream>
#include <memory>
#include <iterator>
#include <limits>
#include <map>
#include <mutex>
#include <optional>
#include <sstream>
#include <system_error>
#include <thread>
#include <string>
#include <string_view>
#include <vector>
#include <nlohmann/json.hpp>
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<std::string> ipv4;
std::optional<std::string> ipv6;
std::optional<std::string> ipv4_error;
std::optional<std::string> 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<std::string> public_ipv6;
fs::path ip_cache_path;
fs::path ping0_html_path;
Ping0::Ip_Info info;
size_t order = 0;
std::optional<int> last_delay_ms;
};
std::vector<Node_Ping0_Result> ai_node_infos;
std::array<std::mutex, 256> 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<char>(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<std::streamsize>(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<std::string>() == key) {
return item.second;
}
}
return {};
}
std::optional<std::string> 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<std::string>());
if (text.empty()) {
return std::nullopt;
}
return text;
}
std::optional<uint16_t> 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<uint16_t>();
}
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<Node_Route> make_node_routes(const YAML::Node& proxies) {
std::vector<Node_Route> 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<std::string>();
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<std::string>{}(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>());
}
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<uint16_t> 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<fs::path> 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<int>::max();
}
int risk_sort_value(int value) {
return value >= 0 ? value : std::numeric_limits<int>::max();
}
int delay_sort_value(const Node_Ping0_Result& result) {
return result.last_delay_ms.value_or(std::numeric_limits<int>::max());
}
std::optional<int> 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<int>{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<std::string> 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<Node_Ping0_Result> load_node_ping0_result(const Node_Route& route, const fs::path& ip_cache_dir, const fs::path& html_cache_dir, std::optional<uint16_t> 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<Node_Ping0_Result> result;
std::string error;
};
asio::awaitable<void> load_node_ping0_result_task(asio::thread_pool& pool,
std::vector<Node_Ping0_Task_Result>& results,
size_t index,
fs::path ip_cache_dir,
fs::path html_cache_dir,
std::optional<uint16_t> mixed_port,
std::shared_ptr<size_t> 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<void> 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<size_t>(std::min(logical_cpu_count, 16u)));
if (routes.empty()) {
co_return;
}
asio::thread_pool pool(thread_count);
std::vector<Node_Ping0_Task_Result> results(routes.size());
for (size_t i = 0; i < routes.size(); ++i) {
results[i].route = routes[i];
}
auto pending = std::make_shared<size_t>(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<std::string> make_ai_site_proxy_list() {
std::map<std::string, size_t> country_count;
for (const auto& info : ai_node_infos) {
++country_count[country_from_location(info.info.location)];
}
std::vector<Node_Ping0_Result> 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<std::string> 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;
}