mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
Remove undici dependency and implement proxy download
Replaces the use of the undici library for HTTP downloads with proxy support by implementing a custom httpDownloadWithProxy function using Node.js http and tls modules. The undici dependency is removed from package.json, reducing external dependencies and improving compatibility.
This commit is contained in:
parent
4bec3aa597
commit
6ea4c9ec65
@ -17,8 +17,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.13.0",
|
"ajv": "^8.13.0",
|
||||||
"file-type": "^21.0.0",
|
"file-type": "^21.0.0"
|
||||||
"undici": "^6.19.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.1"
|
"@types/node": "^22.0.1"
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import fs from 'fs';
|
|||||||
import { stat } from 'fs/promises';
|
import { stat } from 'fs/promises';
|
||||||
import crypto, { randomUUID } from 'crypto';
|
import crypto, { randomUUID } from 'crypto';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import http from 'node:http';
|
||||||
|
import tls from 'node:tls';
|
||||||
import { solveProblem } from '@/napcat-common/src/helper';
|
import { solveProblem } from '@/napcat-common/src/helper';
|
||||||
|
|
||||||
export interface HttpDownloadOptions {
|
export interface HttpDownloadOptions {
|
||||||
@ -122,15 +124,8 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
|
|||||||
// 如果配置了代理,使用代理下载
|
// 如果配置了代理,使用代理下载
|
||||||
if (proxy) {
|
if (proxy) {
|
||||||
try {
|
try {
|
||||||
// Node.js 18+ 内置 undici,使用动态导入
|
const response = await httpDownloadWithProxy(url, headers, proxy);
|
||||||
const undici = await import('undici');
|
return new Response(response, { status: 200, statusText: 'OK' });
|
||||||
const dispatcher = new undici.ProxyAgent(proxy);
|
|
||||||
const response = await undici.fetch(url, {
|
|
||||||
headers,
|
|
||||||
redirect: 'follow',
|
|
||||||
dispatcher,
|
|
||||||
});
|
|
||||||
return response as unknown as Response;
|
|
||||||
} catch (proxyError) {
|
} catch (proxyError) {
|
||||||
// 如果代理失败,记录错误并尝试直接下载
|
// 如果代理失败,记录错误并尝试直接下载
|
||||||
console.error('代理下载失败,尝试直接下载:', proxyError);
|
console.error('代理下载失败,尝试直接下载:', proxyError);
|
||||||
@ -146,6 +141,220 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
|
|||||||
return fetchRes;
|
return fetchRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 HTTP/HTTPS 代理下载文件
|
||||||
|
*/
|
||||||
|
function httpDownloadWithProxy (url: string, headers: Record<string, string>, proxy: string): Promise<Buffer> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const targetUrl = new URL(url);
|
||||||
|
const proxyUrl = new URL(proxy);
|
||||||
|
|
||||||
|
const isTargetHttps = targetUrl.protocol === 'https:';
|
||||||
|
const proxyPort = parseInt(proxyUrl.port) || (proxyUrl.protocol === 'https:' ? 443 : 80);
|
||||||
|
|
||||||
|
// 代理认证头
|
||||||
|
const proxyAuthHeader = proxyUrl.username && proxyUrl.password
|
||||||
|
? { 'Proxy-Authorization': 'Basic ' + Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString('base64') }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
if (isTargetHttps) {
|
||||||
|
// HTTPS 目标:需要通过 CONNECT 建立隧道
|
||||||
|
const connectReq = http.request({
|
||||||
|
host: proxyUrl.hostname,
|
||||||
|
port: proxyPort,
|
||||||
|
method: 'CONNECT',
|
||||||
|
path: `${targetUrl.hostname}:${targetUrl.port || 443}`,
|
||||||
|
headers: {
|
||||||
|
'Host': `${targetUrl.hostname}:${targetUrl.port || 443}`,
|
||||||
|
...proxyAuthHeader,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
connectReq.on('connect', (res, socket) => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
socket.destroy();
|
||||||
|
reject(new Error(`代理 CONNECT 失败: ${res.statusCode} ${res.statusMessage}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在隧道上建立 TLS 连接
|
||||||
|
const tlsSocket = tls.connect({
|
||||||
|
socket: socket,
|
||||||
|
servername: targetUrl.hostname,
|
||||||
|
rejectUnauthorized: true,
|
||||||
|
}, () => {
|
||||||
|
// TLS 握手成功,发送 HTTP 请求
|
||||||
|
const requestPath = targetUrl.pathname + targetUrl.search;
|
||||||
|
const requestHeaders = {
|
||||||
|
...headers,
|
||||||
|
'Host': targetUrl.hostname,
|
||||||
|
'Connection': 'close',
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerLines = Object.entries(requestHeaders)
|
||||||
|
.map(([key, value]) => `${key}: ${value}`)
|
||||||
|
.join('\r\n');
|
||||||
|
|
||||||
|
const httpRequest = `GET ${requestPath} HTTP/1.1\r\n${headerLines}\r\n\r\n`;
|
||||||
|
tlsSocket.write(httpRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 解析 HTTP 响应
|
||||||
|
let responseData = Buffer.alloc(0);
|
||||||
|
let headersParsed = false;
|
||||||
|
let statusCode = 0;
|
||||||
|
let isChunked = false;
|
||||||
|
let bodyData = Buffer.alloc(0);
|
||||||
|
let redirectLocation: string | null = null;
|
||||||
|
|
||||||
|
tlsSocket.on('data', (chunk: Buffer) => {
|
||||||
|
responseData = Buffer.concat([responseData, chunk]);
|
||||||
|
|
||||||
|
if (!headersParsed) {
|
||||||
|
const headerEndIndex = responseData.indexOf('\r\n\r\n');
|
||||||
|
if (headerEndIndex !== -1) {
|
||||||
|
headersParsed = true;
|
||||||
|
const headerStr = responseData.subarray(0, headerEndIndex).toString();
|
||||||
|
const headerLines = headerStr.split('\r\n');
|
||||||
|
|
||||||
|
// 解析状态码
|
||||||
|
const statusLine = headerLines[0];
|
||||||
|
const statusMatch = statusLine?.match(/HTTP\/\d\.\d\s+(\d+)/);
|
||||||
|
statusCode = statusMatch ? parseInt(statusMatch[1]!) : 0;
|
||||||
|
|
||||||
|
// 解析响应头
|
||||||
|
for (const line of headerLines.slice(1)) {
|
||||||
|
const [key, ...valueParts] = line.split(':');
|
||||||
|
const value = valueParts.join(':').trim();
|
||||||
|
if (key?.toLowerCase() === 'transfer-encoding' && value.toLowerCase() === 'chunked') {
|
||||||
|
isChunked = true;
|
||||||
|
} else if (key?.toLowerCase() === 'location') {
|
||||||
|
redirectLocation = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyData = responseData.subarray(headerEndIndex + 4);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bodyData = Buffer.concat([bodyData, chunk]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tlsSocket.on('end', () => {
|
||||||
|
// 处理重定向
|
||||||
|
if (statusCode >= 300 && statusCode < 400 && redirectLocation) {
|
||||||
|
const redirectUrl = redirectLocation.startsWith('http')
|
||||||
|
? redirectLocation
|
||||||
|
: `${targetUrl.protocol}//${targetUrl.host}${redirectLocation}`;
|
||||||
|
httpDownloadWithProxy(redirectUrl, headers, proxy).then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode !== 200) {
|
||||||
|
reject(new Error(`下载失败: ${statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 chunked 编码
|
||||||
|
if (isChunked) {
|
||||||
|
resolve(parseChunkedBody(bodyData));
|
||||||
|
} else {
|
||||||
|
resolve(bodyData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tlsSocket.on('error', (err) => {
|
||||||
|
reject(new Error(`TLS 连接错误: ${err.message}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
connectReq.on('error', (err) => {
|
||||||
|
reject(new Error(`代理连接错误: ${err.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
connectReq.end();
|
||||||
|
} else {
|
||||||
|
// HTTP 目标:直接通过代理请求
|
||||||
|
const req = http.request({
|
||||||
|
host: proxyUrl.hostname,
|
||||||
|
port: proxyPort,
|
||||||
|
method: 'GET',
|
||||||
|
path: url, // 完整 URL
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
'Host': targetUrl.hostname,
|
||||||
|
...proxyAuthHeader,
|
||||||
|
},
|
||||||
|
}, (response) => {
|
||||||
|
handleResponse(response, resolve, reject, url, headers, proxy);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => {
|
||||||
|
reject(new Error(`代理请求错误: ${err.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 chunked 编码的响应体
|
||||||
|
*/
|
||||||
|
function parseChunkedBody (data: Buffer): Buffer {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
while (offset < data.length) {
|
||||||
|
// 查找 chunk 大小行的结束
|
||||||
|
const lineEnd = data.indexOf('\r\n', offset);
|
||||||
|
if (lineEnd === -1) break;
|
||||||
|
|
||||||
|
const sizeStr = data.subarray(offset, lineEnd).toString().split(';')[0]; // 忽略 chunk 扩展
|
||||||
|
const chunkSize = parseInt(sizeStr!, 16);
|
||||||
|
|
||||||
|
if (chunkSize === 0) break; // 最后一个 chunk
|
||||||
|
|
||||||
|
const chunkStart = lineEnd + 2;
|
||||||
|
const chunkEnd = chunkStart + chunkSize;
|
||||||
|
|
||||||
|
if (chunkEnd > data.length) break;
|
||||||
|
|
||||||
|
chunks.push(data.subarray(chunkStart, chunkEnd));
|
||||||
|
offset = chunkEnd + 2; // 跳过 chunk 数据后的 \r\n
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 HTTP 响应
|
||||||
|
*/
|
||||||
|
function handleResponse (
|
||||||
|
response: http.IncomingMessage,
|
||||||
|
resolve: (value: Buffer) => void,
|
||||||
|
reject: (reason: Error) => void,
|
||||||
|
_url: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
proxy: string
|
||||||
|
): void {
|
||||||
|
// 处理重定向
|
||||||
|
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
||||||
|
httpDownloadWithProxy(response.headers.location, headers, proxy).then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
reject(new Error(`下载失败: ${response.statusCode} ${response.statusMessage}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
response.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
response.on('end', () => resolve(Buffer.concat(chunks)));
|
||||||
|
response.on('error', reject);
|
||||||
|
}
|
||||||
|
|
||||||
export async function httpDownload (options: string | HttpDownloadOptions): Promise<Buffer> {
|
export async function httpDownload (options: string | HttpDownloadOptions): Promise<Buffer> {
|
||||||
const useReferer = typeof options === 'string';
|
const useReferer = typeof options === 'string';
|
||||||
let resp = await tryDownload(options);
|
let resp = await tryDownload(options);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user