local-traffic 0.0.30 → 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 +1 -0
- package/dist/localTraffic.js +1 -1
- package/dist/terser.js +1 -0
- package/index.ts +130 -37
- package/package.json +14 -7
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
|
package/dist/localTraffic.js
CHANGED
|
@@ -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?"":"[36m"}${`${t.getHours()}`.padStart(2,"0")}${e?":":"[33m:[36m"}${`${t.getMinutes()}`.padStart(2,"0")}${e?":":"[33m:[36m"}${`${t.getSeconds()}`.padStart(2,"0")}${e?"":"[0m"}`})(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)} ⎹[0m`: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(`[48;5;5m⎸ ↘️ : ${e.ssl?"HTTP/2 ":"HTTP 1.1"} [48;5;21m⎸ ☎️ : ${e.port.toString().padStart(5)} [48;5;31m⎸ ↗️ : ${e.dontUseHttp2Downstream?"HTTP 1.1":"HTTP/2 "} ⎹[0m`)},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,"/")}</i></p>\n <ul class="list-group">\n <li class="list-group-item">📁<a href="${e.pathname.endsWith("/")?"..":"."}"><parent></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 ⓘ 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||"<no-target-url>"}</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.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)||""===t?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?"":"[36m"}${`${t.getHours()}`.padStart(2,"0")}${e?":":"[33m:[36m"}${`${t.getMinutes()}`.padStart(2,"0")}${e?":":"[33m:[36m"}${`${t.getSeconds()}`.padStart(2,"0")}${e?"":"[0m"}`})(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)} ⎹[0m`: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(`[48;5;5m⎸ ↘️ : ${e.ssl?"HTTP/2 ":"HTTP 1.1"} [48;5;21m⎸ ☎️ : ${e.port.toString().padStart(5)} [48;5;31m⎸ ↗️ : ${e.dontUseHttp2Downstream?"HTTP 1.1":"HTTP/2 "} ⎹[0m`)},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,"/")}</i></p>\n <ul class="list-group">\n <li class="list-group-item">📁<a href="${e.pathname.endsWith("/")?"..":"."}"><parent></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 ⓘ 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||"<no-target-url>"}</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?"":"[36m"}${`${t.getHours()}`.padStart(2,"0")}${e?":":"[33m:[36m"}${`${t.getMinutes()}`.padStart(2,"0")}${e?":":"[33m:[36m"}${`${t.getSeconds()}`.padStart(2,"0")}${e?"":"[0m"}`})(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)} ⎹[0m`: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(`[48;5;5m⎸ ↘️ : ${e.ssl?"HTTP/2 ":"HTTP 1.1"} [48;5;21m⎸ ☎️ : ${e.port.toString().padStart(5)} [48;5;31m⎸ ↗️ : ${e.dontUseHttp2Downstream?"HTTP 1.1":"HTTP/2 "} ⎹[0m`)},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,"/")}</i></p>\n <ul class="list-group">\n <li class="list-group-item">📁<a href="${e.pathname.endsWith("/")?"..":"."}"><parent></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 ⓘ 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||"<no-target-url>"}</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
|
|
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
|
|
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 (!
|
|
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
|
|
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,37 +760,44 @@ const start = () => {
|
|
|
728
760
|
})
|
|
729
761
|
);
|
|
730
762
|
}, Promise.resolve(payloadBuffer))
|
|
731
|
-
.then((uncompressedBuffer: Buffer) =>
|
|
732
|
-
|
|
733
|
-
/[^\x00-\x7F]/.test(uncompressedBuffer.toString())
|
|
734
|
-
|
|
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()
|
|
738
774
|
: Object.entries(config.mapping)
|
|
739
775
|
.reduce(
|
|
740
776
|
(inProgress, [path, mapping]) =>
|
|
741
|
-
!path.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)
|
|
742
|
-
path !== ''
|
|
777
|
+
path !== '' && !path.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)
|
|
743
778
|
? inProgress
|
|
744
779
|
: inProgress.replace(
|
|
745
780
|
new RegExp(
|
|
746
781
|
mapping
|
|
747
782
|
.replace(/^file:\/\//, "")
|
|
748
783
|
.replace(/[*+?^${}()|[\]\\]/g, "")
|
|
749
|
-
.replace(/^https/, 'https?'),
|
|
784
|
+
.replace(/^https/, 'https?') + '/*',
|
|
750
785
|
"ig"
|
|
751
786
|
),
|
|
752
|
-
`https://${
|
|
787
|
+
`https://${proxyHostnameAndPort}${path.replace(
|
|
753
788
|
/\/+$/,
|
|
754
789
|
""
|
|
755
790
|
)}/`
|
|
756
791
|
),
|
|
757
792
|
uncompressedBuffer.toString()
|
|
758
793
|
)
|
|
759
|
-
.split(`${
|
|
760
|
-
.join(`${
|
|
761
|
-
|
|
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
|
+
})
|
|
762
801
|
.then((updatedBody: Buffer | string) =>
|
|
763
802
|
(outboundResponseHeaders["content-encoding"] || "")
|
|
764
803
|
.split(",")
|
|
@@ -859,16 +898,70 @@ const start = () => {
|
|
|
859
898
|
if (payload) inboundResponse.end(payload);
|
|
860
899
|
else inboundResponse.end();
|
|
861
900
|
}
|
|
862
|
-
)
|
|
901
|
+
) as Server)
|
|
863
902
|
.addListener("error", (err: Error) => {
|
|
864
|
-
if ((err as
|
|
903
|
+
if ((err as ErrorWithErrno).code === "EACCES")
|
|
865
904
|
log(`permission denied for this port`, LogLevel.ERROR, "⛔");
|
|
866
|
-
if ((err as
|
|
905
|
+
if ((err as ErrorWithErrno).code === "EADDRINUSE")
|
|
867
906
|
log(`port is already used. NOT started`, LogLevel.ERROR, "☠️");
|
|
868
907
|
})
|
|
869
908
|
.addListener("listening", () => {
|
|
870
909
|
logProtocols(config);
|
|
871
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
|
+
})
|
|
872
965
|
.listen(config.port);
|
|
873
966
|
};
|
|
874
967
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-traffic",
|
|
3
|
-
"version": "0.0.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
25
|
-
"terser": "^5.
|
|
26
|
-
"typescript": "^4.
|
|
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
|
}
|