mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e3e4c6e46 | ||
|
|
bc0dd4b175 | ||
|
|
7603fac036 | ||
|
|
9acc161684 | ||
|
|
024582a99d | ||
|
|
1d31a80b9f | ||
|
|
b2d0276836 | ||
|
|
3211fbf357 | ||
|
|
33a17c2d66 | ||
|
|
2c89a0ddbd | ||
|
|
939022e5a3 | ||
|
|
59bca5670d | ||
|
|
07b38cf971 | ||
|
|
28186f596f | ||
|
|
ea31b1d0ec |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.48",
|
||||
"version": "2.14.61",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -2,6 +2,8 @@ export const SCHEMA_VERSION_KEY = 'schemaVersion';
|
||||
export const SETTINGS_KEY = 'settings';
|
||||
export const SUBS_KEY = 'subs';
|
||||
export const COLLECTIONS_KEY = 'collections';
|
||||
export const FILES_KEY = 'files';
|
||||
export const MODULES_KEY = 'modules';
|
||||
export const ARTIFACTS_KEY = 'artifacts';
|
||||
export const RULES_KEY = 'rules';
|
||||
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
|
||||
|
||||
@@ -67,7 +67,7 @@ async function process(proxies, operators = [], targetPlatform) {
|
||||
for (const item of operators) {
|
||||
// process script
|
||||
let script;
|
||||
const $arguments = {};
|
||||
let $arguments = {};
|
||||
if (item.type.indexOf('Script') !== -1) {
|
||||
const { mode, content } = item.args;
|
||||
if (mode === 'link') {
|
||||
@@ -75,10 +75,19 @@ async function process(proxies, operators = [], targetPlatform) {
|
||||
// extract link arguments
|
||||
const rawArgs = url.split('#');
|
||||
if (rawArgs.length > 1) {
|
||||
for (const pair of rawArgs[1].split('&')) {
|
||||
const key = pair.split('=')[0];
|
||||
const value = pair.split('=')[1] || true;
|
||||
$arguments[key] = value;
|
||||
try {
|
||||
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +208,7 @@ function lastParse(proxy) {
|
||||
delete proxy.network;
|
||||
}
|
||||
}
|
||||
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
|
||||
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
|
||||
proxy.tls = true;
|
||||
}
|
||||
if (proxy.tls && !proxy.sni) {
|
||||
|
||||
@@ -267,6 +267,8 @@ function URI_VMess() {
|
||||
params.obfs === 'http'
|
||||
) {
|
||||
proxy.network = 'http';
|
||||
} else if (['grpc'].includes(params.net)) {
|
||||
proxy.network = 'grpc';
|
||||
}
|
||||
if (proxy.network) {
|
||||
let transportHost = params.host ?? params.obfsParam;
|
||||
@@ -285,10 +287,17 @@ function URI_VMess() {
|
||||
}
|
||||
}
|
||||
if (transportPath || transportHost) {
|
||||
proxy[`${proxy.network}-opts`] = {
|
||||
path: getIfNotBlank(transportPath),
|
||||
headers: { Host: getIfNotBlank(transportHost) },
|
||||
};
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
proxy[`${proxy.network}-opts`] = {
|
||||
'grpc-service-name': getIfNotBlank(transportPath),
|
||||
'_grpc-type': getIfNotBlank(params.type),
|
||||
};
|
||||
} else {
|
||||
proxy[`${proxy.network}-opts`] = {
|
||||
path: getIfNotBlank(transportPath),
|
||||
headers: { Host: getIfNotBlank(transportHost) },
|
||||
};
|
||||
}
|
||||
} else {
|
||||
delete proxy.network;
|
||||
}
|
||||
@@ -365,6 +374,10 @@ function URI_VLESS() {
|
||||
if (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) {
|
||||
proxy[`${proxy.network}-opts`] = opts;
|
||||
}
|
||||
@@ -423,6 +436,7 @@ function Clash_All() {
|
||||
'tuic',
|
||||
'vless',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'wireguard',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
|
||||
@@ -3,6 +3,9 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
export default function Clash_Producer() {
|
||||
const type = 'ALL';
|
||||
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
|
||||
proxies = proxies.filter((proxy) => {
|
||||
if (
|
||||
@@ -10,17 +13,17 @@ export default function Clash_Producer() {
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'vless',
|
||||
'socks',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'wireguard',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
return false;
|
||||
} else if (
|
||||
proxy.type === 'snell' &&
|
||||
String(proxy.version) === '4'
|
||||
].includes(proxy.type) ||
|
||||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
|
||||
(proxy.type === 'vless' &&
|
||||
(typeof proxy.flow !== 'undefined' ||
|
||||
proxy['reality-opts']))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -87,10 +90,20 @@ export default function Clash_Producer() {
|
||||
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-fingerprint'];
|
||||
if (
|
||||
['grpc'].includes(proxy.network) &&
|
||||
proxy[`${proxy.network}-opts`]
|
||||
) {
|
||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||
}
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
|
||||
@@ -81,6 +81,11 @@ export default function ClashMeta_Producer() {
|
||||
proxy['preshared-key'] =
|
||||
proxy['preshared-key'] ?? proxy['pre-shared-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 (
|
||||
@@ -103,11 +108,21 @@ 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-fingerprint'];
|
||||
if (
|
||||
['grpc'].includes(proxy.network) &&
|
||||
proxy[`${proxy.network}-opts`]
|
||||
) {
|
||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||
}
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
|
||||
@@ -108,11 +108,21 @@ 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-fingerprint'];
|
||||
if (
|
||||
['grpc'].includes(proxy.network) &&
|
||||
proxy[`${proxy.network}-opts`]
|
||||
) {
|
||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||
}
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
|
||||
@@ -120,10 +120,20 @@ export default function Stash_Producer() {
|
||||
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-fingerprint'];
|
||||
if (
|
||||
['grpc'].includes(proxy.network) &&
|
||||
proxy[`${proxy.network}-opts`]
|
||||
) {
|
||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||
}
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
|
||||
@@ -71,6 +71,12 @@ function shadowsocks(proxy) {
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -107,6 +113,12 @@ function trojan(proxy) {
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -150,6 +162,12 @@ function vmess(proxy) {
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -182,6 +200,12 @@ function http(proxy) {
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -216,6 +240,12 @@ function socks5(proxy) {
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -245,6 +275,12 @@ function snell(proxy) {
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
// reuse
|
||||
result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
|
||||
|
||||
@@ -288,6 +324,12 @@ function tuic(proxy) {
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -304,10 +346,7 @@ function wireguard(proxy) {
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
@@ -316,6 +355,12 @@ function wireguard(proxy) {
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,16 @@ export default function URI_Producer() {
|
||||
? vmessTransportHost[0]
|
||||
: 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));
|
||||
break;
|
||||
@@ -141,6 +151,12 @@ export default function URI_Producer() {
|
||||
let vlessTransport = `&type=${encodeURIComponent(
|
||||
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 =
|
||||
proxy[`${proxy.network}-opts`]?.[
|
||||
|
||||
@@ -20,6 +20,8 @@ import registerArtifactRoutes from '@/restful/artifacts';
|
||||
import registerSettingRoutes from '@/restful/settings';
|
||||
import registerMiscRoutes from '@/restful/miscs';
|
||||
import registerSortRoutes from '@/restful/sort';
|
||||
import registerFileRoutes from '@/restful/file';
|
||||
import registerModuleRoutes from '@/restful/module';
|
||||
|
||||
migrate();
|
||||
serve();
|
||||
@@ -30,6 +32,8 @@ function serve() {
|
||||
// register routes
|
||||
registerCollectionRoutes($app);
|
||||
registerSubscriptionRoutes($app);
|
||||
registerFileRoutes($app);
|
||||
registerModuleRoutes($app);
|
||||
registerArtifactRoutes($app);
|
||||
registerSettingRoutes($app);
|
||||
registerSortRoutes($app);
|
||||
|
||||
109
backend/src/restful/file.js
Normal file
109
backend/src/restful/file.js
Normal 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);
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import $ from '@/core/app';
|
||||
import registerSubscriptionRoutes from './subscriptions';
|
||||
import registerCollectionRoutes from './collections';
|
||||
import registerArtifactRoutes from './artifacts';
|
||||
import registerFileRoutes from './file';
|
||||
import registerModuleRoutes from './module';
|
||||
import registerSyncRoutes from './sync';
|
||||
import registerDownloadRoutes from './download';
|
||||
import registerSettingRoutes from './settings';
|
||||
@@ -23,6 +25,8 @@ export default function serve() {
|
||||
registerSortingRoutes($app);
|
||||
registerSettingRoutes($app);
|
||||
registerArtifactRoutes($app);
|
||||
registerFileRoutes($app);
|
||||
registerModuleRoutes($app);
|
||||
registerSyncRoutes($app);
|
||||
registerNodeInfoRoutes($app);
|
||||
registerMiscRoutes($app);
|
||||
|
||||
114
backend/src/restful/module.js
Normal file
114
backend/src/restful/module.js
Normal 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);
|
||||
}
|
||||
@@ -12,98 +12,122 @@ export default function register($app) {
|
||||
}
|
||||
|
||||
async function compareSub(req, res) {
|
||||
const sub = req.body;
|
||||
const target = req.query.target || 'JSON';
|
||||
let content;
|
||||
if (sub.source === 'local') {
|
||||
content = sub.content;
|
||||
} else {
|
||||
try {
|
||||
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 {
|
||||
const sub = req.body;
|
||||
const target = req.query.target || 'JSON';
|
||||
let content;
|
||||
if (sub.source === 'local') {
|
||||
content = sub.content;
|
||||
} else {
|
||||
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);
|
||||
// apply processors
|
||||
currentProxies = await ProxyUtils.process(
|
||||
currentProxies,
|
||||
sub.process || [],
|
||||
'JSON',
|
||||
);
|
||||
results[name] = currentProxies;
|
||||
content = await download(sub.url, sub.ua);
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'PROCESS_FAILED',
|
||||
`处理子订阅 ${name} 失败`,
|
||||
new NetworkError(
|
||||
'FAILED_TO_DOWNLOAD_RESOURCE',
|
||||
'无法下载远程资源',
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
// parse proxies
|
||||
const original = ProxyUtils.parse(content);
|
||||
|
||||
// merge proxies with the original order
|
||||
const original = Array.prototype.concat.apply(
|
||||
[],
|
||||
subnames.map((name) => results[name] || []),
|
||||
);
|
||||
// add id
|
||||
original.forEach((proxy, i) => {
|
||||
proxy.id = i;
|
||||
});
|
||||
|
||||
original.forEach((proxy, i) => {
|
||||
proxy.id = i;
|
||||
});
|
||||
// apply processors
|
||||
const processed = await ProxyUtils.process(
|
||||
original,
|
||||
sub.process || [],
|
||||
target,
|
||||
);
|
||||
|
||||
const processed = await ProxyUtils.process(
|
||||
original,
|
||||
collection.process || [],
|
||||
'JSON',
|
||||
);
|
||||
|
||||
success(res, { original, processed });
|
||||
// produce
|
||||
success(res, { original, processed });
|
||||
} catch (err) {
|
||||
$.error(err.message ?? err);
|
||||
failed(
|
||||
res,
|
||||
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);
|
||||
// apply processors
|
||||
currentProxies = await ProxyUtils.process(
|
||||
currentProxies,
|
||||
sub.process || [],
|
||||
'JSON',
|
||||
);
|
||||
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;
|
||||
});
|
||||
|
||||
const processed = await ProxyUtils.process(
|
||||
original,
|
||||
collection.process || [],
|
||||
'JSON',
|
||||
);
|
||||
|
||||
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}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function register($app) {
|
||||
function sortSubs(req, res) {
|
||||
const orders = req.body;
|
||||
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);
|
||||
success(res, allSubs);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ function sortSubs(req, res) {
|
||||
function sortCollections(req, res) {
|
||||
const orders = req.body;
|
||||
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);
|
||||
success(res, allCols);
|
||||
}
|
||||
@@ -27,7 +27,9 @@ function sortCollections(req, res) {
|
||||
function sortArtifacts(req, res) {
|
||||
const orders = req.body;
|
||||
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);
|
||||
success(res, allArtifacts);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
import { FILES_KEY, MODULES_KEY } from '@/constants';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { HTTP, ENV } from '@/vendor/open-api';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
import $ from '@/core/app';
|
||||
|
||||
const tasks = new Map();
|
||||
|
||||
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();
|
||||
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
|
||||
const id = hex_md5(ua + url);
|
||||
|
||||
28
backend/src/vendor/open-api.js
vendored
28
backend/src/vendor/open-api.js
vendored
@@ -49,18 +49,23 @@ export class OpenAPI {
|
||||
|
||||
if (isNode) {
|
||||
// create a json for root cache
|
||||
let fpath = 'root.json';
|
||||
if (!this.node.fs.existsSync(fpath)) {
|
||||
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
|
||||
const basePath =
|
||||
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
||||
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',
|
||||
});
|
||||
this.root = {};
|
||||
} 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
|
||||
fpath = `${this.name}.json`;
|
||||
let fpath = `${basePath}/${this.name}.json`;
|
||||
this.log(`Data path: ${fpath}`);
|
||||
if (!this.node.fs.existsSync(fpath)) {
|
||||
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
|
||||
flag: 'wx',
|
||||
@@ -68,7 +73,7 @@ export class OpenAPI {
|
||||
this.cache = {};
|
||||
} else {
|
||||
this.cache = JSON.parse(
|
||||
this.node.fs.readFileSync(`${this.name}.json`),
|
||||
this.node.fs.readFileSync(`${fpath}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -80,14 +85,17 @@ export class OpenAPI {
|
||||
if (isQX) $prefs.setValueForKey(data, this.name);
|
||||
if (isLoon || isSurge) $persistentStore.write(data, this.name);
|
||||
if (isNode) {
|
||||
const basePath =
|
||||
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
||||
|
||||
this.node.fs.writeFileSync(
|
||||
`${this.name}.json`,
|
||||
`${basePath}/${this.name}.json`,
|
||||
data,
|
||||
{ flag: 'w' },
|
||||
(err) => console.log(err),
|
||||
);
|
||||
this.node.fs.writeFileSync(
|
||||
'root.json',
|
||||
`${basePath}/root.json`,
|
||||
JSON.stringify(this.root, null, 2),
|
||||
{ flag: 'w' },
|
||||
(err) => console.log(err),
|
||||
@@ -308,7 +316,9 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
|
||||
return (
|
||||
timer
|
||||
? Promise.race([timer, worker]).then((res) => {
|
||||
clearTimeout(timeoutid);
|
||||
if (typeof clearTimeout !== 'undefined') {
|
||||
clearTimeout(timeoutid);
|
||||
}
|
||||
return res;
|
||||
})
|
||||
: worker
|
||||
|
||||
Reference in New Issue
Block a user