mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-04-23 06:09:33 +08:00
Refactored ProxyUtils and RuleUtils
This commit is contained in:
parent
99cc8ce295
commit
90e611ceef
4
backend/dist/sub-store-parser.loon.min.js
vendored
4
backend/dist/sub-store-parser.loon.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
176
backend/src/core/proxy-utils/index.js
Normal file
176
backend/src/core/proxy-utils/index.js
Normal file
@ -0,0 +1,176 @@
|
||||
import download from '../../utils/download';
|
||||
|
||||
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
|
||||
import PROXY_PREPROCESSORS from './preprocessors';
|
||||
import PROXY_PRODUCERS from './producers';
|
||||
import PROXY_PARSERS from './parsers';
|
||||
import $ from '../app';
|
||||
|
||||
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 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 a 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 = await 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);
|
||||
}
|
||||
}
|
||||
|
||||
export const ProxyUtils = {
|
||||
parse,
|
||||
process,
|
||||
produce,
|
||||
};
|
||||
|
||||
function safeMatch(p, line) {
|
||||
let patternMatched;
|
||||
try {
|
||||
patternMatched = p.test(line);
|
||||
} catch (err) {
|
||||
patternMatched = false;
|
||||
}
|
||||
return patternMatched;
|
||||
}
|
879
backend/src/core/proxy-utils/parsers.js
Normal file
879
backend/src/core/proxy-utils/parsers.js
Normal file
@ -0,0 +1,879 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
// 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*https?/.test(line.split(',')[0]) && !Loon_Http().test(line)
|
||||
);
|
||||
};
|
||||
const parse = (line) => {
|
||||
const params = getSurgeParams(line);
|
||||
const tls = /^.*?=\s?https/.test(line);
|
||||
const proxy = {
|
||||
name: params.name,
|
||||
type: 'http',
|
||||
server: params.server,
|
||||
port: params.port,
|
||||
tls: JSON.parse(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;
|
||||
}
|
||||
|
||||
export default [
|
||||
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(),
|
||||
];
|
92
backend/src/core/proxy-utils/preprocessors.js
Normal file
92
backend/src/core/proxy-utils/preprocessors.js
Normal file
@ -0,0 +1,92 @@
|
||||
import { safeLoad } from 'static-js-yaml';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
function HTML() {
|
||||
const name = 'HTML';
|
||||
const test = (raw) => /^<!DOCTYPE html>/.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 };
|
||||
}
|
||||
|
||||
export default [HTML(), Base64Encoded(), Clash(), SSD()];
|
415
backend/src/core/proxy-utils/processors.js
Normal file
415
backend/src/core/proxy-utils/processors.js
Normal file
@ -0,0 +1,415 @@
|
||||
import { HTTP } from '../../vendor/open-api';
|
||||
import { FULL } from '../../utils/logical';
|
||||
import { getFlag } from '../../utils/geo';
|
||||
import lodash from 'lodash';
|
||||
|
||||
// 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!
|
||||
*/
|
||||
function ScriptOperator(script, targetPlatform, $arguments) {
|
||||
return {
|
||||
name: 'Script Operator',
|
||||
func: async (proxies) => {
|
||||
let output = proxies;
|
||||
await (async function () {
|
||||
const operator = new Function(
|
||||
'$arguments',
|
||||
'HTTP',
|
||||
'lodash',
|
||||
`${script}\n return operator`,
|
||||
)($arguments, HTTP, lodash);
|
||||
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 filter(proxies) {
|
||||
return proxies.map(p => {
|
||||
return p.name.indexOf("🇭🇰") !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
WARNING:
|
||||
1. This function name should be `filter`!
|
||||
2. Always declare variables before using them!
|
||||
*/
|
||||
function ScriptFilter(script, targetPlatform, $arguments) {
|
||||
return {
|
||||
name: 'Script Filter',
|
||||
func: async (proxies) => {
|
||||
let output = FULL(proxies.length, true);
|
||||
await (async function () {
|
||||
const filter = new Function(
|
||||
'$arguments',
|
||||
'HTTP',
|
||||
'lodash',
|
||||
`${script}\n return filter`,
|
||||
)($arguments, HTTP, lodash);
|
||||
output = filter(proxies, targetPlatform);
|
||||
})();
|
||||
return output;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
'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,
|
||||
};
|
||||
|
||||
async function ApplyFilter(filter, objs) {
|
||||
// select proxies
|
||||
let selected = FULL(objs.length, true);
|
||||
try {
|
||||
selected = await 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]);
|
||||
}
|
||||
|
||||
async function ApplyOperator(operator, objs) {
|
||||
let output = clone(objs);
|
||||
try {
|
||||
const output_ = await 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;
|
||||
}
|
||||
|
||||
export async function ApplyProcessor(processor, objs) {
|
||||
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();
|
||||
}
|
422
backend/src/core/proxy-utils/producers.js
Normal file
422
backend/src/core/proxy-utils/producers.js
Normal file
@ -0,0 +1,422 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
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':
|
||||
if (proxy.tls) {
|
||||
tls_opts = `,skip-cert-verify=${proxy['skip-cert-verify']},sni=${proxy.sni}`;
|
||||
}
|
||||
result = `${proxy.name}=${proxy.tls ? 'https' : '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 };
|
||||
}
|
||||
|
||||
export default {
|
||||
QX: QX_Producer(),
|
||||
Surge: Surge_Producer(),
|
||||
Loon: Loon_Producer(),
|
||||
Clash: Clash_Producer(),
|
||||
URI: URI_Producer(),
|
||||
JSON: JSON_Producer(),
|
||||
};
|
@ -1,324 +0,0 @@
|
||||
import { ApplyProcessor } from './proxy-utils';
|
||||
import YAML from 'static-js-yaml';
|
||||
import $ from './app';
|
||||
|
||||
const RULE_TYPES_MAPPING = [
|
||||
[/^(DOMAIN|host|HOST)$/, 'DOMAIN'],
|
||||
[/^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, 'DOMAIN-KEYWORD'],
|
||||
[/^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, 'DOMAIN-SUFFIX'],
|
||||
[/^USER-AGENT$/i, 'USER-AGENT'],
|
||||
[/^PROCESS-NAME$/, 'PROCESS-NAME'],
|
||||
[/^(DEST-PORT|DST-PORT)$/, 'DST-PORT'],
|
||||
[/^SRC-IP(-CIDR)?$/, 'SRC-IP'],
|
||||
[/^(IN|SRC)-PORT$/, 'IN-PORT'],
|
||||
[/^PROTOCOL$/, 'PROTOCOL'],
|
||||
[/^IP-CIDR$/i, 'IP-CIDR'],
|
||||
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/],
|
||||
];
|
||||
|
||||
const RULE_PREPROCESSORS = (function () {
|
||||
function HTML() {
|
||||
const name = 'HTML';
|
||||
const test = (raw) => /^<!DOCTYPE html>/.test(raw);
|
||||
// simply discard HTML
|
||||
const parse = () => '';
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function ClashProvider() {
|
||||
const name = 'Clash Provider';
|
||||
const test = (raw) => raw.indexOf('payload:') === 0;
|
||||
const parse = (raw) => {
|
||||
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
return [HTML(), ClashProvider()];
|
||||
})();
|
||||
const RULE_PARSERS = (function () {
|
||||
function AllRuleParser() {
|
||||
const name = 'Universal Rule Parser';
|
||||
const test = () => true;
|
||||
const parse = (raw) => {
|
||||
const lines = raw.split('\n');
|
||||
const result = [];
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
// skip empty line
|
||||
if (line.length === 0) continue;
|
||||
// skip comments
|
||||
if (/\s*#/.test(line)) continue;
|
||||
try {
|
||||
const params = line.split(',').map((w) => w.trim());
|
||||
let rawType = params[0];
|
||||
let matched = false;
|
||||
for (const item of RULE_TYPES_MAPPING) {
|
||||
const regex = item[0];
|
||||
if (regex.test(rawType)) {
|
||||
matched = true;
|
||||
const rule = {
|
||||
type: item[1],
|
||||
content: params[1],
|
||||
};
|
||||
if (
|
||||
rule.type === 'IP-CIDR' ||
|
||||
rule.type === 'IP-CIDR6'
|
||||
) {
|
||||
rule.options = params.slice(2);
|
||||
}
|
||||
result.push(rule);
|
||||
}
|
||||
}
|
||||
if (!matched)
|
||||
throw new Error('Invalid rule type: ' + rawType);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Failed to parse line: ${line}\n Reason: ${e}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
return [AllRuleParser()];
|
||||
})();
|
||||
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.dump(conf);
|
||||
};
|
||||
return { type, func };
|
||||
}
|
||||
|
||||
return {
|
||||
QX: QXFilter(),
|
||||
Surge: SurgeRuleSet(),
|
||||
Loon: LoonRules(),
|
||||
Clash: ClashRuleProvider(),
|
||||
};
|
||||
})();
|
||||
|
||||
export const RuleUtils = (function () {
|
||||
function preprocess(raw) {
|
||||
for (const processor of RULE_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 parse(raw) {
|
||||
raw = preprocess(raw);
|
||||
for (const parser of RULE_PARSERS) {
|
||||
let matched;
|
||||
try {
|
||||
matched = parser.test(raw);
|
||||
} catch (err) {
|
||||
matched = false;
|
||||
}
|
||||
if (matched) {
|
||||
$.info(`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);
|
||||
$.info(
|
||||
`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 };
|
||||
})();
|
69
backend/src/core/rule-utils/index.js
Normal file
69
backend/src/core/rule-utils/index.js
Normal file
@ -0,0 +1,69 @@
|
||||
import RULE_PREPROCESSORS from './preprocessors';
|
||||
import RULE_PRODUCERS from './producers';
|
||||
import RULE_PARSERS from './parsers';
|
||||
import $ from '../app';
|
||||
|
||||
export const RuleUtils = (function () {
|
||||
function preprocess(raw) {
|
||||
for (const processor of RULE_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 parse(raw) {
|
||||
raw = preprocess(raw);
|
||||
for (const parser of RULE_PARSERS) {
|
||||
let matched;
|
||||
try {
|
||||
matched = parser.test(raw);
|
||||
} catch (err) {
|
||||
matched = false;
|
||||
}
|
||||
if (matched) {
|
||||
$.info(`Rule parser [${parser.name}] is activated!`);
|
||||
return parser.parse(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, produce };
|
||||
})();
|
58
backend/src/core/rule-utils/parsers.js
Normal file
58
backend/src/core/rule-utils/parsers.js
Normal file
@ -0,0 +1,58 @@
|
||||
const RULE_TYPES_MAPPING = [
|
||||
[/^(DOMAIN|host|HOST)$/, 'DOMAIN'],
|
||||
[/^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, 'DOMAIN-KEYWORD'],
|
||||
[/^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, 'DOMAIN-SUFFIX'],
|
||||
[/^USER-AGENT$/i, 'USER-AGENT'],
|
||||
[/^PROCESS-NAME$/, 'PROCESS-NAME'],
|
||||
[/^(DEST-PORT|DST-PORT)$/, 'DST-PORT'],
|
||||
[/^SRC-IP(-CIDR)?$/, 'SRC-IP'],
|
||||
[/^(IN|SRC)-PORT$/, 'IN-PORT'],
|
||||
[/^PROTOCOL$/, 'PROTOCOL'],
|
||||
[/^IP-CIDR$/i, 'IP-CIDR'],
|
||||
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/],
|
||||
];
|
||||
|
||||
function AllRuleParser() {
|
||||
const name = 'Universal Rule Parser';
|
||||
const test = () => true;
|
||||
const parse = (raw) => {
|
||||
const lines = raw.split('\n');
|
||||
const result = [];
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
// skip empty line
|
||||
if (line.length === 0) continue;
|
||||
// skip comments
|
||||
if (/\s*#/.test(line)) continue;
|
||||
try {
|
||||
const params = line.split(',').map((w) => w.trim());
|
||||
let rawType = params[0];
|
||||
let matched = false;
|
||||
for (const item of RULE_TYPES_MAPPING) {
|
||||
const regex = item[0];
|
||||
if (regex.test(rawType)) {
|
||||
matched = true;
|
||||
const rule = {
|
||||
type: item[1],
|
||||
content: params[1],
|
||||
};
|
||||
if (
|
||||
rule.type === 'IP-CIDR' ||
|
||||
rule.type === 'IP-CIDR6'
|
||||
) {
|
||||
rule.options = params.slice(2);
|
||||
}
|
||||
result.push(rule);
|
||||
}
|
||||
}
|
||||
if (!matched) throw new Error('Invalid rule type: ' + rawType);
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse line: ${line}\n Reason: ${e}`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
export default [AllRuleParser()];
|
18
backend/src/core/rule-utils/preprocessors.js
Normal file
18
backend/src/core/rule-utils/preprocessors.js
Normal file
@ -0,0 +1,18 @@
|
||||
function HTML() {
|
||||
const name = 'HTML';
|
||||
const test = (raw) => /^<!DOCTYPE html>/.test(raw);
|
||||
// simply discard HTML
|
||||
const parse = () => '';
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function ClashProvider() {
|
||||
const name = 'Clash Provider';
|
||||
const test = (raw) => raw.indexOf('payload:') === 0;
|
||||
const parse = (raw) => {
|
||||
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
export default [HTML(), ClashProvider()];
|
81
backend/src/core/rule-utils/producers.js
Normal file
81
backend/src/core/rule-utils/producers.js
Normal file
@ -0,0 +1,81 @@
|
||||
import YAML from 'static-js-yaml';
|
||||
|
||||
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.dump(conf);
|
||||
};
|
||||
return { type, func };
|
||||
}
|
||||
|
||||
export default {
|
||||
QX: QXFilter(),
|
||||
Surge: SurgeRuleSet(),
|
||||
Loon: LoonRules(),
|
||||
Clash: ClashRuleProvider(),
|
||||
};
|
4
backend/sub-store.min.js
vendored
4
backend/sub-store.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user