local-traffic 0.0.44 → 0.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,29 +21,31 @@ npx local-traffic
21
21
 
22
22
  ## how to use it
23
23
 
24
- 1. Change that mapping in the `.local-traffic.json` file:
24
+ 1. Change that mapping in the `.local-traffic.json` file:
25
25
 
26
26
  ```json
27
27
  {
28
- "mapping": {
29
- "/npm": "https://www.npmjs.com/",
30
- "/my-static-webapp": "file:///home/user/projects/my-static-webapp",
31
- "": "https://github.com/"
32
- },
28
+ "mapping": {
29
+ "/npm": "https://www.npmjs.com/",
30
+ "/my-static-webapp": "file:///home/user/projects/my-static-webapp",
31
+ "": "https://github.com/"
32
+ }
33
33
  }
34
34
  ```
35
+
35
36
  > "" mapping must always come last
37
+
36
38
  2. Go to [https://localhost/prettier](https://localhost/prettier) with your browser
37
39
  3. Go to [https://localhost/npm](https://localhost/npm) with your browser
38
- 3. Go to [https://localhost/my-static-webapp](https://localhost/my-static-webapp/index.html) with your browser
39
- (given your project name is my-static-webapp, but I am not 100% sure)
40
- 4. Your server now proxies the mapping that you have configured
40
+ 4. Go to [https://localhost/my-static-webapp](https://localhost/my-static-webapp/index.html) with your browser (given your project name is my-static-webapp, but I am not 100% sure)
41
+ 5. Your server now proxies the mapping that you have configured
41
42
 
42
43
  ## usage
43
44
 
44
45
  ```bash
45
46
  npx local-traffic [location-of-the-local-traffic-config-file]
46
47
  ```
48
+
47
49
  > When not specified, the location of the config file will be `$HOME/.local-traffic.json`
48
50
 
49
51
  ## how to change mappings to local / non-local
@@ -56,10 +58,11 @@ npx local-traffic [location-of-the-local-traffic-config-file]
56
58
 
57
59
  - "mapping": ({[path: string]: string}) routing rules (required)
58
60
  - "ssl" : SSL options
59
- * "ssl.cert" : (string) Certificate (PEM format)
60
- * "ssl.key" : (string) Private Key (PEM format)
61
+ - "ssl.cert" : (string) Certificate (PEM format)
62
+ - "ssl.key" : (string) Private Key (PEM format)
61
63
  - "port" : (number) port number
62
64
  - "replaceResponseBodyUrls": (boolean) replace every matching string from the mapping in the response body.
63
65
  - "dontUseHttp2Downstream": (boolean) force calling downstream services in http1.1 only (to save some time)
64
66
  - "simpleLogs": (boolean) disable colored logs for text terminals
65
67
  - "websocket": (boolean) true to activate websocket connections proxying via sockets
68
+ - "disableWebSecurity": (boolean) true for easygoing values in cross origin requests or content security policy headers
@@ -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","keep-alive"].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);
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.SHIELD="🛡️ ",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,disableWebSecurity:!1};let h,u;const m=(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(new RegExp(l.SHIELD,"g"),"web-security").replace(/\|+/g,"|"):t?`[48;5;${t}m⎸ ${process.stdout.isTTY&&r||""} ${e.padEnd(40)} ⎹`:e}`)},g=e=>{m(`⎸${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}⎹⎸${h.disableWebSecurity?l.NO:l.SHIELD}⎹`)},R=async(e=!0)=>new Promise((t=>(0,n.readFile)(c,((r,o)=>{r&&!e&&m("config error. Using default value",i.ERROR,l.ERROR_1);try{h=Object.assign({},d,JSON.parse((o||"{}").toString()))}catch(e){return m("config syntax incorrect, aborting",i.ERROR,l.ERROR_2),h=h||{...d},void t(h)}h.mapping[""]||m('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?m("config file NOT created",i.ERROR,l.ERROR_4):m("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 m("port number invalid. Not refreshing",i.ERROR,l.PORT)):"object"!=typeof h.mapping?(h=e,void m("mapping should be an object. Aborting",i.ERROR,l.ERROR_5)):(h.replaceResponseBodyUrls!==e.replaceResponseBodyUrls&&m(`response body url ${h.replaceResponseBodyUrls?"":"NO "}replacement`,i.INFO,l.BODY_REPLACEMENT),h.dontUseHttp2Downstream!==e.dontUseHttp2Downstream&&m(`http/2 ${h.dontUseHttp2Downstream?"de":""}activated downstream`,i.INFO,l.OUTBOUND),h.disableWebSecurity!==e.disableWebSecurity&&m(`web security ${h.disableWebSecurity?"de":""}activated`,i.INFO,l.SHIELD),h.websocket!==e.websocket&&m(`websocket ${h.websocket?"":"de"}activated`,i.INFO,l.WEBSOCKET),h.simpleLogs!==e.simpleLogs&&m("simple logs "+(h.simpleLogs?"on":"off"),i.INFO,l.COLORED),Object.keys(h.mapping).join("\n")!==Object.keys(e.mapping).join("\n")&&m(`${Object.keys(h.mapping).length.toString().padStart(5)} loaded mapping rules`,i.INFO,l.RULES),h.port!==e.port&&m(`port changed from ${e.port} to ${h.port}`,i.INFO,l.PORT),h.ssl&&!e.ssl&&m("ssl configuration added",i.INFO,l.INBOUND),!h.ssl&&e.ssl&&m("ssl configuration removed",i.INFO,l.INBOUND),void(h.port!==e.port||JSON.stringify(h.ssl)!==JSON.stringify(e.ssl)?(await new Promise((e=>u?u.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/>`,b=(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>`,y=(e,t,r)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":r.length}),t.end(r)},E=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=()=>{u=(h.ssl?e.createSecureServer.bind(null,{...h.ssl,allowHTTP1:!0}):t.createServer)((async(n,a)=>{if(!n.headers.host&&!n.headers[":authority"])return void y(400,a,Buffer.from(b(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:u,target:m}=E(n);if(!m)return void y(502,a,Buffer.from(b(new Error(`No mapping found in config file ${c}`),"proxy",p)));const g=m.host.replace(RegExp(/\/+$/),""),R=`${m.href.substring("https://".length+m.host.length)}${$(d.replace(RegExp($(u)),""))}`.replace(/^\/*/,"/"),f=new o.URL(`${m.protocol}//${g}${R}`);let w=null,v=!h.dontUseHttp2Downstream;const N="file:"===m.protocol?O(f):v?await Promise.race([new Promise((t=>{const r=(0,e.connect)(f,{rejectUnauthorized:!1,protocol:m.protocol},((e,o)=>{v=v&&!!o.alpnProtocol,t(v?r:null)}));r.on("error",(e=>{w=v&&Buffer.from(b(e,"connection",p,f))}))})),new Promise((e=>setTimeout((()=>{v=!1,e(null)}),3e3)))]):null;w instanceof Buffer||(w=null);const S={...[...Object.entries(n.headers)].filter((([e])=>!["host","connection","keep-alive"].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:m.href,referer:f.toString(),":authority":g,":method":n.method,":path":R,":scheme":m.protocol.replace(":","")},U=N&&!w&&N.request(S,{endStream:h.ssl?!(n?.stream?.readableLength??1):!n.readableLength});U?.on("error",(e=>{const t=-505===e.errno;w=Buffer.from(b(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),p,f))}));const L={hostname:m.hostname,path:R,port:m.port,protocol:m.protocol,rejectUnauthorized:!1,method:n.method,headers:{...Object.assign({},...Object.entries(S).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase())).map((([e,t])=>({[e]:t})))),host:m.hostname}},B=!w&&!v&&"file:"!==m.protocol&&await new Promise((e=>{const o="https:"===m.protocol?(0,r.request)(L,e):(0,t.request)(L,e);o.on("error",(t=>{w=Buffer.from(b(t,"request",p,f)),e(null)})),n.on("data",(e=>o.write(e))),n.on("end",(()=>o.end()))}));if(w)return void y(502,a,w);w=null,h.ssl&&n.stream&&n.stream.readableLength&&U&&(n.stream.on("data",(e=>U.write(e))),n.stream.on("end",(()=>U.end()))),!h.ssl&&n.readableLength&&U&&(n.on("data",(e=>U.write(e))),n.on("end",(()=>U.end())));const{outboundResponseHeaders:j}=await new Promise((e=>U?U.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!U&&B?{outboundResponseHeaders:B.headers}:{outboundResponseHeaders:{}}))),x=j.location?new o.URL(j.location.startsWith("/")?`${m.href}${j.location.replace(/^\/+/,"")}`:j.location):null,D=x?x.href.substring(x.origin.length):null,T=p.origin,H=x?`${T}${D}`:null,P=U||B,I=w??await new Promise((e=>{let t=Buffer.alloc(0);P?(P.on("data",(e=>t=Buffer.concat([t,"string"==typeof e?Buffer.from(e):e]))),P.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 y(502,a,Buffer.from(b(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 y(502,a,Buffer.from(b(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(u.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)),C={...Object.entries({...j,...h.replaceResponseBodyUrls?{"content-length":`${I.byteLength}`}:{},...h.disableWebSecurity?{"content-security-policy":"report only","access-control-allow-headers":"*","access-control-allow-method":"*","access-control-allow-origin":"*"}:{}}).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}),{}),...H?{location:[H]}:{}};try{Object.entries(C).forEach((([e,t])=>t&&a.setHeader(e,t)))}catch(e){}a.writeHead(j[":status"]||B.statusCode||200,h.ssl?void 0:B.statusMessage||"Status read from http/2",C),I?a.end(I):a.end()})).addListener("error",(e=>{"EACCES"===e.code&&m("permission denied for this port",i.ERROR,l.NO),"EADDRINUSE"===e.code&&m("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}=E(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=>{m("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=>{m("downstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,l.WEBSOCKET)})),n.on("error",(e=>{m("upstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,l.WEBSOCKET)}))}))})).listen(h.port)};R().then(v);
package/index.ts CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  Http2Session,
6
6
  Http2ServerRequest,
7
7
  Http2ServerResponse,
8
- Http2Stream,
9
8
  OutgoingHttpHeaders,
10
9
  SecureClientSessionOptions,
11
10
  SecureServerOptions,
@@ -51,6 +50,7 @@ enum EMOJIS {
51
50
  BODY_REPLACEMENT = "✒️ ",
52
51
  WEBSOCKET = "☄️ ",
53
52
  COLORED = "✨",
53
+ SHIELD = "🛡️ ",
54
54
  NO = "⛔",
55
55
  ERROR_1 = "❌",
56
56
  ERROR_2 = "⛈️ ",
@@ -68,6 +68,7 @@ interface LocalConfiguration {
68
68
  dontUseHttp2Downstream?: boolean;
69
69
  simpleLogs?: boolean;
70
70
  websocket?: boolean;
71
+ disableWebSecurity?: boolean;
71
72
  }
72
73
 
73
74
  const userHomeConfigFile = resolve(process.env.HOME, ".local-traffic.json");
@@ -84,6 +85,7 @@ const defaultConfig: LocalConfiguration = {
84
85
  dontUseHttp2Downstream: false,
85
86
  simpleLogs: false,
86
87
  websocket: false,
88
+ disableWebSecurity: false,
87
89
  };
88
90
 
89
91
  let config: LocalConfiguration;
@@ -117,11 +119,12 @@ const log = (text: string, level?: LogLevel, emoji?: string) => {
117
119
  "body replacement",
118
120
  )
119
121
  .replace(new RegExp(EMOJIS.WEBSOCKET, "g"), "websocket")
122
+ .replace(new RegExp(EMOJIS.SHIELD, "g"), "web-security")
120
123
  .replace(/\|+/g, "|")
121
124
  : level
122
125
  ? `\u001b[48;5;${level}m⎸ ${
123
126
  !process.stdout.isTTY ? "" : emoji || ""
124
- } ${text.padEnd(36)} ⎹\u001b[0m`
127
+ } ${text.padEnd(40)} ⎹\u001b[0m`
125
128
  : text
126
129
  }`,
127
130
  );
@@ -143,6 +146,8 @@ const quickStatus = (thisConfig: LocalConfiguration) => {
143
146
  config.websocket ? EMOJIS.WEBSOCKET : EMOJIS.NO
144
147
  }⎹\u001b[48;5;93m⎸${
145
148
  !config.simpleLogs ? EMOJIS.COLORED : EMOJIS.NO
149
+ }⎹\u001b[48;5;98m⎸${
150
+ config.disableWebSecurity ? EMOJIS.NO : EMOJIS.SHIELD
146
151
  }⎹\u001b[0m`,
147
152
  );
148
153
  };
@@ -233,6 +238,13 @@ const onWatch = async () => {
233
238
  EMOJIS.OUTBOUND,
234
239
  );
235
240
  }
241
+ if (config.disableWebSecurity !== previousConfig.disableWebSecurity) {
242
+ log(
243
+ `web security ${config.disableWebSecurity ? "de" : ""}activated`,
244
+ LogLevel.INFO,
245
+ EMOJIS.SHIELD,
246
+ );
247
+ }
236
248
  if (config.websocket !== previousConfig.websocket) {
237
249
  log(
238
250
  `websocket ${!config.websocket ? "de" : ""}activated`,
@@ -303,7 +315,7 @@ const fileRequest = (url: URL): ClientHttp2Session => {
303
315
  .replace(/^\/+/, "")
304
316
  .split("/"),
305
317
  );
306
- return ({
318
+ return {
307
319
  error: null as Error,
308
320
  data: null as string | Buffer,
309
321
  hasRun: false,
@@ -401,7 +413,7 @@ const fileRequest = (url: URL): ClientHttp2Session => {
401
413
  request: function () {
402
414
  return this;
403
415
  },
404
- } as unknown) as ClientHttp2Session;
416
+ } as unknown as ClientHttp2Session;
405
417
  };
406
418
 
407
419
  const header = (
@@ -513,413 +525,308 @@ const determineMapping = (
513
525
  };
514
526
 
515
527
  const start = () => {
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
- }`,
528
+ server = (
529
+ (config.ssl
530
+ ? createSecureServer.bind(null, { ...config.ssl, allowHTTP1: true })
531
+ : createServer)(
532
+ async (
533
+ inboundRequest: Http2ServerRequest | IncomingMessage,
534
+ inboundResponse: Http2ServerResponse | ServerResponse,
535
+ ) => {
536
+ // phase: mapping
537
+ if (
538
+ !inboundRequest.headers.host &&
539
+ !inboundRequest.headers[":authority"]
540
+ ) {
541
+ send(
542
+ 400,
543
+ inboundResponse,
544
+ Buffer.from(
545
+ errorPage(
546
+ new Error(`client must supply a 'host' header`),
547
+ "proxy",
548
+ new URL(
549
+ `http${config.ssl ? "s" : ""}://unknowndomain${
550
+ inboundRequest.url
551
+ }`,
552
+ ),
539
553
  ),
540
554
  ),
541
- ),
542
- );
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,
555
+ );
556
+ return;
557
+ }
558
+ const { proxyHostname, proxyHostnameAndPort, url, path, key, target } =
559
+ determineMapping(inboundRequest);
560
+ if (!target) {
561
+ send(
562
+ 502,
563
+ inboundResponse,
564
+ Buffer.from(
565
+ errorPage(
566
+ new Error(`No mapping found in config file ${filename}`),
567
+ "proxy",
568
+ url,
569
+ ),
562
570
  ),
563
- ),
571
+ );
572
+ return;
573
+ }
574
+ const targetHost = target.host.replace(RegExp(/\/+$/), "");
575
+ const targetPrefix = target.href.substring(
576
+ "https://".length + target.host.length,
564
577
  );
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}`);
575
-
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;
617
-
618
- const outboundHeaders: OutgoingHttpHeaders = {
619
- ...[...Object.entries(inboundRequest.headers)]
620
- // host, connection and keep-alive are forbidden in http/2
621
- .filter(
622
- ([key]) => !["host", "connection", "keep-alive"].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
- };
639
-
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
- ),
578
+ const fullPath = `${targetPrefix}${unixNorm(
579
+ path.replace(RegExp(unixNorm(key)), ""),
580
+ )}`.replace(/^\/*/, "/");
581
+ const targetUrl = new URL(
582
+ `${target.protocol}//${targetHost}${fullPath}`,
665
583
  );
666
- });
667
584
 
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);
585
+ // phase: connection
586
+ let error: Buffer = null;
587
+ let http2IsSupported = !config.dontUseHttp2Downstream;
588
+ const outboundRequest: ClientHttp2Session =
589
+ target.protocol === "file:"
590
+ ? fileRequest(targetUrl)
591
+ : !http2IsSupported
592
+ ? null
593
+ : await Promise.race([
594
+ new Promise<ClientHttp2Session>(resolve => {
595
+ const result = connect(
596
+ targetUrl,
597
+ {
598
+ rejectUnauthorized: false,
599
+ protocol: target.protocol,
600
+ } as SecureClientSessionOptions,
601
+ (_, socketPath) => {
602
+ http2IsSupported =
603
+ http2IsSupported && !!(socketPath as any).alpnProtocol;
604
+ resolve(!http2IsSupported ? null : result);
605
+ },
606
+ );
607
+ (result as unknown as Http2Session).on(
608
+ "error",
609
+ (thrown: Error) => {
610
+ error =
611
+ http2IsSupported &&
612
+ Buffer.from(
613
+ errorPage(thrown, "connection", url, targetUrl),
614
+ );
615
+ },
616
+ );
617
+ }),
618
+ new Promise<ClientHttp2Session>(resolve =>
619
+ setTimeout(() => {
620
+ http2IsSupported = false;
621
+ resolve(null);
622
+ }, 3000),
623
+ ),
624
+ ]);
625
+ if (!(error instanceof Buffer)) error = null;
697
626
 
698
- outboundHttp1Request.on("error", thrown => {
699
- error = Buffer.from(errorPage(thrown, "request", url, targetUrl));
700
- resolve(null as IncomingMessage);
627
+ const outboundHeaders: OutgoingHttpHeaders = {
628
+ ...[...Object.entries(inboundRequest.headers)]
629
+ // host, connection and keep-alive are forbidden in http/2
630
+ .filter(
631
+ ([key]) =>
632
+ !["host", "connection", "keep-alive"].includes(
633
+ key.toLowerCase(),
634
+ ),
635
+ )
636
+ .reduce((acc: any, [key, value]) => {
637
+ acc[key] =
638
+ (acc[key] || "") +
639
+ (!Array.isArray(value) ? [value] : value)
640
+ .map(oneValue => oneValue.replace(url.hostname, targetHost))
641
+ .join(", ");
642
+ return acc;
643
+ }, {}),
644
+ origin: target.href,
645
+ referer: targetUrl.toString(),
646
+ ":authority": targetHost,
647
+ ":method": inboundRequest.method,
648
+ ":path": fullPath,
649
+ ":scheme": target.protocol.replace(":", ""),
650
+ };
651
+
652
+ const outboundExchange =
653
+ outboundRequest &&
654
+ !error &&
655
+ outboundRequest.request(outboundHeaders, {
656
+ endStream: config.ssl
657
+ ? !(
658
+ (inboundRequest as Http2ServerRequest)?.stream
659
+ ?.readableLength ?? true
660
+ )
661
+ : !(inboundRequest as IncomingMessage).readableLength,
701
662
  });
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;
710
663
 
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
- }
664
+ outboundExchange?.on("error", (thrown: Error) => {
665
+ const httpVersionSupported =
666
+ (thrown as ErrorWithErrno).errno === -505;
667
+ error = Buffer.from(
668
+ errorPage(
669
+ thrown,
670
+ "stream" +
671
+ (httpVersionSupported
672
+ ? " (error -505 usually means that the downstream service " +
673
+ "does not support this http version)"
674
+ : ""),
675
+ url,
676
+ targetUrl,
677
+ ),
678
+ );
679
+ });
725
680
 
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
- }
681
+ const http1RequestOptions: RequestOptions = {
682
+ hostname: target.hostname,
683
+ path: fullPath,
684
+ port: target.port,
685
+ protocol: target.protocol,
686
+ rejectUnauthorized: false,
687
+ method: inboundRequest.method,
688
+ headers: {
689
+ ...Object.assign(
690
+ {},
691
+ ...Object.entries(outboundHeaders)
692
+ .filter(
693
+ ([h]) =>
694
+ !h.startsWith(":") &&
695
+ h.toLowerCase() !== "transfer-encoding",
696
+ )
697
+ .map(([key, value]) => ({ [key]: value })),
698
+ ),
699
+ host: target.hostname,
700
+ },
701
+ };
702
+ const outboundHttp1Response: IncomingMessage =
703
+ !error &&
704
+ !http2IsSupported &&
705
+ target.protocol !== "file:" &&
706
+ (await new Promise(resolve => {
707
+ const outboundHttp1Request: ClientRequest =
708
+ target.protocol === "https:"
709
+ ? httpsRequest(http1RequestOptions, resolve)
710
+ : httpRequest(http1RequestOptions, resolve);
738
711
 
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
- );
712
+ outboundHttp1Request.on("error", thrown => {
713
+ error = Buffer.from(errorPage(thrown, "request", url, targetUrl));
714
+ resolve(null as IncomingMessage);
715
+ });
716
+ inboundRequest.on("data", chunk =>
717
+ outboundHttp1Request.write(chunk),
718
+ );
719
+ inboundRequest.on("end", () => outboundHttp1Request.end());
720
+ }));
721
+ // intriguingly, error is reset to "false" at this point, even if it was null
722
+ if (error) {
723
+ send(502, inboundResponse, error);
724
+ return;
725
+ } else error = null;
757
726
 
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"],
727
+ // phase : request body
728
+ if (
729
+ config.ssl && // http/2
730
+ (inboundRequest as Http2ServerRequest).stream &&
731
+ (inboundRequest as Http2ServerRequest).stream.readableLength &&
732
+ outboundExchange
733
+ ) {
734
+ (inboundRequest as Http2ServerRequest).stream.on("data", chunk =>
735
+ outboundExchange.write(chunk),
736
+ );
737
+ (inboundRequest as Http2ServerRequest).stream.on("end", () =>
738
+ outboundExchange.end(),
767
739
  );
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}`;
740
+ }
773
741
 
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
- ])),
742
+ if (
743
+ !config.ssl && // http1.1
744
+ (inboundRequest as IncomingMessage).readableLength &&
745
+ outboundExchange
746
+ ) {
747
+ (inboundRequest as IncomingMessage).on("data", chunk =>
748
+ outboundExchange.write(chunk),
793
749
  );
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;
750
+ (inboundRequest as IncomingMessage).on("end", () =>
751
+ outboundExchange.end(),
752
+ );
753
+ }
800
754
 
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
- }
755
+ // phase : response headers
756
+ const { outboundResponseHeaders } = await new Promise<{
757
+ outboundResponseHeaders: IncomingHttpHeaders &
758
+ IncomingHttpStatusHeader;
759
+ }>(resolve =>
760
+ outboundExchange
761
+ ? outboundExchange.on("response", headers => {
762
+ resolve({
763
+ outboundResponseHeaders: headers,
764
+ });
765
+ })
766
+ : !outboundExchange && outboundHttp1Response
767
+ ? resolve({
768
+ outboundResponseHeaders: outboundHttp1Response.headers,
769
+ })
770
+ : resolve({
771
+ outboundResponseHeaders: {},
772
+ }),
773
+ );
838
774
 
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) => {
775
+ const newUrl = !outboundResponseHeaders["location"]
776
+ ? null
777
+ : new URL(
778
+ outboundResponseHeaders["location"].startsWith("/")
779
+ ? `${target.href}${outboundResponseHeaders["location"].replace(
780
+ /^\/+/,
781
+ ``,
782
+ )}`
783
+ : outboundResponseHeaders["location"],
784
+ );
785
+ const newPath = !newUrl
786
+ ? null
787
+ : newUrl.href.substring(newUrl.origin.length);
788
+ const newTarget = url.origin;
789
+ const newTargetUrl = !newUrl ? null : `${newTarget}${newPath}`;
790
+
791
+ // phase : response body
792
+ const payloadSource = outboundExchange || outboundHttp1Response;
793
+ const payload: Buffer =
794
+ error ??
795
+ (await new Promise(resolve => {
796
+ let partialBody = Buffer.alloc(0);
797
+ if (!payloadSource) {
798
+ resolve(partialBody);
799
+ return;
800
+ }
801
+ (payloadSource as ClientHttp2Stream | Duplex).on(
802
+ "data",
803
+ (chunk: Buffer | string) =>
804
+ (partialBody = Buffer.concat([
805
+ partialBody,
806
+ typeof chunk === "string"
807
+ ? Buffer.from(chunk as string)
808
+ : (chunk as Buffer),
809
+ ])),
810
+ );
811
+ (payloadSource as any).on("end", () => {
812
+ resolve(partialBody);
813
+ });
814
+ }).then((payloadBuffer: Buffer) => {
815
+ if (!config.replaceResponseBodyUrls) return payloadBuffer;
816
+ if (!payloadBuffer.length) return payloadBuffer;
817
+
818
+ return (outboundResponseHeaders["content-encoding"] || "")
819
+ .split(",")
820
+ .reduce(
821
+ async (buffer: Promise<Buffer>, formatNotTrimed: string) => {
915
822
  const format = formatNotTrimed.trim().toLowerCase();
916
823
  const method =
917
824
  format === "gzip" || format === "x-gzip"
918
- ? gzip
825
+ ? gunzip
919
826
  : format === "deflate"
920
- ? deflate
827
+ ? inflate
921
828
  : format === "br"
922
- ? brotliCompress
829
+ ? brotliDecompress
923
830
  : format === "identity" || format === ""
924
831
  ? (
925
832
  input: Buffer,
@@ -928,90 +835,216 @@ const start = () => {
928
835
  callback(null, input);
929
836
  }
930
837
  : null;
931
- if (method === null)
932
- throw new Error(
933
- `${format} compression not supported by the proxy`,
838
+ if (method === null) {
839
+ send(
840
+ 502,
841
+ inboundResponse,
842
+ Buffer.from(
843
+ errorPage(
844
+ new Error(
845
+ `${format} compression not supported by the proxy`,
846
+ ),
847
+ "stream",
848
+ url,
849
+ targetUrl,
850
+ ),
851
+ ),
934
852
  );
853
+ return;
854
+ }
935
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
+ const openedBuffer = await buffer;
857
+ return await new Promise<Buffer>(resolve =>
858
+ method(openedBuffer, (err_1, data_1) => {
859
+ if (err_1) {
860
+ send(
861
+ 502,
862
+ inboundResponse,
863
+ Buffer.from(
864
+ errorPage(err_1, "stream", url, targetUrl),
865
+ ),
866
+ );
867
+ resolve(Buffer.from(""));
868
+ return;
869
+ }
870
+ resolve(data_1);
871
+ }),
944
872
  );
945
- }, Promise.resolve(Buffer.from(updatedBody))),
946
- );
947
- }));
948
-
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),
873
+ },
874
+ Promise.resolve(payloadBuffer),
971
875
  )
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
- );
876
+ .then((uncompressedBuffer: Buffer) => {
877
+ const fileTooBig = uncompressedBuffer.length > 1e7;
878
+ const fileHasSpecialChars = () =>
879
+ /[^\x00-\x7F]/.test(uncompressedBuffer.toString());
880
+ const contentTypeCanBeProcessed = [
881
+ "text/html",
882
+ "application/javascript",
883
+ "application/json",
884
+ ].some(allowedContentType =>
885
+ (outboundResponseHeaders["content-type"] ?? "").includes(
886
+ allowedContentType,
887
+ ),
888
+ );
889
+ const willReplace =
890
+ !fileTooBig &&
891
+ (contentTypeCanBeProcessed || !fileHasSpecialChars());
892
+ return !willReplace
893
+ ? uncompressedBuffer
894
+ : !config.replaceResponseBodyUrls
895
+ ? uncompressedBuffer.toString()
896
+ : Object.entries(config.mapping)
897
+ .reduce(
898
+ (inProgress, [path, mapping]) =>
899
+ path !== "" &&
900
+ !path.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)
901
+ ? inProgress
902
+ : inProgress.replace(
903
+ new RegExp(
904
+ mapping
905
+ .replace(/^file:\/\//, "")
906
+ .replace(/[*+?^${}()|[\]\\]/g, "")
907
+ .replace(/^https/, "https?") + "/*",
908
+ "ig",
909
+ ),
910
+ `https://${proxyHostnameAndPort}${path.replace(
911
+ /\/+$/,
912
+ "",
913
+ )}/`,
914
+ ),
915
+ uncompressedBuffer.toString(),
916
+ )
917
+ .split(`${proxyHostnameAndPort}/:`)
918
+ .join(`${proxyHostnameAndPort}:`)
919
+ .replace(
920
+ /\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,
921
+ `?protocol=ws${
922
+ config.ssl ? "s" : ""
923
+ }%3A&hostname=${proxyHostname}&port=${
924
+ config.port
925
+ }&pathname=${encodeURIComponent(
926
+ key.replace(/\/+$/, ""),
927
+ )}`,
928
+ );
929
+ })
930
+ .then((updatedBody: Buffer | string) =>
931
+ (outboundResponseHeaders["content-encoding"] || "")
932
+ .split(",")
933
+ .reduce(
934
+ (buffer: Promise<Buffer>, formatNotTrimed: string) => {
935
+ const format = formatNotTrimed.trim().toLowerCase();
936
+ const method =
937
+ format === "gzip" || format === "x-gzip"
938
+ ? gzip
939
+ : format === "deflate"
940
+ ? deflate
941
+ : format === "br"
942
+ ? brotliCompress
943
+ : format === "identity" || format === ""
944
+ ? (
945
+ input: Buffer,
946
+ callback: (err?: Error, data?: Buffer) => void,
947
+ ) => {
948
+ callback(null, input);
949
+ }
950
+ : null;
951
+ if (method === null)
952
+ throw new Error(
953
+ `${format} compression not supported by the proxy`,
954
+ );
987
955
 
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),
956
+ return buffer.then(
957
+ data =>
958
+ new Promise<Buffer>(resolve =>
959
+ method(data, (err, data) => {
960
+ if (err) throw err;
961
+ resolve(data);
962
+ }),
963
+ ),
964
+ );
965
+ },
966
+ Promise.resolve(Buffer.from(updatedBody)),
967
+ ),
968
+ );
969
+ }));
970
+
971
+ // phase : inbound response
972
+ const responseHeaders = {
973
+ ...Object.entries({
974
+ ...outboundResponseHeaders,
975
+ ...(config.replaceResponseBodyUrls
976
+ ? { ["content-length"]: `${payload.byteLength}` }
977
+ : {}),
978
+ ...(config.disableWebSecurity
979
+ ? {
980
+ ["content-security-policy"]: "report only",
981
+ ["access-control-allow-headers"]: "*",
982
+ ["access-control-allow-method"]: "*",
983
+ ["access-control-allow-origin"]: "*",
984
+ }
985
+ : {}),
986
+ })
987
+ .filter(
988
+ ([h]) =>
989
+ !h.startsWith(":") &&
990
+ h.toLowerCase() !== "transfer-encoding" &&
991
+ h.toLowerCase() !== "connection" &&
992
+ h.toLowerCase() !== "keep-alive",
993
+ )
994
+ .reduce((acc: any, [key, value]: [string, string | string[]]) => {
995
+ const allSubdomains = targetHost
996
+ .split("")
997
+ .map(
998
+ (_, i) =>
999
+ targetHost.substring(i).startsWith(".") &&
1000
+ targetHost.substring(i),
1001
+ )
1002
+ .filter(subdomain => subdomain) as string[];
1003
+ const transformedValue = [targetHost]
1004
+ .concat(allSubdomains)
1005
+ .reduce(
1006
+ (acc1, subDomain) =>
1007
+ (!Array.isArray(acc1) ? [acc1] : (acc1 as string[])).map(
1008
+ oneElement => {
1009
+ return typeof oneElement === "string"
1010
+ ? oneElement.replace(
1011
+ `Domain=${subDomain}`,
1012
+ `Domain=${url.hostname}`,
1013
+ )
1014
+ : oneElement;
1015
+ },
1016
+ ),
1017
+ value,
1018
+ );
1019
+
1020
+ acc[key] = (acc[key] || []).concat(transformedValue);
1021
+ return acc;
1022
+ }, {}),
1023
+ ...(newTargetUrl ? { location: [newTargetUrl] } : {}),
1024
+ };
1025
+ try {
1026
+ Object.entries(responseHeaders).forEach(
1027
+ ([headerName, headerValue]) =>
1028
+ headerValue &&
1029
+ inboundResponse.setHeader(headerName, headerValue as string),
1030
+ );
1031
+ } catch (e) {
1032
+ // ERR_HTTP2_HEADERS_SENT
1033
+ }
1034
+ inboundResponse.writeHead(
1035
+ outboundResponseHeaders[":status"] ||
1036
+ outboundHttp1Response.statusCode ||
1037
+ 200,
1038
+ config.ssl
1039
+ ? undefined // statusMessage is discarded in http/2
1040
+ : outboundHttp1Response.statusMessage || "Status read from http/2",
1041
+ responseHeaders,
998
1042
  );
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)
1043
+ if (payload) inboundResponse.end(payload);
1044
+ else inboundResponse.end();
1045
+ },
1046
+ ) as Server
1047
+ )
1015
1048
  .addListener("error", (err: Error) => {
1016
1049
  if ((err as ErrorWithErrno).code === "EACCES")
1017
1050
  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.44",
3
+ "version": "0.0.46",
4
4
  "main": "index.ts",
5
5
  "private": false,
6
6
  "keywords": [
@@ -24,7 +24,7 @@
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": "^18.15.3",
27
+ "@types/node": "^18.15.5",
28
28
  "terser": "^5.16.6",
29
29
  "typescript": "^5.0.2"
30
30
  },