ai-chatbot/lib/video-canvas/download-media.ts

40 lines
2.2 KiB
TypeScript

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<void> {
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<http.IncomingMessage>((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));
}