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 +14 -11
- package/dist/localTraffic.js +1 -1
- package/index.ts +500 -467
- package/package.json +2 -2
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
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"),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?"":"[36m"}${`${t.getHours()}`.padStart(2,"0")}${e?":":"[33m:[36m"}${`${t.getMinutes()}`.padStart(2,"0")}${e?":":"[33m:[36m"}${`${t.getSeconds()}`.padStart(2,"0")}${e?"":"[0m"}`})(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)} ⎹[0m`:e}`)},g=e=>{u(`[48;5;52m⎸${l.PORT} ${e.port.toString().padStart(5)} [48;5;53m⎸${l.INBOUND} ${e.ssl?"H/2 ":"H1.1"} [48;5;54m⎸${l.OUTBOUND} ${e.dontUseHttp2Downstream?"H1.1":"H/2 "}⎹[48;5;55m⎸${l.RULES}${Object.keys(h.mapping).length.toString().padStart(3)}⎹[48;5;56m⎸${h.replaceResponseBodyUrls?l.BODY_REPLACEMENT:l.NO}⎹[48;5;57m⎸${h.websocket?l.WEBSOCKET:l.NO}⎹[48;5;93m⎸${h.simpleLogs?l.NO:l.COLORED}⎹[0m`)},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,"/")}</i></p><ul class="list-group"><li class="list-group-item">📁<a href="${e.pathname.endsWith("/")?"..":"."}"><parent></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 ⓘ 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>`,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?"":"[36m"}${`${t.getHours()}`.padStart(2,"0")}${e?":":"[33m:[36m"}${`${t.getMinutes()}`.padStart(2,"0")}${e?":":"[33m:[36m"}${`${t.getSeconds()}`.padStart(2,"0")}${e?"":"[0m"}`})(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)} ⎹[0m`:e}`)},g=e=>{m(`[48;5;52m⎸${l.PORT} ${e.port.toString().padStart(5)} [48;5;53m⎸${l.INBOUND} ${e.ssl?"H/2 ":"H1.1"} [48;5;54m⎸${l.OUTBOUND} ${e.dontUseHttp2Downstream?"H1.1":"H/2 "}⎹[48;5;55m⎸${l.RULES}${Object.keys(h.mapping).length.toString().padStart(3)}⎹[48;5;56m⎸${h.replaceResponseBodyUrls?l.BODY_REPLACEMENT:l.NO}⎹[48;5;57m⎸${h.websocket?l.WEBSOCKET:l.NO}⎹[48;5;93m⎸${h.simpleLogs?l.NO:l.COLORED}⎹[48;5;98m⎸${h.disableWebSecurity?l.NO:l.SHIELD}⎹[0m`)},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,"/")}</i></p><ul class="list-group"><li class="list-group-item">📁<a href="${e.pathname.endsWith("/")?"..":"."}"><parent></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 ⓘ 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>`,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(
|
|
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
|
|
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 = (
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
(
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
?
|
|
825
|
+
? gunzip
|
|
919
826
|
: format === "deflate"
|
|
920
|
-
?
|
|
827
|
+
? inflate
|
|
921
828
|
: format === "br"
|
|
922
|
-
?
|
|
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
|
-
|
|
933
|
-
|
|
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
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
},
|
|
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
|
-
.
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
)
|
|
985
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
|
|
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.
|
|
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.
|
|
27
|
+
"@types/node": "^18.15.5",
|
|
28
28
|
"terser": "^5.16.6",
|
|
29
29
|
"typescript": "^5.0.2"
|
|
30
30
|
},
|