@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
JavaScript
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
});