UNPKG

@xiaozhu2007/koishi-plugin-netutils

Version:

[<ruby>多合一网络探测工具<rp>(</rp><rt>适用于 DN42</rt><rp>)</rp></ruby>](https://www.npmjs.com/package/@xiaozhu2007/koishi-plugin-netutils) 适用于 Bird-lg-go 的 traceroute 和 whois 组件。支持引入 Routinator 实现 ROA 检查。

244 lines (238 loc) 10.1 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, name: () => name, usage: () => usage }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var Sentry = __toESM(require("@sentry/node-core/light")); var name = "netutils"; var usage = `按下方配置完成后启用插件即可使用`; var logger = new import_koishi.Logger(name); var Config = import_koishi.Schema.object({ lgEndpoint: import_koishi.Schema.string().description("Bird LG 查询 API 端点").default("https://lg.wolfyang.fan/api/"), rpkiEndpoint: import_koishi.Schema.string().description("RPKI 服务器查询 API 端点").default("https://rpki.xmsl.im/api/v1"), defaultServer: import_koishi.Schema.string().description("Bird LG 默认查询服务器").default("v1"), userAgent: import_koishi.Schema.string().description("默认使用的 User-Agent").default("Mozilla/5.0 (compatible GoNoBot/1.1.4 +https://932.moe/bot)"), debug: import_koishi.Schema.boolean().description("是否开启调试模式").default(false), sentryDsn: import_koishi.Schema.string().description("Sentry DSN,用于错误报告。留空则不发送错误数据。").default("").experimental() }); function apply(ctx, config) { const globalConfig = config; const dsn = config.sentryDsn || process.env.SENTRY_DSN || ""; if (dsn) { Sentry.init({ dsn, sendDefaultPii: true }); if (config.debug) { logger.info("Sentry 已初始化"); } } const BIRD_LG_GO_API = globalConfig.lgEndpoint.trim(); const RPKI_API = globalConfig.rpkiEndpoint.trim(); const USER_AGENT = globalConfig.userAgent; const ipv4Pattern = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; const ipv6Pattern = /^((?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}$)(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:))$/i; const domainPattern = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]{2,6}$/i; const ipv4Regex = /^(?:\d{1,3}\.){3}\d{1,3}$/; const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; const is_valid_target = /* @__PURE__ */ __name((target) => ipv4Pattern.test(target) || ipv6Pattern.test(target) || domainPattern.test(target), "is_valid_target"); const is_valid_asn = /* @__PURE__ */ __name((asn_str) => { try { const asn = parseInt(asn_str); return !isNaN(asn) && asn > 0 && asn <= 4294967295; } catch (_e) { return false; } }, "is_valid_asn"); const is_valid_prefix = /* @__PURE__ */ __name((prefix) => { try { const parts = prefix.split("/"); if (parts.length !== 2) return false; const ip = parts[0]; const mask = parseInt(parts[1]); return (ipv4Regex.test(ip) || ipv6Regex.test(ip)) && !isNaN(mask) && mask > 0; } catch (_e) { return false; } }, "is_valid_prefix"); const handleFetchError = /* @__PURE__ */ __name((error, context) => { throw new Error(`${context} 失败: ${error.message}`); }, "handleFetchError"); const queryLG = /* @__PURE__ */ __name(async (apiType, target, servers = [globalConfig.defaultServer]) => { const payload = { servers, type: apiType, args: target }; try { const response = await fetch(BIRD_LG_GO_API, { method: "POST", headers: { "User-Agent": USER_AGENT, "Content-Type": "application/json" }, body: JSON.stringify(payload) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (data.error) throw new Error(data.error); return data; } catch (error) { handleFetchError(error, "API"); } }, "queryLG"); const queryRPKI = /* @__PURE__ */ __name(async (asn, prefix) => { const url = `${RPKI_API}/validity/${asn}/${prefix}`; try { const response = await fetch(url, { headers: { "User-Agent": USER_AGENT } }); if (!response.ok) throw new Error(`HTTP ${response.status})`); const data = await response.json(); if (data.error) throw new Error(data.error); return data; } catch (error) { handleFetchError(error, "RPKI"); } }, "queryRPKI"); const formatVrpsSection = /* @__PURE__ */ __name((vrps, yesEmoji, noEmoji) => { if (vrps.length > 0) { return `${yesEmoji} 匹配 VRPs: ${vrps.map((vrp) => ` 🌐 ASN: ${vrp.asn} 📍 前缀:${vrp.prefix} 🔑 最大长度:${vrp.max_length}`).join("\n")}`; } return `${noEmoji} 未找到匹配项`; }, "formatVrpsSection"); const formatResponse = /* @__PURE__ */ __name((data) => { try { const { validated_route, generatedTime } = data; const { route, validity } = validated_route; const vrps = validity.VRPs || {}; const stateEmoji = validity.state === "valid" ? "✅" : "❌"; const stateStr = validity.state.charAt(0).toUpperCase() + validity.state.slice(1); let formatted = ` 🌐 路由信息 🏢 源 ASN: ${route.origin_asn} 📍 前缀:${route.prefix} ${stateEmoji} 校验结果 💬 状态:${stateStr} 📝 简介:${validity.description}`; if (validity.reason) { formatted += ` 🚫 原因:${validity.reason.toUpperCase()}`; } formatted += ` 🔍 VRP 分析: ${formatVrpsSection(vrps.matched, "✅", "❌")} ${formatVrpsSection(vrps.unmatched_as, "❌", "✅")} ${formatVrpsSection(vrps.unmatched_length, "❌", "✅")}`; return `🔍 资源公钥基础设施查询服务 ${formatted} ⏳ 报告生成时间 ${generatedTime}`; } catch (error) { throw new Error("API 响应格式错误"); } }, "formatResponse"); const sendError = /* @__PURE__ */ __name((session, message) => session.send(`❌ ${message}`), "sendError"); const sendWarning = /* @__PURE__ */ __name((session, message) => session.send(`⚠️ ${message}`), "sendWarning"); const sendSuccess = /* @__PURE__ */ __name((session, message) => session.send(message), "sendSuccess"); ctx.command("traceroute <target> [server]").alias("trace").action(async ({ session }, target, server) => { if (!target) { sendError(session, "请输入追踪目标,格式为:traceroute <IP/域名> [服务器]"); return; } if (!is_valid_target(target)) { sendError(session, "无效的目标地址,请提供有效的 IPv4/IPv6 地址或域名"); return; } try { const response = await queryLG("traceroute", target, [server || globalConfig.defaultServer]); const results = response.result?.map((item) => item.data?.trim())?.filter(Boolean); if (results?.length) { sendSuccess(session, `🔍 追踪结果: ${results.join("\n\n")}`); } else { sendWarning(session, "未获取到有效追踪结果"); } } catch (error) { sendError(session, `查询失败 ${error.message}`); } }); ctx.command("whois <target>").action(async ({ session }, target) => { if (!target) { sendError(session, "请输入查询目标,格式为:whois <IP/域名/ASN>"); return; } try { const response = await queryLG("whois", target); const whoisData = response.result?.map((item) => item.data?.trim())?.find((data) => data); if (whoisData) { sendSuccess(session, `🔍 Whois 结果: ${whoisData}`); } else { sendWarning(session, "未找到相关 Whois 信息"); } } catch (error) { sendError(session, `查询失败 ${error.message}`); } }); ctx.command("xmroa <asn> <prefix>").alias("roa").action(async ({ session }, asn, prefix) => { if (!asn || !prefix) { sendError(session, "无效的 ASN 或 IP 格式,请输入有效的数字 ASN 和 IPv4/IPv6 前缀"); return; } if (!is_valid_asn(asn)) { sendError(session, "无效的 ASN 格式,请输入有效的数字 ASN(如:13335)"); return; } if (!is_valid_prefix(prefix)) { sendError(session, "无效的 IP 格式,请输入有效的 IPv4/IPv6 前缀(如:1.1.1.1/24)"); return; } try { const response = await queryRPKI(asn, prefix); const formattedResult = formatResponse(response); sendSuccess(session, formattedResult); } catch (error) { sendError(session, `查询失败 ${error.message}`); } }); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, name, usage });