Compare commits

..

37 Commits

Author SHA1 Message Date
xream
adc95bba60 feat: Surge 全协议支持 Shadow TLS, 部分协议增加 TLS Fingerprint 支持 2023-10-24 07:26:34 +08:00
xream
fab3644b86 feat: 支持 Shadowrocket Hysteria2 URI 格式输入 2023-10-18 23:48:45 +08:00
xream
c21ce0be16 fix: Surge Hysteria2 输出重复添加 tfo 的 bug 2023-10-18 05:09:10 +08:00
xream
fa65eb1850 feat: Base64 订阅关键词增加 VLESS 和 Hysteria2 2023-10-16 22:11:26 +08:00
xream
79c9b89c5f feat: Stash Hysteria2 2023-10-15 15:55:19 +08:00
xream
fca508ba8a feat: Surge Hysteria2 输入/输出增加 ecn 参数 2023-10-12 22:15:10 +08:00
xream
21b531a44d feat: Surge TUIC 输入/输出增加 ecn 参数 2023-10-12 22:09:58 +08:00
xream
4e5b46a43d feat: Surge Hysteria2 输出增加 download-bandwidth(若有值但解析失败则为 0) 2023-10-12 00:39:10 +08:00
xream
bf81ca4acf feat: 输入增加 Hysteria2 URI 支持; Surge Hysteria2 输出增加 fingerprint 2023-10-11 23:35:42 +08:00
xream
e7c0b23222 feat: Surge 输入输出增加 Hysteria2 2023-10-09 23:42:22 +08:00
xream
40fb0fd7f3 feat: 兼容更多 VMess URI 格式 2023-10-09 17:36:11 +08:00
xream
b061fca356 feat: Surge Snell 输入支持解析 reuse 字段 2023-10-08 16:42:35 +08:00
xream
d3c6c99b0a feat: proxy 增加 subName(订阅名), collectionName(组合订阅名); 脚本增加第三个参数 env(包含订阅/组合订阅/环境/版本等信息) 2023-10-08 13:21:22 +08:00
xream
3fbc280e28 [+] 重复节点通知中增加订阅名称和重复节点名称 2023-10-02 16:21:08 +08:00
xream
9e3e4c6e46 [+] Surge 输出支持 underlying-proxy; VMess/Vless URI 支持 gRPC mode(默认为 gun) 2023-10-01 22:05:51 +08:00
xream
bc0dd4b175 feat: 支持 hysteria2 2023-09-22 14:43:43 +08:00
xream
7603fac036 fix: 修复部分环境无 clearTimeout 的问题 2023-09-18 20:09:03 +08:00
K
9acc161684 fix @ 2023-09-15 18:52:21 +08:00
xream
024582a99d fix: 修复 sub-store-0 路由 2023-09-15 18:42:53 +08:00
xream
1d31a80b9f fix: 修复文件和模块命名/重复添加的逻辑 2023-09-15 10:08:36 +08:00
xream
b2d0276836 feat: 文件和模块接口获取原始内容; 文件列表不返回原始内容 2023-09-14 18:51:23 +08:00
xream
3211fbf357 feat: 模块接口; 脚本参数支持 JSON 和 URL编码 2023-09-14 17:34:24 +08:00
xream
33a17c2d66 feat: 实验性支持本地脚本复用 2023-09-14 08:56:33 +08:00
xream
2c89a0ddbd feat: 支持 Clash VLESS 输出(与 Clash.Meta 的区别为: 无 XTLS 2023-09-11 02:35:36 +08:00
xream
939022e5a3 fix: 修复了 Clash.Meta 输出 VLESS 时 内部字段 sni 未作用到 servername 的问题 2023-09-09 14:03:40 +08:00
xream
59bca5670d fix: 预览时脚本下载报错导致的崩溃 2023-09-07 23:17:36 +08:00
Peng-YM
07b38cf971 release: backend version 2.14.49 2023-09-04 23:16:52 +08:00
Peng-YM
28186f596f feat: added the ability to change the base path for the data files
before starting node, use the command `export SUB_STORE_DATA_BASE_PATH="<YOUR_PATH>"`
2023-09-04 23:16:13 +08:00
xream
ea31b1d0ec fix: 排序接口修正为使用 name 排序 2023-09-04 21:31:55 +08:00
xream
77191f9caa feat: 为 Gist 备份还原增加基础校验逻辑 2023-09-04 17:06:37 +08:00
xream
07a270963e feat: 支持 Surge WireGuard 的输入和输出(由于 Surge 配置的特殊性, 仅支持 同进同出) 支持的字段格式: HK WARP = wireguard, section-name=Cloudflare, no-error-alert=true, underlying-proxy=HK, test-url=http://1.0.0.1/generate_204, ip-version=v4-only 2023-09-01 02:44:43 +08:00
xream
f1e1d50a2c fix: 暂时将后端上传限制放宽到 1mb 2023-09-01 02:07:24 +08:00
Peng-YM
a65cd1f1c9 Update README.md 2023-08-31 16:41:50 +08:00
xream
5b0e2e1ef2 docs: config 2023-08-30 22:52:05 +08:00
xream
b29266ac57 chore: sync to GitLab 2023-08-30 16:19:17 +08:00
xream
336ddd6706 chore: 调整部分日志 2023-08-29 13:52:02 +08:00
xream
25ec219659 docs: 更新 Surge SSR 协议说明; 模块说明页增加更新说明的链接 2023-08-29 01:59:01 +08:00
34 changed files with 1110 additions and 194 deletions

View File

@@ -64,3 +64,8 @@ jobs:
./backend/dist/sub-store-parser.loon.min.js ./backend/dist/sub-store-parser.loon.min.js
./backend/dist/cron-sync-artifacts.min.js ./backend/dist/cron-sync-artifacts.min.js
./backend/dist/sub-store.bundle.js ./backend/dist/sub-store.bundle.js
- name: Sync to GitLab
env:
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
run: |
curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline

View File

@@ -10,7 +10,7 @@
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket. Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
</p> </p>
[![Build](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/Peng-YM/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/Peng-YM/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/Peng-YM/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/Peng-YM/Sub-Store) [![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM) [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM)
@@ -30,13 +30,14 @@ Core functionalities:
- [x] SSR URI - [x] SSR URI
- [x] SSD URI - [x] SSD URI
- [x] V2RayN URI - [x] V2RayN URI
- [x] Hysteria2 URI
- [x] QX (SS, SSR, VMess, Trojan, HTTP) - [x] QX (SS, SSR, VMess, Trojan, HTTP)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS) - [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS)
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell) - [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria) - [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, Hysteria2)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria) - [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria, Hysteria2)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria) - [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell) - [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard)
### Supported Target Platforms ### Supported Target Platforms
@@ -99,6 +100,11 @@ This project is under the GPL V3 LICENSE.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date)
## Acknowledgements ## Acknowledgements
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work! - Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!

View File

@@ -9,7 +9,7 @@
* @updated: <%= updated %> * @updated: <%= updated %>
* @version: <%= pkg.version %> * @version: <%= pkg.version %>
* @author: Peng-YM * @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store * @github: https://github.com/sub-store-org/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46 * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/ */

View File

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

View File

@@ -2,6 +2,8 @@ export const SCHEMA_VERSION_KEY = 'schemaVersion';
export const SETTINGS_KEY = 'settings'; export const SETTINGS_KEY = 'settings';
export const SUBS_KEY = 'subs'; export const SUBS_KEY = 'subs';
export const COLLECTIONS_KEY = 'collections'; export const COLLECTIONS_KEY = 'collections';
export const FILES_KEY = 'files';
export const MODULES_KEY = 'modules';
export const ARTIFACTS_KEY = 'artifacts'; export const ARTIFACTS_KEY = 'artifacts';
export const RULES_KEY = 'rules'; export const RULES_KEY = 'rules';
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup'; export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';

View File

@@ -63,11 +63,11 @@ function parse(raw) {
return proxies; return proxies;
} }
async function process(proxies, operators = [], targetPlatform) { async function process(proxies, operators = [], targetPlatform, source) {
for (const item of operators) { for (const item of operators) {
// process script // process script
let script; let script;
const $arguments = {}; let $arguments = {};
if (item.type.indexOf('Script') !== -1) { if (item.type.indexOf('Script') !== -1) {
const { mode, content } = item.args; const { mode, content } = item.args;
if (mode === 'link') { if (mode === 'link') {
@@ -75,10 +75,19 @@ async function process(proxies, operators = [], targetPlatform) {
// extract link arguments // extract link arguments
const rawArgs = url.split('#'); const rawArgs = url.split('#');
if (rawArgs.length > 1) { if (rawArgs.length > 1) {
for (const pair of rawArgs[1].split('&')) { try {
const key = pair.split('=')[0]; // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
const value = pair.split('=')[1] || true; $arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
$arguments[key] = value; } catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
} }
} }
@@ -113,6 +122,7 @@ async function process(proxies, operators = [], targetPlatform) {
script, script,
targetPlatform, targetPlatform,
$arguments, $arguments,
source,
); );
} else { } else {
processor = PROXY_PROCESSORS[item.type](item.args || {}); processor = PROXY_PROCESSORS[item.type](item.args || {});
@@ -199,7 +209,7 @@ function lastParse(proxy) {
delete proxy.network; delete proxy.network;
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
proxy.tls = true; proxy.tls = true;
} }
if (proxy.tls && !proxy.sni) { if (proxy.tls && !proxy.sni) {

View File

@@ -215,7 +215,6 @@ function URI_VMess() {
// V2rayN URI format // V2rayN URI format
params = JSON.parse(content); params = JSON.parse(content);
} catch (e) { } catch (e) {
// console.error(e);
// Shadowrocket URI format // Shadowrocket URI format
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line); let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line);
@@ -241,7 +240,7 @@ function URI_VMess() {
params.add = server; params.add = server;
} }
const proxy = { const proxy = {
name: params.ps ?? params.remark, name: params.ps ?? params.remarks,
type: 'vmess', type: 'vmess',
server: params.add, server: params.add,
port: parseInt(getIfPresent(params.port), 10), port: parseInt(getIfPresent(params.port), 10),
@@ -268,9 +267,19 @@ function URI_VMess() {
params.obfs === 'http' params.obfs === 'http'
) { ) {
proxy.network = 'http'; proxy.network = 'http';
} else if (['grpc'].includes(params.net)) {
proxy.network = 'grpc';
} }
if (proxy.network) { if (proxy.network) {
let transportHost = params.host ?? params.obfsParam; let transportHost = params.host ?? params.obfsParam;
try {
const parsedObfs = JSON.parse(transportHost);
const parsedHost = parsedObfs?.Host;
if (parsedHost) {
transportHost = parsedHost;
}
// eslint-disable-next-line no-empty
} catch (e) {}
let transportPath = params.path; let transportPath = params.path;
if (proxy.network === 'http') { if (proxy.network === 'http') {
@@ -286,10 +295,17 @@ function URI_VMess() {
} }
} }
if (transportPath || transportHost) { if (transportPath || transportHost) {
proxy[`${proxy.network}-opts`] = { if (['grpc'].includes(proxy.network)) {
path: getIfNotBlank(transportPath), proxy[`${proxy.network}-opts`] = {
headers: { Host: getIfNotBlank(transportHost) }, 'grpc-service-name': getIfNotBlank(transportPath),
}; '_grpc-type': getIfNotBlank(params.type),
};
} else {
proxy[`${proxy.network}-opts`] = {
path: getIfNotBlank(transportPath),
headers: { Host: getIfNotBlank(transportHost) },
};
}
} else { } else {
delete proxy.network; delete proxy.network;
} }
@@ -366,6 +382,10 @@ function URI_VLESS() {
if (params.serviceName) { if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName; opts[`${proxy.network}-service-name`] = params.serviceName;
} }
// https://github.com/XTLS/Xray-core/issues/91
if (['grpc'].includes(proxy.network)) {
opts['_grpc-type'] = params.mode || 'gun';
}
if (Object.keys(opts).length > 0) { if (Object.keys(opts).length > 0) {
proxy[`${proxy.network}-opts`] = opts; proxy[`${proxy.network}-opts`] = opts;
} }
@@ -384,6 +404,56 @@ function URI_VLESS() {
}; };
return { name, test, parse }; return { name, test, parse };
} }
function URI_Hysteria2() {
const name = 'URI Hysteria2 Parser';
const test = (line) => {
return /^hysteria2:\/\//.test(line);
};
const parse = (line) => {
line = line.split('hysteria2://')[1];
// eslint-disable-next-line no-unused-vars
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}`;
const proxy = {
type: 'hysteria2',
name,
server,
port,
password,
};
const params = {};
for (const addon of addons.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
params[key] = value;
}
proxy.sni = params.sni;
if (!proxy.sni && params.peer) {
proxy.sni = params.peer;
}
if (params.obfs && params.obfs !== 'none') {
proxy.obfs = params.obfs;
}
proxy['obfs-password'] = params['obfs-password'];
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
proxy['tls-fingerprint'] = params.pinSHA256;
return proxy;
};
return { name, test, parse };
}
// Trojan URI format // Trojan URI format
function URI_Trojan() { function URI_Trojan() {
@@ -424,6 +494,7 @@ function Clash_All() {
'tuic', 'tuic',
'vless', 'vless',
'hysteria', 'hysteria',
'hysteria2',
'wireguard', 'wireguard',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
@@ -740,7 +811,7 @@ function Surge_Socks5() {
function Surge_Snell() { function Surge_Snell() {
const name = 'Surge Snell Parser'; const name = 'Surge Snell Parser';
const test = (line) => { const test = (line) => {
return /^.*=\s*snell?/.test(line.split(',')[0]); return /^.*=\s*snell/.test(line.split(',')[0]);
}; };
const parse = (line) => getSurgeParser().parse(line); const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse }; return { name, test, parse };
@@ -749,7 +820,24 @@ function Surge_Snell() {
function Surge_Tuic() { function Surge_Tuic() {
const name = 'Surge Tuic Parser'; const name = 'Surge Tuic Parser';
const test = (line) => { const test = (line) => {
return /^.*=\s*tuic(-v5)??/.test(line.split(',')[0]); return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_WireGuard() {
const name = 'Surge WireGuard Parser';
const test = (line) => {
return /^.*=\s*wireguard/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_Hysteria2() {
const name = 'Surge Hysteria2 Parser';
const test = (line) => {
return /^.*=\s*hysteria2/.test(line.split(',')[0]);
}; };
const parse = (line) => getSurgeParser().parse(line); const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse }; return { name, test, parse };
@@ -760,6 +848,7 @@ export default [
URI_SSR(), URI_SSR(),
URI_VMess(), URI_VMess(),
URI_VLESS(), URI_VLESS(),
URI_Hysteria2(),
URI_Trojan(), URI_Trojan(),
Clash_All(), Clash_All(),
Surge_SS(), Surge_SS(),
@@ -768,6 +857,8 @@ export default [
Surge_Http(), Surge_Http(),
Surge_Snell(), Surge_Snell(),
Surge_Tuic(), Surge_Tuic(),
Surge_WireGuard(),
Surge_Hysteria2(),
Surge_Socks5(), Surge_Socks5(),
Loon_SS(), Loon_SS(),
Loon_SSR(), Loon_SSR(),

View File

@@ -32,11 +32,11 @@ const grammars = String.raw`
} }
} }
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) { start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
return proxy; return proxy;
} }
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -46,7 +46,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
} }
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -56,18 +56,18 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
} }
handleWebsocket(); handleWebsocket();
} }
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
} }
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
} }
http = tag equals "http" address (username password)? (fast_open/others)* { http = tag equals "http" address (username password)? (fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "http"; proxy.type = "http";
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "snell"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -76,17 +76,23 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.path", obfs.path); $set(proxy, "obfs-opts.path", obfs.path);
} }
} }
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
} }
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
} }
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* { wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "wireguard-surge";
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "hysteria2";
}
socks5 = tag equals "socks5" address (username password)? (fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
proxy.tls = true; proxy.tls = true;
} }
@@ -185,8 +191,18 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; } tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); } token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); } alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); } uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }

View File

@@ -30,11 +30,11 @@
} }
} }
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) { start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
return proxy; return proxy;
} }
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -44,7 +44,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
} }
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -54,18 +54,18 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
} }
handleWebsocket(); handleWebsocket();
} }
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
} }
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
} }
http = tag equals "http" address (username password)? (fast_open/others)* { http = tag equals "http" address (username password)? (fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "http"; proxy.type = "http";
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "snell"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -74,17 +74,23 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.path", obfs.path); $set(proxy, "obfs-opts.path", obfs.path);
} }
} }
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
} }
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
} }
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* { wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "wireguard-surge";
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "hysteria2";
}
socks5 = tag equals "socks5" address (username password)? (fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
proxy.tls = true; proxy.tls = true;
} }
@@ -183,8 +189,18 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; } tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); } token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); } alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); } uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }

View File

@@ -13,13 +13,15 @@ function Base64Encoded() {
const name = 'Base64 Pre-processor'; const name = 'Base64 Pre-processor';
const keys = [ const keys = [
'dm1lc3M', 'dm1lc3M', // vmess
'c3NyOi8v', 'c3NyOi8v', // ssr://
'dHJvamFu', 'dHJvamFu', // trojan
'c3M6Ly', 'c3M6Ly', // ss:/
'c3NkOi8v', 'c3NkOi8v', // ssd://
'c2hhZG93', 'c2hhZG93', // shadow
'aHR0c', 'aHR0c', // htt
'dmxlc3M=', // vless
'aHlzdGVyaWEy', // hysteria2
]; ];
const test = function (raw) { const test = function (raw) {

View File

@@ -7,6 +7,7 @@ import lodash from 'lodash';
import $ from '@/core/app'; import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils'; import { ProxyUtils } from '@/core/proxy-utils';
import env from '@/utils/env';
/** /**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows: The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
@@ -294,7 +295,7 @@ function RegexDeleteOperator(regex) {
1. This function name should be `operator`! 1. This function name should be `operator`!
2. Always declare variables before using them! 2. Always declare variables before using them!
*/ */
function ScriptOperator(script, targetPlatform, $arguments) { function ScriptOperator(script, targetPlatform, $arguments, source) {
return { return {
name: 'Script Operator', name: 'Script Operator',
func: async (proxies) => { func: async (proxies) => {
@@ -305,7 +306,7 @@ function ScriptOperator(script, targetPlatform, $arguments) {
script, script,
$arguments, $arguments,
); );
output = operator(proxies, targetPlatform); output = operator(proxies, targetPlatform, { source, ...env });
})(); })();
return output; return output;
}, },
@@ -411,7 +412,6 @@ const DOMAIN_RESOLVERS = {
}, },
}); });
const answers = resp.body.split(';').map((i) => i.split(',')[0]); const answers = resp.body.split(';').map((i) => i.split(',')[0]);
console.log(`answers`, answers);
if (answers.length === 0) { if (answers.length === 0) {
throw new Error('No answers'); throw new Error('No answers');
} }
@@ -563,7 +563,7 @@ function TypeFilter(types) {
1. This function name should be `filter`! 1. This function name should be `filter`!
2. Always declare variables before using them! 2. Always declare variables before using them!
*/ */
function ScriptFilter(script, targetPlatform, $arguments) { function ScriptFilter(script, targetPlatform, $arguments, source) {
return { return {
name: 'Script Filter', name: 'Script Filter',
func: async (proxies) => { func: async (proxies) => {
@@ -574,7 +574,7 @@ function ScriptFilter(script, targetPlatform, $arguments) {
script, script,
$arguments, $arguments,
); );
output = filter(proxies, targetPlatform); output = filter(proxies, targetPlatform, { source, ...env });
})(); })();
return output; return output;
}, },

View File

@@ -3,6 +3,9 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Clash_Producer() { export default function Clash_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies) => { const produce = (proxies) => {
// VLESS XTLS is not supported by Clash
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
// github.com/Dreamacro/clash/pull/2891/files
// filter unsupported proxies // filter unsupported proxies
proxies = proxies.filter((proxy) => { proxies = proxies.filter((proxy) => {
if ( if (
@@ -10,17 +13,17 @@ export default function Clash_Producer() {
'ss', 'ss',
'ssr', 'ssr',
'vmess', 'vmess',
'vless',
'socks', 'socks',
'http', 'http',
'snell', 'snell',
'trojan', 'trojan',
'wireguard', 'wireguard',
].includes(proxy.type) ].includes(proxy.type) ||
) { (proxy.type === 'snell' && String(proxy.version) === '4') ||
return false; (proxy.type === 'vless' &&
} else if ( (typeof proxy.flow !== 'undefined' ||
proxy.type === 'snell' && proxy['reality-opts']))
String(proxy.version) === '4'
) { ) {
return false; return false;
} }
@@ -87,10 +90,26 @@ export default function Clash_Producer() {
proxy['http-opts'].headers.Host = [httpHost]; proxy['http-opts'].headers.Host = [httpHost];
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -81,6 +81,11 @@ export default function ClashMeta_Producer() {
proxy['preshared-key'] = proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key']; proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
} }
if ( if (
@@ -103,11 +108,26 @@ export default function ClashMeta_Producer() {
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -108,11 +108,26 @@ export default function ShadowRocket_Producer() {
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -19,6 +19,8 @@ export default function Stash_Producer() {
'tuic', 'tuic',
'vless', 'vless',
'wireguard', 'wireguard',
'hysteria',
'hysteria2',
].includes(proxy.type) || ].includes(proxy.type) ||
(proxy.type === 'snell' && (proxy.type === 'snell' &&
String(proxy.version) === '4') || String(proxy.version) === '4') ||
@@ -67,6 +69,7 @@ export default function Stash_Producer() {
!isPresent(proxy, 'fast-open') !isPresent(proxy, 'fast-open')
) { ) {
proxy['fast-open'] = proxy.tfo; proxy['fast-open'] = proxy.tfo;
delete proxy.tfo;
} }
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197 // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
if ( if (
@@ -86,6 +89,50 @@ export default function Stash_Producer() {
!isPresent(proxy, 'fast-open') !isPresent(proxy, 'fast-open')
) { ) {
proxy['fast-open'] = proxy.tfo; proxy['fast-open'] = proxy.tfo;
delete proxy.tfo;
}
if (
isPresent(proxy, 'down') &&
!isPresent(proxy, 'down-speed')
) {
proxy['down-speed'] = proxy.down;
delete proxy.down;
}
if (
isPresent(proxy, 'up') &&
!isPresent(proxy, 'up-speed')
) {
proxy['up-speed'] = proxy.up;
delete proxy.up;
}
} else if (proxy.type === 'hysteria2') {
if (
isPresent(proxy, 'password') &&
!isPresent(proxy, 'auth')
) {
proxy.auth = proxy.password;
delete proxy.password;
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
delete proxy.tfo;
}
if (
isPresent(proxy, 'down') &&
!isPresent(proxy, 'down-speed')
) {
proxy['down-speed'] = proxy.down;
delete proxy.down;
}
if (
isPresent(proxy, 'up') &&
!isPresent(proxy, 'up-speed')
) {
proxy['up-speed'] = proxy.up;
delete proxy.up;
} }
} else if (proxy.type === 'wireguard') { } else if (proxy.type === 'wireguard') {
proxy.keepalive = proxy.keepalive =
@@ -120,10 +167,25 @@ export default function Stash_Producer() {
proxy['http-opts'].headers.Host = [httpHost]; proxy['http-opts'].headers.Host = [httpHost];
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -29,6 +29,10 @@ export default function Surge_Producer() {
return snell(proxy); return snell(proxy);
case 'tuic': case 'tuic':
return tuic(proxy); return tuic(proxy);
case 'wireguard-surge':
return wireguard(proxy);
case 'hysteria2':
return hysteria2(proxy);
} }
throw new Error( throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -69,6 +73,27 @@ function shadowsocks(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -105,6 +130,27 @@ function trojan(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -148,6 +194,27 @@ function vmess(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -180,6 +247,27 @@ function http(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -214,6 +302,27 @@ function socks5(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -243,6 +352,27 @@ function snell(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
// reuse // reuse
result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse'); result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
@@ -279,13 +409,163 @@ function tuic(proxy) {
'skip-cert-verify', 'skip-cert-verify',
); );
// tls fingerprint
result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tfo // tfo
result.appendIfPresent(`,tfo=${proxy['fast-open']}`, 'fast-open'); if (isPresent(proxy, 'tfo')) {
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); result.append(`,tfo=${proxy['tfo']}`);
} else if (isPresent(proxy, 'fast-open')) {
result.append(`,tfo=${proxy['fast-open']}`);
}
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString();
}
function wireguard(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=wireguard`);
result.appendIfPresent(
`,section-name=${proxy['section-name']}`,
'section-name',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) {
throw new Error(`obfs is unsupported`);
}
const result = new Result(proxy);
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tfo
if (isPresent(proxy, 'tfo')) {
result.append(`,tfo=${proxy['tfo']}`);
} else if (isPresent(proxy, 'fast-open')) {
result.append(`,tfo=${proxy['fast-open']}`);
}
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
// download-bandwidth
result.appendIfPresent(
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
'down',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString(); return result.toString();
} }

View File

@@ -91,6 +91,16 @@ export default function URI_Producer() {
? vmessTransportHost[0] ? vmessTransportHost[0]
: vmessTransportHost; : vmessTransportHost;
} }
if (['grpc'].includes(proxy.network)) {
result.path =
proxy[`${proxy.network}-opts`]?.[
'grpc-service-name'
];
// https://github.com/XTLS/Xray-core/issues/91
result.type =
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun';
}
} }
result = 'vmess://' + Base64.encode(JSON.stringify(result)); result = 'vmess://' + Base64.encode(JSON.stringify(result));
break; break;
@@ -141,6 +151,12 @@ export default function URI_Producer() {
let vlessTransport = `&type=${encodeURIComponent( let vlessTransport = `&type=${encodeURIComponent(
proxy.network, proxy.network,
)}`; )}`;
if (['grpc'].includes(proxy.network)) {
// https://github.com/XTLS/Xray-core/issues/91
vlessTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
)}`;
}
let vlessTransportServiceName = let vlessTransportServiceName =
proxy[`${proxy.network}-opts`]?.[ proxy[`${proxy.network}-opts`]?.[

View File

@@ -47,7 +47,7 @@ function AllRuleParser() {
} }
if (!matched) throw new Error('Invalid rule type: ' + rawType); if (!matched) throw new Error('Invalid rule type: ' + rawType);
} catch (e) { } catch (e) {
console.error(`Failed to parse line: ${line}\n Reason: ${e}`); console.log(`Failed to parse line: ${line}\n Reason: ${e}`);
} }
} }
return result; return result;

View File

@@ -7,7 +7,7 @@
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ * ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
* Advanced Subscription Manager for QX, Loon, Surge and Clash. * Advanced Subscription Manager for QX, Loon, Surge and Clash.
* @author: Peng-YM * @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store * @github: https://github.com/sub-store-org/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46 * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/ */
import { version } from '../package.json'; import { version } from '../package.json';

View File

@@ -20,6 +20,8 @@ import registerArtifactRoutes from '@/restful/artifacts';
import registerSettingRoutes from '@/restful/settings'; import registerSettingRoutes from '@/restful/settings';
import registerMiscRoutes from '@/restful/miscs'; import registerMiscRoutes from '@/restful/miscs';
import registerSortRoutes from '@/restful/sort'; import registerSortRoutes from '@/restful/sort';
import registerFileRoutes from '@/restful/file';
import registerModuleRoutes from '@/restful/module';
migrate(); migrate();
serve(); serve();
@@ -30,6 +32,8 @@ function serve() {
// register routes // register routes
registerCollectionRoutes($app); registerCollectionRoutes($app);
registerSubscriptionRoutes($app); registerSubscriptionRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerArtifactRoutes($app); registerArtifactRoutes($app);
registerSettingRoutes($app); registerSettingRoutes($app);
registerSortRoutes($app); registerSortRoutes($app);

109
backend/src/restful/file.js Normal file
View File

@@ -0,0 +1,109 @@
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';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
$app.route('/api/file/:name')
.get(getFile)
.patch(updateFile)
.delete(deleteFile);
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
}
// file API
function createFile(req, res) {
const file = req.body;
file.name = `${file.name ?? Date.now()}`;
$.info(`正在创建文件:${file.name}`);
const allFiles = $.read(FILES_KEY);
if (findByName(allFiles, file.name)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_KEY',
req.body.name
? `已存在 name 为 ${file.name} 的文件`
: `无法同时创建相同的文件 可稍后重试`,
),
);
}
allFiles.push(file);
$.write(allFiles, FILES_KEY);
success(res, file, 201);
}
function getFile(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);
} else {
failed(
res,
new ResourceNotFoundError(
`FILE_NOT_FOUND`,
`File ${name} does not exist`,
404,
),
);
}
}
function updateFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let file = req.body;
const allFiles = $.read(FILES_KEY);
const oldFile = findByName(allFiles, name);
if (oldFile) {
const newFile = {
...oldFile,
...file,
};
$.info(`正在更新文件:${name}...`);
updateByName(allFiles, name, newFile);
$.write(allFiles, FILES_KEY);
success(res, newFile);
} else {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`File ${name} does not exist!`,
),
404,
);
}
}
function deleteFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除文件:${name}`);
let allFiles = $.read(FILES_KEY);
deleteByName(allFiles, name);
$.write(allFiles, FILES_KEY);
success(res);
}
function getAllFiles(req, res) {
const allFiles = $.read(FILES_KEY);
success(
res, // eslint-disable-next-line no-unused-vars
allFiles.map(({ content, ...rest }) => rest),
);
}
function replaceFile(req, res) {
const allFiles = req.body;
$.write(allFiles, FILES_KEY);
success(res);
}

View File

@@ -4,6 +4,8 @@ import $ from '@/core/app';
import registerSubscriptionRoutes from './subscriptions'; import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections'; import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts'; import registerArtifactRoutes from './artifacts';
import registerFileRoutes from './file';
import registerModuleRoutes from './module';
import registerSyncRoutes from './sync'; import registerSyncRoutes from './sync';
import registerDownloadRoutes from './download'; import registerDownloadRoutes from './download';
import registerSettingRoutes from './settings'; import registerSettingRoutes from './settings';
@@ -23,6 +25,8 @@ export default function serve() {
registerSortingRoutes($app); registerSortingRoutes($app);
registerSettingRoutes($app); registerSettingRoutes($app);
registerArtifactRoutes($app); registerArtifactRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerSyncRoutes($app); registerSyncRoutes($app);
registerNodeInfoRoutes($app); registerNodeInfoRoutes($app);
registerMiscRoutes($app); registerMiscRoutes($app);

View File

@@ -1,7 +1,6 @@
import $ from '@/core/app'; import $ from '@/core/app';
import { ENV } from '@/vendor/open-api'; import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import { version as substoreVersion } from '../../package.json';
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings'; import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import { import {
@@ -12,6 +11,7 @@ import {
import { InternalServerError, RequestInvalidError } from '@/restful/errors'; import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import Gist from '@/utils/gist'; import Gist from '@/utils/gist';
import migrate from '@/utils/migration'; import migrate from '@/utils/migration';
import env from '@/utils/env';
export default function register($app) { export default function register($app) {
// utils // utils
@@ -49,19 +49,7 @@ export default function register($app) {
} }
function getEnv(req, res) { function getEnv(req, res) {
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV(); success(res, env);
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
success(res, {
backend,
version: substoreVersion,
});
} }
async function refresh(_, res) { async function refresh(_, res) {
@@ -118,6 +106,23 @@ async function gistBackup(req, res) {
case 'download': case 'download':
$.info(`还原备份中...`); $.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME); content = await gist.download(GIST_BACKUP_FILE_NAME);
try {
if (
Object.keys(JSON.parse(content).settings).length ===
0
) {
throw new Error(
'备份文件应该至少包含 settings 字段',
);
}
} catch (err) {
$.error(
`Gist 备份文件校验失败, 无法还原\nReason: ${
err.message ?? err
}`,
);
throw new Error('Gist 备份文件校验失败, 无法还原');
}
// restore settings // restore settings
$.write(content, '#sub-store'); $.write(content, '#sub-store');
if ($.env.isNode) { if ($.env.isNode) {
@@ -125,8 +130,9 @@ async function gistBackup(req, res) {
$.cache = content; $.cache = content;
$.persistCache(); $.persistCache();
} }
// perform migration after restoring from gist $.info(`perform migration after restoring from gist...`);
migrate(); migrate();
$.info(`migration completed`);
$.info(`还原备份完成`); $.info(`还原备份完成`);
break; break;
} }

View File

@@ -0,0 +1,114 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { MODULES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
import { hex_md5 } from '@/vendor/md5';
export default function register($app) {
if (!$.read(MODULES_KEY)) $.write([], MODULES_KEY);
$app.route('/api/module/:name')
.get(getModule)
.patch(updateModule)
.delete(deleteModule);
$app.route('/api/modules')
.get(getAllModules)
.post(createModule)
.put(replaceModule);
}
// module API
function createModule(req, res) {
const module = req.body;
module.name = `${module.name ?? hex_md5(JSON.stringify(module))}`;
$.info(`正在创建模块:${module.name}`);
const allModules = $.read(MODULES_KEY);
if (findByName(allModules, module.name)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_KEY',
req.body.name
? `已存在 name 为 ${module.name} 的模块`
: `已存在相同的模块 请勿重复添加`,
),
);
}
allModules.push(module);
$.write(allModules, MODULES_KEY);
success(res, module, 201);
}
function getModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allModules = $.read(MODULES_KEY);
const module = findByName(allModules, name);
if (module) {
res.status(200).json(module.content);
} else {
failed(
res,
new ResourceNotFoundError(
`MODULE_NOT_FOUND`,
`Module ${name} does not exist`,
404,
),
);
}
}
function updateModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let module = req.body;
const allModules = $.read(MODULES_KEY);
const oldModule = findByName(allModules, name);
if (oldModule) {
const newModule = {
...oldModule,
...module,
};
$.info(`正在更新模块:${name}...`);
updateByName(allModules, name, newModule);
$.write(allModules, MODULES_KEY);
success(res, newModule);
} else {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`Module ${name} does not exist!`,
),
404,
);
}
}
function deleteModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除模块:${name}`);
let allModules = $.read(MODULES_KEY);
deleteByName(allModules, name);
$.write(allModules, MODULES_KEY);
success(res);
}
function getAllModules(req, res) {
const allModules = $.read(MODULES_KEY);
success(
res,
// eslint-disable-next-line no-unused-vars
allModules.map(({ content, ...rest }) => rest),
);
}
function replaceModule(req, res) {
const allModules = req.body;
$.write(allModules, MODULES_KEY);
success(res);
}

View File

@@ -12,98 +12,133 @@ export default function register($app) {
} }
async function compareSub(req, res) { async function compareSub(req, res) {
const sub = req.body; try {
const target = req.query.target || 'JSON'; const sub = req.body;
let content; const target = req.query.target || 'JSON';
if (sub.source === 'local') { let content;
content = sub.content; if (sub.source === 'local') {
} else { content = sub.content;
try { } else {
content = await download(sub.url, sub.ua);
} catch (err) {
failed(
res,
new NetworkError(
'FAILED_TO_DOWNLOAD_RESOURCE',
'无法下载远程资源',
`Reason: ${err}`,
),
);
return;
}
}
// parse proxies
const original = ProxyUtils.parse(content);
// add id
original.forEach((proxy, i) => {
proxy.id = i;
});
// apply processors
const processed = await ProxyUtils.process(
original,
sub.process || [],
target,
);
// produce
success(res, { original, processed });
}
async function compareCollection(req, res) {
const allSubs = $.read(SUBS_KEY);
const collection = req.body;
const subnames = collection.subscriptions;
const results = {};
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
try { try {
let raw; content = await download(sub.url, sub.ua);
if (sub.source === 'local') {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
}
// parse proxies
let currentProxies = ProxyUtils.parse(raw);
// apply processors
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
'JSON',
);
results[name] = currentProxies;
} catch (err) { } catch (err) {
failed( failed(
res, res,
new InternalServerError( new NetworkError(
'PROCESS_FAILED', 'FAILED_TO_DOWNLOAD_RESOURCE',
`处理子订阅 ${name} 失败`, '无法下载远程资源',
`Reason: ${err}`, `Reason: ${err}`,
), ),
); );
return;
} }
}), }
); // parse proxies
const original = ProxyUtils.parse(content);
// merge proxies with the original order // add id
const original = Array.prototype.concat.apply( original.forEach((proxy, i) => {
[], proxy.id = i;
subnames.map((name) => results[name] || []), proxy.subName = sub.name;
); });
original.forEach((proxy, i) => { // apply processors
proxy.id = i; const processed = await ProxyUtils.process(
}); original,
sub.process || [],
target,
{ [sub.name]: sub },
);
const processed = await ProxyUtils.process( // produce
original, success(res, { original, processed });
collection.process || [], } catch (err) {
'JSON', $.error(err.message ?? err);
); failed(
res,
success(res, { original, processed }); new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview subscription`,
`Reason: ${err.message ?? err}`,
),
);
}
}
async function compareCollection(req, res) {
try {
const allSubs = $.read(SUBS_KEY);
const collection = req.body;
const subnames = collection.subscriptions;
const results = {};
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
try {
let raw;
if (sub.source === 'local') {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
}
// parse proxies
let currentProxies = ProxyUtils.parse(raw);
currentProxies.forEach((proxy) => {
proxy.subName = sub.name;
proxy.collectionName = collection.name;
});
// apply processors
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
'JSON',
{ [sub.name]: sub, _collection: collection },
);
results[name] = currentProxies;
} catch (err) {
failed(
res,
new InternalServerError(
'PROCESS_FAILED',
`处理子订阅 ${name} 失败`,
`Reason: ${err}`,
),
);
}
}),
);
// merge proxies with the original order
const original = Array.prototype.concat.apply(
[],
subnames.map((name) => results[name] || []),
);
original.forEach((proxy, i) => {
proxy.id = i;
proxy.collectionName = collection.name;
});
const processed = await ProxyUtils.process(
original,
collection.process || [],
'JSON',
{ _collection: collection },
);
success(res, { original, processed });
} catch (err) {
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview collection`,
`Reason: ${err.message ?? err}`,
),
);
}
} }

View File

@@ -44,8 +44,12 @@ export async function updateGitHubAvatar() {
.then((resp) => JSON.parse(resp.body)); .then((resp) => JSON.parse(resp.body));
settings.avatarUrl = data['avatar_url']; settings.avatarUrl = data['avatar_url'];
$.write(settings, SETTINGS_KEY); $.write(settings, SETTINGS_KEY);
} catch (e) { } catch (err) {
$.error('Failed to fetch GitHub avatar for User: ' + username); $.error(
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
err.message ?? err
}`,
);
} }
} }
} }
@@ -67,7 +71,11 @@ export async function updateArtifactStore() {
$.write(settings, SETTINGS_KEY); $.write(settings, SETTINGS_KEY);
} }
} catch (err) { } catch (err) {
$.error('Failed to fetch artifact store for User: ' + githubUser); $.error(
`Failed to fetch artifact store for User: ${githubUser}. Reason: ${
err.message ?? err
}`,
);
} }
} }
} }

View File

@@ -11,7 +11,7 @@ export default function register($app) {
function sortSubs(req, res) { function sortSubs(req, res) {
const orders = req.body; const orders = req.body;
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
allSubs.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);
success(res, allSubs); success(res, allSubs);
} }
@@ -19,7 +19,7 @@ function sortSubs(req, res) {
function sortCollections(req, res) { function sortCollections(req, res) {
const orders = req.body; const orders = req.body;
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
allCols.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allCols, COLLECTIONS_KEY); $.write(allCols, COLLECTIONS_KEY);
success(res, allCols); success(res, allCols);
} }
@@ -27,7 +27,9 @@ function sortCollections(req, res) {
function sortArtifacts(req, res) { function sortArtifacts(req, res) {
const orders = req.body; const orders = req.body;
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
allArtifacts.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allArtifacts.sort(
(a, b) => orders.indexOf(a.name) - orders.indexOf(b.name),
);
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
success(res, allArtifacts); success(res, allArtifacts);
} }

View File

@@ -36,11 +36,15 @@ async function produceArtifact({ type, name, platform }) {
} }
// parse proxies // parse proxies
let proxies = ProxyUtils.parse(raw); let proxies = ProxyUtils.parse(raw);
proxies.forEach((proxy) => {
proxy.subName = sub.name;
});
// apply processors // apply processors
proxies = await ProxyUtils.process( proxies = await ProxyUtils.process(
proxies, proxies,
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub },
); );
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`订阅 ${name} 中不含有效节点`); throw new Error(`订阅 ${name} 中不含有效节点`);
@@ -51,7 +55,7 @@ async function produceArtifact({ type, name, platform }) {
if (exist[proxy.name]) { if (exist[proxy.name]) {
$.notify( $.notify(
'🌍 Sub-Store', '🌍 Sub-Store',
'⚠️ 订阅包含重复节点!', `⚠️ 订阅 ${name} 包含重复节点 ${proxy.name}`,
'请仔细检测配置!', '请仔细检测配置!',
{ {
'media-url': 'media-url':
@@ -86,11 +90,18 @@ async function produceArtifact({ type, name, platform }) {
} }
// parse proxies // parse proxies
let currentProxies = ProxyUtils.parse(raw); let currentProxies = ProxyUtils.parse(raw);
currentProxies.forEach((proxy) => {
proxy.subName = sub.name;
proxy.collectionName = collection.name;
});
// apply processors // apply processors
currentProxies = await ProxyUtils.process( currentProxies = await ProxyUtils.process(
currentProxies, currentProxies,
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub, _collection: collection },
); );
results[name] = currentProxies; results[name] = currentProxies;
processed++; processed++;
@@ -127,11 +138,16 @@ async function produceArtifact({ type, name, platform }) {
subnames.map((name) => results[name] || []), subnames.map((name) => results[name] || []),
); );
proxies.forEach((proxy) => {
proxy.collectionName = collection.name;
});
// apply own processors // apply own processors
proxies = await ProxyUtils.process( proxies = await ProxyUtils.process(
proxies, proxies,
collection.process || [], collection.process || [],
platform, platform,
{ _collection: collection },
); );
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`组合订阅 ${name} 中不含有效节点`); throw new Error(`组合订阅 ${name} 中不含有效节点`);
@@ -142,7 +158,7 @@ async function produceArtifact({ type, name, platform }) {
if (exist[proxy.name]) { if (exist[proxy.name]) {
$.notify( $.notify(
'🌍 Sub-Store', '🌍 Sub-Store',
'⚠️ 订阅包含重复节点!', `⚠️ 组合订阅 ${name} 包含重复节点 ${proxy.name}`,
'请仔细检测配置!', '请仔细检测配置!',
{ {
'media-url': 'media-url':

View File

@@ -1,10 +1,30 @@
import { FILES_KEY, MODULES_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { HTTP, ENV } from '@/vendor/open-api'; import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import $ from '@/core/app';
const tasks = new Map(); const tasks = new Map();
export default async function download(url, ua) { export default async function download(url, ua) {
const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
if (downloadUrlMatch) {
let type = downloadUrlMatch?.[1];
let name = downloadUrlMatch?.[2];
if (name == null) {
throw new Error(`本地 ${type} URL 无效: ${url}`);
}
name = decodeURIComponent(name);
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
const item = findByName($.read(key), name);
if (!item) {
throw new Error(`找不到本地 ${type}: ${name}`);
}
return item.content;
}
const { isNode } = ENV(); const { isNode } = ENV();
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)'; ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
const id = hex_md5(ua + url); const id = hex_md5(ua + url);

16
backend/src/utils/env.js Normal file
View File

@@ -0,0 +1,16 @@
import { version as substoreVersion } from '../../package.json';
import { ENV } from '@/vendor/open-api';
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
export default {
backend,
version: substoreVersion,
};

View File

@@ -17,7 +17,7 @@ export default function express({ substore: $, port }) {
const express_ = eval(`require("express")`); const express_ = eval(`require("express")`);
const bodyParser = eval(`require("body-parser")`); const bodyParser = eval(`require("body-parser")`);
const app = express_(); const app = express_();
app.use(bodyParser.json({ verify: rawBodySaver })); app.use(bodyParser.json({ verify: rawBodySaver, limit: '1mb' }));
app.use( app.use(
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }), bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
); );

View File

@@ -49,27 +49,32 @@ export class OpenAPI {
if (isNode) { if (isNode) {
// create a json for root cache // create a json for root cache
let fpath = 'root.json'; const basePath =
if (!this.node.fs.existsSync(fpath)) { eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { let rootPath = `${basePath}/root.json`;
this.log(`Root path: ${rootPath}`);
if (!this.node.fs.existsSync(rootPath)) {
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
flag: 'wx', flag: 'wx',
}); });
this.root = {}; this.root = {};
} else { } else {
this.root = JSON.parse(this.node.fs.readFileSync(`${fpath}`)); this.root = JSON.parse(
this.node.fs.readFileSync(`${rootPath}`),
);
} }
// create a json file with the given name if not exists // create a json file with the given name if not exists
fpath = `${this.name}.json`; let fpath = `${basePath}/${this.name}.json`;
this.log(`Data path: ${fpath}`);
if (!this.node.fs.existsSync(fpath)) { if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
flag: 'wx', flag: 'wx',
}); });
this.cache = {}; this.cache = {};
} else { } else {
this.cache = JSON.parse( this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
this.node.fs.readFileSync(`${this.name}.json`),
);
} }
} }
} }
@@ -80,14 +85,17 @@ export class OpenAPI {
if (isQX) $prefs.setValueForKey(data, this.name); if (isQX) $prefs.setValueForKey(data, this.name);
if (isLoon || isSurge) $persistentStore.write(data, this.name); if (isLoon || isSurge) $persistentStore.write(data, this.name);
if (isNode) { if (isNode) {
const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
this.node.fs.writeFileSync( this.node.fs.writeFileSync(
`${this.name}.json`, `${basePath}/${this.name}.json`,
data, data,
{ flag: 'w' }, { flag: 'w' },
(err) => console.log(err), (err) => console.log(err),
); );
this.node.fs.writeFileSync( this.node.fs.writeFileSync(
'root.json', `${basePath}/root.json`,
JSON.stringify(this.root, null, 2), JSON.stringify(this.root, null, 2),
{ flag: 'w' }, { flag: 'w' },
(err) => console.log(err), (err) => console.log(err),
@@ -308,7 +316,9 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
return ( return (
timer timer
? Promise.race([timer, worker]).then((res) => { ? Promise.race([timer, worker]).then((res) => {
clearTimeout(timeoutid); if (typeof clearTimeout !== 'undefined') {
clearTimeout(timeoutid);
}
return res; return res;
}) })
: worker : worker

View File

@@ -2,7 +2,7 @@
#!desc=高级订阅管理工具 #!desc=高级订阅管理工具
#!openUrl=https://sub.store #!openUrl=https://sub.store
#!author=Peng-YM #!author=Peng-YM
#!homepage=https://github.com/Peng-YM/Sub-Store #!homepage=https://github.com/sub-store-org/Sub-Store
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png #!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入 #!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入

View File

@@ -1,20 +1,32 @@
# Sub-Store 配置指南 # Sub-Store 配置指南
## 查看更新说明:
Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](https://github.com/sub-store-org/Sub-Store/releases)
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
## 脚本配置: ## 脚本配置:
### 1. Loon ### 1. Loon
安装使用[插件](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Loon.plugin)即可。 安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
### 2. Surge ### 2. Surge
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。 1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
3. 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)
### 3. QX ### 3. QX
订阅[重写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/QX.snippet)即可 订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可
### 4. Stash ### 4. Stash
安装使用[ Stash 覆写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Stash.stoverride)即可。 安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
### 5. Shadowrocket ### 5. Shadowrocket
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。 安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule) 即可。
## 使用 Sub-Store ## 使用 Sub-Store
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。 1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。