local-traffic 0.0.42 → 0.0.43

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"),o=require("url"),n=require("fs"),s=require("zlib"),a=require("path");var i,l;!function(e){e[e.ERROR=124]="ERROR",e[e.INFO=93]="INFO",e[e.WARNING=172]="WARNING"}(i||(i={})),function(e){e.INBOUND="↘️ ",e.PORT="☎️ ",e.OUTBOUND="↗️ ",e.RULES="🔗",e.BODY_REPLACEMENT="✒️ ",e.WEBSOCKET="☄️ ",e.COLORED="✨",e.NO="⛔",e.ERROR_1="❌",e.ERROR_2="⛈️ ",e.ERROR_3="☢️ ",e.ERROR_4="⁉️ ",e.ERROR_5="⚡",e.ERROR_6="☠️ "}(l||(l={}));const p=(0,a.resolve)(process.env.HOME,".local-traffic.json"),c=(0,a.resolve)(process.cwd(),process.argv.slice(-1)[0].endsWith(".json")?process.argv.slice(-1)[0]:p),d={mapping:{},port:8080,replaceResponseBodyUrls:!1,dontUseHttp2Downstream:!1,simpleLogs:!1,websocket:!1};let h,m;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?"":""}`})(h?.simpleLogs)} ${h?.simpleLogs?e.replace(/⎸/g,"|").replace(/⎹/g,"|").replace(/\u001b\[[^m]*m/g,"").replace(new RegExp(l.INBOUND,"g"),"inbound:").replace(new RegExp(l.PORT,"g"),"port:").replace(new RegExp(l.OUTBOUND,"g"),"outbound:").replace(new RegExp(l.RULES,"g"),"rules:").replace(new RegExp(l.NO,"g"),"").replace(new RegExp(l.BODY_REPLACEMENT,"g"),"body replacement").replace(new RegExp(l.WEBSOCKET,"g"),"websocket").replace(/\|+/g,"|"):t?`[48;5;${t}m⎸ ${process.stdout.isTTY&&r||""} ${e.padEnd(36)} ⎹`:e}`)},g=e=>{u(`⎸${l.PORT} ${e.port.toString().padStart(5)} ⎸${l.INBOUND} ${e.ssl?"H/2 ":"H1.1"} ⎸${l.OUTBOUND} ${e.dontUseHttp2Downstream?"H1.1":"H/2 "}⎹⎸${l.RULES}${Object.keys(h.mapping).length.toString().padStart(3)}⎹⎸${h.replaceResponseBodyUrls?l.BODY_REPLACEMENT:l.NO}⎹⎸${h.websocket?l.WEBSOCKET:l.NO}⎹⎸${h.simpleLogs?l.NO:l.COLORED}⎹`)},R=async(e=!0)=>new Promise((t=>(0,n.readFile)(c,((r,o)=>{r&&!e&&u("config error. Using default value",i.ERROR,l.ERROR_1);try{h=Object.assign({},d,JSON.parse((o||"{}").toString()))}catch(e){return u("config syntax incorrect, aborting",i.ERROR,l.ERROR_2),h=h||{...d},void t(h)}h.mapping[""]||u('default mapping "" not provided.',i.WARNING,l.ERROR_3),r&&"ENOENT"===r.code&&e&&c===p?(0,n.writeFile)(c,JSON.stringify(d),(e=>{e?u("config file NOT created",i.ERROR,l.ERROR_4):u("config file created",i.INFO,l.COLORED),t(h)})):t(h)})))).then((()=>{e&&(0,n.watchFile)(c,f)})),f=async()=>{const e={...h};return await R(!1),isNaN(h.port)||h.port>65535||h.port<0?(h=e,void u("port number invalid. Not refreshing",i.ERROR,l.PORT)):"object"!=typeof h.mapping?(h=e,void u("mapping should be an object. Aborting",i.ERROR,l.ERROR_5)):(h.replaceResponseBodyUrls!==e.replaceResponseBodyUrls&&u(`response body url ${h.replaceResponseBodyUrls?"":"NO "}replacement`,i.INFO,l.BODY_REPLACEMENT),h.dontUseHttp2Downstream!==e.dontUseHttp2Downstream&&u(`http/2 ${h.dontUseHttp2Downstream?"de":""}activated downstream`,i.INFO,l.OUTBOUND),h.websocket!==e.websocket&&u(`websocket ${h.websocket?"":"de"}activated`,i.INFO,l.WEBSOCKET),h.simpleLogs!==e.simpleLogs&&u("simple logs "+(h.simpleLogs?"on":"off"),i.INFO,l.COLORED),Object.keys(h.mapping).join("\n")!==Object.keys(e.mapping).join("\n")&&u(`${Object.keys(h.mapping).length.toString().padStart(5)} loaded mapping rules`,i.INFO,l.RULES),h.port!==e.port&&u(`port changed from ${e.port} to ${h.port}`,i.INFO,l.PORT),h.ssl&&!e.ssl&&u("ssl configuration added",i.INFO,l.INBOUND),!h.ssl&&e.ssl&&u("ssl configuration removed",i.INFO,l.INBOUND),void(h.port!==e.port||JSON.stringify(h.ssl)!==JSON.stringify(e.ssl)?(await new Promise((e=>m?m.close(e):e(void 0))),v()):g(h)))},$=e=>""==e?"":(0,a.normalize)(e).replace(/\\/g,"/"),O=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=`${w(128194,"directory",e.href)}<p>Directory content of <i>${e.href.replace(/\//g,"&#x002F;")}</i></p><ul class="list-group"><li class="list-group-item">&#x1F4C1;<a href="${e.pathname.endsWith("/")?"..":"."}">&lt;parent&gt;</a></li>${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")}</li></ul></body></html>`,r(void 0)}))}))}))))},events:{},on:function(e,r){return this.events[e]=r,this.run().then((()=>{"response"===e&&this.events.response(t.endsWith(".svg")?{Server:"local","Content-Type":"image/svg+xml"}:{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}}},w=(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/>`,E=(e,t,r,o)=>`${w(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>`,b=(e,t,r)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":r.length}),t.end(r)},y=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!==h.port||h.ssl?443===h.port&&h.ssl?"":`:${h.port}`:""}`,n=new o.URL(`http${h.ssl?"s":""}://${r}${e.url}`),s=n.href.substring(n.origin.length),[a,i]=Object.entries({...Object.assign({},...Object.entries(h.mapping).map((([e,t])=>({[e]:new o.URL($(t))}))))}).find((([e])=>s.match(RegExp(e.replace(/^\//,"^/")))))||[];return{proxyHostname:t,proxyHostnameAndPort:r,url:n,path:s,key:a,target:i}},v=()=>{m=(h.ssl?e.createSecureServer.bind(null,{...h.ssl,allowHTTP1:!0}):t.createServer)((async(n,a)=>{if(!n.headers.host&&!n.headers[":authority"])return void b(400,a,Buffer.from(E(new Error("client must supply a 'host' header"),"proxy",new o.URL(`http${h.ssl?"s":""}://unknowndomain${n.url}`))));const{proxyHostname:i,proxyHostnameAndPort:l,url:p,path:d,key:m,target:u}=y(n);if(!u)return void b(502,a,Buffer.from(E(new Error(`No mapping found in config file ${c}`),"proxy",p)));const g=u.host.replace(RegExp(/\/+$/),""),R=`${u.href.substring("https://".length+u.host.length)}${$(d.replace(RegExp($(m)),""))}`.replace(/^\/*/,"/"),f=new o.URL(`${u.protocol}//${g}${R}`);let w=null,v=!h.dontUseHttp2Downstream;const N="file:"===u.protocol?O(f):v?await Promise.race([new Promise((t=>{const r=(0,e.connect)(f,{rejectUnauthorized:!1,protocol:u.protocol},((e,o)=>{v=v&&!!o.alpnProtocol,t(v?r:null)}));r.on("error",(e=>{w=v&&Buffer.from(E(e,"connection",p,f))}))})),new Promise((e=>setTimeout((()=>{v=!1,e(null)}),3e3)))]):null;w instanceof Buffer||(w=null);const U={...[...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(p.hostname,g))).join(", "),e)),{}),origin:u.href,referer:f.toString(),":authority":g,":method":n.method,":path":R,":scheme":u.protocol.replace(":","")},S=N&&!w&&N.request(U,{endStream:h.ssl?!(n?.stream?.readableLength??1):!n.readableLength});S&&S.on("error",(e=>{const t=-505===e.errno;w=Buffer.from(E(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),p,f))}));const B={hostname:u.hostname,path:R,port:u.port,protocol:u.protocol,rejectUnauthorized:!1,method:n.method,headers:{...Object.assign({},...Object.entries(U).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase())).map((([e,t])=>({[e]:t})))),host:u.hostname}},L=!w&&!v&&"file:"!==u.protocol&&await new Promise((e=>{const o="https:"===u.protocol?(0,r.request)(B,e):(0,t.request)(B,e);o.on("error",(t=>{w=Buffer.from(E(t,"request",p,f)),e(null)})),n.on("data",(e=>o.write(e))),n.on("end",(()=>o.end()))}));if(w)return void b(502,a,w);w=null,h.ssl&&n.stream&&n.stream.readableLength&&S&&(n.stream.on("data",(e=>S.write(e))),n.stream.on("end",(()=>S.end()))),!h.ssl&&n.readableLength&&S&&(n.on("data",(e=>S.write(e))),n.on("end",(()=>S.end())));const{outboundResponseHeaders:j}=await new Promise((e=>S?S.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!S&&L?{outboundResponseHeaders:L.headers}:{outboundResponseHeaders:{}}))),x=j.location?new o.URL(j.location.startsWith("/")?`${u.href}${j.location.replace(/^\/+/,"")}`:j.location):null,D=x?x.href.substring(x.origin.length):null,T=p.origin,P=x?`${T}${D}`:null,C=S||L,H=w??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=>h.replaceResponseBodyUrls&&e.length?(j["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 b(502,a,Buffer.from(E(new Error(`${r} compression not supported by the proxy`),"stream",p,f)));const n=await e;return await new Promise((e=>o(n,((t,r)=>{if(t)return b(502,a,Buffer.from(E(t,"stream",p,f))),void e("");e(r)}))))}),Promise.resolve(e)).then((e=>{const t=e.length>1e7,r=["text/html","application/javascript","application/json"].some((e=>(j["content-type"]??"").includes(e)));return!t&&(r||!/[^\x00-\x7F]/.test(e.toString()))?h.replaceResponseBodyUrls?Object.entries(h.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${h.ssl?"s":""}%3A&hostname=${i}&port=${h.port}&pathname=${encodeURIComponent(m.replace(/\/+$/,""))}`):e.toString():e})).then((e=>(j["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)),I={...Object.entries({...j,...h.replaceResponseBodyUrls?{"content-length":`${H.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=${p.hostname}`):e))),r);return e[t]=(e[t]||[]).concat(n),e}),{}),...P?{location:[P]}:{}};try{Object.entries(I).forEach((([e,t])=>t&&a.setHeader(e,t)))}catch(e){}a.writeHead(j[":status"]||L.statusCode||200,h.ssl?void 0:L.statusMessage||"Status read from http/2",I),H?a.end(H):a.end()})).addListener("error",(e=>{"EACCES"===e.code&&u("permission denied for this port",i.ERROR,l.NO),"EADDRINUSE"===e.code&&u("port is already used. NOT started",i.ERROR,l.ERROR_6)})).addListener("listening",(()=>{g(h)})).on("upgrade",((e,n)=>{if(!h.websocket)return void n.end("HTTP/1.1 503 Service Unavailable\r\n\r\n");const{key:s,target:a}=y(e),p=new o.URL(`${a.protocol}//${a.host}${e.url.endsWith("/_next/webpack-hmr")?e.url:e.url.replace(new RegExp(`^${s}`,"g"),"").replace(/^\/*/,"/")}`),c={hostname:p.hostname,path:p.pathname,port:p.port,protocol:p.protocol,rejectUnauthorized:!1,method:e.method,headers:e.headers,host:p.hostname},d="https:"===p.protocol?(0,r.request)(c):(0,t.request)(c);d.end(),d.on("error",(e=>{u("websocket request has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,l.WEBSOCKET)})),d.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,l.WEBSOCKET)})),n.on("error",(e=>{u("upstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,l.WEBSOCKET)}))}))})).listen(h.port)};R().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,l;!function(e){e[e.ERROR=124]="ERROR",e[e.INFO=93]="INFO",e[e.WARNING=172]="WARNING"}(i||(i={})),function(e){e.INBOUND="↘️ ",e.PORT="☎️ ",e.OUTBOUND="↗️ ",e.RULES="🔗",e.BODY_REPLACEMENT="✒️ ",e.WEBSOCKET="☄️ ",e.COLORED="✨",e.NO="⛔",e.ERROR_1="❌",e.ERROR_2="⛈️ ",e.ERROR_3="☢️ ",e.ERROR_4="⁉️ ",e.ERROR_5="⚡",e.ERROR_6="☠️ "}(l||(l={}));const p=(0,a.resolve)(process.env.HOME,".local-traffic.json"),c=(0,a.resolve)(process.cwd(),process.argv.slice(-1)[0].endsWith(".json")?process.argv.slice(-1)[0]:p),d={mapping:{},port:8080,replaceResponseBodyUrls:!1,dontUseHttp2Downstream:!1,simpleLogs:!1,websocket:!1};let h,m;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?"":""}`})(h?.simpleLogs)} ${h?.simpleLogs?e.replace(/⎸/g,"|").replace(/⎹/g,"|").replace(/\u001b\[[^m]*m/g,"").replace(new RegExp(l.INBOUND,"g"),"inbound:").replace(new RegExp(l.PORT,"g"),"port:").replace(new RegExp(l.OUTBOUND,"g"),"outbound:").replace(new RegExp(l.RULES,"g"),"rules:").replace(new RegExp(l.NO,"g"),"").replace(new RegExp(l.BODY_REPLACEMENT,"g"),"body replacement").replace(new RegExp(l.WEBSOCKET,"g"),"websocket").replace(/\|+/g,"|"):t?`[48;5;${t}m⎸ ${process.stdout.isTTY&&r||""} ${e.padEnd(36)} ⎹`:e}`)},g=e=>{u(`⎸${l.PORT} ${e.port.toString().padStart(5)} ⎸${l.INBOUND} ${e.ssl?"H/2 ":"H1.1"} ⎸${l.OUTBOUND} ${e.dontUseHttp2Downstream?"H1.1":"H/2 "}⎹⎸${l.RULES}${Object.keys(h.mapping).length.toString().padStart(3)}⎹⎸${h.replaceResponseBodyUrls?l.BODY_REPLACEMENT:l.NO}⎹⎸${h.websocket?l.WEBSOCKET:l.NO}⎹⎸${h.simpleLogs?l.NO:l.COLORED}⎹`)},R=async(e=!0)=>new Promise((t=>(0,n.readFile)(c,((r,o)=>{r&&!e&&u("config error. Using default value",i.ERROR,l.ERROR_1);try{h=Object.assign({},d,JSON.parse((o||"{}").toString()))}catch(e){return u("config syntax incorrect, aborting",i.ERROR,l.ERROR_2),h=h||{...d},void t(h)}h.mapping[""]||u('default mapping "" not provided.',i.WARNING,l.ERROR_3),r&&"ENOENT"===r.code&&e&&c===p?(0,n.writeFile)(c,JSON.stringify(d),(e=>{e?u("config file NOT created",i.ERROR,l.ERROR_4):u("config file created",i.INFO,l.COLORED),t(h)})):t(h)})))).then((()=>{e&&(0,n.watchFile)(c,f)})),f=async()=>{const e={...h};return await R(!1),isNaN(h.port)||h.port>65535||h.port<0?(h=e,void u("port number invalid. Not refreshing",i.ERROR,l.PORT)):"object"!=typeof h.mapping?(h=e,void u("mapping should be an object. Aborting",i.ERROR,l.ERROR_5)):(h.replaceResponseBodyUrls!==e.replaceResponseBodyUrls&&u(`response body url ${h.replaceResponseBodyUrls?"":"NO "}replacement`,i.INFO,l.BODY_REPLACEMENT),h.dontUseHttp2Downstream!==e.dontUseHttp2Downstream&&u(`http/2 ${h.dontUseHttp2Downstream?"de":""}activated downstream`,i.INFO,l.OUTBOUND),h.websocket!==e.websocket&&u(`websocket ${h.websocket?"":"de"}activated`,i.INFO,l.WEBSOCKET),h.simpleLogs!==e.simpleLogs&&u("simple logs "+(h.simpleLogs?"on":"off"),i.INFO,l.COLORED),Object.keys(h.mapping).join("\n")!==Object.keys(e.mapping).join("\n")&&u(`${Object.keys(h.mapping).length.toString().padStart(5)} loaded mapping rules`,i.INFO,l.RULES),h.port!==e.port&&u(`port changed from ${e.port} to ${h.port}`,i.INFO,l.PORT),h.ssl&&!e.ssl&&u("ssl configuration added",i.INFO,l.INBOUND),!h.ssl&&e.ssl&&u("ssl configuration removed",i.INFO,l.INBOUND),void(h.port!==e.port||JSON.stringify(h.ssl)!==JSON.stringify(e.ssl)?(await new Promise((e=>m?m.close(e):e(void 0))),v()):g(h)))},$=e=>""==e?"":(0,a.normalize)(e).replace(/\\/g,"/"),O=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=`${w(128194,"directory",e.href)}<p>Directory content of <i>${e.href.replace(/\//g,"&#x002F;")}</i></p><ul class="list-group"><li class="list-group-item">&#x1F4C1;<a href="${e.pathname.endsWith("/")?"..":"."}">&lt;parent&gt;</a></li>${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")}</li></ul></body></html>`,r(void 0)}))}))}))))},events:{},on:function(e,r){return this.events[e]=r,this.run().then((()=>{"response"===e&&this.events.response(t.endsWith(".svg")?{Server:"local","Content-Type":"image/svg+xml"}:{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}}},w=(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/>`,E=(e,t,r,o)=>`${w(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>`,b=(e,t,r)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":r.length}),t.end(r)},y=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!==h.port||h.ssl?443===h.port&&h.ssl?"":`:${h.port}`:""}`,n=new o.URL(`http${h.ssl?"s":""}://${r}${e.url}`),s=n.href.substring(n.origin.length),[a,i]=Object.entries({...Object.assign({},...Object.entries(h.mapping).map((([e,t])=>({[e]:new o.URL($(t))}))))}).find((([e])=>s.match(RegExp(e.replace(/^\//,"^/")))))||[];return{proxyHostname:t,proxyHostnameAndPort:r,url:n,path:s,key:a,target:i}},v=()=>{m=(h.ssl?e.createSecureServer.bind(null,{...h.ssl,allowHTTP1:!0}):t.createServer)((async(n,a)=>{if(!n.headers.host&&!n.headers[":authority"])return void b(400,a,Buffer.from(E(new Error("client must supply a 'host' header"),"proxy",new o.URL(`http${h.ssl?"s":""}://unknowndomain${n.url}`))));const{proxyHostname:i,proxyHostnameAndPort:l,url:p,path:d,key:m,target:u}=y(n);if(!u)return void b(502,a,Buffer.from(E(new Error(`No mapping found in config file ${c}`),"proxy",p)));const g=u.host.replace(RegExp(/\/+$/),""),R=`${u.href.substring("https://".length+u.host.length)}${$(d.replace(RegExp($(m)),""))}`.replace(/^\/*/,"/"),f=new o.URL(`${u.protocol}//${g}${R}`);let w=null,v=!h.dontUseHttp2Downstream;const N="file:"===u.protocol?O(f):v?await Promise.race([new Promise((t=>{const r=(0,e.connect)(f,{rejectUnauthorized:!1,protocol:u.protocol},((e,o)=>{v=v&&!!o.alpnProtocol,t(v?r:null)}));r.on("error",(e=>{w=v&&Buffer.from(E(e,"connection",p,f))}))})),new Promise((e=>setTimeout((()=>{v=!1,e(null)}),3e3)))]):null;w instanceof Buffer||(w=null);const U={...[...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(p.hostname,g))).join(", "),e)),{}),origin:u.href,referer:f.toString(),":authority":g,":method":n.method,":path":R,":scheme":u.protocol.replace(":","")},S=N&&!w&&N.request(U,{endStream:h.ssl?!(n?.stream?.readableLength??1):!n.readableLength});S?.on("error",(e=>{const t=-505===e.errno;w=Buffer.from(E(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),p,f))}));const B={hostname:u.hostname,path:R,port:u.port,protocol:u.protocol,rejectUnauthorized:!1,method:n.method,headers:{...Object.assign({},...Object.entries(U).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase())).map((([e,t])=>({[e]:t})))),host:u.hostname}},L=!w&&!v&&"file:"!==u.protocol&&await new Promise((e=>{const o="https:"===u.protocol?(0,r.request)(B,e):(0,t.request)(B,e);o.on("error",(t=>{w=Buffer.from(E(t,"request",p,f)),e(null)})),n.on("data",(e=>o.write(e))),n.on("end",(()=>o.end()))}));if(w)return void b(502,a,w);w=null,h.ssl&&n.stream&&n.stream.readableLength&&S&&(n.stream.on("data",(e=>S.write(e))),n.stream.on("end",(()=>S.end()))),!h.ssl&&n.readableLength&&S&&(n.on("data",(e=>S.write(e))),n.on("end",(()=>S.end())));const{outboundResponseHeaders:j}=await new Promise((e=>S?S.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!S&&L?{outboundResponseHeaders:L.headers}:{outboundResponseHeaders:{}}))),x=j.location?new o.URL(j.location.startsWith("/")?`${u.href}${j.location.replace(/^\/+/,"")}`:j.location):null,D=x?x.href.substring(x.origin.length):null,T=p.origin,P=x?`${T}${D}`:null,C=S||L,H=w??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=>h.replaceResponseBodyUrls&&e.length?(j["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 b(502,a,Buffer.from(E(new Error(`${r} compression not supported by the proxy`),"stream",p,f)));const n=await e;return await new Promise((e=>o(n,((t,r)=>{if(t)return b(502,a,Buffer.from(E(t,"stream",p,f))),void e(Buffer.from(""));e(r)}))))}),Promise.resolve(e)).then((e=>{const t=e.length>1e7,r=["text/html","application/javascript","application/json"].some((e=>(j["content-type"]??"").includes(e)));return!t&&(r||!/[^\x00-\x7F]/.test(e.toString()))?h.replaceResponseBodyUrls?Object.entries(h.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${h.ssl?"s":""}%3A&hostname=${i}&port=${h.port}&pathname=${encodeURIComponent(m.replace(/\/+$/,""))}`):e.toString():e})).then((e=>(j["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)),I={...Object.entries({...j,...h.replaceResponseBodyUrls?{"content-length":`${H.byteLength}`}:{}}).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase()&&"connection"!==e.toLowerCase()&&"keep-alive"!==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=${p.hostname}`):e))),r);return e[t]=(e[t]||[]).concat(n),e}),{}),...P?{location:[P]}:{}};try{Object.entries(I).forEach((([e,t])=>t&&a.setHeader(e,t)))}catch(e){}a.writeHead(j[":status"]||L.statusCode||200,h.ssl?void 0:L.statusMessage||"Status read from http/2",I),H?a.end(H):a.end()})).addListener("error",(e=>{"EACCES"===e.code&&u("permission denied for this port",i.ERROR,l.NO),"EADDRINUSE"===e.code&&u("port is already used. NOT started",i.ERROR,l.ERROR_6)})).addListener("listening",(()=>{g(h)})).on("upgrade",((e,n)=>{if(!h.websocket)return void n.end("HTTP/1.1 503 Service Unavailable\r\n\r\n");const{key:s,target:a}=y(e),p=new o.URL(`${a.protocol}//${a.host}${e.url.endsWith("/_next/webpack-hmr")?e.url:e.url.replace(new RegExp(`^${s}`,"g"),"").replace(/^\/*/,"/")}`),c={hostname:p.hostname,path:p.pathname,port:p.port,protocol:p.protocol,rejectUnauthorized:!1,method:e.method,headers:e.headers,host:p.hostname},d="https:"===p.protocol?(0,r.request)(c):(0,t.request)(c);d.end(),d.on("error",(e=>{u("websocket request has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,l.WEBSOCKET)})),d.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,l.WEBSOCKET)})),n.on("error",(e=>{u("upstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,l.WEBSOCKET)}))}))})).listen(h.port)};R().then(v);
package/index.ts CHANGED
@@ -10,11 +10,13 @@ import {
10
10
  SecureClientSessionOptions,
11
11
  SecureServerOptions,
12
12
  ClientHttp2Stream,
13
+ IncomingHttpStatusHeader,
13
14
  } from "http2";
14
15
  import {
15
16
  request as httpRequest,
16
17
  IncomingMessage,
17
18
  ClientRequest,
19
+ IncomingHttpHeaders,
18
20
  createServer,
19
21
  ServerResponse,
20
22
  Server,
@@ -301,7 +303,7 @@ const fileRequest = (url: URL): ClientHttp2Session => {
301
303
  .replace(/^\/+/, "")
302
304
  .split("/"),
303
305
  );
304
- return {
306
+ return ({
305
307
  error: null as Error,
306
308
  data: null as string | Buffer,
307
309
  hasRun: false,
@@ -399,7 +401,7 @@ const fileRequest = (url: URL): ClientHttp2Session => {
399
401
  request: function () {
400
402
  return this;
401
403
  },
402
- } as unknown as ClientHttp2Session;
404
+ } as unknown) as ClientHttp2Session;
403
405
  };
404
406
 
405
407
  const header = (
@@ -511,306 +513,413 @@ const determineMapping = (
511
513
  };
512
514
 
513
515
  const start = () => {
514
- server = (
515
- (config.ssl
516
- ? createSecureServer.bind(null, { ...config.ssl, allowHTTP1: true })
517
- : createServer)(
518
- async (
519
- inboundRequest: Http2ServerRequest | IncomingMessage,
520
- inboundResponse: Http2ServerResponse | ServerResponse,
521
- ) => {
522
- // phase: mapping
523
- if (
524
- !inboundRequest.headers.host &&
525
- !inboundRequest.headers[":authority"]
526
- ) {
527
- send(
528
- 400,
529
- inboundResponse,
530
- Buffer.from(
531
- errorPage(
532
- new Error(`client must supply a 'host' header`),
533
- "proxy",
534
- new URL(
535
- `http${config.ssl ? "s" : ""}://unknowndomain${
536
- inboundRequest.url
537
- }`,
538
- ),
539
- ),
540
- ),
541
- );
542
- return;
543
- }
544
- const { proxyHostname, proxyHostnameAndPort, url, path, key, target } =
545
- determineMapping(inboundRequest);
546
- if (!target) {
547
- send(
548
- 502,
549
- inboundResponse,
550
- Buffer.from(
551
- errorPage(
552
- new Error(`No mapping found in config file ${filename}`),
553
- "proxy",
554
- url,
516
+ server = ((config.ssl
517
+ ? createSecureServer.bind(null, { ...config.ssl, allowHTTP1: true })
518
+ : createServer)(
519
+ async (
520
+ inboundRequest: Http2ServerRequest | IncomingMessage,
521
+ inboundResponse: Http2ServerResponse | ServerResponse,
522
+ ) => {
523
+ // phase: mapping
524
+ if (
525
+ !inboundRequest.headers.host &&
526
+ !inboundRequest.headers[":authority"]
527
+ ) {
528
+ send(
529
+ 400,
530
+ inboundResponse,
531
+ Buffer.from(
532
+ errorPage(
533
+ new Error(`client must supply a 'host' header`),
534
+ "proxy",
535
+ new URL(
536
+ `http${config.ssl ? "s" : ""}://unknowndomain${
537
+ inboundRequest.url
538
+ }`,
555
539
  ),
556
540
  ),
557
- );
558
- return;
559
- }
560
- const targetHost = target.host.replace(RegExp(/\/+$/), "");
561
- const targetPrefix = target.href.substring(
562
- "https://".length + target.host.length,
541
+ ),
563
542
  );
564
- const fullPath = `${targetPrefix}${unixNorm(
565
- path.replace(RegExp(unixNorm(key)), ""),
566
- )}`.replace(/^\/*/, "/");
567
- const targetUrl = new URL(
568
- `${target.protocol}//${targetHost}${fullPath}`,
543
+ return;
544
+ }
545
+ const {
546
+ proxyHostname,
547
+ proxyHostnameAndPort,
548
+ url,
549
+ path,
550
+ key,
551
+ target,
552
+ } = determineMapping(inboundRequest);
553
+ if (!target) {
554
+ send(
555
+ 502,
556
+ inboundResponse,
557
+ Buffer.from(
558
+ errorPage(
559
+ new Error(`No mapping found in config file ${filename}`),
560
+ "proxy",
561
+ url,
562
+ ),
563
+ ),
569
564
  );
565
+ return;
566
+ }
567
+ const targetHost = target.host.replace(RegExp(/\/+$/), "");
568
+ const targetPrefix = target.href.substring(
569
+ "https://".length + target.host.length,
570
+ );
571
+ const fullPath = `${targetPrefix}${unixNorm(
572
+ path.replace(RegExp(unixNorm(key)), ""),
573
+ )}`.replace(/^\/*/, "/");
574
+ const targetUrl = new URL(`${target.protocol}//${targetHost}${fullPath}`);
570
575
 
571
- // phase: connection
572
- let error: Buffer = null;
573
- let http2IsSupported = !config.dontUseHttp2Downstream;
574
- const outboundRequest: ClientHttp2Session =
575
- target.protocol === "file:"
576
- ? fileRequest(targetUrl)
577
- : !http2IsSupported
578
- ? null
579
- : await Promise.race([
580
- new Promise<ClientHttp2Session>(resolve => {
581
- const result = connect(
582
- targetUrl,
583
- {
584
- rejectUnauthorized: false,
585
- protocol: target.protocol,
586
- } as SecureClientSessionOptions,
587
- (_, socketPath) => {
588
- http2IsSupported =
589
- http2IsSupported && !!(socketPath as any).alpnProtocol;
590
- resolve(!http2IsSupported ? null : result);
591
- },
592
- );
593
- (result as unknown as Http2Session).on(
594
- "error",
595
- (thrown: Error) => {
596
- error =
597
- http2IsSupported &&
598
- Buffer.from(
599
- errorPage(thrown, "connection", url, targetUrl),
600
- );
601
- },
602
- );
603
- }),
604
- new Promise<ClientHttp2Session>(resolve =>
605
- setTimeout(() => {
606
- http2IsSupported = false;
607
- resolve(null);
608
- }, 3000),
609
- ),
610
- ]);
611
- if (!(error instanceof Buffer)) error = null;
576
+ // phase: connection
577
+ let error: Buffer = null;
578
+ let http2IsSupported = !config.dontUseHttp2Downstream;
579
+ const outboundRequest: ClientHttp2Session =
580
+ target.protocol === "file:"
581
+ ? fileRequest(targetUrl)
582
+ : !http2IsSupported
583
+ ? null
584
+ : await Promise.race([
585
+ new Promise<ClientHttp2Session>(resolve => {
586
+ const result = connect(
587
+ targetUrl,
588
+ {
589
+ rejectUnauthorized: false,
590
+ protocol: target.protocol,
591
+ } as SecureClientSessionOptions,
592
+ (_, socketPath) => {
593
+ http2IsSupported =
594
+ http2IsSupported && !!(socketPath as any).alpnProtocol;
595
+ resolve(!http2IsSupported ? null : result);
596
+ },
597
+ );
598
+ ((result as unknown) as Http2Session).on(
599
+ "error",
600
+ (thrown: Error) => {
601
+ error =
602
+ http2IsSupported &&
603
+ Buffer.from(
604
+ errorPage(thrown, "connection", url, targetUrl),
605
+ );
606
+ },
607
+ );
608
+ }),
609
+ new Promise<ClientHttp2Session>(resolve =>
610
+ setTimeout(() => {
611
+ http2IsSupported = false;
612
+ resolve(null);
613
+ }, 3000),
614
+ ),
615
+ ]);
616
+ if (!(error instanceof Buffer)) error = null;
612
617
 
613
- const outboundHeaders: OutgoingHttpHeaders = {
614
- ...[...Object.entries(inboundRequest.headers)]
615
- // host and connection are forbidden in http/2
616
- .filter(
617
- ([key]) => !["host", "connection"].includes(key.toLowerCase()),
618
- )
619
- .reduce((acc: any, [key, value]) => {
620
- acc[key] =
621
- (acc[key] || "") +
622
- (!Array.isArray(value) ? [value] : value)
623
- .map(oneValue => oneValue.replace(url.hostname, targetHost))
624
- .join(", ");
625
- return acc;
626
- }, {}),
627
- origin: target.href,
628
- referer: targetUrl.toString(),
629
- ":authority": targetHost,
630
- ":method": inboundRequest.method,
631
- ":path": fullPath,
632
- ":scheme": target.protocol.replace(":", ""),
633
- };
618
+ const outboundHeaders: OutgoingHttpHeaders = {
619
+ ...[...Object.entries(inboundRequest.headers)]
620
+ // host and connection are forbidden in http/2
621
+ .filter(
622
+ ([key]) => !["host", "connection"].includes(key.toLowerCase()),
623
+ )
624
+ .reduce((acc: any, [key, value]) => {
625
+ acc[key] =
626
+ (acc[key] || "") +
627
+ (!Array.isArray(value) ? [value] : value)
628
+ .map(oneValue => oneValue.replace(url.hostname, targetHost))
629
+ .join(", ");
630
+ return acc;
631
+ }, {}),
632
+ origin: target.href,
633
+ referer: targetUrl.toString(),
634
+ ":authority": targetHost,
635
+ ":method": inboundRequest.method,
636
+ ":path": fullPath,
637
+ ":scheme": target.protocol.replace(":", ""),
638
+ };
634
639
 
635
- const outboundExchange =
636
- outboundRequest &&
637
- !error &&
638
- outboundRequest.request(outboundHeaders, {
639
- endStream: config.ssl
640
- ? !(
641
- (inboundRequest as Http2ServerRequest)?.stream
642
- ?.readableLength ?? true
643
- )
644
- : !(inboundRequest as IncomingMessage).readableLength,
640
+ const outboundExchange =
641
+ outboundRequest &&
642
+ !error &&
643
+ outboundRequest.request(outboundHeaders, {
644
+ endStream: config.ssl
645
+ ? !(
646
+ (inboundRequest as Http2ServerRequest)?.stream
647
+ ?.readableLength ?? true
648
+ )
649
+ : !(inboundRequest as IncomingMessage).readableLength,
650
+ });
651
+
652
+ outboundExchange?.on("error", (thrown: Error) => {
653
+ const httpVersionSupported = (thrown as ErrorWithErrno).errno === -505;
654
+ error = Buffer.from(
655
+ errorPage(
656
+ thrown,
657
+ "stream" +
658
+ (httpVersionSupported
659
+ ? " (error -505 usually means that the downstream service " +
660
+ "does not support this http version)"
661
+ : ""),
662
+ url,
663
+ targetUrl,
664
+ ),
665
+ );
666
+ });
667
+
668
+ const http1RequestOptions: RequestOptions = {
669
+ hostname: target.hostname,
670
+ path: fullPath,
671
+ port: target.port,
672
+ protocol: target.protocol,
673
+ rejectUnauthorized: false,
674
+ method: inboundRequest.method,
675
+ headers: {
676
+ ...Object.assign(
677
+ {},
678
+ ...Object.entries(outboundHeaders)
679
+ .filter(
680
+ ([h]) =>
681
+ !h.startsWith(":") && h.toLowerCase() !== "transfer-encoding",
682
+ )
683
+ .map(([key, value]) => ({ [key]: value })),
684
+ ),
685
+ host: target.hostname,
686
+ },
687
+ };
688
+ const outboundHttp1Response: IncomingMessage =
689
+ !error &&
690
+ !http2IsSupported &&
691
+ target.protocol !== "file:" &&
692
+ (await new Promise(resolve => {
693
+ const outboundHttp1Request: ClientRequest =
694
+ target.protocol === "https:"
695
+ ? httpsRequest(http1RequestOptions, resolve)
696
+ : httpRequest(http1RequestOptions, resolve);
697
+
698
+ outboundHttp1Request.on("error", thrown => {
699
+ error = Buffer.from(errorPage(thrown, "request", url, targetUrl));
700
+ resolve(null as IncomingMessage);
645
701
  });
702
+ inboundRequest.on("data", chunk => outboundHttp1Request.write(chunk));
703
+ inboundRequest.on("end", () => outboundHttp1Request.end());
704
+ }));
705
+ // intriguingly, error is reset to "false" at this point, even if it was null
706
+ if (error) {
707
+ send(502, inboundResponse, error);
708
+ return;
709
+ } else error = null;
646
710
 
647
- outboundExchange &&
648
- (outboundExchange as unknown as Http2Stream).on(
649
- "error",
650
- (thrown: Error) => {
651
- const httpVersionSupported =
652
- (thrown as ErrorWithErrno).errno === -505;
653
- error = Buffer.from(
654
- errorPage(
655
- thrown,
656
- "stream" +
657
- (httpVersionSupported
658
- ? " (error -505 usually means that the downstream service " +
659
- "does not support this http version)"
660
- : ""),
661
- url,
662
- targetUrl,
663
- ),
664
- );
665
- },
666
- );
711
+ // phase : request body
712
+ if (
713
+ config.ssl && // http/2
714
+ (inboundRequest as Http2ServerRequest).stream &&
715
+ (inboundRequest as Http2ServerRequest).stream.readableLength &&
716
+ outboundExchange
717
+ ) {
718
+ (inboundRequest as Http2ServerRequest).stream.on("data", chunk =>
719
+ outboundExchange.write(chunk),
720
+ );
721
+ (inboundRequest as Http2ServerRequest).stream.on("end", () =>
722
+ outboundExchange.end(),
723
+ );
724
+ }
667
725
 
668
- const http1RequestOptions: RequestOptions = {
669
- hostname: target.hostname,
670
- path: fullPath,
671
- port: target.port,
672
- protocol: target.protocol,
673
- rejectUnauthorized: false,
674
- method: inboundRequest.method,
675
- headers: {
676
- ...Object.assign(
677
- {},
678
- ...Object.entries(outboundHeaders)
679
- .filter(
680
- ([h]) =>
681
- !h.startsWith(":") &&
682
- h.toLowerCase() !== "transfer-encoding",
683
- )
684
- .map(([key, value]) => ({ [key]: value })),
685
- ),
686
- host: target.hostname,
687
- },
688
- };
689
- const outboundHttp1Response: IncomingMessage =
690
- !error &&
691
- !http2IsSupported &&
692
- target.protocol !== "file:" &&
693
- (await new Promise(resolve => {
694
- const outboundHttp1Request: ClientRequest =
695
- target.protocol === "https:"
696
- ? httpsRequest(http1RequestOptions, resolve)
697
- : httpRequest(http1RequestOptions, resolve);
726
+ if (
727
+ !config.ssl && // http1.1
728
+ (inboundRequest as IncomingMessage).readableLength &&
729
+ outboundExchange
730
+ ) {
731
+ (inboundRequest as IncomingMessage).on("data", chunk =>
732
+ outboundExchange.write(chunk),
733
+ );
734
+ (inboundRequest as IncomingMessage).on("end", () =>
735
+ outboundExchange.end(),
736
+ );
737
+ }
698
738
 
699
- outboundHttp1Request.on("error", thrown => {
700
- error = Buffer.from(errorPage(thrown, "request", url, targetUrl));
701
- resolve(null as IncomingMessage);
702
- });
703
- inboundRequest.on("data", chunk =>
704
- outboundHttp1Request.write(chunk),
705
- );
706
- inboundRequest.on("end", () => outboundHttp1Request.end());
707
- }));
708
- // intriguingly, error is reset to "false" at this point, even if it was null
709
- if (error) {
710
- send(502, inboundResponse, error);
711
- return;
712
- } else error = null;
739
+ // phase : response headers
740
+ const { outboundResponseHeaders } = await new Promise<{
741
+ outboundResponseHeaders: IncomingHttpHeaders & IncomingHttpStatusHeader;
742
+ }>(resolve =>
743
+ outboundExchange
744
+ ? outboundExchange.on("response", headers => {
745
+ resolve({
746
+ outboundResponseHeaders: headers,
747
+ });
748
+ })
749
+ : !outboundExchange && outboundHttp1Response
750
+ ? resolve({
751
+ outboundResponseHeaders: outboundHttp1Response.headers,
752
+ })
753
+ : resolve({
754
+ outboundResponseHeaders: {},
755
+ }),
756
+ );
713
757
 
714
- // phase : request body
715
- if (
716
- config.ssl && // http/2
717
- (inboundRequest as Http2ServerRequest).stream &&
718
- (inboundRequest as Http2ServerRequest).stream.readableLength &&
719
- outboundExchange
720
- ) {
721
- (inboundRequest as Http2ServerRequest).stream.on("data", chunk =>
722
- outboundExchange.write(chunk),
723
- );
724
- (inboundRequest as Http2ServerRequest).stream.on("end", () =>
725
- outboundExchange.end(),
758
+ const newUrl = !outboundResponseHeaders["location"]
759
+ ? null
760
+ : new URL(
761
+ outboundResponseHeaders["location"].startsWith("/")
762
+ ? `${target.href}${outboundResponseHeaders["location"].replace(
763
+ /^\/+/,
764
+ ``,
765
+ )}`
766
+ : outboundResponseHeaders["location"],
726
767
  );
727
- }
768
+ const newPath = !newUrl
769
+ ? null
770
+ : newUrl.href.substring(newUrl.origin.length);
771
+ const newTarget = url.origin;
772
+ const newTargetUrl = !newUrl ? null : `${newTarget}${newPath}`;
728
773
 
729
- if (
730
- !config.ssl && // http1.1
731
- (inboundRequest as IncomingMessage).readableLength &&
732
- outboundExchange
733
- ) {
734
- (inboundRequest as IncomingMessage).on("data", chunk =>
735
- outboundExchange.write(chunk),
736
- );
737
- (inboundRequest as IncomingMessage).on("end", () =>
738
- outboundExchange.end(),
774
+ // phase : response body
775
+ const payloadSource = outboundExchange || outboundHttp1Response;
776
+ const payload: Buffer =
777
+ error ??
778
+ (await new Promise(resolve => {
779
+ let partialBody = Buffer.alloc(0);
780
+ if (!payloadSource) {
781
+ resolve(partialBody);
782
+ return;
783
+ }
784
+ (payloadSource as ClientHttp2Stream | Duplex).on(
785
+ "data",
786
+ (chunk: Buffer | string) =>
787
+ (partialBody = Buffer.concat([
788
+ partialBody,
789
+ typeof chunk === "string"
790
+ ? Buffer.from(chunk as string)
791
+ : (chunk as Buffer),
792
+ ])),
739
793
  );
740
- }
741
-
742
- // phase : response headers
743
- const { outboundResponseHeaders } = await new Promise(resolve =>
744
- outboundExchange
745
- ? outboundExchange.on("response", headers => {
746
- resolve({
747
- outboundResponseHeaders: headers,
748
- });
749
- })
750
- : !outboundExchange && outboundHttp1Response
751
- ? resolve({
752
- outboundResponseHeaders: outboundHttp1Response.headers,
753
- })
754
- : resolve({
755
- outboundResponseHeaders: {},
756
- }),
757
- );
758
-
759
- const newUrl = !outboundResponseHeaders["location"]
760
- ? null
761
- : new URL(
762
- outboundResponseHeaders["location"].startsWith("/")
763
- ? `${target.href}${outboundResponseHeaders["location"].replace(
764
- /^\/+/,
765
- ``,
766
- )}`
767
- : outboundResponseHeaders["location"],
768
- );
769
- const newPath = !newUrl
770
- ? null
771
- : newUrl.href.substring(newUrl.origin.length);
772
- const newTarget = url.origin;
773
- const newTargetUrl = !newUrl ? null : `${newTarget}${newPath}`;
794
+ (payloadSource as any).on("end", () => {
795
+ resolve(partialBody);
796
+ });
797
+ }).then((payloadBuffer: Buffer) => {
798
+ if (!config.replaceResponseBodyUrls) return payloadBuffer;
799
+ if (!payloadBuffer.length) return payloadBuffer;
774
800
 
775
- // phase : response body
776
- const payloadSource = outboundExchange || outboundHttp1Response;
777
- const payload =
778
- error ??
779
- (await new Promise(resolve => {
780
- let partialBody = Buffer.alloc(0);
781
- if (!payloadSource) {
782
- resolve(partialBody);
783
- return;
784
- }
785
- (payloadSource as ClientHttp2Stream | Duplex).on(
786
- "data",
787
- (chunk: Buffer | string) =>
788
- (partialBody = Buffer.concat([
789
- partialBody,
790
- typeof chunk === "string"
791
- ? Buffer.from(chunk as string)
792
- : (chunk as Buffer),
793
- ])),
794
- );
795
- (payloadSource as any).on("end", () => {
796
- resolve(partialBody);
797
- });
798
- }).then((payloadBuffer: Buffer) => {
799
- if (!config.replaceResponseBodyUrls) return payloadBuffer;
800
- if (!payloadBuffer.length) return payloadBuffer;
801
+ return (outboundResponseHeaders["content-encoding"] || "")
802
+ .split(",")
803
+ .reduce(
804
+ async (buffer: Promise<Buffer>, formatNotTrimed: string) => {
805
+ const format = formatNotTrimed.trim().toLowerCase();
806
+ const method =
807
+ format === "gzip" || format === "x-gzip"
808
+ ? gunzip
809
+ : format === "deflate"
810
+ ? inflate
811
+ : format === "br"
812
+ ? brotliDecompress
813
+ : format === "identity" || format === ""
814
+ ? (
815
+ input: Buffer,
816
+ callback: (err?: Error, data?: Buffer) => void,
817
+ ) => {
818
+ callback(null, input);
819
+ }
820
+ : null;
821
+ if (method === null) {
822
+ send(
823
+ 502,
824
+ inboundResponse,
825
+ Buffer.from(
826
+ errorPage(
827
+ new Error(
828
+ `${format} compression not supported by the proxy`,
829
+ ),
830
+ "stream",
831
+ url,
832
+ targetUrl,
833
+ ),
834
+ ),
835
+ );
836
+ return;
837
+ }
801
838
 
802
- return (outboundResponseHeaders["content-encoding"] || "")
803
- .split(",")
804
- .reduce(
805
- async (buffer: Promise<Buffer>, formatNotTrimed: string) => {
839
+ const openedBuffer = await buffer;
840
+ return await new Promise<Buffer>(resolve =>
841
+ method(openedBuffer, (err_1, data_1) => {
842
+ if (err_1) {
843
+ send(
844
+ 502,
845
+ inboundResponse,
846
+ Buffer.from(errorPage(err_1, "stream", url, targetUrl)),
847
+ );
848
+ resolve(Buffer.from(""));
849
+ return;
850
+ }
851
+ resolve(data_1);
852
+ }),
853
+ );
854
+ },
855
+ Promise.resolve(payloadBuffer),
856
+ )
857
+ .then((uncompressedBuffer: Buffer) => {
858
+ const fileTooBig = uncompressedBuffer.length > 1e7;
859
+ const fileHasSpecialChars = () =>
860
+ /[^\x00-\x7F]/.test(uncompressedBuffer.toString());
861
+ const contentTypeCanBeProcessed = [
862
+ "text/html",
863
+ "application/javascript",
864
+ "application/json",
865
+ ].some(allowedContentType =>
866
+ (outboundResponseHeaders["content-type"] ?? "").includes(
867
+ allowedContentType,
868
+ ),
869
+ );
870
+ const willReplace =
871
+ !fileTooBig &&
872
+ (contentTypeCanBeProcessed || !fileHasSpecialChars());
873
+ return !willReplace
874
+ ? uncompressedBuffer
875
+ : !config.replaceResponseBodyUrls
876
+ ? uncompressedBuffer.toString()
877
+ : Object.entries(config.mapping)
878
+ .reduce(
879
+ (inProgress, [path, mapping]) =>
880
+ path !== "" &&
881
+ !path.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)
882
+ ? inProgress
883
+ : inProgress.replace(
884
+ new RegExp(
885
+ mapping
886
+ .replace(/^file:\/\//, "")
887
+ .replace(/[*+?^${}()|[\]\\]/g, "")
888
+ .replace(/^https/, "https?") + "/*",
889
+ "ig",
890
+ ),
891
+ `https://${proxyHostnameAndPort}${path.replace(
892
+ /\/+$/,
893
+ "",
894
+ )}/`,
895
+ ),
896
+ uncompressedBuffer.toString(),
897
+ )
898
+ .split(`${proxyHostnameAndPort}/:`)
899
+ .join(`${proxyHostnameAndPort}:`)
900
+ .replace(
901
+ /\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,
902
+ `?protocol=ws${
903
+ config.ssl ? "s" : ""
904
+ }%3A&hostname=${proxyHostname}&port=${
905
+ config.port
906
+ }&pathname=${encodeURIComponent(
907
+ key.replace(/\/+$/, ""),
908
+ )}`,
909
+ );
910
+ })
911
+ .then((updatedBody: Buffer | string) =>
912
+ (outboundResponseHeaders["content-encoding"] || "")
913
+ .split(",")
914
+ .reduce((buffer: Promise<Buffer>, formatNotTrimed: string) => {
806
915
  const format = formatNotTrimed.trim().toLowerCase();
807
916
  const method =
808
917
  format === "gzip" || format === "x-gzip"
809
- ? gunzip
918
+ ? gzip
810
919
  : format === "deflate"
811
- ? inflate
920
+ ? deflate
812
921
  : format === "br"
813
- ? brotliDecompress
922
+ ? brotliCompress
814
923
  : format === "identity" || format === ""
815
924
  ? (
816
925
  input: Buffer,
@@ -819,207 +928,90 @@ const start = () => {
819
928
  callback(null, input);
820
929
  }
821
930
  : null;
822
- if (method === null) {
823
- send(
824
- 502,
825
- inboundResponse,
826
- Buffer.from(
827
- errorPage(
828
- new Error(
829
- `${format} compression not supported by the proxy`,
830
- ),
831
- "stream",
832
- url,
833
- targetUrl,
834
- ),
835
- ),
931
+ if (method === null)
932
+ throw new Error(
933
+ `${format} compression not supported by the proxy`,
836
934
  );
837
- return;
838
- }
839
935
 
840
- const openedBuffer = await buffer;
841
- return await new Promise(resolve =>
842
- method(openedBuffer, (err_1, data_1) => {
843
- if (err_1) {
844
- send(
845
- 502,
846
- inboundResponse,
847
- Buffer.from(
848
- errorPage(err_1, "stream", url, targetUrl),
849
- ),
850
- );
851
- resolve("");
852
- return;
853
- }
854
- resolve(data_1);
855
- }),
936
+ return buffer.then(
937
+ data =>
938
+ new Promise<Buffer>(resolve =>
939
+ method(data, (err, data) => {
940
+ if (err) throw err;
941
+ resolve(data);
942
+ }),
943
+ ),
856
944
  );
857
- },
858
- Promise.resolve(payloadBuffer),
859
- )
860
- .then((uncompressedBuffer: Buffer) => {
861
- const fileTooBig = uncompressedBuffer.length > 1e7;
862
- const fileHasSpecialChars = () =>
863
- /[^\x00-\x7F]/.test(uncompressedBuffer.toString());
864
- const contentTypeCanBeProcessed = [
865
- "text/html",
866
- "application/javascript",
867
- "application/json",
868
- ].some(allowedContentType =>
869
- (outboundResponseHeaders["content-type"] ?? "").includes(
870
- allowedContentType,
871
- ),
872
- );
873
- const willReplace =
874
- !fileTooBig &&
875
- (contentTypeCanBeProcessed || !fileHasSpecialChars());
876
- return !willReplace
877
- ? uncompressedBuffer
878
- : !config.replaceResponseBodyUrls
879
- ? uncompressedBuffer.toString()
880
- : Object.entries(config.mapping)
881
- .reduce(
882
- (inProgress, [path, mapping]) =>
883
- path !== "" &&
884
- !path.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)
885
- ? inProgress
886
- : inProgress.replace(
887
- new RegExp(
888
- mapping
889
- .replace(/^file:\/\//, "")
890
- .replace(/[*+?^${}()|[\]\\]/g, "")
891
- .replace(/^https/, "https?") + "/*",
892
- "ig",
893
- ),
894
- `https://${proxyHostnameAndPort}${path.replace(
895
- /\/+$/,
896
- "",
897
- )}/`,
898
- ),
899
- uncompressedBuffer.toString(),
900
- )
901
- .split(`${proxyHostnameAndPort}/:`)
902
- .join(`${proxyHostnameAndPort}:`)
903
- .replace(
904
- /\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,
905
- `?protocol=ws${
906
- config.ssl ? "s" : ""
907
- }%3A&hostname=${proxyHostname}&port=${
908
- config.port
909
- }&pathname=${encodeURIComponent(
910
- key.replace(/\/+$/, ""),
911
- )}`,
912
- );
913
- })
914
- .then((updatedBody: Buffer | string) =>
915
- (outboundResponseHeaders["content-encoding"] || "")
916
- .split(",")
917
- .reduce(
918
- (buffer: Promise<Buffer>, formatNotTrimed: string) => {
919
- const format = formatNotTrimed.trim().toLowerCase();
920
- const method =
921
- format === "gzip" || format === "x-gzip"
922
- ? gzip
923
- : format === "deflate"
924
- ? deflate
925
- : format === "br"
926
- ? brotliCompress
927
- : format === "identity" || format === ""
928
- ? (
929
- input: Buffer,
930
- callback: (err?: Error, data?: Buffer) => void,
931
- ) => {
932
- callback(null, input);
933
- }
934
- : null;
935
- if (method === null)
936
- throw new Error(
937
- `${format} compression not supported by the proxy`,
938
- );
939
-
940
- return buffer.then(
941
- data =>
942
- new Promise(resolve =>
943
- method(data, (err, data) => {
944
- if (err) throw err;
945
- resolve(data);
946
- }),
947
- ),
948
- );
949
- },
950
- Promise.resolve(Buffer.from(updatedBody)),
951
- ),
952
- );
953
- }));
945
+ }, Promise.resolve(Buffer.from(updatedBody))),
946
+ );
947
+ }));
954
948
 
955
- // phase : inbound response
956
- const responseHeaders = {
957
- ...Object.entries({
958
- ...outboundResponseHeaders,
959
- ...(config.replaceResponseBodyUrls
960
- ? { ["content-length"]: `${payload.byteLength}` }
961
- : {}),
962
- })
963
- .filter(
964
- ([h]) =>
965
- !h.startsWith(":") &&
966
- h.toLowerCase() !== "transfer-encoding" &&
967
- h.toLowerCase() !== "connection",
968
- )
969
- .reduce((acc: any, [key, value]: [string, string | string[]]) => {
970
- const allSubdomains = targetHost
971
- .split("")
972
- .map(
973
- (_, i) =>
974
- targetHost.substring(i).startsWith(".") &&
975
- targetHost.substring(i),
976
- )
977
- .filter(subdomain => subdomain) as string[];
978
- const transformedValue = [targetHost]
979
- .concat(allSubdomains)
980
- .reduce(
981
- (acc1, subDomain) =>
982
- (!Array.isArray(acc1) ? [acc1] : (acc1 as string[])).map(
983
- oneElement => {
984
- return typeof oneElement === "string"
985
- ? oneElement.replace(
986
- `Domain=${subDomain}`,
987
- `Domain=${url.hostname}`,
988
- )
989
- : oneElement;
990
- },
991
- ),
992
- value,
993
- );
949
+ // phase : inbound response
950
+ const responseHeaders = {
951
+ ...Object.entries({
952
+ ...outboundResponseHeaders,
953
+ ...(config.replaceResponseBodyUrls
954
+ ? { ["content-length"]: `${payload.byteLength}` }
955
+ : {}),
956
+ })
957
+ .filter(
958
+ ([h]) =>
959
+ !h.startsWith(":") &&
960
+ h.toLowerCase() !== "transfer-encoding" &&
961
+ h.toLowerCase() !== "connection" &&
962
+ h.toLowerCase() !== "keep-alive",
963
+ )
964
+ .reduce((acc: any, [key, value]: [string, string | string[]]) => {
965
+ const allSubdomains = targetHost
966
+ .split("")
967
+ .map(
968
+ (_, i) =>
969
+ targetHost.substring(i).startsWith(".") &&
970
+ targetHost.substring(i),
971
+ )
972
+ .filter(subdomain => subdomain) as string[];
973
+ const transformedValue = [targetHost].concat(allSubdomains).reduce(
974
+ (acc1, subDomain) =>
975
+ (!Array.isArray(acc1) ? [acc1] : (acc1 as string[])).map(
976
+ oneElement => {
977
+ return typeof oneElement === "string"
978
+ ? oneElement.replace(
979
+ `Domain=${subDomain}`,
980
+ `Domain=${url.hostname}`,
981
+ )
982
+ : oneElement;
983
+ },
984
+ ),
985
+ value,
986
+ );
994
987
 
995
- acc[key] = (acc[key] || []).concat(transformedValue);
996
- return acc;
997
- }, {}),
998
- ...(newTargetUrl ? { location: [newTargetUrl] } : {}),
999
- };
1000
- try {
1001
- Object.entries(responseHeaders).forEach(
1002
- ([headerName, headerValue]) =>
1003
- headerValue &&
1004
- inboundResponse.setHeader(headerName, headerValue as string),
1005
- );
1006
- } catch (e) {
1007
- // ERR_HTTP2_HEADERS_SENT
1008
- }
1009
- inboundResponse.writeHead(
1010
- outboundResponseHeaders[":status"] ||
1011
- outboundHttp1Response.statusCode ||
1012
- 200,
1013
- config.ssl
1014
- ? undefined // statusMessage is discarded in http/2
1015
- : outboundHttp1Response.statusMessage || "Status read from http/2",
1016
- responseHeaders,
988
+ acc[key] = (acc[key] || []).concat(transformedValue);
989
+ return acc;
990
+ }, {}),
991
+ ...(newTargetUrl ? { location: [newTargetUrl] } : {}),
992
+ };
993
+ try {
994
+ Object.entries(responseHeaders).forEach(
995
+ ([headerName, headerValue]) =>
996
+ headerValue &&
997
+ inboundResponse.setHeader(headerName, headerValue as string),
1017
998
  );
1018
- if (payload) inboundResponse.end(payload);
1019
- else inboundResponse.end();
1020
- },
1021
- ) as Server
1022
- )
999
+ } catch (e) {
1000
+ // ERR_HTTP2_HEADERS_SENT
1001
+ }
1002
+ inboundResponse.writeHead(
1003
+ outboundResponseHeaders[":status"] ||
1004
+ outboundHttp1Response.statusCode ||
1005
+ 200,
1006
+ config.ssl
1007
+ ? undefined // statusMessage is discarded in http/2
1008
+ : outboundHttp1Response.statusMessage || "Status read from http/2",
1009
+ responseHeaders,
1010
+ );
1011
+ if (payload) inboundResponse.end(payload);
1012
+ else inboundResponse.end();
1013
+ },
1014
+ ) as Server)
1023
1015
  .addListener("error", (err: Error) => {
1024
1016
  if ((err as ErrorWithErrno).code === "EACCES")
1025
1017
  log(`permission denied for this port`, LogLevel.ERROR, EMOJIS.NO);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-traffic",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "main": "index.ts",
5
5
  "private": false,
6
6
  "keywords": [
@@ -24,15 +24,15 @@
24
24
  "build": "npm run clean && npm run typescript && npm run terser && npm run shebang && npm run chmod"
25
25
  },
26
26
  "devDependencies": {
27
- "@types/node": "^17.0.37",
28
- "terser": "^5.14.0",
29
- "typescript": "^4.7.2"
27
+ "@types/node": "^18.11.11",
28
+ "terser": "^5.16.1",
29
+ "typescript": "^4.9.4"
30
30
  },
31
31
  "bin": {
32
32
  "local-traffic": "./dist/localTraffic.js"
33
33
  },
34
34
  "volta": {
35
35
  "node": "18.1.0",
36
- "npm": "8.10.0"
36
+ "npm": "9.1.1"
37
37
  }
38
38
  }