38 lines
2.8 KiB
JavaScript
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};
|