Compare commits

...

11 Commits

19 changed files with 251 additions and 99 deletions

View File

@ -42,7 +42,7 @@ Core functionalities:
- [x] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru, AnyTLS)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
Deprecated:
Deprecated(The frontend doesn't show it, but the backend still supports it, with the query parameter `target=Clash`):
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)

View File

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

View File

@ -142,9 +142,9 @@ async function processFn(
? `#${rawArgs[1]}`
: ''
}`;
const downloadUrlMatch = url.match(
/^\/api\/(file|module)\/(.+)/,
);
const downloadUrlMatch = url
.split('#')[0]
.match(/^\/api\/(file|module)\/(.+)/);
if (downloadUrlMatch) {
let type = '';
try {
@ -174,6 +174,17 @@ async function processFn(
);
throw new Error(`无法加载 ${type}: ${url}`);
}
} else if (url?.startsWith('/')) {
try {
const fs = eval(`require("fs")`);
script = fs.readFileSync(url.split('#')[0], 'utf8');
// $.info(`Script loaded: >>>\n ${script}`);
} catch (err) {
$.error(
`Error when reading local script: ${item.args.content}.\n Reason: ${err}`,
);
throw new Error(`无法从该路径读取脚本文件: ${url}`);
}
} else {
// if this is a remote script, download it
try {

View File

@ -439,7 +439,16 @@ function URI_VMess() {
type: 'vmess',
server,
port,
cipher: getIfPresent(params.scy, 'auto'),
// https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link
// https://github.com/XTLS/Xray-core/issues/91
cipher: [
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(params.scy)
? params.scy
: 'auto',
uuid: params.id,
alterId: parseInt(
getIfPresent(params.aid ?? params.alterId, 0),
@ -473,8 +482,8 @@ function URI_VMess() {
['http'].includes(params.type)
) {
proxy.network = 'http';
} else if (['grpc'].includes(params.net)) {
proxy.network = 'grpc';
} else if (['grpc', 'kcp', 'quic'].includes(params.net)) {
proxy.network = params.net;
} else if (
params.net === 'httpupgrade' ||
proxy.network === 'httpupgrade'
@ -524,13 +533,28 @@ function URI_VMess() {
}
}
// 传输层应该有配置, 暂时不考虑兼容不给配置的节点
if (transportPath || transportHost) {
if (
transportPath ||
transportHost ||
['kcp', 'quic'].includes(proxy.network)
) {
if (['grpc'].includes(proxy.network)) {
proxy[`${proxy.network}-opts`] = {
'grpc-service-name': getIfNotBlank(transportPath),
'_grpc-type': getIfNotBlank(params.type),
'_grpc-authority': getIfNotBlank(params.authority),
};
} else if (['kcp', 'quic'].includes(proxy.network)) {
proxy[`${proxy.network}-opts`] = {
[`_${proxy.network}-type`]: getIfNotBlank(
params.type,
),
[`_${proxy.network}-host`]: getIfNotBlank(
getIfNotBlank(transportHost),
),
[`_${proxy.network}-path`]:
getIfNotBlank(transportPath),
};
} else {
const opts = {
path: getIfNotBlank(transportPath),
@ -546,6 +570,12 @@ function URI_VMess() {
delete proxy.network;
}
}
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
// 然而 wiki 和 app 实测中都没有字段表示这个
// proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
return proxy;
}
};

View File

@ -9,7 +9,7 @@ export default function Shadowrocket_Producer() {
if (opts['include-unsupported-proxy']) return true;
if (proxy.type === 'snell' && String(proxy.version) === '4') {
return false;
} else if (['mieru', 'anytls'].includes(proxy.type)) {
} else if (['mieru'].includes(proxy.type)) {
return false;
}
return true;

View File

@ -519,6 +519,7 @@ const vlessParser = (proxy = {}) => {
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
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);

View File

@ -44,11 +44,9 @@ export default function Stash_Producer() {
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher)) ||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
(opts['include-unsupported-proxy']
? proxy.type === 'vless' &&
proxy['reality-opts'] &&
!['xtls-rprx-vision'].includes(proxy.flow)
: proxy.type === 'vless' && proxy['reality-opts'])
(proxy.type === 'vless' &&
proxy['reality-opts'] &&
!['xtls-rprx-vision'].includes(proxy.flow))
) {
return false;
} else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {

View File

@ -119,12 +119,17 @@ export default function URI_Producer() {
v: '2',
ps: proxy.name,
add: proxy.server,
port: proxy.port,
port: `${proxy.port}`,
id: proxy.uuid,
type,
aid: proxy.alterId || 0,
aid: `${proxy.alterId || 0}`,
scy: proxy.cipher,
net,
type,
tls: proxy.tls ? 'tls' : '',
alpn: Array.isArray(proxy.alpn)
? proxy.alpn.join(',')
: proxy.alpn,
fp: proxy['client-fingerprint'],
};
if (proxy.tls && proxy.sni) {
result.sni = proxy.sni;
@ -135,16 +140,7 @@ export default function URI_Producer() {
proxy[`${proxy.network}-opts`]?.path;
let vmessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (vmessTransportPath) {
result.path = Array.isArray(vmessTransportPath)
? vmessTransportPath[0]
: vmessTransportPath;
}
if (vmessTransportHost) {
result.host = Array.isArray(vmessTransportHost)
? vmessTransportHost[0]
: vmessTransportHost;
}
if (['grpc'].includes(proxy.network)) {
result.path =
proxy[`${proxy.network}-opts`]?.[
@ -156,6 +152,31 @@ export default function URI_Producer() {
'gun';
result.host =
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
} else if (['kcp', 'quic'].includes(proxy.network)) {
// https://github.com/XTLS/Xray-core/issues/91
result.type =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-type`
] || 'none';
result.host =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-host`
];
result.path =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-path`
];
} else {
if (vmessTransportPath) {
result.path = Array.isArray(vmessTransportPath)
? vmessTransportPath[0]
: vmessTransportPath;
}
if (vmessTransportHost) {
result.host = Array.isArray(vmessTransportHost)
? vmessTransportHost[0]
: vmessTransportHost;
}
}
}
result = 'vmess://' + Base64.encode(JSON.stringify(result));

View File

@ -259,7 +259,7 @@ async function downloadSubscription(req, res) {
}
}
}
if (!$arguments.noFlow) {
if (!$arguments.noFlow && /^https?/.test(url)) {
// forward flow headers
flowInfo = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
@ -506,7 +506,7 @@ async function downloadCollection(req, res) {
}
}
}
if (!$arguments.noFlow) {
if (!$arguments.noFlow && /^https?:/.test(url)) {
subUserInfoOfSub = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,

View File

@ -150,6 +150,7 @@ async function getFile(req, res) {
proxy,
noCache,
produceType,
all: true,
});
try {
@ -184,9 +185,15 @@ async function getFile(req, res) {
)}`,
);
}
res.set('Content-Type', 'text/plain; charset=utf-8').send(
output ?? '',
);
res.set('Content-Type', 'text/plain; charset=utf-8');
if (output?.$options?._res?.headers) {
Object.entries(output.$options._res.headers).forEach(
([key, value]) => {
res.set(key, value);
},
);
}
res.send(output?.$content ?? '');
} catch (err) {
$.notify(
`🌍 Sub-Store 下载文件失败`,

View File

@ -77,7 +77,19 @@ function getEnv(req, res) {
if (req.query.share) {
env.feature.share = true;
}
success(res, env);
res.set('Content-Type', 'application/json;charset=UTF-8').send(
JSON.stringify(
{
status: 'success',
data: {
guide: '⚠️⚠️⚠️ 您当前看到的是后端的响应. 若想配合前端使用, 可访问官方前端 https://sub-store.vercel.app 后自行配置后端地址, 或一键配置后端 https://sub-store.vercel.app?api=https://a.com/xxx (假设 https://a.com 是你后端的域名, /xxx 是自定义路径). 需注意 HTTPS 前端无法请求非本地的 HTTP 后端(部分浏览器上也无法访问本地 HTTP 后端). 请配置反代或在局域网自建 HTTP 前端. 如果还有问题, 可查看此排查说明: https://t.me/zhetengsha/1068',
...env,
},
},
null,
2,
),
);
}
async function refresh(_, res) {

View File

@ -140,7 +140,7 @@ async function getFlowInfo(req, res) {
}
}
}
if ($arguments.noFlow) {
if ($arguments.noFlow || !/^https?/.test(url)) {
failed(
res,
new RequestInvalidError(

View File

@ -40,6 +40,7 @@ async function produceArtifact({
$options,
proxy,
noCache,
all,
}) {
platform = platform || 'JSON';
@ -595,7 +596,7 @@ async function produceArtifact({
)
: { $content: filesContent, $files: files, $options };
return processed?.$content ?? '';
return (all ? processed : processed?.$content) ?? '';
}
}

View File

@ -1,4 +1,4 @@
import { SETTINGS_KEY } from '@/constants';
import { SETTINGS_KEY, FILES_KEY, MODULES_KEY } from '@/constants';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils';
@ -11,6 +11,8 @@ import {
validCheck,
} from '@/utils/flow';
import $ from '@/core/app';
import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync';
import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
const clashPreprocessor = PROXY_PREPROCESSORS.find(
(processor) => processor.name === 'Clash Pre-processor',
@ -130,22 +132,53 @@ export default async function download(
}
}
// 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}`);
// }
const downloadUrlMatch = url
.split('#')[0]
.match(/^\/api\/(file|module)\/(.+)/);
if (downloadUrlMatch) {
let type = '';
try {
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;
// }
if (type === 'module') {
return item.content;
} else {
return await produceArtifact({
type: 'file',
name,
});
}
} catch (err) {
$.error(
`Error when loading ${type}: ${
url.split('#')[0]
}.\n Reason: ${err}`,
);
throw new Error(`无法加载 ${type}: ${url}`);
}
} else if (url?.startsWith('/')) {
try {
const fs = eval(`require("fs")`);
return fs.readFileSync(url.split('#')[0], 'utf8');
} catch (err) {
$.error(
`Error when reading local file: ${
url.split('#')[0]
}.\n Reason: ${err}`,
);
throw new Error(`无法从该路径读取文本内容: ${url}`);
}
}
if (!isNode && tasks.has(id)) {
return tasks.get(id);

View File

@ -49,7 +49,7 @@ export async function getFlowHeaders(
}
}
}
if ($arguments?.noFlow) {
if ($arguments?.noFlow || !/^https?/.test(url)) {
return;
}
const { isStash, isLoon, isShadowRocket, isQX } = ENV();

View File

@ -61,41 +61,41 @@ export function getPlatformFromHeaders(headers) {
return getPlatformFromUserAgent({ ua, UA, accept });
}
export function shouldIncludeUnsupportedProxy(platform, ua) {
try {
const target = getPlatformFromUserAgent({
UA: ua,
ua: ua.toLowerCase(),
});
if (!['Stash', 'Egern', 'Loon'].includes(target)) {
return false;
}
const coerceVersion = coerce(ua);
$.log(JSON.stringify(coerceVersion, null, 2));
const { version } = coerceVersion;
if (
platform === 'Stash' &&
target === 'Stash' &&
gte(version, '3.1.0')
) {
return true;
}
if (
platform === 'Egern' &&
target === 'Egern' &&
gte(version, '1.29.0')
) {
return true;
}
// Loon 的 UA 不规范, version 取出来是 build
if (
platform === 'Loon' &&
target === 'Loon' &&
gte(version, '842.0.0')
) {
return true;
}
} catch (e) {
$.error(`获取版本号失败: ${e}`);
}
// try {
// const target = getPlatformFromUserAgent({
// UA: ua,
// ua: ua.toLowerCase(),
// });
// if (!['Stash', 'Egern', 'Loon'].includes(target)) {
// return false;
// }
// const coerceVersion = coerce(ua);
// $.log(JSON.stringify(coerceVersion, null, 2));
// const { version } = coerceVersion;
// if (
// platform === 'Stash' &&
// target === 'Stash' &&
// gte(version, '3.1.0')
// ) {
// return true;
// }
// if (
// platform === 'Egern' &&
// target === 'Egern' &&
// gte(version, '1.29.0')
// ) {
// return true;
// }
// // Loon 的 UA 不规范, version 取出来是 build
// if (
// platform === 'Loon' &&
// target === 'Loon' &&
// gte(version, '842.0.0')
// ) {
// return true;
// }
// } catch (e) {
// $.error(`获取版本号失败: ${e}`);
// }
return false;
}

View File

@ -10,6 +10,14 @@ const isEgern = 'object' == typeof egern;
const isLanceX = 'undefined' != typeof $native;
const isGUIforCores = typeof $Plugins !== 'undefined';
function isPlainObject(obj) {
return (
obj !== null &&
typeof obj === 'object' &&
[null, Object.prototype].includes(Object.getPrototypeOf(obj))
);
}
export class OpenAPI {
constructor(name = 'untitled', debug = false) {
this.name = name;
@ -62,29 +70,50 @@ export class OpenAPI {
const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
let rootPath = `${basePath}/root.json`;
const backupRootPath = `${basePath}/root_${Date.now()}.json`;
this.log(`Root path: ${rootPath}`);
if (!this.node.fs.existsSync(rootPath)) {
if (this.node.fs.existsSync(rootPath)) {
try {
this.root = JSON.parse(
this.node.fs.readFileSync(`${rootPath}`),
);
} catch (e) {
this.node.fs.copyFileSync(rootPath, backupRootPath);
this.error(
`Failed to parse ${rootPath}: ${e.message}. Backup created at ${backupRootPath}`,
);
}
}
if (!isPlainObject(this.root)) {
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
flag: 'wx',
flag: 'w',
});
this.root = {};
} else {
this.root = JSON.parse(
this.node.fs.readFileSync(`${rootPath}`),
);
}
// create a json file with the given name if not exists
let fpath = `${basePath}/${this.name}.json`;
const backupPath = `${basePath}/${this.name}_${Date.now()}.json`;
this.log(`Data path: ${fpath}`);
if (!this.node.fs.existsSync(fpath)) {
if (this.node.fs.existsSync(fpath)) {
try {
this.cache = JSON.parse(
this.node.fs.readFileSync(`${fpath}`),
);
} catch (e) {
this.node.fs.copyFileSync(fpath, backupPath);
this.error(
`Failed to parse ${fpath}: ${e.message}. Backup created at ${backupPath}`,
);
}
}
if (!isPlainObject(this.cache)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
flag: 'wx',
flag: 'w',
});
this.cache = {};
} else {
this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
}
}
}

View File

View File

@ -59,6 +59,15 @@ function operator(proxies = [], targetPlatform, context) {
// }
// console.log($options)
// 若设置 $options._res.headers
// 则会在输出文件时设置响应头, 例如:
// $options._res = {
// headers: {
// 'X-Custom': '1'
// }
// }
// targetPlatform 为输出的目标平台
// lodash