'use strict'; const {isIP}=require('node:net'); const bindings=new WeakSet(); class AuthorityError extends Error { constructor(code){super(code);this.name='AuthorityError';this.code=code;} } const deny=code=>{throw new AuthorityError(code);}; function parseAuthority(value,{requirePort=false}={}) { if(typeof value!=='string'||!value||value.length>320||/[^\x21-\x7e]|[@/\\?#%]/.test(value))deny('invalid-authority'); let hostname,portText; if(value.startsWith('[')) { const match=/^\[([^\]]+)\](?::([^:]+))?$/.exec(value); if(!match||isIP(match[1])!==6)deny('invalid-authority'); hostname=new URL(`https://[${match[1]}]/`).hostname.slice(1,-1); portText=match[2]; } else { const match=/^([^:]+)(?::([^:]+))?$/.exec(value); if(!match)deny('invalid-authority'); hostname=match[1].toLowerCase();portText=match[2]; if(isIP(hostname)!==4) { // Forbid URL-parser legacy numerical IPv4 aliases (127.1, octal, // decimal and hex). DNS labels remain explicit ASCII / punycode. if(/^[0-9.]+$/.test(hostname)||hostname.split('.').some(label=>/^0x[0-9a-f]+$/i.test(label)))deny('invalid-authority'); if(hostname.length>253||hostname.split('.').some(label=>!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))deny('invalid-authority'); } } if(requirePort&&portText===undefined)deny('missing-port'); if(portText!==undefined&&!/^[1-9][0-9]{0,4}$/.test(portText))deny('invalid-port'); const port=portText===undefined?443:Number(portText); if(port>65535)deny('invalid-port'); return Object.freeze({hostname,port,authority:`${isIP(hostname)===6?'['+hostname+']':hostname}:${port}`}); } /** Caller authenticates CONNECT before creating this immutable binding. * Parsing does not authenticate a user, authorize a destination, terminate * TLS, validate upstream certificates or constrain process/OS egress. */ function bindConnectAuthority(value) { const binding=parseAuthority(value,{requirePort:true});bindings.add(binding);return binding; } function verifyInnerAuthority(binding,{rawHeaders,url,servername}) { if(!binding||!bindings.has(binding))deny('invalid-binding'); const matches=value=>parseAuthority(value).authority===binding.authority; if(!Array.isArray(rawHeaders)||rawHeaders.length%2)deny('invalid-headers'); let host; for(let index=0;indexcharacter.charCodeAt(0)<=32||character.charCodeAt(0)===127))deny('invalid-target'); if(url.startsWith('/')) { if(url.startsWith('//')||url.includes('#'))deny('invalid-target'); } else { const match=/^https:\/\/([^/;?#]+)(?:\/[^#]*)?$/.exec(url); if(!match)deny('invalid-target'); if(!matches(match[1]))deny('authority-mismatch'); } if(servername===undefined||servername==='') { if(!isIP(binding.hostname))deny('missing-sni'); } else { // TLS SNI is a hostname, never an authority carrying a port. const sni=isIP(servername)===6?`[${servername}]`:servername; const parsed=parseAuthority(sni); if(sni.includes(':')&&isIP(servername)!==6)deny('invalid-sni'); if(parsed.hostname!==binding.hostname)deny('sni-mismatch'); } return binding; } module.exports={AuthorityError,parseAuthority,bindConnectAuthority,verifyInnerAuthority};