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