/** # Quantumult X 资源解析器 (2020-05-08: 20:33) 本资源解析器作者: Shawn(请勿私聊问怎么用),有bug请反馈: @Shawn_KOP_bot 更新请关注tg频道: https://t.me/QuanX_API 主要功能: 将各类服务器订阅解析成 QuantumultX 格式引用(支持 V2RayN/SSR/SS/Trojan/QuanX(conf&list)/Surge3⬆️(conf&list)格式),并提供下列可选参数; 附加功能: rewrite(重写) /filter(分流) 过滤, 可用于解决无法单独禁用远程引用中某(几)条 rewrite/hostname/filter, 以及直接导入 Surge 类型规则 list 的问题 0️⃣ 请在“订阅链接”后加入 "#" 后再加参数, 不同参数间请使用 "&" 来连接, 如: "https://mysub.com#in=香港+台湾&emoji=1&tfo=1" 1️⃣ "节点"订阅--参数说明: - in, out, 分别为 保留/排除, 多参数用 "+" 连接, 可直接用中文, 空格用"%20"代替 (如 "in=香港+台湾&out=香港%20BGP" ); - emoji=1,2 或 -1, 为添加/删除节点名中的 emoji 旗帜 (国行设备请用 emoji=2 ); - udp=1, tfo=1 参数开启 udp-relay 及 fast-open (默认关闭, 此参数对源类型为 QuanX/Surge 的链接无效); - rename 重命名, rename=旧名@新名, 以及 "前缀@", "@后缀", 用 "+" 连接, 如 "rename=香港@HK+[SS]@+@[1X]"; - cert=0,跳过证书验证(vmess/trojan),即强制"tls-verification=false"; - tls13=1, 开启 "tls13=true"(vmess/trojan), 请自行确认服务端是否支持; - sort=1 或 sort=-1, 排序参数,分别根据节点名 正序/逆序 排列 2⃣️ "rewrite(重写)/filter(分流)"引用--参数说明: - 参数为 "out=xxx", 多个参数用 "+" 连接; - 分流规则额外支持 "policy=xx" 参数, 可用于直接指定策略组,或者为 Surge 格式的 rule-set 生成策略组(默认"Shawn"策略组) 3⃣️ 通用参数: info=1, 用于打开资源解析器的提示通知 (默认关闭), - rewrite/filter 类型则会强制在有 out 参数时开启通知提示被删除(禁用)的内容,以防止规则误删除 */ /** * 使用说明, 0️⃣ 在Quantumult X 配置文件中[general] 部分,加入 resource_parser_url=https://raw.githubusercontent.com/KOP-XIAO/QuantumultX/master/Scripts/resource-parser.js 1️⃣ 假设原始订阅连接为: https://raw.githubusercontent.com/crossutility/Quantumult-X/master/server-complete.txt , 2️⃣ 假设你想要保留的参数为 in=tls+ss, 想要过滤的参数为 out=http+2, 请注意下面订阅链接后一定要加 ”#“ 符号 3️⃣ 则填入 Quanx 节点引用的的总链接为 https://raw.githubusercontent.com/crossutility/Quantumult-X/master/server-complete.txt#in=tls+ss&out=http+2 4️⃣ 填入上述链接并打开的资源解析器开关 5⃣️ 因为 rewrite/filter 的 UI 中暂时没有提供解析器开关,需要去配置文件中的相关行,自行添加参数以开启,如: https://Advertising.list, tag=🚦去广告,update-interval=86400, opt-parser=true, enabled=true */ var content0=$resource.content; var para=decodeURIComponent($resource.link); var type0=Type_Check(content0); var Pin0=para.indexOf("in=")!=-1? para.split("#")[1].split("in=")[1].split("&")[0].split("+"):null; var Pout0=para.indexOf("out=")!=-1? para.split("#")[1].split("out=")[1].split("&")[0].split("+"):null; var Pemoji=para.indexOf("emoji=")!=-1? para.split("#")[1].split("emoji=")[1].split("&")[0].split("+"):null; var Pudp0=para.indexOf("udp=")!=-1? para.split("#")[1].split("udp=")[1].split("&")[0].split("+"):0; var Ptfo0=para.indexOf("tfo=")!=-1? para.split("#")[1].split("tfo=")[1].split("&")[0].split("+"):0; var Pinfo=para.indexOf("info=")!=-1? para.split("#")[1].split("info=")[1].split("&")[0].split("+"):0; var Prname=para.indexOf("rename=")!=-1? para.split("#")[1].split("rename=")[1].split("&")[0].split("+"):null; var Ppolicy=para.indexOf("policy=")!=-1? para.split("#")[1].split("policy=")[1].split("&")[0].split("+"):"Shawn"; var Pcert0=para.indexOf("cert=")!=-1? para.split("#")[1].split("cert=")[1].split("&")[0].split("+"):1; var Psort0=para.indexOf("sort=")!=-1? para.split("#")[1].split("sort=")[1].split("&")[0].split("+"):0; var PTls13=para.indexOf("tls13=")!=-1? para.split("#")[1].split("tls13=")[1].split("&")[0].split("+"):0; //$notify(type0) if(type0=="Vmess"){ total=V2QX(content0,Pudp0,Ptfo0,Pcert0,PTls13); flag=1; }else if(type0=="QuanX"){ total=isQuanX(content0); flag=1; }else if(type0=="SSR"){ total=SSR2QX(content0,Pudp0,Ptfo0); flag=1; }else if(type0=="Trojan"){ total=TJ2QX(content0,Pudp0,Ptfo0,Pcert0,PTls13); flag=1; }else if(type0=="SS"){ total=SS2QX(content0,Pudp0,Ptfo0); flag=1 }else if(type0=="Surge"){ total=Surge2QX(content0); flag=1; }else if(type0=="rewrite"){ flag=2; content0=content0.split("\n"); total=Rewrite_Filter(content0,Pout0); }else if(type0=="Rule"){ flag=3; total=content0.split("\n"); total=Rule_Handle(total,Pout0); }else { $notify("👻 该解析器暂未支持您的订阅格式, 已尝试直接导入","😭 太难写了", "☠️ stay tuned"); flag=0; } if(flag==3){ $done({content : total.join("\n")}); }else if(flag==2){ $done({content:total.join("\n")}); }else if(flag==1){ if(Pin0||Pout0){ if(Pinfo!=0){ $notify("👥 开始转换节点,类型:"+type0,"🐶 您已添加节点筛选参数,如下","👍️ 保留的关键字:"+Pin0+"\n👎️ 排除的关键字:"+Pout0);} total=filter(total,Pin0,Pout0) } else { if(Pinfo!=0){ $notify("🐷 开始转换节点,类型:"+type0,"🐼️ 如需筛选节点请使用in/out及其他参数,可参考此示范:","👉 https://t.me/QuanXNews/110");} } if(Pemoji){ if(Pinfo!=0){ $notify("🏳️‍🌈 开始更改旗帜 emoji","清除emoji请用参数 -1, 国行设备添加emoji请使用参数 2","你当前所用的参数为 emoji="+Pemoji)}; total=emoji_handle(total,Pemoji); } if(Prname){ if(Pinfo!=0){ $notify("🏳️‍🌈 开始节点重命名","格式为 \"旧名字@新名字\"","你当前所用的参数为"+Prname);} var Prn=Prname; total=total.map(Rename); } if(Psort0==1 || Psort0==-1){ total=QXSort(total,Psort0); } $done({content : total.join("\n")}); }else { $done({content : content0}); } //判断订阅类型 function Type_Check(subs){ var type="" var RuleK=["host","domain","ip-cidr","geoip","user-agent"]; const RuleCheck = (item) => subs.toLowerCase().indexOf(item)!=-1; if (subs.indexOf("dm1lc3M6Ly")!= -1){ type="Vmess" } else if(subs.indexOf("[Proxy]")!=-1){ type="Surge"; } else if (subs.indexOf("tag")!=-1 && subs.indexOf("ss"||"vmess"||"trojan"||"http")!=-1){ type="QuanX" } else if (subs.indexOf("c3NyOi8v")!= -1){ type="SSR" } else if (subs.indexOf("dHJvamFu")!= -1){ type="Trojan" } else if (subs.indexOf("c3M6Ly")!= -1){ type="SS" } else if(subs.indexOf("hostname")!=-1){ type="rewrite" } else if(RuleK.some(RuleCheck)){ type="Rule"; } else if(subs.indexOf("ss"||"vmess"||"trojan"||"http")!=-1){ type="Surge" } return type } function Trim(item){ return item.trim() } //删除 rewrite 引用中的某部分 function Rewrite_Filter(subs,Pout){ cnt=subs; nlist=[]; drewrite=[]; if(Pout!="" && Pout){ Pout=Pout.map(Trim); for(var i=0;i cc.indexOf(item)!=-1; if(Pout.some(exclude)){ if(cc.indexOf("hostname")!=-1 && cc.indexOf("=")!=-1){ //hostname 部分 nname=[];//保留项 dname=[];//删除项目 hname=cc.split("=")[1].split(","); for(var j=0;j dd.indexOf(item)!=-1; if(!Pout.some(excludehn)){ nname.push(hname[j]) }else{dname.push(hname[j])} } //for j hname="hostname="+nname.join(", "); //console.log(hname) nlist.push(hname) if(dname.length>0){$notify("🤖 您添加的[rewrite]过滤关键词为:"+Pout0.join(", "),"☠️ 主机名 hostname 中已为您删除以下"+dname.length+"个匹配项",dname.join(",") )} } // if cc -hostname else{ drewrite.push(cc); nlist.push(cc.replace(/ url /g," - ")); } }else{ //if Pout.some nlist.push(cc) } //else } }//cnt for if(drewrite.length>0){$notify("🤖 您添加的[rewrite]过滤关键词为:"+Pout0.join(", "),"☠️ 复写 rewrite 中已为您禁用以下"+drewrite.length+"个匹配项",drewrite.join("\n") )}; return nlist }else { // Pout if //$notify("no filter at all") return cnt;} } //分流规则转换及过滤,可用于 surge 及 quanx 的 rule-list function Rule_Handle(subs,Pout){ cnt=subs //.split("\n"); out=Pout; //过滤参数 ply=Ppolicy; //策略组 var nlist=[] var RuleK=["//","#",";"]; if(Pout!="" && Pout!=null){ var dlist=[]; for(var i=0;icc.indexOf(item)!=-1; const RuleCheck = (item) => cc.indexOf(item)!=-1; //无视注释行 if(Pout.some(exclude) && !RuleK.some(RuleCheck)){ dlist.push(cnt[i]) } else if(!RuleK.some(RuleCheck) && cc){ //if Pout.some, 不操作注释项 dd=Rule_Policy(cc); nlist.push(dd); } }//for cnt var no=dlist.length if(dlist.length>0){$notify("🤖 您添加的分流 [filter] 过滤关键词为:"+out,"☠️ 已为您删除以下 "+no+"条匹配规则", dlist.join("\n")) }else{$notify("🤖 您添加的[filter]过滤关键词为:"+out,"☠️ 没有发现任何匹配项",dlist)} return nlist } else{return cnt.map(Rule_Policy)}//if Pout } function Rule_Policy(content){ //增加、替换 policy var cnt=content.split(","); var RuleK=["//","#",";"]; const RuleCheck = (item) => cnt[0].indexOf(item)!=-1; //无视注释行 if(cnt.length==3 && cnt.indexOf("no-resolve")==-1){ ply0 = Ppolicy!="Shawn"? Ppolicy:cnt[2] nn=cnt[0]+", "+cnt[1]+", "+ply0 } else if(cnt.length==2){ //Surge rule-set ply0 = Ppolicy!="Shawn"? Ppolicy:"Shawn" nn=cnt[0]+", "+cnt[1]+", "+ply0 }else if(cnt.length==3 && cnt[2].indexOf("no-resolve")!=-1){ ply0 = Ppolicy!="Shawn"? Ppolicy:"Shawn" nn=cnt[0]+", "+cnt[1]+", "+ply0+", "+cnt[2] }else if(cnt.length==4 && cnt[3].indexOf("no-resolve")!=-1){ ply0 = Ppolicy!="Shawn"? Ppolicy:cnt[2] nn=cnt[0]+", "+cnt[1]+", "+ply0+", "+cnt[3] }else if(!RuleK.some(RuleCheck)&& content){ $notify("未能解析其中部分规则",content); return "" }else{return ""} if(cnt[0].indexOf("URL-REGEX")!=-1 || cnt[0].indexOf("PROCESS")!=-1){ nn="" } else {nn=nn.replace("IP-CIDR6","ip6-cidr")} return nn } //V2RayN 订阅转换成 QUANX 格式 function V2QX(subs,Pudp,Ptfo,Pcert,Ptls13){ const $base64 = new Base64() var list0=$base64.decode(subs).split("\n"); var QXList=[] var cert=Pcert var tls13=Ptls13 for(var i=0;i name.indexOf(item.toUpperCase()) != -1; const exclude = (item) => name.indexOf(item.toUpperCase()) != -1; if(Pin){ if(Pin.some(include)&&Pout){ if(!Pout.some(exclude)){ NList.push(Servers[i]) } } else if(Pin.some(include)&&!Pout) {NList.push(Servers[i])} } else{ if(!Pout.some(exclude)){ NList.push(Servers[i]) } } } } return NList } // Vmess obfs 参数 function Pobfs(jsonl,Pcert,Ptls13){ var obfsi=[]; var cert=Pcert; tcert= cert==0? "tls-verification=false":"tls-verification=true"; tls13= Ptls13==1? "tls13=true":"tls13=false" if(jsonl.net=="ws" && jsonl.tls=="tls"){ obfs0="obfs=wss, "+tcert+", "+tls13+", "; uri0=jsonl.path!=""? "obfs-uri="+jsonl.path:"obfs-uri=/"; host0= jsonl.host!=""? "obfs-host="+jsonl.host+",":""; obfsi.push(obfs0+host0+uri0) return obfsi.join(", ") }else if(jsonl.net=="ws"){ obfs0="obfs=ws"; uri0=jsonl.path!=""? "obfs-uri="+jsonl.path:"obfs-uri=/"; host0= jsonl.host!=""? "obfs-host="+jsonl.host+",":""; obfsi.push(obfs0,host0+uri0); return obfsi.join(", ") }else if(jsonl.tls=="tls"){ obfs0="obfs=over-tls, "+tcert+", "+tls13; uri0=jsonl.path!=""? "obfs-uri="+jsonl.path:""; host0=jsonl.host!=""? "obfs-host="+jsonl.host:""; obfsi.push(obfs0+host0) return obfsi.join(", ") } } //SSR 转换 quanx 格式 function SSR2QX(subs,Pudp,Ptfo){ const $base64 = new Base64() var list0=$base64.decode(subs).split("\n"); var QXList=[]; for(var i=0;itag2? 1:-1 return res } //逆序 function ToTagR(elem1,elem2){ var tag1=emoji_del(elem1.split("tag")[1].split("=")[1].trim()) var tag2=emoji_del(elem2.split("tag")[1].split("=")[1].trim()) res = tag1>tag2? -1:1 return res } //节点重命名 function Rename(str){ var server=str; if(server.indexOf("tag=")!=-1){ hd=server.split("tag=")[0] name=server.split("tag=")[1] for(i=0;i> 2); out += base64EncodeChars.charAt((c1 & 0x3) << 4); out += "=="; break; } c2 = str.charCodeAt(i++); // 当最后剩余两个字节时 if(i == len){ out += base64EncodeChars.charAt(c1 >> 2); out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); out += base64EncodeChars.charAt((c2 & 0xF) << 2); out += "="; break; } //当剩余字节数大于等于3时 c3 = str.charCodeAt(i++); out += base64EncodeChars.charAt(c1 >> 2); out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); out += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)); out += base64EncodeChars.charAt(c3 & 0x3F); } return out; } /** * Base64解码函数 * @param str * @returns {*} */ function base64decode(str){ var c1, c2, c3, c4; var i, len, out; len = str.length; i = 0; out = ""; while(i < len){ /* 得到第一个字符 c1 * 并过虑掉前后所有与Base64编码无关的字符 * */ do{ c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff]; }while(i < len && c1 == -1); // 如果已经到达字符串结尾,并最后还未得到有效的Base64编码字符就结尾循环 if(c1 == -1) break; /* 得到字符 c2 * 并过滤掉所有与Base64编码无关的字符 */ do{ c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff]; }while(i < len && c2 == -1); // 如果已经到达字符串结尾,并最后还未得到有效的Base64编码字符就结尾循环 if(c2 == -1) break; // 根据Base64编码的 c1 和 c2 解码得到一个编码前的字符 out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4)); /* 得到字符 c3 * 并过滤掉所有与Base64编码无关的字符 * 如果获取的 c3 是 '=' 字符则说明已经解码完成,返回解码得到的字符串 */ do{ c3 = str.charCodeAt(i++) & 0xff; if(c3 == 61) return out; c3 = base64DecodeChars[c3]; }while(i < len && c3 == -1); // 如果已经到达字符串结尾,并最后还未得到有效的Base64编码字符就结尾循环 if(c3 == -1) break; // 根据Base64编码的 c2 和 c3 解码得到一个编码前的字符 out += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2)); /* 这一步就比较复杂了 * 先是尝试获取第四个Base64 编码的字符 c4 * 如果获取的 c4 是 '=' 字符则说明已经解码完成,返回解码得到的字符串 * */ do{ c4 = str.charCodeAt(i++) & 0xff; if(c4 == 61) return out; c4 = base64DecodeChars[c4]; }while(i < len && c4 == -1); // 如果已经到达字符串结尾,并最后还未得到有效的Base64编码字符就结尾循环 if(c4 == -1) break; // 根据Base64编码的 c3 和 c4 解码得到一个编码前的字符 out += String.fromCharCode(((c3 & 0x03) << 6) | c4); } return out; } /** * 把 unicode 码转换成 utf8 编码 * @param str * @returns {string} */ function unicodeToUtf8(str){ var out, i, len, c; out = ""; len = str.length; for(i = 0; i < len; i++){ c = str.charCodeAt(i); // 兼容 ASCII if((c >= 0x0001) && (c <= 0x007F)){ out += str.charAt(i); }else if(c > 0x07FF){ // 占三个字节的 utf8 out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F)); out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F)); out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); }else{ // 占两个字节的 utf8 out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F)); out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); } } return out; } /** * 把 utf8 编码转换成 unicode 码 * @param str * @returns {string} */ function utf8ToUnicode(str){ var out, i, len, c; var char2, char3; out = ""; len = str.length; i = 0; while(i < len){ c = str.charCodeAt(i++); switch(c >> 4){ case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: // 0xxxxxxx ASCII 编码 out += str.charAt(i - 1); break; case 12: case 13: // 110x xxxx 10xx xxxx // 占两个字节的 utf8 char2 = str.charCodeAt(i++); out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); break; case 14: // 1110 xxxx 10xx xxxx 10xx xxxx // 占三个字节的 utf8 char2 = str.charCodeAt(i++); char3 = str.charCodeAt(i++); out += String.fromCharCode(((c & 0x0F) << 12) | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0)); break; } } return out; } /** * 转成 十六 进制编码 * @param str * @returns {string} * @constructor */ function CharToHex(str){ var out, i, len, c, h; out = ""; len = str.length; i = 0; while(i < len){ c = str.charCodeAt(i++); // 把数据转换成十六进制的字符串 h = c.toString(16); if(h.length < 2) h = "0" + h; out += "\\x" + h + " "; if(i > 0 && i % 8 == 0) out += "\r\n"; } return out; } this.encode=function(str){ // 普通 Base64 编码 return base64encode(unicodeToUtf8(str)); }; this.decode=function(str){ // 普通 Base64 编码 return utf8ToUnicode(base64decode(str)); }; // base64={ // encode:function(str){ // // 普通 Base64 编码 // return base64encode(unicodeToUtf8(str)); // }, // encodeUrl:function(str){ // // 使用 Base64 编码字符串 // return base64encode(unicodeToUtf8(str),1) // }, // decode:function(str){ // // 兼容的 Base64 解码 // return utf8ToUnicode(base64decode(str)); // }, // encodeToHex:function(str){ // // 普通 Base64 编码 以十六进制显示 // return CharToHex(base64encode(unicodeToUtf8(str))); // }, // encodeUrlToHex:function(str){ // // 使用 Base64 编码 url 以十六进制显示 // return CharToHex(base64encode(unicodeToUtf8(str),1)); // } // } };