Compare commits

...

5 Commits

Author SHA1 Message Date
xream
ef4d0a228b feat: 支持从 Gist 恢复时保留当前 GitHub Token 2025-07-18 14:54:13 +08:00
xream
e816e5b3c0 fix: 尝试修复上传 Gist 2025-07-18 13:37:35 +08:00
xream
005051c4ac feat: Node 运行环境支持 SOCKS5 代理 2025-07-18 13:13:43 +08:00
xream
61078b10f3 feat: 备份数据到 Gist 时, 清除 GitHub Token. 恢复后请重新设置 GitHub Token
Some checks failed
build / build (push) Has been cancelled
2025-07-18 10:14:18 +08:00
xream
bfa1b11a0e feat: 正式弃用 SUB_STORE_BACKEND_CRON 和 SUB_STORE_CRON, 请使用 SUB_STORE_BACKEND_SYNC_CRON
Some checks failed
build / build (push) Has been cancelled
2025-07-17 18:49:34 +08:00
7 changed files with 117 additions and 24 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.19.76",
"version": "2.19.83",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
"main": "src/main.js",
"scripts": {
@@ -32,6 +32,7 @@
"dns-packet": "^5.6.1",
"dotenv": "^16.4.7",
"express": "^4.17.1",
"fetch-socks": "^1.3.2",
"http-proxy-middleware": "^3.0.3",
"ip-address": "^9.0.5",
"js-base64": "^3.7.2",

26
backend/pnpm-lock.yaml generated
View File

@@ -40,6 +40,9 @@ importers:
express:
specifier: ^4.17.1
version: 4.21.2
fetch-socks:
specifier: ^1.3.2
version: 1.3.2
http-proxy-middleware:
specifier: ^3.0.3
version: 3.0.3
@@ -1974,6 +1977,9 @@ packages:
fastq@1.18.0:
resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==}
fetch-socks@1.3.2:
resolution: {integrity: sha512-vkH5+Zgj2yEbU57Cei0iyLgTZ4OkEKJj56Xu3ViB5dpsl599JgEooQ3x6NVagIFRHWnWJ+7K0MO0aIV1TMgvnw==}
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -3636,6 +3642,10 @@ packages:
resolution: {integrity: sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==}
engines: {node: '>=0.10.0'}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
snapdragon-node@2.1.1:
resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==}
engines: {node: '>=0.10.0'}
@@ -3648,6 +3658,10 @@ packages:
resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==}
engines: {node: '>=0.10.0'}
socks@2.8.6:
resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
source-map-generator@0.8.0:
resolution: {integrity: sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==}
engines: {node: '>= 10'}
@@ -6553,6 +6567,11 @@ snapshots:
dependencies:
reusify: 1.0.4
fetch-socks@1.3.2:
dependencies:
socks: 2.8.6
undici: 7.4.0
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.2.0
@@ -8424,6 +8443,8 @@ snapshots:
slash@1.0.0: {}
smart-buffer@4.2.0: {}
snapdragon-node@2.1.1:
dependencies:
define-property: 1.0.0
@@ -8447,6 +8468,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
socks@2.8.6:
dependencies:
ip-address: 9.0.5
smart-buffer: 4.2.0
source-map-generator@0.8.0: {}
source-map-resolve@0.5.3:

View File

@@ -105,11 +105,11 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
proxy.type = "wireguard-surge";
handleShadowTLS();
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/fast_open/tfo/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "hysteria2";
handleShadowTLS();
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5";
handleShadowTLS();
}
@@ -121,7 +121,6 @@ socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek pas
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {
proxy.type = "direct";
}
address = comma server:server comma port:port {
proxy.server = server;
proxy.port = port;
@@ -212,7 +211,7 @@ ws_headers = comma "ws-headers" equals headers:$[^,]+ {
const result = {};
pairs.forEach(pair => {
const [key, value] = pair.trim().split(":");
result[key.trim()] = value.trim().trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1');
result[key.trim()] = value.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1');
})
obfs["ws-headers"] = result;
}

View File

@@ -119,10 +119,11 @@ export default function serve() {
$app.start();
if ($.env.isNode) {
// Deprecated: SUB_STORE_BACKEND_CRON
const backend_sync_cron =
eval('process.env.SUB_STORE_BACKEND_SYNC_CRON') ||
eval('process.env.SUB_STORE_BACKEND_CRON');
// Deprecated: SUB_STORE_BACKEND_CRON, SUB_STORE_CRON
const backend_sync_cron = eval(
'process.env.SUB_STORE_BACKEND_SYNC_CRON',
);
if (backend_sync_cron) {
$.info(`[SYNC CRON] ${backend_sync_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
@@ -145,6 +146,17 @@ export default function serve() {
true, // start
// 'Asia/Shanghai' // timeZone
);
} else {
if (eval('process.env.SUB_STORE_BACKEND_CRON')) {
$.error(
`[SYNC CRON] SUB_STORE_BACKEND_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,
);
}
if (eval('process.env.SUB_STORE_CRON')) {
$.error(
`[SYNC CRON] SUB_STORE_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,
);
}
}
// 格式: 0 */2 * * *,sub,a;0 */3 * * *,col,b
// 每 2 小时处理一次单条订阅 a, 每 3 小时处理一次组合订阅 b

View File

@@ -1,3 +1,4 @@
import _ from 'lodash';
import $ from '@/core/app';
import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response';
@@ -104,7 +105,7 @@ async function refresh(_, res) {
success(res);
}
async function gistBackupAction(action) {
async function gistBackupAction(action, keep) {
// read token
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) throw new Error('GitHub Token is required for backup!');
@@ -114,6 +115,9 @@ async function gistBackupAction(action) {
key: GIST_BACKUP_KEY,
syncPlatform,
});
let currentContent = $.read('#sub-store');
currentContent = currentContent ? JSON.parse(currentContent) : {};
if ($.env.isNode) currentContent = JSON.parse(JSON.stringify($.cache));
let content;
const settings = $.read(SETTINGS_KEY);
const updated = settings.syncTime;
@@ -121,7 +125,10 @@ async function gistBackupAction(action) {
case 'upload':
try {
content = $.read('#sub-store');
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
content = content ? JSON.parse(content) : {};
if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));
content.settings.gistToken = '恢复后请重新设置 GitHub Token';
content = JSON.stringify(content, null, ` `);
$.info(`下载备份, 与本地内容对比...`);
const onlineContent = await gist.download(
GIST_BACKUP_FILE_NAME,
@@ -138,7 +145,10 @@ async function gistBackupAction(action) {
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
content = content ? JSON.parse(content) : {};
if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));
content.settings.gistToken = '恢复后请重新设置 GitHub Token';
content = JSON.stringify(content, null, ` `);
$.info(`上传备份中...`);
try {
await gist.upload({
@@ -156,7 +166,8 @@ async function gistBackupAction(action) {
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
try {
if (Object.keys(JSON.parse(content).settings).length === 0) {
content = JSON.parse(content);
if (Object.keys(content.settings).length === 0) {
throw new Error('备份文件应该至少包含 settings 字段');
}
} catch (err) {
@@ -167,10 +178,15 @@ async function gistBackupAction(action) {
);
throw new Error('Gist 备份文件校验失败, 无法还原');
}
if (keep) {
$.info(`保留原有设置 ${keep}`);
keep.split(',').forEach((path) => {
_.set(content, path, _.get(currentContent, path));
});
}
// restore settings
$.write(content, '#sub-store');
$.write(JSON.stringify(content, null, ` `), '#sub-store');
if ($.env.isNode) {
content = JSON.parse(content);
$.cache = content;
$.persistCache();
}
@@ -182,7 +198,7 @@ async function gistBackupAction(action) {
}
}
async function gistBackup(req, res) {
const { action } = req.query;
const { action, keep } = req.query;
// read token
const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) {
@@ -195,7 +211,7 @@ async function gistBackup(req, res) {
);
} else {
try {
await gistBackupAction(action);
await gistBackupAction(action, keep);
success(res);
} catch (err) {
$.error(

View File

@@ -18,6 +18,26 @@ function isPlainObject(obj) {
);
}
function parseSocks5Uri(uri) {
// eslint-disable-next-line no-unused-vars
let [__, username, password, server, port, query, name] = uri.match(
/^socks5:\/\/(?:(.*?):(.*?)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
);
if (port) {
port = parseInt(port, 10);
} else {
$.error(`port is not present in line: ${uri}`);
throw new Error(`port is not present in line: ${uri}`);
}
return {
type: 5,
host: server,
port,
userId: username != null ? decodeURIComponent(username) : undefined,
password: password != null ? decodeURIComponent(password) : undefined,
};
}
export class OpenAPI {
constructor(name = 'untitled', debug = false) {
this.name = name;
@@ -393,6 +413,7 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
}
if (isNode) {
const undici = eval("require('undici')");
const { socksDispatcher } = eval("require('fetch-socks')");
const {
ProxyAgent,
EnvHttpProxyAgent,
@@ -422,16 +443,34 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
).toString('base64')}`,
};
}
let dispatcher;
if (!opts.proxy) {
const allProxy =
eval('process.env.all_proxy') ||
eval('process.env.ALL_PROXY');
if (allProxy && /^socks5:\/\//.test(allProxy)) {
opts.proxy = allProxy;
}
}
if (opts.proxy) {
if (/^socks5:\/\//.test(opts.proxy)) {
dispatcher = socksDispatcher(
parseSocks5Uri(opts.proxy),
agentOpts,
);
} else {
dispatcher = new ProxyAgent({
...agentOpts,
uri: opts.proxy,
});
}
} else {
dispatcher = new EnvHttpProxyAgent(agentOpts);
}
const response = await request(opts.url, {
...opts,
method: method.toUpperCase(),
dispatcher: (opts.proxy
? new ProxyAgent({
...agentOpts,
uri: opts.proxy,
})
: new EnvHttpProxyAgent(agentOpts)
).compose(
dispatcher: dispatcher.compose(
interceptors.redirect({
maxRedirections: 3,
throwOnMaxRedirects: true,