mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e0028219d | ||
|
|
54750d552b | ||
|
|
0e7561a069 | ||
|
|
6804c6368a | ||
|
|
9c5d6e9a10 | ||
|
|
ef2d6be8eb | ||
|
|
04e12a4836 | ||
|
|
f94cf7185a | ||
|
|
fa7df51f8c | ||
|
|
18659d1cc8 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sub-store",
|
"name": "sub-store",
|
||||||
"version": "2.19.41",
|
"version": "2.19.52",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -50,6 +50,26 @@ function Base64Encoded() {
|
|||||||
return { name, test, parse };
|
return { name, test, parse };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fallbackBase64Encoded() {
|
||||||
|
const name = 'Fallback Base64 Pre-processor';
|
||||||
|
|
||||||
|
const test = function (raw) {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const parse = function (raw) {
|
||||||
|
const decoded = Base64.decode(raw);
|
||||||
|
if (!/^\w+(:\/\/|\s*?=\s*?)\w+/m.test(decoded)) {
|
||||||
|
$.error(
|
||||||
|
`Fallback Base64 Pre-processor error: decoded line does not start with protocol`,
|
||||||
|
);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
};
|
||||||
|
return { name, test, parse };
|
||||||
|
}
|
||||||
|
|
||||||
function Clash() {
|
function Clash() {
|
||||||
const name = 'Clash Pre-processor';
|
const name = 'Clash Pre-processor';
|
||||||
const test = function (raw) {
|
const test = function (raw) {
|
||||||
@@ -163,4 +183,11 @@ function FullConfig() {
|
|||||||
return { name, test, parse };
|
return { name, test, parse };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default [HTML(), Clash(), Base64Encoded(), SSD(), FullConfig()];
|
export default [
|
||||||
|
HTML(),
|
||||||
|
Clash(),
|
||||||
|
Base64Encoded(),
|
||||||
|
SSD(),
|
||||||
|
FullConfig(),
|
||||||
|
fallbackBase64Encoded(),
|
||||||
|
];
|
||||||
|
|||||||
@@ -405,6 +405,8 @@ function vless(proxy) {
|
|||||||
else append(`,obfs=ws`);
|
else append(`,obfs=ws`);
|
||||||
} else if (proxy.network === 'http') {
|
} else if (proxy.network === 'http') {
|
||||||
append(`,obfs=http`);
|
append(`,obfs=http`);
|
||||||
|
} else if (['tcp'].includes(proxy.network)) {
|
||||||
|
if (proxy.tls) append(`,obfs=over-tls`);
|
||||||
} else if (!['tcp'].includes(proxy.network)) {
|
} else if (!['tcp'].includes(proxy.network)) {
|
||||||
throw new Error(`network ${proxy.network} is unsupported`);
|
throw new Error(`network ${proxy.network} is unsupported`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,7 @@ export default function Shadowrocket_Producer() {
|
|||||||
if (opts['include-unsupported-proxy']) return true;
|
if (opts['include-unsupported-proxy']) return true;
|
||||||
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
||||||
return false;
|
return false;
|
||||||
} else if (['mieru', 'anytls'].includes(proxy.type)) {
|
} else if (['mieru'].includes(proxy.type)) {
|
||||||
return false;
|
|
||||||
} else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {
|
|
||||||
$.error(
|
|
||||||
`Shadowrocket 不支持前置代理字段. 已过滤节点 ${proxy.name}. 请使用 App 内的 "代理通过" 功能`,
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ export default function URI_Producer() {
|
|||||||
switch (proxy.type) {
|
switch (proxy.type) {
|
||||||
case 'socks5':
|
case 'socks5':
|
||||||
result = `socks://${encodeURIComponent(
|
result = `socks://${encodeURIComponent(
|
||||||
Base64.encode(`${proxy.username}:${proxy.password}`),
|
Base64.encode(
|
||||||
|
`${proxy.username ?? ''}:${proxy.password ?? ''}`,
|
||||||
|
),
|
||||||
)}@${proxy.server}:${proxy.port}#${proxy.name}`;
|
)}@${proxy.server}:${proxy.port}#${proxy.name}`;
|
||||||
break;
|
break;
|
||||||
case 'ss':
|
case 'ss':
|
||||||
|
|||||||
@@ -49,11 +49,17 @@ export default function register($app) {
|
|||||||
success(res);
|
success(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect sub.store to vercel webpage
|
if (ENV().isNode) {
|
||||||
$app.get('/', async (req, res) => {
|
$app.get('/', getEnv);
|
||||||
// 302 redirect
|
} else {
|
||||||
res.set('location', 'https://sub-store.vercel.app/').status(302).end();
|
// Redirect sub.store to vercel webpage
|
||||||
});
|
$app.get('/', async (req, res) => {
|
||||||
|
// 302 redirect
|
||||||
|
res.set('location', 'https://sub-store.vercel.app/')
|
||||||
|
.status(302)
|
||||||
|
.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// handle preflight request for QX
|
// handle preflight request for QX
|
||||||
if (ENV().isQX) {
|
if (ENV().isQX) {
|
||||||
@@ -71,7 +77,19 @@ function getEnv(req, res) {
|
|||||||
if (req.query.share) {
|
if (req.query.share) {
|
||||||
env.feature.share = true;
|
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) {
|
async function refresh(_, res) {
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ async function produceArtifact({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (produceType === 'raw') {
|
if (produceType === 'raw') {
|
||||||
return (Array.isArray(raw) ? raw : [raw]).flat();
|
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
|
||||||
}
|
}
|
||||||
// parse proxies
|
// parse proxies
|
||||||
let proxies = (Array.isArray(raw) ? raw : [raw])
|
let proxies = (Array.isArray(raw) ? raw : [raw])
|
||||||
@@ -574,7 +574,7 @@ async function produceArtifact({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (produceType === 'raw') {
|
if (produceType === 'raw') {
|
||||||
return (Array.isArray(raw) ? raw : [raw]).flat();
|
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
|
||||||
}
|
}
|
||||||
const files = (Array.isArray(raw) ? raw : [raw]).flat();
|
const files = (Array.isArray(raw) ? raw : [raw]).flat();
|
||||||
let filesContent = files
|
let filesContent = files
|
||||||
|
|||||||
6
backend/src/vendor/express.js
vendored
6
backend/src/vendor/express.js
vendored
@@ -17,10 +17,12 @@ export default function express({ substore: $, port, host }) {
|
|||||||
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_();
|
||||||
|
const limit = eval('process.env.SUB_STORE_BODY_JSON_LIMIT') || '1mb';
|
||||||
|
$.info(`[BACKEND] body JSON limit: ${limit}`);
|
||||||
app.use(
|
app.use(
|
||||||
bodyParser.json({
|
bodyParser.json({
|
||||||
verify: rawBodySaver,
|
verify: rawBodySaver,
|
||||||
limit: eval('process.env.SUB_STORE_BODY_JSON_LIMIT') || '1mb',
|
limit,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
@@ -36,7 +38,7 @@ export default function express({ substore: $, port, host }) {
|
|||||||
app.start = () => {
|
app.start = () => {
|
||||||
const listener = app.listen(port, host, () => {
|
const listener = app.listen(port, host, () => {
|
||||||
const { address, port } = listener.address();
|
const { address, port } = listener.address();
|
||||||
$.info(`[BACKEND] ${address}:${port}`);
|
$.info(`[BACKEND] listening on ${address}:${port}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
49
backend/src/vendor/open-api.js
vendored
49
backend/src/vendor/open-api.js
vendored
@@ -10,6 +10,14 @@ const isEgern = 'object' == typeof egern;
|
|||||||
const isLanceX = 'undefined' != typeof $native;
|
const isLanceX = 'undefined' != typeof $native;
|
||||||
const isGUIforCores = typeof $Plugins !== 'undefined';
|
const isGUIforCores = typeof $Plugins !== 'undefined';
|
||||||
|
|
||||||
|
function isPlainObject(obj) {
|
||||||
|
return (
|
||||||
|
obj !== null &&
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
[null, Object.prototype].includes(Object.getPrototypeOf(obj))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export class OpenAPI {
|
export class OpenAPI {
|
||||||
constructor(name = 'untitled', debug = false) {
|
constructor(name = 'untitled', debug = false) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -62,29 +70,50 @@ export class OpenAPI {
|
|||||||
const basePath =
|
const basePath =
|
||||||
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
||||||
let rootPath = `${basePath}/root.json`;
|
let rootPath = `${basePath}/root.json`;
|
||||||
|
const backupRootPath = `${basePath}/root_${Date.now()}.json`;
|
||||||
|
|
||||||
this.log(`Root path: ${rootPath}`);
|
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({}), {
|
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
|
||||||
flag: 'wx',
|
flag: 'w',
|
||||||
});
|
});
|
||||||
this.root = {};
|
this.root = {};
|
||||||
} else {
|
|
||||||
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
|
||||||
let fpath = `${basePath}/${this.name}.json`;
|
let fpath = `${basePath}/${this.name}.json`;
|
||||||
|
const backupPath = `${basePath}/${this.name}_${Date.now()}.json`;
|
||||||
|
|
||||||
this.log(`Data path: ${fpath}`);
|
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({}), {
|
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
|
||||||
flag: 'wx',
|
flag: 'w',
|
||||||
});
|
});
|
||||||
this.cache = {};
|
this.cache = {};
|
||||||
} else {
|
|
||||||
this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
backend/sub-store_1748083027961.json
Normal file
0
backend/sub-store_1748083027961.json
Normal file
@@ -14,10 +14,12 @@ function operator(proxies = [], targetPlatform, context) {
|
|||||||
// 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名
|
// 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名
|
||||||
// 7. `tls-fingerprint` 为 tls 指纹
|
// 7. `tls-fingerprint` 为 tls 指纹
|
||||||
// 8. `underlying-proxy` 为前置代理, 不同平台会自动转换
|
// 8. `underlying-proxy` 为前置代理, 不同平台会自动转换
|
||||||
|
// 例如 $server['underlying-proxy'] = '名称'
|
||||||
// 只给 mihomo 输出的话, `dialer-proxy` 也行
|
// 只给 mihomo 输出的话, `dialer-proxy` 也行
|
||||||
// 只给 sing-box 输出的话, `detour` 也行
|
// 只给 sing-box 输出的话, `detour` 也行
|
||||||
// 只给 egern 输出的话, `prev_hop` 也行
|
// 只给 Egern 输出的话, `prev_hop` 也行
|
||||||
// 输出到 Clash/Stash/Shadowrocket 时, 会过滤掉配置了前置代理的节点, 并提示使用对应的功能.
|
// 只给 Shadowrocket 输出的话, `chain` 也行
|
||||||
|
// 输出到 Clash/Stash 时, 会过滤掉配置了前置代理的节点, 并提示使用对应的功能.
|
||||||
// 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
|
// 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
|
||||||
// 10. `sni` 在某些协议里会自动与 `servername` 转换
|
// 10. `sni` 在某些协议里会自动与 `servername` 转换
|
||||||
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
|
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
|
||||||
|
|||||||
Reference in New Issue
Block a user