Compare commits

...

12 Commits

Author SHA1 Message Date
xream
aff7ddf41e feat: 脚本筛选的快捷操作支持 await 2024-01-13 13:55:07 +08:00
xream
164ae9a7a8 feat: 快捷脚本支持 await; 脚本操作支持 produceArtifact 2024-01-13 13:40:34 +08:00
xream
3aacd26b79 feat: 支持输出到 sing-box; 文件脚本支持 ` ; 脚本支持 ProxyUtils.yaml` 2024-01-13 10:28:07 +08:00
xream
5915416232 feat: 文件支持远程/合并, /api/file/name 接口支持参数覆盖 2024-01-12 07:22:25 +08:00
xream
c059296224 feat: 文件支持脚本操作 2024-01-12 06:16:39 +08:00
xream
9ae70eca09 feat: 同步配置支持文件 2024-01-12 03:52:41 +08:00
xream
d0acf49b83 feat: 文件接口 2024-01-12 02:23:57 +08:00
xream
c51f3511dd fix: 兼容部分不带节点名的 URI 2024-01-08 09:44:53 +08:00
xream
ee2fcc7ee3 fix: 兼容部分不带参数的 URI 输入 2024-01-08 09:28:33 +08:00
xream
95615d1877 feat: 支持全局请求超时(前端 > 2.14.29) 2024-01-08 07:22:03 +08:00
xream
962bcda9dd chore: 同步远程配置输出更多日志 2024-01-07 17:44:03 +08:00
xream
9bb4739d56 Node.js 版的通知支持第三方推送服务. 环境变量名 SUB_STORE_PUSH_SERVICE. 支持 Bark/PushPlus 等服务. 形如: https://api.day.app/XXXXXXXXX/[推送标题]/[推送内容]?group=SubStore&autoCopy=1&isArchive=1&sound=shake&level=timeSensitivehttp://www.pushplus.plus/send?token=XXXXXXXXX&title=[推送标题]&content=[推送内容]&channel=wechat 的 URL, [推送标题][推送内容] 会被自动替换 2024-01-02 22:52:33 +08:00
18 changed files with 1096 additions and 42 deletions

View File

@@ -37,7 +37,6 @@ jobs:
- name: Bundle
run: |
cd backend
pnpm i -D estrella
pnpm run bundle
- id: tag
name: Generate release tag

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.14.137",
"version": "2.14.150",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {

View File

@@ -1,3 +1,4 @@
import YAML from 'static-js-yaml';
import download from '@/utils/download';
import { isIPv4, isIPv6, isValidPortNumber } from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
@@ -59,7 +60,6 @@ function parse(raw) {
$.error(`Failed to parse line: ${line}`);
}
}
return proxies;
}
@@ -193,6 +193,7 @@ export const ProxyUtils = {
isIPv4,
isIPv6,
isIP,
yaml: YAML,
};
function tryParse(parser, line) {
@@ -218,7 +219,7 @@ function lastParse(proxy) {
proxy.port = parseInt(proxy.port, 10);
}
if (proxy.server) {
proxy.server = proxy.server
proxy.server = `${proxy.server}`
.trim()
.replace(/^\[/, '')
.replace(/\]$/, '');

View File

@@ -332,11 +332,14 @@ function URI_VLESS() {
const parse = (line) => {
line = line.split('vless://')[1];
// eslint-disable-next-line no-unused-vars
let [__, uuid, server, port, addons, name] =
/^(.*?)@(.*?):(\d+)\/?\?(.*?)(?:#(.*?))$/.exec(line);
let [__, uuid, server, port, ___, addons = '', name] =
/^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10);
uuid = decodeURIComponent(uuid);
name = decodeURIComponent(name) ?? `VLESS ${server}:${port}`;
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `VLESS ${server}:${port}`;
const proxy = {
type: 'vless',
name,
@@ -414,14 +417,17 @@ function URI_Hysteria2() {
const parse = (line) => {
line = line.split(/(hysteria2|hy2):\/\//)[2];
// eslint-disable-next-line no-unused-vars
let [__, password, server, ___, port, addons, name] =
/^(.*?)@(.*?)(:(\d+))?\/?\?(.*?)(?:#(.*?))$/.exec(line);
let [__, password, server, ___, port, ____, addons = '', name] =
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
password = decodeURIComponent(password);
name = decodeURIComponent(name) ?? `Hysteria2 ${server}:${port}`;
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `Hysteria2 ${server}:${port}`;
const proxy = {
type: 'hysteria2',

View File

@@ -7,6 +7,8 @@ import lodash from 'lodash';
import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils';
import { produceArtifact } from '@/restful/sync';
import env from '@/utils/env';
import { getFlowHeaders, parseFlowHeaders, flowTransfer } from '@/utils/flow';
@@ -316,11 +318,21 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
await (async function () {
const operator = createDynamicFunction(
'operator',
`async function operator(proxies = []) {
return proxies.map(($server = {}) => {
${script}
return $server
})
`async function operator(input = []) {
if (Array.isArray(input)) {
let proxies = input
let list = []
for await (let $server of proxies) {
${script}
list.push($server)
}
return list
} else {
let { $content, $files } = input
${script}
return { $content, $files }
}
}`,
$arguments,
);
@@ -610,10 +622,16 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
await (async function () {
const filter = createDynamicFunction(
'filter',
`async function filter(proxies = []) {
return proxies.filter(($server = {}) => {
${script}
})
`async function filter(input = []) {
let proxies = input
let list = []
const fn = async ($server) => {
${script}
}
for await (let $server of proxies) {
list.push(await fn($server))
}
return list
}`,
$arguments,
);
@@ -649,13 +667,14 @@ async function ApplyFilter(filter, objs) {
try {
selected = await filter.func(objs);
} catch (err) {
// print log and skip this filter
$.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
let funcErr = '';
let funcErrMsg = `${err.message ?? err}`;
if (funcErrMsg.includes('$server is not defined')) {
funcErr = '';
} else {
$.error(
`Cannot apply filter ${filter.name}(function filter)! Reason: ${err}`,
);
funcErr = `执行 function filter 失败 ${funcErrMsg}; `;
}
try {
@@ -684,14 +703,18 @@ async function ApplyOperator(operator, objs) {
const output_ = await operator.func(output);
if (output_) output = output_;
} catch (err) {
$.error(
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
);
let funcErr = '';
let funcErrMsg = `${err.message ?? err}`;
if (funcErrMsg.includes('$server is not defined')) {
if (
funcErrMsg.includes('$server is not defined') ||
funcErrMsg.includes('$content is not defined') ||
funcErrMsg.includes('$files is not defined')
) {
funcErr = '';
} else {
$.error(
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
);
funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
}
try {
@@ -769,6 +792,7 @@ function createDynamicFunction(name, script, $arguments) {
'ProxyUtils',
'scriptResourceCache',
'flowUtils',
'produceArtifact',
`${script}\n return ${name}`,
)(
$arguments,
@@ -783,6 +807,7 @@ function createDynamicFunction(name, script, $arguments) {
ProxyUtils,
scriptResourceCache,
flowUtils,
produceArtifact,
);
} else {
return new Function(
@@ -792,8 +817,17 @@ function createDynamicFunction(name, script, $arguments) {
'ProxyUtils',
'scriptResourceCache',
'flowUtils',
'produceArtifact',
`${script}\n return ${name}`,
)($arguments, $, lodash, ProxyUtils, scriptResourceCache, flowUtils);
)(
$arguments,
$,
lodash,
ProxyUtils,
scriptResourceCache,
flowUtils,
produceArtifact,
);
}
}

View File

@@ -2,9 +2,10 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function ClashMeta_Producer() {
const type = 'ALL';
const produce = (proxies, type) => {
const produce = (proxies, type, opts = {}) => {
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (proxy.type === 'snell' && String(proxy.version) === '4') {
return false;
}

View File

@@ -9,6 +9,7 @@ import V2Ray_Producer from './v2ray';
import QX_Producer from './qx';
import ShadowRocket_Producer from './shadowrocket';
import Surfboard_Producer from './surfboard';
import singbox_Producer from './sing-box';
function JSON_Producer() {
const type = 'ALL';
@@ -29,4 +30,5 @@ export default {
Stash: Stash_Producer(),
ShadowRocket: ShadowRocket_Producer(),
Surfboard: Surfboard_Producer(),
'sing-box': singbox_Producer(),
};

View File

@@ -0,0 +1,682 @@
import ClashMeta_Producer from './clashmeta';
import $ from '@/core/app';
const tfoParser = (proxy, parsedProxy) => {
parsedProxy.tcp_fast_open = false;
if (proxy.tfo) parsedProxy.tcp_fast_open = true;
if (proxy.tcp_fast_open) parsedProxy.tcp_fast_open = true;
if (proxy['tcp-fast-open']) parsedProxy.tcp_fast_open = true;
if (!parsedProxy.tcp_fast_open) delete parsedProxy.tcp_fast_open;
};
const smuxParser = (smux, proxy) => {
if (!smux || !smux.enabled) return;
proxy.multiplex = { enabled: true };
proxy.multiplex.protocol = smux.protocol;
if (smux['max-connections'])
proxy.multiplex.max_connections = parseInt(
`${smux['max-connections']}`,
10,
);
if (smux['max-streams'])
proxy.multiplex.max_streams = parseInt(`${smux['max-streams']}`, 10);
if (smux['min-streams'])
proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10);
if (smux.padding) proxy.multiplex.padding = true;
};
const wsParser = (proxy, parsedProxy) => {
const transport = { type: 'ws', headers: {} };
if (proxy['ws-opts']) {
const { path: wsPath = '', headers: wsHeaders = {} } = proxy['ws-opts'];
if (wsPath !== '') transport.path = `${wsPath}`;
if (Object.keys(wsHeaders).length > 0) {
const headers = {};
for (const key of Object.keys(wsHeaders)) {
let value = wsHeaders[key];
if (value === '') continue;
if (!Array.isArray(value)) value = [`${value}`];
if (value.length > 0) headers[key] = value;
}
const { Host: wsHost } = headers;
if (wsHost.length === 1)
for (const item of `Host:${wsHost[0]}`.split('\n')) {
const [key, value] = item.split(':');
if (value.trim() === '') continue;
headers[key.trim()] = value.trim().split(',');
}
transport.headers = headers;
}
}
if (proxy['ws-headers']) {
const headers = {};
for (const key of Object.keys(proxy['ws-headers'])) {
let value = proxy['ws-headers'][key];
if (value === '') continue;
if (!Array.isArray(value)) value = [`${value}`];
if (value.length > 0) headers[key] = value;
}
const { Host: wsHost } = headers;
if (wsHost.length === 1)
for (const item of `Host:${wsHost[0]}`.split('\n')) {
const [key, value] = item.split(':');
if (value.trim() === '') continue;
headers[key.trim()] = value.trim().split(',');
}
for (const key of Object.keys(headers))
transport.headers[key] = headers[key];
}
if (proxy['ws-path'] && proxy['ws-path'] !== '')
transport.path = `${proxy['ws-path']}`;
if (transport.path) {
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
// eslint-disable-next-line no-unused-vars
const [_, path = '', ed = ''] = reg.exec(transport.path);
transport.path = path;
if (ed !== '') {
transport.early_data_header_name = 'Sec-WebSocket-Protocol';
transport.max_early_data = parseInt(ed, 10);
}
}
if (parsedProxy.tls.insecure)
parsedProxy.tls.server_name = transport.headers.Host[0];
if (proxy['ws-opts'] && proxy['ws-opts']['v2ray-http-upgrade']) {
transport.type = 'httpupgrade';
if (transport.headers.Host) {
transport.host = transport.headers.Host[0];
delete transport.headers.Host;
}
if (transport.max_early_data) delete transport.max_early_data;
if (transport.early_data_header_name)
delete transport.early_data_header_name;
}
for (const key of Object.keys(transport.headers)) {
const value = transport.headers[key];
if (value.length === 1) transport.headers[key] = value[0];
}
parsedProxy.transport = transport;
};
const h1Parser = (proxy, parsedProxy) => {
const transport = { type: 'http', headers: {} };
if (proxy['http-opts']) {
const {
method = '',
path: h1Path = '',
headers: h1Headers = {},
} = proxy['http-opts'];
if (method !== '') transport.method = method;
if (Array.isArray(h1Path)) {
transport.path = `${h1Path[0]}`;
} else if (h1Path !== '') transport.path = `${h1Path}`;
for (const key of Object.keys(h1Headers)) {
let value = h1Headers[key];
if (value === '') continue;
if (key.toLowerCase() === 'host') {
let host = value;
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
continue;
}
if (!Array.isArray(value))
value = `${value}`.split(',').map((i) => i.trim());
if (value.length > 0) transport.headers[key] = value;
}
}
if (proxy['http-host'] && proxy['http-host'] !== '') {
let host = proxy['http-host'];
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
}
if (!transport.host) return;
if (proxy['http-path'] && proxy['http-path'] !== '') {
const path = proxy['http-path'];
if (Array.isArray(path)) {
transport.path = `${path[0]}`;
} else if (path !== '') transport.path = `${path}`;
}
if (parsedProxy.tls.insecure)
parsedProxy.tls.server_name = transport.host[0];
if (transport.host.length === 1) transport.host = transport.host[0];
for (const key of Object.keys(transport.headers)) {
const value = transport.headers[key];
if (value.length === 1) transport.headers[key] = value[0];
}
parsedProxy.transport = transport;
};
const h2Parser = (proxy, parsedProxy) => {
const transport = { type: 'http' };
if (proxy['h2-opts']) {
let { host = '', path = '' } = proxy['h2-opts'];
if (path !== '') transport.path = `${path}`;
if (host !== '') {
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
}
}
if (proxy['h2-host'] && proxy['h2-host'] !== '') {
let host = proxy['h2-host'];
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
}
if (proxy['h2-path'] && proxy['h2-path'] !== '')
transport.path = `${proxy['h2-path']}`;
parsedProxy.tls.enabled = true;
if (parsedProxy.tls.insecure)
parsedProxy.tls.server_name = transport.host[0];
if (transport.host.length === 1) transport.host = transport.host[0];
parsedProxy.transport = transport;
};
const grpcParser = (proxy, parsedProxy) => {
const transport = { type: 'grpc' };
if (proxy['grpc-opts']) {
const serviceName = proxy['grpc-opts']['grpc-service-name'];
if (serviceName && serviceName !== '')
transport.service_name = serviceName;
}
parsedProxy.transport = transport;
};
const tlsParser = (proxy, parsedProxy) => {
if (proxy.tls) parsedProxy.tls.enabled = true;
if (proxy.servername && proxy.servername !== '')
parsedProxy.tls.server_name = proxy.servername;
if (proxy.peer && proxy.peer !== '')
parsedProxy.tls.server_name = proxy.peer;
if (proxy.sni && proxy.sni !== '') parsedProxy.tls.server_name = proxy.sni;
if (proxy['skip-cert-verify']) parsedProxy.tls.insecure = true;
if (proxy.insecure) parsedProxy.tls.insecure = true;
if (proxy['disable-sni']) parsedProxy.tls.disable_sni = true;
if (typeof proxy.alpn === 'string') {
parsedProxy.tls.alpn = [proxy.alpn];
} else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn;
if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
if (proxy.ca_str) parsedProxy.tls.certificate = proxy.ca_sStr;
if (proxy['ca-str']) parsedProxy.tls.certificate = proxy['ca-str'];
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
parsedProxy.tls.utls = {
enabled: true,
fingerprint: proxy['client-fingerprint'],
};
if (proxy['reality-opts']) {
parsedProxy.tls.reality = { enabled: true };
if (proxy['reality-opts']['public-key'])
parsedProxy.tls.reality.public_key =
proxy['reality-opts']['public-key'];
if (proxy['reality-opts']['short-id'])
parsedProxy.tls.reality.short_id =
proxy['reality-opts']['short-id'];
}
if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
};
const httpParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'http',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
tls: { enabled: false, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.username) parsedProxy.username = proxy.username;
if (proxy.password) parsedProxy.password = proxy.password;
if (proxy.headers) {
parsedProxy.headers = {};
for (const k of Object.keys(proxy.headers)) {
parsedProxy.headers[k] = `${proxy.headers[k]}`;
}
if (Object.keys(parsedProxy.headers).length === 0)
delete parsedProxy.headers;
}
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
return parsedProxy;
};
const socks5Parser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'socks',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
version: '5',
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.username) parsedProxy.username = proxy.username;
if (proxy.password) parsedProxy.password = proxy.password;
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
return parsedProxy;
};
const ssParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'shadowsocks',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
method: proxy.cipher,
password: proxy.password,
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
if (proxy.plugin) {
const optArr = [];
if (proxy.plugin === 'obfs') {
parsedProxy.plugin = 'obfs-local';
parsedProxy.plugin_opts = '';
if (proxy['obfs-host'])
proxy['plugin-opts'].host = proxy['obfs-host'];
Object.keys(proxy['plugin-opts']).forEach((k) => {
switch (k) {
case 'mode':
optArr.push(`obfs=${proxy['plugin-opts'].mode}`);
break;
case 'host':
optArr.push(`obfs-host=${proxy['plugin-opts'].host}`);
break;
default:
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
break;
}
});
}
if (proxy.plugin === 'v2ray-plugin') {
parsedProxy.plugin = 'v2ray-plugin';
if (proxy['ws-host']) proxy['plugin-opts'].host = proxy['ws-host'];
if (proxy['ws-path']) proxy['plugin-opts'].path = proxy['ws-path'];
Object.keys(proxy['plugin-opts']).forEach((k) => {
switch (k) {
case 'tls':
if (proxy['plugin-opts'].tls) optArr.push('tls');
break;
case 'host':
optArr.push(`host=${proxy['plugin-opts'].host}`);
break;
case 'path':
optArr.push(`path=${proxy['plugin-opts'].path}`);
break;
case 'headers':
optArr.push(
`headers=${JSON.stringify(
proxy['plugin-opts'].headers,
)}`,
);
break;
case 'mux':
if (proxy['plugin-opts'].mux)
parsedProxy.multiplex = { enabled: true };
break;
default:
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
}
});
}
parsedProxy.plugin_opts = optArr.join(';');
}
return parsedProxy;
};
// eslint-disable-next-line no-unused-vars
const ssrParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'shadowsocksr',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
method: proxy.cipher,
password: proxy.password,
obfs: proxy.obfs,
protocol: proxy.protocol,
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['obfs-param']) parsedProxy.obfs_param = proxy['obfs-param'];
if (proxy['protocol-param'] && proxy['protocol-param'] !== '')
parsedProxy.protocol_param = proxy['protocol-param'];
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const vmessParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'vmess',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
uuid: proxy.uuid,
security: proxy.cipher,
alter_id: parseInt(`${proxy.alterId}`, 10),
tls: { enabled: false, server_name: proxy.server, insecure: false },
};
if (
[
'auto',
'none',
'zero',
'aes-128-gcm',
'chacha20-poly1305',
'aes-128-ctr',
].indexOf(parsedProxy.security) === -1
)
parsedProxy.security = 'auto';
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const vlessParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'vless',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
uuid: proxy.uuid,
tls: { enabled: false, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
tlsParser(proxy, parsedProxy);
return parsedProxy;
};
const trojanParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'trojan',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const hysteriaParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'hysteria',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
disable_mtu_discovery: false,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.auth_str) parsedProxy.auth_str = `${proxy.auth_str}`;
if (proxy['auth-str']) parsedProxy.auth_str = `${proxy['auth-str']}`;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
// eslint-disable-next-line no-control-regex
const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$');
if (reg.test(`${proxy.up}`)) {
parsedProxy.up = `${proxy.up}`;
} else {
parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
}
if (reg.test(`${proxy.down}`)) {
parsedProxy.down = `${proxy.down}`;
} else {
parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
}
if (proxy.obfs) parsedProxy.obfs = proxy.obfs;
if (proxy.recv_window_conn)
parsedProxy.recv_window_conn = proxy.recv_window_conn;
if (proxy['recv-window-conn'])
parsedProxy.recv_window_conn = proxy['recv-window-conn'];
if (proxy.recv_window) parsedProxy.recv_window = proxy.recv_window;
if (proxy['recv-window']) parsedProxy.recv_window = proxy['recv-window'];
if (proxy.disable_mtu_discovery) {
if (typeof proxy.disable_mtu_discovery === 'boolean') {
parsedProxy.disable_mtu_discovery = proxy.disable_mtu_discovery;
} else {
if (proxy.disable_mtu_discovery === 1)
parsedProxy.disable_mtu_discovery = true;
}
}
tlsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const hysteria2Parser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'hysteria2',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
obfs: {},
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';
if (proxy['obfs-password'])
parsedProxy.obfs.password = proxy['obfs-password'];
if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
tlsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const tuic5Parser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'tuic',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
uuid: proxy.uuid,
password: proxy.password,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (
proxy['congestion-controller'] &&
proxy['congestion-controller'] !== 'cubic'
)
parsedProxy.congestion_control = proxy['congestion-controller'];
if (proxy['udp-relay-mode'] && proxy['udp-relay-mode'] !== 'native')
parsedProxy.udp_relay_mode = proxy['udp-relay-mode'];
if (proxy['reduce-rtt']) parsedProxy.zero_rtt_handshake = true;
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
if (proxy['heartbeat-interval'])
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
tfoParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const wireguardParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'wireguard',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
local_address: [proxy.ip, proxy.ipv6],
private_key: proxy['private-key'],
peer_public_key: proxy['public-key'],
pre_shared_key: proxy['pre-shared-key'],
reserved: [],
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (typeof proxy.reserved === 'string') {
parsedProxy.reserved.push(proxy.reserved);
} else {
for (const r of proxy.reserved) parsedProxy.reserved.push(r);
}
if (proxy.peers && proxy.peers.length > 0) {
parsedProxy.peers = [];
for (const p of proxy.peers) {
const peer = {
server: p.server,
server_port: parseInt(`${p.port}`, 10),
public_key: p['public-key'],
allowed_ips: p.allowed_ips,
reserved: [],
};
if (typeof p.reserved === 'string') {
peer.reserved.push(p.reserved);
} else {
for (const r of p.reserved) peer.reserved.push(r);
}
if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key'];
parsedProxy.peers.push(peer);
}
}
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
export default function singbox_Producer() {
const type = 'ALL';
const produce = (proxies, type) => {
const list = [];
ClashMeta_Producer()
.produce(proxies, 'internal', { 'include-unsupported-proxy': true })
.map((proxy) => {
try {
switch (proxy.type) {
case 'http':
list.push(httpParser(proxy));
break;
case 'socks5':
if (proxy.tls) {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with tls`,
);
} else {
list.push(socks5Parser(proxy));
}
break;
case 'ss':
if (proxy.plugin === 'shadow-tls') {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with shadow-tls`,
);
} else {
list.push(ssParser(proxy));
}
break;
// case 'ssr':
// list.push(ssrParser(proxy));
// break;
case 'vmess':
if (
!proxy.network ||
['ws', 'grpc', 'h2', 'http'].includes(
proxy.network,
)
) {
list.push(vmessParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with network ${proxy.network}`,
);
}
break;
case 'vless':
if (
!proxy.flow ||
['xtls-rprx-vision'].includes(proxy.flow)
) {
list.push(vlessParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
);
}
break;
case 'trojan':
if (!proxy.flow) {
list.push(trojanParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
);
}
break;
case 'hysteria':
list.push(hysteriaParser(proxy));
break;
case 'hysteria2':
list.push(hysteria2Parser(proxy));
break;
case 'tuic':
if (!proxy.token || proxy.token.length === 0) {
list.push(tuic5Parser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: TUIC v4`,
);
}
break;
case 'wireguard':
list.push(wireguardParser(proxy));
break;
default:
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type}`,
);
}
} catch (e) {
// console.log(e);
$.error(e.message ?? e);
}
});
return type === 'internal' ? list : JSON.stringify(list, null, 2);
};
return { type, produce };
}

View File

@@ -2,7 +2,12 @@ import { deleteByName, findByName, updateByName } from '@/utils/database';
import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
import {
RequestInvalidError,
ResourceNotFoundError,
InternalServerError,
} from '@/restful/errors';
import { produceArtifact } from '@/restful/sync';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
@@ -12,7 +17,10 @@ export default function register($app) {
.patch(updateFile)
.delete(deleteFile);
$app.route('/api/wholeFile/:name').get(getWholeFile);
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
$app.route('/api/wholeFiles').get(getAllWholeFiles);
}
// file API
@@ -37,13 +45,85 @@ function createFile(req, res) {
success(res, file, 201);
}
function getFile(req, res) {
async function getFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在下载文件:${name}`);
let { url, ua, content, mergeSources, ignoreFailedRemoteFile } = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程文件 URL: ${url}`);
}
if (ua) {
ua = decodeURIComponent(ua);
$.info(`指定远程文件 User-Agent: ${ua}`);
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地文件: ${content}`);
}
if (mergeSources) {
mergeSources = decodeURIComponent(mergeSources);
$.info(`指定合并来源: ${mergeSources}`);
}
if (ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '') {
ignoreFailedRemoteFile = decodeURIComponent(ignoreFailedRemoteFile);
$.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
}
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
try {
const output = await produceArtifact({
type: 'file',
name,
url,
ua,
content,
mergeSources,
ignoreFailedRemoteFile,
});
res.set('Content-Type', 'text/plain; charset=utf-8').send(
output ?? '',
);
} catch (err) {
$.notify(
`🌍 Sub-Store 下载文件失败`,
`❌ 无法下载文件:${name}`,
`🤔 原因:${err.message ?? err}`,
);
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
'INTERNAL_SERVER_ERROR',
`Failed to download file: ${name}`,
`Reason: ${err.message ?? err}`,
),
);
}
} else {
$.notify(`🌍 Sub-Store 下载文件失败`, `❌ 未找到文件:${name}`);
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`File ${name} does not exist!`,
),
404,
);
}
}
function getWholeFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
res.status(200).json(file.content);
success(res, file);
} else {
failed(
res,
@@ -102,6 +182,11 @@ function getAllFiles(req, res) {
);
}
function getAllWholeFiles(req, res) {
const allFiles = $.read(FILES_KEY);
success(res, allFiles);
}
function replaceFile(req, res) {
const allFiles = req.body;
$.write(allFiles, FILES_KEY);

View File

@@ -47,7 +47,9 @@ function getModule(req, res) {
const allModules = $.read(MODULES_KEY);
const module = findByName(allModules, name);
if (module) {
res.status(200).json(module.content);
res.set('Content-Type', 'text/plain; charset=utf-8').send(
module.content,
);
} else {
failed(
res,

View File

@@ -22,10 +22,10 @@ async function getNodeInfo(req, res) {
const info = await $http
.get({
url: `http://ip-api.com/json/${encodeURIComponent(
proxy.server
`${proxy.server}`
.trim()
.replace(/^\[/, '')
.replace(/\]$/, '')
.replace(/\]$/, ''),
)}?lang=${lang}`,
headers: {
'User-Agent':

View File

@@ -9,6 +9,85 @@ import $ from '@/core/app';
export default function register($app) {
$app.post('/api/preview/sub', compareSub);
$app.post('/api/preview/collection', compareCollection);
$app.post('/api/preview/file', previewFile);
}
async function previewFile(req, res) {
try {
const file = req.body;
let content;
if (
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
content = file.content;
} else {
const errors = {};
content = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
if (
!file.ignoreFailedRemoteFile &&
Object.keys(errors).length > 0
) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (file.mergeSources === 'localFirst') {
content.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
content.push(file.content);
}
}
// parse proxies
const files = (Array.isArray(content) ? content : [content]).flat();
const filesContent = files
.filter((i) => i != null && i !== '')
.join('\n');
// apply processors
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent },
file.process,
)
: filesContent;
// produce
success(res, {
original: filesContent,
processed: processed?.$content ?? '',
});
} catch (err) {
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview file`,
`Reason: ${err.message ?? err}`,
),
);
}
}
async function compareSub(req, res) {

View File

@@ -1,4 +1,9 @@
import { ARTIFACTS_KEY, COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
import {
ARTIFACTS_KEY,
COLLECTIONS_KEY,
SUBS_KEY,
FILES_KEY,
} from '@/constants';
import $ from '@/core/app';
import { success } from '@/restful/response';
@@ -6,6 +11,7 @@ export default function register($app) {
$app.post('/api/sort/subs', sortSubs);
$app.post('/api/sort/collections', sortCollections);
$app.post('/api/sort/artifacts', sortArtifacts);
$app.post('/api/sort/files', sortFiles);
}
function sortSubs(req, res) {
@@ -33,3 +39,11 @@ function sortArtifacts(req, res) {
$.write(allArtifacts, ARTIFACTS_KEY);
success(res, allArtifacts);
}
function sortFiles(req, res) {
const orders = req.body;
const allFiles = $.read(FILES_KEY);
allFiles.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allFiles, FILES_KEY);
success(res, allFiles);
}

View File

@@ -4,6 +4,7 @@ import {
COLLECTIONS_KEY,
RULES_KEY,
SUBS_KEY,
FILES_KEY,
} from '@/constants';
import { failed, success } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
@@ -31,6 +32,7 @@ async function produceArtifact({
content,
mergeSources,
ignoreFailedRemoteSub,
ignoreFailedRemoteFile,
}) {
platform = platform || 'JSON';
@@ -327,6 +329,109 @@ async function produceArtifact({
]);
// produce output
return RuleUtils.produce(rules, platform);
} else if (type === 'file') {
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (!file) throw new Error(`找不到文件 ${name}`);
let raw;
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content;
} else if (url) {
const errors = {};
raw = await Promise.all(
url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (mergeSources === 'localFirst') {
raw.unshift(content);
} else if (mergeSources === 'remoteFirst') {
raw.push(content);
}
} else if (
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
raw = file.content;
} else {
const errors = {};
raw = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (file.mergeSources === 'localFirst') {
raw.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
raw.push(file.content);
}
}
const files = (Array.isArray(raw) ? raw : [raw]).flat();
const filesContent = files
.filter((i) => i != null && i !== '')
.join('\n');
// apply processors
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent },
file.process,
)
: filesContent;
return processed?.$content ?? '';
}
}
@@ -386,10 +491,12 @@ async function syncAllArtifacts(_, res) {
async function syncArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`开始同步远程配置 ${name}...`);
const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = findByName(allArtifacts, name);
if (!artifact) {
$.error(`找不到远程配置 ${name}`);
failed(
res,
new ResourceNotFoundError(
@@ -428,6 +535,7 @@ async function syncArtifact(req, res) {
$.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact);
} catch (err) {
$.error(`远程配置 ${artifact.name} 发生错误: ${err}`);
failed(
res,
new InternalServerError(

View File

@@ -7,7 +7,7 @@ import $ from '@/core/app';
const tasks = new Map();
export default async function download(url, ua) {
export default async function download(url, ua, timeout) {
let $arguments = {};
const rawArgs = url.split('#');
if (rawArgs.length > 1) {
@@ -45,17 +45,19 @@ export default async function download(url, ua) {
}
const { isNode } = ENV();
const { defaultUserAgent } = $.read(SETTINGS_KEY);
ua = ua || defaultUserAgent || 'clash.meta';
const id = hex_md5(ua + url);
const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url);
if (!isNode && tasks.has(id)) {
return tasks.get(id);
}
const http = HTTP({
headers: {
'User-Agent': ua,
'User-Agent': userAgent,
},
timeout: requestTimeout,
});
const result = new Promise((resolve, reject) => {
@@ -64,7 +66,9 @@ export default async function download(url, ua) {
if (!$arguments?.noCache && cached) {
resolve(cached);
} else {
$.info(`Downloading...\nUser-Agent: ${ua}\nURL: ${url}`);
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
);
http.get(url)
.then((resp) => {
const body = resp.body;

View File

@@ -1,6 +1,14 @@
import { SETTINGS_KEY } from '@/constants';
import { HTTP } from '@/vendor/open-api';
import $ from '@/core/app';
export async function getFlowHeaders(url) {
export async function getFlowHeaders(url, ua, timeout) {
const { defaultFlowUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const userAgent =
ua ||
defaultFlowUserAgent ||
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
const requestTimeout = timeout || defaultTimeout;
const http = HTTP();
const { headers } = await http.get({
url: url
@@ -8,8 +16,9 @@ export async function getFlowHeaders(url) {
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
'User-Agent': userAgent,
},
timeout: requestTimeout,
});
const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k),

View File

@@ -32,6 +32,8 @@ export function getPlatformFromHeaders(headers) {
return 'Clash';
} else if (ua.indexOf('v2ray') !== -1) {
return 'V2Ray';
} else if (ua.indexOf('sing-box') !== -1) {
return 'sing-box';
} else {
return 'JSON';
}

View File

@@ -191,6 +191,32 @@ export class OpenAPI {
(openURL ? `\n点击跳转: ${openURL}` : '') +
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
let push = eval('process.env.SUB_STORE_PUSH_SERVICE');
if (push) {
const url = push
.replace(
'[推送标题]',
encodeURIComponent(title || 'Sub-Store'),
)
.replace(
'[推送内容]',
encodeURIComponent(
[subtitle, content_].map((i) => i).join('\n'),
),
);
const $http = HTTP();
$http
.get({ url })
.then((resp) => {
console.log(
`[Push Service] URL: ${url}\nRES: ${resp.statusCode} ${resp.body}`,
);
})
.catch((e) => {
console.log(`[Push Service] URL: ${url}\nERROR: ${e}`);
});
}
}
}