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": {
|
||||
"ajv": "^8.13.0",
|
||||
"file-type": "^21.0.0",
|
||||
"undici": "^6.19.0"
|
||||
"file-type": "^21.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.1"
|
||||
|
||||
@ -2,6 +2,8 @@ import fs from 'fs';
|
||||
import { stat } from 'fs/promises';
|
||||
import crypto, { randomUUID } from 'crypto';
|
||||
import path from 'node:path';
|
||||
import http from 'node:http';
|
||||
import tls from 'node:tls';
|
||||
import { solveProblem } from '@/napcat-common/src/helper';
|
||||
|
||||
export interface HttpDownloadOptions {
|
||||
@ -122,15 +124,8 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
|
||||
// 如果配置了代理,使用代理下载
|
||||
if (proxy) {
|
||||
try {
|
||||
// Node.js 18+ 内置 undici,使用动态导入
|
||||
const undici = await import('undici');
|
||||
const dispatcher = new undici.ProxyAgent(proxy);
|
||||
const response = await undici.fetch(url, {
|
||||
headers,
|
||||
redirect: 'follow',
|
||||
dispatcher,
|
||||
});
|
||||
return response as unknown as Response;
|
||||
const response = await httpDownloadWithProxy(url, headers, proxy);
|
||||
return new Response(response, { status: 200, statusText: 'OK' });
|
||||
} catch (proxyError) {
|
||||
// 如果代理失败,记录错误并尝试直接下载
|
||||
console.error('代理下载失败,尝试直接下载:', proxyError);
|
||||
@ -146,6 +141,220 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
|
||||
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> {
|
||||
const useReferer = typeof options === 'string';
|
||||
let resp = await tryDownload(options);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user