mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5d8aff73c | ||
|
|
5696492dde | ||
|
|
e6d05fd873 | ||
|
|
4111b8fabf | ||
|
|
dfc619a181 | ||
|
|
ff5283a66f | ||
|
|
6c54518e84 | ||
|
|
dd92a26e6c | ||
|
|
bb5c9d43d0 | ||
|
|
e54ac92357 | ||
|
|
507e37021c | ||
|
|
a70dc7b913 | ||
|
|
fc56df7bfd | ||
|
|
1281df59f3 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.18.2",
|
||||
"version": "2.19.1",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
@@ -27,10 +27,12 @@
|
||||
"automerge": "1.0.1-preview.7",
|
||||
"body-parser": "^1.19.0",
|
||||
"buffer": "^6.0.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"cron": "^3.1.6",
|
||||
"dns-packet": "^5.6.1",
|
||||
"express": "^4.17.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"ip-address": "^9.0.5",
|
||||
"js-base64": "^3.7.2",
|
||||
|
||||
@@ -342,7 +342,7 @@ function URI_VMess() {
|
||||
};
|
||||
const parse = (line) => {
|
||||
line = line.split('vmess://')[1];
|
||||
let content = Base64.decode(line);
|
||||
let content = Base64.decode(line.replace(/\?.*?$/, ''));
|
||||
if (/=\s*vmess/.test(content)) {
|
||||
// Quantumult VMess URI format
|
||||
const partitions = content.split(',').map((p) => p.trim());
|
||||
|
||||
@@ -75,6 +75,8 @@ function Clash() {
|
||||
// 是否被引号包裹
|
||||
if (/^(['"]).*\1$/.test(afterTrim)) {
|
||||
return `short-id: ${afterTrim}`;
|
||||
} else if (['null'].includes(afterTrim)) {
|
||||
return `short-id: ${afterTrim}`;
|
||||
} else {
|
||||
return `short-id: "${afterTrim}"`;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
||||
*/
|
||||
import { version } from '../package.json';
|
||||
import $ from '@/core/app';
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
@@ -18,7 +19,6 @@ console.log(
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
|
||||
import migrate from '@/utils/migration';
|
||||
import serve from '@/restful';
|
||||
|
||||
|
||||
@@ -89,8 +89,10 @@ async function doSync() {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const subNames = [];
|
||||
let enabledCount = 0;
|
||||
allArtifacts.map((artifact) => {
|
||||
if (artifact.sync && artifact.source) {
|
||||
enabledCount++;
|
||||
if (artifact.type === 'subscription') {
|
||||
const subName = artifact.source;
|
||||
const sub = findByName(allSubs, subName);
|
||||
@@ -111,6 +113,13 @@ async function doSync() {
|
||||
}
|
||||
});
|
||||
|
||||
if (enabledCount === 0) {
|
||||
$.info(
|
||||
`需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subNames.length > 0) {
|
||||
await Promise.all(
|
||||
subNames.map(async (subName) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import $ from '@/core/app';
|
||||
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
|
||||
import { formatDateTime } from '@/utils';
|
||||
|
||||
export default function register($app) {
|
||||
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
|
||||
@@ -60,20 +61,9 @@ function getCollection(req, res) {
|
||||
.set(
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent(
|
||||
`sub-store_collection_${name}_${new Date()
|
||||
.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.replace(
|
||||
/^(\d+?)\/(\d+?)\/(\d+?)\s*?(\d+?):(\d+?):(\d+?)$/,
|
||||
'$1-$2-$3_$4-$5-$6',
|
||||
)}.json`,
|
||||
`sub-store_collection_${name}_${formatDateTime(
|
||||
new Date(),
|
||||
)}.json`,
|
||||
)}"`,
|
||||
)
|
||||
.send(JSON.stringify(collection));
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
InternalServerError,
|
||||
} from '@/restful/errors';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
import { formatDateTime } from '@/utils';
|
||||
|
||||
export default function register($app) {
|
||||
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
|
||||
@@ -210,20 +211,9 @@ function getWholeFile(req, res) {
|
||||
.set(
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent(
|
||||
`sub-store_file_${name}_${new Date()
|
||||
.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.replace(
|
||||
/^(\d+?)\/(\d+?)\/(\d+?)\s*?(\d+?):(\d+?):(\d+?)$/,
|
||||
'$1-$2-$3_$4-$5-$6',
|
||||
)}.json`,
|
||||
`sub-store_file_${name}_${formatDateTime(
|
||||
new Date(),
|
||||
)}.json`,
|
||||
)}"`,
|
||||
)
|
||||
.send(JSON.stringify(file));
|
||||
|
||||
@@ -29,6 +29,72 @@ export default function serve() {
|
||||
host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::';
|
||||
}
|
||||
const $app = express({ substore: $, port, host });
|
||||
if ($.env.isNode) {
|
||||
const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');
|
||||
const be_prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX');
|
||||
const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
|
||||
const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');
|
||||
if (be_prefix || be_merge) {
|
||||
if(!fe_be_path.startsWith('/')){
|
||||
throw new Error(
|
||||
'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
|
||||
);
|
||||
}
|
||||
if (be_merge) {
|
||||
$.info(`[BACKEND] MERGE mode is [ON].`);
|
||||
$.info(`[BACKEND && FRONTEND] ${host}:${port}`);
|
||||
}
|
||||
$.info(`[BACKEND PREFIX] ${host}:${port}${fe_be_path}`);
|
||||
$app.use((req, res, next) => {
|
||||
if (req.path.startsWith(fe_be_path)) {
|
||||
req.url = req.url.replace(fe_be_path, '') || '/';
|
||||
if(be_merge && req.url.startsWith('/api/')){
|
||||
req.query['share'] = 'true';
|
||||
}
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const pathname = decodeURIComponent(req._parsedUrl.pathname) || '/';
|
||||
if(be_merge && req.path.startsWith('/share/') && req.query.token){
|
||||
if (req.method.toLowerCase() !== 'get'){
|
||||
res.status(405).send('Method not allowed');
|
||||
return;
|
||||
}
|
||||
const tokens = $.read(TOKENS_KEY) || [];
|
||||
const token = tokens.find(
|
||||
(t) =>
|
||||
t.token === req.query.token &&
|
||||
`/share/${t.type}/${t.name}` === pathname &&
|
||||
(t.exp == null || t.exp > Date.now()),
|
||||
);
|
||||
if (token){
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (be_merge && fe_path && req.path.indexOf('/',1) == -1) {
|
||||
if (req.path.indexOf('.') == -1){
|
||||
req.url = "/index.html"
|
||||
}
|
||||
const express_ = eval(`require("express")`);
|
||||
const mime_ = eval(`require("mime-types")`);
|
||||
const path_ = eval(`require("path")`);
|
||||
const staticFileMiddleware = express_.static(fe_path, {
|
||||
setHeaders: (res, path) => {
|
||||
const type = mime_.contentType(path_.extname(path));
|
||||
if (type) {
|
||||
res.set('Content-Type', type);
|
||||
}
|
||||
}
|
||||
});
|
||||
staticFileMiddleware(req, res, next);
|
||||
return;
|
||||
}
|
||||
res.status(403).end('Forbbiden');
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
// register routes
|
||||
registerCollectionRoutes($app);
|
||||
registerSubscriptionRoutes($app);
|
||||
@@ -175,7 +241,8 @@ export default function serve() {
|
||||
const fe_abs_path = path.resolve(
|
||||
fe_path || path.join(__dirname, 'frontend'),
|
||||
);
|
||||
if (fe_path) {
|
||||
const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');
|
||||
if (fe_path && !be_merge) {
|
||||
try {
|
||||
fs.accessSync(path.join(fe_abs_path, 'index.html'));
|
||||
} catch (e) {
|
||||
@@ -200,6 +267,9 @@ export default function serve() {
|
||||
let be_download_rewrite = '';
|
||||
let be_api_rewrite = '';
|
||||
let be_share_rewrite = `${be_share}:type/:name`;
|
||||
let prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX')
|
||||
? fe_be_path
|
||||
: '';
|
||||
if (fe_be_path) {
|
||||
if (!fe_be_path.startsWith('/')) {
|
||||
throw new Error(
|
||||
@@ -216,7 +286,7 @@ export default function serve() {
|
||||
app.use(
|
||||
be_share_rewrite,
|
||||
createProxyMiddleware({
|
||||
target: `http://127.0.0.1:${port}`,
|
||||
target: `http://127.0.0.1:${port}${prefix}`,
|
||||
changeOrigin: true,
|
||||
pathRewrite: async (path, req) => {
|
||||
if (req.method.toLowerCase() !== 'get')
|
||||
@@ -237,7 +307,7 @@ export default function serve() {
|
||||
app.use(
|
||||
be_api_rewrite,
|
||||
createProxyMiddleware({
|
||||
target: `http://127.0.0.1:${port}${be_api}`,
|
||||
target: `http://127.0.0.1:${port}${prefix}${be_api}`,
|
||||
pathRewrite: async (path) => {
|
||||
return path.includes('?')
|
||||
? `${path}&share=true`
|
||||
@@ -248,7 +318,7 @@ export default function serve() {
|
||||
app.use(
|
||||
be_download_rewrite,
|
||||
createProxyMiddleware({
|
||||
target: `http://127.0.0.1:${port}${be_download}`,
|
||||
target: `http://127.0.0.1:${port}${prefix}${be_download}`,
|
||||
changeOrigin: true,
|
||||
}),
|
||||
);
|
||||
@@ -269,10 +339,10 @@ export default function serve() {
|
||||
$.info(`[FRONTEND] ${fe_address}:${fe_port}`);
|
||||
if (fe_be_path) {
|
||||
$.info(
|
||||
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> http://127.0.0.1:${port}${be_api}`,
|
||||
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> ${host}:${port}${prefix}${be_api}`,
|
||||
);
|
||||
$.info(
|
||||
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> http://127.0.0.1:${port}${be_download}`,
|
||||
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> ${host}:${port}${prefix}${be_download}`,
|
||||
);
|
||||
$.info(
|
||||
`[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { InternalServerError, RequestInvalidError } from '@/restful/errors';
|
||||
import Gist from '@/utils/gist';
|
||||
import migrate from '@/utils/migration';
|
||||
import env from '@/utils/env';
|
||||
import { formatDateTime } from '@/utils';
|
||||
|
||||
export default function register($app) {
|
||||
// utils
|
||||
@@ -28,20 +29,7 @@ export default function register($app) {
|
||||
.set(
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent(
|
||||
`sub-store_data_${new Date()
|
||||
.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.replace(
|
||||
/^(\d+?)\/(\d+?)\/(\d+?)\s*?(\d+?):(\d+?):(\d+?)$/,
|
||||
'$1-$2-$3_$4-$5-$6',
|
||||
)}.json`,
|
||||
`sub-store_data_${formatDateTime(new Date())}.json`,
|
||||
)}"`,
|
||||
)
|
||||
.send(
|
||||
|
||||
@@ -134,11 +134,15 @@ export async function updateArtifactStore() {
|
||||
settings.artifactStore = url;
|
||||
settings.artifactStoreStatus = 'VALID';
|
||||
} else {
|
||||
$.error(`找不到 Sub-Store Gist`);
|
||||
$.error(`找不到 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY})`);
|
||||
settings.artifactStoreStatus = 'NOT FOUND';
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`);
|
||||
$.error(
|
||||
`查找 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY}) 时发生错误: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
settings.artifactStoreStatus = 'ERROR';
|
||||
}
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@/utils/flow';
|
||||
import { success, failed } from './response';
|
||||
import $ from '@/core/app';
|
||||
import { formatDateTime } from '@/utils';
|
||||
|
||||
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
|
||||
|
||||
@@ -265,20 +266,9 @@ function getSubscription(req, res) {
|
||||
.set(
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent(
|
||||
`sub-store_subscription_${name}_${new Date()
|
||||
.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.replace(
|
||||
/^(\d+?)\/(\d+?)\/(\d+?)\s*?(\d+?):(\d+?):(\d+?)$/,
|
||||
'$1-$2-$3_$4-$5-$6',
|
||||
)}.json`,
|
||||
`sub-store_subscription_${name}_${formatDateTime(
|
||||
new Date(),
|
||||
)}.json`,
|
||||
)}"`,
|
||||
)
|
||||
.send(JSON.stringify(sub));
|
||||
|
||||
@@ -556,8 +556,10 @@ async function syncArtifacts() {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const subNames = [];
|
||||
let enabledCount = 0;
|
||||
allArtifacts.map((artifact) => {
|
||||
if (artifact.sync && artifact.source) {
|
||||
enabledCount++;
|
||||
if (artifact.type === 'subscription') {
|
||||
const subName = artifact.source;
|
||||
const sub = findByName(allSubs, subName);
|
||||
@@ -578,6 +580,13 @@ async function syncArtifacts() {
|
||||
}
|
||||
});
|
||||
|
||||
if (enabledCount === 0) {
|
||||
$.info(
|
||||
`需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subNames.length > 0) {
|
||||
await Promise.all(
|
||||
subNames.map(async (subName) => {
|
||||
|
||||
@@ -280,7 +280,7 @@ export default class Gist {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject('找不到 Sub-Store Gist');
|
||||
return Promise.reject(`找不到 Sub-Store Gist (${this.key})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,32 @@ function isValidUUID(uuid) {
|
||||
);
|
||||
}
|
||||
|
||||
function formatDateTime(date, format = 'YYYY-MM-DD_HH-mm-ss') {
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
|
||||
if (isNaN(d.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const pad = (num) => String(num).padStart(2, '0');
|
||||
|
||||
const replacements = {
|
||||
YYYY: d.getFullYear(),
|
||||
MM: pad(d.getMonth() + 1),
|
||||
DD: pad(d.getDate()),
|
||||
HH: pad(d.getHours()),
|
||||
mm: pad(d.getMinutes()),
|
||||
ss: pad(d.getSeconds()),
|
||||
};
|
||||
|
||||
return format.replace(
|
||||
/YYYY|MM|DD|HH|mm|ss/g,
|
||||
(match) => replacements[match],
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
formatDateTime,
|
||||
isValidUUID,
|
||||
ipAddress,
|
||||
isIPv4,
|
||||
|
||||
21
backend/src/vendor/open-api.js
vendored
21
backend/src/vendor/open-api.js
vendored
@@ -18,6 +18,10 @@ export class OpenAPI {
|
||||
this.http = HTTP();
|
||||
this.env = ENV();
|
||||
|
||||
if (isNode) {
|
||||
const dotenv = eval(`require("dotenv")`);
|
||||
dotenv.config();
|
||||
}
|
||||
this.node = (() => {
|
||||
if (isNode) {
|
||||
const fs = eval("require('fs')");
|
||||
@@ -360,7 +364,12 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
|
||||
}
|
||||
if (isNode) {
|
||||
const undici = eval("require('undici')");
|
||||
const { ProxyAgent, EnvHttpProxyAgent, request } = undici;
|
||||
const {
|
||||
ProxyAgent,
|
||||
EnvHttpProxyAgent,
|
||||
request,
|
||||
interceptors,
|
||||
} = undici;
|
||||
const agentOpts = {
|
||||
connect: {
|
||||
rejectUnauthorized:
|
||||
@@ -387,12 +396,18 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
|
||||
const response = await request(opts.url, {
|
||||
...opts,
|
||||
method: method.toUpperCase(),
|
||||
dispatcher: opts.proxy
|
||||
dispatcher: (opts.proxy
|
||||
? new ProxyAgent({
|
||||
...agentOpts,
|
||||
uri: opts.proxy,
|
||||
})
|
||||
: new EnvHttpProxyAgent(agentOpts),
|
||||
: new EnvHttpProxyAgent(agentOpts)
|
||||
).compose(
|
||||
interceptors.redirect({
|
||||
maxRedirections: 3,
|
||||
throwOnMaxRedirects: true,
|
||||
}),
|
||||
),
|
||||
});
|
||||
resolve({
|
||||
statusCode: response.statusCode,
|
||||
|
||||
Reference in New Issue
Block a user