import {lookup} from 'node:dns/promises'; import http from 'node:http'; import https from 'node:https'; import {createWriteStream} from 'node:fs'; import {pipeline} from 'node:stream/promises'; import {Transform} from 'node:stream'; const MAX_BYTES=125*1024*1024; export function isPublicMediaIpv4(address:string) { const p=address.split('.').map(Number); return p.length===4 && p.every(v=>Number.isInteger(v)&&v>=0&&v<=255) && ![0,10,127].includes(p[0]) && p[0]<224 && !(p[0]===169&&p[1]===254) && !(p[0]===172&&p[1]>=16&&p[1]<=31) && !(p[0]===192&&(p[1]===168||p[1]===0)) && !(p[0]===100&&p[1]>=64&&p[1]<=127) && !(p[0]===198&&(p[1]===18||p[1]===19)); } export async function downloadCanvasMedia(url:string,output:string,redirects=0):Promise { const target=new URL(url); if(!['http:','https:'].includes(target.protocol)||target.username||target.password||redirects>4) throw new Error('视频地址无效'); const addresses=await lookup(target.hostname,{family:4,all:true}); if(!addresses.length||addresses.some(v=>!isPublicMediaIpv4(v.address))) throw new Error('视频地址不可访问'); // Pin the validated address while preserving Host and TLS server name. const response=await new Promise((resolve,reject)=>{ const request=(target.protocol==='https:'?https:http).get({ protocol:target.protocol,hostname:addresses[0].address,port:target.port||undefined, path:target.pathname+target.search,headers:{Host:target.host},servername:target.hostname, signal:AbortSignal.timeout(90000), },resolve); request.on('error',reject); }); if([301,302,303,307,308].includes(response.statusCode||0)&&response.headers.location) { response.destroy(); return downloadCanvasMedia(new URL(response.headers.location,target).href,output,redirects+1); } if(response.statusCode!==200||Number(response.headers['content-length']||0)>MAX_BYTES) {response.destroy();throw new Error('视频下载失败或超过 125 MiB 处理上限');} let bytes=0; const limit=new Transform({transform(chunk,_encoding,callback){bytes+=chunk.length;callback(bytes>MAX_BYTES?new Error('视频超过 125 MiB 处理上限'):null,chunk);}}); await pipeline(response,limit,createWriteStream(output)); }