Compare commits

..

11 Commits

11 changed files with 192 additions and 23 deletions

View File

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

View File

@@ -292,6 +292,7 @@ export const ProxyUtils = {
getISO,
MMDB,
Gist,
download,
};
function tryParse(parser, line) {

View File

@@ -50,10 +50,32 @@ function Clash() {
};
const parse = function (raw) {
// Clash YAML format
// 防止 VLESS节点 reality-opts 选项中的 short-id 被解析成 Infinity
// 匹配 short-id 冒号后面的值(包含空格和引号)
const afterReplace = raw.replace(
/short-id:([ ]*[^,\n}]*)/g,
(matched, value) => {
const afterTrim = value.trim();
// 为空
if (!afterTrim || afterTrim === '') {
return 'short-id: ""'
}
// 是否被引号包裹
if (/^(['"]).*\1$/.test(afterTrim)) {
return `short-id: ${afterTrim}`;
} else {
return `short-id: "${afterTrim}"`
}
}
);
const {
proxies,
'global-client-fingerprint': globalClientFingerprint,
} = safeLoad(raw);
} = safeLoad(afterReplace);
return proxies
.map((p) => {
// https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml#L73C1-L73C26

View File

@@ -69,6 +69,7 @@ async function downloadSubscription(req, res) {
produceType,
includeUnsupportedProxy,
resultFormat,
proxy,
} = req.query;
let $options = {};
if (req.query.$options) {
@@ -92,6 +93,10 @@ async function downloadSubscription(req, res) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
}
if (proxy) {
proxy = decodeURIComponent(proxy);
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
}
if (ua) {
ua = decodeURIComponent(ua);
$.info(`指定远程订阅 User-Agent: ${ua}`);
@@ -135,6 +140,7 @@ async function downloadSubscription(req, res) {
'include-unsupported-proxy': includeUnsupportedProxy,
},
$options,
proxy,
});
if (
@@ -175,7 +181,7 @@ async function downloadSubscription(req, res) {
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
proxy || sub.proxy,
$arguments.flowUrl,
);
if (flowInfo) {
@@ -264,6 +270,7 @@ async function downloadCollection(req, res) {
produceType,
includeUnsupportedProxy,
resultFormat,
proxy,
} = req.query;
let $options = {};
@@ -285,6 +292,11 @@ async function downloadCollection(req, res) {
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (proxy) {
proxy = decodeURIComponent(proxy);
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
}
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
@@ -311,6 +323,7 @@ async function downloadCollection(req, res) {
'include-unsupported-proxy': includeUnsupportedProxy,
},
$options,
proxy,
});
// forward flow header from the first subscription in this collection
@@ -355,7 +368,7 @@ async function downloadCollection(req, res) {
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
proxy || sub.proxy || collection.proxy,
$arguments.flowUrl,
);
if (flowInfo) {

View File

@@ -59,6 +59,7 @@ async function getFile(req, res) {
content,
mergeSources,
ignoreFailedRemoteFile,
proxy,
} = req.query;
let $options = {};
if (req.query.$options) {
@@ -82,6 +83,10 @@ async function getFile(req, res) {
url = decodeURIComponent(url);
$.info(`指定远程文件 URL: ${url}`);
}
if (proxy) {
proxy = decodeURIComponent(proxy);
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
}
if (ua) {
ua = decodeURIComponent(ua);
$.info(`指定远程文件 User-Agent: ${ua}`);
@@ -120,6 +125,7 @@ async function getFile(req, res) {
mergeSources,
ignoreFailedRemoteFile,
$options,
proxy,
});
try {
@@ -129,6 +135,8 @@ async function getFile(req, res) {
const flowInfo = await getFlowHeaders(
subInfoUrl,
subInfoUserAgent || file.subInfoUserAgent,
undefined,
proxy || file.proxy,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
@@ -141,7 +149,14 @@ async function getFile(req, res) {
)}`,
);
}
if (file.download) {
res.set(
'Content-Disposition',
`attachment; filename*=UTF-8''${encodeURIComponent(
file.displayName || file.name,
)}`,
);
}
res.set('Content-Type', 'text/plain; charset=utf-8').send(
output ?? '',
);

View File

@@ -177,7 +177,20 @@ async function compareCollection(req, res) {
try {
const allSubs = $.read(SUBS_KEY);
const collection = req.body;
const subnames = collection.subscriptions;
const subnames = [...collection.subscriptions];
let subscriptionTags = collection.subscriptionTags;
if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {
allSubs.forEach((sub) => {
if (
Array.isArray(sub.tag) &&
sub.tag.length > 0 &&
!subnames.includes(sub.name) &&
sub.tag.some((tag) => subscriptionTags.includes(tag))
) {
subnames.push(sub.name);
}
});
}
const results = {};
const errors = {};
await Promise.all(

View File

@@ -38,6 +38,7 @@ async function produceArtifact({
subscription,
awaitCustomCache,
$options,
proxy,
}) {
platform = platform || 'JSON';
@@ -68,7 +69,7 @@ async function produceArtifact({
url,
ua || sub.ua,
undefined,
sub.proxy,
proxy || sub.proxy,
undefined,
awaitCustomCache,
);
@@ -115,7 +116,7 @@ async function produceArtifact({
url,
ua || sub.ua,
undefined,
sub.proxy,
proxy || sub.proxy,
undefined,
awaitCustomCache,
);
@@ -189,7 +190,20 @@ async function produceArtifact({
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
if (!collection) throw new Error(`找不到组合订阅 ${name}`);
const subnames = collection.subscriptions;
const subnames = [...collection.subscriptions];
let subscriptionTags = collection.subscriptionTags;
if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {
allSubs.forEach((sub) => {
if (
Array.isArray(sub.tag) &&
sub.tag.length > 0 &&
!subnames.includes(sub.name) &&
sub.tag.some((tag) => subscriptionTags.includes(tag))
) {
subnames.push(sub.name);
}
});
}
const results = {};
const errors = {};
let processed = 0;
@@ -220,7 +234,9 @@ async function produceArtifact({
url,
sub.ua,
undefined,
sub.proxy,
proxy ||
sub.proxy ||
collection.proxy,
);
} catch (err) {
errors[url] = err;
@@ -344,7 +360,6 @@ async function produceArtifact({
}
exist[proxy.name] = true;
}
console.log(proxies);
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'rule') {
const allRules = $.read(RULES_KEY);
@@ -390,7 +405,12 @@ async function produceArtifact({
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || file.ua);
return await download(
url,
ua || file.ua,
undefined,
file.proxy || proxy,
);
} catch (err) {
errors[url] = err;
$.error(
@@ -433,7 +453,12 @@ async function produceArtifact({
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || file.ua);
return await download(
url,
ua || file.ua,
undefined,
file.proxy || proxy,
);
} catch (err) {
errors[url] = err;
$.error(

View File

@@ -18,7 +18,7 @@ export default async function download(
rawUrl = '',
ua,
timeout,
proxy,
customProxy,
skipCustomCache,
awaitCustomCache,
) {
@@ -43,8 +43,12 @@ export default async function download(
}
}
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
const { defaultProxy, defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url);

View File

@@ -10,7 +10,13 @@ export function getFlowField(headers) {
)[0];
return headers[subkey];
}
export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
export async function getFlowHeaders(
rawUrl,
ua,
timeout,
customProxy,
flowUrl,
) {
let url = flowUrl || rawUrl || '';
let $arguments = {};
const rawArgs = url.split('#');
@@ -41,7 +47,13 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
// $.info(`使用缓存的流量信息: ${url}`);
flowInfo = cached;
} else {
const { defaultFlowUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
$.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy =
proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent =
ua ||
defaultFlowUserAgent ||
@@ -67,7 +79,7 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
$.info(
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
}, Proxy: ${proxy}`,
);
const { headers } = await http.head({
url: url
@@ -97,14 +109,14 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
$.error(
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
userAgent || ''
}: ${e.message ?? e}`,
}, Proxy: ${proxy}: ${e.message ?? e}`,
);
}
if (!flowInfo) {
$.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
}, Proxy: ${proxy}`,
);
const { headers } = await http.get({
url: url
@@ -113,8 +125,21 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
});
flowInfo = getFlowField(headers);
}

View File

@@ -1,10 +1,21 @@
import { HTTP } from '@/vendor/open-api';
import { HTTP, ENV } from '@/vendor/open-api';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app';
import { SETTINGS_KEY } from '@/constants';
/**
* Gist backup
*/
export default class Gist {
constructor({ token, key, syncPlatform }) {
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultProxy, defaultTimeout: timeout } = $.read(SETTINGS_KEY);
let proxy = defaultProxy;
if ($.env.isNode) {
proxy =
proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
if (syncPlatform === 'gitlab') {
this.headers = {
'PRIVATE-TOKEN': `${token}`,
@@ -13,7 +24,25 @@ export default class Gist {
};
this.http = HTTP({
baseURL: 'https://gitlab.com/api/v4',
headers: { ...this.headers },
headers: {
...this.headers,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
timeout,
events: {
onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) {
@@ -35,7 +64,25 @@ export default class Gist {
};
this.http = HTTP({
baseURL: 'https://api.github.com',
headers: { ...this.headers },
headers: {
...this.headers,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
timeout,
events: {
onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) {

View File

@@ -18,6 +18,9 @@ function operator(proxies = [], targetPlatform, context) {
// 10. `sni` 在某些协议里会自动与 `servername` 转换
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
// 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down`
// 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时
// 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
// 15. `ip-version` 设置节点使用 IP 版本可选dualipv4ipv6ipv4-preferipv6-prefer. 会进行内部转换, 若无法匹配则使用原始值
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
@@ -63,6 +66,7 @@ function operator(proxies = [], targetPlatform, context) {
// removeFlag, // 移除 emoji 旗帜
// getISO, // 获取 ISO 3166-1 alpha-2 代码
// Gist, // Gist 类
// download, // 内部的下载方法, 见 backend/src/utils/download.js
// }
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009