local-traffic 0.0.62 → 0.0.63
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 +5 -3
- package/dist/localTraffic.js +1 -1
- package/index.ts +257 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ That is a secure http/2 (or insecure http1.1) reverse-proxy installed on your ma
|
|
|
5
5
|
- with 0 transitive dependency
|
|
6
6
|
- with 1 install step
|
|
7
7
|
- with a startup time of a few milliseconds
|
|
8
|
-
- with one
|
|
8
|
+
- with one 30kb index.js file
|
|
9
9
|
|
|
10
10
|
How simple is that ?
|
|
11
11
|
|
|
@@ -30,6 +30,7 @@ npx local-traffic
|
|
|
30
30
|
"mapping": {
|
|
31
31
|
"/npm/": "https://www.npmjs.com/",
|
|
32
32
|
"/my-static-webapp/": "file:///home/user/projects/my-static-webapp/",
|
|
33
|
+
"/config/": "config://",
|
|
33
34
|
"/logs/": "logs://",
|
|
34
35
|
"/jquery-local/jquery.js": {
|
|
35
36
|
"replaceBody": "https://mycdn.net/jquery/jquery-3.6.4.js",
|
|
@@ -46,8 +47,9 @@ npx local-traffic
|
|
|
46
47
|
3. Go to [http://localhost:8080/npm/](http://localhost:8080/npm) with your browser
|
|
47
48
|
4. Go to [http://localhost:8080/my-static-webapp/index.html](http://localhost:8080/my-static-webapp/index.html) with your browser (given your project name is my-static-webapp, but I am not 100% sure)
|
|
48
49
|
5. Go to [http://localhost:8080/logs/](http://localhost:8080/logs/) to watch the request logs
|
|
49
|
-
6.
|
|
50
|
-
7. Your
|
|
50
|
+
6. Go to [http://localhost:8080/config/](http://localhost:8080/config/) to change the config in a web editor
|
|
51
|
+
7. Your page will use /jquery-local/jquery.js instead of the CDN asset, and will serve the file from your hard drive
|
|
52
|
+
8. Your server now proxies the mapping that you have configured
|
|
51
53
|
|
|
52
54
|
## usage
|
|
53
55
|
|
package/dist/localTraffic.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var e=this&&this.__awaiter||function(e,t,n,o){return new(n||(n=Promise))((function(r,s){function a(e){try{l(o.next(e))}catch(e){s(e)}}function i(e){try{l(o.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,i)}l((o=o.apply(e,t||[])).next())}))};Object.defineProperty(exports,"__esModule",{value:!0});const t=require("http2"),n=require("http"),o=require("https"),r=require("url"),s=require("fs"),a=require("zlib"),i=require("path"),l=require("crypto"),c=require("process");var d,p,u;!function(e){e[e.ERROR=124]="ERROR",e[e.INFO=93]="INFO",e[e.WARNING=172]="WARNING"}(d||(d={})),function(e){e.INBOUND="↘️ ",e.PORT="☎️ ",e.OUTBOUND="↗️ ",e.RULES="🔗",e.REWRITE="✒️ ",e.RESTART="🔄",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="☠️ "}(p||(p={})),function(e){e.INBOUND="INBOUND",e.OUTBOUND="OUTBOUND"}(u||(u={}));const h=(0,i.resolve)(process.env.HOME,".local-traffic.json"),m=(0,i.resolve)(process.cwd(),process.argv.slice(-1)[0].endsWith(".json")?process.argv.slice(-1)[0]:h),f={mapping:{"/logs/":"logs://"},port:8080,replaceRequestBodyUrls:!1,replaceResponseBodyUrls:!1,dontUseHttp2Downstream:!1,dontTranslateLocationHeader:!1,simpleLogs:!1,websocket:!0,disableWebSecurity:!1};let g,y,v=[];const b=e=>e===d.ERROR?"error":e===d.WARNING?"warning":"info",R=(e,t,n)=>{const o=(null==g?void 0:g.simpleLogs)||v.length?e.replace(/⎸/g,"|").replace(/⎹/g,"|").replace(/\u001b\[[^m]*m/g,"").replace(new RegExp(p.INBOUND,"g"),"inbound:").replace(new RegExp(p.PORT,"g"),"port:").replace(new RegExp(p.OUTBOUND,"g"),"outbound:").replace(new RegExp(p.RULES,"g"),"rules:").replace(new RegExp(p.NO,"g"),"").replace(new RegExp(p.REWRITE,"g"),"+rewrite").replace(new RegExp(p.WEBSOCKET,"g"),"websocket").replace(new RegExp(p.SHIELD,"g"),"web-security").replace(/\|+/g,"|"):e;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"}`})(null==g?void 0:g.simpleLogs)} ${(null==g?void 0:g.simpleLogs)?o:t?`[48;5;${t}m⎸ ${process.stdout.isTTY&&n||""} ${e.padEnd(40)} ⎹[0m`:e}`),w({event:o,level:b(t)})},O=(e,t)=>{const n=Array(4).fill(0).map((()=>t?Math.floor(256*Math.random()):0)),o=[...e.substring(0,65536)].map(((e,t)=>e.charCodeAt(0)^n[3&t])),r=Math.min(65535,e.length),s=e.length<126?Buffer.from(Uint8Array.from([129,(t?128:0)+r]).buffer):Buffer.concat([Buffer.from(Uint8Array.from([129,126|(t?128:0)]).buffer),Buffer.from(Uint8Array.from([r>>8]).buffer),Buffer.from(Uint8Array.from([255&r]).buffer)]),a=Buffer.from(Int8Array.from(n).buffer),i=Buffer.from(Int8Array.from(o).buffer);return Buffer.concat(t?[s,a,i]:[s,i])},w=e=>{if(!v.length)return;const t=JSON.stringify(e),n=new Set(v.map((e=>e.wantsMask))),o=n.has(!1)&&O(t,!1),r=n.has(!0)&&O(t,!0);v.forEach((e=>{e.wantsMask?e.stream.write(r,"ascii",(()=>{})):e.stream.write(o,"ascii",(()=>{}))}))},$=e=>{R(`[48;5;52m⎸${p.PORT} ${e.port.toString().padStart(5)} [48;5;53m⎸${p.OUTBOUND} ${e.dontUseHttp2Downstream?"H1.1":"H/2 "}${e.replaceRequestBodyUrls?p.REWRITE:" "}⎹⎸${p.INBOUND} ${e.ssl?"H/2 ":"H1.1"}${e.replaceResponseBodyUrls?p.REWRITE:" "}⎹[48;5;54m[48;5;55m⎸${p.RULES}${Object.keys(g.mapping).length.toString().padStart(3)}⎹[48;5;56m⎸${g.websocket?p.WEBSOCKET:p.NO}⎹[48;5;57m⎸${g.simpleLogs?p.NO:p.COLORED}⎹[48;5;93m⎸${g.disableWebSecurity?p.NO:p.SHIELD}⎹[0m`)},E=(t=!0)=>e(void 0,void 0,void 0,(function*(){return new Promise((e=>(0,s.readFile)(m,((n,o)=>{n&&!t&&R("config error. Using default value",d.ERROR,p.ERROR_1);try{g=Object.assign({},f,JSON.parse((o||"{}").toString()))}catch(t){return R("config syntax incorrect, aborting",d.ERROR,p.ERROR_2),g=g||Object.assign({},f),void e(g)}g.mapping[""]||R('default mapping "" not provided.',d.WARNING,p.ERROR_3),n&&"ENOENT"===n.code&&t&&m===h?(0,s.writeFile)(m,JSON.stringify(f),(t=>{t?R("config file NOT created",d.ERROR,p.ERROR_4):R("config file created",d.INFO,p.COLORED),e(g)})):e(g)})))).then((()=>{t&&(0,s.watchFile)(m,N)}))})),N=()=>e(void 0,void 0,void 0,(function*(){const e=Object.assign({},g);if(yield E(!1),isNaN(g.port)||g.port>65535||g.port<0)return g=e,void R("port number invalid. Not refreshing",d.ERROR,p.PORT);if("object"!=typeof g.mapping)return g=e,void R("mapping should be an object. Aborting",d.ERROR,p.ERROR_5);if(g.replaceRequestBodyUrls!==e.replaceRequestBodyUrls&&R(`request body url ${g.replaceRequestBodyUrls?"":"NO "}rewriting`,d.INFO,p.REWRITE),g.replaceResponseBodyUrls!==e.replaceResponseBodyUrls&&R(`response body url ${g.replaceResponseBodyUrls?"":"NO "}rewriting`,d.INFO,p.REWRITE),g.dontTranslateLocationHeader!==e.dontTranslateLocationHeader&&R(`response location header ${g.dontTranslateLocationHeader?"NO ":""}translation`,d.INFO,p.REWRITE),g.dontUseHttp2Downstream!==e.dontUseHttp2Downstream&&R(`http/2 ${g.dontUseHttp2Downstream?"de":""}activated downstream`,d.INFO,p.OUTBOUND),g.disableWebSecurity!==e.disableWebSecurity&&R(`web security ${g.disableWebSecurity?"de":""}activated`,d.INFO,p.SHIELD),g.websocket!==e.websocket&&R(`websocket ${g.websocket?"":"de"}activated`,d.INFO,p.WEBSOCKET),g.simpleLogs!==e.simpleLogs&&R("simple logs "+(g.simpleLogs?"on":"off"),d.INFO,p.COLORED),Object.keys(g.mapping).join("\n")!==Object.keys(e.mapping).join("\n")&&R(`${Object.keys(g.mapping).length.toString().padStart(5)} loaded mapping rules`,d.INFO,p.RULES),g.port!==e.port&&R(`port changed from ${e.port} to ${g.port}`,d.INFO,p.PORT),g.ssl&&!e.ssl&&R("ssl configuration added",d.INFO,p.INBOUND),!g.ssl&&e.ssl&&R("ssl configuration removed",d.INFO,p.INBOUND),g.port!==e.port||JSON.stringify(g.ssl)!==JSON.stringify(e.ssl)){R("restarting server",d.INFO,p.RESTART),yield Promise.all(v.map((e=>new Promise((t=>e.stream.end(t)))))),v=[];(yield Promise.race([new Promise((e=>y?y.close(e):e(void 0))).then((()=>!0)),new Promise((e=>setTimeout(e,5e3))).then((()=>!1))]))||R("error during restart (websockets ?)",d.WARNING,p.RESTART),C()}else $(g)})),S=e=>""==e?"":(0,i.normalize)(e).replace(/\\/g,"/"),T=e=>{const t=(0,i.resolve)("/",e.hostname,...e.pathname.replace(/[?#].*$/,"").replace(/^\/+/,"").split("/"));return{error:null,data:null,hasRun:!1,run:function(){return this.hasRun?Promise.resolve():new Promise((n=>(0,s.readFile)(t,((o,r)=>{if(this.hasRun=!0,!o||"EISDIR"!==o.code)return this.error=o,this.data=r,void n(void 0);(0,s.readdir)(t,((t,o)=>{this.error=t,this.data=o,t?n(void 0):Promise.all(o.map((t=>new Promise((n=>(0,s.lstat)((0,i.resolve)(e.pathname,t),((e,o)=>n([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=`${H(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>`,n(void 0)}))}))}))))},events:{},on:function(e,n){return this.events[e]=n,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}}},B=e=>({error:null,data:null,run:function(){return new Promise((t=>{this.data=`${H(128250,"logs","")}\n<nav class="navbar navbar-expand-lg navbar-dark bg-primary nav-fill">\n <div class="container-fluid">\n <ul class="navbar-nav">\n <li class="nav-item">\n <a class="nav-link active" aria-current="page" href="javascript:show(0)">Access</a>\n </li>\n <li class="nav-item">\n <a class="nav-link" href="javascript:show(1)">Proxy</a>\n </li>\n </ul>\n <span class="navbar-text">\n Limit : <select id="limit" onchange="javascript:cleanup()"><option value="-1">0 (clear)</option><option value="10">10</option>\n <option value="50">50</option><option value="100">100</option><option value="200">200</option>\n <option selected="selected" value="500">500</option><option value="0">Infinity (discouraged)</option>\n </select> rows\n </span>\n </div>\n</nav>\n<table id="table-access" class="table table-striped" style="display: block; width: 100%; overflow-y: auto">\n <thead>\n <tr>\n <th scope="col">...</th>\n <th scope="col">Date</th>\n <th scope="col">Level</th>\n <th scope="col">Protocol</th>\n <th scope="col">Method</th>\n <th scope="col">Status</th>\n <th scope="col">Duration</th>\n <th scope="col">Upstream Path</th>\n <th scope="col">Downstream Path</th>\n </tr>\n </thead>\n <tbody id="access">\n </tbody>\n</table>\n<table id="table-proxy" class="table table-striped" style="display: none; width: 100%; overflow-y: auto">\n <thead>\n <tr>\n <th scope="col">Date</th>\n <th scope="col">Level</th>\n <th scope="col">Message</th>\n </tr>\n </thead>\n <tbody id="proxy">\n </tbody>\n</table>\n<script type="text/javascript">\n function start() {\n document.getElementById('table-access').style.height =\n (document.documentElement.clientHeight - 150) + 'px';\n document.getElementById('table-proxy').style.height =\n (document.documentElement.clientHeight - 150) + 'px';\n const socket = new WebSocket("ws${g.ssl?"s":""}://${e}/local-traffic-logs");\n socket.onmessage = function(event) {\n let data = event.data\n let uniqueHash;\n try {\n const { uniqueHash: uniqueHash1, ...data1 } = JSON.parse(event.data);\n data = data1;\n uniqueHash = uniqueHash1;\n } catch(e) { }\n const time = new Date().toISOString().split('T')[1].replace('Z', '');\n const replay = uniqueHash ? '<button data-uniquehash="' + uniqueHash + '" onclick="javascript:replay(event)" ' +\n 'type="button" class="btn btn-primary"' + \n (uniqueHash === 'N/A' ? ' disabled="disabled"' : '') + '>🔁</button>' : '';\n if(data.statusCode && uniqueHash) {\n const color = Math.floor(data.statusCode / 100) === 1 ? "info" :\n Math.floor(data.statusCode / 100) === 2 ? "success" :\n Math.floor(data.statusCode / 100) === 3 ? "dark" :\n Math.floor(data.statusCode / 100) === 4 ? "warning" :\n Math.floor(data.statusCode / 100) === 5 ? "danger" :\n "secondary";\n const statusCodeColumn = document.querySelector("#event-" + data.randomId + " .statusCode");\n if (statusCodeColumn)\n statusCodeColumn.innerHTML = '<span class="badge bg-' + color + '">' + data.statusCode + '</span>';\n\n const durationColumn = document.querySelector("#event-" + data.randomId + " .duration");\n if (durationColumn) {\n const duration = data.duration > 10000 ? Math.floor(data.duration / 1000) + 's' :\n data.duration + 'ms';\n durationColumn.innerHTML = duration;\n }\n\n const protocolColumn = document.querySelector("#event-" + data.randomId + " .protocol");\n if (protocolColumn) {\n protocolColumn.innerHTML = data.protocol;\n }\n\n const replayColumn = document.querySelector("#event-" + data.randomId + " .replay");\n if (replayColumn) {\n replayColumn.innerHTML = replay;\n }\n } else if (uniqueHash) {\n document.getElementById("access")\n .insertAdjacentHTML('afterbegin', '<tr id="event-' + data.randomId + '">' +\n '<td scope="col" class="replay">' + replay + '</td>' +\n '<td scope="col">' + time + '</td>' +\n '<td scope="col">' + (data.level || 'info')+ '</td>' + \n '<td scope="col" class="protocol">' + data.protocol + '</td>' + \n '<td scope="col">' + data.method + '</td>' + \n '<td scope="col" class="statusCode"><span class="badge bg-secondary">...</span></td>' +\n '<td scope="col" class="duration">⏱</td>' +\n '<td scope="col">' + data.upstreamPath + '</td>' + \n '<td scope="col">' + data.downstreamPath + '</td>' + \n '</tr>');\n } else if(data.event) {\n document.getElementById("proxy")\n .insertAdjacentHTML('afterbegin', '<tr><td scope="col">' + time + '</td>' +\n '<td scope="col">' + (data.level || 'info')+ '</td>' + \n '<td scope="col">' + data.event + '</td></tr>');\n }\n cleanup();\n };\n socket.onerror = function(error) {\n console.log(\`[error] \${error}\`);\n setTimeout(start, 5000);\n };\n };\n function show(id) {\n [...document.querySelectorAll('table')].forEach((table, index) => {\n table.style.display = index === id ? 'block': 'none'\n });\n [...document.querySelectorAll('.navbar-nav .nav-item .nav-link')].forEach((link, index) => {\n if (index === id) { link.classList.add('active') } else link.classList.remove('active');\n });\n }\n function cleanup() {\n const currentLimit = parseInt(document.getElementById('limit').value)\n for (let table of ['access', 'proxy']) {\n while (currentLimit && document.getElementById(table).childNodes.length && \n document.getElementById(table).childNodes.length > currentLimit) {\n [...document.getElementById(table).childNodes].slice(-1)[0].remove();\n }\n }\n }\n function replay(event) {\n const uniqueHash = event.target.dataset.uniquehash;\n const { method, url, headers, body } = JSON.parse(atob(uniqueHash));\n fetch(url, {\n method,\n headers,\n body: !body.data || !body.data.length \n ? undefined\n : new TextDecoder().decode(new Int8Array(body.data))\n });\n }\n window.addEventListener("DOMContentLoaded", start);\n<\/script>\n</body></html>`,t(void 0)}))},events:{},on:function(e,t){return this.events[e]=t,this.run().then((()=>{"response"===e&&this.events.response({Server:"local","Content-Type":"text/html"},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}}),H=(e,t,n)=>`<!doctype html>\n<html lang="en">\n<head>\n<title>&#x${e.toString(16)}; local-traffic ${t} | ${n}</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/>`,I=(e,t,n,o)=>`${H(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>${n}</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>`,U=(t,n,o)=>e(void 0,void 0,void 0,(function*(){var r,s;return(null!==(s=null===(r=n["content-encoding"])||void 0===r?void 0:r.toString())&&void 0!==s?s:"").split(",").reduce(((t,n)=>e(void 0,void 0,void 0,(function*(){const e=n.trim().toLowerCase(),o="gzip"===e||"x-gzip"===e?a.gunzip:"deflate"===e?a.inflate:"br"===e?a.brotliDecompress:"identity"===e||""===e?(e,t)=>{t(null,e)}:null;if(null===o)throw new Error(`${e} compression not supported by the proxy`);const r=yield t;return yield new Promise(((e,t)=>o(r,((n,o)=>{n&&t(n),e(o)}))))}))),Promise.resolve(t)).then((e=>{const t=e.length>1e7,r=["text/html","application/javascript","application/json"].some((e=>{var t;return(null!==(t=n["content-type"])&&void 0!==t?t:"").toString().includes(e)}));return!t&&(r||!/[^\x00-\x7F]/.test(e.toString()))?g.replaceResponseBodyUrls?x(e.toString(),o.direction,o.proxyHostnameAndPort).replace(/\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,`?protocol=ws${g.ssl?"s":""}%3A&hostname=${o.proxyHostname}&port=${g.port}&pathname=${encodeURIComponent(o.key.replace(/\/+$/,""))}`):e.toString():e})).then((e=>{var t,o;return(null!==(o=null===(t=n["content-encoding"])||void 0===t?void 0:t.toString())&&void 0!==o?o:"").split(",").reduce(((e,t)=>{const n=t.trim().toLowerCase(),o="gzip"===n||"x-gzip"===n?a.gzip:"deflate"===n?a.deflate:"br"===n?a.brotliCompress:"identity"===n||""===n?(e,t)=>{t(null,e)}:null;if(null===o)throw new Error(`${n} compression not supported by the proxy`);return e.then((e=>new Promise((t=>o(e,((e,n)=>{if(e)throw e;t(n)}))))))}),Promise.resolve(Buffer.from(e)))}))})),x=(e,t,n)=>Object.entries(g.mapping).map((([e,t])=>[e,"string"==typeof t?t:t.replaceBody])).reduce(((e,[o,r])=>r.startsWith("logs:")||""!==o&&!o.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)?e:t===u.INBOUND?e.replace(new RegExp(r.replace(/^(file|logs):\/\//,"").replace(/[*+?^${}()|[\]\\]/g,"").replace(/^https/,"https?")+"/*","ig"),`http${g.ssl?"s":""}://${n}${o.replace(/\/+$/,"")}/`):e.split(`http${g.ssl?"s":""}://${n}${o.replace(/\/+$/,"")}`).join(r)),e).split(`${n}/:`).join(`${n}:`),j=(e,t,n)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":n.length}),t.end(n)},L=e=>{var t,n,o;const s=(null!==(o=null!==(n=null===(t=e.headers[":authority"])||void 0===t?void 0:t.toString())&&void 0!==n?n:e.headers.host)&&void 0!==o?o:"localhost").replace(/:.*/,""),a=e.headers[":authority"]||`${e.headers.host}${e.headers.host.match(/:[0-9]+$/)?"":80!==g.port||g.ssl?443===g.port&&g.ssl?"":`:${g.port}`:""}`,i=new r.URL(`http${g.ssl?"s":""}://${a}${e.url}`),l=i.href.substring(i.origin.length),[c,d]=Object.entries(Object.assign({},Object.assign({},...Object.entries(g.mapping).map((([e,t])=>({[e]:new r.URL(S("string"==typeof t?t:t.downstreamUrl))})))))).find((([e])=>l.match(RegExp(e.replace(/^\//,"^/")))))||[];return{proxyHostname:s,proxyHostnameAndPort:a,url:i,path:l,key:c,target:d}},C=()=>{y=(g.ssl?t.createSecureServer.bind(null,Object.assign(Object.assign({},g.ssl),{allowHTTP1:!0})):n.createServer)(((s,a)=>e(void 0,void 0,void 0,(function*(){var e,i,h,f;if(!s.headers.host&&!s.headers[":authority"])return void j(400,a,Buffer.from(I(new Error("client must supply a 'host' header"),"proxy",new r.URL(`http${g.ssl?"s":""}://unknowndomain${s.url}`))));const{proxyHostname:y,proxyHostnameAndPort:b,url:O,path:$,key:E,target:N}=L(s);if(!N)return void j(502,a,Buffer.from(I(new Error(`No mapping found in config file ${m}`),"proxy",O)));const H=N.host.replace(RegExp(/\/+$/),""),C=`${N.href.substring(8+N.host.length)}${S($.replace(RegExp(S(E)),""))}`.replace(/^\/*/,"/"),P=new r.URL(`${N.protocol}//${H}${C}`);let A=!g.dontUseHttp2Downstream;const D=(0,l.randomBytes)(20).toString("hex");w({level:"info",protocol:A?"HTTP/2":"HTTP1.1",method:s.method,upstreamPath:$,downstreamPath:P.href,randomId:D,uniqueHash:"N/A"});const q=c.hrtime.bigint();let k=null;const W="file:"===N.protocol?T(P):"logs:"===N.protocol?B(b):A?yield Promise.race([new Promise((e=>{const n=(0,t.connect)(P,{rejectUnauthorized:!1,protocol:N.protocol},((t,o)=>{A=A&&!!o.alpnProtocol,e(A?n:null)}));n.on("error",(e=>{k=A&&Buffer.from(I(e,"connection",O,P))}))})),new Promise((e=>setTimeout((()=>{A=!1,e(null)}),3e3)))]):null;k instanceof Buffer||(k=null);const M=null==s?void 0:s.readableLength,F=null===(e=null==s?void 0:s.stream)||void 0===e?void 0:e.readableLength;let _=null;const z=g.replaceRequestBodyUrls||v.length;if(z){const e=null!==(i=null==s?void 0:s.stream)&&void 0!==i?i:s;let t=Buffer.from([]);const n=!((g.ssl&&0===F||!g.ssl&&0===M)&&("0"===s.headers["content-length"]||void 0===s.headers["content-length"]));yield Promise.race([new Promise((e=>setTimeout(e,1e4))),new Promise((o=>{n||o(void 0),e.on("data",(e=>{t=Buffer.concat([t,e])})),e.on("end",o),e.on("error",o)}))]),n&&!t.length&&R(`body replacement error ${$.slice(-17)}`,d.WARNING,p.ERROR_4),_=yield U(t,s.headers,{proxyHostnameAndPort:b,proxyHostname:y,key:E,direction:u.OUTBOUND})}const G=Object.assign(Object.assign({},[...Object.entries(s.headers)].filter((([e])=>!["host","connection","keep-alive"].includes(e.toLowerCase()))).reduce(((e,[t,n])=>(e[t]=(e[t]||"")+(Array.isArray(n)?n:[n]).map((e=>e.replace(O.hostname,H))).join(", "),e)),{})),{origin:N.href,referer:P.toString(),"content-length":null!==(f=null!==(h=null==_?void 0:_.length)&&void 0!==h?h:s.headers["content-length"])&&void 0!==f?f:0,":authority":H,":method":s.method,":path":C,":scheme":N.protocol.replace(":","")}),J=W&&!k&&W.request(G,{endStream:g.ssl?!(null==F||F):!M});null==J||J.on("error",(e=>{const t=-505===e.errno;k=Buffer.from(I(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),O,P))}));const K={hostname:N.hostname,path:C,port:N.port,protocol:N.protocol,rejectUnauthorized:!1,method:s.method,headers:Object.assign(Object.assign({},Object.assign({},...Object.entries(G).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase())).map((([e,t])=>({[e]:t}))))),{host:N.hostname})},Z=!k&&!A&&!["file:","logs:"].includes(N.protocol)&&(yield new Promise((e=>{const t="https:"===N.protocol?(0,o.request)(K,e):(0,n.request)(K,e);t.on("error",(t=>{k=Buffer.from(I(t,"request",O,P)),e(null)})),z&&(t.write(_),t.end()),z||(s.on("data",(e=>t.write(e))),s.on("end",(()=>t.end())))})));if(k)return void j(502,a,k);k=null,g.ssl&&F&&J&&(z&&(J.write(_),J.end()),z||(s.stream.on("data",(e=>{J.write(e)})),s.stream.on("end",(()=>J.end())))),!g.ssl&&M&&J&&(z&&(J.write(_),J.end()),z||(s.on("data",(e=>{J.write(e)})),s.on("end",(()=>J.end()))));const{outboundResponseHeaders:V}=yield new Promise((e=>J?J.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!J&&Z?{outboundResponseHeaders:Z.headers}:{outboundResponseHeaders:{}}))),Y=V.location?new r.URL(V.location.startsWith("/")?`${N.href}${V.location.replace(/^\/+/,"")}`:V.location.replace(/^file:\/+/,"file:///").replace(/^(http)(s?):\/+/,"$1$2://")):null,Q=g.replaceResponseBodyUrls&&Y?new r.URL(x(Y.href,u.INBOUND,b).replace(/^(logs:|file:)\/+/,"")):Y,X=Y?Q.origin!==Y.origin||g.dontTranslateLocationHeader?Q:`${O.origin}${Q.href.substring(Q.origin.length)}`:Y,ee=J||Z,te=null!=k?k:yield new Promise((e=>{let t=Buffer.alloc(0);ee?(ee.on("data",(e=>t=Buffer.concat([t,"string"==typeof e?Buffer.from(e):e]))),ee.on("end",(()=>{e(t)}))):e(t)})).then((e=>g.replaceResponseBodyUrls&&e.length?U(e,V,{proxyHostnameAndPort:b,proxyHostname:y,key:E,direction:u.INBOUND}).catch((e=>(j(502,a,Buffer.from(I(e,"stream",O,P))),Buffer.from("")))):e)),ne=Object.assign(Object.assign({},Object.entries(Object.assign(Object.assign(Object.assign({},V),g.replaceResponseBodyUrls?{"content-length":`${te.byteLength}`}:{}),g.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,n])=>{const o=H.split("").map(((e,t)=>H.substring(t).startsWith(".")&&H.substring(t))).filter((e=>e)),r=[H].concat(o).reduce(((e,t)=>(Array.isArray(e)?e:[e]).map((e=>"string"==typeof e?e.replace(`Domain=${t}`,`Domain=${O.hostname}`):e))),n);return e[t]=(e[t]||[]).concat(r),e}),{})),X?{location:[X]}:{});try{Object.entries(ne).forEach((([e,t])=>t&&a.setHeader(e,t)))}catch(e){}const oe=V[":status"]||Z.statusCode||200;a.writeHead(oe,g.ssl?void 0:Z.statusMessage||"Status read from http/2",ne),te?a.end(te):a.end();const re=c.hrtime.bigint();w({randomId:D,statusCode:oe,protocol:A?"HTTP/2":"HTTP1.1",duration:Math.floor(Number(re-q)/1e6),uniqueHash:Buffer.from(JSON.stringify({method:s.method,url:s.url,headers:Object.assign({},...Object.entries(s.headers).filter((([e])=>!e.startsWith(":"))).map((([e,t])=>({[e]:t})))),body:null==_?void 0:_.toJSON()})).toString("base64")})})))).addListener("error",(e=>{"EACCES"===e.code&&R("permission denied for this port",d.ERROR,p.NO),"EADDRINUSE"===e.code&&R("port is already used. NOT started",d.ERROR,p.ERROR_6)})).addListener("listening",(()=>{$(g)})).on("upgrade",((e,t)=>{var s,a;if(!g.websocket)return void t.end("HTTP/1.1 503 Service Unavailable\r\n\r\n");const{key:i,target:c,path:u}=L(e);if("/local-traffic-logs"===u){const n=(0,l.createHash)("sha1");n.update(e.headers["sec-websocket-key"]+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11");const o=n.digest("base64");return t.allowHalfOpen=!0,t.write(`HTTP/1.1 101 Switching Protocols\r\ndate: ${(new Date).toUTCString()}\r\nconnection: upgrade\r\nupgrade: websocket\r\nserver: local\r\nsec-websocket-accept: ${o}\r\n\r\n`),t.on("close",(()=>{v=v.filter((e=>t!==e.stream))})),void v.push({stream:t,wantsMask:!(null!==(a=null===(s=e.headers["user-agent"])||void 0===s?void 0:s.toString())&&void 0!==a?a:"").includes("Chrome")})}const h=new r.URL(`${c.protocol}//${c.host}${e.url.endsWith("/_next/webpack-hmr")?e.url:e.url.replace(new RegExp(`^${i}`,"g"),"").replace(/^\/*/,"/")}`),m={hostname:h.hostname,path:h.pathname,port:h.port,protocol:h.protocol,rejectUnauthorized:!1,method:e.method,headers:e.headers,host:h.hostname},f="https:"===h.protocol?(0,o.request)(m):(0,n.request)(m);f.end(),f.on("error",(e=>{R("websocket request has errored "+(e.errno?`(${e.errno})`:""),d.WARNING,p.WEBSOCKET)})),f.on("upgrade",((e,n)=>{const o=`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`;t.write(o),t.allowHalfOpen=!0,n.allowHalfOpen=!0,n.on("data",(e=>t.write(e))),t.on("data",(e=>n.write(e))),n.on("error",(e=>{R("downstream socket has errored "+(e.errno?`(${e.errno})`:""),d.WARNING,p.WEBSOCKET)})),t.on("error",(e=>{R("upstream socket has errored "+(e.errno?`(${e.errno})`:""),d.WARNING,p.WEBSOCKET)}))}))})).listen(g.port)};process.on("error",(function(e){["EPIPE","ECONNRESET"].includes(e.code)&&R(`ignoring ${e.code} error`,d.WARNING,p.ERROR_5)})),E().then(C);
|
|
2
|
+
"use strict";var e=this&&this.__awaiter||function(e,t,n,o){return new(n||(n=Promise))((function(r,s){function a(e){try{l(o.next(e))}catch(e){s(e)}}function i(e){try{l(o.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,i)}l((o=o.apply(e,t||[])).next())}))};Object.defineProperty(exports,"__esModule",{value:!0});const t=require("http2"),n=require("http"),o=require("https"),r=require("url"),s=require("fs"),a=require("zlib"),i=require("path"),l=require("crypto"),d=require("process");var c,p,u;!function(e){e[e.ERROR=124]="ERROR",e[e.INFO=93]="INFO",e[e.WARNING=172]="WARNING"}(c||(c={})),function(e){e.INBOUND="↘️ ",e.PORT="☎️ ",e.OUTBOUND="↗️ ",e.RULES="🔗",e.REWRITE="✒️ ",e.RESTART="🔄",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="☠️ "}(p||(p={})),function(e){e.INBOUND="INBOUND",e.OUTBOUND="OUTBOUND"}(u||(u={}));const h=(0,i.resolve)(process.env.HOME,".local-traffic.json"),m=(0,i.resolve)(process.cwd(),process.argv.slice(-1)[0].endsWith(".json")?process.argv.slice(-1)[0]:h),f={mapping:{"/config/":"config://","/logs/":"logs://"},port:8080,replaceRequestBodyUrls:!1,replaceResponseBodyUrls:!1,dontUseHttp2Downstream:!1,dontTranslateLocationHeader:!1,simpleLogs:!1,websocket:!0,disableWebSecurity:!1};let g,y,v=[],b=[];const R=e=>e===c.ERROR?"error":e===c.WARNING?"warning":"info",O=(e,t,n)=>{const o=(null==g?void 0:g.simpleLogs)||v.length?e.replace(/⎸/g,"|").replace(/⎹/g,"|").replace(/\u001b\[[^m]*m/g,"").replace(new RegExp(p.INBOUND,"g"),"inbound:").replace(new RegExp(p.PORT,"g"),"port:").replace(new RegExp(p.OUTBOUND,"g"),"outbound:").replace(new RegExp(p.RULES,"g"),"rules:").replace(new RegExp(p.NO,"g"),"").replace(new RegExp(p.REWRITE,"g"),"+rewrite").replace(new RegExp(p.WEBSOCKET,"g"),"websocket").replace(new RegExp(p.SHIELD,"g"),"web-security").replace(/\|+/g,"|"):e;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"}`})(null==g?void 0:g.simpleLogs)} ${(null==g?void 0:g.simpleLogs)?o:t?`[48;5;${t}m⎸ ${process.stdout.isTTY&&n||""} ${e.padEnd(40)} ⎹[0m`:e}`),E({event:o,level:R(t)})},w=(e,t)=>{const n=Array(4).fill(0).map((()=>t?Math.floor(256*Math.random()):0)),o=[...e.substring(0,65536)].map(((e,t)=>e.charCodeAt(0)^n[3&t])),r=Math.min(65535,e.length),s=e.length<126?Buffer.from(Uint8Array.from([129,(t?128:0)+r]).buffer):Buffer.concat([Buffer.from(Uint8Array.from([129,126|(t?128:0)]).buffer),Buffer.from(Uint8Array.from([r>>8]).buffer),Buffer.from(Uint8Array.from([255&r]).buffer)]),a=Buffer.from(Int8Array.from(n).buffer),i=Buffer.from(Int8Array.from(o).buffer);return Buffer.concat(t?[s,a,i]:[s,i])},$=(e,t)=>{const n=(0,l.createHash)("sha1");n.update(t+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11");const o=n.digest("base64");e.allowHalfOpen=!0,e.write(`HTTP/1.1 101 Switching Protocols\r\ndate: ${(new Date).toUTCString()}\r\nconnection: upgrade\r\nupgrade: websocket\r\nserver: local\r\nsec-websocket-accept: ${o}\r\n\r\n`)},E=e=>N(e,v),N=(e,t)=>{if(!t.length)return;const n=JSON.stringify(e),o=new Set(t.map((e=>e.wantsMask))),r=o.has(!1)&&w(n,!1),s=o.has(!0)&&w(n,!0);t.forEach((e=>{e.wantsMask?e.stream.write(s,"ascii",(()=>{})):e.stream.write(r,"ascii",(()=>{}))}))},S=e=>{O(`[48;5;52m⎸${p.PORT} ${e.port.toString().padStart(5)} [48;5;53m⎸${p.OUTBOUND} ${e.dontUseHttp2Downstream?"H1.1":"H/2 "}${e.replaceRequestBodyUrls?p.REWRITE:" "}⎹⎸${p.INBOUND} ${e.ssl?"H/2 ":"H1.1"}${e.replaceResponseBodyUrls?p.REWRITE:" "}⎹[48;5;54m[48;5;55m⎸${p.RULES}${Object.keys(g.mapping).length.toString().padStart(3)}⎹[48;5;56m⎸${g.websocket?p.WEBSOCKET:p.NO}⎹[48;5;57m⎸${g.simpleLogs?p.NO:p.COLORED}⎹[48;5;93m⎸${g.disableWebSecurity?p.NO:p.SHIELD}⎹[0m`),N(e,b)},B=(t=!0)=>e(void 0,void 0,void 0,(function*(){return new Promise((e=>(0,s.readFile)(m,((n,o)=>{n&&!t&&O("config error. Using default value",c.ERROR,p.ERROR_1);try{g=Object.assign({},f,JSON.parse((o||"{}").toString()))}catch(t){return O("config syntax incorrect, aborting",c.ERROR,p.ERROR_2),g=g||Object.assign({},f),void e(g)}g.mapping[""]||O('default mapping "" not provided.',c.WARNING,p.ERROR_3),n&&"ENOENT"===n.code&&t&&m===h?(0,s.writeFile)(m,JSON.stringify(f,null,2),(t=>{t?O("config file NOT created",c.ERROR,p.ERROR_4):O("config file created",c.INFO,p.COLORED),e(g)})):e(g)})))).then((()=>{t&&(0,s.watchFile)(m,j)}))})),j=()=>e(void 0,void 0,void 0,(function*(){const e=Object.assign({},g);if(yield B(!1),isNaN(g.port)||g.port>65535||g.port<0)return g=e,void O("port number invalid. Not refreshing",c.ERROR,p.PORT);if("object"!=typeof g.mapping)return g=e,void O("mapping should be an object. Aborting",c.ERROR,p.ERROR_5);if(g.replaceRequestBodyUrls!==e.replaceRequestBodyUrls&&O(`request body url ${g.replaceRequestBodyUrls?"":"NO "}rewriting`,c.INFO,p.REWRITE),g.replaceResponseBodyUrls!==e.replaceResponseBodyUrls&&O(`response body url ${g.replaceResponseBodyUrls?"":"NO "}rewriting`,c.INFO,p.REWRITE),g.dontTranslateLocationHeader!==e.dontTranslateLocationHeader&&O(`response location header ${g.dontTranslateLocationHeader?"NO ":""}translation`,c.INFO,p.REWRITE),g.dontUseHttp2Downstream!==e.dontUseHttp2Downstream&&O(`http/2 ${g.dontUseHttp2Downstream?"de":""}activated downstream`,c.INFO,p.OUTBOUND),g.disableWebSecurity!==e.disableWebSecurity&&O(`web security ${g.disableWebSecurity?"de":""}activated`,c.INFO,p.SHIELD),g.websocket!==e.websocket&&O(`websocket ${g.websocket?"":"de"}activated`,c.INFO,p.WEBSOCKET),g.simpleLogs!==e.simpleLogs&&O("simple logs "+(g.simpleLogs?"on":"off"),c.INFO,p.COLORED),Object.keys(g.mapping).join("\n")!==Object.keys(e.mapping).join("\n")&&O(`${Object.keys(g.mapping).length.toString().padStart(5)} loaded mapping rules`,c.INFO,p.RULES),g.port!==e.port&&O(`port changed from ${e.port} to ${g.port}`,c.INFO,p.PORT),g.ssl&&!e.ssl&&O("ssl configuration added",c.INFO,p.INBOUND),!g.ssl&&e.ssl&&O("ssl configuration removed",c.INFO,p.INBOUND),g.port!==e.port||JSON.stringify(g.ssl)!==JSON.stringify(e.ssl)){O("restarting server",c.INFO,p.RESTART),yield Promise.all(v.concat(b).map((e=>new Promise((t=>e.stream.end(t)))))),v=[],b=[];(yield Promise.race([new Promise((e=>y?y.close(e):e(void 0))).then((()=>!0)),new Promise((e=>setTimeout(e,5e3))).then((()=>!1))]))||O("error during restart (websockets ?)",c.WARNING,p.RESTART),q()}else S(g)})),I=e=>""==e?"":(0,i.normalize)(e).replace(/\\/g,"/"),U=e=>{const t=(0,i.resolve)("/",e.hostname,...e.pathname.replace(/[?#].*$/,"").replace(/^\/+/,"").split("/"));return{error:null,data:null,hasRun:!1,run:function(){return this.hasRun?Promise.resolve():new Promise((n=>(0,s.readFile)(t,((o,r)=>{if(this.hasRun=!0,!o||"EISDIR"!==o.code)return this.error=o,this.data=r,void n(void 0);(0,s.readdir)(t,((t,o)=>{this.error=t,this.data=o,t?n(void 0):Promise.all(o.map((t=>new Promise((n=>(0,s.lstat)((0,i.resolve)(e.pathname,t),((e,o)=>n([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=`${L(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>`,n(void 0)}))}))}))))},events:{},on:function(e,n){return this.events[e]=n,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}}},T=e=>({error:null,data:null,run:function(){return new Promise((t=>{this.data=e,t(void 0)}))},events:{},on:function(e,t){return this.events[e]=t,this.run().then((()=>{"response"===e&&this.events.response({Server:"local","Content-Type":"text/html"},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}}),L=(e,t,n)=>`<!doctype html>\n<html lang="en">\n<head>\n<title>&#x${e.toString(16)}; local-traffic ${t} | ${n}</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/>`,x=(e,t,n,o)=>`${L(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>${n}</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>`,H=(t,n,o)=>e(void 0,void 0,void 0,(function*(){var r,s;return(null!==(s=null===(r=n["content-encoding"])||void 0===r?void 0:r.toString())&&void 0!==s?s:"").split(",").reduce(((t,n)=>e(void 0,void 0,void 0,(function*(){const e=n.trim().toLowerCase(),o="gzip"===e||"x-gzip"===e?a.gunzip:"deflate"===e?a.inflate:"br"===e?a.brotliDecompress:"identity"===e||""===e?(e,t)=>{t(null,e)}:null;if(null===o)throw new Error(`${e} compression not supported by the proxy`);const r=yield t;return yield new Promise(((e,t)=>o(r,((n,o)=>{n&&t(n),e(o)}))))}))),Promise.resolve(t)).then((e=>{const t=e.length>1e7,r=["text/html","application/javascript","application/json"].some((e=>{var t;return(null!==(t=n["content-type"])&&void 0!==t?t:"").toString().includes(e)}));return!t&&(r||!/[^\x00-\x7F]/.test(e.toString()))?g.replaceResponseBodyUrls?C(e.toString(),o.direction,o.proxyHostnameAndPort).replace(/\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,`?protocol=ws${g.ssl?"s":""}%3A&hostname=${o.proxyHostname}&port=${g.port}&pathname=${encodeURIComponent(o.key.replace(/\/+$/,""))}`):e.toString():e})).then((e=>{var t,o;return(null!==(o=null===(t=n["content-encoding"])||void 0===t?void 0:t.toString())&&void 0!==o?o:"").split(",").reduce(((e,t)=>{const n=t.trim().toLowerCase(),o="gzip"===n||"x-gzip"===n?a.gzip:"deflate"===n?a.deflate:"br"===n?a.brotliCompress:"identity"===n||""===n?(e,t)=>{t(null,e)}:null;if(null===o)throw new Error(`${n} compression not supported by the proxy`);return e.then((e=>new Promise((t=>o(e,((e,n)=>{if(e)throw e;t(n)}))))))}),Promise.resolve(Buffer.from(e)))}))})),C=(e,t,n)=>Object.entries(g.mapping).map((([e,t])=>[e,"string"==typeof t?t:t.replaceBody])).reduce(((e,[o,r])=>r.startsWith("logs:")||r.startsWith("config:")||""!==o&&!o.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)?e:t===u.INBOUND?e.replace(new RegExp(r.replace(/^(file|logs):\/\//,"").replace(/[*+?^${}()|[\]\\]/g,"").replace(/^https/,"https?")+"/*","ig"),`http${g.ssl?"s":""}://${n}${o.replace(/\/+$/,"")}/`):e.split(`http${g.ssl?"s":""}://${n}${o.replace(/\/+$/,"")}`).join(r)),e).split(`${n}/:`).join(`${n}:`),k=(e,t,n)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":n.length}),t.end(n)},P=e=>{var t,n,o;const s=(null!==(o=null!==(n=null===(t=e.headers[":authority"])||void 0===t?void 0:t.toString())&&void 0!==n?n:e.headers.host)&&void 0!==o?o:"localhost").replace(/:.*/,""),a=e.headers[":authority"]||`${e.headers.host}${e.headers.host.match(/:[0-9]+$/)?"":80!==g.port||g.ssl?443===g.port&&g.ssl?"":`:${g.port}`:""}`,i=new r.URL(`http${g.ssl?"s":""}://${a}${e.url}`),l=i.href.substring(i.origin.length),[d,c]=Object.entries(Object.assign({},Object.assign({},...Object.entries(g.mapping).map((([e,t])=>({[e]:new r.URL(I("string"==typeof t?t:t.downstreamUrl))})))))).find((([e])=>l.match(RegExp(e.replace(/^\//,"^/")))))||[];return{proxyHostname:s,proxyHostnameAndPort:a,url:i,path:l,key:d,target:c}},q=()=>{y=(g.ssl?t.createSecureServer.bind(null,Object.assign(Object.assign({},g.ssl),{allowHTTP1:!0})):n.createServer)(((s,a)=>e(void 0,void 0,void 0,(function*(){var e,i,h,y;if(!s.headers.host&&!s.headers[":authority"])return void k(400,a,Buffer.from(x(new Error("client must supply a 'host' header"),"proxy",new r.URL(`http${g.ssl?"s":""}://unknowndomain${s.url}`))));const{proxyHostname:b,proxyHostnameAndPort:R,url:w,path:$,key:N,target:S}=P(s);if(!S)return void k(502,a,Buffer.from(x(new Error(`No mapping found in config file ${m}`),"proxy",w)));const B=S.host.replace(RegExp(/\/+$/),""),j=`${S.href.substring(8+S.host.length)}${I($.replace(RegExp(I(N)),""))}`.replace(/^\/*/,"/"),q=new r.URL(`${S.protocol}//${B}${j}`);let A=!g.dontUseHttp2Downstream;const D=(0,l.randomBytes)(20).toString("hex");E({level:"info",protocol:A?"HTTP/2":"HTTP1.1",method:s.method,upstreamPath:$,downstreamPath:q.href,randomId:D,uniqueHash:"N/A"});const W=d.hrtime.bigint();let M=null;const F="file:"===S.protocol?U(q):"logs:"===S.protocol?(e=>T(`${L(128250,"logs","")}\n<nav class="navbar navbar-expand-lg navbar-dark bg-primary nav-fill">\n <div class="container-fluid">\n <ul class="navbar-nav">\n <li class="nav-item">\n <a class="nav-link active" aria-current="page" href="javascript:show(0)">Access</a>\n </li>\n <li class="nav-item">\n <a class="nav-link" href="javascript:show(1)">Proxy</a>\n </li>\n </ul>\n <span class="navbar-text">\n Limit : <select id="limit" onchange="javascript:cleanup()"><option value="-1">0 (clear)</option><option value="10">10</option>\n <option value="50">50</option><option value="100">100</option><option value="200">200</option>\n <option selected="selected" value="500">500</option><option value="0">Infinity (discouraged)</option>\n </select> rows\n </span>\n </div>\n</nav>\n<table id="table-access" class="table table-striped" style="display: block; width: 100%; overflow-y: auto">\n <thead>\n <tr>\n <th scope="col">...</th>\n <th scope="col">Date</th>\n <th scope="col">Level</th>\n <th scope="col">Protocol</th>\n <th scope="col">Method</th>\n <th scope="col">Status</th>\n <th scope="col">Duration</th>\n <th scope="col">Upstream Path</th>\n <th scope="col">Downstream Path</th>\n </tr>\n </thead>\n <tbody id="access">\n </tbody>\n</table>\n<table id="table-proxy" class="table table-striped" style="display: none; width: 100%; overflow-y: auto">\n <thead>\n <tr>\n <th scope="col">Date</th>\n <th scope="col">Level</th>\n <th scope="col">Message</th>\n </tr>\n </thead>\n <tbody id="proxy">\n </tbody>\n</table>\n<script type="text/javascript">\n function start() {\n document.getElementById('table-access').style.height =\n (document.documentElement.clientHeight - 150) + 'px';\n const socket = new WebSocket("ws${g.ssl?"s":""}://${e}/local-traffic-logs");\n socket.onmessage = function(event) {\n let data = event.data\n let uniqueHash;\n try {\n const { uniqueHash: uniqueHash1, ...data1 } = JSON.parse(event.data);\n data = data1;\n uniqueHash = uniqueHash1;\n } catch(e) { }\n const time = new Date().toISOString().split('T')[1].replace('Z', '');\n const replay = uniqueHash ? '<button data-uniquehash="' + uniqueHash + '" onclick="javascript:replay(event)" ' +\n 'type="button" class="btn btn-primary"' + \n (uniqueHash === 'N/A' ? ' disabled="disabled"' : '') + '>🔁</button>' : '';\n if(data.statusCode && uniqueHash) {\n const color = Math.floor(data.statusCode / 100) === 1 ? "info" :\n Math.floor(data.statusCode / 100) === 2 ? "success" :\n Math.floor(data.statusCode / 100) === 3 ? "dark" :\n Math.floor(data.statusCode / 100) === 4 ? "warning" :\n Math.floor(data.statusCode / 100) === 5 ? "danger" :\n "secondary";\n const statusCodeColumn = document.querySelector("#event-" + data.randomId + " .statusCode");\n if (statusCodeColumn)\n statusCodeColumn.innerHTML = '<span class="badge bg-' + color + '">' + data.statusCode + '</span>';\n\n const durationColumn = document.querySelector("#event-" + data.randomId + " .duration");\n if (durationColumn) {\n const duration = data.duration > 10000 ? Math.floor(data.duration / 1000) + 's' :\n data.duration + 'ms';\n durationColumn.innerHTML = duration;\n }\n\n const protocolColumn = document.querySelector("#event-" + data.randomId + " .protocol");\n if (protocolColumn) {\n protocolColumn.innerHTML = data.protocol;\n }\n\n const replayColumn = document.querySelector("#event-" + data.randomId + " .replay");\n if (replayColumn) {\n replayColumn.innerHTML = replay;\n }\n } else if (uniqueHash) {\n document.getElementById("access")\n .insertAdjacentHTML('afterbegin', '<tr id="event-' + data.randomId + '">' +\n '<td scope="col" class="replay">' + replay + '</td>' +\n '<td scope="col">' + time + '</td>' +\n '<td scope="col">' + (data.level || 'info')+ '</td>' + \n '<td scope="col" class="protocol">' + data.protocol + '</td>' + \n '<td scope="col">' + data.method + '</td>' + \n '<td scope="col" class="statusCode"><span class="badge bg-secondary">...</span></td>' +\n '<td scope="col" class="duration">⏱</td>' +\n '<td scope="col">' + data.upstreamPath + '</td>' + \n '<td scope="col">' + data.downstreamPath + '</td>' + \n '</tr>');\n } else if(data.event) {\n document.getElementById("proxy")\n .insertAdjacentHTML('afterbegin', '<tr><td scope="col">' + time + '</td>' +\n '<td scope="col">' + (data.level || 'info')+ '</td>' + \n '<td scope="col">' + data.event + '</td></tr>');\n }\n cleanup();\n };\n socket.onerror = function(error) {\n console.log(\`[error] \${error}\`);\n setTimeout(start, 5000);\n };\n };\n function show(id) {\n [...document.querySelectorAll('table')].forEach((table, index) => {\n table.style.display = index === id ? 'block': 'none'\n });\n [...document.querySelectorAll('.navbar-nav .nav-item .nav-link')].forEach((link, index) => {\n if (index === id) { link.classList.add('active') } else link.classList.remove('active');\n });\n }\n function cleanup() {\n const currentLimit = parseInt(document.getElementById('limit').value)\n for (let table of ['access', 'proxy']) {\n while (currentLimit && document.getElementById(table).childNodes.length && \n document.getElementById(table).childNodes.length > currentLimit) {\n [...document.getElementById(table).childNodes].slice(-1)[0].remove();\n }\n }\n }\n function replay(event) {\n const uniqueHash = event.target.dataset.uniquehash;\n const { method, url, headers, body } = JSON.parse(atob(uniqueHash));\n fetch(url, {\n method,\n headers,\n body: !body.data || !body.data.length \n ? undefined\n : new TextDecoder().decode(new Int8Array(body.data))\n });\n }\n window.addEventListener("DOMContentLoaded", start);\n<\/script>\n</body></html>`))(R):"config:"===S.protocol?(e=>T(`${L(127899,"config","")}\n <link href="https://cdn.jsdelivr.net/npm/jsoneditor/dist/jsoneditor.min.css" rel="stylesheet" type="text/css">\n <script src="https://cdn.jsdelivr.net/npm/jsoneditor/dist/jsoneditor.min.js"><\/script>\n <div id="jsoneditor" style="width: 400px; height: 400px;"></div>\n <script>\n // create the editor\n const container = document.getElementById("jsoneditor")\n const options = {mode: "code", allowSchemaSuggestions: true, schema: {\n type: "object",\n properties: {\n ${Object.entries(Object.assign(Object.assign({},f),{ssl:{cert:"",key:""}})).map((([e,t])=>`${e}: {type: "${"number"==typeof t?"integer":"string"==typeof t?"string":"boolean"==typeof t?"boolean":"object"}"}`)).join(",\n ")}\n },\n required: [],\n additionalProperties: false\n }}\n\n function save() {\n socket.send(JSON.stringify(editor.get()));\n }\n\n const editor = new JSONEditor(container, options);\n let socket;\n const initialJson = ${JSON.stringify(g)}\n editor.set(initialJson)\n editor.validate();\n editor.aceEditor.commands.addCommand({\n name: 'save',\n bindKey: {win: 'Ctrl-S', mac: 'Command-S'},\n exec: save,\n });\n\n window.addEventListener("DOMContentLoaded", function() {\n document.getElementById('jsoneditor').style.height =\n (document.documentElement.clientHeight - 150) + 'px';\n document.getElementById('jsoneditor').style.width =\n parseInt(window.getComputedStyle(\n document.querySelector('.container')).maxWidth) + 'px';\n const saveButton = document.createElement('button');\n saveButton.addEventListener("click", save);\n saveButton.type="button";\n saveButton.classList.add("btn");\n saveButton.classList.add("btn-primary");\n saveButton.innerHTML="💾";\n document.querySelector('.jsoneditor-menu')\n .appendChild(saveButton);\n socket = new WebSocket("ws${g.ssl?"s":""}://${e}/local-traffic-config");\n socket.onmessage = function(event) {\n editor.set(JSON.parse(event.data))\n editor.validate()\n }\n });\n <\/script>\n </body></html>`))(R):A?yield Promise.race([new Promise((e=>{const n=(0,t.connect)(q,{rejectUnauthorized:!1,protocol:S.protocol},((t,o)=>{A=A&&!!o.alpnProtocol,e(A?n:null)}));n.on("error",(e=>{M=A&&Buffer.from(x(e,"connection",w,q))}))})),new Promise((e=>setTimeout((()=>{A=!1,e(null)}),3e3)))]):null;M instanceof Buffer||(M=null);const _=null==s?void 0:s.readableLength,J=null===(e=null==s?void 0:s.stream)||void 0===e?void 0:e.readableLength;let z=null;const G=g.replaceRequestBodyUrls||v.length;if(G){const e=null!==(i=null==s?void 0:s.stream)&&void 0!==i?i:s;let t=Buffer.from([]);const n=!((g.ssl&&0===J||!g.ssl&&0===_)&&("0"===s.headers["content-length"]||void 0===s.headers["content-length"]));yield Promise.race([new Promise((e=>setTimeout(e,1e4))),new Promise((o=>{n||o(void 0),e.on("data",(e=>{t=Buffer.concat([t,e])})),e.on("end",o),e.on("error",o)}))]),n&&!t.length&&O(`body replacement error ${$.slice(-17)}`,c.WARNING,p.ERROR_4),z=yield H(t,s.headers,{proxyHostnameAndPort:R,proxyHostname:b,key:N,direction:u.OUTBOUND})}const K=Object.assign(Object.assign({},[...Object.entries(s.headers)].filter((([e])=>!["host","connection","keep-alive"].includes(e.toLowerCase()))).reduce(((e,[t,n])=>(e[t]=(e[t]||"")+(Array.isArray(n)?n:[n]).map((e=>e.replace(w.hostname,B))).join(", "),e)),{})),{origin:S.href,referer:q.toString(),"content-length":null!==(y=null!==(h=null==z?void 0:z.length)&&void 0!==h?h:s.headers["content-length"])&&void 0!==y?y:0,":authority":B,":method":s.method,":path":j,":scheme":S.protocol.replace(":","")}),Z=F&&!M&&F.request(K,{endStream:g.ssl?!(null==J||J):!_});null==Z||Z.on("error",(e=>{const t=-505===e.errno;M=Buffer.from(x(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),w,q))}));const V={hostname:S.hostname,path:j,port:S.port,protocol:S.protocol,rejectUnauthorized:!1,method:s.method,headers:Object.assign(Object.assign({},Object.assign({},...Object.entries(K).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase())).map((([e,t])=>({[e]:t}))))),{host:S.hostname})},Y=!M&&!A&&!["file:","logs:","config:"].includes(S.protocol)&&(yield new Promise((e=>{const t="https:"===S.protocol?(0,o.request)(V,e):(0,n.request)(V,e);t.on("error",(t=>{M=Buffer.from(x(t,"request",w,q)),e(null)})),G&&(t.write(z),t.end()),G||(s.on("data",(e=>t.write(e))),s.on("end",(()=>t.end())))})));if(M)return void k(502,a,M);M=null,g.ssl&&J&&Z&&(G&&(Z.write(z),Z.end()),G||(s.stream.on("data",(e=>{Z.write(e)})),s.stream.on("end",(()=>Z.end())))),!g.ssl&&_&&Z&&(G&&(Z.write(z),Z.end()),G||(s.on("data",(e=>{Z.write(e)})),s.on("end",(()=>Z.end()))));const{outboundResponseHeaders:Q}=yield new Promise((e=>Z?Z.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!Z&&Y?{outboundResponseHeaders:Y.headers}:{outboundResponseHeaders:{}}))),X=Q.location?new r.URL(Q.location.startsWith("/")?`${S.href}${Q.location.replace(/^\/+/,"")}`:Q.location.replace(/^file:\/+/,"file:///").replace(/^(http)(s?):\/+/,"$1$2://")):null,ee=g.replaceResponseBodyUrls&&X?new r.URL(C(X.href,u.INBOUND,R).replace(/^(config:|logs:|file:)\/+/,"")):X,te=X?ee.origin!==X.origin||g.dontTranslateLocationHeader?ee:`${w.origin}${ee.href.substring(ee.origin.length)}`:X,ne=Z||Y,oe=null!=M?M:yield new Promise((e=>{let t=Buffer.alloc(0);ne?(ne.on("data",(e=>t=Buffer.concat([t,"string"==typeof e?Buffer.from(e):e]))),ne.on("end",(()=>{e(t)}))):e(t)})).then((e=>g.replaceResponseBodyUrls&&e.length?"config:"===S.protocol?e:H(e,Q,{proxyHostnameAndPort:R,proxyHostname:b,key:N,direction:u.INBOUND}).catch((e=>(k(502,a,Buffer.from(x(e,"stream",w,q))),Buffer.from("")))):e)),re=Object.assign(Object.assign({},Object.entries(Object.assign(Object.assign(Object.assign({},Q),g.replaceResponseBodyUrls?{"content-length":`${oe.byteLength}`}:{}),g.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,n])=>{const o=B.split("").map(((e,t)=>B.substring(t).startsWith(".")&&B.substring(t))).filter((e=>e)),r=[B].concat(o).reduce(((e,t)=>(Array.isArray(e)?e:[e]).map((e=>"string"==typeof e?e.replace(`Domain=${t}`,`Domain=${w.hostname}`):e))),n);return e[t]=(e[t]||[]).concat(r),e}),{})),te?{location:[te]}:{});try{Object.entries(re).forEach((([e,t])=>t&&a.setHeader(e,t)))}catch(e){}const se=Q[":status"]||Y.statusCode||200;a.writeHead(se,g.ssl?void 0:Y.statusMessage||"Status read from http/2",re),oe?a.end(oe):a.end();const ae=d.hrtime.bigint();E({randomId:D,statusCode:se,protocol:A?"HTTP/2":"HTTP1.1",duration:Math.floor(Number(ae-W)/1e6),uniqueHash:Buffer.from(JSON.stringify({method:s.method,url:s.url,headers:Object.assign({},...Object.entries(s.headers).filter((([e])=>!e.startsWith(":"))).map((([e,t])=>({[e]:t})))),body:null==z?void 0:z.toJSON()})).toString("base64")})})))).addListener("error",(e=>{"EACCES"===e.code&&O("permission denied for this port",c.ERROR,p.NO),"EADDRINUSE"===e.code&&O("port is already used. NOT started",c.ERROR,p.ERROR_6)})).addListener("listening",(()=>{S(g)})).on("upgrade",((e,t)=>{var a,i,l,d;if(!g.websocket)return void t.end("HTTP/1.1 503 Service Unavailable\r\n\r\n");const{key:u,target:h,path:f}=P(e);if("/local-traffic-logs"===f)return $(t,e.headers["sec-websocket-key"]),t.on("close",(()=>{v=v.filter((e=>t!==e.stream))})),void v.push({stream:t,wantsMask:!(null!==(i=null===(a=e.headers["user-agent"])||void 0===a?void 0:a.toString())&&void 0!==i?i:"").includes("Chrome")});if("/local-traffic-config"===f){$(t,e.headers["sec-websocket-key"]),t.on("close",(()=>{b=b.filter((e=>t!==e.stream))}));let n=null;return t.on("data",(e=>{const t=((e,t)=>{var n;if(!t&&0==(1&e.readUInt8(0)))return{payloadLength:0,mask:[0,0,0,0],body:""};const o=t?0:e.readUInt8(1),r=o>>7,s=127&o,a=t?t.payloadLength:127!==s?s:e.readUInt8(2)<<8+e.readUInt8(3),i=t?t.mask:r?Array(4).fill(0).map(((t,n)=>e.readUInt8(n+4))):[0,0,0,0],l=t?0:r?8:4,d=Array(e.length-l).fill(0).map(((t,n)=>String.fromCharCode(e.readUInt8(n+l)^i[3&n]))).join("");return{payloadLength:a,mask:i,body:(null!==(n=null==t?void 0:t.body)&&void 0!==n?n:"").concat(d)}})(e,n);if(null===n&&t.body.length<t.payloadLength)n=t;else if(null!==n&&t.body.length>=t.payloadLength){n=null;const e=JSON.parse(t.body);(0,s.writeFile)(m,JSON.stringify(e,null,2),(e=>{e?O("config file NOT saved",c.ERROR,p.ERROR_4):O("config file saved... will reload",c.INFO,p.COLORED)}))}})),void b.push({stream:t,wantsMask:!(null!==(d=null===(l=e.headers["user-agent"])||void 0===l?void 0:l.toString())&&void 0!==d?d:"").includes("Chrome")})}const y=new r.URL(`${h.protocol}//${h.host}${e.url.endsWith("/_next/webpack-hmr")?e.url:e.url.replace(new RegExp(`^${u}`,"g"),"").replace(/^\/*/,"/")}`),R={hostname:y.hostname,path:y.pathname,port:y.port,protocol:y.protocol,rejectUnauthorized:!1,method:e.method,headers:e.headers,host:y.hostname},w="https:"===y.protocol?(0,o.request)(R):(0,n.request)(R);w.end(),w.on("error",(e=>{O("websocket request has errored "+(e.errno?`(${e.errno})`:""),c.WARNING,p.WEBSOCKET)})),w.on("upgrade",((e,n)=>{const o=`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`;t.write(o),t.allowHalfOpen=!0,n.allowHalfOpen=!0,n.on("data",(e=>t.write(e))),t.on("data",(e=>n.write(e))),n.on("error",(e=>{O("downstream socket has errored "+(e.errno?`(${e.errno})`:""),c.WARNING,p.WEBSOCKET)})),t.on("error",(e=>{O("upstream socket has errored "+(e.errno?`(${e.errno})`:""),c.WARNING,p.WEBSOCKET)}))}))})).listen(g.port)};process.on("error",(function(e){["EPIPE","ECONNRESET"].includes(e.code)&&O(`ignoring ${e.code} error`,c.WARNING,p.ERROR_5)})),B().then(q);
|
package/index.ts
CHANGED
|
@@ -68,6 +68,17 @@ enum REPLACEMENT_DIRECTION {
|
|
|
68
68
|
OUTBOUND = "OUTBOUND",
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
interface WebsocketListener {
|
|
72
|
+
stream: Duplex;
|
|
73
|
+
wantsMask: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface PartialRead {
|
|
77
|
+
payloadLength: number;
|
|
78
|
+
mask: number[];
|
|
79
|
+
body: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
interface LocalConfiguration {
|
|
72
83
|
mapping?: {
|
|
73
84
|
[subPath: string]: string | { replaceBody: string; downstreamUrl: string };
|
|
@@ -92,6 +103,7 @@ const filename = resolve(
|
|
|
92
103
|
);
|
|
93
104
|
const defaultConfig: LocalConfiguration = {
|
|
94
105
|
mapping: {
|
|
106
|
+
"/config/": "config://",
|
|
95
107
|
"/logs/": "logs://",
|
|
96
108
|
},
|
|
97
109
|
port: 8080,
|
|
@@ -106,7 +118,8 @@ const defaultConfig: LocalConfiguration = {
|
|
|
106
118
|
|
|
107
119
|
let config: LocalConfiguration;
|
|
108
120
|
let server: Server;
|
|
109
|
-
let logsListeners:
|
|
121
|
+
let logsListeners: WebsocketListener[] = [];
|
|
122
|
+
let configListeners: WebsocketListener[] = [];
|
|
110
123
|
const getCurrentTime = (simpleLogs?: boolean) => {
|
|
111
124
|
const date = new Date();
|
|
112
125
|
return `${simpleLogs ? "" : "\u001b[36m"}${`${date.getHours()}`.padStart(
|
|
@@ -153,7 +166,7 @@ const log = (text: string, level?: LogLevel, emoji?: string) => {
|
|
|
153
166
|
: text
|
|
154
167
|
}`,
|
|
155
168
|
);
|
|
156
|
-
|
|
169
|
+
notifyLogsListeners({
|
|
157
170
|
event: simpleLog,
|
|
158
171
|
level: levelToString(level),
|
|
159
172
|
});
|
|
@@ -184,7 +197,7 @@ const createWebsocketBufferFrom = (
|
|
|
184
197
|
]).buffer,
|
|
185
198
|
),
|
|
186
199
|
Buffer.from(Uint8Array.from([length >> 8]).buffer),
|
|
187
|
-
Buffer.from(Uint8Array.from([length & ((
|
|
200
|
+
Buffer.from(Uint8Array.from([length & ((1 << 8) - 1)]).buffer),
|
|
188
201
|
]);
|
|
189
202
|
const maskingKey = Buffer.from(Int8Array.from(mask).buffer);
|
|
190
203
|
const payload = Buffer.from(Int8Array.from(maskedTextBits).buffer);
|
|
@@ -193,20 +206,72 @@ const createWebsocketBufferFrom = (
|
|
|
193
206
|
);
|
|
194
207
|
};
|
|
195
208
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
209
|
+
const readWebsocketBuffer = (
|
|
210
|
+
buffer: Buffer,
|
|
211
|
+
partialRead: PartialRead,
|
|
212
|
+
): PartialRead => {
|
|
213
|
+
if (!partialRead && (buffer.readUInt8(0) & 1) === 0)
|
|
214
|
+
return { payloadLength: 0, mask: [0, 0, 0, 0], body: "" };
|
|
215
|
+
const headerSecondByte = partialRead ? 0 : buffer.readUInt8(1);
|
|
216
|
+
const hasMask = headerSecondByte >> 7;
|
|
217
|
+
const payloadLengthFirstByte = headerSecondByte & ((1 << 7) - 1);
|
|
218
|
+
const payloadLength = partialRead
|
|
219
|
+
? partialRead.payloadLength
|
|
220
|
+
: payloadLengthFirstByte !== (1 << 7) - 1
|
|
221
|
+
? payloadLengthFirstByte
|
|
222
|
+
: buffer.readUInt8(2) << (8 + buffer.readUInt8(3));
|
|
223
|
+
const mask = partialRead
|
|
224
|
+
? partialRead.mask
|
|
225
|
+
: !hasMask
|
|
226
|
+
? [0, 0, 0, 0]
|
|
227
|
+
: Array(4)
|
|
228
|
+
.fill(0)
|
|
229
|
+
.map((_, i) => buffer.readUInt8(i + 4));
|
|
230
|
+
const payloadStart = partialRead ? 0 : hasMask ? 8 : 4;
|
|
231
|
+
const body = Array(buffer.length - payloadStart)
|
|
232
|
+
.fill(0)
|
|
233
|
+
.map((_, i) =>
|
|
234
|
+
String.fromCharCode(buffer.readUInt8(i + payloadStart) ^ mask[i & 3]),
|
|
235
|
+
)
|
|
236
|
+
.join("");
|
|
237
|
+
return { payloadLength, mask, body: (partialRead?.body ?? "").concat(body) };
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const acknowledgeWebsocket = (socket: Duplex, key: string) => {
|
|
241
|
+
const shasum = createHash("sha1");
|
|
242
|
+
shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
|
243
|
+
const accept = shasum.digest("base64");
|
|
244
|
+
socket.allowHalfOpen = true;
|
|
245
|
+
socket.write(
|
|
246
|
+
"HTTP/1.1 101 Switching Protocols\r\n" +
|
|
247
|
+
`date: ${new Date().toUTCString()}\r\n` +
|
|
248
|
+
"connection: upgrade\r\n" +
|
|
249
|
+
"upgrade: websocket\r\n" +
|
|
250
|
+
"server: local\r\n" +
|
|
251
|
+
`sec-websocket-accept: ${accept}\r\n` +
|
|
252
|
+
"\r\n",
|
|
201
253
|
);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const notifyConfigListeners = (data: Record<string, unknown>) =>
|
|
257
|
+
notifyListeners(data, configListeners);
|
|
258
|
+
const notifyLogsListeners = (data: Record<string, unknown>) =>
|
|
259
|
+
notifyListeners(data, logsListeners);
|
|
260
|
+
const notifyListeners = (
|
|
261
|
+
data: Record<string, unknown>,
|
|
262
|
+
listeners: WebsocketListener[],
|
|
263
|
+
) => {
|
|
264
|
+
if (!listeners.length) return;
|
|
265
|
+
const text = JSON.stringify(data);
|
|
266
|
+
const wantsMask = new Set(listeners.map(listener => listener.wantsMask));
|
|
202
267
|
const bufferWithoutMask =
|
|
203
268
|
wantsMask.has(false) && createWebsocketBufferFrom(text, false);
|
|
204
269
|
const bufferWithMask =
|
|
205
270
|
wantsMask.has(true) && createWebsocketBufferFrom(text, true);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
?
|
|
209
|
-
:
|
|
271
|
+
listeners.forEach(listener => {
|
|
272
|
+
listener.wantsMask
|
|
273
|
+
? listener.stream.write(bufferWithMask, "ascii", () => {})
|
|
274
|
+
: listener.stream.write(bufferWithoutMask, "ascii", () => {});
|
|
210
275
|
});
|
|
211
276
|
};
|
|
212
277
|
|
|
@@ -232,6 +297,7 @@ const quickStatus = (thisConfig: LocalConfiguration) => {
|
|
|
232
297
|
config.disableWebSecurity ? EMOJIS.NO : EMOJIS.SHIELD
|
|
233
298
|
}⎹\u001b[0m`,
|
|
234
299
|
);
|
|
300
|
+
notifyConfigListeners(thisConfig as Record<string, unknown>);
|
|
235
301
|
};
|
|
236
302
|
|
|
237
303
|
const load = async (firstTime: boolean = true) =>
|
|
@@ -273,12 +339,16 @@ const load = async (firstTime: boolean = true) =>
|
|
|
273
339
|
firstTime &&
|
|
274
340
|
filename === userHomeConfigFile
|
|
275
341
|
) {
|
|
276
|
-
writeFile(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
342
|
+
writeFile(
|
|
343
|
+
filename,
|
|
344
|
+
JSON.stringify(defaultConfig, null, 2),
|
|
345
|
+
fileWriteErr => {
|
|
346
|
+
if (fileWriteErr)
|
|
347
|
+
log("config file NOT created", LogLevel.ERROR, EMOJIS.ERROR_4);
|
|
348
|
+
else log("config file created", LogLevel.INFO, EMOJIS.COLORED);
|
|
349
|
+
resolve(config);
|
|
350
|
+
},
|
|
351
|
+
);
|
|
282
352
|
} else resolve(config);
|
|
283
353
|
}),
|
|
284
354
|
).then(() => {
|
|
@@ -393,12 +463,12 @@ const onWatch = async () => {
|
|
|
393
463
|
) {
|
|
394
464
|
log(`restarting server`, LogLevel.INFO, EMOJIS.RESTART);
|
|
395
465
|
await Promise.all(
|
|
396
|
-
logsListeners
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
),
|
|
466
|
+
logsListeners
|
|
467
|
+
.concat(configListeners)
|
|
468
|
+
.map(listener => new Promise(resolve => listener.stream.end(resolve))),
|
|
400
469
|
);
|
|
401
470
|
logsListeners = [];
|
|
471
|
+
configListeners = [];
|
|
402
472
|
const stopped = await Promise.race([
|
|
403
473
|
new Promise(resolve =>
|
|
404
474
|
!server ? resolve(void 0) : server.close(resolve),
|
|
@@ -539,13 +609,48 @@ const fileRequest = (url: URL): ClientHttp2Session => {
|
|
|
539
609
|
} as unknown as ClientHttp2Session;
|
|
540
610
|
};
|
|
541
611
|
|
|
542
|
-
const
|
|
612
|
+
const staticPage = (data: string): ClientHttp2Session =>
|
|
543
613
|
({
|
|
544
614
|
error: null as Error,
|
|
545
615
|
data: null as string | Buffer,
|
|
546
616
|
run: function () {
|
|
547
617
|
return new Promise(resolve => {
|
|
548
|
-
this.data =
|
|
618
|
+
this.data = data;
|
|
619
|
+
resolve(void 0);
|
|
620
|
+
});
|
|
621
|
+
},
|
|
622
|
+
events: {} as { [name: string]: (...any: any) => any },
|
|
623
|
+
on: function (name: string, action: (...any: any) => any) {
|
|
624
|
+
this.events[name] = action;
|
|
625
|
+
this.run().then(() => {
|
|
626
|
+
if (name === "response")
|
|
627
|
+
this.events["response"](
|
|
628
|
+
{
|
|
629
|
+
Server: "local",
|
|
630
|
+
"Content-Type": "text/html",
|
|
631
|
+
},
|
|
632
|
+
0,
|
|
633
|
+
);
|
|
634
|
+
if (name === "data" && this.data) {
|
|
635
|
+
this.events["data"](this.data);
|
|
636
|
+
this.events["end"]();
|
|
637
|
+
}
|
|
638
|
+
if (name === "error" && this.error) {
|
|
639
|
+
this.events["error"](this.error);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
return this;
|
|
643
|
+
},
|
|
644
|
+
end: function () {
|
|
645
|
+
return this;
|
|
646
|
+
},
|
|
647
|
+
request: function () {
|
|
648
|
+
return this;
|
|
649
|
+
},
|
|
650
|
+
} as unknown as ClientHttp2Session);
|
|
651
|
+
|
|
652
|
+
const logsPage = (proxyHostnameAndPort: string) =>
|
|
653
|
+
staticPage(`${header(0x1f4fa, "logs", "")}
|
|
549
654
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary nav-fill">
|
|
550
655
|
<div class="container-fluid">
|
|
551
656
|
<ul class="navbar-nav">
|
|
@@ -596,8 +701,6 @@ const logsPage = (proxyHostnameAndPort: string): ClientHttp2Session =>
|
|
|
596
701
|
function start() {
|
|
597
702
|
document.getElementById('table-access').style.height =
|
|
598
703
|
(document.documentElement.clientHeight - 150) + 'px';
|
|
599
|
-
document.getElementById('table-proxy').style.height =
|
|
600
|
-
(document.documentElement.clientHeight - 150) + 'px';
|
|
601
704
|
const socket = new WebSocket("ws${
|
|
602
705
|
config.ssl ? "s" : ""
|
|
603
706
|
}://${proxyHostnameAndPort}/local-traffic-logs");
|
|
@@ -696,39 +799,77 @@ const logsPage = (proxyHostnameAndPort: string): ClientHttp2Session =>
|
|
|
696
799
|
}
|
|
697
800
|
window.addEventListener("DOMContentLoaded", start);
|
|
698
801
|
</script>
|
|
699
|
-
</body></html
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
802
|
+
</body></html>`);
|
|
803
|
+
|
|
804
|
+
const configPage = (proxyHostnameAndPort: string) =>
|
|
805
|
+
staticPage(`${header(0x1f39b, "config", "")}
|
|
806
|
+
<link href="https://cdn.jsdelivr.net/npm/jsoneditor/dist/jsoneditor.min.css" rel="stylesheet" type="text/css">
|
|
807
|
+
<script src="https://cdn.jsdelivr.net/npm/jsoneditor/dist/jsoneditor.min.js"></script>
|
|
808
|
+
<div id="jsoneditor" style="width: 400px; height: 400px;"></div>
|
|
809
|
+
<script>
|
|
810
|
+
// create the editor
|
|
811
|
+
const container = document.getElementById("jsoneditor")
|
|
812
|
+
const options = {mode: "code", allowSchemaSuggestions: true, schema: {
|
|
813
|
+
type: "object",
|
|
814
|
+
properties: {
|
|
815
|
+
${Object.entries({ ...defaultConfig, ssl: { cert: "", key: "" } })
|
|
816
|
+
.map(
|
|
817
|
+
([property, exampleValue]) =>
|
|
818
|
+
`${property}: {type: "${
|
|
819
|
+
typeof exampleValue === "number"
|
|
820
|
+
? "integer"
|
|
821
|
+
: typeof exampleValue === "string"
|
|
822
|
+
? "string"
|
|
823
|
+
: typeof exampleValue === "boolean"
|
|
824
|
+
? "boolean"
|
|
825
|
+
: "object"
|
|
826
|
+
}"}`,
|
|
827
|
+
)
|
|
828
|
+
.join(",\n ")}
|
|
829
|
+
},
|
|
830
|
+
required: [],
|
|
831
|
+
additionalProperties: false
|
|
832
|
+
}}
|
|
833
|
+
|
|
834
|
+
function save() {
|
|
835
|
+
socket.send(JSON.stringify(editor.get()));
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const editor = new JSONEditor(container, options);
|
|
839
|
+
let socket;
|
|
840
|
+
const initialJson = ${JSON.stringify(config)}
|
|
841
|
+
editor.set(initialJson)
|
|
842
|
+
editor.validate();
|
|
843
|
+
editor.aceEditor.commands.addCommand({
|
|
844
|
+
name: 'save',
|
|
845
|
+
bindKey: {win: 'Ctrl-S', mac: 'Command-S'},
|
|
846
|
+
exec: save,
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
window.addEventListener("DOMContentLoaded", function() {
|
|
850
|
+
document.getElementById('jsoneditor').style.height =
|
|
851
|
+
(document.documentElement.clientHeight - 150) + 'px';
|
|
852
|
+
document.getElementById('jsoneditor').style.width =
|
|
853
|
+
parseInt(window.getComputedStyle(
|
|
854
|
+
document.querySelector('.container')).maxWidth) + 'px';
|
|
855
|
+
const saveButton = document.createElement('button');
|
|
856
|
+
saveButton.addEventListener("click", save);
|
|
857
|
+
saveButton.type="button";
|
|
858
|
+
saveButton.classList.add("btn");
|
|
859
|
+
saveButton.classList.add("btn-primary");
|
|
860
|
+
saveButton.innerHTML="💾";
|
|
861
|
+
document.querySelector('.jsoneditor-menu')
|
|
862
|
+
.appendChild(saveButton);
|
|
863
|
+
socket = new WebSocket("ws${
|
|
864
|
+
config.ssl ? "s" : ""
|
|
865
|
+
}://${proxyHostnameAndPort}/local-traffic-config");
|
|
866
|
+
socket.onmessage = function(event) {
|
|
867
|
+
editor.set(JSON.parse(event.data))
|
|
868
|
+
editor.validate()
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
</script>
|
|
872
|
+
</body></html>`);
|
|
732
873
|
|
|
733
874
|
const header = (
|
|
734
875
|
icon: number,
|
|
@@ -901,6 +1042,7 @@ const replaceTextUsingMapping = (
|
|
|
901
1042
|
.reduce(
|
|
902
1043
|
(inProgress, [path, value]) =>
|
|
903
1044
|
value.startsWith("logs:") ||
|
|
1045
|
+
value.startsWith("config:") ||
|
|
904
1046
|
(path !== "" && !path.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/))
|
|
905
1047
|
? inProgress
|
|
906
1048
|
: direction === REPLACEMENT_DIRECTION.INBOUND
|
|
@@ -1043,7 +1185,7 @@ const start = () => {
|
|
|
1043
1185
|
|
|
1044
1186
|
let http2IsSupported = !config.dontUseHttp2Downstream;
|
|
1045
1187
|
const randomId = randomBytes(20).toString("hex");
|
|
1046
|
-
|
|
1188
|
+
notifyLogsListeners({
|
|
1047
1189
|
level: "info",
|
|
1048
1190
|
protocol: http2IsSupported ? "HTTP/2" : "HTTP1.1",
|
|
1049
1191
|
method: inboundRequest.method,
|
|
@@ -1061,6 +1203,8 @@ const start = () => {
|
|
|
1061
1203
|
? fileRequest(targetUrl)
|
|
1062
1204
|
: target.protocol === "logs:"
|
|
1063
1205
|
? logsPage(proxyHostnameAndPort)
|
|
1206
|
+
: target.protocol === "config:"
|
|
1207
|
+
? configPage(proxyHostnameAndPort)
|
|
1064
1208
|
: !http2IsSupported
|
|
1065
1209
|
? null
|
|
1066
1210
|
: await Promise.race([
|
|
@@ -1228,7 +1372,7 @@ const start = () => {
|
|
|
1228
1372
|
const outboundHttp1Response: IncomingMessage =
|
|
1229
1373
|
!error &&
|
|
1230
1374
|
!http2IsSupported &&
|
|
1231
|
-
!["file:", "logs:"].includes(target.protocol) &&
|
|
1375
|
+
!["file:", "logs:", "config:"].includes(target.protocol) &&
|
|
1232
1376
|
(await new Promise(resolve => {
|
|
1233
1377
|
const outboundHttp1Request: ClientRequest =
|
|
1234
1378
|
target.protocol === "https:"
|
|
@@ -1330,7 +1474,7 @@ const start = () => {
|
|
|
1330
1474
|
redirectUrl.href,
|
|
1331
1475
|
REPLACEMENT_DIRECTION.INBOUND,
|
|
1332
1476
|
proxyHostnameAndPort,
|
|
1333
|
-
).replace(/^(logs:|file:)\/+/, ""),
|
|
1477
|
+
).replace(/^(config:|logs:|file:)\/+/, ""),
|
|
1334
1478
|
);
|
|
1335
1479
|
const translatedReplacedRedirectUrl = !redirectUrl
|
|
1336
1480
|
? redirectUrl
|
|
@@ -1367,6 +1511,7 @@ const start = () => {
|
|
|
1367
1511
|
}).then((payloadBuffer: Buffer) => {
|
|
1368
1512
|
if (!config.replaceResponseBodyUrls) return payloadBuffer;
|
|
1369
1513
|
if (!payloadBuffer.length) return payloadBuffer;
|
|
1514
|
+
if (target.protocol === "config:") return payloadBuffer;
|
|
1370
1515
|
|
|
1371
1516
|
return replaceBody(payloadBuffer, outboundResponseHeaders, {
|
|
1372
1517
|
proxyHostnameAndPort,
|
|
@@ -1463,7 +1608,7 @@ const start = () => {
|
|
|
1463
1608
|
else inboundResponse.end();
|
|
1464
1609
|
const endTime = hrtime.bigint();
|
|
1465
1610
|
|
|
1466
|
-
|
|
1611
|
+
notifyLogsListeners({
|
|
1467
1612
|
randomId,
|
|
1468
1613
|
statusCode,
|
|
1469
1614
|
protocol: http2IsSupported ? "HTTP/2" : "HTTP1.1",
|
|
@@ -1511,21 +1656,9 @@ const start = () => {
|
|
|
1511
1656
|
} = determineMapping(request);
|
|
1512
1657
|
|
|
1513
1658
|
if (path === "/local-traffic-logs") {
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
request.headers["sec-websocket-key"]
|
|
1517
|
-
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
|
|
1518
|
-
);
|
|
1519
|
-
const accept = shasum.digest("base64");
|
|
1520
|
-
upstreamSocket.allowHalfOpen = true;
|
|
1521
|
-
upstreamSocket.write(
|
|
1522
|
-
"HTTP/1.1 101 Switching Protocols\r\n" +
|
|
1523
|
-
`date: ${new Date().toUTCString()}\r\n` +
|
|
1524
|
-
"connection: upgrade\r\n" +
|
|
1525
|
-
"upgrade: websocket\r\n" +
|
|
1526
|
-
"server: local\r\n" +
|
|
1527
|
-
`sec-websocket-accept: ${accept}\r\n` +
|
|
1528
|
-
"\r\n",
|
|
1659
|
+
acknowledgeWebsocket(
|
|
1660
|
+
upstreamSocket,
|
|
1661
|
+
request.headers["sec-websocket-key"],
|
|
1529
1662
|
);
|
|
1530
1663
|
upstreamSocket.on("close", () => {
|
|
1531
1664
|
logsListeners = logsListeners.filter(
|
|
@@ -1541,6 +1674,52 @@ const start = () => {
|
|
|
1541
1674
|
return;
|
|
1542
1675
|
}
|
|
1543
1676
|
|
|
1677
|
+
if (path === "/local-traffic-config") {
|
|
1678
|
+
acknowledgeWebsocket(
|
|
1679
|
+
upstreamSocket,
|
|
1680
|
+
request.headers["sec-websocket-key"],
|
|
1681
|
+
);
|
|
1682
|
+
upstreamSocket.on("close", () => {
|
|
1683
|
+
configListeners = configListeners.filter(
|
|
1684
|
+
oneConfigListener => upstreamSocket !== oneConfigListener.stream,
|
|
1685
|
+
);
|
|
1686
|
+
});
|
|
1687
|
+
let partialRead = null;
|
|
1688
|
+
upstreamSocket.on("data", buffer => {
|
|
1689
|
+
const read = readWebsocketBuffer(buffer, partialRead);
|
|
1690
|
+
if (partialRead === null && read.body.length < read.payloadLength) {
|
|
1691
|
+
partialRead = read;
|
|
1692
|
+
} else if (
|
|
1693
|
+
partialRead !== null &&
|
|
1694
|
+
read.body.length >= read.payloadLength
|
|
1695
|
+
) {
|
|
1696
|
+
partialRead = null;
|
|
1697
|
+
const newConfig = JSON.parse(read.body);
|
|
1698
|
+
writeFile(
|
|
1699
|
+
filename,
|
|
1700
|
+
JSON.stringify(newConfig, null, 2),
|
|
1701
|
+
fileWriteErr => {
|
|
1702
|
+
if (fileWriteErr)
|
|
1703
|
+
log("config file NOT saved", LogLevel.ERROR, EMOJIS.ERROR_4);
|
|
1704
|
+
else
|
|
1705
|
+
log(
|
|
1706
|
+
"config file saved... will reload",
|
|
1707
|
+
LogLevel.INFO,
|
|
1708
|
+
EMOJIS.COLORED,
|
|
1709
|
+
);
|
|
1710
|
+
},
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
configListeners.push({
|
|
1715
|
+
stream: upstreamSocket,
|
|
1716
|
+
wantsMask: !(
|
|
1717
|
+
request.headers["user-agent"]?.toString() ?? ""
|
|
1718
|
+
).includes("Chrome"),
|
|
1719
|
+
});
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1544
1723
|
const target = new URL(
|
|
1545
1724
|
`${targetWithForcedPrefix.protocol}//${targetWithForcedPrefix.host}${
|
|
1546
1725
|
request.url.endsWith("/_next/webpack-hmr")
|