Skip to content

Instantly share code, notes, and snippets.

@xream
Last active March 25, 2024 22:26
Show Gist options
  • Save xream/4c4083769f24f1a7b4d254aad6917cf1 to your computer and use it in GitHub Desktop.
Save xream/4c4083769f24f1a7b4d254aad6917cf1 to your computer and use it in GitHub Desktop.
HTTP META
/**
* @Sub-Store-Page
* CNAME 接口查询去重/重命名 2023-11-16 20:34:08
* - 入口查询[国内spapi 识别到国外为ip-api] 落地查询[ip-api]
* - 根据接口返回的真实结果,重新对节点命名。
* - 添加入口城市、落地国家或地区、国内运营商信息,并对这些数据做持久化缓存(48小时有效期),减少API请求次数,提高运行效率。
* - 仅兼容 Surge, Loon 客户端。
* - Surge 需要固定带 ability 参数版本。
* 特别说明:
* - 符号:🅳电信 🅻联通 🆈移动 🅶广电 🅲公司 🆉直连 🎮游戏
* - 首次运行或者在没有缓存的情况下会通知进度
* - 无参数时的节点命名格式: "美国 01"
* - 1. 官方默认版(目前不带 ability 参数, 不保证以后不会改动): 》https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule
*
* - 2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如 [加国旗脚本或者cname脚本] 请使用此带 ability 参数版本: https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule
*
* - 3. 固定不带 ability 参数版本:https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule
*
* - 参数必须以"#"开头,多个参数使用"&"连接,例如 https://github.com/Keywos/rule/raw/main/cname.js#city&iisp&name=Name
* - 以下是此脚本支持的参数,必须以"#"开头,多个参数使用"&"连接,需要传入参数的话用 "=" 例如 "name=一元" 参考上述地址为例使用参数。
* - 无参数时的节点命名格式: "美国 01",如果 [入口IP或国家]或 [落地IP或国家]一样则为 "直连 德国 01"
* - 首次运行或者在没有缓存的情况下会通知进度
*
*
* 入口参数
* - [iisp] 增加入口运营商或者直连标识;
* - [city] 增加入口城市文字标识;
* - [sheng] 增加入口省份文字标识;
* - [yuan] 为境外入口添加真实的入口属地标识,当未配置此此参数时,则将境外入口统一标记为 [境外],默认未配置此参数;
* - [inflag] 增加入口国旗
*
* 落地参数
* - [yisp] 显示落地详细运营商名称;
* - [yw] 落地归属地使用英文缩写标识,不建议与其他入口参数配合使用,因为其他参数API没有返回英文;
* - [xy] 此参数关闭落地查询,仅查询入口;开启 yisp || yw || flag 参数后 xy 参数无效
*
* 图标参数
* - [game] 增加游戏节点标识;
* - [flag] 增加国家或地区的旗帜标识,默认无此参数;
* - [bl] 保留倍率标识;
* - [snone] 清理某地区内只有一个节点的序号;
*
* 分隔符参数
* - [fgf=] 设置入口和落地之间的分隔符,默认为空格;
* - [sn=] 设置国家与序号之间的分隔符,默认为空格;
* - [name=] 为节点添加机场名称前缀;
*
* 通知参数
* - [offtz] 关闭脚本通知;
*
* 解析参数
* - [dnsjx] 将节点域名解析为IP,普通用户不建议使用;
*
* 逻辑参数
* - [bs=] 批处理节点数建议10个左右,如果经常读不到节点建议减小批处理个数;
*
* 缓存参数
* - [h=] 节点缓存有效期,单位小时,时间参数只能二选一,Loon用户不需填写要此参数,请进入Sub-Store插件的配置界面自定义缓存有效期;
* - [min=] 节点缓存有效期,单位分钟,时间参数只能二选一,Loon用户不需填写要此参数,请进入Sub-Store插件的配置界面自定义缓存有效期;
*
* 超时参数
* - [timeout=] 当无任何节点缓存时测试节点HTTP延时允许的最大超时参数,超出允许范围则判定为无效节点,默认2000ms;
* - [cd=] 当有缓存时,会先读取缓存,直接输出结果;默认 [cd=]的值等于0,微直接读取缓存;
当设为更高的值: 比如'460'则每次读缓存都会再次处理之前判定为超时的节点,超时为460ms
*
* HTTP META 参数
* - [http_meta_protocol] 协议 默认: http
* - [http_meta_host] 服务地址 默认: 127.0.0.1
* - [http_meta_port] 端口号 默认: 9876
* - [http_meta_start_delay] 初始启动延时(单位: 毫秒) 默认: 3000
* - [http_meta_proxy_timeout] 每个节点耗时(单位: 毫秒). 此参数是为了防止脚本异常退出未关闭核心. 设置过小将导致核心过早退出. 目前逻辑: 启动初始的延时 + 每个节点耗时. 默认: 10000
* 其他参数
* - [debug] 调试日志,普通用户不建议使用。
* - 异常:如遇问题,Loon可以进入[配置]→[持久化缓存]→[删除指定数据]→输入Key [sub-store-cached-script-resource]并删除缓存。累计输出节点为0个3次以上将清理所有缓存
* - Surge需要进入[脚本编辑器]→左下角[设置]→[$persistentStore] [sub-store-cached-script-resource]删除缓存数据。
*/
const SUB_STORE_SCHEMA = {
title: "CNAME",
description: "根据接口返回的真实结果,重新对节点命名/去重。 如:入口/落地详细地区信息",
scope: ["Surge", "Loon"],
author: "@Key @奶茶姐 @小一 @可莉",
updateTime: "2023-11-11 18:26:00",
version: "1.2.2",
params: {
flag: {
datatype: "boolean",
description: "增加落地国家或地区的旗帜标识,默认无此参数",
defaultValue: false,
},
inflag:{
datatype: "boolean",
description: "增加入口国家或地区的旗帜标识,默认无此参数",
defaultValue: false,
},
xy: {
datatype: "boolean",
description: "关闭落地查询,仅查询入口;开启 yisp || yw || flag 参数后 xy 参数无效",
defaultValue: false,
},
iisp: {
datatype: "boolean",
description: "增加入口运营商或者直连标识",
defaultValue: false,
},
city: {
datatype: "boolean",
description: "增加入口城市文字标识",
defaultValue: false,
},
sheng: {
datatype: "boolean",
description: "增加入口省份文字标识",
defaultValue: false,
},
yuan: {
datatype: "boolean",
description:
"为境外入口添加真实的入口属地标识,当未配置此此参数时,则将境外入口统一标记为[境外],默认未配置此参数",
defaultValue: false,
},
yisp: {
datatype: "boolean",
description: "显示落地详细运营商名称",
defaultValue: false,
},
yw: {
datatype: "boolean",
description:
"落地归属地使用英文缩写标识,不建议与其他入口参数配合使用,因为其他参数API没有返回英文",
defaultValue: false,
},
game: {
datatype: "boolean",
description: "增加游戏节点标识",
defaultValue: false,
},
bl: {
datatype: "boolean",
description: "保留倍率标识",
defaultValue: false,
},
snone: {
datatype: "boolean",
description: "清理某地区内只有一个节点的序号",
defaultValue: false,
},
offtz: {
datatype: "boolean",
description: "关闭脚本通知",
defaultValue: false,
},
dnsjx: {
datatype: "boolean",
description: "将节点域名解析为IP, 普通用户不建议使用",
defaultValue: false,
},
debug: {
datatype: "boolean",
description: "调试日志,普通用户不建议使用",
defaultValue: false,
},
fgf: {
datatype: "string",
description: "设置入口和落地之间的分隔符,默认为空格",
defaultValue: " ",
},
sn: {
datatype: "string",
description: "设置国家与序号之间的分隔符,默认为空格",
defaultValue: " ",
},
name: {
datatype: "string",
description: "为节点添加机场名称前缀",
defaultValue: "",
},
timeout: {
datatype: "number",
description:
"当无任何节点缓存时测试节点HTTP延时允许的最大超时参数,超出允许范围则判定为无效节点,默认2000ms",
defaultValue: 2000,
},
cd: {
datatype: "number",
description:
"当有缓存时,会先读取缓存,直接输出结果;默认[cd=]的值等于0,微直接读取缓存; 当设为更高的值: 比如'460'则每次读缓存都会再次处理之前判定为超时的节点,超时为460ms",
defaultValue: 0,
},
bs: {
datatype: "number",
description:
"批处理节点数建议10个左右,如果经常读不到节点建议减小批处理个数",
defaultValue: 10,
},
h: {
datatype: "number",
description:
"节点缓存有效期,单位小时,时间参数只能二选一,Loon用户不需填写要此参数,请进入Sub-Store插件的配置界面自定义缓存有效期",
defaultValue: "",
},
min: {
datatype: "number",
description:
"节点缓存有效期,单位分钟,时间参数只能二选一,Loon用户不需填写要此参数,请进入Sub-Store插件的配置界面自定义缓存有效期",
defaultValue: "",
},
http_meta_protocol: {
datatype: "string",
description:
"HTTP META 协议",
defaultValue: "http",
},
http_meta_host: {
datatype: "string",
description:
"HTTP META 服务地址",
defaultValue: "127.0.0.1",
},
http_meta_port: {
datatype: "number",
description:
"HTTP META 端口号",
defaultValue: 9876,
},
http_meta_start_delay: {
datatype: "number",
description:
"HTTP META 初始启动延时(单位: 毫秒)",
defaultValue: 3000,
},
http_meta_proxy_timeout: {
datatype: "number",
description:
"每个节点耗时(单位: 毫秒). 此参数是为了防止脚本异常退出未关闭核心. 设置过小将导致核心过早退出. 目前逻辑: 启动初始的延时 + 每个节点耗时. 默认: 10000",
defaultValue: 10000,
},
},
};
const $ = $substore;
const iar = $arguments;
let FGF = iar.fgf == undefined ? " " : decodeURI(iar.fgf),FGFS = FGF,debug = iar.debug;
const { yw, bl, iisp, xy, yisp, yun, city, flag, inflag, game, yuan, sheng, offtz, snone: numone} = iar;
const h = iar.h ? decodeURI(iar.h) : "",min = iar.min ? decodeURI(iar.min) : "",firstN = iar.name ? decodeURI(iar.name) : "";
const XHFGF = iar.sn == undefined ? " " : decodeURI(iar.sn),{ isLoon: isLoon, isSurge: isSurge, isNode } = $substore.env, dns = iar.dnsjx,target = isLoon ? "Loon" : isSurge ? "Surge" : isNode ? 'ClashMeta' : undefined ,keypr= "peedtest";
let cd = iar.cd ? iar.cd : 0, timeout = iar.timeout ? iar.timeout : 2000, writet = "", innum = 1728e5, loontrue = false, onen = false, Sue = false, rawtime = 1500;
const keyp = "3.s",EXPIRATION_KEY = "#sub-store-csr-expiration-time";
if (min !== "") {
Sue = true;
innum = parseInt(min, 10) * 6e4;
writet = $.write(JSON.stringify(innum), EXPIRATION_KEY);
} else if (h !== "") {
Sue = true;
innum = parseInt(h, 10) * 36e5;
writet = $.write(JSON.stringify(innum), EXPIRATION_KEY);
} else {
writet = $.write(JSON.stringify(innum), EXPIRATION_KEY);
}
let TIMEDKEY = $.read(EXPIRATION_KEY),inapi=0;
// operator 外的其他函数要用到
let proxies
let http_meta_ports = [];
const http_meta_host = iar.http_meta_host || '127.0.0.1';
async function operator(e = [], targetPlatform, env) {
// 启动 HTTP META
proxies = ProxyUtils.produce(e, 'ClashMeta', 'internal')
const http_meta_port = iar.http_meta_port || 9876;
const http_meta_protocol = iar.http_meta_protocol || 'http';
const http_meta_api = `${http_meta_protocol}://${http_meta_host}:${http_meta_port}`;
// 若未手动关闭, 将按此超时设置自动关闭
// 此参数是为了防止脚本异常退出未关闭核心. 设置过小将导致核心过早退出. 目前逻辑: 启动初始的延时 + 每个节点耗时(因为测入口的节点为全部节点, 测落地的节点为核心支持的有效节点 先偷懒按全部节点计算)
const http_meta_start_delay = parseFloat(iar.http_meta_start_delay || 3000);
const http_meta_proxy_timeout = parseFloat(iar.http_meta_proxy_timeout || 10000);
const http_meta_timeout = http_meta_start_delay + (e.length * http_meta_proxy_timeout)
let http_meta_pid;
const res = await $.http.post({
url: `${http_meta_api}/start`,
timeout,
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
proxies,
timeout: http_meta_timeout,
}),
});
let body = res.body;
try {
body = JSON.parse(body);
} catch (e) {}
const { ports, pid } = body;
if (!pid || !ports) {
throw new Error(`failed to start http_meta: ${body}`);
}
http_meta_pid = pid;
http_meta_ports = ports;
console.log(`\n======== HTTP META 启动 ====\n[端口] ${ports}\n[PID] ${pid}\n[超时] 若未手动关闭 ${http_meta_timeout / 60 / 1000} 分钟后自动关闭\n[核心支持节点数] ${proxies.length}\n`);
if (debug) {
console.log(`[核心支持节点数据]\n${JSON.stringify(proxies, null, 2)}\n`);
}
$.info(`等待 ${http_meta_start_delay / 1000} 秒后开始检测`)
await $.wait(http_meta_start_delay)
let tzname = "",subcoll = "";
if (env?.source?.[e?.[0]?.subName]) {
tzname = env.source[e[0].subName].name;
subcoll = "单个订阅 ";
} else if (env?.source?._collection?.name){
tzname = env.source._collection.name;
subcoll = "组合订阅 ";
}
const startTime = new Date();
const support = isLoon || isSurge || isNode;
if (!xy) {
if (!support) {
$.notify("No Loon or Surge or Node")
$.error(`No Loon or Surge or Node`);
return e;
}
}
function klog(...arg) {
console.log('[CNAME] ' +subcoll+ tzname +" : "+ arg);
}
if (e.length < 1) {$.notify(subcoll +tzname,"订阅无节点","");return e;}
if (typeof scriptResourceCache === "undefined")return e;
let bs = iar.bs ? iar.bs : 9;
const ein = e.length;
const eins = ein/2;
klog(`开始处理节点: ${ein} 个`);
klog(`批处理节点数: ${bs} 个`);
klog(`设定api超时: ${zhTime(timeout)}`);
klog(`有缓api超时: ${zhTime(cd)}`);
// e = e.filter((item) => !nlc.test(item.name));
let o = 0,Pushtd = "",intimed = "",stops = false,rere=false,iflag="",cachen = 0;
while (o < e.length && !stops) {
const batchs = e.slice(o, o + 1);
await Promise.all(
batchs.map(async (pk) => {
try {
const inss = new Map();
const id = getid(pk);
if (inss.has(id)) {
return inss.get(id);
}
const cacheds = scriptResourceCache.get(id);
if (cacheds) cachen++;
if (cachen > eins) {
if (!onen) {
klog(`检查缓存数量: ${cachen}/${ein} 个`);
rawtime = timeout;
timeout = cd;
onen = true;
stops = true;
}
const readt = scriptResourceCache.gettime(id);
let nt = new Date().getTime();
let timedPush = "";
if (isLoon) {
let loontd = "";
const loonkkk={"1分钟":6e4,"5分钟":3e5,"10分钟":6e5,"30分钟":18e5,"1小时":36e5,"2小时":72e5,"3小时":108e5,"6小时":216e5,"12小时":432e5,"24小时":864e5,"48小时":1728e5,"72小时":2592e5,参数传入:"innums"};
intimed = $.read("#节点缓存有效期");
loontd = loonkkk[intimed] || 1728e5;
if (loontd == "innums") {
loontd = innum;
}
timedPush = zhTime(
parseInt(readt, 10) - nt + parseInt(loontd, 10)
);
} else if (target === "Surge" && Sue) {
timedPush = zhTime(
parseInt(readt, 10) - nt + parseInt(innum, 10)
);
} else {
timedPush = zhTime(
parseInt(readt, 10) - nt + parseInt(TIMEDKEY, 10)
);
}
Pushtd = `, ${timedPush}后过期 \n`;
}
} catch (err) {delog(err.message)}
})
);
o += 1;
}
if (!onen && !offtz) $.notify(subcoll+tzname, `开始处理节点: ${ein} 个 批处理数量: ${bs} 个`, "请等待处理完毕后再次点击预览");
let retryi = 0,breaki=false, isone = 0;
do {
let i = 0,newnode = [];isone++;
while (i < e.length) {
const batch = e.slice(i, i + bs);
await Promise.all(
batch.map(async (pk) => {
try {
let keyover = [], Yserver = pk.server,luodi = "",inQcip = "",nxx = "",adflag = "",OGame="",Oisp="",Oispflag="",Osh="", Oct="",zhi = "",yuanisp ="",isCN = false,v4 = false, v6 = false, isNoAli = false;
let inServer = await AliD(Yserver);
delog(inServer)
switch (inServer) {
case "keyn":
isNoAli = true;
inServer = Yserver;
break;
default:
pk.keyrk = inServer;
if (!isNoAli) {
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(inServer)) {
v4 = true;
} else if (/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/.test(inServer)) {
v6 = true;
}
}
break;
}
let btip = true,outu="",outips="";
if (!xy || yisp || yw || flag) {
if (!support) {
$.notify("No Loon or Surge or Node")
$.error(`No Loon or Surge or Node, 开启 yisp || yw || flag 参数后 xy 参数无效`);
return e;
}
const outip = await OUTIA(pk);
let {country:outUsq, countryCode:outUs, city:outCity, query:outQuery, isp:outisp} = outip;//落地
if (yisp) {
yuanisp = FGFS+outisp
};
debug && (pk.keyoutld = outip);
delog("落地信息 " + JSON.stringify(outip))
outu = outUs;
outips = outQuery;
luodi = (outUsq === "中国") ? outCity : (yw ? outUs : outUsq);
btip = outQuery !== inServer
};
if (btip || xy) {
if (!isNoAli || v4) {
const spkey = await SPEC(inServer);
let {country:inSpCn,regionName:inSpSheng,city:inSpCity,isp:inSpIsp,ip:inSpIp,countryCode:inCode} = spkey;
inflag && (iflag = getflag(inCode));
debug && (pk.keyinsp = spkey);
isCN = inSpCn === "中国";
inQcip = inServer;
const keycm = {电信:"🅳", 联通:"🅻", 移动: "🆈",广电:"🅶"};
if (isCN){
debug && (pk.keyinsp = spkey)
delog("国内入口 " + JSON.stringify(spkey));
if(iisp && flag){
inSpIsp=inSpIsp.replace(/中国/g, "")
flag && (Oispflag = keycm.hasOwnProperty(inSpIsp) ? keycm[inSpIsp] : "🅲");
} else if(iisp){
Oisp = /电信|联通|移动|广电/.test(inSpIsp) ? inSpIsp.replace(/中国/g, "") : "企业";
}
(inSpSheng === inSpCity) && (inSpCity = "");
if (sheng && city){
Osh = inSpSheng;Oct = inSpCity
} else if (sheng){
Osh = inSpSheng;
} else if (city){
Oct = inSpCity ? inSpCity : inSpSheng;
}
}
}
if (isNoAli || v6 || !isCN) {
const inip = await INIA(Yserver);
let {country: inUsq, city: inCity, query: inQuery, regionName: inIpSh, countryCode:inaCode} = inip;
inflag && (iflag = getflag(inaCode));
debug && (pk.keyinipapi = inip);
delog("ipapi入口 " + JSON.stringify(inip));
inQcip = inQuery; //去重ip
if (inUsq === "中国") {
// inCity === inUs ? (incity=inCity)
(/[a-zA-Z]/.test(inCity)) && (inCity = inIpSh);
(inCity === inIpSh) && (inIpSh="");
if (sheng && city){
Osh = inIpSh;Oct = inCity;
} else if (sheng){
Osh = inIpSh;
} else if (city){
Oct = inCity ? inCity : inIpSh;
}
// 运营商 未知
flag && (Oispflag = "🅲");
} else {
if(inQuery === outips){
flag && (Oispflag = "🆉");
(sheng || city || iisp) && (zhi = "直连");
} else if (yuan){
flag && (Oispflag = "🅲");
(sheng || city || iisp) && (zhi = inUsq);
} else {
flag && (Oispflag = "🆇");
(sheng || city || iisp) && (zhi = "境外");
}
}
}
} else {
flag && (Oispflag = "🆉");
(sheng || city || iisp) && (zhi = "直连");
}
flag && (adflag = getflag(outu));
game && (OGame = /game|游戏/i.test(pk.name) ? (flag ? "🎮" : FGF+"Game") : OGame);
if (bl){
const match = pk.name.match(/((倍率|X|x|×)\D?((\d\.)?\d+)\D?)|((\d\.)?\d+)(倍|X|x|×)/);
if (match) {
const matchVa = match[0].match(/(\d[\d.]*)/)[0];
if (matchVa !== "1") {
nxx = XHFGF + matchVa + "X";
}
}
}
(!iisp && !city && !sheng && !xy && !inflag) && (Oispflag = "",FGF ="");
keyover = keyover.concat(
firstN, Oispflag,Osh,Oct,Oisp,zhi,FGF,adflag,luodi,OGame,nxx,yuanisp
).filter(ki => ki !== "");
// delog(keyover)
let overName = keyover.join("");
xy && (overName = iflag +overName +FGF+ pk.name);
// delog(overName)
newnode.push(outips);
dns && (pk.server = inQcip);
pk.name = overName;
inflag && (pk.name = iflag + overName);
pk.qc = inQcip + outips;
} catch (err) {
if (inapi >= 1) {
retryi++;
breaki = true;
}
delog(err.message)
};
})
);
i += bs;
klog(`处理进度${i}/${ein}`)
if (!onen){
if(!offtz && (ein > (i*2))){
if (i >= (e.length / 3) && i < (e.length * 2 / 3) && ein>i) {
$.notify(subcoll+tzname, `处理进度${i}/${ein}`, "耐心等待, 请勿重复点击预览...");
}
}
await sleep(GRa());
}
}
!xy && (e = removels(e));
var eout = e.length;
if (eout > 3 && isSurge){
const allsame = newnode.every((value, index, arr) => value === arr[0]);
if(allsame){
klog(`未使用带指定节点功能的 SubStore, 或所有节点落地IP相同`);
$.notify('CNAME:点击以安装对应版本','未使用带指定节点功能的 SubStore,或所有节点落地IP相同','',{url: "https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule",})
return e;
}
}
if (inapi >= 1) {
retryi++;
timeout = rawtime;
onen = false;
spMap.clear();alMap.clear();iaMap.clear();oaMap.clear();
klog(`重试中...`);
} else {
retryi = 2;
}
} while(retryi < 2);
!xy && (e = removeqc(e));
e = jxh(e);
// if (firstN !== "") {e.forEach((pk) => {pk.name = firstN + " " + pk.name;});}
numone && (e = onee(e));
const endTime = new Date();
const timeDiff = endTime.getTime() - startTime.getTime();
if (dns) {
klog(`dns解析后共: ${eout} 个`);
}
apiRead > 0 ? klog(`读取api缓存: ${apiRead} 个`) : null;
apiw > 0 ? klog(`写入api缓存: ${apiw} 个`) : null;
klog(`处理完后剩余: ${eout} 个`);
const Nullv ='#SubStoreNullvalue';
if (eout === 0 && ein !== 0){
let Nullvi = parseInt($.read(Nullv), 10);
if (isNaN(Nullvi)) {
klog(`错误1次, 3次后将清理所有节点缓存`);
$.write("0", Nullv);
} else {
Nullvi += 1;
klog(`错误${Nullvi}次, 3次后将清理所有节点缓存`);
$.write(Nullvi.toString(), Nullv);
}
if (Nullvi > 3) {
$.write(null, "#sub-store-cached-script-resource");
klog(`错误${Nullvi}次, 已清理所有节点缓存`);
Nullvi = 0;
$.write(Nullvi.toString(), Nullv);
}
}
if (isLoon) {
klog("缓存过期时间: " + intimed + ", 还剩" + Pushtd.replace(/,|\n/g, ""));
} else {
klog("缓存过期时间: " +zhTime(TIMEDKEY) +", 还剩" +Pushtd.replace(/,|\n/g, ""));
}
klog(`此方法总用时: ${zhTime(timeDiff)}\n----For New CNAME----\n\n\n\n\n`);
const readklog = apiRead ? `读取缓存:${apiRead} ` : "";
const writeklog = apiw ? `写入缓存:${apiw}, ` : "";
const Push = (eout === ein && eout === 0) ? "" : (eout === ein ? "全部通过测试, " : "去除无效节点后有" + eout + "个, ");
if (!offtz) {$.notify(
`${subcoll}${tzname} 共${ein}个节点`,
"",
`${writeklog}${readklog}${Pushtd}${Push}用时:${zhTime(timeDiff)}`
);}
// 关闭 HTTP META
console.log(`\n======== HTTP META 关闭 ====\n[PID] ${pid}`);
try {
const res = await $.http.post({
url: `${http_meta_api}/stop`,
timeout,
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
pid: [http_meta_pid],
}),
});
console.log(res.body);
} catch (e) {
console.log(e);
}
return e;
}
function getflag(e) { const t = e .toUpperCase() .split("") .map((e) => 127397 + e.charCodeAt()); return String.fromCodePoint(...t).replace(/🇹🇼/g, "🇨🇳"); } function sleep(e) { return new Promise((t) => setTimeout(t, e)); } let apiRead = 0, apiw = 0;
const oaMap = new Map(); async function OUTIA(e) { const t = getid(e); if (oaMap.has(t)) return oaMap.get(t); const cached = scriptResourceCache.get(t);if (cached) { apiRead++; return cached; } else {inapi++;};const maxRE = 2; const url = `http://ip-api.com/json?lang=zh-CN&fields=status,message,country,countryCode,city,query,isp`; const getHttp = async (reTry) => { try { let r = ProxyUtils.produce([e], target);
// 找不到端口时, 通常为核心不支持的协议, 暂设为 0, 目前会导致结果中不包含该节点
const proxy = isNode ? `http://${http_meta_host}:${http_meta_ports[proxies.indexOf(ProxyUtils.produce([e], 'ClashMeta', 'internal')[0])] || 0}` : undefined; const response = await Promise.race([ $.http.get({ url: url, node: r, "policy-descriptor": r, proxy }), new Promise((_, reject) => setTimeout(() => reject(new Error("timeout-OUTIA")), timeout) ), ]); const data = JSON.parse(response.body); if (data.status === "success") { scriptResourceCache.set(t, data); return data; } else { throw new Error(resdata.message); } } catch (error) { if (reTry < maxRE) { await sleep(GRa()); delog(e.name + "-> [outipApi超时查询次数] " + reTry); return getHttp(reTry + 1); } else { throw error; } } }; const resGet = new Promise((resolve, reject) => { if (cd < 1 && onen) return resGet; getHttp(1) .then((data) => { apiw++; resolve(data); }) .catch(reject); }); oaMap.set(t, resGet); return resGet; };
const alMap = new Map(); async function AliD(e) { const ti = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/.test( e ); if (ti) return e; const t = getaliid(e); if (alMap.has(t)) return alMap.get(t); const cached = scriptResourceCache.get(t); if (cached) { apiRead++; return cached;} else {inapi++;};const maxRE = 2; let alip = Math.random() < 0.5 ? '223.5.5.5' : '223.6.6.6';const url = `https://${alip}/resolve?name=${e}&type=A&short=1`; const getHttp = async (reTry) => { try { const response = await Promise.race([ $.http.get({ url: url }), new Promise((_, reject) => setTimeout(() => reject(new Error("timeout-AliD")), timeout) ), ]); const resdata = JSON.parse(response.body); if (resdata.length > 0) { scriptResourceCache.set(t, resdata[0]); return resdata[0]; } else { return "keyn"; } } catch (error) { if (reTry < maxRE) { await sleep(GRa()); delog(e + " [->Ali超时查询次数] " + reTry); return getHttp(reTry + 1); } else { throw error; } } }; const resGet = new Promise((resolve, reject) => { if (cd < 1 && onen) { return resGet; } else { getHttp(1) .then((data) => { resolve(data); }) .catch(reject); } }); alMap.set(t, resGet); return resGet; };function getaliid(e){let t="al";return MD5(`${t}-${e}`)};function getspcn(e){let t="sc";return MD5(`${t}-${e}`)};
const spMap = new Map(); async function SPEC(e) { const n = getspcn(e); if (spMap.has(n)) return spMap.get(n);const cached = scriptResourceCache.get(n); if (cached) {apiRead++;return cached;} else {inapi++;}; const maxRE = 2; const url = `https://api-v${keyp}${keypr}.cn/ip?ip=${e}`; const getHttp = async (reTry) => { try { const response = await Promise.race([ $.http.get({ url: url }), new Promise((_, reject) => setTimeout(() => reject(new Error("timeout-SPEC")), timeout) ), ]); const resdata = JSON.parse(response.body); delog(resdata); if (resdata.data) { const { country: e, province: o, city: r, isp: i, ip: c, countryCode: k, } = resdata.data; const a = { country: e, regionName: o, city: r, isp: i, ip: c, countryCode: k, }; delog("写入"); scriptResourceCache.set(n, a); return a; } else { throw new Error(resdata.message); } } catch (error) { if (reTry < maxRE) { await sleep(GRa()); delog(e + "-> [SP超时查询次数] " + reTry); return getHttp(reTry + 1); } else { throw error; } } }; const resGet = new Promise((resolve, reject) => {if (cd < 1 && onen) return resGet; getHttp(1) .then((data) => { resolve(data); }) .catch(reject); }); spMap.set(n, resGet); return resGet; };
const iaMap = new Map(); async function INIA(e) { const t = getinid(e); if (iaMap.has(t)) return iaMap.get(t); const cached = scriptResourceCache.get(t); if (cached) {apiRead++;return cached;} else {inapi++;}; const maxRE = 2; const url = `http://ip-api.com/json/${e}?lang=zh-CN&fields=status,message,country,city,query,regionName,countryCode`; const getHttp = async (reTry) => { try { delog(url); const response = await Promise.race([ $.http.get({ url: url }), new Promise((_, reject) => setTimeout(() => reject(new Error("timeout-INIA")), timeout) ), ]); const data = JSON.parse(response.body); if (data.status === "success") { scriptResourceCache.set(t, data); return data; } else { throw new Error(resdata.message); } } catch (error) { if (reTry < maxRE) { await sleep(GRa()); delog(e + "-> [inipApi超时查询次数] " + reTry); return getHttp(reTry + 1); } else { throw error; } } }; const resGet = new Promise((resolve, reject) => { if (cd < 1 && onen) return resGet; getHttp(1) .then((data) => { resolve(data); }) .catch(reject); }); iaMap.set(t, resGet); return resGet; } function GRa() { return Math.floor(Math.random() * (500 - 50 + 1) + 50); };
function delog(...arg) { if (debug) { console.log("[CNAME] :" + arg); } } function removels(e) { const t = new Set(); const n = []; for (const s of e) { if (s.qc && !t.has(s.qc)) { t.add(s.qc); n.push(s); } } return n; } function removeqc(e) { const t = new Set(); const n = []; for (const s of e) { if (!t.has(s.qc)) { t.add(s.qc); const e = { ...s }; delete e.qc; n.push(e); } } return n; } function jxh(e) { const t = e.reduce((e, t) => { const n = e.find((e) => e.name === t.name); if (n) { n.count++; n.items.push({ ...t, name: `${t.name}${XHFGF}${n.count.toString().padStart(2, "0")}`, }); } else { e.push({ name: t.name, count: 1, items: [{ ...t, name: `${t.name}${XHFGF}01` }], }); } return e; }, []); const n = t.flatMap((e) => e.items); e.splice(0, e.length, ...n); return e; } function onee(e) { const t = e.reduce((e, t) => { const n = t.name.replace(/[^A-Za-z0-9\u00C0-\u017F\u4E00-\u9FFF]+\d+$/, ""); if (!e[n]) { e[n] = []; } e[n].push(t); return e; }, {}); for (const e in t) { if (t[e].length === 1 && t[e][0].name.endsWith("01")) { const n = t[e][0]; n.name = e; } } return e; } function zhTime(e) { e = e.toString().replace(/-/g, ""); if (e < 1e3) { return `${Math.round(e)}毫秒`; } else if (e < 6e4) { return `${Math.round(e / 1e3)}秒`; } else if (e < 36e5) { return `${Math.round(e / 6e4)}分钟`; } else if (e >= 36e5) { return `${Math.round(e / 36e5)}小时`; } }
var MD5=function(e){var t=M(V(Y(X(e),8*e.length)));return t.toLowerCase()};function M(e){for(var t,n="0123456789ABCDEF",s="",o=0;o<e.length;o++)t=e.charCodeAt(o),s+=n.charAt(t>>>4&15)+n.charAt(15&t);return s}function X(e){for(var t=Array(e.length>>2),n=0;n<t.length;n++)t[n]=0;for(n=0;n<8*e.length;n+=8)t[n>>5]|=(255&e.charCodeAt(n/8))<<n%32;return t}function V(e){for(var t="",n=0;n<32*e.length;n+=8)t+=String.fromCharCode(e[n>>5]>>>n%32&255);return t}function Y(e,t){e[t>>5]|=128<<t%32,e[14+(t+64>>>9<<4)]=t;for(var n=1732584193,s=-271733879,o=-1732584194,r=271733878,i=0;i<e.length;i+=16){var c=n,a=s,u=o,m=r;s=md5_ii(s=md5_ii(s=md5_ii(s=md5_ii(s=md5_hh(s=md5_hh(s=md5_hh(s=md5_hh(s=md5_gg(s=md5_gg(s=md5_gg(s=md5_gg(s=md5_ff(s=md5_ff(s=md5_ff(s=md5_ff(s,o=md5_ff(o,r=md5_ff(r,n=md5_ff(n,s,o,r,e[i+0],7,-680876936),s,o,e[i+1],12,-389564586),n,s,e[i+2],17,606105819),r,n,e[i+3],22,-1044525330),o=md5_ff(o,r=md5_ff(r,n=md5_ff(n,s,o,r,e[i+4],7,-176418897),s,o,e[i+5],12,1200080426),n,s,e[i+6],17,-1473231341),r,n,e[i+7],22,-45705983),o=md5_ff(o,r=md5_ff(r,n=md5_ff(n,s,o,r,e[i+8],7,1770035416),s,o,e[i+9],12,-1958414417),n,s,e[i+10],17,-42063),r,n,e[i+11],22,-1990404162),o=md5_ff(o,r=md5_ff(r,n=md5_ff(n,s,o,r,e[i+12],7,1804603682),s,o,e[i+13],12,-40341101),n,s,e[i+14],17,-1502002290),r,n,e[i+15],22,1236535329),o=md5_gg(o,r=md5_gg(r,n=md5_gg(n,s,o,r,e[i+1],5,-165796510),s,o,e[i+6],9,-1069501632),n,s,e[i+11],14,643717713),r,n,e[i+0],20,-373897302),o=md5_gg(o,r=md5_gg(r,n=md5_gg(n,s,o,r,e[i+5],5,-701558691),s,o,e[i+10],9,38016083),n,s,e[i+15],14,-660478335),r,n,e[i+4],20,-405537848),o=md5_gg(o,r=md5_gg(r,n=md5_gg(n,s,o,r,e[i+9],5,568446438),s,o,e[i+14],9,-1019803690),n,s,e[i+3],14,-187363961),r,n,e[i+8],20,1163531501),o=md5_gg(o,r=md5_gg(r,n=md5_gg(n,s,o,r,e[i+13],5,-1444681467),s,o,e[i+2],9,-51403784),n,s,e[i+7],14,1735328473),r,n,e[i+12],20,-1926607734),o=md5_hh(o,r=md5_hh(r,n=md5_hh(n,s,o,r,e[i+5],4,-378558),s,o,e[i+8],11,-2022574463),n,s,e[i+11],16,1839030562),r,n,e[i+14],23,-35309556),o=md5_hh(o,r=md5_hh(r,n=md5_hh(n,s,o,r,e[i+1],4,-1530992060),s,o,e[i+4],11,1272893353),n,s,e[i+7],16,-155497632),r,n,e[i+10],23,-1094730640),o=md5_hh(o,r=md5_hh(r,n=md5_hh(n,s,o,r,e[i+13],4,681279174),s,o,e[i+0],11,-358537222),n,s,e[i+3],16,-722521979),r,n,e[i+6],23,76029189),o=md5_hh(o,r=md5_hh(r,n=md5_hh(n,s,o,r,e[i+9],4,-640364487),s,o,e[i+12],11,-421815835),n,s,e[i+15],16,530742520),r,n,e[i+2],23,-995338651),o=md5_ii(o,r=md5_ii(r,n=md5_ii(n,s,o,r,e[i+0],6,-198630844),s,o,e[i+7],10,1126891415),n,s,e[i+14],15,-1416354905),r,n,e[i+5],21,-57434055),o=md5_ii(o,r=md5_ii(r,n=md5_ii(n,s,o,r,e[i+12],6,1700485571),s,o,e[i+3],10,-1894986606),n,s,e[i+10],15,-1051523),r,n,e[i+1],21,-2054922799),o=md5_ii(o,r=md5_ii(r,n=md5_ii(n,s,o,r,e[i+8],6,1873313359),s,o,e[i+15],10,-30611744),n,s,e[i+6],15,-1560198380),r,n,e[i+13],21,1309151649),o=md5_ii(o,r=md5_ii(r,n=md5_ii(n,s,o,r,e[i+4],6,-145523070),s,o,e[i+11],10,-1120210379),n,s,e[i+2],15,718787259),r,n,e[i+9],21,-343485551),n=safe_add(n,c),s=safe_add(s,a),o=safe_add(o,u),r=safe_add(r,m)}return Array(n,s,o,r)}function md5_cmn(e,t,n,s,o,r){return safe_add(bit_rol(safe_add(safe_add(t,e),safe_add(s,r)),o),n)}function md5_ff(e,t,n,s,o,r,i){return md5_cmn(t&n|~t&s,e,t,o,r,i)}function md5_gg(e,t,n,s,o,r,i){return md5_cmn(t&s|n&~s,e,t,o,r,i)}function md5_hh(e,t,n,s,o,r,i){return md5_cmn(t^n^s,e,t,o,r,i)}function md5_ii(e,t,n,s,o,r,i){return md5_cmn(n^(t|~s),e,t,o,r,i)}function safe_add(e,t){var n=(65535&e)+(65535&t);return(e>>16)+(t>>16)+(n>>16)<<16|65535&n}function bit_rol(e,t){return e<<t|e>>>32-t}function getid(e){let t="ld";return MD5(`${t}-${e.server}-${e.port}`)}function getinid(e){let t="ia";return MD5(`${t}-${e}`)};
/**
* 注意! 本脚本不进行域名解析 必须在此脚本操作前面先加一个 Sub-Store 的域名解析
*
* 使用示范: https://t.me/zhetengsha/1003
*
* HTTP META(https://github.com/xream/http-meta) 参数
* 不测落地可以不用 HTTP META
* - [http_meta_protocol] 协议 默认: http
* - [http_meta_host] 服务地址 默认: 127.0.0.1
* - [http_meta_port] 端口号 默认: 9876
* - [http_meta_start_delay] 初始启动延时(单位: 毫秒) 默认: 3000
* - [http_meta_proxy_timeout] 每个节点耗时(单位: 毫秒). 此参数是为了防止脚本异常退出未关闭核心. 设置过小将导致核心过早退出. 目前逻辑: 启动初始的延时 + 每个节点耗时. 默认: 10000
*
* 其它参数
* - [no_entrance] 为 true 时, 将关闭入口检测(不可与落地检测同时关闭)
* - [no_landing] 为 true 时, 将关闭落地检测(不可与入口检测同时关闭)
* - [entrance_api] 入口检测服务商 sp-cn,ip-api,ip-sb 英文逗号分隔. 默认 sp-cn
* - [landing_api] 落地检测服务商 sp-cn,ip-api,ip-sb 英文逗号分隔. 默认 ip-api
* - [uniq] 根据落地 IP 去重(关闭落地检测时, 将按入口 IP 去重)
* - [max_retry_count] 最大重试次数 默认: 2
* - [retry_delay] 重试延时(单位: 毫秒) 默认: 1000
* - [timeout] 超时(单位: 毫秒) 默认: 3000
* - [limit] 并发数 默认: 15
* - [debug] debug
*/
async function operator(proxies = []) {
const headers = {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
}
const $ = $substore
const cache = scriptResourceCache
const { isIP } = ProxyUtils
const entrances = {}
const landings = {}
let entranceAPIs = ($arguments.entrance_api || 'sp-cn')
.split(/\s*[,,]\s*/g)
.filter(i => ['sp-cn', 'ip-api', 'ip-sb'].includes(i))
if (entranceAPIs.length === 0) {
entranceAPIs = ['sp-cn']
}
console.log(`[入口检测服务商]`, entranceAPIs)
let landingAPIs = ($arguments.landing_api || 'ip-api')
.split(/\s*[,,]\s*/g)
.filter(i => ['sp-cn', 'ip-api', 'ip-sb'].includes(i))
if (landingAPIs.length === 0) {
landingAPIs = ['ip-api']
}
console.log(`[落地检测服务商]`, landingAPIs)
const no_entrance = $arguments.no_entrance
const no_landing = $arguments.no_landing
const debug = $arguments.debug
const uniq = $arguments.uniq
const max_retry_count = $arguments.max_retry_count ?? 2
const retry_delay = $arguments.retry_delay ?? 1000
const timeout = $arguments.timeout ?? 3000
const limit = $arguments.limit ?? 15 // more than 20 concurrency may result in surge TCP connection shortage.
const http_meta_host = $arguments.http_meta_host ?? '127.0.0.1'
const http_meta_port = $arguments.http_meta_port ?? 9876
const http_meta_protocol = $arguments.http_meta_protocol ?? 'http'
const http_meta_api = `${http_meta_protocol}://${http_meta_host}:${http_meta_port}`
// 若未手动关闭, 将按此超时设置自动关闭
// 此参数是为了防止脚本异常退出未关闭核心. 设置过小将导致核心过早退出. 目前逻辑: 启动初始的延时 + 每个节点耗时(因为测入口的节点为全部节点, 测落地的节点为核心支持的有效节点 先偷懒按全部节点计算)
const http_meta_start_delay = parseFloat($arguments.http_meta_start_delay ?? 3000)
const http_meta_proxy_timeout = parseFloat($arguments.http_meta_proxy_timeout ?? 10000)
const http_meta_timeout = http_meta_start_delay + proxies.length * http_meta_proxy_timeout
let http_meta_pid
let http_meta_ports = []
if (!no_entrance) {
const entranceList = [...new Set(proxies.filter(p => isIP(p.server)).map(c => c.server))]
const entranceBatch = Math.ceil(entranceList.length / limit)
for (let i = 0; i < entranceBatch; i++) {
const current = []
for (let ip of entranceList.splice(0, limit)) {
current.push(
Promise.any(entranceAPIs.map(service => entranceResolver(ip, ip, service)))
.then(result => {
entrances[ip] = result
if (debug) {
$.info(`Successfully resolved ip: ${ip} ➟ ${JSON.stringify(result, null, 2)}`)
}
})
.catch(err => {
if (debug) {
$.error(`Failed to resolve ip: ${ip}: ${err}`)
}
})
)
}
await Promise.all(current)
}
}
let internalProxies = proxies
if (!no_landing) {
// 启动 HTTP META
internalProxies = ProxyUtils.produce(proxies, 'ClashMeta', 'internal')
const res = await $.http.post({
url: `${http_meta_api}/start`,
timeout,
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
proxies: internalProxies,
timeout: http_meta_timeout,
}),
})
let body = res.body
try {
body = JSON.parse(body)
} catch (e) {}
const { ports, pid } = body
if (!pid || !ports) {
throw new Error(`======== HTTP META 启动失败 ====\n${body}`)
}
http_meta_pid = pid
http_meta_ports = ports
console.log(
`\n======== HTTP META 启动 ====\n[端口] ${ports}\n[PID] ${pid}\n[超时] 若未手动关闭 ${
http_meta_timeout / 60 / 1000
} 分钟后自动关闭\n[核心支持节点数] ${internalProxies.length}\n`
)
if (debug) {
console.log(`[核心支持节点数据]\n${JSON.stringify(internalProxies, null, 2)}\n`)
}
$.info(`等待 ${http_meta_start_delay / 1000} 秒后开始检测`)
await $.wait(http_meta_start_delay)
const landingList = [...internalProxies]
const landingBatch = Math.ceil(landingList.length / limit)
for (let i = 0; i < landingBatch; i++) {
const current = []
for (let proxy of landingList.splice(0, limit)) {
const index = internalProxies.indexOf(proxy)
current.push(
Promise.any(
landingAPIs.map(service =>
landingResolver(`http://${http_meta_host}:${http_meta_ports[index]}`, JSON.stringify(
Object.fromEntries(
Object.entries(proxy).filter(([key]) => !/^(name|collectionName|subName|id|_.*)$/i.test(key))
)
), service)
)
)
.then(result => {
landings[index] = result
if (debug) {
$.info(`Successfully resolved proxy: ${proxy.name} ➟ ${JSON.stringify(result, null, 2)}`)
}
})
.catch(err => {
if (debug) {
$.error(`Failed to resolve proxy: ${proxy.name}: ${err}`)
}
})
)
}
await Promise.all(current)
}
}
let result = []
const querySet = new Set()
for (const [i, p] of internalProxies.entries()) {
let entrance
if (!no_entrance) {
entrance = entrances[p.server]
if (entrance) {
entrance = parseInfo(entrance)
}
}
let landing
if (!no_landing) {
landing = landings[i]
if (landing) {
landing = parseInfo(landing)
}
} else if (entrance) {
landing = entrance
}
if (no_entrance && landing) {
entrance = landing
}
if (debug) {
if (!no_entrance) {
console.log(`入口`, entrance)
}
if (!no_landing) {
console.log(`落地`, landing)
}
}
if (entrance && landing) {
if (entrance.query === landing.query) {
p.name = `${entrance.flag} ${entrance.loc} ${entrance.isp} [${p.type}] (${p.name})`
} else {
p.name = `${entrance.flag} ${entrance.loc} ${entrance.isp} ➮ ${landing.flag} ${landing.loc} ${landing.isp} [${p.type}] (${p.name})`
}
p.name = p.name.replace(/\s+/g, ' ').replace(/,/g, '')
p._entrance = entrance
p._landing = landing
if (!uniq || !querySet.has(p._landing.query)) {
querySet.add(p._landing.query)
result.push(p)
}
if (no_landing) {
delete p._landing
}
if (no_entrance) {
delete p._entrance
}
}
}
if (!no_landing) {
// stop http meta
try {
const res = await $.http.post({
url: `${http_meta_api}/stop`,
timeout,
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
pid: [http_meta_pid],
}),
})
console.log(res.body)
} catch (e) {
console.log(e)
}
}
return result
async function entranceResolver(ip, _id, service = 'ip-api') {
const id = `xname-entrance-${service}${_id}`
const cached = cache.get(id)
if (cached) {
return cached
}
let retryCount = 0
while (retryCount < max_retry_count) {
try {
if (service === 'ip-sb') {
const res = await $.http.get({
url: `https://api.ip.sb/geoip/${ip}`,
timeout,
headers,
})
const body = res.body
let data = JSON.parse(body)
if (!data.message) {
data = {
...data,
query: data.ip || '',
isp: data.organization || '',
country: data.country || '',
city: data.city || '',
regionName: data.region || '',
countryCode: data.country_code || '',
}
cache.set(id, data)
return data
}
throw new Error(data.message)
} else if (service === 'sp-cn') {
const res = await $.http.get({
url: `https://forge.speedtest.cn/api/location/geo?ip=${ip}`,
timeout,
headers,
})
const body = res.body
let data = JSON.parse(body)
if (!data.message) {
data = {
...data,
query: data.ip || '',
isp: data.isp || '',
country: data.country || '',
city: data.city || '',
regionName: data.province || '',
countryCode: data.country_code || '',
}
cache.set(id, data)
return data
}
throw new Error(data.message)
} else {
const res = await $.http.get({
url: `http://ip-api.com/json/${ip}?lang=zh-CN&fields=status,message,countryCode,country,city,query,isp,regionName`,
timeout,
headers,
})
const body = res.body
const data = JSON.parse(body)
if (data.status === 'success') {
cache.set(id, data)
return data
}
throw new Error(data.message)
}
} catch (e) {
retryCount++
await $.wait(retry_delay)
if (retryCount === max_retry_count) {
throw e
}
}
}
}
async function landingResolver(proxy, _id, service = 'ip-api') {
const id = `xname-landing-${service}${_id}`
const cached = cache.get(id)
if (cached) {
return cached
}
let retryCount = 0
while (retryCount < max_retry_count) {
try {
if (service === 'ip-sb') {
const res = await $.http.get({
url: `https://api.ip.sb/geoip`,
timeout,
proxy,
headers,
})
const body = res.body
let data = JSON.parse(body)
if (!data.message) {
data = {
...data,
query: data.ip || '',
isp: data.organization || '',
country: data.country || '',
city: data.city || '',
regionName: data.region || '',
countryCode: data.country_code || '',
}
cache.set(id, data)
return data
}
throw new Error(data.message)
} else if (service === 'sp-cn') {
const res = await $.http.get({
url: `https://forge.speedtest.cn/api/location/info`,
timeout,
proxy,
headers,
})
const body = res.body
let data = JSON.parse(body)
if (!data.message) {
data = {
...data,
query: data.ip || '',
isp: data.isp || '',
country: data.country || '',
city: data.city || '',
regionName: data.province || '',
countryCode: data.country_code || '',
}
cache.set(id, data)
return data
}
throw new Error(data.message)
} else {
const res = await $.http.get({
url: `http://ip-api.com/json?lang=zh-CN&fields=status,message,countryCode,country,city,query,isp,regionName`,
timeout,
proxy,
headers,
})
const body = res.body
const data = JSON.parse(body)
if (data.status === 'success') {
cache.set(id, data)
return data
}
throw new Error(data.message)
}
} catch (e) {
retryCount++
await $.wait(retry_delay)
if (retryCount === max_retry_count) {
throw e
}
}
}
}
function getflag(e) {
const t = e
.toUpperCase()
.split('')
.map(e => 127397 + e.charCodeAt())
// return String.fromCodePoint(...t).replace(/🇹🇼/g, '🇨🇳');
return String.fromCodePoint(...t).replace(/🇹🇼/g, '🇼🇸')
}
function parseInfo(info) {
let loc = ''
let city_regex = /(省|特别市|市|區|区|城)$/
let city = info.city.replace(city_regex, '')
let country = info.country.replace(/德意志联邦共和国/, '德国').replace(/(联邦)$/, '')
let regionName = info.regionName.replace(city_regex, '')
const isp_regex =
/\s*(Co\.,?\s*?Ltd\.?|,?\s*?Inc\.?|\s+Ltd|Cloud Services|limited|Networks|Network|\s+LLC|Corporation|Technologies|Technology|Data Centers|Province|\.com|\.org|\.net|Hosting|\s+Host|\s+data center|Co\.,)\s*$/i
let isp = info.isp
.replace(/\s*communications/i, '')
.replace(/\s*Industries Solutions/i, '')
.replace(/\s*Enterprise Solutions/i, '')
.replace(/(\s*)Cloud Service/i, '$1Cloud')
.replace(/Hong Kong/i, 'HK')
.replace(isp_regex, '')
.replace(isp_regex, '')
if (['CN'].includes(info.countryCode)) {
if (regionName !== city) {
loc = `${regionName} ${city}`
} else {
loc = `${city}`
}
} else {
loc = `${country} ${city}`
}
return {
...info,
city,
country,
flag: getflag(info.countryCode),
loc,
isp,
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment