diff --git a/backend/sub-store.js b/backend/sub-store.js index 02590d1..45592df 100644 --- a/backend/sub-store.js +++ b/backend/sub-store.js @@ -2894,7 +2894,7 @@ function getBuiltInRules() { }; } -/****************************************** Supporting Functions **********************************************************/ +/****************************************** Supporting Functions ********************************************** */ function ApplyProcessor(process, objs) { function ApplyFilter(filter, objs) { // select proxies @@ -2949,7 +2949,7 @@ function clone(object) { return JSON.parse(JSON.stringify(object)); } -/****************************************** Own Libraries **********************************************************/ +/****************************************** Own Libraries *******************************************************/ /** @@ -3645,7 +3645,7 @@ function express({port} = {port: 3000}) { } } -/****************************************** Third Party Libraries **********************************************************/ +/****************************************** Third Party Libraries **********************************************/ /** * Base64 Coding Library diff --git a/backend/sub-store.min.js b/backend/sub-store.min.js index b190c62..52980e5 100644 --- a/backend/sub-store.min.js +++ b/backend/sub-store.min.js @@ -1,2 +1,2 @@ -// UPDATED AT: 2020年11月26日 星期四 19时59分00秒 CST +// UPDATED AT: 2020年11月27日 星期五 09时47分15秒 CST const $=API("sub-store"),Base64=new Base64Code;function service(){console.log("========== Sub-Store ===========");const e=express(),t="settings",s="subs",r="collections",n="builtin";function o(e){const t=Object.keys(e);let s="";for(let r of t)if(/USER-AGENT/i.test(r)){s=e[r];break}return-1!==s.indexOf("Quantumult%20X")?"QX":-1!==s.indexOf("Surge")?"Surge":-1!==s.indexOf("Decar")||-1!==s.indexOf("Loon")?"Loon":null}async function a(e,t=!0){const s=HTTP({headers:{"User-Agent":"Quantumult%20X"}}),r="#"+Base64.safeEncode(e),n=$.read(r),o=`#TIME-${Base64.safeEncode(e)}`,a=(new Date).getTime()-$.read(o)>864e5;if(t&&n&&!a)return $.log(`Use cached for url: ${e}`),n;let i="";try{i=(await s.get(e)).body}catch(e){throw new Error(e)}finally{$.write(i,r),$.write((new Date).getTime(),o)}if(0===i.replace(/\s/g,"").length)throw new Error("订阅内容为空!");return i}$.read(s)||$.write({},s),$.read(r)||$.write({},r),$.read(t)||$.write({},t),$.read("rules")||$.write({},"rules"),$.write({rules:getBuiltInRules()},n),e.get("/download/collection/:name",async function(e,t){const{name:n}=e.params,{cache:i}=e.query||"false",p=e.query.target||o(e.headers)||"JSON",l=$.read(r),c=$.read(s),u=l[n];if($.info(`正在下载组合订阅:${n}`),u){const e=u.subscriptions;let s=[];for(let t=0;te!==n);$.write(a,r),t.json({status:"success"})}),e.route("/api/subs").get(function(e,t){const r=$.read(s);t.json({status:"success",data:r})}).post(function(e,t){const r=e.body,n=$.read(s);$.info(`正在创建订阅: ${r.name}`),n[r.name]&&t.status(500).json({status:"failed",message:`订阅${r.name}已存在!`});/^[\w-_]*$/.test(r.name)?(n[r.name]=r,$.write(n,s),t.status(201).json({status:"success",data:r})):t.status(500).json({status:"failed",message:`订阅名称 ${r.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`})}),e.route("/api/collection/:name").get(function(e,t){const{name:s}=e.params,n=$.read(r)[s];n?t.json({status:"success",data:n}):t.status(404).json({status:"failed",message:`未找到订阅集:${s}!`})}).patch(function(e,t){const{name:s}=e.params;let n=e.body;const o=$.read(r);if(o[s]){const e={...o[s],...n};$.info(`正在更新组合订阅:${s}...`),delete o[s],o[n.name||s]=e,$.write(o,r),t.json({status:"success",data:e})}else t.status(500).json({status:"failed",message:`订阅集${s}不存在,无法更新!`})}).delete(function(e,t){const{name:s}=e.params;$.info(`正在删除组合订阅:${s}`);let n=$.read(r);delete n[s],$.write(n,r),t.json({status:"success"})}),e.route("/api/collections").get(function(e,t){const s=$.read(r);t.json({status:"success",data:s})}).post(function(e,t){const s=e.body;$.info(`正在创建组合订阅:${s.name}`);const n=$.read(r);n[s.name]&&t.status(500).json({status:"failed",message:`订阅集${s.name}已存在!`});/^[\w-_]*$/.test(s.name)?(n[s.name]=s,$.write(n,r),t.status(201).json({status:"success",data:s})):t.status(500).json({status:"failed",message:`订阅集名称 ${s.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`})}),e.get("/download/rule/:name",async function(e,t){const{name:s}=e.params,{builtin:r}=e.query,a=e.query.target||o(e.headers)||"Surge";let i;$.info(`正在下载${r?"内置":""}分流订阅:${s}...`),r&&(i=$.read(n).rules[s]);if(i){let e=[];for(let t=0;t{t.json($.read("#sub-store"))}).post((e,t)=>{const s=e.body;$.write(JSON.stringify(s),"#sub-store"),t.end()}),e.route("/api/settings").get(function(e,s){const r=$.read(t);s.json(r)}).patch(function(e,s){const r=e.body,n=$.read(t);$.write({...n,...r},t),s.json({status:"success"})}),e.get("/api/utils/IP_API/:server",async function(e,t){const s=decodeURIComponent(e.params.server),r=await $.http.get(`http://ip-api.com/json/${s}?lang=zh-CN`).then(e=>JSON.parse(e.body));t.json(r)}),e.post("/api/utils/refresh",async function(e,t){const{url:s}=e.body;$.info(`Refreshing cache for URL: ${s}`);try{const e=await a(s,!1);$.write(e,`#${Base64.safeEncode(s)}`),t.json({status:"success"})}catch(e){t.status(500).json({status:"failed",message:`无法刷新资源 ${s}: ${e}`})}}),e.get("/api/utils/env",function(e,t){const{isNode:s,isQX:r,isLoon:n,isSurge:o}=ENV();let a="Node";s&&(a="Node");r&&(a="QX");n&&(a="Loon");o&&(a="Surge");t.json({backend:a})}),e.get("/api/utils/backup",async function(e,s){const{action:r}=e.query,{gistToken:n}=$.read(t);if(n){const e=new Gist("Auto Generated Sub-Store Backup",n);try{let t;switch(r){case"upload":t=$.read("#sub-store"),$.info("上传备份中..."),await e.upload(t);break;case"download":$.info("还原备份中..."),t=await e.download(),$.write(t,"#sub-store")}s.json({status:"success"})}catch(e){const t=`${"upload"===r?"上传":"下载"}备份失败!${e}`;$.error(t),s.status(500).json({status:"failed",message:t})}}else s.status(500).json({status:"failed",message:"未找到Gist备份Token!"})}),e.get("/",async(e,t)=>{t.set("location","https://sub-store.vercel.app/").status(302).end()}),ENV().isQX&&e.options("/",async(e,t)=>{t.status(200).end()}),e.all("/",(e,t)=>{t.send("Hello from sub-store, made with ❤️ by Peng-YM")}),e.start()}service();var ProxyUtils=function(){const PROXY_PREPROCESSORS=function(){return[{name:"HTML",test:e=>/^/.test(e),parse:e=>""},function(){const e=["dm1lc3M","c3NyOi8v","dHJvamFu","c3M6Ly","c3NkOi8v"];return{name:"Base64 Pre-processor",test:function(t){return e.some(e=>-1!==t.indexOf(e))},parse:function(e){return e=Base64.safeDecode(e)}}}(),{name:"Clash Pre-processor",test:function(e){return/proxies/.test(e)},parse:function(e){return-1!==e.indexOf("{")&&(e=e.replace(/ - /g," - ").replace(/:(?!\s)/g,": ").replace(/\,\"/g,', "').replace(/: {/g,": {, ").replace(/, (\"?host|path|tls|mux|skip\"?)/g,", $1").replace(/{name: /g,'{name: "').replace(/, server:/g,'", server:').replace(/{|}/g,"").replace(/,/g,"\n ")),e=-1===(e=e.replace(/ -\n.*name/g," - name").replace(/\$|\`/g,"").split("proxy-providers:")[0].split("proxy-groups:")[0].replace(/\"([\w-]+)\"\s*:/g,"$1:")).indexOf("proxies:")?"proxies:\n"+e:"proxies:"+e.split("proxies:")[1],YAML.eval(e).proxies.map(e=>JSON.stringify(e)).join("\n")}},{name:"SSD Pre-processor",test:function(e){return 0===e.indexOf("ssd://")},parse:function(e){const t=[];let s=JSON.parse(Base64.safeDecode(e.split("ssd://")[1]));s.traffic_used,s.traffic_total,s.expiry,s.airport;let r=s.port,n=s.encryption,o=s.password,a=s.servers;for(let e=0;e{let[t,n]=e.split("=");if(t=t.trim(),n=n.trim(),-1!==r.indexOf(t)){s.type=t;const e=n.split(":");s.server=e[0],s.port=e[1]}else s[t.trim()]=n.trim()}),s}function t(){return{name:"Loon HTTP Parser",test:e=>/^.*=\s*http/i.test(e.split(",")[0])&&5===e.split(",").length&&-1===e.indexOf("username")&&-1===e.indexOf("password"),parse:e=>{const t=e.split("=")[1].split(","),s={name:e.split("=")[0].trim(),type:"http",server:t[1],port:t[2],tls:"443"===t[2]};return t[3]&&(s.username=t[3]),t[4]&&(s.password=t[4]),s.tls&&(s.sni=t["tls-name"]||s.server,s["skip-cert-verify"]=JSON.parse(t["skip-cert-verify"]||"false")),s}}}function s(e){const t={};t.name=e.split("=")[0].trim();const s=e.split(",");t.server=s[1].trim(),t.port=s[2].trim();for(let e=3;e/^ss:\/\//.test(e),parse:e=>{const t={};let s=e.split("ss://")[1];const r={name:decodeURIComponent(e.split("#")[1]),type:"ss",supported:t},n=(s=s.split("#")[0]).match(/@([^\/]*)(\/|$)/)[1],o=n.lastIndexOf(":");r.server=n.substring(0,o),r.port=n.substring(o+1);const a=Base64.safeDecode(s.split("@")[0]).split(":");if(r.cipher=a[0],r.password=a[1],-1!==s.indexOf("?plugin=")){const e=("plugin="+decodeURIComponent(s.split("?plugin=")[1].split("&")[0])).split(";"),n={};for(const t of e){const[e,s]=t.split("=");e&&(n[e]=s||!0)}switch(n.plugin){case"obfs-local":case"simple-obfs":r.plugin="obfs",r["plugin-opts"]={mode:n.obfs,host:n["obfs-host"]};break;case"v2ray-plugin":r.supported={...t,Loon:!1,Surge:!1},r.obfs="v2ray-plugin",r["plugin-opts"]={mode:"websocket",host:n["obfs-host"],path:n.path||"",tls:n.tls||!1};break;default:throw new Error(`Unsupported plugin option: ${n.plugin}`)}}return r}},function(){const e={Surge:!1};return{name:"URI SSR Parser",test:e=>/^ssr:\/\//.test(e),parse:t=>{let s=(t=Base64.safeDecode(t.split("ssr://")[1])).indexOf(":origin");-1===s&&(s=t.indexOf(":auth_"));const r=t.substring(0,s),n=r.substring(0,r.lastIndexOf(":")),o=r.substring(r.lastIndexOf(":")+1);let a=t.substring(s+1).split("/?")[0].split(":"),i={type:"ssr",server:n,port:o,protocol:a[0],cipher:a[1],obfs:a[2],password:Base64.safeDecode(a[3]),supported:e};if(a={},(t=t.split("/?")[1].split("&")).length>1)for(const e of t){const[t,s]=e.split("=");a[t]=s}return i={...i,name:Base64.safeDecode(a.remarks),"protocol-param":Base64.safeDecode(a.protoparam).replace(/\s/g,"")||"","obfs-param":Base64.safeDecode(a.obfsparam).replace(/\s/g,"")||""}}}}(),{name:"URI VMess Parser",test:e=>/^vmess:\/\//.test(e),parse:e=>{const t={};e=e.split("vmess://")[1];const s=Base64.safeDecode(e);if(/=\s*vmess/.test(s)){const e=s.split(",").map(e=>e.trim()),t={};for(const s of e)if(-1!==s.indexOf("=")){const[e,r]=s.split("=");t[e.trim()]=r.trim()}const r={name:e[0].split("=")[0].trim(),type:"vmess",server:e[1],port:e[2],cipher:e[3],uuid:e[4].match(/^"(.*)"$/)[1],tls:"over-tls"===t.obfs||"wss"===t.obfs,udp:JSON.parse(t["udp-relay"]||"false"),tfo:JSON.parse(t["fast-open"]||"false")};return"ws"!==t.obfs&&"wss"!==t.obfs||(r.network="ws",r["ws-path"]=t["obfs-uri"],r["ws-headers"]={Host:t["obfs-host"]||r.server}),r.tls&&"false"===t['"tls-verification"']&&(r["skip-cert-verify"]=!0),r.tls&&t["obfs-host"]&&(r.sni=t["obfs-host"]),r}{const e=JSON.parse(s),r={name:e.ps,type:"vmess",server:e.add,port:e.port,cipher:"auto",uuid:e.id,alterId:e.aid||0,tls:"tls"===e.tls||!0===e.tls,supported:t};return"ws"===e.net&&(r.network="ws",r["ws-path"]=e.path,r["ws-headers"]={Host:e.host||e.add},r.tls&&e.host&&(r.sni=e.host)),!1===e.verify_cert&&(r["skip-cert-verify"]=!0),r}}},{name:"URI Trojan Parser",test:e=>/^trojan:\/\//.test(e),parse:e=>{if(-1===e.indexOf(":443"))throw new Error("Trojan port should always be 443!");const t=(e=e.split("trojan://")[1]).split("@")[1].split(":443")[0];return{name:decodeURIComponent(e.split("#")[1].trim())||`[Trojan] ${t}`,type:"trojan",server:t,port:443,password:e.split("@")[0],supported:{}}}},{name:"Clash Parser",test:e=>{try{JSON.parse(e)}catch(e){return!1}return!0},parse:e=>JSON.parse(e)},{name:"Surge SS Parser",test:e=>/^.*=\s*ss/.test(e.split(",")[0]),parse:e=>{const t=s(e),r={name:t.name,type:"ss",server:t.server,port:t.port,cipher:t["encrypt-method"],password:t.password,tfo:JSON.parse(t.tfo||"false"),udp:JSON.parse(t["udp-relay"]||"false")};return t.obfs&&(r.plugin="obfs",r["plugin-opts"]={mode:t.obfs,host:t["obfs-host"]}),r}},{name:"Surge VMess Parser",test:e=>/^.*=\s*vmess/.test(e.split(",")[0])&&-1!==e.indexOf("username"),parse:e=>{const t=s(e),r={name:t.name,type:"vmess",server:t.server,port:t.port,uuid:t.username,alterId:0,cipher:"none",tls:JSON.parse(t.tls||"false"),tfo:JSON.parse(t.tfo||"false")};return r.tls&&(void 0!==t["skip-cert-verify"]&&(r["skip-cert-verify"]=!0===t["skip-cert-verify"]||"1"===t["skip-cert-verify"]),r.sni=t.sni||t.server),JSON.parse(t.ws||"false")&&(r.network="ws",r["ws-path"]=t["ws-path"],r["ws-headers"]={Host:t.sni}),r}},{name:"Surge Trojan Parser",test:e=>/^.*=\s*trojan/.test(e.split(",")[0])&&-1!==e.indexOf("sni"),parse:e=>{const t=s(e),r={name:t.name,type:"trojan",server:t.server,port:t.port,password:t.password,sni:t.sni||t.server,tfo:JSON.parse(t.tfo||"false")};return void 0!==t["skip-cert-verify"]&&(r["skip-cert-verify"]=!0===t["skip-cert-verify"]||"1"===t["skip-cert-verify"]),r}},{name:"Surge HTTP Parser",test:e=>/^.*=\s*http/.test(e.split(",")[0])&&!t().test(e),parse:e=>{const t=s(e),r={name:t.name,type:"http",server:t.server,port:t.port,tls:JSON.parse(t.tls||"false"),tfo:JSON.parse(t.tfo||"false")};return r.tls&&(void 0!==t["skip-cert-verify"]&&(r["skip-cert-verify"]=!0===t["skip-cert-verify"]||"1"===t["skip-cert-verify"]),r.sni=t.sni||t.server),t.username&&"none"!==t.username&&(r.username=t.username),t.password&&"none"!==t.password&&(r.password=t.password),r}},{name:"Loon SS Parser",test:e=>"shadowsocks"===e.split(",")[0].split("=")[1].trim().toLowerCase(),parse:e=>{const t=e.split("=")[1].split(","),s={name:e.split("=")[0].trim(),type:"ss",server:t[1],port:t[2],cipher:t[3],password:t[4].replace(/"/g,"")};return t.length>5&&(s.plugin="obfs",s["plugin-opts"]={mode:t[5],host:t[6]}),s}},{name:"Loon SSR Parser",test:e=>"shadowsocksr"===e.split(",")[0].split("=")[1].trim().toLowerCase(),parse:e=>{const t=e.split("=")[1].split(",");return{name:e.split("=")[0].trim(),type:"ssr",server:t[1],port:t[2],cipher:t[3],password:t[4].replace(/"/g,""),protocol:t[5],"protocol-param":t[6].match(/{(.*)}/)[1],supported:{Surge:!1},obfs:t[7],"obfs-param":t[8].match(/{(.*)}/)[1]}}},{name:"Loon VMess Parser",test:e=>/^.*=\s*vmess/i.test(e.split(",")[0])&&-1===e.indexOf("username"),parse:e=>{let t=e.split("=")[1].split(",");const s={name:e.split("=")[0].trim(),type:"vmess",server:t[1],port:t[2],cipher:t[3]||"none",uuid:t[4].replace(/"/g,""),alterId:0};t=t.splice(5);for(const e of t){const[s,r]=e.split(":");t[s]=r}switch(s.tls=JSON.parse(t["over-tls"]||"false"),s.tls&&(s.sni=t["tls-name"]||s.server,s["skip-cert-verify"]=JSON.parse(t["skip-cert-verify"]||"false")),t.transport){case"tcp":break;case"ws":s.network=t.transport,s["ws-path"]=t.path,s["ws-headers"]={Host:t.host}}return s.tls&&(s["skip-cert-verify"]=JSON.parse(t["skip-cert-verify"]||"false")),s}},{name:"Loon Trojan Parser",test:e=>/^.*=\s*trojan/i.test(e.split(",")[0])&&-1===e.indexOf("password"),parse:e=>{const t=e.split("=")[1].split(","),s={name:e.split("=")[0].trim(),type:"trojan",server:t[1],port:t[2],password:t[3].replace(/"/g,""),sni:t[1],"skip-cert-verify":JSON.parse(t["skip-cert-verify"]||"false")};if(t.length>4){const[r,n]=t[4].split(":");if("tls-name"!==r)throw new Error(`Unknown option ${r} for line: \n${e}`);s.sni=n}return s}},t(),{name:"QX SS Parser",test:e=>/^shadowsocks\s*=/.test(e.split(",")[0].trim())&&-1===e.indexOf("ssr-protocol"),parse:t=>{const s=e(t),r={name:s.tag,type:"ss",server:s.server,port:s.port,cipher:s.method,password:s.password,udp:JSON.parse(s["udp-relay"]||"false"),tfo:JSON.parse(s["fast-open"]||"false"),supported:{}};if(s.obfs)switch(r["plugin-opts"]={host:s["obfs-host"]||r.server},s.obfs){case"http":case"tls":r.plugin="obfs",r["plugin-opts"].mode=s.obfs;break;case"ws":case"wss":r["plugin-opts"]={...r["plugin-opts"],mode:"websocket",path:s["obfs-uri"]||"/",tls:"wss"===s.obfs},r["plugin-opts"].tls&&void 0!==s["tls-verification"]&&(r["plugin-opts"]["skip-cert-verify"]=s["tls-verification"]),r.plugin="v2ray-plugin",r.supported.Surge=!1,r.supported.Loon=!1}return r}},{name:"QX SSR Parser",test:e=>/^shadowsocks\s*=/.test(e.split(",")[0].trim())&&-1!==e.indexOf("ssr-protocol"),parse:t=>{const s=e(t),r={name:s.tag,type:"ssr",server:s.server,port:s.port,cipher:s.method,password:s.password,protocol:s["ssr-protocol"],obfs:"plain","protocol-param":s["ssr-protocol-param"],udp:JSON.parse(s["udp-relay"]||"false"),tfo:JSON.parse(s["fast-open"]||"false"),supported:{Surge:!1}};return s.obfs&&(r.obfs=s.obfs,r["obfs-param"]=s["obfs-host"]),r}},{name:"QX VMess Parser",test:e=>/^vmess\s*=/.test(e.split(",")[0].trim()),parse:t=>{const s=e(t),r={type:"vmess",name:s.tag,server:s.server,port:s.port,cipher:s.method||"none",uuid:s.password,alterId:0,tls:"over-tls"===s.obfs||"wss"===s.obfs,udp:JSON.parse(s["udp-relay"]||"false"),tfo:JSON.parse(s["fast-open"]||"false")};return r.tls&&(r.sni=s["obfs-host"]||s.server,r["skip-cert-verify"]=!JSON.parse(s["tls-verification"]||"true")),"ws"!==s.obfs&&"wss"!==s.obfs||(r.network="ws",r["ws-path"]=s["obfs-uri"],r["ws-headers"]={Host:s["obfs-host"]||s.server}),r}},{name:"QX Trojan Parser",test:e=>/^trojan\s*=/.test(e.split(",")[0].trim()),parse:t=>{const s=e(t),r={type:"trojan",name:s.tag,server:s.server,port:s.port,password:s.password,sni:s["tls-host"]||s.server,udp:JSON.parse(s["udp-relay"]||"false"),tfo:JSON.parse(s["fast-open"]||"false")};return r["skip-cert-verify"]=!JSON.parse(s["tls-verification"]||"true"),r}},{name:"QX HTTP Parser",test:e=>/^http\s*=/.test(e.split(",")[0].trim()),parse:t=>{const s=e(t),r={type:"http",name:s.tag,server:s.server,port:s.port,tls:JSON.parse(s["over-tls"]||"false"),udp:JSON.parse(s["udp-relay"]||"false"),tfo:JSON.parse(s["fast-open"]||"false")};return s.username&&"none"!==s.username&&(r.username=s.username),s.password&&"none"!==s.password&&(r.password=s.password),r.tls&&(r.sni=s["tls-host"]||r.server,r["skip-cert-verify"]=!JSON.parse(s["tls-verification"]||"true")),r}}]}(),PROXY_PROCESSORS=function(){function SetPropertyOperator({key:e,value:t}){return{name:"Set Property Operator",func:s=>s.map(s=>(s[e]=t,s))}}function FlagOperator(e=!0){return{name:"Flag Operator",func:t=>t.map(t=>{if(e){const e=getFlag(t.name);t.name=removeFlag(t.name),t.name=e+" "+t.name,t.name=t.name.replace(/🇹🇼/g,"🇨🇳")}else t.name=removeFlag(t.name);return t})}}function SortOperator(e="asc"){return{name:"Sort Operator",func:t=>{switch(e){case"asc":case"desc":return t.sort((t,s)=>{let r=t.name>s.name?1:-1;return r*="desc"===e?-1:1});case"random":return shuffle(t);default:throw new Error("Unknown sort option: "+e)}}}}function KeywordSortOperator(e){return{name:"Keyword Sort Operator",func:t=>t.sort((t,s)=>{const r=getKeywordOrder(e,t.name),n=getKeywordOrder(e,s.name);return r&&!n?-1:n&&!r?1:r&&n?rt.map(t=>{for(const{old:s,now:r}of e)t.name=t.name.replaceAll(s,r).trim();return t})}}function RegexRenameOperator(e){return{name:"Regex Rename Operator",func:t=>t.map(t=>{for(const{expr:s,now:r}of e)t.name=t.name.replace(new RegExp(s,"g"),r).trim();return t})}}function KeywordDeleteOperator(e){return{name:"Keyword Delete Operator",func:KeywordRenameOperator(e.map(e=>({old:e,now:""}))).func}}function RegexDeleteOperator(e){return{name:"Regex Delete Operator",func:RegexRenameOperator(e.map(e=>({expr:e,now:""}))).func}}function ScriptOperator(script){return{name:"Script Operator",func:proxies=>{let output=proxies;return function(){const $get=(e,t)=>{return(0,AVAILABLE_OPERATORS[e])(t)},$process=(e,t)=>-1!==e.name.indexOf("Filter")?ApplyOperator(e,t):-1!==e.name.indexOf("Operator")?ApplyFilter(e,t):void 0;eval(script),output=operator(proxies)}(),output}}}function KeywordFilter({keywords:e=[],keep:t=!0}){return{name:"Keyword Filter",func:s=>s.map(s=>{const r=e.some(e=>-1!==s.name.indexOf(e));return t?r:!r})}}function UselessFilter(){return{name:"Useless Filter",func:KeywordFilter({keywords:["网址","流量","时间","应急","过期","Bandwidth","expire"],keep:!1}).func}}function RegionFilter(e){const t={HK:"🇭🇰",TW:"🇹🇼",US:"🇺🇸",SG:"🇸🇬",JP:"🇯🇵",UK:"🇬🇧"};return{name:"Region Filter",func:s=>s.map(s=>{const r=getFlag(s.name);return e.some(e=>t[e]===r)})}}function RegexFilter({regex:e=[],keep:t=!0}){return{name:"Regex Filter",func:s=>s.map(s=>{const r=e.some(e=>(e=new RegExp(e)).test(s.name));return t?r:!r})}}function TypeFilter(e){return{name:"Type Filter",func:t=>t.map(t=>e.some(e=>t.type===e))}}function ScriptFilter(script){return{name:"Script Filter",func:proxies=>{let output=FULL(proxies.length,!0);return function(){eval(script),output=filter(proxies)}(),output}}}function getFlag(e){const t={"🇦🇨":["AC"],"🇦🇹":["奥地利","维也纳"],"🇦🇺":["AU","Australia","Sydney","澳大利亚","澳洲","墨尔本","悉尼"],"🇧🇪":["BE","比利时"],"🇧🇬":["保加利亚","Bulgaria"],"🇧🇷":["BR","Brazil","巴西","圣保罗"],"🇨🇦":["CA","Canada","Waterloo","加拿大","蒙特利尔","温哥华","楓葉","枫叶","滑铁卢","多伦多"],"🇨🇭":["瑞士","苏黎世","Switzerland"],"🇩🇪":["DE","German","GERMAN","德国","德國","法兰克福"],"🇩🇰":["丹麦"],"🇪🇸":["ES","西班牙","Spain"],"🇪🇺":["EU","欧盟","欧罗巴"],"🇫🇮":["Finland","芬兰","赫尔辛基"],"🇫🇷":["FR","France","法国","法國","巴黎"],"🇬🇧":["UK","GB","England","United Kingdom","英国","伦敦","英"],"🇲🇴":["MO","Macao","澳门","CTM"],"🇭🇺":["匈牙利","Hungary"],"🇭🇰":["HK","Hongkong","Hong Kong","香港","深港","沪港","呼港","HKT","HKBN","HGC","WTT","CMI","穗港","京港","港"],"🇮🇩":["Indonesia","印尼","印度尼西亚","雅加达"],"🇮🇪":["Ireland","爱尔兰","都柏林"],"🇮🇳":["India","印度","孟买","Mumbai"],"🇰🇵":["KP","朝鲜"],"🇰🇷":["KR","Korea","KOR","韩国","首尔","韩","韓"],"🇱🇻":["Latvia","Latvija","拉脱维亚"],"🇲🇽️":["MEX","MX","墨西哥"],"🇲🇾":["MY","Malaysia","马来西亚","吉隆坡"],"🇳🇱":["NL","Netherlands","荷兰","荷蘭","尼德蘭","阿姆斯特丹"],"🇵🇭":["PH","Philippines","菲律宾"],"🇷🇴":["RO","罗马尼亚"],"🇷🇺":["RU","Russia","俄罗斯","俄羅斯","伯力","莫斯科","圣彼得堡","西伯利亚","新西伯利亚","京俄","杭俄"],"🇸🇦":["沙特","迪拜"],"🇸🇪":["SE","Sweden"],"🇸🇬":["SG","Singapore","新加坡","狮城","沪新","京新","泉新","穗新","深新","杭新","广新"],"🇹🇭":["TH","Thailand","泰国","泰國","曼谷"],"🇹🇷":["TR","Turkey","土耳其","伊斯坦布尔"],"🇹🇼":["TW","Taiwan","台湾","台北","台中","新北","彰化","CHT","台","HINET"],"🇺🇸":["US","USA","America","United States","美国","美","京美","波特兰","达拉斯","俄勒冈","凤凰城","费利蒙","硅谷","矽谷","拉斯维加斯","洛杉矶","圣何塞","圣克拉拉","西雅图","芝加哥","沪美","哥伦布","纽约"],"🇻🇳":["VN","越南","胡志明市"],"🇮🇹":["Italy","IT","Nachash","意大利","米兰","義大利"],"🇿🇦":["South Africa","南非"],"🇦🇪":["United Arab Emirates","阿联酋"],"🇯🇵":["JP","Japan","日","日本","东京","大阪","埼玉","沪日","穗日","川日","中日","泉日","杭日","深日","辽日","广日"],"🇦🇷":["AR","阿根廷"],"🇳🇴":["Norway","挪威","NO"],"🇨🇳":["CN","China","回国","中国","江苏","北京","上海","广州","深圳","杭州","徐州","青岛","宁波","镇江","back"],"🏳️‍🌈":["流量","时间","应急","过期","Bandwidth","expire"]};for(let s of Object.keys(t))if(t[s].some(t=>-1!==e.indexOf(t)))return s;return(e.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/)||[])[0]||"🏴‍☠️"}function removeFlag(e){return e.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g,"").trim()}function shuffle(e){let t,s,r=e.length;for(;0!==r;)s=Math.floor(Math.random()*r),t=e[r-=1],e[r]=e[s],e[s]=t;return e}return{"Keyword Filter":KeywordFilter,"Useless Filter":UselessFilter,"Region Filter":RegionFilter,"Regex Filter":RegexFilter,"Type Filter":TypeFilter,"Script Filter":ScriptFilter,"Set Property Operator":SetPropertyOperator,"Flag Operator":FlagOperator,"Sort Operator":SortOperator,"Keyword Sort Operator":KeywordSortOperator,"Keyword Rename Operator":KeywordRenameOperator,"Keyword Delete Operator":KeywordDeleteOperator,"Regex Rename Operator":RegexRenameOperator,"Regex Delete Operator":RegexDeleteOperator,"Script Operator":ScriptOperator}}(),PROXY_PRODUCERS=function(){return{QX:{produce:e=>{let t,s;switch(e.type){case"ss":if(t="","obfs"===e.plugin){const{host:s,mode:r}=e["plugin-opts"];t=`,obfs=${r}${s?",obfs-host="+s:""}`}if("v2ray-plugin"===e.plugin){const{tls:s,host:r,path:n}=e["plugin-opts"];t=`,obfs=${s?"wss":"ws"}${r?",obfs-host="+r:""}${n?",obfs-uri="+n:""}`}return`shadowsocks=${e.server}:${e.port},method=${e.cipher},password=${e.password}${t}${e.tfo?",fast-open=true":",fast-open=false"}${e.udp?",udp-relay=true":",udp-relay=false"},tag=${e.name}`;case"ssr":return`shadowsocks=${e.server}:${e.port},method=${e.cipher},password=${e.password},ssr-protocol=${e.protocol}${e["protocol-param"]?",ssr-protocol-param="+e["protocol-param"]:""}${e.obfs?",obfs="+e.obfs:""}${e["obfs-param"]?",obfs-host="+e["obfs-param"]:""}${e.tfo?",fast-open=true":",fast-open=false"}${e.udp?",udp-relay=true":",udp-relay=false"},tag=${e.name}`;case"vmess":return t="","ws"===e.network?t=e.tls?`,obfs=wss${e.sni?",obfs-host="+e.sni:""}${e["ws-path"]?",obfs-uri="+e["ws-path"]:""},tls-verification=${e["skip-cert-verify"]?"false":"true"}`:`,obfs=ws${e["ws-headers"].Host?",obfs-host="+e["ws-headers"].Host:""}${e["ws-path"]?",obfs-uri="+e["ws-path"]:""}`:e.tls&&(t=`,obfs=over-tls${e.sni?",obfs-host="+e.sni:""},tls-verification=${e["skip-cert-verify"]?"false":"true"}`),`vmess=${e.server}:${e.port},method=${"auto"===e.cipher?"none":e.cipher},password=${e.uuid}${t}${e.tfo?",fast-open=true":",fast-open=false"}${e.udp?",udp-relay=true":",udp-relay=false"},tag=${e.name}`;case"trojan":return`trojan=${e.server}:${e.port},password=${e.password}${e.sni?",tls-host="+e.sni:""},over-tls=true,tls-verification=${e["skip-cert-verify"]?"false":"true"}${e.tfo?",fast-open=true":",fast-open=false"}${e.udp?",udp-relay=true":",udp-relay=false"},tag=${e.name}`;case"http":return s="",e.tls&&(s=`,over-tls=true,tls-verification=${e["skip-cert-verify"]?"false":"true"}${e.sni?",tls-host="+e.sni:""}`),`http=${e.server}:${e.port},username=${e.username},password=${e.password}${s}${e.tfo?",fast-open=true":",fast-open=false"},tag=${e.name}`}throw new Error(`Platform QX does not support proxy type: ${e.type}`)}},Surge:{produce:e=>{let t,s;switch(e.type){case"ss":if(t="",e.plugin){const{host:s,mode:r}=e["plugin-opts"];if("obfs"!==e.plugin)throw new Error(`Platform Surge does not support obfs option: ${e.obfs}`);t=`,obfs=${r}${s?",obfs-host="+s:""}`}return`${e.name}=ss,${e.server}, ${e.port},encrypt-method=${e.cipher},password=${e.password}${t},tfo=${e.tfo||"false"},udp-relay=${e.udp||"false"}`;case"vmess":s="";let r=`${e.name}=vmess,${e.server},${e.port},username=${e.uuid},tls=${e.tls||"false"},tfo=${e.tfo||"false"}`;if("ws"===e.network){const t=e["ws-path"]||"/",s=e["ws-headers"].Host;r+=`,ws=true${t?",ws-path="+t:""}${s?",ws-headers=HOST:"+s:""}`}return e.tls&&(r+=`${void 0!==e["skip-cert-verify"]?",skip-cert-verify="+e["skip-cert-verify"]:""}`,r+=e.sni?`,sni=${e.sni}`:""),r;case"trojan":return`${e.name}=trojan,${e.server},${e.port},password=${e.password}${void 0!==e["skip-cert-verify"]?",skip-cert-verify="+e["skip-cert-verify"]:""}${e.sni?",sni="+e.sni:""},tfo=${e.tfo||"false"}`;case"http":return s=", tls=false",e.tls&&(s=`,tls=true,skip-cert-verify=${e["skip-cert-verify"]},sni=${e.sni}`),`${e.name}=http, ${e.server}, ${e.port}${e.username?",username="+e.username:""}${e.password?",password="+e.password:""}${s},tfo=${e.tfo||"false"}`}throw new Error(`Platform Surge does not support proxy type: ${e.type}`)}},Loon:{produce:e=>{let t,s;switch(e.type){case"ss":if(t=",,",e.plugin){if("obfs"!==e.plugin)throw new Error(`Platform Loon does not support obfs option: ${e.obfs}`);{const{mode:s,host:r}=e["plugin-opts"];t=`,${s},${r||""}`}}return`${e.name}=shadowsocks,${e.server},${e.port},${e.cipher},"${e.password}"${t}`;case"ssr":return`${e.name}=shadowsocksr,${e.server},${e.port},${e.cipher},"${e.password}",${e.protocol},{${e["protocol-param"]||""}},${e.obfs},{${e["obfs-param"]||""}}`;case"vmess":return t="",t="ws"===e.network?`,transport:ws,host:${e["ws-headers"].Host||e.server},path:${e["ws-path"]||"/"}`:",transport:tcp",e.tls&&(t+=`${e.sni?",tls-name:"+e.sni:""},skip-cert-verify:${e["skip-cert-verify"]||"false"}`),`${e.name}=vmess,${e.server},${e.port},${"auto"===e.cipher?"none":e.cipher},"${e.uuid}",over-tls:${e.tls||"false"}${t}`;case"trojan":return`${e.name}=trojan,${e.server},${e.port},"${e.password}"${e.sni?",tls-name:"+e.sni:""},skip-cert-verify:${e["skip-cert-verify"]||"false"}`;case"http":s="";const r=`${e.name}=${e.tls?"http":"https"},${e.server},${e.port},${e.username||""},${e.password||""}`;return e.tls?r+(s=`${e.sni?",tls-name:"+e.sni:""},skip-cert-verify:${e["skip-cert-verify"]}`):r}throw new Error(`Platform Loon does not support proxy type: ${e.type}`)}},Clash:{type:"ALL",produce:e=>"proxies:\n"+e.map(e=>(delete e.supported," - "+JSON.stringify(e)+"\n")).join("")},URI:{type:"SINGLE",produce:e=>{let t="";switch(e.type){case"ss":const s=`${e.cipher}:${e.password}`;if(t=`ss://${Base64.safeEncode(s)}@${e.server}:${e.port}/`,e.plugin){t+="?plugin=";const s=e["plugin-opts"];switch(e.plugin){case"obfs":t+=encodeURIComponent(`simple-obfs;obfs=${s.mode}${s.host?";obfs-host="+s.host:""}`);break;case"v2ray-plugin":t+=encodeURIComponent(`v2ray-plugin;obfs=${s.mode}${s.host?";obfs-host"+s.host:""}${s.tls?";tls":""}`);break;default:throw new Error(`Unsupported plugin option: ${e.plugin}`)}}t+=`#${encodeURIComponent(e.name)}`;break;case"ssr":t=`${e.server}:${e.port}:${e.protocol}:${e.cipher}:${e.obfs}:${Base64.safeEncode(e.password)}/`,t+=`?remarks=${Base64.safeEncode(e.name)}${e["obfs-param"]?"&obfsparam="+Base64.safeEncode(e["obfs-param"]):""}${e["protocol-param"]?"&protocolparam="+Base64.safeEncode(e["protocol-param"]):""}`,t="ssr://"+Base64.safeEncode(t);break;case"vmess":t={ps:e.name,add:e.server,port:e.port,id:e.uuid,type:"",aid:0,net:e.network||"tcp",tls:e.tls?"tls":""},"ws"===e.network&&(t.path=e["ws-path"]||"/",t.host=e["ws-headers"].Host||e.server),t="vmess://"+Base64.safeEncode(JSON.stringify(t));break;case"trojan":t=`trojan://${e.password}@${e.server}:${e.port}#${encodeURIComponent(e.name)}`;break;default:throw new Error(`Cannot handle proxy type: ${e.type}`)}return t}},JSON:{type:"ALL",produce:e=>JSON.stringify(e,null,2)}}}();function preprocess(e){for(const t of PROXY_PREPROCESSORS)try{if(t.test(e))return $.log(`Pre-processor [${t.name}] activated`),t.parse(e)}catch(e){$.error(`Parser [${t.name}] failed\n Reason: ${e}`)}return e}function safeMatch(e,t){let s;try{s=e.test(t)}catch(e){s=!1}return s}function parse(e){const t=(e=preprocess(e)).split("\n"),s=[];let r;for(let e of t){if(0===(e=e.trim()).length)continue;let t=r&&safeMatch(r,e);if(!t)for(const s of PROXY_PARSERS)if(safeMatch(s,e)){r=s,t=!0,$.log(`Proxy parser: ${s.name} is activated`);break}if(t)try{const t=r.parse(e);t||$.error(`Parser ${r.name} return nothing for \n${e}\n`),s.push(t)}catch(t){$.error(`Failed to parse line: \n ${e}\n Reason: ${t.stack}`)}else $.error(`Failed to find a rule to parse line: \n${e}\n`)}return s}async function process(e,t=[]){for(const s of t){let t,r;if(-1!==s.type.indexOf("Script")){const{mode:e,content:r}=s.args;t="link"===e?await $.http.get(r).then(e=>e.body).catch(e=>{throw new Error(`Error when downloading remote script: ${s.args.content}.\n Reason: ${e}`)}):r}PROXY_PROCESSORS[s.type]?($.log(`Applying "${s.type}" with arguments:\n >>> ${JSON.stringify(s.args,null,2)||"None"}`),e=ApplyProcessor(r=-1!==s.type.indexOf("Script")?PROXY_PROCESSORS[s.type](t):PROXY_PROCESSORS[s.type](s.args),e)):$.error(`Unknown operator: "${s.type}"`)}return e}function produce(e,t){const s=PROXY_PRODUCERS[t];if(!s)throw new Error(`Target platform: ${t} is not supported!`);return e=e.filter(e=>!(e.supported&&!1===e.supported[t])),$.log(`Producing proxies for target: ${t}`),void 0===s.type||"SINGLE"===s.type?e.map(e=>{try{return s.produce(e)}catch(t){return $.error(`Cannot produce proxy: ${JSON.stringify(e,null,2)}\nReason: ${t}`),""}}).filter(e=>e.length>0).join("\n"):"ALL"===s.type?s.produce(e):void 0}return{parse:parse,process:process,produce:produce}}(),RuleUtils=function(){const e=function(){function e(){const e=["DOMAIN","DOMAIN-SUFFIX","DOMAIN-KEYWORD","IP-CIDR","IP-CIDR6","USER-AGENT","URL-REGEX","DEST-PORT","SRC-IP","IN-PORT","PROTOCOL"];return{name:"Surge Rule Set Parser",test:t=>0!==t.indexOf("payload:")&&e.some(e=>-1!==t.indexOf(e)),parse:t=>{const s=t.split("\n"),r=[];for(let t of s)if(t=t.trim(),!/\s*#/.test(t)&&e.some(e=>0===t.indexOf(e)))try{const e=t.split(",").map(e=>e.trim()),s={type:e[0],content:e[1]};"IP-CIDR"!==s.type&&"IP-CIDR6"!==s.type||(s.options=e.slice(2)),r.push(s)}catch(e){console.error(`Failed to parse line: ${t}\n Reason: ${e}`)}return r}}}return[e(),function(){const t=["host","host-suffix","host-keyword","ip-cidr","ip6-cidr","user-agent"];return{name:"QX Filter",test:e=>t.some(t=>0===e.indexOf(t.toLowerCase())),parse:t=>{const s=t.split("\n");for(let e=0;e0===e.indexOf("payload:")&&t.some(t=>-1!==e.indexOf(t)),parse:t=>{try{const s=YAML.eval(t).payload.map(e=>"string"==typeof e?e.replace(/DST-PORT/i,"DEST-PORT").replace(/SRC-IP-CIDR/i,"SRC-IP").replace(/SRC-PORT/i,"IN-PORT"):"").filter(e=>e.length>0).join("\n");return e().parse(s)}catch(e){console.error(`Cannot parse rules: ${e}`)}return[]}}}()]}(),t=function(){return{"Regex Filter":function({regex:e=[],keep:t=!0}){return{name:"Regex Filter",func:s=>s.map(s=>{const r=e.some(e=>(e=new RegExp(e)).test(s));return t?r:!r})}},"Remove Duplicate Filter":function(){return{name:"Remove Duplicate Filter",func:e=>{const t=new Set,s=[];return e.forEach(e=>{const r=e.options||[];r.sort();const n=`${e.type},${e.content},${JSON.stringify(r)}`;t.has(n)||(s.push(e),t.add(n))}),s}}},"Type Filter":function(e){return{name:"Type Filter",func:t=>t.map(t=>e.some(e=>t.type===e))}},"Regex Replace Operator":function(e){return{name:"Regex Rename Operator",func:t=>t.map(t=>{for(const{expr:s,now:r}of e)t.content=t.content.replace(new RegExp(s,"g"),r).trim();return t})}}}}(),s=function(){return{QX:{type:"SINGLE",func:e=>-1!==["URL-REGEX","DEST-PORT","SRC-IP","IN-PORT","PROTOCOL"].indexOf(e.type)?null:`${{"DOMAIN-KEYWORD":"HOST-KEYWORD","DOMAIN-SUFFIX":"HOST-SUFFIX",DOMAIN:"HOST","IP-CIDR6":"IP6-CIDR"}[e.type]||e.type},${e.content},SUB-STORE`},Surge:{type:"SINGLE",func:e=>{let t=`${e.type},${e.content}`;return"IP-CIDR"!==e.type&&"IP-CIDR6"!==e.type||(t+=e.options?`,${e.options[0]}`:""),t}},Loon:{type:"SINGLE",func:e=>-1!==["DEST-PORT","SRC-IP","IN-PORT","PROTOCOL"].indexOf(e.type)?null:(e=>{let t=`${e.type},${e.content}`;return"IP-CIDR"!==e.type&&"IP-CIDR6"!==e.type||(t+=e.options?`,${e.options[0]}`:""),t})(e)},Clash:{type:"ALL",func:e=>{const t={"DEST-PORT":"DST-PORT","SRC-IP":"SRC-IP-CIDR","IN-PORT":"SRC-PORT"},s={payload:e.map(e=>{let s=`${t[e.type]||e.type},${e.content}`;return"IP-CIDR"!==e.type&&"IP-CIDR6"!==e.type||(s+=e.options?`,${e.options[0]}`:""),s})};return YAML.stringify(s)}}}}();return{parse:function(t){for(const s of e){let e;try{e=s.test(t)}catch{e=!1}if(e)return console.log(`Rule parser [${s.name}] is activated!`),s.parse(t)}},process:async function(e,s){for(const r of s){if(!t[r.type]){console.error(`Unknown operator: ${r.type}!`);continue}const s=t[r.type](r.args);console.log(`Applying "${r.type}" with arguments: \n >>> ${JSON.stringify(r.args)||"None"}`),e=ApplyProcessor(s,e)}return e},produce:function(e,t){const r=s[t];if(!r)throw new Error(`Target platform: ${t} is not supported!`);return void 0===r.type||"SINGLE"===r.type?e.map(e=>{try{return r.func(e)}catch(t){return console.log(`ERROR: cannot produce rule: ${JSON.stringify(e)}\nReason: ${t}`),""}}).filter(e=>e.length>0).join("\n"):"ALL"===r.type?r.func(e):void 0}}}();function getBuiltInRules(){return{AD:{name:"AD",description:"",urls:["https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/anti-ad-surge.txt","https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Providers/BanAD.yaml"]},Global:{name:"Global",description:"",urls:["https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Providers/ProxyGFWlist.yaml","https://raw.githubusercontent.com/DivineEngine/Profiles/master/Quantumult/Filter/Global.list"]},CN:{name:"CN",description:"",urls:["https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Providers/ChinaDomain.yaml","https://raw.githubusercontent.com/DivineEngine/Profiles/master/Quantumult/Filter/China.list"]}}}function ApplyProcessor(e,t){return-1!==e.name.indexOf("Filter")?function(e,t){let s=FULL(t.length,!0);try{s=AND(s,e.func(t))}catch(t){console.log(`Cannot apply filter ${e.name}\n Reason: ${t}`)}return t.filter((e,t)=>s[t])}(e,t):-1!==e.name.indexOf("Operator")?function(e,t){let s=clone(t);try{const t=e.func(s);t&&(s=t)}catch(t){console.log(`Cannot apply operator ${e.name}! Reason: ${t}`)}return s}(e,t):void 0}function AND(...e){return e.reduce((e,t)=>e.map((e,s)=>t[s]&&e))}function OR(...e){return e.reduce((e,t)=>e.map((e,s)=>t[s]||e))}function NOT(e){return e.map(e=>!e)}function FULL(e,t){return[...Array(e).keys()].map(()=>t)}function clone(e){return JSON.parse(JSON.stringify(e))}function ENV(){const e="undefined"!=typeof $task,t="undefined"!=typeof $loon,s="undefined"!=typeof $httpClient&&!t,r="function"==typeof require&&"undefined"!=typeof $jsbox;return{isQX:e,isLoon:t,isSurge:s,isNode:"function"==typeof require&&!r,isJSBox:r,isRequest:"undefined"!=typeof $request,isScriptable:"undefined"!=typeof importModule}}function HTTP(e={baseURL:""}){const{isQX:t,isLoon:s,isSurge:r,isScriptable:n,isNode:o}=ENV(),a=/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/;const i={};return["GET","POST","PUT","DELETE","HEAD","OPTIONS","PATCH"].forEach(p=>i[p.toLowerCase()]=(i=>(function(i,p){p="string"==typeof p?{url:p}:p;const l=e.baseURL;l&&!a.test(p.url||"")&&(p.url=l?l+p.url:p.url);const c=(p={...e,...p}).timeout,u={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...p.events};let f,d;if(u.onRequest(i,p),t)f=$task.fetch({method:i,...p});else if(s||r||o)f=new Promise((e,t)=>{(o?require("request"):$httpClient)[i.toLowerCase()](p,(s,r,n)=>{s?t(s):e({statusCode:r.status||r.statusCode,headers:r.headers,body:n})})});else if(n){const e=new Request(p.url);e.method=i,e.headers=p.headers,e.body=p.body,f=new Promise((t,s)=>{e.loadString().then(s=>{t({statusCode:e.response.statusCode,headers:e.response.headers,body:s})}).catch(e=>s(e))})}const h=c?new Promise((e,t)=>{d=setTimeout(()=>(u.onTimeout(),t(`${i} URL: ${p.url} exceeds the timeout ${c} ms`)),c)}):null;return(h?Promise.race([h,f]).then(e=>(clearTimeout(d),e)):f).then(e=>u.onResponse(e))})(p,i))),i}function API(e="untitled",t=!1){const{isQX:s,isLoon:r,isSurge:n,isNode:o,isJSBox:a,isScriptable:i}=ENV();return new class{constructor(e,t){this.name=e,this.debug=t,this.http=HTTP(),this.env=ENV(),this.node=(()=>{if(o){return{fs:require("fs")}}return null})(),this.initCache();Promise.prototype.delay=function(e){return this.then(function(t){return((e,t)=>new Promise(function(s){setTimeout(s.bind(null,t),e)}))(e,t)})}}initCache(){if(s&&(this.cache=JSON.parse($prefs.valueForKey(this.name)||"{}")),(r||n)&&(this.cache=JSON.parse($persistentStore.read(this.name)||"{}")),o){let e="root.json";this.node.fs.existsSync(e)||this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.root={},e=`${this.name}.json`,this.node.fs.existsSync(e)?this.cache=JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)):(this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.cache={})}}persistCache(){const e=JSON.stringify(this.cache,null,2);s&&$prefs.setValueForKey(e,this.name),(r||n)&&$persistentStore.write(e,this.name),o&&(this.node.fs.writeFileSync(`${this.name}.json`,e,{flag:"w"},e=>console.log(e)),this.node.fs.writeFileSync("root.json",JSON.stringify(this.root,null,2),{flag:"w"},e=>console.log(e)))}write(e,t){if(this.log(`SET ${t}`),-1!==t.indexOf("#")){if(t=t.substr(1),n||r)return $persistentStore.write(e,t);if(s)return $prefs.setValueForKey(e,t);o&&(this.root[t]=e)}else this.cache[t]=e;this.persistCache()}read(e){return this.log(`READ ${e}`),-1===e.indexOf("#")?this.cache[e]:(e=e.substr(1),n||r?$persistentStore.read(e):s?$prefs.valueForKey(e):o?this.root[e]:void 0)}delete(e){if(this.log(`DELETE ${e}`),-1!==e.indexOf("#")){if(e=e.substr(1),n||r)return $persistentStore.write(null,e);if(s)return $prefs.removeValueForKey(e);o&&delete this.root[e]}else delete this.cache[e];this.persistCache()}notify(e,t="",p="",l={}){const c=l["open-url"],u=l["media-url"];if(s&&$notify(e,t,p,l),n&&$notification.post(e,t,p+`${u?"\n多媒体:"+u:""}`,{url:c}),r){let s={};c&&(s.openUrl=c),u&&(s.mediaUrl=u),"{}"===JSON.stringify(s)?$notification.post(e,t,p):$notification.post(e,t,p,s)}if(o||i){const s=p+(c?`\n点击跳转: ${c}`:"")+(u?`\n多媒体: ${u}`:"");if(a){require("push").schedule({title:e,body:(t?t+"\n":"")+s})}else console.log(`${e}\n${t}\n${s}\n\n`)}}log(e){this.debug&&console.log(`[${this.name}] LOG: ${e}`)}info(e){console.log(`[${this.name}] INFO: ${e}`)}error(e){console.log(`[${this.name}] ERROR: ${e}`)}wait(e){return new Promise(t=>setTimeout(t,e))}done(e={}){s||r||n?$done(e):o&&!a&&"undefined"!=typeof $context&&($context.headers=e.headers,$context.statusCode=e.statusCode,$context.body=e.body)}}(e,t)}function Gist(e,t){const s=HTTP({baseURL:"https://api.github.com",headers:{Authorization:`token ${t}`,"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36"},events:{onResponse:e=>/^[45]/.test(String(e.statusCode))?Promise.reject(`ERROR: ${JSON.parse(e.body).message}`):e}});async function r(){return s.get("/gists").then(t=>{const s=JSON.parse(t.body);for(let t of s)if(t.description===e)return t.id;return-1})}this.upload=async function(t){const n=await r(),o={"Sub-Store":{content:t}};return-1===n?s.post({url:"/gists",body:JSON.stringify({description:e,public:!1,files:o})}):s.patch({url:`/gists/${n}`,body:JSON.stringify({files:o})})},this.download=async function(){const e=await r();if(-1===e)return Promise.reject("未找到Gist备份!");try{const{files:t}=await s.get(`/gists/${e}`).then(e=>JSON.parse(e.body)),r=t["Sub-Store"].raw_url;return await s.get(r).then(e=>e.body)}catch(e){return Promise.reject(e)}}}function express({port:e}={port:3e3}){const{isNode:t}=ENV(),s={"Content-Type":"text/plain;charset=UTF-8","Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"POST,GET,OPTIONS,PATCH,PUT,DELETE","Access-Control-Allow-Headers":"Origin, X-Requested-With, Content-Type, Accept"};if(t){const t=require("express"),r=require("body-parser"),n=t();return n.use(r.json({verify:i})),n.use(r.urlencoded({verify:i,extended:!0})),n.use(r.raw({verify:i,type:"*/*"})),n.use((e,t,r)=>{t.set(s),r()}),n.start=(()=>{n.listen(e,()=>{$.info(`Express started on port: ${e}`)})}),n}const r=[],n=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD'","ALL"],o=(e,t=0)=>{let{method:s,url:n,headers:a,body:i}=e;/json/i.test(a["Content-Type"])&&(i=JSON.parse(i)),s=s.toUpperCase();const{path:u,query:f}=function(e){const t=(e.match(/https?:\/\/[^\/]+(\/[^?]*)/)||[])[1]||"/",s=e.indexOf("?"),r={};if(-1!==s){let t=e.slice(e.indexOf("?")+1).split("&");for(let e=0;em&&(h=r[d],m=e.split("/").length)}if(h){const e=()=>{o(s,n,d)},t={method:s,url:n,path:u,query:f,params:c(h.pattern,u),headers:a,body:i},r=p(),l=h.callback,m=e=>{r.status(500).json({status:"failed",message:`Internal Server Error: ${e}`})};if("AsyncFunction"===l.constructor.name)l(t,r,e).catch(m);else try{l(t,r,e)}catch(e){m(e)}}else{p().status(404).json({status:"failed",message:"ERROR: 404 not found"})}},a={};return n.forEach(e=>{a[e.toLowerCase()]=((t,s)=>{r.push({method:e,pattern:t,callback:s})})}),a.route=(e=>{const t={};return n.forEach(s=>{t[s.toLowerCase()]=(n=>(r.push({method:s,pattern:e,callback:n}),t))}),t}),a.start=(()=>{o($request)}),a;function i(e,t,s,r){s&&s.length&&(e.rawBody=s.toString(r||"utf8"))}function p(){let e=200;const{isQX:t,isLoon:r,isSurge:n}=ENV(),o=s,a={200:"HTTP/1.1 200 OK",201:"HTTP/1.1 201 Created",302:"HTTP/1.1 302 Found",307:"HTTP/1.1 307 Temporary Redirect",308:"HTTP/1.1 308 Permanent Redirect",404:"HTTP/1.1 404 Not Found",500:"HTTP/1.1 500 Internal Server Error"};return new class{status(t){return e=t,this}send(s=""){const i={status:t?a[e]:e,body:s,headers:o};t?$done(i):(r||n)&&$done({response:i})}end(){this.send()}html(e){this.set("Content-Type","text/html;charset=UTF-8"),this.send(e)}json(e){this.set("Content-Type","application/json;charset=UTF-8"),this.send(JSON.stringify(e))}set(e,t){return o[e]=t,this}}}function l(e,t){if(e instanceof RegExp&&e.test(t))return!0;if("/"===e)return!0;if(-1===e.indexOf(":")){const s=t.split("/"),r=e.split("/");for(let e=0;e>>6)+s(128|63&t):s(224|t>>>12&15)+s(128|t>>>6&63)+s(128|63&t):(t=65536+1024*(e.charCodeAt(0)-55296)+(e.charCodeAt(1)-56320),s(240|t>>>18&7)+s(128|t>>>12&63)+s(128|t>>>6&63)+s(128|63&t))},n=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g,o=function(t){const s=[0,2,1][t.length%3],r=t.charCodeAt(0)<<16|(t.length>1?t.charCodeAt(1):0)<<8|(t.length>2?t.charCodeAt(2):0);return[e.charAt(r>>>18),e.charAt(r>>>12&63),s>=2?"=":e.charAt(r>>>6&63),s>=1?"=":e.charAt(63&r)].join("")};this.encode=function(e){return"[object Uint8Array]"===Object.prototype.toString.call(e)?e.toString("base64"):function(e){return e.replace(n,r)}(String(e)).replace(/[\s\S]{1,3}/g,o)};const a=/[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g,i=function(e){switch(e.length){case 4:const t=((7&e.charCodeAt(0))<<18|(63&e.charCodeAt(1))<<12|(63&e.charCodeAt(2))<<6|63&e.charCodeAt(3))-65536;return s(55296+(t>>>10))+s(56320+(1023&t));case 3:return s((15&e.charCodeAt(0))<<12|(63&e.charCodeAt(1))<<6|63&e.charCodeAt(2));default:return s((31&e.charCodeAt(0))<<6|63&e.charCodeAt(1))}},p=function(e){const r=e.length,n=r%4,o=(r>0?t[e.charAt(0)]<<18:0)|(r>1?t[e.charAt(1)]<<12:0)|(r>2?t[e.charAt(2)]<<6:0)|(r>3?t[e.charAt(3)]:0),a=[s(o>>>16),s(o>>>8&255),s(255&o)];return a.length-=[0,0,2,1][n],a.join("")},l=function(e){return e.replace(/\S{1,4}/g,p)},c=function(e){return l(e).replace(a,i)};this.decode=function(e){return c(String(e).replace(/[-_]/g,function(e){return"-"===e?"+":"/"}).replace(/[^A-Za-z0-9\+\/]/g,"")).replace(/>/g,">").replace(/</g,"<")},this.safeEncode=function(e){return this.encode(e.replace(/\+/g,"-").replace(/\//g,"_"))},this.safeDecode=function(e){return this.decode(e.replace(/-/g,"+").replace(/_/g,"/"))}}var YAML=function(){var e=[],t=[],s=0,r={regLevel:new RegExp("^([\\s\\-]+)"),invalidLine:new RegExp("^\\-\\-\\-|^\\.\\.\\.|^\\s*#.*|^\\s*$"),dashesString:new RegExp('^\\s*\\"([^\\"]*)\\"\\s*$'),quotesString:new RegExp("^\\s*\\'([^\\']*)\\'\\s*$"),float:new RegExp("^[+-]?[0-9]+\\.[0-9]+(e[+-]?[0-9]+(\\.[0-9]+)?)?$"),integer:new RegExp("^[+-]?[0-9]+$"),array:new RegExp("\\[\\s*(.*)\\s*\\]"),map:new RegExp("\\{\\s*(.*)\\s*\\}"),key_value:new RegExp("([a-z0-9_-][ a-z0-9_-]*):( .+)","i"),single_key_value:new RegExp("^([a-z0-9_-][ a-z0-9_-]*):( .+?)$","i"),key:new RegExp("([a-z0-9_-][ a-z0-9_-]*):( .+)?","i"),item:new RegExp("^-\\s+"),trim:new RegExp("^\\s+|\\s+$"),comment:new RegExp("([^\\'\\\"#]+([\\'\\\"][^\\'\\\"]*[\\'\\\"])*)*(#.*)?")};function n(e){return{parent:null,length:0,level:e,lines:[],children:[],addChild:function(e){this.children.push(e),e.parent=this,++this.length}}}function o(e){var t=null;if("true"==(e=e.replace(r.trim,"")))return!0;if("false"==e)return!1;if(".NaN"==e)return Number.NaN;if("null"==e)return null;if(".inf"==e)return Number.POSITIVE_INFINITY;if("-.inf"==e)return Number.NEGATIVE_INFINITY;if(t=e.match(r.dashesString))return t[1];if(t=e.match(r.quotesString))return t[1];if(t=e.match(r.float))return parseFloat(t[0]);if(t=e.match(r.integer))return parseInt(t[0]);if(isNaN(t=Date.parse(e))){if(t=e.match(r.single_key_value))return(a={})[t[1]]=o(t[2]),a;if(t=e.match(r.array)){for(var s=0,n=" ",a=[],i="",p=!1,l=0,c=t[1].length;l0&&a.push(o(i)),a}if(t=e.match(r.map)){for(s=0,n=" ",a=[],i="",p=!1,l=0,c=t[1].length;l0&&a.push(i);var u={};for(l=0,c=a.length;l"==v[0]?null!=f?f[O]=a(u.shift()):l[O]=a(u.shift()):null!=f?f[O]=o(v):l[O]=o(v)}else null!=f?f[O]=s(u):l[O]=s(u)}else S.match(/^-\s*$/)?(m&&(m=!1,void 0===l.length&&(l=[])),null!=f&&l.push(f),f={},m=!0):(p=S.match(/^-\s*(.*)/))&&(null!=f?f.push(o(p[1])):(m&&(m=!1,void 0===l.length&&(l=[])),l.push(o(p[1]))))}null!=f&&(m&&(m=!1,void 0===l.length&&(l=[])),l.push(f))}for($=h.length-1;$>=0;--$)n.splice.call(n,h[$],1);return l}(s.children)}return{eval:function(o){e=[],t=[],s=(new Date).getTime();var a=p(function(t){var s,o=r.regLevel,a=r.invalidLine,i=t.split("\n"),p=0,l=0,c=[],u=new n(-1),f=new n(0);u.addChild(f);var d=[],h="";c.push(f),d.push(p);for(var m=0,$=i.length;m<$;++m)if(!(h=i[m]).match(a)){if((p=(s=o.exec(h))?s[1].length:0)>l){var g=f;f=new n(p),g.addChild(f),c.push(f),d.push(p)}else if(p=0;--w)if(d[w]==p){f=new n(p),c.push(f),d.push(p),null!=c[w].parent&&c[w].parent.addChild(f),y=!0;break}if(!y)return void e.push("Error: Invalid indentation at line "+m+": "+h)}f.lines.push(h.replace(r.trim,"")),l=p}return u}(function(e){var t,s=e.split("\n"),n=r.comment;for(var o in s)(t="string"==typeof s[o]&&s[o].match(n))&&void 0!==t[3]&&(s[o]=t[0].substr(0,t[0].length-t[3].length));return s.join("\n")}(o)));return s=(new Date).getTime()-s,a},getErrors:function(){return e},getProcessingTime:function(){return s}}}(); \ No newline at end of file diff --git a/scripts/sub-store-parser.js b/scripts/sub-store-parser.js new file mode 100644 index 0000000..af9cd1c --- /dev/null +++ b/scripts/sub-store-parser.js @@ -0,0 +1,3621 @@ +/* + /$$$$$$ /$$ /$$$$$$ /$$ + /$$__ $$ | $$ /$$__ $$| $$ +| $$ \__//$$ /$| $$$$$$$ | $$ \__/$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ +| $$$$$$| $$ | $| $$__ $$/$$$$$| $$$$$|_ $$_/ /$$__ $$/$$__ $$/$$__ $$ + \____ $| $$ | $| $$ \ $|______/\____ $$| $$ | $$ \ $| $$ \__| $$$$$$$$ + /$$ \ $| $$ | $| $$ | $$ /$$ \ $$| $$ /$| $$ | $| $$ | $$_____/ +| $$$$$$| $$$$$$| $$$$$$$/ | $$$$$$/| $$$$| $$$$$$| $$ | $$$$$$$ + \______/ \______/|_______/ \______/ \___/ \______/|__/ \_______/ + +Sub-Store 资源解析器版 © Peng-YM +@author: Peng-YM +@github: https://github.com/Peng-YM/Sub-Store +*/ +const $ = API("sub-store"); +const Base64 = new Base64Code(); + +function parseResource() { + // parse + let result = $resource; + switch ($resourceType) { + case 1: + const proxies = ProxyUtils.parse($resource); + result = ProxyUtils.produce(proxies, "Loon"); + break; + case 2: + const rules = RuleUtils.parse($resource); + result = RuleUtils.produce(rules, "Loon"); + break; + } + $done(result); +} + +/****************************************** Proxy Utils **********************************************************/ +var ProxyUtils = (function () { + const PROXY_PREPROCESSORS = (function () { + function HTML() { + const name = "HTML"; + const test = raw => /^/.test(raw); + // simply discard HTML + const parse = _ => ""; + return {name, test, parse}; + } + + function Base64Encoded() { + const name = "Base64 Pre-processor"; + + const keys = ["dm1lc3M", "c3NyOi8v", "dHJvamFu", "c3M6Ly", "c3NkOi8v"]; + + const test = function (raw) { + return keys.some(k => raw.indexOf(k) !== -1); + } + const parse = function (raw) { + raw = Base64.safeDecode(raw); + return raw; + } + return {name, test, parse}; + } + + function Clash() { + const name = "Clash Pre-processor"; + const test = function (raw) { + return /proxies/.test(raw); + } + const parse = function (raw) { + // Clash YAML format + // codes are modified from @KOP-XIAO + // https://github.com/KOP-XIAO/QuantumultX + if (raw.indexOf("{") !== -1) { + raw = raw + .replace(/ - /g, " - ") + .replace(/:(?!\s)/g, ": ") + .replace(/\,\"/g, ', "') + .replace(/: {/g, ": {, ") + .replace(/, (\"?host|path|tls|mux|skip\"?)/g, ", $1") + .replace(/{name: /g, '{name: "') + .replace(/, server:/g, '", server:') + .replace(/{|}/g, "") + .replace(/,/g, "\n "); + } + raw = raw.replace(/ -\n.*name/g, " - name") + .replace(/\$|\`/g, "") + .split("proxy-providers:")[0] + .split("proxy-groups:")[0] + .replace(/\"([\w-]+)\"\s*:/g, "$1:") + raw = raw.indexOf("proxies:") === -1 ? "proxies:\n" + raw : "proxies:" + raw.split("proxies:")[1] + const proxies = YAML.eval(raw).proxies; + return proxies.map(p => JSON.stringify(p)).join("\n"); + } + return {name, test, parse}; + } + + function SSD() { + const name = "SSD Pre-processor"; + const test = function (raw) { + return raw.indexOf("ssd://") === 0; + }; + const parse = function (raw) { + // preprocessing for SSD subscription format + const output = []; + let ssdinfo = JSON.parse(Base64.safeDecode(raw.split("ssd://")[1])); + // options (traffic_used, traffic_total, expiry, url) + const traffic_used = ssdinfo.traffic_used; // GB + const traffic_total = ssdinfo.traffic_total; // GB, -1 means unlimited + const expiry = ssdinfo.expiry; // YYYY-MM-DD HH:mm:ss + // default setting + let name = ssdinfo.airport; // name of the airport + let port = ssdinfo.port; + let method = ssdinfo.encryption; + let password = ssdinfo.password; + // servers config + let servers = ssdinfo.servers; + for (let i = 0; i < servers.length; i++) { + let server = servers[i]; + method = server.encryption ? server.encryption : method; + password = server.password ? server.password : password; + let userinfo = Base64.safeEncode(method + ":" + password); + let hostname = server.server; + port = server.port ? server.port : port; + let tag = server.remarks ? server.remarks : i; + let plugin = server.plugin_options + ? "/?plugin=" + + encodeURIComponent(server.plugin + ";" + server.plugin_options) + : ""; + output[i] = + "ss://" + userinfo + "@" + hostname + ":" + port + plugin + "#" + tag; + } + return output.join("\n"); + }; + return {name, test, parse}; + } + + return [ + HTML(), Base64Encoded(), Clash(), SSD() + ]; + })(); + const PROXY_PARSERS = (function () { + // Parse SS URI format (only supports new SIP002, legacy format is depreciated). + // reference: https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html + function URI_SS() { + const name = "URI SS Parser"; + const test = (line) => { + return /^ss:\/\//.test(line); + }; + const parse = (line) => { + const supported = {}; + // parse url + let content = line.split("ss://")[1]; + + const proxy = { + name: decodeURIComponent(line.split("#")[1]), + type: "ss", + supported, + }; + content = content.split("#")[0]; // strip proxy name + // handle IPV4 and IPV6 + const serverAndPort = content.match(/@([^\/]*)(\/|$)/)[1]; + const portIdx = serverAndPort.lastIndexOf(":"); + proxy.server = serverAndPort.substring(0, portIdx); + proxy.port = serverAndPort.substring(portIdx + 1); + + const userInfo = Base64.safeDecode(content.split("@")[0]).split(":"); + proxy.cipher = userInfo[0]; + proxy.password = userInfo[1]; + + // handle obfs + const idx = content.indexOf("?plugin="); + if (idx !== -1) { + const pluginInfo = ( + "plugin=" + + decodeURIComponent(content.split("?plugin=")[1].split("&")[0]) + ).split(";"); + const params = {}; + for (const item of pluginInfo) { + const [key, val] = item.split("="); + if (key) params[key] = val || true; // some options like "tls" will not have value + } + switch (params.plugin) { + case "obfs-local": + case "simple-obfs": + proxy.plugin = "obfs"; + proxy["plugin-opts"] = { + mode: params.obfs, + host: params["obfs-host"], + }; + break; + case "v2ray-plugin": + proxy.supported = { + ...supported, + Loon: false, + Surge: false, + }; + proxy.obfs = "v2ray-plugin"; + proxy["plugin-opts"] = { + mode: "websocket", + host: params["obfs-host"], + path: params.path || "", + tls: params.tls || false, + }; + break; + default: + throw new Error(`Unsupported plugin option: ${params.plugin}`); + } + } + return proxy; + }; + return {name, test, parse}; + } + + // Parse URI SSR format, such as ssr://xxx + function URI_SSR() { + const name = "URI SSR Parser"; + const test = (line) => { + return /^ssr:\/\//.test(line); + }; + const supported = { + Surge: false, + }; + + const parse = (line) => { + line = Base64.safeDecode(line.split("ssr://")[1]); + + // handle IPV6 & IPV4 format + let splitIdx = line.indexOf(":origin"); + if (splitIdx === -1) { + splitIdx = line.indexOf(":auth_"); + } + const serverAndPort = line.substring(0, splitIdx); + const server = serverAndPort.substring(0, serverAndPort.lastIndexOf(":")); + const port = serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1); + + let params = line + .substring(splitIdx + 1) + .split("/?")[0] + .split(":"); + let proxy = { + type: "ssr", + server, + port, + protocol: params[0], + cipher: params[1], + obfs: params[2], + password: Base64.safeDecode(params[3]), + supported, + }; + // get other params + params = {}; + line = line.split("/?")[1].split("&"); + if (line.length > 1) { + for (const item of line) { + const [key, val] = item.split("="); + params[key] = val; + } + } + proxy = { + ...proxy, + name: Base64.safeDecode(params.remarks), + "protocol-param": + Base64.safeDecode(params.protoparam).replace(/\s/g, "") || "", + "obfs-param": + Base64.safeDecode(params.obfsparam).replace(/\s/g, "") || "", + }; + return proxy; + }; + + return {name, test, parse}; + } + + // V2rayN URI VMess format + // reference: https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) + + // Quantumult VMess format + function URI_VMess() { + const name = "URI VMess Parser"; + const test = (line) => { + return /^vmess:\/\//.test(line); + }; + const parse = (line) => { + const supported = {}; + line = line.split("vmess://")[1]; + const content = Base64.safeDecode(line); + if (/=\s*vmess/.test(content)) { + const partitions = content.split(",").map((p) => p.trim()); + // Quantumult VMess URI format + // get keyword params + const params = {}; + for (const part of partitions) { + if (part.indexOf("=") !== -1) { + const [key, val] = part.split("="); + params[key.trim()] = val.trim(); + } + } + + const proxy = { + name: partitions[0].split("=")[0].trim(), + type: "vmess", + server: partitions[1], + port: partitions[2], + cipher: partitions[3], + uuid: partitions[4].match(/^"(.*)"$/)[1], + tls: params.obfs === "over-tls" || params.obfs === "wss", + udp: JSON.parse(params["udp-relay"] || "false"), + tfo: JSON.parse(params["fast-open"] || "false"), + }; + + // handle ws headers + if (params.obfs === "ws" || params.obfs === "wss") { + proxy.network = "ws"; + proxy["ws-path"] = params["obfs-uri"]; + proxy["ws-headers"] = { + Host: params["obfs-host"] || proxy.server, // if no host provided, use the same as server + }; + } + + // handle scert + if (proxy.tls && params['"tls-verification"'] === "false") { + proxy['skip-cert-verify'] = true; + } + + // handle sni + if (proxy.tls && params["obfs-host"]) { + proxy.sni = params["obfs-host"]; + } + + return proxy; + } else { + // V2rayN URI format + const params = JSON.parse(content); + const proxy = { + name: params.ps, + type: "vmess", + server: params.add, + port: params.port, + cipher: "auto", // V2rayN has no default cipher! use aes-128-gcm as default. + uuid: params.id, + alterId: params.aid || 0, + tls: params.tls === "tls" || params.tls === true, + supported, + }; + // handle obfs + if (params.net === "ws") { + proxy.network = "ws"; + proxy["ws-path"] = params.path; + proxy["ws-headers"] = { + Host: params.host || params.add, + }; + if (proxy.tls && params.host) { + proxy.sni = params.host; + } + } + // handle scert + if (params.verify_cert === false) { + proxy['skip-cert-verify'] = true; + } + return proxy; + } + }; + return {name, test, parse}; + } + + // Trojan URI format + function URI_Trojan() { + const name = "URI Trojan Parser"; + const test = (line) => { + return /^trojan:\/\//.test(line); + }; + + const parse = (line) => { + const supported = {}; + // trojan forces to use 443 port + if (line.indexOf(":443") === -1) { + throw new Error("Trojan port should always be 443!"); + } + line = line.split("trojan://")[1]; + const server = line.split("@")[1].split(":443")[0]; + const name = decodeURIComponent(line.split("#")[1].trim()); + + return { + name: name || `[Trojan] ${server}`, // trojan uri may have no server tag! + type: "trojan", + server, + port: 443, + password: line.split("@")[0], + supported, + }; + }; + return {name, test, parse}; + } + + function Clash_All() { + const name = "Clash Parser"; + const test = (line) => { + try { + JSON.parse(line); + } catch (e) { + return false; + } + return true; + }; + const parse = (line) => JSON.parse(line); + return {name, test, parse}; + } + + function QX_SS() { + const name = "QX SS Parser"; + const test = (line) => { + return ( + /^shadowsocks\s*=/.test(line.split(",")[0].trim()) && + line.indexOf("ssr-protocol") === -1 + ); + }; + const parse = (line) => { + const supported = {}; + const params = getQXParams(line); + const proxy = { + name: params.tag, + type: "ss", + server: params.server, + port: params.port, + cipher: params.method, + password: params.password, + udp: JSON.parse(params["udp-relay"] || "false"), + tfo: JSON.parse(params["fast-open"] || "false"), + supported, + }; + // handle obfs options + if (params.obfs) { + proxy["plugin-opts"] = { + host: params["obfs-host"] || proxy.server, + }; + switch (params.obfs) { + case "http": + case "tls": + proxy.plugin = "obfs"; + proxy["plugin-opts"].mode = params.obfs; + break; + case "ws": + case "wss": + proxy["plugin-opts"] = { + ...proxy["plugin-opts"], + mode: "websocket", + path: params["obfs-uri"] || "/", + tls: params.obfs === "wss", + }; + if (proxy["plugin-opts"].tls && typeof params['tls-verification'] !== "undefined") { + proxy["plugin-opts"]['skip-cert-verify'] = params['tls-verification']; + } + proxy.plugin = "v2ray-plugin"; + // Surge and Loon lack support for v2ray-plugin obfs + proxy.supported.Surge = false; + proxy.supported.Loon = false; + break; + } + } + return proxy; + }; + return {name, test, parse}; + } + + function QX_SSR() { + const name = "QX SSR Parser"; + const test = (line) => { + return ( + /^shadowsocks\s*=/.test(line.split(",")[0].trim()) && + line.indexOf("ssr-protocol") !== -1 + ); + }; + + const parse = (line) => { + const supported = { + Surge: false, + }; + const params = getQXParams(line); + const proxy = { + name: params.tag, + type: "ssr", + server: params.server, + port: params.port, + cipher: params.method, + password: params.password, + protocol: params["ssr-protocol"], + obfs: "plain", // default obfs + "protocol-param": params["ssr-protocol-param"], + udp: JSON.parse(params["udp-relay"] || "false"), + tfo: JSON.parse(params["fast-open"] || "false"), + supported, + }; + // handle obfs options + if (params.obfs) { + proxy.obfs = params.obfs; + proxy["obfs-param"] = params["obfs-host"]; + } + return proxy; + }; + return {name, test, parse}; + } + + function QX_VMess() { + const name = "QX VMess Parser"; + const test = (line) => { + return /^vmess\s*=/.test(line.split(",")[0].trim()); + }; + const parse = (line) => { + const params = getQXParams(line); + const proxy = { + type: "vmess", + name: params.tag, + server: params.server, + port: params.port, + cipher: params.method || "none", + uuid: params.password, + alterId: 0, + tls: params.obfs === "over-tls" || params.obfs === "wss", + udp: JSON.parse(params["udp-relay"] || "false"), + tfo: JSON.parse(params["fast-open"] || "false"), + }; + if (proxy.tls) { + proxy.sni = params["obfs-host"] || params.server; + proxy['skip-cert-verify'] = !JSON.parse(params["tls-verification"] || "true"); + } + // handle ws headers + if (params.obfs === "ws" || params.obfs === "wss") { + proxy.network = "ws"; + proxy["ws-path"] = params["obfs-uri"]; + proxy["ws-headers"] = { + Host: params["obfs-host"] || params.server, // if no host provided, use the same as server + }; + } + return proxy; + }; + + return {name, test, parse}; + } + + function QX_Trojan() { + const name = "QX Trojan Parser"; + const test = (line) => { + return /^trojan\s*=/.test(line.split(",")[0].trim()); + }; + const parse = (line) => { + const params = getQXParams(line); + const proxy = { + type: "trojan", + name: params.tag, + server: params.server, + port: params.port, + password: params.password, + sni: params["tls-host"] || params.server, + udp: JSON.parse(params["udp-relay"] || "false"), + tfo: JSON.parse(params["fast-open"] || "false"), + }; + proxy['skip-cert-verify'] = !JSON.parse(params["tls-verification"] || "true"); + return proxy; + }; + return {name, test, parse}; + } + + function QX_Http() { + const name = "QX HTTP Parser"; + const test = (line) => { + return /^http\s*=/.test(line.split(",")[0].trim()); + }; + const parse = (line) => { + const params = getQXParams(line); + const proxy = { + type: "http", + name: params.tag, + server: params.server, + port: params.port, + tls: JSON.parse(params["over-tls"] || "false"), + udp: JSON.parse(params["udp-relay"] || "false"), + tfo: JSON.parse(params["fast-open"] || "false"), + }; + if (params.username && params.username !== 'none') proxy.username = params.username; + if (params.password && params.password !== 'none') proxy.password = params.password; + if (proxy.tls) { + proxy.sni = params["tls-host"] || proxy.server; + proxy['skip-cert-verify'] = !JSON.parse(params["tls-verification"] || "true"); + } + return proxy; + }; + + return {name, test, parse}; + } + + function getQXParams(line) { + const groups = line.split(","); + const params = {}; + const protocols = ["shadowsocks", "vmess", "http", "trojan"]; + groups.forEach((g) => { + let [key, value] = g.split("="); + key = key.trim(); + value = value.trim(); + if (protocols.indexOf(key) !== -1) { + params.type = key; + const conf = value.split(":"); + params.server = conf[0]; + params.port = conf[1]; + } else { + params[key.trim()] = value.trim(); + } + }); + return params; + } + + function Loon_SS() { + const name = "Loon SS Parser"; + const test = (line) => { + return ( + line.split(",")[0].split("=")[1].trim().toLowerCase() === "shadowsocks" + ); + }; + const parse = (line) => { + const params = line.split("=")[1].split(","); + const proxy = { + name: line.split("=")[0].trim(), + type: "ss", + server: params[1], + port: params[2], + cipher: params[3], + password: params[4].replace(/"/g, ""), + }; + // handle obfs + if (params.length > 5) { + proxy.plugin = "obfs"; + proxy["plugin-opts"] = { + mode: params[5], + host: params[6], + }; + } + return proxy; + }; + return {name, test, parse}; + } + + function Loon_SSR() { + const name = "Loon SSR Parser"; + const test = (line) => { + return ( + line.split(",")[0].split("=")[1].trim().toLowerCase() === "shadowsocksr" + ); + }; + const parse = (line) => { + const params = line.split("=")[1].split(","); + const supported = { + Surge: false, + }; + return { + name: line.split("=")[0].trim(), + type: "ssr", + server: params[1], + port: params[2], + cipher: params[3], + password: params[4].replace(/"/g, ""), + protocol: params[5], + "protocol-param": params[6].match(/{(.*)}/)[1], + supported, + obfs: params[7], + "obfs-param": params[8].match(/{(.*)}/)[1], + }; + }; + return {name, test, parse}; + } + + function Loon_VMess() { + const name = "Loon VMess Parser"; + const test = (line) => { + // distinguish between surge vmess + return ( + /^.*=\s*vmess/i.test(line.split(",")[0]) && + line.indexOf("username") === -1 + ); + }; + const parse = (line) => { + let params = line.split("=")[1].split(","); + const proxy = { + name: line.split("=")[0].trim(), + type: "vmess", + server: params[1], + port: params[2], + cipher: params[3] || "none", + uuid: params[4].replace(/"/g, ""), + alterId: 0, + }; + // get transport options + params = params.splice(5); + for (const item of params) { + const [key, val] = item.split(":"); + params[key] = val; + } + proxy.tls = JSON.parse(params["over-tls"] || "false"); + if (proxy.tls) { + proxy.sni = params["tls-name"] || proxy.server; + proxy['skip-cert-verify'] = JSON.parse(params["skip-cert-verify"] || "false"); + } + switch (params.transport) { + case "tcp": + break; + case "ws": + proxy.network = params.transport; + proxy["ws-path"] = params.path; + proxy["ws-headers"] = { + Host: params.host, + }; + } + if (proxy.tls) { + proxy['skip-cert-verify'] = JSON.parse(params["skip-cert-verify"] || "false"); + } + return proxy; + }; + return {name, test, parse}; + } + + function Loon_Trojan() { + const name = "Loon Trojan Parser"; + const test = (line) => { + return ( + /^.*=\s*trojan/i.test(line.split(",")[0]) && + line.indexOf("password") === -1 + ); + }; + + const parse = (line) => { + const params = line.split("=")[1].split(","); + const proxy = { + name: line.split("=")[0].trim(), + type: "trojan", + server: params[1], + port: params[2], + password: params[3].replace(/"/g, ""), + sni: params[1], // default sni is the server itself + "skip-cert-verify": JSON.parse(params["skip-cert-verify"] || "false"), + }; + // trojan sni + if (params.length > 4) { + const [key, val] = params[4].split(":"); + if (key === "tls-name") proxy.sni = val; + else throw new Error(`Unknown option ${key} for line: \n${line}`); + } + return proxy; + }; + + return {name, test, parse}; + } + + function Loon_Http() { + const name = "Loon HTTP Parser"; + const test = (line) => { + return ( + /^.*=\s*http/i.test(line.split(",")[0]) && + line.split(",").length === 5 && + line.indexOf("username") === -1 && + line.indexOf("password") === -1 + ); + }; + + const parse = (line) => { + const params = line.split("=")[1].split(","); + const proxy = { + name: line.split("=")[0].trim(), + type: "http", + server: params[1], + port: params[2], + tls: params[2] === "443", // port 443 is considered as https type + }; + if (params[3]) proxy.username = params[3]; + if (params[4]) proxy.password = params[4]; + + if (proxy.tls) { + proxy.sni = params["tls-name"] || proxy.server; + proxy['skip-cert-verify'] = JSON.parse(params["skip-cert-verify"] || "false"); + } + + return proxy; + }; + return {name, test, parse}; + } + + function Surge_SS() { + const name = "Surge SS Parser"; + const test = (line) => { + return /^.*=\s*ss/.test(line.split(",")[0]); + }; + const parse = (line) => { + const params = getSurgeParams(line); + const proxy = { + name: params.name, + type: "ss", + server: params.server, + port: params.port, + cipher: params["encrypt-method"], + password: params.password, + tfo: JSON.parse(params.tfo || "false"), + udp: JSON.parse(params["udp-relay"] || "false"), + }; + // handle obfs + if (params.obfs) { + proxy.plugin = "obfs"; + proxy["plugin-opts"] = { + mode: params.obfs, + host: params["obfs-host"], + }; + } + return proxy; + }; + return {name, test, parse}; + } + + function Surge_VMess() { + const name = "Surge VMess Parser"; + const test = (line) => { + return ( + /^.*=\s*vmess/.test(line.split(",")[0]) && line.indexOf("username") !== -1 + ); + }; + const parse = (line) => { + const params = getSurgeParams(line); + const proxy = { + name: params.name, + type: "vmess", + server: params.server, + port: params.port, + uuid: params.username, + alterId: 0, // surge does not have this field + cipher: "none", // surge does not have this field + tls: JSON.parse(params.tls || "false"), + tfo: JSON.parse(params.tfo || "false"), + }; + if (proxy.tls) { + if (typeof params["skip-cert-verify"] !== "undefined") { + proxy['skip-cert-verify'] = params["skip-cert-verify"] === true || params["skip-cert-verify"] === "1"; + } + proxy.sni = params["sni"] || params.server; + } + // use websocket + if (JSON.parse(params.ws || "false")) { + proxy.network = "ws"; + proxy["ws-path"] = params["ws-path"]; + proxy["ws-headers"] = { + Host: params.sni, + }; + } + return proxy; + }; + return {name, test, parse}; + } + + function Surge_Trojan() { + const name = "Surge Trojan Parser"; + const test = (line) => { + return ( + /^.*=\s*trojan/.test(line.split(",")[0]) && line.indexOf("sni") !== -1 + ); + }; + const parse = (line) => { + const params = getSurgeParams(line); + const proxy = { + name: params.name, + type: "trojan", + server: params.server, + port: params.port, + password: params.password, + sni: params.sni || params.server, + tfo: JSON.parse(params.tfo || "false"), + }; + if (typeof params["skip-cert-verify"] !== "undefined") { + proxy['skip-cert-verify'] = params["skip-cert-verify"] === true || params["skip-cert-verify"] === "1"; + } + return proxy; + }; + + return {name, test, parse}; + } + + function Surge_Http() { + const name = "Surge HTTP Parser"; + const test = (line) => { + return ( + /^.*=\s*http/.test(line.split(",")[0]) && !Loon_Http().test(line) + ); + }; + const parse = (line) => { + const params = getSurgeParams(line); + const proxy = { + name: params.name, + type: "http", + server: params.server, + port: params.port, + tls: JSON.parse(params.tls || "false"), + tfo: JSON.parse(params.tfo || "false"), + }; + if (proxy.tls) { + if (typeof params["skip-cert-verify"] !== "undefined") { + proxy['skip-cert-verify'] = params["skip-cert-verify"] === true || params["skip-cert-verify"] === "1"; + } + proxy.sni = params.sni || params.server; + } + if (params.username && params.username !== "none") proxy.username = params.username; + if (params.password && params.password !== "none") proxy.password = params.password; + return proxy; + }; + return {name, test, parse}; + } + + function getSurgeParams(line) { + const params = {}; + params.name = line.split("=")[0].trim(); + const segments = line.split(","); + params.server = segments[1].trim(); + params.port = segments[2].trim(); + for (let i = 3; i < segments.length; i++) { + const item = segments[i]; + if (item.indexOf("=") !== -1) { + const [key, value] = item.split("="); + params[key.trim()] = value.trim(); + } + } + return params; + } + + return [ + URI_SS(), URI_SSR(), URI_VMess(), URI_Trojan(), + Clash_All(), + Surge_SS(), Surge_VMess(), Surge_Trojan(), Surge_Http(), + Loon_SS(), Loon_SSR(), Loon_VMess(), Loon_Trojan(), Loon_Http(), + QX_SS(), QX_SSR(), QX_VMess(), QX_Trojan(), QX_Http() + ]; + })(); + const PROXY_PROCESSORS = (function () { + // force to set some properties (e.g., skip-cert-verify, udp, tfo, etc.) + function SetPropertyOperator({key, value}) { + return { + name: "Set Property Operator", + func: (proxies) => { + return proxies.map((p) => { + p[key] = value; + return p; + }); + }, + }; + } + + // add or remove flag for proxies + function FlagOperator(add = true) { + return { + name: "Flag Operator", + func: (proxies) => { + return proxies.map((proxy) => { + if (!add) { + // no flag + proxy.name = removeFlag(proxy.name); + } else { + // get flag + const newFlag = getFlag(proxy.name); + // remove old flag + proxy.name = removeFlag(proxy.name); + proxy.name = newFlag + " " + proxy.name; + proxy.name = proxy.name.replace(/🇹🇼/g, "🇨🇳"); + } + return proxy; + }); + }, + }; + } + + // sort proxies according to their names + function SortOperator(order = "asc") { + return { + name: "Sort Operator", + func: (proxies) => { + switch (order) { + case "asc": + case "desc": + return proxies.sort((a, b) => { + let res = a.name > b.name ? 1 : -1; + res *= order === "desc" ? -1 : 1; + return res; + }); + case "random": + return shuffle(proxies); + default: + throw new Error("Unknown sort option: " + order); + } + }, + }; + } + + // sort by keywords + function KeywordSortOperator(keywords) { + return { + name: "Keyword Sort Operator", + func: (proxies) => + proxies.sort((a, b) => { + const oA = getKeywordOrder(keywords, a.name); + const oB = getKeywordOrder(keywords, b.name); + if (oA && !oB) return -1; + if (oB && !oA) return 1; + if (oA && oB) return oA < oB ? -1 : 1; + if ((!oA && !oB) || (oA && oB && oA === oB)) + return a.name < b.name ? -1 : 1; // fallback to normal sort + }), + }; + } + + function getKeywordOrder(keywords, str) { + let order = null; + for (let i = 0; i < keywords.length; i++) { + if (str.indexOf(keywords[i]) !== -1) { + order = i + 1; // plus 1 is important! 0 will be treated as false!!! + break; + } + } + return order; + } + + // rename by keywords + // keywords: [{old: "old", now: "now"}] + function KeywordRenameOperator(keywords) { + return { + name: "Keyword Rename Operator", + func: (proxies) => { + return proxies.map((proxy) => { + for (const {old, now} of keywords) { + proxy.name = proxy.name.replaceAll(old, now).trim(); + } + return proxy; + }); + }, + }; + } + + // rename by regex + // keywords: [{expr: "string format regex", now: "now"}] + function RegexRenameOperator(regex) { + return { + name: "Regex Rename Operator", + func: (proxies) => { + return proxies.map((proxy) => { + for (const {expr, now} of regex) { + proxy.name = proxy.name.replace(new RegExp(expr, "g"), now).trim(); + } + return proxy; + }); + }, + }; + } + + // delete keywords operator + // keywords: ['a', 'b', 'c'] + function KeywordDeleteOperator(keywords) { + const keywords_ = keywords.map((k) => { + return { + old: k, + now: "", + }; + }); + return { + name: "Keyword Delete Operator", + func: KeywordRenameOperator(keywords_).func, + }; + } + + // delete regex operator + // regex: ['a', 'b', 'c'] + function RegexDeleteOperator(regex) { + const regex_ = regex.map((r) => { + return { + expr: r, + now: "", + }; + }); + return { + name: "Regex Delete Operator", + func: RegexRenameOperator(regex_).func, + }; + } + + // use base64 encoded script to rename + /** Example script + function operator(proxies) { + // do something + return proxies; + } + + WARNING: + 1. This function name should be `operator`! + 2. Always declare variables before using them! + */ + function ScriptOperator(script) { + return { + name: "Script Operator", + func: (proxies) => { + let output = proxies; + (function () { + // interface to get internal operators + const $get = (name, args) => { + const item = AVAILABLE_OPERATORS[name]; + return item(args); + }; + const $process = (item, proxies) => { + if (item.name.indexOf("Filter") !== -1) { + return ApplyOperator(item, proxies); + } else if (item.name.indexOf("Operator") !== -1) { + return ApplyFilter(item, proxies); + } + }; + eval(script); + output = operator(proxies); + })(); + return output; + }, + }; + } + + /**************************** Filters ***************************************/ + // filter by keywords + function KeywordFilter({keywords = [], keep = true}) { + return { + name: "Keyword Filter", + func: (proxies) => { + return proxies.map((proxy) => { + const selected = keywords.some((k) => proxy.name.indexOf(k) !== -1); + return keep ? selected : !selected; + }); + }, + }; + } + + // filter useless proxies + function UselessFilter() { + const KEYWORDS = [ + "网址", + "流量", + "时间", + "应急", + "过期", + "Bandwidth", + "expire", + ]; + return { + name: "Useless Filter", + func: KeywordFilter({ + keywords: KEYWORDS, + keep: false, + }).func, + }; + } + + // filter by regions + function RegionFilter(regions) { + const REGION_MAP = { + HK: "🇭🇰", + TW: "🇹🇼", + US: "🇺🇸", + SG: "🇸🇬", + JP: "🇯🇵", + UK: "🇬🇧", + }; + return { + name: "Region Filter", + func: (proxies) => { + // this would be high memory usage + return proxies.map((proxy) => { + const flag = getFlag(proxy.name); + return regions.some((r) => REGION_MAP[r] === flag); + }); + }, + }; + } + + // filter by regex + function RegexFilter({regex = [], keep = true}) { + return { + name: "Regex Filter", + func: (proxies) => { + return proxies.map((proxy) => { + const selected = regex.some((r) => { + r = new RegExp(r); + return r.test(proxy.name); + }); + return keep ? selected : !selected; + }); + }, + }; + } + + // filter by proxy types + function TypeFilter(types) { + return { + name: "Type Filter", + func: (proxies) => { + return proxies.map((proxy) => types.some((t) => proxy.type === t)); + }, + }; + } + + // use base64 encoded script to filter proxies + /** + Script Example + function func(proxies) { + const selected = FULL(proxies.length, true); + // do something + return selected; + } + WARNING: + 1. This function name should be `func`! + 2. Always declare variables before using them! + */ + function ScriptFilter(script) { + return { + name: "Script Filter", + func: (proxies) => { + let output = FULL(proxies.length, true); + !(function () { + eval(script); + output = filter(proxies); + })(); + return output; + }, + }; + } + + /******************************** Utility Functions *********************************************/ + // get proxy flag according to its name + function getFlag(name) { + // flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js + const flags = { + "🇦🇨": ["AC"], + "🇦🇹": ["奥地利", "维也纳"], + "🇦🇺": ["AU", "Australia", "Sydney", "澳大利亚", "澳洲", "墨尔本", "悉尼"], + "🇧🇪": ["BE", "比利时"], + "🇧🇬": ["保加利亚", "Bulgaria"], + "🇧🇷": ["BR", "Brazil", "巴西", "圣保罗"], + "🇨🇦": [ + "CA", + "Canada", + "Waterloo", + "加拿大", + "蒙特利尔", + "温哥华", + "楓葉", + "枫叶", + "滑铁卢", + "多伦多", + ], + "🇨🇭": ["瑞士", "苏黎世", "Switzerland"], + "🇩🇪": ["DE", "German", "GERMAN", "德国", "德國", "法兰克福"], + "🇩🇰": ["丹麦"], + "🇪🇸": ["ES", "西班牙", "Spain"], + "🇪🇺": ["EU", "欧盟", "欧罗巴"], + "🇫🇮": ["Finland", "芬兰", "赫尔辛基"], + "🇫🇷": ["FR", "France", "法国", "法國", "巴黎"], + "🇬🇧": ["UK", "GB", "England", "United Kingdom", "英国", "伦敦", "英"], + "🇲🇴": ["MO", "Macao", "澳门", "CTM"], + "🇭🇺": ["匈牙利", "Hungary"], + "🇭🇰": [ + "HK", + "Hongkong", + "Hong Kong", + "香港", + "深港", + "沪港", + "呼港", + "HKT", + "HKBN", + "HGC", + "WTT", + "CMI", + "穗港", + "京港", + "港", + ], + "🇮🇩": ["Indonesia", "印尼", "印度尼西亚", "雅加达"], + "🇮🇪": ["Ireland", "爱尔兰", "都柏林"], + "🇮🇳": ["India", "印度", "孟买", "Mumbai"], + "🇰🇵": ["KP", "朝鲜"], + "🇰🇷": ["KR", "Korea", "KOR", "韩国", "首尔", "韩", "韓"], + "🇱🇻": ["Latvia", "Latvija", "拉脱维亚"], + "🇲🇽️": ["MEX", "MX", "墨西哥"], + "🇲🇾": ["MY", "Malaysia", "马来西亚", "吉隆坡"], + "🇳🇱": ["NL", "Netherlands", "荷兰", "荷蘭", "尼德蘭", "阿姆斯特丹"], + "🇵🇭": ["PH", "Philippines", "菲律宾"], + "🇷🇴": ["RO", "罗马尼亚"], + "🇷🇺": [ + "RU", + "Russia", + "俄罗斯", + "俄羅斯", + "伯力", + "莫斯科", + "圣彼得堡", + "西伯利亚", + "新西伯利亚", + "京俄", + "杭俄", + ], + "🇸🇦": ["沙特", "迪拜"], + "🇸🇪": ["SE", "Sweden"], + "🇸🇬": [ + "SG", + "Singapore", + "新加坡", + "狮城", + "沪新", + "京新", + "泉新", + "穗新", + "深新", + "杭新", + "广新", + ], + "🇹🇭": ["TH", "Thailand", "泰国", "泰國", "曼谷"], + "🇹🇷": ["TR", "Turkey", "土耳其", "伊斯坦布尔"], + "🇹🇼": [ + "TW", + "Taiwan", + "台湾", + "台北", + "台中", + "新北", + "彰化", + "CHT", + "台", + "HINET", + ], + "🇺🇸": [ + "US", + "USA", + "America", + "United States", + "美国", + "美", + "京美", + "波特兰", + "达拉斯", + "俄勒冈", + "凤凰城", + "费利蒙", + "硅谷", + "矽谷", + "拉斯维加斯", + "洛杉矶", + "圣何塞", + "圣克拉拉", + "西雅图", + "芝加哥", + "沪美", + "哥伦布", + "纽约", + ], + "🇻🇳": ["VN", "越南", "胡志明市"], + "🇮🇹": ["Italy", "IT", "Nachash", "意大利", "米兰", "義大利"], + "🇿🇦": ["South Africa", "南非"], + "🇦🇪": ["United Arab Emirates", "阿联酋"], + "🇯🇵": [ + "JP", + "Japan", + "日", + "日本", + "东京", + "大阪", + "埼玉", + "沪日", + "穗日", + "川日", + "中日", + "泉日", + "杭日", + "深日", + "辽日", + "广日", + ], + "🇦🇷": ["AR", "阿根廷"], + "🇳🇴": ["Norway", "挪威", "NO"], + "🇨🇳": [ + "CN", + "China", + "回国", + "中国", + "江苏", + "北京", + "上海", + "广州", + "深圳", + "杭州", + "徐州", + "青岛", + "宁波", + "镇江", + "back", + ], + "🏳️‍🌈": ["流量", "时间", "应急", "过期", "Bandwidth", "expire"], + }; + for (let k of Object.keys(flags)) { + if (flags[k].some((item) => name.indexOf(item) !== -1)) { + return k; + } + } + // no flag found + const oldFlag = (name.match( + /[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/ + ) || [])[0]; + return oldFlag || "🏴‍☠️"; + } + + // remove flag + function removeFlag(str) { + return str + .replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, "") + .trim(); + } + + // shuffle array + function shuffle(array) { + let currentIndex = array.length, + temporaryValue, + randomIndex; + + // While there remain elements to shuffle... + while (0 !== currentIndex) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + + // And swap it with the current element. + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + + return array; + } + + return { + "Keyword Filter": KeywordFilter, + "Useless Filter": UselessFilter, + "Region Filter": RegionFilter, + "Regex Filter": RegexFilter, + "Type Filter": TypeFilter, + "Script Filter": ScriptFilter, + + "Set Property Operator": SetPropertyOperator, + "Flag Operator": FlagOperator, + "Sort Operator": SortOperator, + "Keyword Sort Operator": KeywordSortOperator, + "Keyword Rename Operator": KeywordRenameOperator, + "Keyword Delete Operator": KeywordDeleteOperator, + "Regex Rename Operator": RegexRenameOperator, + "Regex Delete Operator": RegexDeleteOperator, + "Script Operator": ScriptOperator, + }; + })(); + const PROXY_PRODUCERS = (function () { + function QX_Producer() { + const targetPlatform = "QX"; + const produce = (proxy) => { + let obfs_opts; + let tls_opts; + switch (proxy.type) { + case "ss": + obfs_opts = ""; + if (proxy.plugin === "obfs") { + const {host, mode} = proxy['plugin-opts']; + obfs_opts = `,obfs=${mode}${ + host ? ",obfs-host=" + host : "" + }`; + } + if (proxy.plugin === "v2ray-plugin") { + const {tls, host, path} = proxy["plugin-opts"]; + obfs_opts = `,obfs=${tls ? "wss" : "ws"}${ + host ? ",obfs-host=" + host : "" + }${ + path ? ",obfs-uri=" + path : "" + }`; + } + return `shadowsocks=${proxy.server}:${proxy.port},method=${ + proxy.cipher + },password=${proxy.password}${obfs_opts}${ + proxy.tfo ? ",fast-open=true" : ",fast-open=false" + }${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${ + proxy.name + }`; + case "ssr": + return `shadowsocks=${proxy.server}:${proxy.port},method=${ + proxy.cipher + },password=${proxy.password},ssr-protocol=${proxy.protocol}${ + proxy["protocol-param"] + ? ",ssr-protocol-param=" + proxy["protocol-param"] + : "" + }${proxy.obfs ? ",obfs=" + proxy.obfs : ""}${ + proxy["obfs-param"] ? ",obfs-host=" + proxy["obfs-param"] : "" + }${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${ + proxy.udp ? ",udp-relay=true" : ",udp-relay=false" + },tag=${proxy.name}`; + case "vmess": + obfs_opts = ""; + if (proxy.network === "ws") { + // websocket + if (proxy.tls) { + // ws-tls + obfs_opts = `,obfs=wss${ + proxy.sni ? ",obfs-host=" + proxy.sni : "" + }${ + proxy["ws-path"] ? ",obfs-uri=" + proxy["ws-path"] : "" + },tls-verification=${proxy['skip-cert-verify'] ? "false" : "true"}`; + } else { + // ws + obfs_opts = `,obfs=ws${ + proxy["ws-headers"].Host ? ",obfs-host=" + proxy["ws-headers"].Host : "" + }${ + proxy["ws-path"] ? ",obfs-uri=" + proxy["ws-path"] : "" + }`; + } + } else { + // tcp + if (proxy.tls) { + obfs_opts = `,obfs=over-tls${ + proxy.sni ? ",obfs-host=" + proxy.sni : "" + },tls-verification=${proxy['skip-cert-verify'] ? "false" : "true"}`; + } + } + return `vmess=${proxy.server}:${proxy.port},method=${ + proxy.cipher === "auto" ? "none" : proxy.cipher + },password=${proxy.uuid}${obfs_opts}${ + proxy.tfo ? ",fast-open=true" : ",fast-open=false" + }${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${ + proxy.name + }`; + case "trojan": + return `trojan=${proxy.server}:${proxy.port},password=${ + proxy.password + }${proxy.sni ? ",tls-host=" + proxy.sni : ""},over-tls=true,tls-verification=${ + proxy['skip-cert-verify'] ? "false" : "true" + }${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${ + proxy.udp ? ",udp-relay=true" : ",udp-relay=false" + },tag=${proxy.name}`; + case "http": + tls_opts = ""; + if (proxy.tls) { + tls_opts = `,over-tls=true,tls-verification=${ + proxy['skip-cert-verify'] ? "false" : "true" + }${ + proxy.sni ? ",tls-host=" + proxy.sni : "" + }`; + } + return `http=${proxy.server}:${proxy.port},username=${ + proxy.username + },password=${proxy.password}${tls_opts}${ + proxy.tfo ? ",fast-open=true" : ",fast-open=false" + },tag=${proxy.name}`; + } + throw new Error( + `Platform ${targetPlatform} does not support proxy type: ${proxy.type}` + ); + }; + return {produce}; + } + + function Loon_Producer() { + const targetPlatform = "Loon"; + const produce = (proxy) => { + let obfs_opts, tls_opts; + switch (proxy.type) { + case "ss": + obfs_opts = ",,"; + if (proxy.plugin) { + if (proxy.plugin === "obfs") { + const {mode, host} = proxy["plugin-opts"]; + obfs_opts = `,${mode},${host || ""}`; + } else { + throw new Error( + `Platform ${targetPlatform} does not support obfs option: ${proxy.obfs}` + ); + } + } + + return `${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"${obfs_opts}`; + case "ssr": + return `${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}",${proxy.protocol},{${proxy["protocol-param"] || ""}},${proxy.obfs},{${proxy["obfs-param"] || ""}}`; + case "vmess": + obfs_opts = ""; + if (proxy.network === "ws") { + const host = proxy["ws-headers"].Host || proxy.server; + obfs_opts = `,transport:ws,host:${host},path:${ + proxy["ws-path"] || "/" + }`; + } else { + obfs_opts = `,transport:tcp`; + } + if (proxy.tls) { + obfs_opts += `${ + proxy.sni ? ",tls-name:" + proxy.sni : "" + },skip-cert-verify:${proxy['skip-cert-verify'] || "false"}`; + } + return `${proxy.name}=vmess,${proxy.server},${proxy.port},${ + proxy.cipher === "auto" ? "none" : proxy.cipher + },"${proxy.uuid}",over-tls:${proxy.tls || "false"}${obfs_opts}`; + case "trojan": + return `${proxy.name}=trojan,${proxy.server},${proxy.port},"${ + proxy.password + }"${ + proxy.sni ? ",tls-name:" + proxy.sni : "" + },skip-cert-verify:${ + proxy['skip-cert-verify'] || "false" + }`; + case "http": + tls_opts = ""; + const base = `${proxy.name}=${proxy.tls ? "http" : "https"},${ + proxy.server + },${proxy.port},${proxy.username || ""},${proxy.password || ""}`; + if (proxy.tls) { + // https + tls_opts = `${ + proxy.sni ? ",tls-name:" + proxy.sni : "" + },skip-cert-verify:${proxy['skip-cert-verify']}`; + return base + tls_opts; + } else return base; + } + throw new Error( + `Platform ${targetPlatform} does not support proxy type: ${proxy.type}` + ); + }; + return {produce}; + } + + function Surge_Producer() { + const targetPlatform = "Surge"; + const produce = (proxy) => { + let obfs_opts, tls_opts; + switch (proxy.type) { + case "ss": + obfs_opts = ""; + if (proxy.plugin) { + const {host, mode} = proxy['plugin-opts']; + if (proxy.plugin === "obfs") { + obfs_opts = `,obfs=${mode}${ + host ? ",obfs-host=" + host : "" + }`; + } else { + throw new Error( + `Platform ${targetPlatform} does not support obfs option: ${proxy.obfs}` + ); + } + } + return `${proxy.name}=ss,${proxy.server}, ${proxy.port},encrypt-method=${ + proxy.cipher + },password=${proxy.password}${obfs_opts},tfo=${ + proxy.tfo || "false" + },udp-relay=${proxy.udp || "false"}`; + case "vmess": + tls_opts = ""; + let config = `${proxy.name}=vmess,${proxy.server},${ + proxy.port + },username=${proxy.uuid},tls=${proxy.tls || "false"},tfo=${proxy.tfo || "false"}`; + if (proxy.network === "ws") { + const path = proxy["ws-path"] || "/"; + const host = proxy["ws-headers"].Host; + config += `,ws=true${path ? ",ws-path=" + path : ""}${ + host ? ",ws-headers=HOST:" + host : "" + }`; + } + if (proxy.tls) { + config += `${ + typeof proxy['skip-cert-verify'] !== "undefined" + ? ",skip-cert-verify=" + proxy['skip-cert-verify'] + : "" + }`; + config += proxy.sni ? `,sni=${proxy.sni}` : ""; + } + return config; + case "trojan": + return `${proxy.name}=trojan,${proxy.server},${proxy.port},password=${ + proxy.password + }${ + typeof proxy['skip-cert-verify'] !== "undefined" + ? ",skip-cert-verify=" + proxy['skip-cert-verify'] + : "" + }${proxy.sni ? ",sni=" + proxy.sni : ""},tfo=${proxy.tfo || "false"}`; + case "http": + tls_opts = ", tls=false"; + if (proxy.tls) { + tls_opts = `,tls=true,skip-cert-verify=${proxy['skip-cert-verify']},sni=${proxy.sni}`; + } + return `${proxy.name}=http, ${proxy.server}, ${proxy.port}${ + proxy.username ? ",username=" + proxy.username : "" + }${ + proxy.password ? ",password=" + proxy.password : "" + }${tls_opts},tfo=${proxy.tfo || "false"}`; + } + throw new Error( + `Platform ${targetPlatform} does not support proxy type: ${proxy.type}` + ); + }; + return {produce}; + } + + function Clash_Producer() { + const type = "ALL"; + const produce = (proxies) => { + return "proxies:\n" + proxies.map(proxy => { + delete proxy.supported; + return " - " + JSON.stringify(proxy) + "\n"; + }).join(""); + }; + return {type, produce}; + } + + function URI_Producer() { + const type = "SINGLE"; + const produce = (proxy) => { + let result = ""; + switch (proxy.type) { + case "ss": + const userinfo = `${proxy.cipher}:${proxy.password}`; + result = `ss://${Base64.safeEncode(userinfo)}@${proxy.server}:${ + proxy.port + }/`; + if (proxy.plugin) { + result += "?plugin="; + const opts = proxy["plugin-opts"]; + switch (proxy.plugin) { + case "obfs": + result += encodeURIComponent( + `simple-obfs;obfs=${opts.mode}${ + opts.host ? ";obfs-host=" + opts.host : "" + }` + ); + break; + case "v2ray-plugin": + result += encodeURIComponent( + `v2ray-plugin;obfs=${opts.mode}${ + opts.host ? ";obfs-host" + opts.host : "" + }${opts.tls ? ";tls" : ""}` + ); + break; + default: + throw new Error(`Unsupported plugin option: ${proxy.plugin}`); + } + } + result += `#${encodeURIComponent(proxy.name)}`; + break; + case "ssr": + result = `${proxy.server}:${proxy.port}:${proxy.protocol}:${ + proxy.cipher + }:${proxy.obfs}:${Base64.safeEncode(proxy.password)}/`; + result += `?remarks=${Base64.safeEncode(proxy.name)}${ + proxy["obfs-param"] + ? "&obfsparam=" + Base64.safeEncode(proxy["obfs-param"]) + : "" + }${ + proxy["protocol-param"] + ? "&protocolparam=" + Base64.safeEncode(proxy["protocol-param"]) + : "" + }`; + result = "ssr://" + Base64.safeEncode(result); + break; + case "vmess": + // V2RayN URI format + result = { + ps: proxy.name, + add: proxy.server, + port: proxy.port, + id: proxy.uuid, + type: "", + aid: 0, + net: proxy.network || "tcp", + tls: proxy.tls ? "tls" : "", + }; + // obfs + if (proxy.network === "ws") { + result.path = proxy["ws-path"] || "/"; + result.host = proxy["ws-headers"].Host || proxy.server; + } + result = "vmess://" + Base64.safeEncode(JSON.stringify(result)); + break; + case "trojan": + result = `trojan://${proxy.password}@${proxy.server}:${proxy.port}#${encodeURIComponent(proxy.name)}`; + break; + default: + throw new Error(`Cannot handle proxy type: ${proxy.type}`); + } + return result; + } + return {type, produce}; + } + + function JSON_Producer() { + const type = "ALL"; + const produce = proxies => JSON.stringify(proxies, null, 2); + return {type, produce}; + } + + + return { + "QX": QX_Producer(), + "Surge": Surge_Producer(), + "Loon": Loon_Producer(), + "Clash": Clash_Producer(), + "URI": URI_Producer(), + "JSON": JSON_Producer() + } + })(); + + function preprocess(raw) { + for (const processor of PROXY_PREPROCESSORS) { + try { + if (processor.test(raw)) { + $.log(`Pre-processor [${processor.name}] activated`); + return processor.parse(raw); + } + } catch (e) { + $.error(`Parser [${processor.name}] failed\n Reason: ${e}`); + } + } + return raw; + } + + function safeMatch(p, line) { + let patternMatched; + try { + patternMatched = p.test(line); + } catch (err) { + patternMatched = false; + } + return patternMatched; + } + + function parse(raw) { + raw = preprocess(raw); + // parse + const lines = raw.split("\n"); + const proxies = []; + let lastParser; + + for (let line of lines) { + line = line.trim(); + if (line.length === 0) continue; // skip empty line + let matched = lastParser && safeMatch(lastParser, line); + if (!matched) { + for (const parser of PROXY_PARSERS) { + if (safeMatch(parser, line)) { + lastParser = parser; + matched = true; + $.log(`Proxy parser: ${parser.name} is activated`); + break; + } + } + } + if (!matched) { + $.error(`Failed to find a rule to parse line: \n${line}\n`); + } else { + try { + const proxy = lastParser.parse(line); + if (!proxy) { + $.error(`Parser ${lastParser.name} return nothing for \n${line}\n`); + } + proxies.push(proxy); + } catch (err) { + $.error( + `Failed to parse line: \n ${line}\n Reason: ${err.stack}` + ); + } + } + } + + return proxies; + } + + async function process(proxies, operators = []) { + for (const item of operators) { + // process script + let script; + if (item.type.indexOf("Script") !== -1) { + const {mode, content} = item.args; + if (mode === "link") { + // if this is remote script, download it + script = await $.http + .get(content) + .then((resp) => resp.body) + .catch((err) => { + throw new Error( + `Error when downloading remote script: ${item.args.content}.\n Reason: ${err}` + ); + }); + } else { + script = content; + } + } + + if (!PROXY_PROCESSORS[item.type]) { + $.error(`Unknown operator: "${item.type}"`); + continue; + } + + $.log( + `Applying "${item.type}" with arguments:\n >>> ${ + JSON.stringify(item.args, null, 2) || "None" + }` + ); + let processor; + if (item.type.indexOf('Script') !== -1) { + processor = PROXY_PROCESSORS[item.type](script); + } else { + processor = PROXY_PROCESSORS[item.type](item.args); + } + proxies = ApplyProcessor(processor, proxies); + } + return proxies; + } + + function produce(proxies, targetPlatform) { + const producer = PROXY_PRODUCERS[targetPlatform]; + if (!producer) { + throw new Error(`Target platform: ${targetPlatform} is not supported!`); + } + + // filter unsupported proxies + proxies = proxies.filter(proxy => !(proxy.supported && proxy.supported[targetPlatform] === false)); + + $.log(`Producing proxies for target: ${targetPlatform}`); + if (typeof producer.type === "undefined" || producer.type === 'SINGLE') { + return proxies + .map(proxy => { + try { + return producer.produce(proxy); + } catch (err) { + $.error( + `Cannot produce proxy: ${JSON.stringify( + proxy, null, 2 + )}\nReason: ${err}` + ); + return ""; + } + }) + .filter(line => line.length > 0) + .join("\n"); + } else if (producer.type === "ALL") { + return producer.produce(proxies); + } + } + + return { + parse, process, produce + } +})(); + +/****************************************** Rule Utils **********************************************************/ +var RuleUtils = (function () { + const RULE_PARSERS = (function () { + // Rule set format for Surge + function SurgeRuleSet() { + const name = "Surge Rule Set Parser" + + const SURGE_RULE_TYPES = [ + // Domain-based rules + "DOMAIN", "DOMAIN-SUFFIX", "DOMAIN-KEYWORD", + // IP based rules + "IP-CIDR", "IP-CIDR6", + // HTTP rules + "USER-AGENT", "URL-REGEX", + // Misc rules + "DEST-PORT", "SRC-IP", "IN-PORT", "PROTOCOL" + ]; + + const test = (raw) => ( + raw.indexOf("payload:") !== 0 && + SURGE_RULE_TYPES.some(k => raw.indexOf(k) !== -1) + ); + + const parse = (raw) => { + const lines = raw.split("\n"); + const result = []; + for (let line of lines) { + line = line.trim(); + // skip comments + if (/\s*#/.test(line)) continue; + if (!SURGE_RULE_TYPES.some(k => line.indexOf(k) === 0)) continue; + try { + const params = line.split(",").map(w => w.trim()); + const rule = { + type: params[0], + content: params[1], + }; + if (rule.type === "IP-CIDR" || rule.type === "IP-CIDR6") { + rule.options = params.slice(2) + } + result.push(rule); + } catch (e) { + console.error(`Failed to parse line: ${line}\n Reason: ${e}`); + } + } + return result; + }; + + return {name, test, parse}; + } + + function ClashRuleProvider() { + const name = "Clash Rule Provider"; + const CLASH_RULE_TYPES = [ + // Domain-based rules + "DOMAIN", "DOMAIN-SUFFIX", "DOMAIN-KEYWORD", + // IP based rules + "IP-CIDR", "IP-CIDR6", + // HTTP rules + "USER-AGENT", "URL-REGEX", + // Process rules + "PROCESS-NAME", + // Misc rules + "DST-PORT", "SRC-IP-CIDR", "SRC-PORT" + ]; + + const test = (raw) => ( + raw.indexOf("payload:") === 0 && + CLASH_RULE_TYPES.some(k => raw.indexOf(k) !== -1) + ); + const parse = (raw) => { + const result = []; + try { + const conf = YAML.eval(raw); + const payload = conf["payload"] + .map( + rule => { + if (typeof rule === "string") { + return rule.replace(/DST-PORT/i, "DEST-PORT") + .replace(/SRC-IP-CIDR/i, "SRC-IP") + .replace(/SRC-PORT/i, "IN-PORT") + } else { + return ""; + } + } + ) + .filter(line => line.length > 0) + .join("\n"); + return SurgeRuleSet().parse(payload); + } catch (e) { + console.error(`Cannot parse rules: ${e}`); + } + return result; + }; + return {name, test, parse}; + } + + function QX() { + const name = "QX Filter"; + const QX_RULE_TYPES = [ + "host", "host-suffix", "host-keyword", + "ip-cidr", "ip6-cidr", + "user-agent" + ]; + const test = (raw) => ( + QX_RULE_TYPES.some(k => raw.indexOf(k.toLowerCase()) === 0) + ) + const parse = (raw) => { + const lines = raw.split("\n"); + for (let i = 0; i < lines.length; i++) { + lines[i] = lines[i] + .replace(/host-suffix/i, "DOMAIN-SUFFIX") + .replace(/host-keyword/i, "DOMAIN-KEYWORD") + .replace(/host/i, "DOMAIN") + .replace("ip-cidr", "IP-CIDR") + .replace(/ip6-cidr/i, "IP-CIDR6") + .replace("user-agent", "USER-AGENT"); + } + return SurgeRuleSet().parse(lines.join("\n")); + }; + return {name, test, parse}; + } + + return [SurgeRuleSet(), QX(), ClashRuleProvider()]; + })(); + const RULE_PROCESSORS = (function () { + function RegexFilter({regex = [], keep = true}) { + return { + name: "Regex Filter", + func: (rules) => { + return rules.map((rule) => { + const selected = regex.some((r) => { + r = new RegExp(r); + return r.test(rule); + }); + return keep ? selected : !selected; + }); + }, + }; + } + + function TypeFilter(types) { + return { + name: "Type Filter", + func: (rules) => { + return rules.map((rule) => types.some((t) => rule.type === t)); + }, + }; + } + + function RemoveDuplicateFilter() { + return { + name: "Remove Duplicate Filter", + func: rules => { + const seen = new Set(); + const result = []; + rules.forEach(rule => { + const options = rule.options || []; + options.sort(); + const key = `${rule.type},${rule.content},${JSON.stringify(options)}`; + if (!seen.has(key)) { + result.push(rule) + seen.add(key); + } + }); + return result; + } + } + } + + // regex: [{expr: "string format regex", now: "now"}] + function RegexReplaceOperator(regex) { + return { + name: "Regex Rename Operator", + func: (rules) => { + return rules.map((rule) => { + for (const {expr, now} of regex) { + rule.content = rule.content.replace(new RegExp(expr, "g"), now).trim(); + } + return rule; + }); + }, + }; + } + + return { + "Regex Filter": RegexFilter, + "Remove Duplicate Filter": RemoveDuplicateFilter, + "Type Filter": TypeFilter, + + "Regex Replace Operator": RegexReplaceOperator + }; + })(); + const RULE_PRODUCERS = (function () { + function QXFilter() { + const type = "SINGLE"; + const func = (rule) => { + // skip unsupported rules + const UNSUPPORTED = [ + "URL-REGEX", "DEST-PORT", "SRC-IP", "IN-PORT", "PROTOCOL" + ]; + if (UNSUPPORTED.indexOf(rule.type) !== -1) return null; + + const TRANSFORM = { + "DOMAIN-KEYWORD": "HOST-KEYWORD", + "DOMAIN-SUFFIX": "HOST-SUFFIX", + "DOMAIN": "HOST", + "IP-CIDR6": "IP6-CIDR" + }; + + // QX does not support the no-resolve option + return `${TRANSFORM[rule.type] || rule.type},${rule.content},SUB-STORE`; + } + return {type, func}; + } + + function SurgeRuleSet() { + const type = "SINGLE"; + const func = (rule) => { + let output = `${rule.type},${rule.content}`; + if (rule.type === "IP-CIDR" || rule.type === "IP-CIDR6") { + output += rule.options ? `,${rule.options[0]}` : ""; + } + return output; + } + return {type, func}; + } + + function LoonRules() { + const type = "SINGLE"; + const func = (rule) => { + // skip unsupported rules + const UNSUPPORTED = [ + "DEST-PORT", "SRC-IP", "IN-PORT", "PROTOCOL" + ]; + if (UNSUPPORTED.indexOf(rule.type) !== -1) return null; + return SurgeRuleSet().func(rule); + } + return {type, func}; + } + + function ClashRuleProvider() { + const type = "ALL"; + const func = (rules) => { + const TRANSFORM = { + "DEST-PORT": "DST-PORT", + "SRC-IP": "SRC-IP-CIDR", + "IN-PORT": "SRC-PORT" + }; + const conf = { + payload: rules.map(rule => { + let output = `${TRANSFORM[rule.type] || rule.type},${rule.content}`; + if (rule.type === "IP-CIDR" || rule.type === "IP-CIDR6") { + output += rule.options ? `,${rule.options[0]}` : ""; + } + return output; + }) + } + return YAML.stringify(conf); + } + return {type, func}; + } + + return { + "QX": QXFilter(), + "Surge": SurgeRuleSet(), + "Loon": LoonRules(), + "Clash": ClashRuleProvider() + }; + })(); + + function parse(raw) { + for (const parser of RULE_PARSERS) { + let matched; + try { + matched = parser.test(raw); + } catch { + matched = false; + } + if (matched) { + console.log(`Rule parser [${parser.name}] is activated!`); + return parser.parse(raw); + } + } + } + + async function process(rules, operators) { + for (const item of operators) { + if (!RULE_PROCESSORS[item.type]) { + console.error(`Unknown operator: ${item.type}!`); + continue; + } + const processor = RULE_PROCESSORS[item.type](item.args); + console.log( + `Applying "${item.type}" with arguments: \n >>> ${ + JSON.stringify(item.args) || "None" + }` + ); + rules = ApplyProcessor(processor, rules); + } + return rules; + } + + function produce(rules, targetPlatform) { + const producer = RULE_PRODUCERS[targetPlatform]; + if (!producer) { + throw new Error(`Target platform: ${targetPlatform} is not supported!`); + } + if (typeof producer.type === "undefined" || producer.type === 'SINGLE') { + return rules + .map(rule => { + try { + return producer.func(rule); + } catch (err) { + console.log( + `ERROR: cannot produce rule: ${JSON.stringify( + rule + )}\nReason: ${err}` + ); + return ""; + } + }) + .filter(line => line.length > 0) + .join("\n"); + } else if (producer.type === "ALL") { + return producer.func(rules); + } + } + + return {parse, process, produce}; +})(); + +parseResource(); +/****************************************** Supporting Functions ********************************************** */ +function ApplyProcessor(process, objs) { + function ApplyFilter(filter, objs) { + // select proxies + let selected = FULL(objs.length, true); + try { + selected = AND(selected, filter.func(objs)); + } catch (err) { + // print log and skip this filter + console.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`); + } + return objs.filter((_, i) => selected[i]); + } + + function ApplyOperator(operator, objs) { + let output = clone(objs); + try { + const output_ = operator.func(output); + if (output_) output = output_; + } catch (err) { + // print log and skip this operator + console.log(`Cannot apply operator ${operator.name}! Reason: ${err}`); + } + return output; + } + + if (process.name.indexOf("Filter") !== -1) { + return ApplyFilter(process, objs); + } else if (process.name.indexOf("Operator") !== -1) { + return ApplyOperator(process, objs); + } + +} + +// some logical functions +function AND(...args) { + return args.reduce((a, b) => a.map((c, i) => b[i] && c)); +} + +function OR(...args) { + return args.reduce((a, b) => a.map((c, i) => b[i] || c)); +} + +function NOT(array) { + return array.map((c) => !c); +} + +function FULL(length, bool) { + return [...Array(length).keys()].map(() => bool); +} + +function clone(object) { + return JSON.parse(JSON.stringify(object)); +} + +/****************************************** Own Libraries *******************************************************/ + + +/** + * OpenAPI + * https://github.com/Peng-YM/QuanX/blob/master/Tools/OpenAPI/README.md + */ +function ENV() { + const isQX = typeof $task !== "undefined"; + const isLoon = typeof $loon !== "undefined"; + const isSurge = typeof $httpClient !== "undefined" && !isLoon; + const isJSBox = typeof require == "function" && typeof $jsbox != "undefined"; + const isNode = typeof require == "function" && !isJSBox; + const isRequest = typeof $request !== "undefined"; + const isScriptable = typeof importModule !== "undefined"; + return {isQX, isLoon, isSurge, isNode, isJSBox, isRequest, isScriptable}; +} + +function HTTP(defaultOptions = {baseURL: ""}) { + const {isQX, isLoon, isSurge, isScriptable, isNode} = ENV(); + const methods = ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"]; + const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/ + + function send(method, options) { + options = typeof options === "string" ? {url: options} : options; + const baseURL = defaultOptions.baseURL; + if (baseURL && !URL_REGEX.test(options.url || "")) { + options.url = baseURL ? baseURL + options.url : options.url; + } + options = {...defaultOptions, ...options}; + const timeout = options.timeout; + const events = { + ...{ + onRequest: () => { + }, + onResponse: (resp) => resp, + onTimeout: () => { + }, + }, + ...options.events, + }; + + events.onRequest(method, options); + + let worker; + if (isQX) { + worker = $task.fetch({method, ...options}); + } else if (isLoon || isSurge || isNode) { + worker = new Promise((resolve, reject) => { + const request = isNode ? require("request") : $httpClient; + request[method.toLowerCase()](options, (err, response, body) => { + if (err) reject(err); + else + resolve({ + statusCode: response.status || response.statusCode, + headers: response.headers, + body, + }); + }); + }); + } else if (isScriptable) { + const request = new Request(options.url); + request.method = method; + request.headers = options.headers; + request.body = options.body; + worker = new Promise((resolve, reject) => { + request + .loadString() + .then((body) => { + resolve({ + statusCode: request.response.statusCode, + headers: request.response.headers, + body, + }); + }) + .catch((err) => reject(err)); + }); + } + + let timeoutid; + const timer = timeout + ? new Promise((_, reject) => { + timeoutid = setTimeout(() => { + events.onTimeout(); + return reject( + `${method} URL: ${options.url} exceeds the timeout ${timeout} ms` + ); + }, timeout); + }) + : null; + + return (timer + ? Promise.race([timer, worker]).then((res) => { + clearTimeout(timeoutid); + return res; + }) + : worker + ).then((resp) => events.onResponse(resp)); + } + + const http = {}; + methods.forEach( + (method) => + (http[method.toLowerCase()] = (options) => send(method, options)) + ); + return http; +} + +function API(name = "untitled", debug = false) { + const {isQX, isLoon, isSurge, isNode, isJSBox, isScriptable} = ENV(); + return new (class { + constructor(name, debug) { + this.name = name; + this.debug = debug; + + this.http = HTTP(); + this.env = ENV(); + + this.node = (() => { + if (isNode) { + const fs = require("fs"); + + return { + fs, + }; + } else { + return null; + } + })(); + this.initCache(); + + const delay = (t, v) => + new Promise(function (resolve) { + setTimeout(resolve.bind(null, v), t); + }); + + Promise.prototype.delay = function (t) { + return this.then(function (v) { + return delay(t, v); + }); + }; + } + + // persistence + // initialize cache + initCache() { + if (isQX) this.cache = JSON.parse($prefs.valueForKey(this.name) || "{}"); + if (isLoon || isSurge) + this.cache = JSON.parse($persistentStore.read(this.name) || "{}"); + + if (isNode) { + // create a json for root cache + let fpath = "root.json"; + if (!this.node.fs.existsSync(fpath)) { + this.node.fs.writeFileSync( + fpath, + JSON.stringify({}), + {flag: "wx"}, + (err) => console.log(err) + ); + } + this.root = {}; + + // create a json file with the given name if not exists + fpath = `${this.name}.json`; + if (!this.node.fs.existsSync(fpath)) { + this.node.fs.writeFileSync( + fpath, + JSON.stringify({}), + {flag: "wx"}, + (err) => console.log(err) + ); + this.cache = {}; + } else { + this.cache = JSON.parse( + this.node.fs.readFileSync(`${this.name}.json`) + ); + } + } + } + + // store cache + persistCache() { + const data = JSON.stringify(this.cache, null, 2); + if (isQX) $prefs.setValueForKey(data, this.name); + if (isLoon || isSurge) $persistentStore.write(data, this.name); + if (isNode) { + this.node.fs.writeFileSync( + `${this.name}.json`, + data, + {flag: "w"}, + (err) => console.log(err) + ); + this.node.fs.writeFileSync( + "root.json", + JSON.stringify(this.root, null, 2), + {flag: "w"}, + (err) => console.log(err) + ); + } + } + + write(data, key) { + this.log(`SET ${key}`); + if (key.indexOf("#") !== -1) { + key = key.substr(1); + if (isSurge || isLoon) { + return $persistentStore.write(data, key); + } + if (isQX) { + return $prefs.setValueForKey(data, key); + } + if (isNode) { + this.root[key] = data; + } + } else { + this.cache[key] = data; + } + this.persistCache(); + } + + read(key) { + this.log(`READ ${key}`); + if (key.indexOf("#") !== -1) { + key = key.substr(1); + if (isSurge || isLoon) { + return $persistentStore.read(key); + } + if (isQX) { + return $prefs.valueForKey(key); + } + if (isNode) { + return this.root[key]; + } + } else { + return this.cache[key]; + } + } + + delete(key) { + this.log(`DELETE ${key}`); + if (key.indexOf("#") !== -1) { + key = key.substr(1); + if (isSurge || isLoon) { + return $persistentStore.write(null, key); + } + if (isQX) { + return $prefs.removeValueForKey(key); + } + if (isNode) { + delete this.root[key]; + } + } else { + delete this.cache[key]; + } + this.persistCache(); + } + + // notification + notify(title, subtitle = "", content = "", options = {}) { + const openURL = options["open-url"]; + const mediaURL = options["media-url"]; + + if (isQX) $notify(title, subtitle, content, options); + if (isSurge) { + $notification.post( + title, + subtitle, + content + `${mediaURL ? "\n多媒体:" + mediaURL : ""}`, + { + url: openURL, + } + ); + } + if (isLoon) { + let opts = {}; + if (openURL) opts["openUrl"] = openURL; + if (mediaURL) opts["mediaUrl"] = mediaURL; + if (JSON.stringify(opts) === "{}") { + $notification.post(title, subtitle, content); + } else { + $notification.post(title, subtitle, content, opts); + } + } + if (isNode || isScriptable) { + const content_ = + content + + (openURL ? `\n点击跳转: ${openURL}` : "") + + (mediaURL ? `\n多媒体: ${mediaURL}` : ""); + if (isJSBox) { + const push = require("push"); + push.schedule({ + title: title, + body: (subtitle ? subtitle + "\n" : "") + content_, + }); + } else { + console.log(`${title}\n${subtitle}\n${content_}\n\n`); + } + } + } + + // other helper functions + log(msg) { + if (this.debug) console.log(`[${this.name}] LOG: ${msg}`); + } + + info(msg) { + console.log(`[${this.name}] INFO: ${msg}`); + } + + error(msg) { + console.log(`[${this.name}] ERROR: ${msg}`); + } + + wait(millisec) { + return new Promise((resolve) => setTimeout(resolve, millisec)); + } + + done(value = {}) { + if (isQX || isLoon || isSurge) { + $done(value); + } else if (isNode && !isJSBox) { + if (typeof $context !== "undefined") { + $context.headers = value.headers; + $context.statusCode = value.statusCode; + $context.body = value.body; + } + } + } + })(name, debug); +} + +/** + * Gist backup + */ +function Gist(backupKey, token) { + const FILE_NAME = "Sub-Store"; + const http = HTTP({ + baseURL: "https://api.github.com", + headers: { + Authorization: `token ${token}`, + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36", + }, + events: { + onResponse: (resp) => { + if (/^[45]/.test(String(resp.statusCode))) { + return Promise.reject(`ERROR: ${JSON.parse(resp.body).message}`); + } else { + return resp; + } + }, + }, + }); + + async function locate() { + return http.get("/gists").then((response) => { + const gists = JSON.parse(response.body); + for (let g of gists) { + if (g.description === backupKey) { + return g.id; + } + } + return -1; + }); + } + + this.upload = async function (content) { + const id = await locate(); + const files = { + [FILE_NAME]: {content} + }; + + if (id === -1) { + // create a new gist for backup + return http.post({ + url: "/gists", + body: JSON.stringify({ + description: backupKey, + public: false, + files + }) + }); + } else { + // update an existing gist + return http.patch({ + url: `/gists/${id}`, + body: JSON.stringify({files}) + }); + } + }; + + this.download = async function () { + const id = await locate(); + if (id === -1) { + return Promise.reject("未找到Gist备份!"); + } else { + try { + const {files} = await http + .get(`/gists/${id}`) + .then(resp => JSON.parse(resp.body)); + const url = files[FILE_NAME].raw_url; + return await http.get(url).then(resp => resp.body); + } catch (err) { + return Promise.reject(err); + } + } + }; +} + +/** + * Mini Express Framework + * https://github.com/Peng-YM/QuanX/blob/master/Tools/OpenAPI/Express.js + */ +function express({port} = {port: 3000}) { + const {isNode} = ENV(); + const DEFAULT_HEADERS = { + "Content-Type": "text/plain;charset=UTF-8", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST,GET,OPTIONS,PATCH,PUT,DELETE", + "Access-Control-Allow-Headers": + "Origin, X-Requested-With, Content-Type, Accept", + }; + + // node support + if (isNode) { + const express_ = require("express"); + const bodyParser = require("body-parser"); + const app = express_(); + app.use(bodyParser.json({verify: rawBodySaver})); + app.use(bodyParser.urlencoded({verify: rawBodySaver, extended: true})); + app.use(bodyParser.raw({verify: rawBodySaver, type: "*/*"})); + app.use((req, res, next) => { + res.set(DEFAULT_HEADERS); + next(); + }); + + // adapter + app.start = () => { + app.listen(port, () => { + $.info(`Express started on port: ${port}`); + }); + }; + return app; + } + + // route handlers + const handlers = []; + + // http methods + const METHODS_NAMES = [ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "OPTIONS", + "HEAD'", + "ALL", + ]; + + // dispatch url to route + const dispatch = (request, start = 0) => { + let {method, url, headers, body} = request; + if (/json/i.test(headers["Content-Type"])) { + body = JSON.parse(body); + } + + method = method.toUpperCase(); + const {path, query} = extractURL(url); + + // pattern match + let handler = null; + let i; + let longestMatchedPattern = 0; + for (i = start; i < handlers.length; i++) { + if (handlers[i].method === "ALL" || method === handlers[i].method) { + const {pattern} = handlers[i]; + if (patternMatched(pattern, path)) { + if (pattern.split("/").length > longestMatchedPattern) { + handler = handlers[i]; + longestMatchedPattern = pattern.split("/").length; + } + } + } + } + if (handler) { + // dispatch to next handler + const next = () => { + dispatch(method, url, i); + }; + const req = { + method, + url, + path, + query, + params: extractPathParams(handler.pattern, path), + headers, + body, + }; + const res = Response(); + const cb = handler.callback; + + const errFunc = err => { + res.status(500).json({ + status: "failed", + message: `Internal Server Error: ${err}`, + }); + } + + if (cb.constructor.name === 'AsyncFunction') { + cb(req, res, next).catch(errFunc); + } else { + try { + cb(req, res, next); + } catch (err) { + errFunc(err); + } + } + } else { + // no route, return 404 + const res = Response(); + res.status(404).json({ + status: "failed", + message: "ERROR: 404 not found", + }); + } + }; + + const app = {}; + + // attach http methods + METHODS_NAMES.forEach((method) => { + app[method.toLowerCase()] = (pattern, callback) => { + // add handler + handlers.push({method, pattern, callback}); + }; + }); + + // chainable route + app.route = (pattern) => { + const chainApp = {}; + METHODS_NAMES.forEach((method) => { + chainApp[method.toLowerCase()] = (callback) => { + // add handler + handlers.push({method, pattern, callback}); + return chainApp; + }; + }); + return chainApp; + }; + + // start service + app.start = () => { + dispatch($request); + }; + + return app; + + /************************************************ + Utility Functions + *************************************************/ + function rawBodySaver(req, res, buf, encoding) { + if (buf && buf.length) { + req.rawBody = buf.toString(encoding || "utf8"); + } + } + + function Response() { + let statusCode = 200; + const {isQX, isLoon, isSurge} = ENV(); + const headers = DEFAULT_HEADERS; + const STATUS_CODE_MAP = { + 200: "HTTP/1.1 200 OK", + 201: "HTTP/1.1 201 Created", + 302: "HTTP/1.1 302 Found", + 307: "HTTP/1.1 307 Temporary Redirect", + 308: "HTTP/1.1 308 Permanent Redirect", + 404: "HTTP/1.1 404 Not Found", + 500: "HTTP/1.1 500 Internal Server Error", + }; + return new (class { + status(code) { + statusCode = code; + return this; + } + + send(body = "") { + const response = { + status: isQX ? STATUS_CODE_MAP[statusCode] : statusCode, + body, + headers, + }; + if (isQX) { + $done(response); + } else if (isLoon || isSurge) { + $done({ + response, + }); + } + } + + end() { + this.send(); + } + + html(data) { + this.set("Content-Type", "text/html;charset=UTF-8"); + this.send(data); + } + + json(data) { + this.set("Content-Type", "application/json;charset=UTF-8"); + this.send(JSON.stringify(data)); + } + + set(key, val) { + headers[key] = val; + return this; + } + })(); + } + + function patternMatched(pattern, path) { + if (pattern instanceof RegExp && pattern.test(path)) { + return true; + } else { + // root pattern, match all + if (pattern === "/") return true; + // normal string pattern + if (pattern.indexOf(":") === -1) { + const spath = path.split("/"); + const spattern = pattern.split("/"); + for (let i = 0; i < spattern.length; i++) { + if (spath[i] !== spattern[i]) { + return false; + } + } + return true; + } + // string pattern with path parameters + else if (extractPathParams(pattern, path)) { + return true; + } + } + return false; + } + + function extractURL(url) { + // extract path + const match = url.match(/https?:\/\/[^\/]+(\/[^?]*)/) || []; + const path = match[1] || "/"; + + // extract query string + const split = url.indexOf("?"); + const query = {}; + if (split !== -1) { + let hashes = url.slice(url.indexOf("?") + 1).split("&"); + for (let i = 0; i < hashes.length; i++) { + hash = hashes[i].split("="); + query[hash[0]] = hash[1]; + } + } + return { + path, + query, + }; + } + + function extractPathParams(pattern, path) { + if (pattern.indexOf(":") === -1) { + return null; + } else { + const params = {}; + for (let i = 0, j = 0; i < pattern.length; i++, j++) { + if (pattern[i] === ":") { + let key = []; + let val = []; + while (pattern[++i] !== "/" && i < pattern.length) { + key.push(pattern[i]); + } + while (path[j] !== "/" && j < path.length) { + val.push(path[j++]); + } + params[key.join("")] = val.join(""); + } else { + if (pattern[i] !== path[j]) { + return null; + } + } + } + return params; + } + } +} + +/****************************************** Third Party Libraries **********************************************/ + +/** + * Base64 Coding Library + * https://github.com/dankogai/js-base64#readme + */ +function Base64Code() { + // constants + const b64chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + const b64tab = (function (bin) { + const t = {}; + let i = 0; + const l = bin.length; + for (; i < l; i++) t[bin.charAt(i)] = i; + return t; + })(b64chars); + const fromCharCode = String.fromCharCode; + // encoder stuff + const cb_utob = function (c) { + let cc; + if (c.length < 2) { + cc = c.charCodeAt(0); + return cc < 0x80 + ? c + : cc < 0x800 + ? fromCharCode(0xc0 | (cc >>> 6)) + fromCharCode(0x80 | (cc & 0x3f)) + : fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) + + fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) + + fromCharCode(0x80 | (cc & 0x3f)); + } else { + cc = + 0x10000 + + (c.charCodeAt(0) - 0xd800) * 0x400 + + (c.charCodeAt(1) - 0xdc00); + return ( + fromCharCode(0xf0 | ((cc >>> 18) & 0x07)) + + fromCharCode(0x80 | ((cc >>> 12) & 0x3f)) + + fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) + + fromCharCode(0x80 | (cc & 0x3f)) + ); + } + }; + const re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g; + const utob = function (u) { + return u.replace(re_utob, cb_utob); + }; + const cb_encode = function (ccc) { + const padlen = [0, 2, 1][ccc.length % 3], + ord = + (ccc.charCodeAt(0) << 16) | + ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8) | + (ccc.length > 2 ? ccc.charCodeAt(2) : 0), + chars = [ + b64chars.charAt(ord >>> 18), + b64chars.charAt((ord >>> 12) & 63), + padlen >= 2 ? "=" : b64chars.charAt((ord >>> 6) & 63), + padlen >= 1 ? "=" : b64chars.charAt(ord & 63), + ]; + return chars.join(""); + }; + const btoa = function (b) { + return b.replace(/[\s\S]{1,3}/g, cb_encode); + }; + this.encode = function (u) { + const isUint8Array = + Object.prototype.toString.call(u) === "[object Uint8Array]"; + return isUint8Array ? u.toString("base64") : btoa(utob(String(u))); + }; + const uriencode = function (u, urisafe) { + return !urisafe + ? _encode(u) + : _encode(String(u)) + .replace(/[+\/]/g, function (m0) { + return m0 === "+" ? "-" : "_"; + }) + .replace(/=/g, ""); + }; + const encodeURI = function (u) { + return uriencode(u, true); + }; + // decoder stuff + const re_btou = /[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g; + const cb_btou = function (cccc) { + switch (cccc.length) { + case 4: + const cp = + ((0x07 & cccc.charCodeAt(0)) << 18) | + ((0x3f & cccc.charCodeAt(1)) << 12) | + ((0x3f & cccc.charCodeAt(2)) << 6) | + (0x3f & cccc.charCodeAt(3)), + offset = cp - 0x10000; + return ( + fromCharCode((offset >>> 10) + 0xd800) + + fromCharCode((offset & 0x3ff) + 0xdc00) + ); + case 3: + return fromCharCode( + ((0x0f & cccc.charCodeAt(0)) << 12) | + ((0x3f & cccc.charCodeAt(1)) << 6) | + (0x3f & cccc.charCodeAt(2)) + ); + default: + return fromCharCode( + ((0x1f & cccc.charCodeAt(0)) << 6) | (0x3f & cccc.charCodeAt(1)) + ); + } + }; + const btou = function (b) { + return b.replace(re_btou, cb_btou); + }; + const cb_decode = function (cccc) { + const len = cccc.length, + padlen = len % 4, + n = + (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0) | + (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0) | + (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0) | + (len > 3 ? b64tab[cccc.charAt(3)] : 0), + chars = [ + fromCharCode(n >>> 16), + fromCharCode((n >>> 8) & 0xff), + fromCharCode(n & 0xff), + ]; + chars.length -= [0, 0, 2, 1][padlen]; + return chars.join(""); + }; + const _atob = function (a) { + return a.replace(/\S{1,4}/g, cb_decode); + }; + const atob = function (a) { + return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g, "")); + }; + const _decode = function (u) { + return btou(_atob(u)); + }; + this.decode = function (a) { + return _decode( + String(a) + .replace(/[-_]/g, function (m0) { + return m0 === "-" ? "+" : "/"; + }) + .replace(/[^A-Za-z0-9\+\/]/g, "") + ) + .replace(/>/g, ">") + .replace(/</g, "<"); + }; + this.safeEncode = function (a) { + return this.encode(a.replace(/\+/g, "-").replace(/\//g, "_")); + }; + this.safeDecode = function (a) { + return this.decode(a.replace(/-/g, "+").replace(/_/g, "/")); + }; +} + +/** + * YAML parser for Javascript + * Author: Diogo Costa + */ +var YAML = (function () { + var errors = [], + reference_blocks = [], + processing_time = 0, + regex = { + regLevel: new RegExp("^([\\s\\-]+)"), + invalidLine: new RegExp("^\\-\\-\\-|^\\.\\.\\.|^\\s*#.*|^\\s*$"), + dashesString: new RegExp('^\\s*\\"([^\\"]*)\\"\\s*$'), + quotesString: new RegExp("^\\s*\\'([^\\']*)\\'\\s*$"), + float: new RegExp("^[+-]?[0-9]+\\.[0-9]+(e[+-]?[0-9]+(\\.[0-9]+)?)?$"), + integer: new RegExp("^[+-]?[0-9]+$"), + array: new RegExp("\\[\\s*(.*)\\s*\\]"), + map: new RegExp("\\{\\s*(.*)\\s*\\}"), + key_value: new RegExp("([a-z0-9_-][ a-z0-9_-]*):( .+)", "i"), + single_key_value: new RegExp("^([a-z0-9_-][ a-z0-9_-]*):( .+?)$", "i"), + key: new RegExp("([a-z0-9_-][ a-z0-9_-]*):( .+)?", "i"), + item: new RegExp("^-\\s+"), + trim: new RegExp("^\\s+|\\s+$"), + comment: new RegExp( + "([^\\'\\\"#]+([\\'\\\"][^\\'\\\"]*[\\'\\\"])*)*(#.*)?" + ), + }; + + /** + * @class A block of lines of a given level. + * @param {int} lvl The block's level. + * @private + */ + function Block(lvl) { + return { + /* The block's parent */ + parent: null, + /* Number of children */ + length: 0, + /* Block's level */ + level: lvl, + /* Lines of code to process */ + lines: [], + /* Blocks with greater level */ + children: [], + /* Add a block to the children collection */ + addChild: function (obj) { + this.children.push(obj); + obj.parent = this; + ++this.length; + }, + }; + } + + function parser(str) { + var regLevel = regex["regLevel"]; + var invalidLine = regex["invalidLine"]; + var lines = str.split("\n"); + var m; + var level = 0, + curLevel = 0; + + var blocks = []; + + var result = new Block(-1); + var currentBlock = new Block(0); + result.addChild(currentBlock); + var levels = []; + var line = ""; + + blocks.push(currentBlock); + levels.push(level); + + for (var i = 0, len = lines.length; i < len; ++i) { + line = lines[i]; + + if (line.match(invalidLine)) { + continue; + } + + if ((m = regLevel.exec(line))) { + level = m[1].length; + } else level = 0; + + if (level > curLevel) { + var oldBlock = currentBlock; + currentBlock = new Block(level); + oldBlock.addChild(currentBlock); + blocks.push(currentBlock); + levels.push(level); + } else if (level < curLevel) { + var added = false; + + var k = levels.length - 1; + for (; k >= 0; --k) { + if (levels[k] == level) { + currentBlock = new Block(level); + blocks.push(currentBlock); + levels.push(level); + if (blocks[k].parent != null) + blocks[k].parent.addChild(currentBlock); + added = true; + break; + } + } + + if (!added) { + errors.push("Error: Invalid indentation at line " + i + ": " + line); + return; + } + } + + currentBlock.lines.push(line.replace(regex["trim"], "")); + curLevel = level; + } + + return result; + } + + function processValue(val) { + val = val.replace(regex["trim"], ""); + var m = null; + + if (val == "true") { + return true; + } else if (val == "false") { + return false; + } else if (val == ".NaN") { + return Number.NaN; + } else if (val == "null") { + return null; + } else if (val == ".inf") { + return Number.POSITIVE_INFINITY; + } else if (val == "-.inf") { + return Number.NEGATIVE_INFINITY; + } else if ((m = val.match(regex["dashesString"]))) { + return m[1]; + } else if ((m = val.match(regex["quotesString"]))) { + return m[1]; + } else if ((m = val.match(regex["float"]))) { + return parseFloat(m[0]); + } else if ((m = val.match(regex["integer"]))) { + return parseInt(m[0]); + } else if (!isNaN((m = Date.parse(val)))) { + return new Date(m); + } else if ((m = val.match(regex["single_key_value"]))) { + var res = {}; + res[m[1]] = processValue(m[2]); + return res; + } else if ((m = val.match(regex["array"]))) { + var count = 0, + c = " "; + var res = []; + var content = ""; + var str = false; + for (var j = 0, lenJ = m[1].length; j < lenJ; ++j) { + c = m[1][j]; + if (c == "'" || c == '"') { + if (str === false) { + str = c; + content += c; + continue; + } else if ((c == "'" && str == "'") || (c == '"' && str == '"')) { + str = false; + content += c; + continue; + } + } else if (str === false && (c == "[" || c == "{")) { + ++count; + } else if (str === false && (c == "]" || c == "}")) { + --count; + } else if (str === false && count == 0 && c == ",") { + res.push(processValue(content)); + content = ""; + continue; + } + + content += c; + } + + if (content.length > 0) res.push(processValue(content)); + return res; + } else if ((m = val.match(regex["map"]))) { + var count = 0, + c = " "; + var res = []; + var content = ""; + var str = false; + for (var j = 0, lenJ = m[1].length; j < lenJ; ++j) { + c = m[1][j]; + if (c == "'" || c == '"') { + if (str === false) { + str = c; + content += c; + continue; + } else if ((c == "'" && str == "'") || (c == '"' && str == '"')) { + str = false; + content += c; + continue; + } + } else if (str === false && (c == "[" || c == "{")) { + ++count; + } else if (str === false && (c == "]" || c == "}")) { + --count; + } else if (str === false && count == 0 && c == ",") { + res.push(content); + content = ""; + continue; + } + + content += c; + } + + if (content.length > 0) res.push(content); + + var newRes = {}; + for (var j = 0, lenJ = res.length; j < lenJ; ++j) { + if ((m = res[j].match(regex["key_value"]))) { + newRes[m[1]] = processValue(m[2]); + } + } + + return newRes; + } else return val; + } + + function processFoldedBlock(block) { + var lines = block.lines; + var children = block.children; + var str = lines.join(" "); + var chunks = [str]; + for (var i = 0, len = children.length; i < len; ++i) { + chunks.push(processFoldedBlock(children[i])); + } + return chunks.join("\n"); + } + + function processLiteralBlock(block) { + var lines = block.lines; + var children = block.children; + var str = lines.join("\n"); + for (var i = 0, len = children.length; i < len; ++i) { + str += processLiteralBlock(children[i]); + } + return str; + } + + function processBlock(blocks) { + var m = null; + var res = {}; + var lines = null; + var children = null; + var currentObj = null; + + var level = -1; + + var processedBlocks = []; + + var isMap = true; + + for (var j = 0, lenJ = blocks.length; j < lenJ; ++j) { + if (level != -1 && level != blocks[j].level) continue; + + processedBlocks.push(j); + + level = blocks[j].level; + lines = blocks[j].lines; + children = blocks[j].children; + currentObj = null; + + for (var i = 0, len = lines.length; i < len; ++i) { + var line = lines[i]; + + if ((m = line.match(regex["key"]))) { + var key = m[1]; + + if (key[0] == "-") { + key = key.replace(regex["item"], ""); + if (isMap) { + isMap = false; + if (typeof res.length === "undefined") { + res = []; + } + } + if (currentObj != null) res.push(currentObj); + currentObj = {}; + isMap = true; + } + + if (typeof m[2] != "undefined") { + var value = m[2].replace(regex["trim"], ""); + if (value[0] == "&") { + var nb = processBlock(children); + if (currentObj != null) currentObj[key] = nb; + else res[key] = nb; + reference_blocks[value.substr(1)] = nb; + } else if (value[0] == "|") { + if (currentObj != null) + currentObj[key] = processLiteralBlock(children.shift()); + else res[key] = processLiteralBlock(children.shift()); + } else if (value[0] == "*") { + var v = value.substr(1); + var no = {}; + + if (typeof reference_blocks[v] == "undefined") { + errors.push("Reference '" + v + "' not found!"); + } else { + for (var k in reference_blocks[v]) { + no[k] = reference_blocks[v][k]; + } + + if (currentObj != null) currentObj[key] = no; + else res[key] = no; + } + } else if (value[0] == ">") { + if (currentObj != null) + currentObj[key] = processFoldedBlock(children.shift()); + else res[key] = processFoldedBlock(children.shift()); + } else { + if (currentObj != null) currentObj[key] = processValue(value); + else res[key] = processValue(value); + } + } else { + if (currentObj != null) currentObj[key] = processBlock(children); + else res[key] = processBlock(children); + } + } else if (line.match(/^-\s*$/)) { + if (isMap) { + isMap = false; + if (typeof res.length === "undefined") { + res = []; + } + } + if (currentObj != null) res.push(currentObj); + currentObj = {}; + isMap = true; + } else if ((m = line.match(/^-\s*(.*)/))) { + if (currentObj != null) currentObj.push(processValue(m[1])); + else { + if (isMap) { + isMap = false; + if (typeof res.length === "undefined") { + res = []; + } + } + res.push(processValue(m[1])); + } + } + } + + if (currentObj != null) { + if (isMap) { + isMap = false; + if (typeof res.length === "undefined") { + res = []; + } + } + res.push(currentObj); + } + } + + for (var j = processedBlocks.length - 1; j >= 0; --j) { + blocks.splice.call(blocks, processedBlocks[j], 1); + } + + return res; + } + + function semanticAnalysis(blocks) { + var res = processBlock(blocks.children); + return res; + } + + function preProcess(src) { + var m; + var lines = src.split("\n"); + + var r = regex["comment"]; + + for (var i in lines) { + if ((m = typeof lines[i] === "string" && lines[i].match(r))) { + /* var cmt = ""; + if(typeof m[3] != "undefined") + lines[i] = m[1]; + else if(typeof m[3] != "undefined") + lines[i] = m[3]; + else + lines[i] = ""; + */ + if (typeof m[3] !== "undefined") { + lines[i] = m[0].substr(0, m[0].length - m[3].length); + } + } + } + + return lines.join("\n"); + } + + function eval(str) { + errors = []; + reference_blocks = []; + processing_time = new Date().getTime(); + var pre = preProcess(str); + var doc = parser(pre); + var res = semanticAnalysis(doc); + processing_time = new Date().getTime() - processing_time; + + return res; + } + + return { + /** + * Parse a YAML file from a string. + * @param {String} str String with the YAML file contents. + * @function + */ + eval: eval, + + /** + * Get errors found when parsing the last file. + * @function + * @returns Errors found when parsing the last file. + */ + getErrors: function () { + return errors; + }, + + /** + * Get the time it took to parse the last file. + * @function + * @returns Time in milliseconds. + */ + getProcessingTime: function () { + return processing_time; + }, + }; +})(); \ No newline at end of file