/* eslint-disable no-case-declarations */ import { safeLoad } from 'static-js-yaml'; import { Base64 } from 'js-base64'; import { AND, FULL } from '../utils/logical'; import download from '../utils/download'; import { getFlag } from '../utils/geo'; import $ from './app'; 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', 'c2hhZG93', 'aHR0c', ]; const test = function (raw) { return keys.some((k) => raw.indexOf(k) !== -1); }; const parse = function (raw) { raw = Base64.decode(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 const proxies = safeLoad(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.decode(raw.split('ssd://')[1])); 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.encode(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.decode(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.decode(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.decode(params[3]), supported, }; // get other params const other_params = {}; line = line.split('/?')[1].split('&'); if (line.length > 1) { for (const item of line) { const [key, val] = item.split('='); other_params[key] = val.trim(); } } proxy = { ...proxy, name: other_params.remarks ? Base64.decode(other_params.remarks) : proxy.server, 'protocol-param': Base64.decode( other_params.protoparam || '', ).replace(/\s/g, ''), 'obfs-param': Base64.decode( other_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.decode(line); if (/=\s*vmess/.test(content)) { // Quantumult VMess URI format const partitions = content.split(',').map((p) => p.trim()); // 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', }; if (typeof params['udp-relay'] !== 'undefined') proxy.udp = JSON.parse(params['udp-relay']); if (typeof params['fast-open'] !== 'undefined') proxy.udp = JSON.parse(params['fast-open']); // handle ws headers if (params.obfs === 'ws' || params.obfs === 'wss') { proxy.network = 'ws'; proxy['ws-opts'].path = ( params['obfs-path'] || '"/"' ).match(/^"(.*)"$/)[1]; let obfs_host = params['obfs-header']; if (obfs_host && obfs_host.indexOf('Host') !== -1) { obfs_host = obfs_host.match( /Host:\s*([a-zA-Z0-9-.]*)/, )[1]; } proxy['ws-opts'].headers = { Host: 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-opts'] = { path: params.path, 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 = {}; line = line.split('trojan://')[1]; const [server, port] = line.split('@')[1].split('?')[0].split(':'); const name = decodeURIComponent(line.split('#')[1].trim()); let paramArr = line.split('?'); let sni = null; if (paramArr.length > 1) { paramArr = paramArr[1].split('#')[0].split('&'); const params = new Map( paramArr.map((item) => { return item.split('='); }), ); sni = params.get('sni'); } return { name: name || `[Trojan] ${server}`, // trojan uri may have no server tag! type: 'trojan', server, port, password: line.split('@')[0], sni, 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-opts'] = { path: params['obfs-uri'], 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-opts'] = { path: params.path, 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-opts'] = { path: params['ws-path'], }; const res = params['ws-headers'].match( /(,|^|\s)*HOST:\s*(.*?)(,|$)/, ); const host = res ? res[2] : proxy.server; proxy['ws-opts'].headers = { Host: host || params.server, }; } 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; }); }, }; } // duplicate handler function HandleDuplicateOperator(arg) { const { action, template, link, position } = { ...{ action: 'rename', template: '0 1 2 3 4 5 6 7 8 9', link: '-', position: 'back', }, ...arg, }; return { name: 'Handle Duplicate Operator', func: (proxies) => { if (action === 'delete') { const chosen = {}; return proxies.filter((p) => { if (chosen[p.name]) { return false; } chosen[p.name] = true; return true; }); } else if (action === 'rename') { const numbers = template.split(' '); // count occurrences of each name const counter = {}; let maxLen = 0; proxies.forEach((p) => { if (typeof counter[p.name] === 'undefined') counter[p.name] = 1; else counter[p.name]++; maxLen = Math.max( counter[p.name].toString().length, maxLen, ); }); const increment = {}; return proxies.map((p) => { if (counter[p.name] > 1) { if (typeof increment[p.name] == 'undefined') increment[p.name] = 1; let num = ''; let cnt = increment[p.name]++; let numDigits = 0; while (cnt > 0) { num = numbers[cnt % 10] + num; cnt = parseInt(cnt / 10); numDigits++; } // padding while (numDigits++ < maxLen) { num = numbers[0] + num; } if (position === 'front') { p.name = num + link + p.name; } else if (position === 'back') { p.name = p.name + link + num; } } return p; }); } }, }; } // 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 regex function RegexSortOperator(expressions) { return { name: 'Regex Sort Operator', func: (proxies) => { expressions = expressions.map((expr) => buildRegex(expr)); return proxies.sort((a, b) => { const oA = getRegexOrder(expressions, a.name); const oB = getRegexOrder(expressions, 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 getRegexOrder(expressions, str) { let order = null; for (let i = 0; i < expressions.length; i++) { if (expressions[i].test(str)) { order = i + 1; // plus 1 is important! 0 will be treated as false!!! break; } } return order; } // 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(buildRegex(expr, 'g'), now) .trim(); } return proxy; }); }, }; } // 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, }; } /** Script Operator function operator(proxies) { const {arg1} = $arguments; // do something return proxies; } WARNING: 1. This function name should be `operator`! 2. Always declare variables before using them! */ // eslint-disable-next-line no-unused-vars function ScriptOperator(script, targetPlatform, $arguments) { return { name: 'Script Operator', func: (proxies) => { let output = proxies; (function () { // interface to get internal operators // eslint-disable-next-line no-unused-vars const $get = (name, args) => { const item = PROXY_PROCESSORS[name]; return item(args); }; // eslint-disable-next-line no-unused-vars const $process = ApplyProcessor; eval(script); // eslint-disable-next-line no-undef output = operator(proxies, targetPlatform); })(); return output; }, }; } /**************************** Filters ***************************************/ // filter useless proxies function UselessFilter() { const KEYWORDS = [ '网址', '桁量', 'ζ—Άι—΄', 'εΊ”ζ€₯', 'θΏ‡ζœŸ', 'Bandwidth', 'expire', ]; return { name: 'Useless Filter', func: RegexFilter({ regex: 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) => { return buildRegex(r).test(proxy.name); }); return keep ? selected : !selected; }); }, }; } function buildRegex(str, ...options) { options = options.join(''); if (str.startsWith('(?i)')) { str = str.substr(4); return new RegExp(str, 'i' + options); } else { return new RegExp(str, options); } } // filter by proxy types function TypeFilter(types) { return { name: 'Type Filter', func: (proxies) => { return proxies.map((proxy) => types.some((t) => proxy.type === t), ); }, }; } /** 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! */ // eslint-disable-next-line no-unused-vars function ScriptFilter(script, targetPlatform, $arguments) { return { name: 'Script Filter', func: (proxies) => { let output = FULL(proxies.length, true); !(function () { eval(script); // eslint-disable-next-line no-undef output = filter(proxies, targetPlatform); })(); return output; }, }; } return { '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, 'Regex Sort Operator': RegexSortOperator, 'Regex Rename Operator': RegexRenameOperator, 'Regex Delete Operator': RegexDeleteOperator, 'Script Operator': ScriptOperator, 'Handle Duplicate Operator': HandleDuplicateOperator, }; })(); 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'] : '' },fast-open=${proxy.tfo || 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-opts'].path ? ',obfs-uri=' + proxy['ws-opts'].path : '' },tls-verification=${ proxy['skip-cert-verify'] ? 'false' : 'true' }`; } else { // ws obfs_opts = `,obfs=ws${ proxy['ws-opts'].headers.Host ? ',obfs-host=' + proxy['ws-opts'].headers.Host : '' }${ proxy['ws-opts'].path ? ',obfs-uri=' + proxy['ws-opts'].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' }`; } } let result = `vmess=${proxy.server}:${proxy.port},method=${ proxy.cipher === 'auto' ? 'none' : proxy.cipher },password=${proxy.uuid}${obfs_opts},fast-open=${ proxy.tfo || false }${proxy.udp ? ',udp-relay=true' : ',udp-relay=false'}`; if (proxy.alterId === 0) proxy['vmess-aead'] = true; if (typeof proxy['vmess-aead'] !== 'undefined') { result += `,aead=${proxy['vmess-aead']}`; } result += `,tag=${proxy.name}`; return result; 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' },fast-open=${proxy.tfo || 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},fast-open=${ proxy.tfo || 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 = '', udp_opts = '', tfo_opts = ''; if (typeof proxy.udp !== 'undefined') { udp_opts = proxy.udp ? ',udp=true' : ',udp=false'; } tfo_opts = `,fast-open=${proxy.tfo || false}`; 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}${udp_opts}${tfo_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'] || '' }}${udp_opts}${tfo_opts}`; case 'vmess': obfs_opts = ''; if (proxy.network === 'ws') { const host = proxy['ws-opts'].headers.Host || proxy.server; obfs_opts = `,transport:ws,host:${host},path:${ proxy['ws-opts'].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' }`; } let result = `${proxy.name}=vmess,${proxy.server},${ proxy.port },${proxy.cipher === 'auto' ? 'none' : proxy.cipher},"${ proxy.uuid }",over-tls:${proxy.tls || 'false'}${obfs_opts}`; if (proxy.alterId === 0) proxy['vmess-aead'] = true; if (typeof proxy['vmess-aead'] !== 'undefined') { result += `,vmess-aead=${proxy['vmess-aead']}`; } return result; 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' }${udp_opts}`; 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 result = ''; 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}`, ); } } result = `${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' }`; break; case 'vmess': tls_opts = ''; result = `${proxy.name}=vmess,${proxy.server},${ proxy.port },username=${proxy.uuid},tls=${proxy.tls || 'false'},tfo=${ proxy.tfo || 'false' }`; if (proxy.alterId === 0) proxy['vmess-aead'] = true; if (typeof proxy['vmess-aead'] !== 'undefined') { result += `,vmess-aead=${proxy['vmess-aead']}`; } if (proxy.network === 'ws') { const path = proxy['ws-opts'].path || '/'; const wsHeaders = Object.entries( proxy['ws-opts'].headers, ) .map(([key, value]) => `${key}:"${value}"`) .join('|'); result += `,ws=true${path ? ',ws-path=' + path : ''}${ wsHeaders ? ',ws-headers=' + wsHeaders : '' }`; } if (proxy.tls) { result += `${ typeof proxy['skip-cert-verify'] !== 'undefined' ? ',skip-cert-verify=' + proxy['skip-cert-verify'] : '' }`; result += proxy.sni ? `,sni=${proxy.sni}` : ''; } break; case 'trojan': result = `${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' },udp-relay=${proxy.udp || 'false'}`; break; case 'http': tls_opts = ', tls=false'; if (proxy.tls) { tls_opts = `,tls=true,skip-cert-verify=${proxy['skip-cert-verify']},sni=${proxy.sni}`; } result = `${proxy.name}=http, ${proxy.server}, ${ proxy.port }${proxy.username ? ',username=' + proxy.username : ''}${ proxy.password ? ',password=' + proxy.password : '' }${tls_opts},tfo=${proxy.tfo || 'false'}`; break; default: throw new Error( `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, ); } // handle surge hybrid param result += proxy['surge-hybrid'] !== undefined ? `,hybrid=${proxy['surge-hybrid']}` : ''; return result; }; 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.encode(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.encode(proxy.password)}/`; result += `?remarks=${Base64.encode(proxy.name)}${ proxy['obfs-param'] ? '&obfsparam=' + Base64.encode(proxy['obfs-param']) : '' }${ proxy['protocol-param'] ? '&protocolparam=' + Base64.encode(proxy['protocol-param']) : '' }`; result = 'ssr://' + Base64.encode(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-opts'].path || '/'; result.host = proxy['ws-opts'].headers.Host || proxy.server; } result = 'vmess://' + Base64.encode(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(), }; })(); export const ProxyUtils = (function () { function preprocess(raw) { for (const processor of PROXY_PREPROCESSORS) { try { if (processor.test(raw)) { $.info(`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; $.info(`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 = [], targetPlatform) { for (const item of operators) { // process script let script; const $arguments = {}; if (item.type.indexOf('Script') !== -1) { const { mode, content } = item.args; if (mode === 'link') { const url = content; // extract link arguments const rawArgs = url.split('#'); if (rawArgs.length > 1) { for (const pair of rawArgs[1].split('&')) { const key = pair.split('=')[0]; const value = pair.split('=')[1] || true; $arguments[key] = value; } } // if this is remote script, download it try { script = await download(url.split('#')[0]); $.info(`Script loaded: >>>\n ${script}`); } catch (err) { $.error( `Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`, ); // skip the script if download failed. continue; } } else { script = content; } } if (!PROXY_PROCESSORS[item.type]) { $.error(`Unknown operator: "${item.type}"`); continue; } $.info( `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, targetPlatform, $arguments, ); } 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), ); $.info(`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, }; })(); export function ApplyProcessor(processor, 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 (processor.name.indexOf('Filter') !== -1) { return ApplyFilter(processor, objs); } else if (processor.name.indexOf('Operator') !== -1) { return ApplyOperator(processor, objs); } } // 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; } // deep clone object function clone(object) { return JSON.parse(JSON.stringify(object)); } // remove flag function removeFlag(str) { return str .replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '') .trim(); }