import http from 'node:http'; import net from 'node:net'; import dns from 'node:dns/promises'; import fs from 'node:fs'; import path from 'node:path'; const privateAddresses=new net.BlockList(); for(const [address,prefix] of [['0.0.0.0',8],['10.0.0.0',8],['100.64.0.0',10],['127.0.0.0',8],['169.254.0.0',16],['172.16.0.0',12],['192.0.0.0',24],['192.168.0.0',16],['198.18.0.0',15],['224.0.0.0',4],['240.0.0.0',4]])privateAddresses.addSubnet(address,prefix,'ipv4'); for(const [address,prefix] of [['::',128],['::1',128],['::ffff:0:0',96],['fc00::',7],['fe80::',10],['ff00::',8],['2001:db8::',32]])privateAddresses.addSubnet(address,prefix,'ipv6'); export async function startNativeExtensionProxy(workspace){ const baseHosts=['extensions.blender.org','download.blender.org','projects.blender.org',...(process.env.BLENDER_WEB_EXTENSION_HOSTS??'').split(',').map(host=>host.trim().toLowerCase()).filter(Boolean)]; const hosts=new Set(baseHosts); const sockets=new Set();let tunnels=0,closed=false; const status={mode:'REPOSITORY_HTTPS_PROXY',hosts:[...hosts],lastError:null}; const server=http.createServer((request,response)=>{response.writeHead(405);response.end('HTTPS CONNECT required');}); server.headersTimeout=10000;server.requestTimeout=15000; server.on('connection',socket=>{sockets.add(socket);socket.on('close',()=>sockets.delete(socket));socket.on('error',()=>{});}); server.on('clientError',(_error,socket)=>socket.destroy()); server.on('connect',async(request,client,head)=>{ let upstream,reserved=false; try{ hosts.clear();for(const host of baseHosts)hosts.add(host); const registration=path.join(workspace,'extension-hosts.json'); if(fs.existsSync(registration)&&fs.statSync(registration).size<=32768){ const data=JSON.parse(fs.readFileSync(registration,'utf8')); if(Array.isArray(data.hosts))for(const host of data.hosts.slice(0,128))if(typeof host==='string'&&/^[a-z0-9.-]+$/.test(host))hosts.add(host); status.hosts=[...hosts]; } const target=new URL('https://'+request.url); if(target.username||target.password||target.pathname!=='/'||target.search||target.hash||(target.port&&target.port!=='443')||!hosts.has(target.hostname))throw new Error('EXTENSION_HOST_NOT_ALLOWED: '+target.hostname); if(tunnels>=8)throw new Error('EXTENSION_CONNECTION_BUDGET');tunnels++;reserved=true; const addresses=await dns.lookup(target.hostname,{all:true}); const address=addresses.find(item=>!privateAddresses.check(item.address,item.family===6?'ipv6':'ipv4')); if(!address)throw new Error('EXTENSION_PUBLIC_ADDRESS_REQUIRED'); if(closed||client.destroyed)return; upstream=net.createConnection({host:address.address,family:address.family,port:443});sockets.add(upstream); let bytes=0;const timeout=setTimeout(()=>{client.destroy();upstream.destroy();},240000);timeout.unref(); const account=chunk=>{bytes+=chunk.length;if(bytes>512*1024*1024){status.lastError='EXTENSION_TRANSFER_BUDGET';client.destroy();upstream.destroy();}}; upstream.setTimeout(60000,()=>upstream.destroy());client.setTimeout(60000,()=>client.destroy()); upstream.on('data',account);client.on('data',account); upstream.once('connect',()=>{status.lastError=null;client.write('HTTP/1.1 200 Connection Established\r\n\r\n');if(head.length)upstream.write(head);client.pipe(upstream);upstream.pipe(client);}); upstream.on('error',()=>{status.lastError='EXTENSION_UPSTREAM_UNAVAILABLE';client.destroy();}); upstream.once('close',()=>{clearTimeout(timeout);sockets.delete(upstream);client.destroy();}); client.once('close',()=>{clearTimeout(timeout);upstream.destroy();}); }catch(error){status.lastError=String(error.message);if(!client.destroyed)client.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n'+status.lastError);} finally{if(reserved){if(client.destroyed)tunnels--;else client.once('close',()=>tunnels--);}} }); const socketPath=path.join(workspace,'extension-proxy.sock'); const directory=fs.openSync(workspace,fs.constants.O_RDONLY|fs.constants.O_DIRECTORY|fs.constants.O_NOFOLLOW);fs.rmSync(socketPath,{force:true}); try{await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(`/proc/self/fd/${directory}/extension-proxy.sock`,resolve);});}finally{fs.closeSync(directory);} fs.chmodSync(socketPath,0o600); return {status,async close(){closed=true;for(const socket of sockets)socket.destroy();await new Promise(resolve=>server.close(resolve));fs.rmSync(socketPath,{force:true});}}; }