Compare commits

...

6 Commits

9 changed files with 242 additions and 64 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.14.342",
"version": "2.14.346",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
@@ -20,8 +20,10 @@
"@maxmind/geoip2-node": "^5.0.0",
"automerge": "1.0.1-preview.7",
"body-parser": "^1.19.0",
"buffer": "^6.0.3",
"connect-history-api-fallback": "^2.0.0",
"cron": "^3.1.6",
"dns-packet": "^5.6.1",
"express": "^4.17.1",
"http-proxy-middleware": "^2.0.6",
"js-base64": "^3.7.2",

32
backend/pnpm-lock.yaml generated
View File

@@ -14,12 +14,18 @@ dependencies:
body-parser:
specifier: ^1.19.0
version: registry.npmmirror.com/body-parser@1.19.0
buffer:
specifier: ^6.0.3
version: registry.npmmirror.com/buffer@6.0.3
connect-history-api-fallback:
specifier: ^2.0.0
version: registry.npmmirror.com/connect-history-api-fallback@2.0.0
cron:
specifier: ^3.1.6
version: registry.npmmirror.com/cron@3.1.6
dns-packet:
specifier: ^5.6.1
version: registry.npmmirror.com/dns-packet@5.6.1
express:
specifier: ^4.17.1
version: registry.npmmirror.com/express@4.17.1
@@ -1973,6 +1979,12 @@ packages:
'@jridgewell/sourcemap-codec': registry.npmmirror.com/@jridgewell/sourcemap-codec@1.4.13
dev: true
registry.npmmirror.com/@leichtgewicht/ip-codec@2.0.5:
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz}
name: '@leichtgewicht/ip-codec'
version: 2.0.5
dev: false
registry.npmmirror.com/@maxmind/geoip2-node@5.0.0:
resolution: {integrity: sha512-ki+q5//oU4tZ3BAhegZJcB5czoZyic5JSTEKbrUAQB/BzAoAiGyLW0immEmQvVVyy2SMlvBTJ3zqyRj8K9BbwQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@maxmind/geoip2-node/-/geoip2-node-5.0.0.tgz}
name: '@maxmind/geoip2-node'
@@ -2707,7 +2719,6 @@ packages:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz}
name: base64-js
version: 1.5.1
dev: true
registry.npmmirror.com/base@0.11.2:
resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/base/-/base-0.11.2.tgz}
@@ -3078,6 +3089,15 @@ packages:
ieee754: registry.npmmirror.com/ieee754@1.2.1
dev: true
registry.npmmirror.com/buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz}
name: buffer
version: 6.0.3
dependencies:
base64-js: registry.npmmirror.com/base64-js@1.5.1
ieee754: registry.npmmirror.com/ieee754@1.2.1
dev: false
registry.npmmirror.com/builtin-status-codes@3.0.0:
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz}
name: builtin-status-codes
@@ -4047,6 +4067,15 @@ packages:
randombytes: registry.npmmirror.com/randombytes@2.1.0
dev: true
registry.npmmirror.com/dns-packet@5.6.1:
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/dns-packet/-/dns-packet-5.6.1.tgz}
name: dns-packet
version: 5.6.1
engines: {node: '>=6'}
dependencies:
'@leichtgewicht/ip-codec': registry.npmmirror.com/@leichtgewicht/ip-codec@2.0.5
dev: false
registry.npmmirror.com/doctrine@3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz}
name: doctrine
@@ -5874,7 +5903,6 @@ packages:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz}
name: ieee754
version: 1.2.1
dev: true
registry.npmmirror.com/ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz}

View File

@@ -1,13 +1,8 @@
import { Buffer } from 'buffer';
import rs from '@/utils/rs';
import YAML from '@/utils/yaml';
import download from '@/utils/download';
import {
isIPv4,
isIPv6,
isValidPortNumber,
isNotBlank,
utf8ArrayToStr,
} from '@/utils';
import { isIPv4, isIPv6, isValidPortNumber, isNotBlank } from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors';
import PROXY_PRODUCERS from './producers';
@@ -413,6 +408,13 @@ function lastParse(proxy) {
delete proxy.ports;
}
if (['vless'].includes(proxy.type)) {
// 删除 reality-opts: {}
if (
proxy['reality-opts'] &&
Object.keys(proxy['reality-opts']).length === 0
) {
delete proxy['reality-opts'];
}
// 非 reality, 空 flow 没有意义
if (!proxy['reality-opts'] && !proxy.flow) {
delete proxy.flow;
@@ -427,6 +429,7 @@ function lastParse(proxy) {
}
}
}
if (typeof proxy.name !== 'string') {
if (/^\d+$/.test(proxy.name)) {
proxy.name = `${proxy.name}`;
@@ -435,7 +438,7 @@ function lastParse(proxy) {
if (proxy.name?.data) {
proxy.name = Buffer.from(proxy.name.data).toString('utf8');
} else {
proxy.name = utf8ArrayToStr(proxy.name);
proxy.name = Buffer.from(proxy.name).toString('utf8');
}
} catch (e) {
$.error(`proxy.name decode failed\nReason: ${e}`);

View File

@@ -3,11 +3,13 @@ import scriptResourceCache from '@/utils/script-resource-cache';
import { isIPv4, isIPv6 } from '@/utils';
import { FULL } from '@/utils/logical';
import { getFlag, removeFlag } from '@/utils/geo';
import { doh } from '@/utils/dns';
import lodash from 'lodash';
import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils';
import { produceArtifact } from '@/restful/sync';
import { SETTINGS_KEY } from '@/constants';
import env from '@/utils/env';
import {
@@ -389,17 +391,42 @@ function parseIP4P(IP4P) {
}
const DOMAIN_RESOLVERS = {
Google: async function (domain, type, noCache) {
Custom: async function (domain, type, noCache, timeout, edns, url) {
const id = hex_md5(`CUSTOM:${url}:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const res = await doh({
url,
domain,
type: type === 'IPv6' ? 'AAAA' : 'A',
timeout,
edns,
});
const { answers } = res;
if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers');
}
const result = answers.map((i) => i?.data).filter((i) => i);
if (result.length === 0) {
throw new Error('No answers');
}
resourceCache.set(id, result);
return result;
},
Google: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`GOOGLE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
domain,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`,
)}&type=${
type === 'IPv6' ? 'AAAA' : 'A'
}&edns_client_subnet=${edns}`,
headers: {
accept: 'application/dns-json',
},
timeout,
});
const body = JSON.parse(resp.body);
if (body['Status'] !== 0) {
@@ -416,7 +443,7 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
'IP-API': async function (domain, type, noCache) {
'IP-API': async function (domain, type, noCache, timeout) {
if (['IPv6'].includes(type)) {
throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`);
}
@@ -427,6 +454,7 @@ const DOMAIN_RESOLVERS = {
url: `http://ip-api.com/json/${encodeURIComponent(
domain,
)}?lang=zh-CN`,
timeout,
});
const body = JSON.parse(resp.body);
if (body['status'] !== 'success') {
@@ -442,7 +470,7 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
Cloudflare: async function (domain, type, noCache) {
Cloudflare: async function (domain, type, noCache, timeout) {
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
@@ -453,6 +481,7 @@ const DOMAIN_RESOLVERS = {
headers: {
accept: 'application/dns-json',
},
timeout,
});
const body = JSON.parse(resp.body);
if (body['Status'] !== 0) {
@@ -469,17 +498,18 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
Ali: async function (domain, type, noCache) {
Ali: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://223.6.6.6/resolve?edns_client_subnet=223.6.6.6/24&name=${encodeURIComponent(
url: `http://223.6.6.6/resolve?edns_client_subnet=${edns}/24&name=${encodeURIComponent(
domain,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}&short=1`,
headers: {
accept: 'application/dns-json',
},
timeout,
});
const answers = JSON.parse(resp.body);
if (!Array.isArray(answers) || answers.length === 0) {
@@ -492,17 +522,18 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
Tencent: async function (domain, type, noCache) {
Tencent: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`TENCENT:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://119.28.28.28/d?ip=119.28.28.28&type=${
url: `http://119.28.28.28/d?ip=${edns}&type=${
type === 'IPv6' ? 'AAAA' : 'A'
}&dn=${encodeURIComponent(domain)}`,
headers: {
accept: 'application/dns-json',
},
timeout,
});
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
if (answers.length === 0 || String(answers) === '0') {
@@ -517,16 +548,31 @@ const DOMAIN_RESOLVERS = {
},
};
function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
function ResolveDomainOperator({
provider,
type: _type,
filter,
cache,
url,
timeout,
edns: _edns,
}) {
if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
}
const { defaultTimeout } = $.read(SETTINGS_KEY);
const requestTimeout = timeout || defaultTimeout;
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
const resolver = DOMAIN_RESOLVERS[provider];
if (!resolver) {
throw new Error(`找不到域名解析服务提供方: ${provider}`);
}
let edns = _edns || '223.6.6.6';
if (!isIP(edns)) throw new Error(`域名解析 EDNS 应为 IP`);
$.info(
`Domain Resolver: [${_type}] ${provider} ${edns || ''} ${url || ''}`,
);
return {
name: 'Resolve Domain Operator',
func: async (proxies) => {
@@ -549,7 +595,14 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
const currentBatch = [];
for (let domain of totalDomain.splice(0, limit)) {
currentBatch.push(
resolver(domain, type, cache === 'disabled')
resolver(
domain,
type,
cache === 'disabled',
requestTimeout,
edns,
url,
)
.then((ip) => {
results[domain] = ip;
$.info(

View File

@@ -1,4 +1,5 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow';
import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
@@ -50,7 +51,15 @@ async function getFile(req, res) {
name = decodeURIComponent(name);
$.info(`正在下载文件:${name}`);
let { url, ua, content, mergeSources, ignoreFailedRemoteFile } = req.query;
let {
url,
subInfoUrl,
subInfoUserAgent,
ua,
content,
mergeSources,
ignoreFailedRemoteFile,
} = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程文件 URL: ${url}`);
@@ -59,6 +68,14 @@ async function getFile(req, res) {
ua = decodeURIComponent(ua);
$.info(`指定远程文件 User-Agent: ${ua}`);
}
if (subInfoUrl) {
subInfoUrl = decodeURIComponent(subInfoUrl);
$.info(`指定获取流量的 subInfoUrl: ${subInfoUrl}`);
}
if (subInfoUserAgent) {
subInfoUserAgent = decodeURIComponent(subInfoUserAgent);
$.info(`指定获取流量的 subInfoUserAgent: ${subInfoUserAgent}`);
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地文件: ${content}`);
@@ -86,6 +103,26 @@ async function getFile(req, res) {
ignoreFailedRemoteFile,
});
try {
subInfoUrl = subInfoUrl || file.subInfoUrl;
if (subInfoUrl) {
// forward flow headers
const flowInfo = await getFlowHeaders(
subInfoUrl,
subInfoUserAgent || file.subInfoUserAgent,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
} catch (err) {
$.error(
`文件 ${name} 获取流量信息时发生错误: ${JSON.stringify(
err,
)}`,
);
}
res.set('Content-Type', 'text/plain; charset=utf-8').send(
output ?? '',
);

49
backend/src/utils/dns.js Normal file
View File

@@ -0,0 +1,49 @@
import $ from '@/core/app';
import dnsPacket from 'dns-packet';
import { Buffer } from 'buffer';
export async function doh({ url, domain, type = 'A', timeout, edns }) {
const buf = dnsPacket.encode({
type: 'query',
id: 0,
flags: dnsPacket.RECURSION_DESIRED,
questions: [
{
type,
name: domain,
},
],
additionals: [
{
type: 'OPT',
name: '.',
udpPayloadSize: 4096,
flags: 0,
options: [
{
code: 'CLIENT_SUBNET',
ip: edns,
sourcePrefixLength: 24,
scopePrefixLength: 0,
},
],
},
],
});
const res = await $.http.get({
url: `${url}?dns=${buf
.toString('base64')
.toString('utf-8')
.replace(/=/g, '')}`,
headers: {
Accept: 'application/dns-message',
// 'Content-Type': 'application/dns-message',
},
// body: buf,
'binary-mode': true,
encoding: null, // 使用 null 编码以确保响应是原始二进制数据
timeout,
});
return dnsPacket.decode(Buffer.from($.env.isQX ? res.bodyBytes : res.body));
}

View File

@@ -46,52 +46,52 @@ function getPolicyDescriptor(str) {
};
}
const utf8ArrayToStr =
typeof TextDecoder !== 'undefined'
? (v) => new TextDecoder().decode(new Uint8Array(v))
: (function () {
var charCache = new Array(128); // Preallocate the cache for the common single byte chars
var charFromCodePt = String.fromCodePoint || String.fromCharCode;
var result = [];
// const utf8ArrayToStr =
// typeof TextDecoder !== 'undefined'
// ? (v) => new TextDecoder().decode(new Uint8Array(v))
// : (function () {
// var charCache = new Array(128); // Preallocate the cache for the common single byte chars
// var charFromCodePt = String.fromCodePoint || String.fromCharCode;
// var result = [];
return function (array) {
var codePt, byte1;
var buffLen = array.length;
// return function (array) {
// var codePt, byte1;
// var buffLen = array.length;
result.length = 0;
// result.length = 0;
for (var i = 0; i < buffLen; ) {
byte1 = array[i++];
// for (var i = 0; i < buffLen; ) {
// byte1 = array[i++];
if (byte1 <= 0x7f) {
codePt = byte1;
} else if (byte1 <= 0xdf) {
codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);
} else if (byte1 <= 0xef) {
codePt =
((byte1 & 0x0f) << 12) |
((array[i++] & 0x3f) << 6) |
(array[i++] & 0x3f);
} else if (String.fromCodePoint) {
codePt =
((byte1 & 0x07) << 18) |
((array[i++] & 0x3f) << 12) |
((array[i++] & 0x3f) << 6) |
(array[i++] & 0x3f);
} else {
codePt = 63; // Cannot convert four byte code points, so use "?" instead
i += 3;
}
// if (byte1 <= 0x7f) {
// codePt = byte1;
// } else if (byte1 <= 0xdf) {
// codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);
// } else if (byte1 <= 0xef) {
// codePt =
// ((byte1 & 0x0f) << 12) |
// ((array[i++] & 0x3f) << 6) |
// (array[i++] & 0x3f);
// } else if (String.fromCodePoint) {
// codePt =
// ((byte1 & 0x07) << 18) |
// ((array[i++] & 0x3f) << 12) |
// ((array[i++] & 0x3f) << 6) |
// (array[i++] & 0x3f);
// } else {
// codePt = 63; // Cannot convert four byte code points, so use "?" instead
// i += 3;
// }
result.push(
charCache[codePt] ||
(charCache[codePt] = charFromCodePt(codePt)),
);
}
// result.push(
// charCache[codePt] ||
// (charCache[codePt] = charFromCodePt(codePt)),
// );
// }
return result.join('');
};
})();
// return result.join('');
// };
// })();
export {
isIPv4,
@@ -101,6 +101,6 @@ export {
getIfNotBlank,
isPresent,
getIfPresent,
utf8ArrayToStr,
// utf8ArrayToStr,
getPolicyDescriptor,
};

View File

@@ -341,7 +341,10 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
const request = isNode
? eval("require('request')")
: $httpClient;
const body = options.body;
const opts = JSON.parse(JSON.stringify(options));
opts.body = body;
if (!isNode && opts.timeout) {
opts.timeout++;
let unit = 'ms';

View File

@@ -5,8 +5,8 @@ function operator(proxies = [], targetPlatform, context) {
// proxies 为传入的内部节点数组
// 结构大致参考了 Clash.Meta(mihomo) 有私货
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
// 0. 结构大致参考了 Clash.Meta(mihomo), 可参考 mihomo 的文档, 例如 `xudp`, `smux` 都可以自己设置. 但是有私货, 下面是我能想起来的一些私货
// 1. `_no-resolve` 为不解析域名
// 2. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP
@@ -18,6 +18,9 @@ function operator(proxies = [], targetPlatform, context) {
// 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
// 10. `sni` 在某些协议里会自动与 `servername` 转换
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
// 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down`
//
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009
// $arguments 为传入的脚本参数