local-traffic 0.0.32 → 0.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,3 +62,4 @@ npx local-traffic [location-of-the-local-traffic-config-file]
62
62
  - "replaceResponseBodyUrls": (boolean) replace every matching string from the mapping in the response body.
63
63
  - "dontUseHttp2Downstream": (boolean) force calling downstream services in http1.1 only (to save some time)
64
64
  - "simpleLogs": (boolean) disable colored logs for text terminals
65
+ - "websocket": (boolean) true to activate websocket connections proxying via sockets
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});const e=require("http2"),t=require("http"),r=require("https"),n=require("url"),o=require("fs"),s=require("zlib"),i=require("path");var a;!function(e){e[e.ERROR=124]="ERROR",e[e.SUCCESS=35]="SUCCESS",e[e.INFO=21]="INFO",e[e.WARNING=172]="WARNING"}(a||(a={}));const l=(0,i.resolve)(process.env.HOME,".local-traffic.json"),p=(0,i.resolve)(process.cwd(),process.argv.slice(-1)[0].endsWith(".json")?process.argv.slice(-1)[0]:l),c={mapping:{},port:8080,replaceResponseBodyUrls:!1,dontUseHttp2Downstream:!1,simpleLogs:!1};let d,h;const u=(e,t,r)=>{console.log(`${(e=>{const t=new Date;return`${e?"":""}${`${t.getHours()}`.padStart(2,"0")}${e?":":":"}${`${t.getMinutes()}`.padStart(2,"0")}${e?":":":"}${`${t.getSeconds()}`.padStart(2,"0")}${e?"":""}`})(d.simpleLogs)} ${d.simpleLogs?e.replace(/⎸/g,"|").replace(/⎹/g,"|").replace(/\u001b\[[^m]*m/g,"").replace(/↘️/g,"inbound").replace(/☎️/g,"port").replace(/↗️/g,"outbound"):t?`[48;5;${t}m⎸ ${process.stdout.isTTY&&r||""} ${e.padEnd(36)} ⎹`:e}`)},m=async(e=!0)=>new Promise((t=>(0,o.readFile)(p,((r,n)=>{r&&!e&&u("config error. Using default value",a.ERROR,"❌");try{d=Object.assign({},c,JSON.parse((n||"{}").toString()))}catch(e){return u("config syntax incorrect, aborting",a.ERROR,"⛈️"),d=d||{...c},void t(d)}d.mapping[""]||u('default mapping "" not provided.',a.WARNING,"☢️"),r&&"ENOENT"===r.code&&e&&p===l?(0,o.writeFile)(p,JSON.stringify(c),(e=>{e?u("config file NOT created",a.ERROR,"⁉️"):u("config file created",a.SUCCESS,"✨"),t(d)})):t(d)})))).then((()=>{e&&(0,o.watchFile)(p,f)})),g=e=>{u(`⎸ ↘️ : ${e.ssl?"HTTP/2 ":"HTTP 1.1"} ⎸ ☎️ : ${e.port.toString().padStart(5)} ⎸ ↗️ : ${e.dontUseHttp2Downstream?"HTTP 1.1":"HTTP/2 "} ⎹`)},f=async()=>{const e={...d};return await m(!1),isNaN(d.port)||d.port>65535||d.port<0?(d=e,void u("port number invalid. Not refreshing",a.ERROR,"☎️")):"object"!=typeof d.mapping?(d=e,void u("mapping should be an object. Aborting",a.ERROR,"⚡")):(d.replaceResponseBodyUrls&&!e.replaceResponseBodyUrls&&u("response body url replacement",a.INFO,"✔️"),!d.replaceResponseBodyUrls&&e.replaceResponseBodyUrls&&u("response body url NO replacement",a.INFO,"✖️"),u(`${Object.keys(d.mapping).length.toString().padStart(5)} loaded mapping rules`,a.SUCCESS,"↻"),void(d.port!==e.port||JSON.stringify(d.ssl)!==JSON.stringify(e.ssl)?(await new Promise((e=>h?h.close(e):e(void 0))),v()):d.dontUseHttp2Downstream!==e.dontUseHttp2Downstream&&g(d)))},$=e=>""==e?"":(0,i.normalize)(e).replace(/\\/g,"/"),b=e=>{const t=(0,i.resolve)("/",e.hostname,...e.pathname.replace(/[?#].*$/,"").replace(/^\/+/,"").split("/"));return{error:null,data:null,hasRun:!1,run:function(){return this.hasRun?Promise.resolve():new Promise((r=>(0,o.readFile)(t,((n,s)=>{if(this.hasRun=!0,!n||"EISDIR"!==n.code)return this.error=n,this.data=s,void r(void 0);(0,o.readdir)(t,((t,n)=>{this.error=t,this.data=n,t?r(void 0):Promise.all(n.map((t=>new Promise((r=>(0,o.lstat)((0,i.resolve)(e.pathname,t),((e,n)=>r([t,n,e])))))))).then((t=>{const n=t.filter((e=>!e[2]&&e[1].isDirectory())).concat(t.filter((e=>!e[2]&&e[1].isFile())));this.data=`${y(128194,"directory",e.href)}\n <p>Directory content of <i>${e.href.replace(/\//g,"&#x002F;")}</i></p>\n <ul class="list-group">\n <li class="list-group-item">&#x1F4C1;<a href="${e.pathname.endsWith("/")?"..":"."}">&lt;parent&gt;</a></li>\n ${n.filter((e=>!e[2])).map((t=>`<li class="list-group-item">&#x${(t[1].isDirectory()?128193:128196).toString(16)};<a href="${e.pathname.endsWith("/")?"":`${e.pathname.split("/").slice(-1)[0]}/`}${t[0]}">${t[0]}</a></li>`)).join("\n")}\n </li>\n </ul>\n </body></html>`,r(void 0)}))}))}))))},events:{},on:function(e,r){return this.events[e]=r,this.run().then((()=>{"response"===e&&this.events.response({Server:"local","Content-Type":t.endsWith(".svg")?"image/svg+xml":null},0),"data"===e&&this.data&&(this.events.data(this.data),this.events.end()),"error"===e&&this.error&&this.events.error(this.error)})),this},end:function(){return this},request:function(){return this}}},y=(e,t,r)=>`<!doctype html>\n<html lang="en">\n<head>\n<title>&#x${e.toString(16)}; local-traffic ${t} | ${r}</title>\n<link href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css" rel="stylesheet"/>\n<script src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"><\/script>\n<script src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"><\/script>\n</head>\n<body><div class="container"><h1>&#x${e.toString(16)}; local-traffic ${t}</h1>\n<br/>`,w=(e,t,r,n)=>`${y(128163,"error",e.message)}\n<p>An error happened while trying to proxy a remote exchange</p>\n<div class="alert alert-warning" role="alert">\n &#x24D8;&nbsp;This is not an error from the downstream service.\n</div>\n<div class="alert alert-danger" role="alert">\n<pre><code>${e.stack||`<i>${e.name} : ${e.message}</i>`}${e.errno?`<br/>(code : ${e.errno})`:""}</code></pre>\n</div>\nMore information about the request :\n<table class="table">\n <tbody>\n <tr>\n <td>phase</td>\n <td>${t}</td>\n </tr>\n <tr>\n <td>requested URL</td>\n <td>${r}</td>\n </tr>\n <tr>\n <td>downstream URL</td>\n <td>${n||"&lt;no-target-url&gt;"}</td>\n </tr>\n </tbody>\n</table>\n</div></body></html>`,R=(e,t,r)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":r.length}),t.end(r)},v=()=>{h=(d.ssl?e.createSecureServer.bind(null,{...d.ssl,allowHTTP1:!0}):t.createServer)((async(o,i)=>{if(!o.headers.host&&!o.headers[":authority"])return void R(400,i,Buffer.from(w(new Error("client must supply a 'host' header"),"proxy",new n.URL(`http${d.ssl?"s":""}://unknowndomain${o.url}`))));const a=o.headers[":authority"]||`${o.headers.host}${o.headers.host.match(/:[0-9]+$/)?"":80!==d.port||d.ssl?443===d.port&&d.ssl?"":`:${d.port}`:""}`,l=new n.URL(`http${d.ssl?"s":""}://${a}${o.url}`),c=l.href.substring(l.origin.length),[h,u]=Object.entries({...Object.assign({},...Object.entries(d.mapping).map((([e,t])=>({[e]:new n.URL($(t))}))))}).find((([e])=>c.match(RegExp(e))))||[];if(!u)return void R(502,i,Buffer.from(w(new Error(`No mapping found in config file ${p}`),"proxy",l)));const m=u.host.replace(RegExp(/\/+$/),""),g=`${u.href.substring("https://".length+u.host.length)}${$(c.replace(RegExp($(h)),""))}`.replace(/^\/*/,"/"),f=new n.URL(`${u.protocol}//${m}${g}`);let y=null,v=!d.dontUseHttp2Downstream;const S="file:"===u.protocol?b(f):v?await Promise.race([new Promise((t=>{const r=(0,e.connect)(f,{rejectUnauthorized:!1,protocol:u.protocol},((e,n)=>{v=v&&!!n.alpnProtocol,t(v?r:null)}));r.on("error",(e=>{y=v&&Buffer.from(w(e,"connection",l,f))}))})),new Promise((e=>setTimeout((()=>{v=!1,e(null)}),3e3)))]):null;!v&&y&&(y=null);const O={...[...Object.entries(o.headers)].filter((([e])=>!["host","connection"].includes(e.toLowerCase()))).reduce(((e,[t,r])=>(e[t]=(e[t]||"")+(Array.isArray(r)?r:[r]).map((e=>e.replace(l.hostname,m))).join(", "),e)),{}),origin:u.href,referer:f.toString(),":authority":m,":method":o.method,":path":g,":scheme":u.protocol.replace(":","")},E=S&&!y&&S.request(O,{endStream:d.ssl?!(o?.stream?.readableLength??1):!o.readableLength});E&&E.on("error",(e=>{const t=-505===e.errno;y=Buffer.from(w(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),l,f))}));const j={hostname:u.hostname,path:g,port:u.port,protocol:u.protocol,rejectUnauthorized:!1,method:o.method,headers:{...Object.assign({},...Object.entries(O).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase())).map((([e,t])=>({[e]:t})))),host:u.hostname}},U=!y&&!v&&"file:"!==u.protocol&&await new Promise((e=>{const n="https:"===u.protocol?(0,r.request)(j,e):(0,t.request)(j,e);n.on("error",(t=>{y=Buffer.from(w(t,"request",l,f)),e(null)})),o.on("data",(e=>n.write(e))),o.on("end",(()=>n.end()))}));if(y)return void R(502,i,y);d.ssl&&o.stream&&o.stream.readableLength&&E&&(o.stream.on("data",(e=>E.write(e))),o.stream.on("end",(()=>E.end()))),!d.ssl&&o.readableLength&&E&&(o.on("data",(e=>E.write(e))),o.on("end",(()=>E.end())));const{outboundResponseHeaders:x}=await new Promise((e=>E?E.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!E&&U?{outboundResponseHeaders:U.headers}:{outboundResponseHeaders:{}}))),N=x.location?new n.URL(x.location.startsWith("/")?`${u.href}${x.location.replace(/^\/+/,"")}`:x.location):null,L=N?N.href.substring(N.origin.length):null,P=l.origin,C=N?`${P}${L}`:null,B=E||U,H=y??await new Promise((e=>{let t=Buffer.alloc(0);B?(B.on("data",(e=>t=Buffer.concat([t,"string"==typeof e?Buffer.from(e):e]))),B.on("end",(()=>{e(t)}))):e(t)})).then((e=>d.replaceResponseBodyUrls&&e.length?(x["content-encoding"]||"").split(",").reduce((async(e,t)=>{const r=t.trim().toLowerCase(),n="gzip"===r||"x-gzip"===r?s.gunzip:"deflate"===r?s.inflate:"br"===r?s.brotliDecompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===n)return void R(502,i,Buffer.from(w(new Error(`${r} compression not supported by the proxy`),"stream",l,f)));const o=await e;return await new Promise((e=>n(o,((t,r)=>{if(t)return R(502,i,Buffer.from(w(t,"stream",l,f))),void e("");e(r)}))))}),Promise.resolve(e)).then((e=>e.length>1e6||/[^\x00-\x7F]/.test(e.toString())&&!(x["content-type"]??"").includes("text/html")?e:d.replaceResponseBodyUrls?Object.entries(d.mapping).reduce(((e,[t,r])=>""===t||t.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)?e.replace(new RegExp(r.replace(/^file:\/\//,"").replace(/[*+?^${}()|[\]\\]/g,"").replace(/^https/,"https?")+"/*","ig"),`https://${a}${t.replace(/\/+$/,"")}/`):e),e.toString()).split(`${a}/:`).join(`${a}:`):e.toString())).then((e=>(x["content-encoding"]||"").split(",").reduce(((e,t)=>{const r=t.trim().toLowerCase(),n="gzip"===r||"x-gzip"===r?s.gzip:"deflate"===r?s.deflate:"br"===r?s.brotliCompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===n)throw new Error(`${r} compression not supported by the proxy`);return e.then((e=>new Promise((t=>n(e,((e,r)=>{if(e)throw e;t(r)}))))))}),Promise.resolve(Buffer.from(e))))):e)),T={...Object.entries({...x,...d.replaceResponseBodyUrls?{"content-length":`${H.byteLength}`}:{}}).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase()&&"connection"!==e.toLowerCase())).reduce(((e,[t,r])=>{const n=m.split("").map(((e,t)=>m.substring(t).startsWith(".")&&m.substring(t))).filter((e=>e)),o=[m].concat(n).reduce(((e,t)=>(Array.isArray(e)?e:[e]).map((e=>"string"==typeof e?e.replace(`Domain=${t}`,`Domain=${l.hostname}`):e))),r);return e[t]=(e[t]||[]).concat(o),e}),{}),...C?{location:[C]}:{}};try{Object.entries(T).forEach((([e,t])=>t&&i.setHeader(e,t)))}catch(e){}i.writeHead(x[":status"]||U.statusCode||200,d.ssl?void 0:U.statusMessage||"Status read from http/2",T),H?i.end(H):i.end()})).addListener("error",(e=>{"EACCES"===e.code&&u("permission denied for this port",a.ERROR,"⛔"),"EADDRINUSE"===e.code&&u("port is already used. NOT started",a.ERROR,"☠️")})).addListener("listening",(()=>{g(d)})).listen(d.port)};m().then(v);
2
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});const e=require("http2"),t=require("http"),r=require("https"),o=require("url"),n=require("fs"),s=require("zlib"),a=require("path");var i;!function(e){e[e.ERROR=124]="ERROR",e[e.SUCCESS=35]="SUCCESS",e[e.INFO=21]="INFO",e[e.WARNING=172]="WARNING"}(i||(i={}));const l=(0,a.resolve)(process.env.HOME,".local-traffic.json"),p=(0,a.resolve)(process.cwd(),process.argv.slice(-1)[0].endsWith(".json")?process.argv.slice(-1)[0]:l),c={mapping:{},port:8080,replaceResponseBodyUrls:!1,dontUseHttp2Downstream:!1,simpleLogs:!1,websocket:!1};let d,h;const u=(e,t,r)=>{console.log(`${(e=>{const t=new Date;return`${e?"":""}${`${t.getHours()}`.padStart(2,"0")}${e?":":":"}${`${t.getMinutes()}`.padStart(2,"0")}${e?":":":"}${`${t.getSeconds()}`.padStart(2,"0")}${e?"":""}`})(d.simpleLogs)} ${d.simpleLogs?e.replace(/⎸/g,"|").replace(/⎹/g,"|").replace(/\u001b\[[^m]*m/g,"").replace(/↘️/g,"inbound").replace(/☎️/g,"port").replace(/↗️/g,"outbound"):t?`[48;5;${t}m⎸ ${process.stdout.isTTY&&r||""} ${e.padEnd(36)} ⎹`:e}`)},m=async(e=!0)=>new Promise((t=>(0,n.readFile)(p,((r,o)=>{r&&!e&&u("config error. Using default value",i.ERROR,"❌");try{d=Object.assign({},c,JSON.parse((o||"{}").toString()))}catch(e){return u("config syntax incorrect, aborting",i.ERROR,"⛈️"),d=d||{...c},void t(d)}d.mapping[""]||u('default mapping "" not provided.',i.WARNING,"☢️"),r&&"ENOENT"===r.code&&e&&p===l?(0,n.writeFile)(p,JSON.stringify(c),(e=>{e?u("config file NOT created",i.ERROR,"⁉️"):u("config file created",i.SUCCESS,"✨"),t(d)})):t(d)})))).then((()=>{e&&(0,n.watchFile)(p,f)})),g=e=>{u(`⎸ ↘️ : ${e.ssl?"HTTP/2 ":"HTTP 1.1"} ⎸ ☎️ : ${e.port.toString().padStart(5)} ⎸ ↗️ : ${e.dontUseHttp2Downstream?"HTTP 1.1":"HTTP/2 "} ⎹`)},f=async()=>{const e={...d};return await m(!1),isNaN(d.port)||d.port>65535||d.port<0?(d=e,void u("port number invalid. Not refreshing",i.ERROR,"☎️")):"object"!=typeof d.mapping?(d=e,void u("mapping should be an object. Aborting",i.ERROR,"⚡")):(d.replaceResponseBodyUrls&&!e.replaceResponseBodyUrls&&u("response body url replacement",i.INFO,"✔️"),!d.replaceResponseBodyUrls&&e.replaceResponseBodyUrls&&u("response body url NO replacement",i.INFO,"✖️"),d.websocket&&!e.websocket&&u("websocket activated",i.INFO,"☄️"),!d.websocket&&e.websocket&&u("websocket deactivated",i.INFO,"☄️"),u(`${Object.keys(d.mapping).length.toString().padStart(5)} loaded mapping rules`,i.SUCCESS,"↻"),void(d.port!==e.port||JSON.stringify(d.ssl)!==JSON.stringify(e.ssl)?(await new Promise((e=>h?h.close(e):e(void 0))),S()):d.dontUseHttp2Downstream!==e.dontUseHttp2Downstream&&g(d)))},$=e=>""==e?"":(0,a.normalize)(e).replace(/\\/g,"/"),w=e=>{const t=(0,a.resolve)("/",e.hostname,...e.pathname.replace(/[?#].*$/,"").replace(/^\/+/,"").split("/"));return{error:null,data:null,hasRun:!1,run:function(){return this.hasRun?Promise.resolve():new Promise((r=>(0,n.readFile)(t,((o,s)=>{if(this.hasRun=!0,!o||"EISDIR"!==o.code)return this.error=o,this.data=s,void r(void 0);(0,n.readdir)(t,((t,o)=>{this.error=t,this.data=o,t?r(void 0):Promise.all(o.map((t=>new Promise((r=>(0,n.lstat)((0,a.resolve)(e.pathname,t),((e,o)=>r([t,o,e])))))))).then((t=>{const o=t.filter((e=>!e[2]&&e[1].isDirectory())).concat(t.filter((e=>!e[2]&&e[1].isFile())));this.data=`${b(128194,"directory",e.href)}\n <p>Directory content of <i>${e.href.replace(/\//g,"&#x002F;")}</i></p>\n <ul class="list-group">\n <li class="list-group-item">&#x1F4C1;<a href="${e.pathname.endsWith("/")?"..":"."}">&lt;parent&gt;</a></li>\n ${o.filter((e=>!e[2])).map((t=>`<li class="list-group-item">&#x${(t[1].isDirectory()?128193:128196).toString(16)};<a href="${e.pathname.endsWith("/")?"":`${e.pathname.split("/").slice(-1)[0]}/`}${t[0]}">${t[0]}</a></li>`)).join("\n")}\n </li>\n </ul>\n </body></html>`,r(void 0)}))}))}))))},events:{},on:function(e,r){return this.events[e]=r,this.run().then((()=>{"response"===e&&this.events.response({Server:"local","Content-Type":t.endsWith(".svg")?"image/svg+xml":null},0),"data"===e&&this.data&&(this.events.data(this.data),this.events.end()),"error"===e&&this.error&&this.events.error(this.error)})),this},end:function(){return this},request:function(){return this}}},b=(e,t,r)=>`<!doctype html>\n<html lang="en">\n<head>\n<title>&#x${e.toString(16)}; local-traffic ${t} | ${r}</title>\n<link href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css" rel="stylesheet"/>\n<script src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"><\/script>\n<script src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"><\/script>\n</head>\n<body><div class="container"><h1>&#x${e.toString(16)}; local-traffic ${t}</h1>\n<br/>`,y=(e,t,r,o)=>`${b(128163,"error",e.message)}\n<p>An error happened while trying to proxy a remote exchange</p>\n<div class="alert alert-warning" role="alert">\n &#x24D8;&nbsp;This is not an error from the downstream service.\n</div>\n<div class="alert alert-danger" role="alert">\n<pre><code>${e.stack||`<i>${e.name} : ${e.message}</i>`}${e.errno?`<br/>(code : ${e.errno})`:""}</code></pre>\n</div>\nMore information about the request :\n<table class="table">\n <tbody>\n <tr>\n <td>phase</td>\n <td>${t}</td>\n </tr>\n <tr>\n <td>requested URL</td>\n <td>${r}</td>\n </tr>\n <tr>\n <td>downstream URL</td>\n <td>${o||"&lt;no-target-url&gt;"}</td>\n </tr>\n </tbody>\n</table>\n</div></body></html>`,R=(e,t,r)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":r.length}),t.end(r)},v=e=>{const t=(e.headers[":authority"]?.toString()??e.headers.host??"localhost").replace(/:.*/,""),r=e.headers[":authority"]||`${e.headers.host}${e.headers.host.match(/:[0-9]+$/)?"":80!==d.port||d.ssl?443===d.port&&d.ssl?"":`:${d.port}`:""}`,n=new o.URL(`http${d.ssl?"s":""}://${r}${e.url}`),s=n.href.substring(n.origin.length),[a,i]=Object.entries({...Object.assign({},...Object.entries(d.mapping).map((([e,t])=>({[e]:new o.URL($(t))}))))}).find((([e])=>s.match(RegExp(e))))||[];return{proxyHostname:t,proxyHostnameAndPort:r,url:n,path:s,key:a,target:i}},S=()=>{h=(d.ssl?e.createSecureServer.bind(null,{...d.ssl,allowHTTP1:!0}):t.createServer)((async(n,a)=>{if(!n.headers.host&&!n.headers[":authority"])return void R(400,a,Buffer.from(y(new Error("client must supply a 'host' header"),"proxy",new o.URL(`http${d.ssl?"s":""}://unknowndomain${n.url}`))));const{proxyHostname:i,proxyHostnameAndPort:l,url:c,path:h,key:u,target:m}=v(n);if(!m)return void R(502,a,Buffer.from(y(new Error(`No mapping found in config file ${p}`),"proxy",c)));const g=m.host.replace(RegExp(/\/+$/),""),f=`${m.href.substring("https://".length+m.host.length)}${$(h.replace(RegExp($(u)),""))}`.replace(/^\/*/,"/"),b=new o.URL(`${m.protocol}//${g}${f}`);let S=null,O=!d.dontUseHttp2Downstream;const j="file:"===m.protocol?w(b):O?await Promise.race([new Promise((t=>{const r=(0,e.connect)(b,{rejectUnauthorized:!1,protocol:m.protocol},((e,o)=>{O=O&&!!o.alpnProtocol,t(O?r:null)}));r.on("error",(e=>{S=O&&Buffer.from(y(e,"connection",c,b))}))})),new Promise((e=>setTimeout((()=>{O=!1,e(null)}),3e3)))]):null;S instanceof Buffer||(S=null);const E={...[...Object.entries(n.headers)].filter((([e])=>!["host","connection"].includes(e.toLowerCase()))).reduce(((e,[t,r])=>(e[t]=(e[t]||"")+(Array.isArray(r)?r:[r]).map((e=>e.replace(c.hostname,g))).join(", "),e)),{}),origin:m.href,referer:b.toString(),":authority":g,":method":n.method,":path":f,":scheme":m.protocol.replace(":","")},N=j&&!S&&j.request(E,{endStream:d.ssl?!(n?.stream?.readableLength??1):!n.readableLength});N&&N.on("error",(e=>{const t=-505===e.errno;S=Buffer.from(y(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),c,b))}));const U={hostname:m.hostname,path:f,port:m.port,protocol:m.protocol,rejectUnauthorized:!1,method:n.method,headers:{...Object.assign({},...Object.entries(E).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase())).map((([e,t])=>({[e]:t})))),host:m.hostname}},x=!S&&!O&&"file:"!==m.protocol&&await new Promise((e=>{const o="https:"===m.protocol?(0,r.request)(U,e):(0,t.request)(U,e);o.on("error",(t=>{S=Buffer.from(y(t,"request",c,b)),e(null)})),n.on("data",(e=>o.write(e))),n.on("end",(()=>o.end()))}));if(S)return void R(502,a,S);S=null,d.ssl&&n.stream&&n.stream.readableLength&&N&&(n.stream.on("data",(e=>N.write(e))),n.stream.on("end",(()=>N.end()))),!d.ssl&&n.readableLength&&N&&(n.on("data",(e=>N.write(e))),n.on("end",(()=>N.end())));const{outboundResponseHeaders:H}=await new Promise((e=>N?N.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!N&&x?{outboundResponseHeaders:x.headers}:{outboundResponseHeaders:{}}))),P=H.location?new o.URL(H.location.startsWith("/")?`${m.href}${H.location.replace(/^\/+/,"")}`:H.location):null,L=P?P.href.substring(P.origin.length):null,C=c.origin,T=P?`${C}${L}`:null,A=N||x,B=S??await new Promise((e=>{let t=Buffer.alloc(0);A?(A.on("data",(e=>t=Buffer.concat([t,"string"==typeof e?Buffer.from(e):e]))),A.on("end",(()=>{e(t)}))):e(t)})).then((e=>d.replaceResponseBodyUrls&&e.length?(H["content-encoding"]||"").split(",").reduce((async(e,t)=>{const r=t.trim().toLowerCase(),o="gzip"===r||"x-gzip"===r?s.gunzip:"deflate"===r?s.inflate:"br"===r?s.brotliDecompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===o)return void R(502,a,Buffer.from(y(new Error(`${r} compression not supported by the proxy`),"stream",c,b)));const n=await e;return await new Promise((e=>o(n,((t,r)=>{if(t)return R(502,a,Buffer.from(y(t,"stream",c,b))),void e("");e(r)}))))}),Promise.resolve(e)).then((e=>{const t=e.length>1e7,r=["text/html","application/javascript","application/json"].some((e=>(H["content-type"]??"").includes(e)));return!t&&(r||!/[^\x00-\x7F]/.test(e.toString()))?d.replaceResponseBodyUrls?Object.entries(d.mapping).reduce(((e,[t,r])=>""===t||t.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)?e.replace(new RegExp(r.replace(/^file:\/\//,"").replace(/[*+?^${}()|[\]\\]/g,"").replace(/^https/,"https?")+"/*","ig"),`https://${l}${t.replace(/\/+$/,"")}/`):e),e.toString()).split(`${l}/:`).join(`${l}:`).replace(/\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,`?protocol=ws${d.ssl?"s":""}%3A&hostname=${i}&port=${d.port}&pathname=${encodeURIComponent(u.replace(/\/+$/,""))}`):e.toString():e})).then((e=>(H["content-encoding"]||"").split(",").reduce(((e,t)=>{const r=t.trim().toLowerCase(),o="gzip"===r||"x-gzip"===r?s.gzip:"deflate"===r?s.deflate:"br"===r?s.brotliCompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===o)throw new Error(`${r} compression not supported by the proxy`);return e.then((e=>new Promise((t=>o(e,((e,r)=>{if(e)throw e;t(r)}))))))}),Promise.resolve(Buffer.from(e))))):e)),q={...Object.entries({...H,...d.replaceResponseBodyUrls?{"content-length":`${B.byteLength}`}:{}}).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase()&&"connection"!==e.toLowerCase())).reduce(((e,[t,r])=>{const o=g.split("").map(((e,t)=>g.substring(t).startsWith(".")&&g.substring(t))).filter((e=>e)),n=[g].concat(o).reduce(((e,t)=>(Array.isArray(e)?e:[e]).map((e=>"string"==typeof e?e.replace(`Domain=${t}`,`Domain=${c.hostname}`):e))),r);return e[t]=(e[t]||[]).concat(n),e}),{}),...T?{location:[T]}:{}};try{Object.entries(q).forEach((([e,t])=>t&&a.setHeader(e,t)))}catch(e){}a.writeHead(H[":status"]||x.statusCode||200,d.ssl?void 0:x.statusMessage||"Status read from http/2",q),B?a.end(B):a.end()})).addListener("error",(e=>{"EACCES"===e.code&&u("permission denied for this port",i.ERROR,"⛔"),"EADDRINUSE"===e.code&&u("port is already used. NOT started",i.ERROR,"☠️")})).addListener("listening",(()=>{g(d)})).on("upgrade",((e,n)=>{if(!d.websocket)return void n.end("HTTP/1.1 503 Service Unavailable\r\n\r\n");const{key:s,target:a}=v(e),l=new o.URL(`${a.protocol}//${a.host}${e.url.replace(new RegExp(`^${s}`,"g"),"").replace(/^\/*/,"/")}`),p={hostname:l.hostname,path:l.pathname,port:l.port,protocol:l.protocol,rejectUnauthorized:!1,method:e.method,headers:e.headers,host:l.hostname},c="https:"===l.protocol?(0,r.request)(p):(0,t.request)(p);c.end(),c.on("error",(e=>{u("websocket request has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,"☄️")})),c.on("upgrade",((e,t)=>{const r=`HTTP/${e.httpVersion} ${e.statusCode} ${e.statusMessage}\r\n${Object.entries(e.headers).flatMap((([e,t])=>(Array.isArray(t)?t:[t]).map((t=>[e,t])))).map((([e,t])=>`${e}: ${t}\r\n`)).join("")}\r\n`;n.write(r),n.allowHalfOpen=!0,t.allowHalfOpen=!0,t.on("data",(e=>n.write(e))),n.on("data",(e=>t.write(e))),t.on("error",(e=>{u("downstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,"☄️")})),n.on("error",(e=>{u("upstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,"☄️")}))}))})).listen(d.port)};m().then(S);
package/dist/terser.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});const e=require("http2"),t=require("http"),r=require("https"),o=require("url"),n=require("fs"),s=require("zlib"),a=require("path");var i;!function(e){e[e.ERROR=124]="ERROR",e[e.SUCCESS=35]="SUCCESS",e[e.INFO=21]="INFO",e[e.WARNING=172]="WARNING"}(i||(i={}));const l=(0,a.resolve)(process.env.HOME,".local-traffic.json"),p=(0,a.resolve)(process.cwd(),process.argv.slice(-1)[0].endsWith(".json")?process.argv.slice(-1)[0]:l),c={mapping:{},port:8080,replaceResponseBodyUrls:!1,dontUseHttp2Downstream:!1,simpleLogs:!1,websocket:!1};let d,h;const u=(e,t,r)=>{console.log(`${(e=>{const t=new Date;return`${e?"":""}${`${t.getHours()}`.padStart(2,"0")}${e?":":":"}${`${t.getMinutes()}`.padStart(2,"0")}${e?":":":"}${`${t.getSeconds()}`.padStart(2,"0")}${e?"":""}`})(d.simpleLogs)} ${d.simpleLogs?e.replace(/⎸/g,"|").replace(/⎹/g,"|").replace(/\u001b\[[^m]*m/g,"").replace(/↘️/g,"inbound").replace(/☎️/g,"port").replace(/↗️/g,"outbound"):t?`[48;5;${t}m⎸ ${process.stdout.isTTY&&r||""} ${e.padEnd(36)} ⎹`:e}`)},m=async(e=!0)=>new Promise((t=>(0,n.readFile)(p,((r,o)=>{r&&!e&&u("config error. Using default value",i.ERROR,"❌");try{d=Object.assign({},c,JSON.parse((o||"{}").toString()))}catch(e){return u("config syntax incorrect, aborting",i.ERROR,"⛈️"),d=d||{...c},void t(d)}d.mapping[""]||u('default mapping "" not provided.',i.WARNING,"☢️"),r&&"ENOENT"===r.code&&e&&p===l?(0,n.writeFile)(p,JSON.stringify(c),(e=>{e?u("config file NOT created",i.ERROR,"⁉️"):u("config file created",i.SUCCESS,"✨"),t(d)})):t(d)})))).then((()=>{e&&(0,n.watchFile)(p,f)})),g=e=>{u(`⎸ ↘️ : ${e.ssl?"HTTP/2 ":"HTTP 1.1"} ⎸ ☎️ : ${e.port.toString().padStart(5)} ⎸ ↗️ : ${e.dontUseHttp2Downstream?"HTTP 1.1":"HTTP/2 "} ⎹`)},f=async()=>{const e={...d};return await m(!1),isNaN(d.port)||d.port>65535||d.port<0?(d=e,void u("port number invalid. Not refreshing",i.ERROR,"☎️")):"object"!=typeof d.mapping?(d=e,void u("mapping should be an object. Aborting",i.ERROR,"⚡")):(d.replaceResponseBodyUrls&&!e.replaceResponseBodyUrls&&u("response body url replacement",i.INFO,"✔️"),!d.replaceResponseBodyUrls&&e.replaceResponseBodyUrls&&u("response body url NO replacement",i.INFO,"✖️"),d.websocket&&!e.websocket&&u("websocket activated",i.INFO,"☄️"),!d.websocket&&e.websocket&&u("websocket deactivated",i.INFO,"☄️"),u(`${Object.keys(d.mapping).length.toString().padStart(5)} loaded mapping rules`,i.SUCCESS,"↻"),void(d.port!==e.port||JSON.stringify(d.ssl)!==JSON.stringify(e.ssl)?(await new Promise((e=>h?h.close(e):e(void 0))),S()):d.dontUseHttp2Downstream!==e.dontUseHttp2Downstream&&g(d)))},$=e=>""==e?"":(0,a.normalize)(e).replace(/\\/g,"/"),w=e=>{const t=(0,a.resolve)("/",e.hostname,...e.pathname.replace(/[?#].*$/,"").replace(/^\/+/,"").split("/"));return{error:null,data:null,hasRun:!1,run:function(){return this.hasRun?Promise.resolve():new Promise((r=>(0,n.readFile)(t,((o,s)=>{if(this.hasRun=!0,!o||"EISDIR"!==o.code)return this.error=o,this.data=s,void r(void 0);(0,n.readdir)(t,((t,o)=>{this.error=t,this.data=o,t?r(void 0):Promise.all(o.map((t=>new Promise((r=>(0,n.lstat)((0,a.resolve)(e.pathname,t),((e,o)=>r([t,o,e])))))))).then((t=>{const o=t.filter((e=>!e[2]&&e[1].isDirectory())).concat(t.filter((e=>!e[2]&&e[1].isFile())));this.data=`${b(128194,"directory",e.href)}\n <p>Directory content of <i>${e.href.replace(/\//g,"&#x002F;")}</i></p>\n <ul class="list-group">\n <li class="list-group-item">&#x1F4C1;<a href="${e.pathname.endsWith("/")?"..":"."}">&lt;parent&gt;</a></li>\n ${o.filter((e=>!e[2])).map((t=>`<li class="list-group-item">&#x${(t[1].isDirectory()?128193:128196).toString(16)};<a href="${e.pathname.endsWith("/")?"":`${e.pathname.split("/").slice(-1)[0]}/`}${t[0]}">${t[0]}</a></li>`)).join("\n")}\n </li>\n </ul>\n </body></html>`,r(void 0)}))}))}))))},events:{},on:function(e,r){return this.events[e]=r,this.run().then((()=>{"response"===e&&this.events.response({Server:"local","Content-Type":t.endsWith(".svg")?"image/svg+xml":null},0),"data"===e&&this.data&&(this.events.data(this.data),this.events.end()),"error"===e&&this.error&&this.events.error(this.error)})),this},end:function(){return this},request:function(){return this}}},b=(e,t,r)=>`<!doctype html>\n<html lang="en">\n<head>\n<title>&#x${e.toString(16)}; local-traffic ${t} | ${r}</title>\n<link href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css" rel="stylesheet"/>\n<script src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"><\/script>\n<script src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"><\/script>\n</head>\n<body><div class="container"><h1>&#x${e.toString(16)}; local-traffic ${t}</h1>\n<br/>`,y=(e,t,r,o)=>`${b(128163,"error",e.message)}\n<p>An error happened while trying to proxy a remote exchange</p>\n<div class="alert alert-warning" role="alert">\n &#x24D8;&nbsp;This is not an error from the downstream service.\n</div>\n<div class="alert alert-danger" role="alert">\n<pre><code>${e.stack||`<i>${e.name} : ${e.message}</i>`}${e.errno?`<br/>(code : ${e.errno})`:""}</code></pre>\n</div>\nMore information about the request :\n<table class="table">\n <tbody>\n <tr>\n <td>phase</td>\n <td>${t}</td>\n </tr>\n <tr>\n <td>requested URL</td>\n <td>${r}</td>\n </tr>\n <tr>\n <td>downstream URL</td>\n <td>${o||"&lt;no-target-url&gt;"}</td>\n </tr>\n </tbody>\n</table>\n</div></body></html>`,R=(e,t,r)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":r.length}),t.end(r)},v=e=>{const t=(e.headers[":authority"]?.toString()??e.headers.host??"localhost").replace(/:.*/,""),r=e.headers[":authority"]||`${e.headers.host}${e.headers.host.match(/:[0-9]+$/)?"":80!==d.port||d.ssl?443===d.port&&d.ssl?"":`:${d.port}`:""}`,n=new o.URL(`http${d.ssl?"s":""}://${r}${e.url}`),s=n.href.substring(n.origin.length),[a,i]=Object.entries({...Object.assign({},...Object.entries(d.mapping).map((([e,t])=>({[e]:new o.URL($(t))}))))}).find((([e])=>s.match(RegExp(e))))||[];return{proxyHostname:t,proxyHostnameAndPort:r,url:n,path:s,key:a,target:i}},S=()=>{h=(d.ssl?e.createSecureServer.bind(null,{...d.ssl,allowHTTP1:!0}):t.createServer)((async(n,a)=>{if(!n.headers.host&&!n.headers[":authority"])return void R(400,a,Buffer.from(y(new Error("client must supply a 'host' header"),"proxy",new o.URL(`http${d.ssl?"s":""}://unknowndomain${n.url}`))));const{proxyHostname:i,proxyHostnameAndPort:l,url:c,path:h,key:u,target:m}=v(n);if(!m)return void R(502,a,Buffer.from(y(new Error(`No mapping found in config file ${p}`),"proxy",c)));const g=m.host.replace(RegExp(/\/+$/),""),f=`${m.href.substring("https://".length+m.host.length)}${$(h.replace(RegExp($(u)),""))}`.replace(/^\/*/,"/"),b=new o.URL(`${m.protocol}//${g}${f}`);let S=null,O=!d.dontUseHttp2Downstream;const j="file:"===m.protocol?w(b):O?await Promise.race([new Promise((t=>{const r=(0,e.connect)(b,{rejectUnauthorized:!1,protocol:m.protocol},((e,o)=>{O=O&&!!o.alpnProtocol,t(O?r:null)}));r.on("error",(e=>{S=O&&Buffer.from(y(e,"connection",c,b))}))})),new Promise((e=>setTimeout((()=>{O=!1,e(null)}),3e3)))]):null;S instanceof Buffer||(S=null);const E={...[...Object.entries(n.headers)].filter((([e])=>!["host","connection"].includes(e.toLowerCase()))).reduce(((e,[t,r])=>(e[t]=(e[t]||"")+(Array.isArray(r)?r:[r]).map((e=>e.replace(c.hostname,g))).join(", "),e)),{}),origin:m.href,referer:b.toString(),":authority":g,":method":n.method,":path":f,":scheme":m.protocol.replace(":","")},N=j&&!S&&j.request(E,{endStream:d.ssl?!(n?.stream?.readableLength??1):!n.readableLength});N&&N.on("error",(e=>{const t=-505===e.errno;S=Buffer.from(y(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),c,b))}));const U={hostname:m.hostname,path:f,port:m.port,protocol:m.protocol,rejectUnauthorized:!1,method:n.method,headers:{...Object.assign({},...Object.entries(E).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase())).map((([e,t])=>({[e]:t})))),host:m.hostname}},x=!S&&!O&&"file:"!==m.protocol&&await new Promise((e=>{const o="https:"===m.protocol?(0,r.request)(U,e):(0,t.request)(U,e);o.on("error",(t=>{S=Buffer.from(y(t,"request",c,b)),e(null)})),n.on("data",(e=>o.write(e))),n.on("end",(()=>o.end()))}));if(S)return void R(502,a,S);S=null,d.ssl&&n.stream&&n.stream.readableLength&&N&&(n.stream.on("data",(e=>N.write(e))),n.stream.on("end",(()=>N.end()))),!d.ssl&&n.readableLength&&N&&(n.on("data",(e=>N.write(e))),n.on("end",(()=>N.end())));const{outboundResponseHeaders:H}=await new Promise((e=>N?N.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!N&&x?{outboundResponseHeaders:x.headers}:{outboundResponseHeaders:{}}))),P=H.location?new o.URL(H.location.startsWith("/")?`${m.href}${H.location.replace(/^\/+/,"")}`:H.location):null,L=P?P.href.substring(P.origin.length):null,C=c.origin,T=P?`${C}${L}`:null,A=N||x,B=S??await new Promise((e=>{let t=Buffer.alloc(0);A?(A.on("data",(e=>t=Buffer.concat([t,"string"==typeof e?Buffer.from(e):e]))),A.on("end",(()=>{e(t)}))):e(t)})).then((e=>d.replaceResponseBodyUrls&&e.length?(H["content-encoding"]||"").split(",").reduce((async(e,t)=>{const r=t.trim().toLowerCase(),o="gzip"===r||"x-gzip"===r?s.gunzip:"deflate"===r?s.inflate:"br"===r?s.brotliDecompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===o)return void R(502,a,Buffer.from(y(new Error(`${r} compression not supported by the proxy`),"stream",c,b)));const n=await e;return await new Promise((e=>o(n,((t,r)=>{if(t)return R(502,a,Buffer.from(y(t,"stream",c,b))),void e("");e(r)}))))}),Promise.resolve(e)).then((e=>{const t=e.length>1e7,r=["text/html","application/javascript","application/json"].some((e=>(H["content-type"]??"").includes(e)));return!t&&(r||!/[^\x00-\x7F]/.test(e.toString()))?d.replaceResponseBodyUrls?Object.entries(d.mapping).reduce(((e,[t,r])=>""===t||t.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)?e.replace(new RegExp(r.replace(/^file:\/\//,"").replace(/[*+?^${}()|[\]\\]/g,"").replace(/^https/,"https?")+"/*","ig"),`https://${l}${t.replace(/\/+$/,"")}/`):e),e.toString()).split(`${l}/:`).join(`${l}:`).replace(/\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,`?protocol=ws${d.ssl?"s":""}%3A&hostname=${i}&port=${d.port}&pathname=${encodeURIComponent(u.replace(/\/+$/,""))}`):e.toString():e})).then((e=>(H["content-encoding"]||"").split(",").reduce(((e,t)=>{const r=t.trim().toLowerCase(),o="gzip"===r||"x-gzip"===r?s.gzip:"deflate"===r?s.deflate:"br"===r?s.brotliCompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===o)throw new Error(`${r} compression not supported by the proxy`);return e.then((e=>new Promise((t=>o(e,((e,r)=>{if(e)throw e;t(r)}))))))}),Promise.resolve(Buffer.from(e))))):e)),q={...Object.entries({...H,...d.replaceResponseBodyUrls?{"content-length":`${B.byteLength}`}:{}}).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase()&&"connection"!==e.toLowerCase())).reduce(((e,[t,r])=>{const o=g.split("").map(((e,t)=>g.substring(t).startsWith(".")&&g.substring(t))).filter((e=>e)),n=[g].concat(o).reduce(((e,t)=>(Array.isArray(e)?e:[e]).map((e=>"string"==typeof e?e.replace(`Domain=${t}`,`Domain=${c.hostname}`):e))),r);return e[t]=(e[t]||[]).concat(n),e}),{}),...T?{location:[T]}:{}};try{Object.entries(q).forEach((([e,t])=>t&&a.setHeader(e,t)))}catch(e){}a.writeHead(H[":status"]||x.statusCode||200,d.ssl?void 0:x.statusMessage||"Status read from http/2",q),B?a.end(B):a.end()})).addListener("error",(e=>{"EACCES"===e.code&&u("permission denied for this port",i.ERROR,"⛔"),"EADDRINUSE"===e.code&&u("port is already used. NOT started",i.ERROR,"☠️")})).addListener("listening",(()=>{g(d)})).on("upgrade",((e,n)=>{if(!d.websocket)return void n.end("HTTP/1.1 503 Service Unavailable\r\n\r\n");const{key:s,target:a}=v(e),l=new o.URL(`${a.protocol}//${a.host}${e.url.replace(new RegExp(`^${s}`,"g"),"").replace(/^\/*/,"/")}`),p={hostname:l.hostname,path:l.pathname,port:l.port,protocol:l.protocol,rejectUnauthorized:!1,method:e.method,headers:e.headers,host:l.hostname},c="https:"===l.protocol?(0,r.request)(p):(0,t.request)(p);c.end(),c.on("error",(e=>{u("websocket request has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,"☄️")})),c.on("upgrade",((e,t)=>{const r=`HTTP/${e.httpVersion} ${e.statusCode} ${e.statusMessage}\r\n${Object.entries(e.headers).flatMap((([e,t])=>(Array.isArray(t)?t:[t]).map((t=>[e,t])))).map((([e,t])=>`${e}: ${t}\r\n`)).join("")}\r\n`;n.write(r),n.allowHalfOpen=!0,t.allowHalfOpen=!0,t.on("data",(e=>n.write(e))),n.on("data",(e=>t.write(e))),t.on("error",(e=>{u("downstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,"☄️")})),n.on("error",(e=>{u("upstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,"☄️")}))}))})).listen(d.port)};m().then(S);
package/index.ts CHANGED
@@ -33,6 +33,8 @@ import {
33
33
  import { resolve, normalize } from "path";
34
34
  import type { Duplex } from "stream";
35
35
 
36
+ type ErrorWithErrno = NodeJS.ErrnoException;
37
+
36
38
  enum LogLevel {
37
39
  ERROR = 124,
38
40
  SUCCESS = 35,
@@ -47,6 +49,7 @@ interface LocalConfiguration {
47
49
  replaceResponseBodyUrls?: boolean;
48
50
  dontUseHttp2Downstream?: boolean;
49
51
  simpleLogs?: boolean;
52
+ websocket?: boolean;
50
53
  }
51
54
 
52
55
  const userHomeConfigFile = resolve(process.env.HOME, ".local-traffic.json");
@@ -62,6 +65,7 @@ const defaultConfig: LocalConfiguration = {
62
65
  replaceResponseBodyUrls: false,
63
66
  dontUseHttp2Downstream: false,
64
67
  simpleLogs: false,
68
+ websocket: false,
65
69
  };
66
70
 
67
71
  let config: LocalConfiguration;
@@ -172,6 +176,18 @@ const onWatch = async () => {
172
176
  ) {
173
177
  log("response body url NO replacement", LogLevel.INFO, "✖️");
174
178
  }
179
+ if (
180
+ config.websocket &&
181
+ !previousConfig.websocket
182
+ ) {
183
+ log("websocket activated", LogLevel.INFO, "☄️");
184
+ }
185
+ if (
186
+ !config.websocket &&
187
+ previousConfig.websocket
188
+ ) {
189
+ log("websocket deactivated", LogLevel.INFO, "☄️");
190
+ }
175
191
  log(
176
192
  `${Object.keys(config.mapping)
177
193
  .length.toString()
@@ -348,7 +364,7 @@ const errorPage = (
348
364
  </div>
349
365
  <div class="alert alert-danger" role="alert">
350
366
  <pre><code>${thrown.stack || `<i>${thrown.name} : ${thrown.message}</i>`}${
351
- (thrown as any).errno ? `<br/>(code : ${(thrown as any).errno})` : ""
367
+ (thrown as ErrorWithErrno).errno ? `<br/>(code : ${(thrown as ErrorWithErrno).errno})` : ""
352
368
  }</code></pre>
353
369
  </div>
354
370
  More information about the request :
@@ -386,8 +402,39 @@ const send = (
386
402
  inboundResponse.end(errorBuffer);
387
403
  };
388
404
 
405
+ const determineMapping = (inboundRequest: Http2ServerRequest | IncomingMessage): {
406
+ proxyHostname: string,
407
+ proxyHostnameAndPort: string,
408
+ url: URL,
409
+ path: string,
410
+ key: string,
411
+ target: URL
412
+ } => {
413
+
414
+ const proxyHostname =
415
+ (inboundRequest.headers[":authority"]?.toString() ??
416
+ inboundRequest.headers.host ?? 'localhost').replace(/:.*/, '');
417
+ const proxyHostnameAndPort =
418
+ inboundRequest.headers[":authority"] as string ||
419
+ `${inboundRequest.headers.host}${inboundRequest.headers.host.match(/:[0-9]+$/)
420
+ ? ""
421
+ : config.port === 80 && !config.ssl
422
+ ? ""
423
+ : config.port === 443 && config.ssl
424
+ ? ""
425
+ : `:${config.port}`
426
+ }`;
427
+ const url = new URL(
428
+ `http${config.ssl ? "s" : ""}://${proxyHostnameAndPort}${inboundRequest.url}`
429
+ );
430
+ const path = url.href.substring(url.origin.length);
431
+ const [key, target] =
432
+ Object.entries(envs()).find(([key]) => path.match(RegExp(key))) || [];
433
+ return { proxyHostname, proxyHostnameAndPort, url, path, key, target };
434
+ }
435
+
389
436
  const start = () => {
390
- server = (config.ssl
437
+ server = ((config.ssl
391
438
  ? createSecureServer.bind(null, { ...config.ssl, allowHTTP1: true })
392
439
  : createServer)(
393
440
  async (
@@ -409,23 +456,8 @@ const start = () => {
409
456
  );
410
457
  return;
411
458
  }
412
- const proxyHostname =
413
- inboundRequest.headers[":authority"] ||
414
- `${inboundRequest.headers.host}${
415
- inboundRequest.headers.host.match(/:[0-9]+$/)
416
- ? ""
417
- : config.port === 80 && !config.ssl
418
- ? ""
419
- : config.port === 443 && config.ssl
420
- ? ""
421
- : `:${config.port}`
422
- }`;
423
- const url = new URL(
424
- `http${config.ssl ? "s" : ""}://${proxyHostname}${inboundRequest.url}`
425
- );
426
- const path = url.href.substring(url.origin.length);
427
- const [key, target] =
428
- Object.entries(envs()).find(([key]) => path.match(RegExp(key))) || [];
459
+ const { proxyHostname, proxyHostnameAndPort, url, path, key, target } =
460
+ determineMapping(inboundRequest);
429
461
  if (!target) {
430
462
  send(
431
463
  502,
@@ -489,7 +521,7 @@ const start = () => {
489
521
  }, 3000)
490
522
  ),
491
523
  ]);
492
- if (!http2IsSupported && error) error = null;
524
+ if (!(error instanceof Buffer)) error = null;
493
525
 
494
526
  const outboundHeaders: OutgoingHttpHeaders = {
495
527
  ...[...Object.entries(inboundRequest.headers)]
@@ -526,7 +558,7 @@ const start = () => {
526
558
  ((outboundExchange as unknown) as Http2Stream).on(
527
559
  "error",
528
560
  (thrown: Error) => {
529
- const httpVersionSupported = (thrown as any).errno === -505;
561
+ const httpVersionSupported = (thrown as ErrorWithErrno).errno === -505;
530
562
  error = Buffer.from(
531
563
  errorPage(
532
564
  thrown,
@@ -581,11 +613,11 @@ const start = () => {
581
613
  );
582
614
  inboundRequest.on("end", () => outboundHttp1Request.end());
583
615
  }));
584
-
616
+ // intriguingly, error is reset to "false" at this point, even if it was null
585
617
  if (error) {
586
618
  send(502, inboundResponse, error);
587
619
  return;
588
- }
620
+ } else error = null;
589
621
 
590
622
  // phase : request body
591
623
  if (
@@ -728,10 +760,14 @@ const start = () => {
728
760
  })
729
761
  );
730
762
  }, Promise.resolve(payloadBuffer))
731
- .then((uncompressedBuffer: Buffer) =>
732
- (uncompressedBuffer.length > 1E6 ||
733
- /[^\x00-\x7F]/.test(uncompressedBuffer.toString()) &&
734
- !(outboundResponseHeaders["content-type"] ?? "").includes('text/html')) ?
763
+ .then((uncompressedBuffer: Buffer) => {
764
+ const fileTooBig = uncompressedBuffer.length > 1E7;
765
+ const fileHasSpecialChars = () => /[^\x00-\x7F]/.test(uncompressedBuffer.toString());
766
+ const contentTypeCanBeProcessed =
767
+ ['text/html', 'application/javascript', 'application/json'].some(allowedContentType =>
768
+ (outboundResponseHeaders["content-type"] ?? "").includes(allowedContentType));
769
+ const willReplace = !fileTooBig && (contentTypeCanBeProcessed || !fileHasSpecialChars());
770
+ return !willReplace ?
735
771
  uncompressedBuffer :
736
772
  !config.replaceResponseBodyUrls
737
773
  ? uncompressedBuffer.toString()
@@ -748,16 +784,20 @@ const start = () => {
748
784
  .replace(/^https/, 'https?') + '/*',
749
785
  "ig"
750
786
  ),
751
- `https://${proxyHostname}${path.replace(
787
+ `https://${proxyHostnameAndPort}${path.replace(
752
788
  /\/+$/,
753
789
  ""
754
790
  )}/`
755
791
  ),
756
792
  uncompressedBuffer.toString()
757
793
  )
758
- .split(`${proxyHostname}/:`)
759
- .join(`${proxyHostname}:`)
760
- )
794
+ .split(`${proxyHostnameAndPort}/:`)
795
+ .join(`${proxyHostnameAndPort}:`)
796
+ .replace(/\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,
797
+ `?protocol=ws${config.ssl ?
798
+ "s" : ""}%3A&hostname=${proxyHostname}&port=${config.port}&pathname=${
799
+ encodeURIComponent(key.replace(/\/+$/, ''))}`)
800
+ })
761
801
  .then((updatedBody: Buffer | string) =>
762
802
  (outboundResponseHeaders["content-encoding"] || "")
763
803
  .split(",")
@@ -858,16 +898,70 @@ const start = () => {
858
898
  if (payload) inboundResponse.end(payload);
859
899
  else inboundResponse.end();
860
900
  }
861
- )
901
+ ) as Server)
862
902
  .addListener("error", (err: Error) => {
863
- if ((err as any).code === "EACCES")
903
+ if ((err as ErrorWithErrno).code === "EACCES")
864
904
  log(`permission denied for this port`, LogLevel.ERROR, "⛔");
865
- if ((err as any).code === "EADDRINUSE")
905
+ if ((err as ErrorWithErrno).code === "EADDRINUSE")
866
906
  log(`port is already used. NOT started`, LogLevel.ERROR, "☠️");
867
907
  })
868
908
  .addListener("listening", () => {
869
909
  logProtocols(config);
870
910
  })
911
+ .on("upgrade", (request: IncomingMessage, upstreamSocket: Duplex) => {
912
+ if (!config.websocket) {
913
+ upstreamSocket.end(`HTTP/1.1 503 Service Unavailable\r\n\r\n`)
914
+ return;
915
+ }
916
+
917
+ const { key, target: targetWithForcedPrefix } = determineMapping(request);
918
+ const target = new URL(`${targetWithForcedPrefix.protocol}//${
919
+ targetWithForcedPrefix.host}${request.url.replace(
920
+ new RegExp(`^${key}`, 'g'), '').replace(/^\/*/, '/')}`);
921
+ const downstreamRequestOptions: RequestOptions = {
922
+ hostname: target.hostname,
923
+ path: target.pathname,
924
+ port: target.port,
925
+ protocol: target.protocol,
926
+ rejectUnauthorized: false,
927
+ method: request.method,
928
+ headers: request.headers,
929
+ host: target.hostname,
930
+ };
931
+
932
+ const downstreamRequest = target.protocol === "https:"
933
+ ? httpsRequest(downstreamRequestOptions)
934
+ : httpRequest(downstreamRequestOptions);
935
+ downstreamRequest.end();
936
+ downstreamRequest.on('error', (error) => {
937
+ log(`websocket request has errored ${
938
+ (error as ErrorWithErrno).errno ?
939
+ `(${(error as ErrorWithErrno).errno})` : ''}`, LogLevel.WARNING, "☄️")
940
+ });
941
+ downstreamRequest.on('upgrade', (response, downstreamSocket) => {
942
+ const upgradeResponse = `HTTP/${response.httpVersion} ${response.statusCode} ${
943
+ response.statusMessage}\r\n${Object.entries(response.headers)
944
+ .flatMap(([key, value]) => (!Array.isArray(value) ? [value] : value)
945
+ .map(oneValue => [key, oneValue]))
946
+ .map(([key, value]) =>
947
+ `${key}: ${value}\r\n`).join('')}\r\n`;
948
+ upstreamSocket.write(upgradeResponse);
949
+ upstreamSocket.allowHalfOpen = true;
950
+ downstreamSocket.allowHalfOpen = true;
951
+ downstreamSocket.on('data', (data) => upstreamSocket.write(data));
952
+ upstreamSocket.on('data', (data) => downstreamSocket.write(data));
953
+ downstreamSocket.on('error', (error) => {
954
+ log(`downstream socket has errored ${
955
+ (error as ErrorWithErrno).errno ?
956
+ `(${(error as ErrorWithErrno).errno})` : ''}`, LogLevel.WARNING, "☄️")
957
+ })
958
+ upstreamSocket.on('error', (error) => {
959
+ log(`upstream socket has errored ${
960
+ (error as ErrorWithErrno).errno ?
961
+ `(${(error as ErrorWithErrno).errno})` : ''}`, LogLevel.WARNING, "☄️")
962
+ })
963
+ });
964
+ })
871
965
  .listen(config.port);
872
966
  };
873
967
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-traffic",
3
- "version": "0.0.32",
3
+ "version": "0.0.33",
4
4
  "main": "index.ts",
5
5
  "private": false,
6
6
  "keywords": [
@@ -8,7 +8,8 @@
8
8
  "proxy",
9
9
  "h2",
10
10
  "http2",
11
- "https"
11
+ "https",
12
+ "websocket"
12
13
  ],
13
14
  "license": "MIT",
14
15
  "repository": "git@github.com:libetl/local-traffic.git",
@@ -16,16 +17,22 @@
16
17
  "scripts": {
17
18
  "start": "./dist/localTraffic.js",
18
19
  "typescript": "tsc",
19
- "terser": "echo '#!/usr/bin/env node\n' \"$(terser ./dist/index.js -c -m --toplevel)\" > ./dist/localTraffic.js",
20
+ "shebang": "echo '#!/usr/bin/env node' | cat - ./dist/terser.js > ./dist/localTraffic.js",
21
+ "terser": "terser ./dist/index.js -c -m --toplevel > ./dist/terser.js",
20
22
  "chmod": "chmod a+x ./dist/localTraffic.js",
21
- "build": "yarn typescript && yarn terser && yarn chmod"
23
+ "clean": "rm -rf dist",
24
+ "build": "npm run clean && npm run typescript && npm run terser && npm run shebang && npm run chmod"
22
25
  },
23
26
  "devDependencies": {
24
- "@types/node": "^17.0.17",
25
- "terser": "^5.10.0",
26
- "typescript": "^4.5.5"
27
+ "@types/node": "^17.0.33",
28
+ "terser": "^5.13.1",
29
+ "typescript": "^4.6.4"
27
30
  },
28
31
  "bin": {
29
32
  "local-traffic": "./dist/localTraffic.js"
33
+ },
34
+ "volta": {
35
+ "node": "18.1.0",
36
+ "npm": "8.10.0"
30
37
  }
31
38
  }
@@ -1 +0,0 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});const e=require("http2"),t=require("http"),r=require("https"),n=require("url"),o=require("fs"),s=require("zlib"),i=require("path");var a;!function(e){e[e.ERROR=124]="ERROR",e[e.SUCCESS=35]="SUCCESS",e[e.INFO=21]="INFO",e[e.WARNING=172]="WARNING"}(a||(a={}));const l=(0,i.resolve)(process.env.HOME,".local-traffic.json"),p=(0,i.resolve)(process.cwd(),process.argv.slice(-1)[0].endsWith(".json")?process.argv.slice(-1)[0]:l),c={mapping:{},port:8080,replaceResponseBodyUrls:!1,dontUseHttp2Downstream:!1,simpleLogs:!1};let d,h;const u=(e,t,r)=>{console.log(`${(e=>{const t=new Date;return`${e?"":""}${`${t.getHours()}`.padStart(2,"0")}${e?":":":"}${`${t.getMinutes()}`.padStart(2,"0")}${e?":":":"}${`${t.getSeconds()}`.padStart(2,"0")}${e?"":""}`})(d.simpleLogs)} ${d.simpleLogs?e.replace(/⎸/g,"|").replace(/⎹/g,"|").replace(/\u001b\[[^m]*m/g,"").replace(/↘️/g,"inbound").replace(/☎️/g,"port").replace(/↗️/g,"outbound"):t?`[48;5;${t}m⎸ ${process.stdout.isTTY&&r||""} ${e.padEnd(36)} ⎹`:e}`)},m=async(e=!0)=>new Promise((t=>(0,o.readFile)(p,((r,n)=>{r&&!e&&u("config error. Using default value",a.ERROR,"❌");try{d=Object.assign({},c,JSON.parse((n||"{}").toString()))}catch(e){return u("config syntax incorrect, aborting",a.ERROR,"⛈️"),d=d||{...c},void t(d)}d.mapping[""]||u('default mapping "" not provided.',a.WARNING,"☢️"),r&&"ENOENT"===r.code&&e&&p===l?(0,o.writeFile)(p,JSON.stringify(c),(e=>{e?u("config file NOT created",a.ERROR,"⁉️"):u("config file created",a.SUCCESS,"✨"),t(d)})):t(d)})))).then((()=>{e&&(0,o.watchFile)(p,f)})),g=e=>{u(`⎸ ↘️ : ${e.ssl?"HTTP/2 ":"HTTP 1.1"} ⎸ ☎️ : ${e.port.toString().padStart(5)} ⎸ ↗️ : ${e.dontUseHttp2Downstream?"HTTP 1.1":"HTTP/2 "} ⎹`)},f=async()=>{const e={...d};return await m(!1),isNaN(d.port)||d.port>65535||d.port<0?(d=e,void u("port number invalid. Not refreshing",a.ERROR,"☎️")):"object"!=typeof d.mapping?(d=e,void u("mapping should be an object. Aborting",a.ERROR,"⚡")):(d.replaceResponseBodyUrls&&!e.replaceResponseBodyUrls&&u("response body url replacement",a.INFO,"✔️"),!d.replaceResponseBodyUrls&&e.replaceResponseBodyUrls&&u("response body url NO replacement",a.INFO,"✖️"),u(`${Object.keys(d.mapping).length.toString().padStart(5)} loaded mapping rules`,a.SUCCESS,"↻"),void(d.port!==e.port||JSON.stringify(d.ssl)!==JSON.stringify(e.ssl)?(await new Promise((e=>h?h.close(e):e(void 0))),v()):d.dontUseHttp2Downstream!==e.dontUseHttp2Downstream&&g(d)))},$=e=>""==e?"":(0,i.normalize)(e).replace(/\\/g,"/"),b=e=>{const t=(0,i.resolve)("/",e.hostname,...e.pathname.replace(/[?#].*$/,"").replace(/^\/+/,"").split("/"));return{error:null,data:null,hasRun:!1,run:function(){return this.hasRun?Promise.resolve():new Promise((r=>(0,o.readFile)(t,((n,s)=>{if(this.hasRun=!0,!n||"EISDIR"!==n.code)return this.error=n,this.data=s,void r(void 0);(0,o.readdir)(t,((t,n)=>{this.error=t,this.data=n,t?r(void 0):Promise.all(n.map((t=>new Promise((r=>(0,o.lstat)((0,i.resolve)(e.pathname,t),((e,n)=>r([t,n,e])))))))).then((t=>{const n=t.filter((e=>!e[2]&&e[1].isDirectory())).concat(t.filter((e=>!e[2]&&e[1].isFile())));this.data=`${y(128194,"directory",e.href)}\n <p>Directory content of <i>${e.href.replace(/\//g,"&#x002F;")}</i></p>\n <ul class="list-group">\n <li class="list-group-item">&#x1F4C1;<a href="${e.pathname.endsWith("/")?"..":"."}">&lt;parent&gt;</a></li>\n ${n.filter((e=>!e[2])).map((t=>`<li class="list-group-item">&#x${(t[1].isDirectory()?128193:128196).toString(16)};<a href="${e.pathname.endsWith("/")?"":`${e.pathname.split("/").slice(-1)[0]}/`}${t[0]}">${t[0]}</a></li>`)).join("\n")}\n </li>\n </ul>\n </body></html>`,r(void 0)}))}))}))))},events:{},on:function(e,r){return this.events[e]=r,this.run().then((()=>{"response"===e&&this.events.response({Server:"local","Content-Type":t.endsWith(".svg")?"image/svg+xml":null},0),"data"===e&&this.data&&(this.events.data(this.data),this.events.end()),"error"===e&&this.error&&this.events.error(this.error)})),this},end:function(){return this},request:function(){return this}}},y=(e,t,r)=>`<!doctype html>\n<html lang="en">\n<head>\n<title>&#x${e.toString(16)}; local-traffic ${t} | ${r}</title>\n<link href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css" rel="stylesheet"/>\n<script src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"><\/script>\n<script src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"><\/script>\n</head>\n<body><div class="container"><h1>&#x${e.toString(16)}; local-traffic ${t}</h1>\n<br/>`,w=(e,t,r,n)=>`${y(128163,"error",e.message)}\n<p>An error happened while trying to proxy a remote exchange</p>\n<div class="alert alert-warning" role="alert">\n &#x24D8;&nbsp;This is not an error from the downstream service.\n</div>\n<div class="alert alert-danger" role="alert">\n<pre><code>${e.stack||`<i>${e.name} : ${e.message}</i>`}${e.errno?`<br/>(code : ${e.errno})`:""}</code></pre>\n</div>\nMore information about the request :\n<table class="table">\n <tbody>\n <tr>\n <td>phase</td>\n <td>${t}</td>\n </tr>\n <tr>\n <td>requested URL</td>\n <td>${r}</td>\n </tr>\n <tr>\n <td>downstream URL</td>\n <td>${n||"&lt;no-target-url&gt;"}</td>\n </tr>\n </tbody>\n</table>\n</div></body></html>`,R=(e,t,r)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":r.length}),t.end(r)},v=()=>{h=(d.ssl?e.createSecureServer.bind(null,{...d.ssl,allowHTTP1:!0}):t.createServer)((async(o,i)=>{if(!o.headers.host&&!o.headers[":authority"])return void R(400,i,Buffer.from(w(new Error("client must supply a 'host' header"),"proxy",new n.URL(`http${d.ssl?"s":""}://unknowndomain${o.url}`))));const a=o.headers[":authority"]||`${o.headers.host}${o.headers.host.match(/:[0-9]+$/)?"":80!==d.port||d.ssl?443===d.port&&d.ssl?"":`:${d.port}`:""}`,l=new n.URL(`http${d.ssl?"s":""}://${a}${o.url}`),c=l.href.substring(l.origin.length),[h,u]=Object.entries({...Object.assign({},...Object.entries(d.mapping).map((([e,t])=>({[e]:new n.URL($(t))}))))}).find((([e])=>c.match(RegExp(e))))||[];if(!u)return void R(502,i,Buffer.from(w(new Error(`No mapping found in config file ${p}`),"proxy",l)));const m=u.host.replace(RegExp(/\/+$/),""),g=`${u.href.substring("https://".length+u.host.length)}${$(c.replace(RegExp($(h)),""))}`.replace(/^\/*/,"/"),f=new n.URL(`${u.protocol}//${m}${g}`);let y=null,v=!d.dontUseHttp2Downstream;const S="file:"===u.protocol?b(f):v?await Promise.race([new Promise((t=>{const r=(0,e.connect)(f,{rejectUnauthorized:!1,protocol:u.protocol},((e,n)=>{v=v&&!!n.alpnProtocol,t(v?r:null)}));r.on("error",(e=>{y=v&&Buffer.from(w(e,"connection",l,f))}))})),new Promise((e=>setTimeout((()=>{v=!1,e(null)}),3e3)))]):null;!v&&y&&(y=null);const O={...[...Object.entries(o.headers)].filter((([e])=>!["host","connection"].includes(e.toLowerCase()))).reduce(((e,[t,r])=>(e[t]=(e[t]||"")+(Array.isArray(r)?r:[r]).map((e=>e.replace(l.hostname,m))).join(", "),e)),{}),origin:u.href,referer:f.toString(),":authority":m,":method":o.method,":path":g,":scheme":u.protocol.replace(":","")},E=S&&!y&&S.request(O,{endStream:d.ssl?!(o?.stream?.readableLength??1):!o.readableLength});E&&E.on("error",(e=>{const t=-505===e.errno;y=Buffer.from(w(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),l,f))}));const j={hostname:u.hostname,path:g,port:u.port,protocol:u.protocol,rejectUnauthorized:!1,method:o.method,headers:{...Object.assign({},...Object.entries(O).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase())).map((([e,t])=>({[e]:t})))),host:u.hostname}},U=!y&&!v&&"file:"!==u.protocol&&await new Promise((e=>{const n="https:"===u.protocol?(0,r.request)(j,e):(0,t.request)(j,e);n.on("error",(t=>{y=Buffer.from(w(t,"request",l,f)),e(null)})),o.on("data",(e=>n.write(e))),o.on("end",(()=>n.end()))}));if(y)return void R(502,i,y);d.ssl&&o.stream&&o.stream.readableLength&&E&&(o.stream.on("data",(e=>E.write(e))),o.stream.on("end",(()=>E.end()))),!d.ssl&&o.readableLength&&E&&(o.on("data",(e=>E.write(e))),o.on("end",(()=>E.end())));const{outboundResponseHeaders:x}=await new Promise((e=>E?E.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!E&&U?{outboundResponseHeaders:U.headers}:{outboundResponseHeaders:{}}))),N=x.location?new n.URL(x.location.startsWith("/")?`${u.href}${x.location.replace(/^\/+/,"")}`:x.location):null,L=N?N.href.substring(N.origin.length):null,P=l.origin,C=N?`${P}${L}`:null,B=E||U,H=y??await new Promise((e=>{let t=Buffer.alloc(0);B?(B.on("data",(e=>t=Buffer.concat([t,"string"==typeof e?Buffer.from(e):e]))),B.on("end",(()=>{e(t)}))):e(t)})).then((e=>d.replaceResponseBodyUrls&&e.length?(x["content-encoding"]||"").split(",").reduce((async(e,t)=>{const r=t.trim().toLowerCase(),n="gzip"===r||"x-gzip"===r?s.gunzip:"deflate"===r?s.inflate:"br"===r?s.brotliDecompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===n)return void R(502,i,Buffer.from(w(new Error(`${r} compression not supported by the proxy`),"stream",l,f)));const o=await e;return await new Promise((e=>n(o,((t,r)=>{if(t)return R(502,i,Buffer.from(w(t,"stream",l,f))),void e("");e(r)}))))}),Promise.resolve(e)).then((e=>e.length>1e6||/[^\x00-\x7F]/.test(e.toString())&&!(x["content-type"]??"").includes("text/html")?e:d.replaceResponseBodyUrls?Object.entries(d.mapping).reduce(((e,[t,r])=>""===t||t.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)?e.replace(new RegExp(r.replace(/^file:\/\//,"").replace(/[*+?^${}()|[\]\\]/g,"").replace(/^https/,"https?")+"/*","ig"),`https://${a}${t.replace(/\/+$/,"")}/`):e),e.toString()).split(`${a}/:`).join(`${a}:`):e.toString())).then((e=>(x["content-encoding"]||"").split(",").reduce(((e,t)=>{const r=t.trim().toLowerCase(),n="gzip"===r||"x-gzip"===r?s.gzip:"deflate"===r?s.deflate:"br"===r?s.brotliCompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===n)throw new Error(`${r} compression not supported by the proxy`);return e.then((e=>new Promise((t=>n(e,((e,r)=>{if(e)throw e;t(r)}))))))}),Promise.resolve(Buffer.from(e))))):e)),T={...Object.entries({...x,...d.replaceResponseBodyUrls?{"content-length":`${H.byteLength}`}:{}}).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase()&&"connection"!==e.toLowerCase())).reduce(((e,[t,r])=>{const n=m.split("").map(((e,t)=>m.substring(t).startsWith(".")&&m.substring(t))).filter((e=>e)),o=[m].concat(n).reduce(((e,t)=>(Array.isArray(e)?e:[e]).map((e=>"string"==typeof e?e.replace(`Domain=${t}`,`Domain=${l.hostname}`):e))),r);return e[t]=(e[t]||[]).concat(o),e}),{}),...C?{location:[C]}:{}};try{Object.entries(T).forEach((([e,t])=>t&&i.setHeader(e,t)))}catch(e){}i.writeHead(x[":status"]||U.statusCode||200,d.ssl?void 0:U.statusMessage||"Status read from http/2",T),H?i.end(H):i.end()})).addListener("error",(e=>{"EACCES"===e.code&&u("permission denied for this port",a.ERROR,"⛔"),"EADDRINUSE"===e.code&&u("port is already used. NOT started",a.ERROR,"☠️")})).addListener("listening",(()=>{g(d)})).listen(d.port)};m().then(v);