Compare commits

...

9 Commits

Author SHA1 Message Date
xream
c8c22c3901 fix: 修复 VMess URI SNI 2024-11-01 20:27:23 +08:00
xream
d8f9466b84 feat(wip): 支持自定义 share token 2024-10-31 23:33:34 +08:00
xream
d12ccad382 feat: MMDB 加入 $utils.ipasn 2024-10-31 01:39:13 +08:00
xream
b4358663cc feat(wip): 支持 JWT 2024-10-31 00:23:45 +08:00
xream
aba6264988 feat(wip): 支持 JWT 2024-10-30 23:08:01 +08:00
xream
2320ab3838 feat(wip): 支持 JWT 2024-10-30 22:51:31 +08:00
xream
542957d34a feat(wip): 支持 JWT 2024-10-30 22:27:39 +08:00
xream
07e50175f9 feat: cipher 应为小写 2024-10-30 16:07:27 +08:00
xream
e09d66060d feat: 远程订阅支持 insecure 不验证服务器证书 2024-10-30 14:33:34 +08:00
15 changed files with 7490 additions and 7415 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.14.405",
"version": "2.14.412",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
@@ -30,11 +30,10 @@
"js-base64": "^3.7.2",
"jsrsasign": "^11.1.0",
"lodash": "^4.17.21",
"ms": "^2.1.3",
"nanoid": "^3.3.3",
"request": "^2.88.2",
"requests": "^0.3.0",
"semver": "^7.3.7",
"static-js-yaml": "^1.0.0",
"uuid": "^8.3.2"
"static-js-yaml": "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.18.0",

14661
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ export const FILES_KEY = 'files';
export const MODULES_KEY = 'modules';
export const ARTIFACTS_KEY = 'artifacts';
export const RULES_KEY = 'rules';
export const TOKENS_KEY = 'tokens';
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';

View File

@@ -327,6 +327,9 @@ function formatTransportPath(path) {
}
function lastParse(proxy) {
if (typeof proxy.cipher === 'string') {
proxy.cipher = proxy.cipher.toLowerCase();
}
if (typeof proxy.password === 'number') {
proxy.password = numberToString(proxy.password);
}

View File

@@ -316,7 +316,7 @@ function URI_VMess() {
);
}
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
if (proxy.tls && proxy.sni) {
if (proxy.tls && params.sni && params.sni !== '') {
proxy.sni = params.sni;
}
let httpupgrade = false;

View File

@@ -13,6 +13,9 @@ import { getISO } from '@/utils/geo';
import env from '@/utils/env';
export default function register($app) {
$app.get('/share/col/:name', downloadCollection);
$app.get('/share/sub/:name', downloadSubscription);
$app.get('/download/collection/:name', downloadCollection);
$app.get('/download/:name', downloadSubscription);
$app.get(
@@ -185,7 +188,7 @@ async function downloadSubscription(req, res) {
if (!$arguments.noFlow) {
// forward flow headers
const flowInfo = await getFlowHeaders(
url,
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
proxy || sub.proxy,
@@ -378,7 +381,7 @@ async function downloadCollection(req, res) {
}
if (!$arguments.noFlow) {
const flowInfo = await getFlowHeaders(
url,
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
proxy || sub.proxy || collection.proxy,

View File

@@ -13,6 +13,8 @@ import { produceArtifact } from '@/restful/sync';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
$app.get('/share/file/:name', getFile);
$app.route('/api/file/:name')
.get(getFile)
.patch(updateFile)

View File

@@ -4,6 +4,7 @@ import migrate from '@/utils/migration';
import download from '@/utils/download';
import { syncArtifacts } from '@/restful/sync';
import { gistBackupAction } from '@/restful/miscs';
import { TOKENS_KEY } from '@/constants';
import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections';
@@ -143,7 +144,7 @@ export default function serve() {
try {
fs.accessSync(path.join(fe_abs_path, 'index.html'));
} catch (e) {
throw new Error(
$.error(
`[FRONTEND] index.html file not found in ${fe_abs_path}`,
);
}
@@ -158,6 +159,7 @@ export default function serve() {
const staticFileMiddleware = express_.static(fe_path);
let be_share_rewrite = '/share/:type/:name';
let be_api_rewrite = '';
let be_download_rewrite = '';
let be_api = '/api/';
@@ -174,15 +176,39 @@ export default function serve() {
be_download_rewrite = `${
fe_be_path === '/' ? '' : fe_be_path
}${be_download}`;
app.use(
be_share_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
pathRewrite: (path, req) => {
if (req.method.toLowerCase() !== 'get')
throw new Error('Method not allowed');
const tokens = $.read(TOKENS_KEY) || [];
const token = tokens.find(
(t) =>
t.token === req.query.token &&
t.type === req.params.type &&
t.name === req.params.name &&
(t.exp == null || t.exp > Date.now()),
);
if (!token) throw new Error('Forbbiden');
return path;
},
}),
);
app.use(
be_api_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
pathRewrite: (path) => {
return path.startsWith(be_api_rewrite)
const newPath = path.startsWith(be_api_rewrite)
? path.replace(be_api_rewrite, be_api)
: path;
return newPath.includes('?')
? `${newPath}&share=true`
: `${newPath}?share=true`;
},
}),
);
@@ -220,6 +246,9 @@ export default function serve() {
$.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> http://127.0.0.1:${port}${be_download}`,
);
$.info(
`[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`,
);
}
});
}

View File

@@ -9,6 +9,10 @@ import {
GIST_BACKUP_FILE_NAME,
GIST_BACKUP_KEY,
SETTINGS_KEY,
TOKENS_KEY,
FILES_KEY,
COLLECTIONS_KEY,
SUBS_KEY,
} from '@/constants';
import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import Gist from '@/utils/gist';
@@ -20,6 +24,7 @@ export default function register($app) {
$app.get('/api/utils/env', getEnv); // get runtime environment
$app.get('/api/utils/backup', gistBackup); // gist backup actions
$app.get('/api/utils/refresh', refresh);
$app.post('/api/token', signToken);
// Storage management
$app.route('/api/storage')
@@ -65,9 +70,151 @@ export default function register($app) {
}
function getEnv(req, res) {
if (req.query.share) {
env.feature.share = true;
}
success(res, env);
}
async function signToken(req, res) {
if (!ENV().isNode) {
return failed(
res,
new RequestInvalidError(
'INVALID_ENV',
`This endpoint is only available in Node.js environment`,
),
);
}
try {
const { payload, options } = req.body;
const ms = eval(`require("ms")`);
let token = payload?.token;
if (token != null) {
if (typeof token !== 'string' || token.length < 1) {
return failed(
res,
new RequestInvalidError(
'INVALID_CUSTOM_TOKEN',
`Invalid custom token: ${token}`,
),
);
}
const tokens = $.read(TOKENS_KEY) || [];
if (tokens.find((t) => t.token === token)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_TOKEN',
`Token ${token} already exists`,
),
);
}
}
const type = payload?.type;
const name = payload?.name;
if (!type || !name)
return failed(
res,
new RequestInvalidError(
'INVALID_PAYLOAD',
`payload type and name are required`,
),
);
if (type === 'col') {
const collections = $.read(COLLECTIONS_KEY) || [];
const collection = collections.find((c) => c.name === name);
if (!collection)
return failed(
res,
new RequestInvalidError(
'INVALID_COLLECTION',
`collection ${name} not found`,
),
);
} else if (type === 'file') {
const files = $.read(FILES_KEY) || [];
const file = files.find((f) => f.name === name);
if (!file)
return failed(
res,
new RequestInvalidError(
'INVALID_FILE',
`file ${name} not found`,
),
);
} else if (type === 'sub') {
const subs = $.read(SUBS_KEY) || [];
const sub = subs.find((s) => s.name === name);
if (!sub)
return failed(
res,
new RequestInvalidError(
'INVALID_SUB',
`sub ${name} not found`,
),
);
} else {
return failed(
res,
new RequestInvalidError(
'INVALID_TYPE',
`type ${name} not supported`,
),
);
}
let expiresIn = options?.expiresIn;
if (options?.expiresIn != null) {
expiresIn = ms(options.expiresIn);
if (expiresIn == null || isNaN(expiresIn) || expiresIn <= 0) {
return failed(
res,
new RequestInvalidError(
'INVALID_EXPIRES_IN',
`Invalid expiresIn option: ${options.expiresIn}`,
),
);
}
}
const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const nanoid = eval(`require("nanoid")`);
const tokens = $.read(TOKENS_KEY) || [];
// const now = Date.now();
// for (const key in tokens) {
// const token = tokens[key];
// if (token.exp != null || token.exp < now) {
// delete tokens[key];
// }
// }
if (!token) {
do {
token = nanoid.customAlphabet(nanoid.urlAlphabet)();
} while (tokens.find((t) => t.token === token));
}
tokens.push({
...payload,
token,
createdAt: Date.now(),
expiresIn: expiresIn > 0 ? options?.expiresIn : undefined,
exp: expiresIn > 0 ? Date.now() + expiresIn : undefined,
});
$.write(tokens, TOKENS_KEY);
return success(res, {
token,
secret,
});
} catch (e) {
return failed(
res,
new InternalServerError(
'TOKEN_SIGN_FAILED',
`Failed to sign token`,
`Reason: ${e.message ?? e}`,
),
);
}
}
async function refresh(_, res) {
// 1. get GitHub avatar and artifact store
await updateAvatar();

View File

@@ -152,7 +152,7 @@ async function getFlowInfo(req, res) {
}
} else {
const flowHeaders = await getFlowHeaders(
url,
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
sub.proxy,

View File

@@ -157,8 +157,13 @@ export default async function download(
$.write(cached, customCacheKey);
}
} else {
const insecure = $arguments?.insecure
? isNode
? { strictSSL: false }
: { insecure: true }
: undefined;
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nURL: ${url}`,
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nInsecure: ${!!insecure}\nURL: ${url}`,
);
try {
const { body, headers } = await http.get({
@@ -167,6 +172,7 @@ export default async function download(
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
if (headers) {

View File

@@ -24,6 +24,7 @@ if (isLanceX) backend = 'LanceX';
if (isGUIforCores) backend = 'GUI.for.Cores';
let meta = {};
let feature = {};
try {
if (typeof $environment !== 'undefined') {
@@ -63,5 +64,6 @@ try {
export default {
backend,
version: substoreVersion,
feature,
meta,
};

View File

@@ -47,6 +47,11 @@ export async function getFlowHeaders(
// $.info(`使用缓存的流量信息: ${url}`);
flowInfo = cached;
} else {
const insecure = $arguments?.insecure
? $.env.isNode
? { strictSSL: false }
: { insecure: true }
: undefined;
const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
$.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
@@ -64,7 +69,7 @@ export async function getFlowHeaders(
$.info(
`使用 GET 方法从响应体获取流量信息: ${flowUrl}, User-Agent: ${
userAgent || ''
}`,
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
);
const { body } = await http.get({
url: flowUrl,
@@ -72,6 +77,11 @@ export async function getFlowHeaders(
'User-Agent': userAgent,
},
timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = body;
} else {
@@ -79,7 +89,7 @@ export async function getFlowHeaders(
$.info(
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}, Proxy: ${proxy}`,
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
);
const { headers } = await http.head({
url: url
@@ -103,20 +113,23 @@ export async function getFlowHeaders(
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = getFlowField(headers);
} catch (e) {
$.error(
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
userAgent || ''
}, Proxy: ${proxy}: ${e.message ?? e}`,
}, Insecure: ${!!insecure}, Proxy: ${proxy}: ${
e.message ?? e
}`,
);
}
if (!flowInfo) {
$.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}, Proxy: ${proxy}`,
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
);
const { headers } = await http.get({
url: url
@@ -140,6 +153,7 @@ export async function getFlowHeaders(
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = getFlowField(headers);
}

View File

@@ -474,4 +474,7 @@ export class MMDB {
ipaso(ip) {
return this.asnReader?.asn(ip)?.autonomousSystemOrganization;
}
ipasn(ip) {
return this.asnReader?.asn(ip)?.autonomousSystemNumber;
}
}

View File

@@ -67,6 +67,7 @@ function operator(proxies = [], targetPlatform, context) {
// getISO, // 获取 ISO 3166-1 alpha-2 代码
// Gist, // Gist 类
// download, // 内部的下载方法, 见 backend/src/utils/download.js
// MMDB, // Node.js 环境 可用于模拟 Surge/Loon 的 $utils.ipasn, $utils.ipaso, $utils.geoip. 具体见 https://t.me/zhetengsha/1269
// }
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009