local-traffic 0.0.25 → 0.0.29

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.
@@ -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=i.resolve(process.env.HOME,".local-traffic.json"),p=i.resolve(__dirname,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=>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?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&&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?"":i.normalize(e).replace(/\\/g,"/"),b=e=>{const t=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=>o.readFile(t,((n,s)=>{if(this.hasRun=!0,!n||"EISDIR"!==n.code)return this.error=n,this.data=s,void r(void 0);o.readdir(t,((t,n)=>{this.error=t,this.data=n,t?r(void 0):Promise.all(n.map((t=>new Promise((r=>o.lstat(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,t){return this.events[e]=t,this.run().then((()=>{"response"===e&&this.events.response({Server:"local"},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/>`,R=(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>`,w=(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 w(400,i,Buffer.from(R(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 w(502,i,Buffer.from(R(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=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(R(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:!o.readableLength});E&&E.on("error",(e=>{const t=-505===e.errno;y=Buffer.from(R(e,"streaming"+(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?r.request(j,e):t.request(j,e);n.on("error",(t=>{y=Buffer.from(R(t,"request",l,f)),e(null)})),o.on("data",(e=>n.write(e))),o.on("end",(()=>n.end()))}));if(y)return void w(502,i,y);d.ssl&&o.stream&&o.stream.readableLength&&E&&(E.setEncoding("utf8"),o.stream.on("data",(e=>E.write(e))),o.stream.on("end",(()=>E.end()))),!d.ssl&&o.readableLength&&E&&(E.setEncoding("utf8"),o.on("data",(e=>E.write(e))),o.on("end",(()=>E.end())));const{outboundResponseHeaders:N}=await new Promise((e=>E?E.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!E&&U?{outboundResponseHeaders:U.headers}:{outboundResponseHeaders:{}}))),L=N.location?new n.URL(N.location.startsWith("/")?`${u.href}${N.location.replace(/^\/+/,"")}`:N.location):null,P=L?L.href.substring(L.origin.length):null,x=l.origin,B=L?`${x}${P}`:null,C=E||U,H=y||await new Promise((e=>{let t=Buffer.alloc(0);C?(C.on("data",(e=>t=Buffer.concat([t,"string"==typeof e?Buffer.from(e):e]))),C.on("end",(()=>{e(t)}))):e(t)})).then((e=>d.replaceResponseBodyUrls&&e.length?(N["content-encoding"]||"").split(",").reduce(((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 e.then((e=>new Promise((t=>n(e,((e,r)=>{if(e)return w(502,i,Buffer.from(R(e,"stream",l,f))),void t("");t(r)}))))));w(502,i,Buffer.from(R(new Error(`${r} compression not supported by the proxy`),"stream",l,f)))}),Promise.resolve(e)).then((e=>d.replaceResponseBodyUrls?Object.entries(d.mapping).reduce(((e,[t,r])=>t.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)?e.replace(new RegExp(r.replace(/^file:\/\//,"").replace(/[*+?^${}()|[\]\\]/g,""),"ig"),`https://${a}${t.replace(/\/+$/,"")}/`):e),e.toString()).split(`${a}/:`).join(`${a}:`):e.toString())).then((e=>(N["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({...N,...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}),{}),...B?{location:[B]}:{}};i.writeHead(N[":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"),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:N}=await new Promise((e=>E?E.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!E&&U?{outboundResponseHeaders:U.headers}:{outboundResponseHeaders:{}}))),L=N.location?new n.URL(N.location.startsWith("/")?`${u.href}${N.location.replace(/^\/+/,"")}`:N.location):null,P=L?L.href.substring(L.origin.length):null,x=l.origin,C=L?`${x}${P}`: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?(N["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=>d.replaceResponseBodyUrls?Object.entries(d.mapping).reduce(((e,[t,r])=>t.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)?e.replace(new RegExp(r.replace(/^file:\/\//,"").replace(/[*+?^${}()|[\]\\]/g,""),"ig"),`https://${a}${t.replace(/\/+$/,"")}/`):e),e.toString()).split(`${a}/:`).join(`${a}:`):e.toString())).then((e=>(N["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({...N,...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(N[":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);
package/index.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  OutgoingHttpHeaders,
10
10
  SecureClientSessionOptions,
11
11
  SecureServerOptions,
12
+ ClientHttp2Stream,
12
13
  } from "http2";
13
14
  import {
14
15
  request as httpRequest,
@@ -30,6 +31,7 @@ import {
30
31
  brotliDecompress,
31
32
  } from "zlib";
32
33
  import { resolve, normalize } from "path";
34
+ import type { Duplex } from "stream";
33
35
 
34
36
  enum LogLevel {
35
37
  ERROR = 124,
@@ -49,7 +51,7 @@ interface LocalConfiguration {
49
51
 
50
52
  const userHomeConfigFile = resolve(process.env.HOME, ".local-traffic.json");
51
53
  const filename = resolve(
52
- __dirname,
54
+ process.cwd(),
53
55
  process.argv.slice(-1)[0].endsWith(".json")
54
56
  ? process.argv.slice(-1)[0]
55
57
  : userHomeConfigFile
@@ -291,7 +293,8 @@ const fileRequest = (url: URL): ClientHttp2Session => {
291
293
  this.events[name] = action;
292
294
  this.run().then(() => {
293
295
  if (name === "response")
294
- this.events["response"]({ Server: "local" }, 0);
296
+ this.events["response"]({ Server: "local",
297
+ 'Content-Type': file.endsWith('.svg') ? 'image/svg+xml' : null}, 0);
295
298
  if (name === "data" && this.data) {
296
299
  this.events["data"](this.data);
297
300
  this.events["end"]();
@@ -515,7 +518,7 @@ const start = () => {
515
518
  !error &&
516
519
  outboundRequest.request(outboundHeaders, {
517
520
  endStream: config.ssl
518
- ? !(inboundRequest as Http2ServerRequest).stream.readableLength
521
+ ? !((inboundRequest as Http2ServerRequest)?.stream?.readableLength ?? true)
519
522
  : !(inboundRequest as IncomingMessage).readableLength,
520
523
  });
521
524
 
@@ -527,7 +530,7 @@ const start = () => {
527
530
  error = Buffer.from(
528
531
  errorPage(
529
532
  thrown,
530
- "streaming" +
533
+ "stream" +
531
534
  (httpVersionSupported
532
535
  ? " (error -505 usually means that the downstream service " +
533
536
  "does not support this http version)"
@@ -591,7 +594,6 @@ const start = () => {
591
594
  (inboundRequest as Http2ServerRequest).stream.readableLength &&
592
595
  outboundExchange
593
596
  ) {
594
- outboundExchange.setEncoding("utf8");
595
597
  (inboundRequest as Http2ServerRequest).stream.on("data", (chunk) =>
596
598
  outboundExchange.write(chunk)
597
599
  );
@@ -605,7 +607,6 @@ const start = () => {
605
607
  (inboundRequest as IncomingMessage).readableLength &&
606
608
  outboundExchange
607
609
  ) {
608
- outboundExchange.setEncoding("utf8");
609
610
  (inboundRequest as IncomingMessage).on("data", (chunk) =>
610
611
  outboundExchange.write(chunk)
611
612
  );
@@ -650,14 +651,14 @@ const start = () => {
650
651
  // phase : response body
651
652
  const payloadSource = outboundExchange || outboundHttp1Response;
652
653
  const payload =
653
- error ||
654
+ error ??
654
655
  (await new Promise((resolve) => {
655
656
  let partialBody = Buffer.alloc(0);
656
657
  if (!payloadSource) {
657
658
  resolve(partialBody);
658
659
  return;
659
660
  }
660
- (payloadSource as any).on(
661
+ (payloadSource as ClientHttp2Stream | Duplex).on(
661
662
  "data",
662
663
  (chunk: Buffer | string) =>
663
664
  (partialBody = Buffer.concat([
@@ -666,7 +667,7 @@ const start = () => {
666
667
  ? Buffer.from(chunk as string)
667
668
  : (chunk as Buffer),
668
669
  ]))
669
- );
670
+ );
670
671
  (payloadSource as any).on("end", () => {
671
672
  resolve(partialBody);
672
673
  });
@@ -676,7 +677,7 @@ const start = () => {
676
677
 
677
678
  return (outboundResponseHeaders["content-encoding"] || "")
678
679
  .split(",")
679
- .reduce((buffer: Promise<Buffer>, formatNotTrimed: string) => {
680
+ .reduce(async (buffer: Promise<Buffer>, formatNotTrimed: string) => {
680
681
  const format = formatNotTrimed.trim().toLowerCase();
681
682
  const method =
682
683
  format === "gzip" || format === "x-gzip"
@@ -711,22 +712,20 @@ const start = () => {
711
712
  return;
712
713
  }
713
714
 
714
- return buffer.then(
715
- (openedBuffer) =>
716
- new Promise((resolve) => {
717
- return method(openedBuffer, (err, data) => {
718
- if (err) {
719
- send(
720
- 502,
721
- inboundResponse,
722
- Buffer.from(errorPage(err, "stream", url, targetUrl))
723
- );
724
- resolve("");
725
- return;
726
- }
727
- resolve(data);
728
- });
729
- })
715
+ const openedBuffer = await buffer;
716
+ return await new Promise((resolve) =>
717
+ method(openedBuffer, (err_1, data_1) => {
718
+ if (err_1) {
719
+ send(
720
+ 502,
721
+ inboundResponse,
722
+ Buffer.from(errorPage(err_1, "stream", url, targetUrl))
723
+ );
724
+ resolve("");
725
+ return;
726
+ }
727
+ resolve(data_1);
728
+ })
730
729
  );
731
730
  }, Promise.resolve(payloadBuffer))
732
731
  .then((uncompressedBuffer: Buffer) =>
@@ -835,6 +834,13 @@ const start = () => {
835
834
  }, {}),
836
835
  ...(newTargetUrl ? { location: [newTargetUrl] } : {}),
837
836
  };
837
+ try {
838
+ Object.entries(responseHeaders).forEach(([headerName, headerValue]) =>
839
+ headerValue && inboundResponse.setHeader(headerName, headerValue as string)
840
+ );
841
+ } catch(e) {
842
+ // ERR_HTTP2_HEADERS_SENT
843
+ }
838
844
  inboundResponse.writeHead(
839
845
  outboundResponseHeaders[":status"] ||
840
846
  outboundHttp1Response.statusCode ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-traffic",
3
- "version": "0.0.25",
3
+ "version": "0.0.29",
4
4
  "main": "index.ts",
5
5
  "private": false,
6
6
  "keywords": [
@@ -21,9 +21,9 @@
21
21
  "build": "yarn typescript && yarn terser && yarn chmod"
22
22
  },
23
23
  "devDependencies": {
24
- "@types/node": "^16.3.1",
25
- "terser": "^5.7.1",
26
- "typescript": "^4.3.5"
24
+ "@types/node": "^17.0.12",
25
+ "terser": "^5.10.0",
26
+ "typescript": "^4.5.5"
27
27
  },
28
28
  "bin": {
29
29
  "local-traffic": "./dist/localTraffic.js"