From 32dcca4a265715970dc16647ed846ae549b0a09b Mon Sep 17 00:00:00 2001 From: xream Date: Thu, 20 Jun 2024 21:42:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9F=9F=E5=90=8D=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89=20DoH(=E9=9C=80?= =?UTF-8?q?=E6=96=B0=E7=89=88=E5=89=8D=E7=AB=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 4 +- backend/pnpm-lock.yaml | 32 +++++++- backend/src/core/proxy-utils/index.js | 12 +-- .../src/core/proxy-utils/processors/index.js | 25 ++++++ backend/src/utils/dns.js | 55 +++++++++++++ backend/src/utils/index.js | 82 +++++++++---------- backend/src/vendor/open-api.js | 3 + 7 files changed, 161 insertions(+), 52 deletions(-) create mode 100644 backend/src/utils/dns.js diff --git a/backend/package.json b/backend/package.json index 1d8146d..1086f7c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "sub-store", - "version": "2.14.342", + "version": "2.14.343", "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", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 5457d88..05ffd66 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -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} diff --git a/backend/src/core/proxy-utils/index.js b/backend/src/core/proxy-utils/index.js index edbefb4..6b1086b 100644 --- a/backend/src/core/proxy-utils/index.js +++ b/backend/src/core/proxy-utils/index.js @@ -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'; @@ -427,6 +422,7 @@ function lastParse(proxy) { } } } + if (typeof proxy.name !== 'string') { if (/^\d+$/.test(proxy.name)) { proxy.name = `${proxy.name}`; @@ -435,7 +431,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}`); diff --git a/backend/src/core/proxy-utils/processors/index.js b/backend/src/core/proxy-utils/processors/index.js index 344d40e..6d7ffb0 100644 --- a/backend/src/core/proxy-utils/processors/index.js +++ b/backend/src/core/proxy-utils/processors/index.js @@ -3,6 +3,7 @@ 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'; @@ -390,6 +391,27 @@ function parseIP4P(IP4P) { } const DOMAIN_RESOLVERS = { + Custom: async function (domain, type, noCache, timeout, 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, + }); + 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) { const id = hex_md5(`GOOGLE:${domain}:${type}`); const cached = resourceCache.get(id); @@ -528,6 +550,7 @@ function ResolveDomainOperator({ type: _type, filter, cache, + url, timeout, }) { if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) { @@ -541,6 +564,7 @@ function ResolveDomainOperator({ if (!resolver) { throw new Error(`找不到域名解析服务提供方: ${provider}`); } + $.info(`Domain Resolver: [${_type}] ${provider} ${url || ''}`); return { name: 'Resolve Domain Operator', func: async (proxies) => { @@ -568,6 +592,7 @@ function ResolveDomainOperator({ type, cache === 'disabled', requestTimeout, + url, ) .then((ip) => { results[domain] = ip; diff --git a/backend/src/utils/dns.js b/backend/src/utils/dns.js new file mode 100644 index 0000000..c678057 --- /dev/null +++ b/backend/src/utils/dns.js @@ -0,0 +1,55 @@ +import $ from '@/core/app'; +import dnsPacket from 'dns-packet'; +import { Buffer } from 'buffer'; + +export async function doh({ + url, + domain, + type = 'A', + timeout, + ip = '223.6.6.6', +}) { + 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, + 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)); +} diff --git a/backend/src/utils/index.js b/backend/src/utils/index.js index 6380b72..bd70c8a 100644 --- a/backend/src/utils/index.js +++ b/backend/src/utils/index.js @@ -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, }; diff --git a/backend/src/vendor/open-api.js b/backend/src/vendor/open-api.js index cb0b7fd..4d81298 100644 --- a/backend/src/vendor/open-api.js +++ b/backend/src/vendor/open-api.js @@ -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';