Files

38 lines
2.8 KiB
JavaScript

'use strict';
const {GateError}=require('./http-request-gate.cjs');
const allowed=new Set(['content-disposition','content-type','content-language','cache-control','expires','last-modified','etag','location','set-cookie','vary','content-security-policy','content-security-policy-report-only','referrer-policy','permissions-policy','x-content-type-options','x-frame-options','strict-transport-security']);
const invalid=value=>typeof value!=='string'||value.length>8192||[...value].some(character=>character.charCodeAt(0)<32&&character!=='\t'||character.charCodeAt(0)===127);
/** Provisional buffered response. No streaming/compression forwarding.
* Content-Length is recomputed from a private body copy. Hop-by-hop and
* unrecognized headers are never forwarded. Set-Cookie may be a bounded
* string array; values are forwarded separately without comma combining.
*/
function validateResponse(record,maxBytes=1048576) {
if(!record||!Number.isSafeInteger(record.statusCode)||record.statusCode<200||record.statusCode>599||!Number.isSafeInteger(maxBytes)||maxBytes<0||!Buffer.isBuffer(record.body)||record.body.length>maxBytes||!record.headers||typeof record.headers!=='object'||Array.isArray(record.headers))throw new GateError('invalid-upstream-response');
const headers=Object.create(null);
const seen=new Set(),hop=new Set();
for(const [key,value] of Object.entries(record.headers))if(key.toLowerCase()==='connection') {
if(invalid(value))throw new GateError('invalid-upstream-response');
for(const token of value.split(',')){if(!/^[a-zA-Z0-9-]+$/.test(token.trim()))throw new GateError('invalid-upstream-response');hop.add(token.trim().toLowerCase());}
}
let size=0;
for(const [key,value] of Object.entries(record.headers)) {
if(!/^[a-zA-Z0-9-]+$/.test(key))throw new GateError('invalid-upstream-response');
const name=key.toLowerCase();
if(seen.has(name))throw new GateError('invalid-upstream-response');seen.add(name);
if(name==='content-encoding'&&value!==undefined&&value!=='identity')throw new GateError('unsupported-upstream-encoding');
if(!allowed.has(name)||hop.has(name))continue;
const values=Array.isArray(value)?value:[value];
if(values.length>32||(Array.isArray(value)&&name!=='set-cookie')||values.some(invalid))throw new GateError('invalid-upstream-response');
size+=key.length+values.reduce((sum,item)=>sum+item.length,0);
if(size>32768)throw new GateError('invalid-upstream-response');
headers[name]=Array.isArray(value)?Object.freeze([...value]):value;
}
if((record.statusCode===204||record.statusCode===304)&&record.body.length)throw new GateError('invalid-upstream-response');
const body=Buffer.from(record.body);
headers['content-length']=String(body.length);headers.connection='close';
return Object.freeze({statusCode:record.statusCode,headers:Object.freeze(headers),body});
}
module.exports={validateResponse};