Files
safetynet/apps/network-broker/framed-channel.cjs
T

69 lines
3.9 KiB
JavaScript

'use strict';
const {EventEmitter}=require('node:events');
const {TextDecoder}=require('node:util');
const SMALL=65536,LARGE=2097152;
// Transport only: the native host and parent must authenticate their pipe peer
// before constructing this channel. Framing does not grant policy authority.
function createFramedChannel({readable,writable,inboundFirstLimit=SMALL,outboundFirstLimit=SMALL}={}){
if(!readable||!writable||typeof readable.on!=='function'||typeof writable.write!=='function'||![SMALL,LARGE].includes(inboundFirstLimit)||![SMALL,LARGE].includes(outboundFirstLimit))throw Error('broker-channel-invalid');
const channel=new EventEmitter(),decoder=new TextDecoder('utf-8',{fatal:true});
let connected=true,firstRead=true,firstWrite=true,header=Buffer.alloc(4),headerBytes=0,payload,payloadBytes=0,pending=0;
const outstanding=new Set();
Object.defineProperty(channel,'connected',{get:()=>connected});
function disconnect(){
if(!connected)return;connected=false;header.fill(0);payload?.fill(0);payload=undefined;
readable.off('data',data);readable.off('end',disconnect);readable.off('close',disconnect);
// Keep bounded error listeners until destroyed streams settle.
readable.destroy?.();if(writable!==readable)writable.destroy?.();
for(const record of outstanding)record.notify(true);
channel.emit('disconnect');
}
function data(chunk){
if(!connected)return;
try{
if(!Buffer.isBuffer(chunk))throw Error();
let offset=0;
while(offset<chunk.length&&connected){
if(!payload){
const take=Math.min(4-headerBytes,chunk.length-offset);chunk.copy(header,headerBytes,offset,offset+take);headerBytes+=take;offset+=take;
if(headerBytes!==4)continue;
const length=header.readUInt32LE(0),limit=firstRead?inboundFirstLimit:SMALL;
if(length<1||length>limit)throw Error();
payload=Buffer.alloc(length);payloadBytes=0;headerBytes=0;header.fill(0);
}
const take=Math.min(payload.length-payloadBytes,chunk.length-offset);chunk.copy(payload,payloadBytes,offset,offset+take);payloadBytes+=take;offset+=take;
if(payloadBytes===payload.length){
const completed=payload;payload=undefined;payloadBytes=0;firstRead=false;
let message;try{message=JSON.parse(decoder.decode(completed));}finally{completed.fill(0);}
if(!message||typeof message!=='object'||Array.isArray(message))throw Error();
channel.emit('message',message);
}
}
}catch{disconnect();}
}
channel.send=(message,callback=()=>{})=>{
if(typeof callback!=='function')throw Error('broker-channel-invalid');
const notifyFailure=()=>{disconnect();try{callback(Error('broker-channel-unavailable'));}catch{}};
if(!connected||pending>=8){notifyFailure();return;}
let frame;
try{
if(!message||typeof message!=='object'||Array.isArray(message))throw Error();
const text=JSON.stringify(message),bytes=Buffer.byteLength(text),limit=firstWrite?outboundFirstLimit:SMALL;
if(bytes<1||bytes>limit)throw Error();
frame=Buffer.alloc(bytes+4);frame.writeUInt32LE(bytes);frame.write(text,4);firstWrite=false;pending++;
}catch{notifyFailure();return;}
let settled=false,notified=false;
const record={notify(error){if(notified)return;notified=true;try{callback(error?Error('broker-channel-unavailable'):undefined);}catch{disconnect();}}};
outstanding.add(record);
// Completion of the underlying write owns buffer release. Disconnect notifies
// failure immediately, but cannot claim that an outstanding write has settled.
const done=error=>{if(settled)return;settled=true;pending--;outstanding.delete(record);frame.fill(0);if(error)disconnect();record.notify(error||!connected);};
try{writable.write(frame,done);}catch{done(Error());}
};
channel.disconnect=disconnect;
readable.on('data',data);readable.once('end',disconnect);readable.once('close',disconnect);readable.on('error',disconnect);
if(writable!==readable){writable.once('close',disconnect);writable.on('error',disconnect);}
return channel;
}
module.exports={createFramedChannel};