local-traffic 0.0.61 → 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 +289 -79
- 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}`),O({event:o,level:b(t)})},O=e=>{if(!v.length)return;const t=JSON.stringify(e),n=Array(4).fill(0).map((()=>Math.floor(256*Math.random()))),o=[...t.substring(0,65536)].map(((e,t)=>e.charCodeAt(0)^n[3&t])),r=Math.min(65535,t.length),s=t.length<126?Buffer.from(Uint8Array.from([129,128+r]).buffer):Buffer.concat([Buffer.from(Uint8Array.from([129,254]).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),l=Buffer.concat([s,a,i]);v.forEach((e=>{e.write(l,"ascii",(()=>{}))}))},w=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`)},$=(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,E)}))})),E=()=>e(void 0,void 0,void 0,(function*(){const e=Object.assign({},g);if(yield $(!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.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),L()}else w(g)})),N=e=>""==e?"":(0,i.normalize)(e).replace(/\\/g,"/"),S=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=`${B(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=`${B(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}}),B=(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/>`,H=(e,t,n,o)=>`${B(128163,"error",e.message)}\n<p>An error happened while trying to proxy a remote exchange</p>\n<div class="alert alert-warning" role="alert">\n ⓘ This is not an error from the downstream service.\n</div>\n<div class="alert alert-danger" role="alert">\n<pre><code>${e.stack||`<i>${e.name} : ${e.message}</i>`}${e.errno?`<br/>(code : ${e.errno})`:""}</code></pre>\n</div>\nMore information about the request :\n<table class="table">\n <tbody>\n <tr>\n <td>phase</td>\n <td>${t}</td>\n </tr>\n <tr>\n <td>requested URL</td>\n <td>${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>`,I=(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?U(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)))}))})),U=(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}:`),x=(e,t,n)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":n.length}),t.end(n)},j=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(N("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}},L=()=>{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 x(400,a,Buffer.from(H(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:w,path:$,key:E,target:B}=j(s);if(!B)return void x(502,a,Buffer.from(H(new Error(`No mapping found in config file ${m}`),"proxy",w)));const L=B.host.replace(RegExp(/\/+$/),""),C=`${B.href.substring(8+B.host.length)}${N($.replace(RegExp(N(E)),""))}`.replace(/^\/*/,"/"),P=new r.URL(`${B.protocol}//${L}${C}`);let A=!g.dontUseHttp2Downstream;const D=(0,l.randomBytes)(20).toString("hex");O({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:"===B.protocol?S(P):"logs:"===B.protocol?T(b):A?yield Promise.race([new Promise((e=>{const n=(0,t.connect)(P,{rejectUnauthorized:!1,protocol:B.protocol},((t,o)=>{A=A&&!!o.alpnProtocol,e(A?n:null)}));n.on("error",(e=>{k=A&&Buffer.from(H(e,"connection",w,P))}))})),new Promise((e=>setTimeout((()=>{A=!1,e(null)}),3e3)))]):null;k instanceof Buffer||(k=null);const F=null==s?void 0:s.readableLength,M=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===M||!g.ssl&&0===F)&&("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 I(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(w.hostname,L))).join(", "),e)),{})),{origin:B.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":L,":method":s.method,":path":C,":scheme":B.protocol.replace(":","")}),J=W&&!k&&W.request(G,{endStream:g.ssl?!(null==M||M):!F});null==J||J.on("error",(e=>{const t=-505===e.errno;k=Buffer.from(H(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),w,P))}));const K={hostname:B.hostname,path:C,port:B.port,protocol:B.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:B.hostname})},Z=!k&&!A&&!["file:","logs:"].includes(B.protocol)&&(yield new Promise((e=>{const t="https:"===B.protocol?(0,o.request)(K,e):(0,n.request)(K,e);t.on("error",(t=>{k=Buffer.from(H(t,"request",w,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 x(502,a,k);k=null,g.ssl&&M&&J&&(z&&(J.write(_),J.end()),z||(s.stream.on("data",(e=>{J.write(e)})),s.stream.on("end",(()=>J.end())))),!g.ssl&&F&&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("/")?`${B.href}${V.location.replace(/^\/+/,"")}`:V.location.replace(/^file:\/+/,"file:///").replace(/^(http)(s?):\/+/,"$1$2://")):null,Q=g.replaceResponseBodyUrls&&Y?new r.URL(U(Y.href,u.INBOUND,b).replace(/^(logs:|file:)\/+/,"")):Y,X=Y?Q.origin!==Y.origin||g.dontTranslateLocationHeader?Q:`${w.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?I(e,V,{proxyHostnameAndPort:b,proxyHostname:y,key:E,direction:u.INBOUND}).catch((e=>(x(502,a,Buffer.from(H(e,"stream",w,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=L.split("").map(((e,t)=>L.substring(t).startsWith(".")&&L.substring(t))).filter((e=>e)),r=[L].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}),{})),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();O({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",(()=>{w(g)})).on("upgrade",((e,t)=>{if(!g.websocket)return void t.end("HTTP/1.1 503 Service Unavailable\r\n\r\n");const{key:s,target:a,path:i}=j(e);if("/local-traffic-logs"===i){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))})),void v.push(t)}const c=new r.URL(`${a.protocol}//${a.host}${e.url.endsWith("/_next/webpack-hmr")?e.url:e.url.replace(new RegExp(`^${s}`,"g"),"").replace(/^\/*/,"/")}`),u={hostname:c.hostname,path:c.pathname,port:c.port,protocol:c.protocol,rejectUnauthorized:!1,method:e.method,headers:e.headers,host:c.hostname},h="https:"===c.protocol?(0,o.request)(u):(0,n.request)(u);h.end(),h.on("error",(e=>{R("websocket request has errored "+(e.errno?`(${e.errno})`:""),d.WARNING,p.WEBSOCKET)})),h.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)})),$().then(L);
|
|
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,35 +166,112 @@ 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
|
});
|
|
160
173
|
};
|
|
161
174
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
const createWebsocketBufferFrom = (
|
|
176
|
+
text: string,
|
|
177
|
+
wantsMask: boolean,
|
|
178
|
+
): Buffer => {
|
|
165
179
|
const mask = Array(4)
|
|
166
180
|
.fill(0)
|
|
167
|
-
.map(() => Math.floor(Math.random() * (2 << 7)));
|
|
181
|
+
.map(() => (wantsMask ? Math.floor(Math.random() * (2 << 7)) : 0));
|
|
168
182
|
const maskedTextBits = [...text.substring(0, 2 << 15)].map(
|
|
169
183
|
(c, i) => c.charCodeAt(0) ^ mask[i & 3],
|
|
170
184
|
);
|
|
171
185
|
const length = Math.min((2 << 15) - 1, text.length);
|
|
172
186
|
const header =
|
|
173
187
|
text.length < (2 << 6) - 2
|
|
174
|
-
? Buffer.from(
|
|
188
|
+
? Buffer.from(
|
|
189
|
+
Uint8Array.from([(1 << 7) + 1, (wantsMask ? 1 << 7 : 0) + length])
|
|
190
|
+
.buffer,
|
|
191
|
+
)
|
|
175
192
|
: Buffer.concat([
|
|
176
|
-
Buffer.from(
|
|
193
|
+
Buffer.from(
|
|
194
|
+
Uint8Array.from([
|
|
195
|
+
(1 << 7) + 1,
|
|
196
|
+
((1 << 7) - 2) | (wantsMask ? 1 << 7 : 0),
|
|
197
|
+
]).buffer,
|
|
198
|
+
),
|
|
177
199
|
Buffer.from(Uint8Array.from([length >> 8]).buffer),
|
|
178
|
-
Buffer.from(Uint8Array.from([length & ((
|
|
200
|
+
Buffer.from(Uint8Array.from([length & ((1 << 8) - 1)]).buffer),
|
|
179
201
|
]);
|
|
180
202
|
const maskingKey = Buffer.from(Int8Array.from(mask).buffer);
|
|
181
203
|
const payload = Buffer.from(Int8Array.from(maskedTextBits).buffer);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
204
|
+
return Buffer.concat(
|
|
205
|
+
wantsMask ? [header, maskingKey, payload] : [header, payload],
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
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",
|
|
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));
|
|
267
|
+
const bufferWithoutMask =
|
|
268
|
+
wantsMask.has(false) && createWebsocketBufferFrom(text, false);
|
|
269
|
+
const bufferWithMask =
|
|
270
|
+
wantsMask.has(true) && createWebsocketBufferFrom(text, true);
|
|
271
|
+
listeners.forEach(listener => {
|
|
272
|
+
listener.wantsMask
|
|
273
|
+
? listener.stream.write(bufferWithMask, "ascii", () => {})
|
|
274
|
+
: listener.stream.write(bufferWithoutMask, "ascii", () => {});
|
|
185
275
|
});
|
|
186
276
|
};
|
|
187
277
|
|
|
@@ -207,6 +297,7 @@ const quickStatus = (thisConfig: LocalConfiguration) => {
|
|
|
207
297
|
config.disableWebSecurity ? EMOJIS.NO : EMOJIS.SHIELD
|
|
208
298
|
}⎹\u001b[0m`,
|
|
209
299
|
);
|
|
300
|
+
notifyConfigListeners(thisConfig as Record<string, unknown>);
|
|
210
301
|
};
|
|
211
302
|
|
|
212
303
|
const load = async (firstTime: boolean = true) =>
|
|
@@ -248,12 +339,16 @@ const load = async (firstTime: boolean = true) =>
|
|
|
248
339
|
firstTime &&
|
|
249
340
|
filename === userHomeConfigFile
|
|
250
341
|
) {
|
|
251
|
-
writeFile(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
);
|
|
257
352
|
} else resolve(config);
|
|
258
353
|
}),
|
|
259
354
|
).then(() => {
|
|
@@ -368,11 +463,12 @@ const onWatch = async () => {
|
|
|
368
463
|
) {
|
|
369
464
|
log(`restarting server`, LogLevel.INFO, EMOJIS.RESTART);
|
|
370
465
|
await Promise.all(
|
|
371
|
-
logsListeners
|
|
372
|
-
|
|
373
|
-
|
|
466
|
+
logsListeners
|
|
467
|
+
.concat(configListeners)
|
|
468
|
+
.map(listener => new Promise(resolve => listener.stream.end(resolve))),
|
|
374
469
|
);
|
|
375
470
|
logsListeners = [];
|
|
471
|
+
configListeners = [];
|
|
376
472
|
const stopped = await Promise.race([
|
|
377
473
|
new Promise(resolve =>
|
|
378
474
|
!server ? resolve(void 0) : server.close(resolve),
|
|
@@ -513,13 +609,48 @@ const fileRequest = (url: URL): ClientHttp2Session => {
|
|
|
513
609
|
} as unknown as ClientHttp2Session;
|
|
514
610
|
};
|
|
515
611
|
|
|
516
|
-
const
|
|
612
|
+
const staticPage = (data: string): ClientHttp2Session =>
|
|
517
613
|
({
|
|
518
614
|
error: null as Error,
|
|
519
615
|
data: null as string | Buffer,
|
|
520
616
|
run: function () {
|
|
521
617
|
return new Promise(resolve => {
|
|
522
|
-
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", "")}
|
|
523
654
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary nav-fill">
|
|
524
655
|
<div class="container-fluid">
|
|
525
656
|
<ul class="navbar-nav">
|
|
@@ -570,8 +701,6 @@ const logsPage = (proxyHostnameAndPort: string): ClientHttp2Session =>
|
|
|
570
701
|
function start() {
|
|
571
702
|
document.getElementById('table-access').style.height =
|
|
572
703
|
(document.documentElement.clientHeight - 150) + 'px';
|
|
573
|
-
document.getElementById('table-proxy').style.height =
|
|
574
|
-
(document.documentElement.clientHeight - 150) + 'px';
|
|
575
704
|
const socket = new WebSocket("ws${
|
|
576
705
|
config.ssl ? "s" : ""
|
|
577
706
|
}://${proxyHostnameAndPort}/local-traffic-logs");
|
|
@@ -670,39 +799,77 @@ const logsPage = (proxyHostnameAndPort: string): ClientHttp2Session =>
|
|
|
670
799
|
}
|
|
671
800
|
window.addEventListener("DOMContentLoaded", start);
|
|
672
801
|
</script>
|
|
673
|
-
</body></html
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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>`);
|
|
706
873
|
|
|
707
874
|
const header = (
|
|
708
875
|
icon: number,
|
|
@@ -875,6 +1042,7 @@ const replaceTextUsingMapping = (
|
|
|
875
1042
|
.reduce(
|
|
876
1043
|
(inProgress, [path, value]) =>
|
|
877
1044
|
value.startsWith("logs:") ||
|
|
1045
|
+
value.startsWith("config:") ||
|
|
878
1046
|
(path !== "" && !path.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/))
|
|
879
1047
|
? inProgress
|
|
880
1048
|
: direction === REPLACEMENT_DIRECTION.INBOUND
|
|
@@ -1017,7 +1185,7 @@ const start = () => {
|
|
|
1017
1185
|
|
|
1018
1186
|
let http2IsSupported = !config.dontUseHttp2Downstream;
|
|
1019
1187
|
const randomId = randomBytes(20).toString("hex");
|
|
1020
|
-
|
|
1188
|
+
notifyLogsListeners({
|
|
1021
1189
|
level: "info",
|
|
1022
1190
|
protocol: http2IsSupported ? "HTTP/2" : "HTTP1.1",
|
|
1023
1191
|
method: inboundRequest.method,
|
|
@@ -1035,6 +1203,8 @@ const start = () => {
|
|
|
1035
1203
|
? fileRequest(targetUrl)
|
|
1036
1204
|
: target.protocol === "logs:"
|
|
1037
1205
|
? logsPage(proxyHostnameAndPort)
|
|
1206
|
+
: target.protocol === "config:"
|
|
1207
|
+
? configPage(proxyHostnameAndPort)
|
|
1038
1208
|
: !http2IsSupported
|
|
1039
1209
|
? null
|
|
1040
1210
|
: await Promise.race([
|
|
@@ -1202,7 +1372,7 @@ const start = () => {
|
|
|
1202
1372
|
const outboundHttp1Response: IncomingMessage =
|
|
1203
1373
|
!error &&
|
|
1204
1374
|
!http2IsSupported &&
|
|
1205
|
-
!["file:", "logs:"].includes(target.protocol) &&
|
|
1375
|
+
!["file:", "logs:", "config:"].includes(target.protocol) &&
|
|
1206
1376
|
(await new Promise(resolve => {
|
|
1207
1377
|
const outboundHttp1Request: ClientRequest =
|
|
1208
1378
|
target.protocol === "https:"
|
|
@@ -1304,7 +1474,7 @@ const start = () => {
|
|
|
1304
1474
|
redirectUrl.href,
|
|
1305
1475
|
REPLACEMENT_DIRECTION.INBOUND,
|
|
1306
1476
|
proxyHostnameAndPort,
|
|
1307
|
-
).replace(/^(logs:|file:)\/+/, ""),
|
|
1477
|
+
).replace(/^(config:|logs:|file:)\/+/, ""),
|
|
1308
1478
|
);
|
|
1309
1479
|
const translatedReplacedRedirectUrl = !redirectUrl
|
|
1310
1480
|
? redirectUrl
|
|
@@ -1341,6 +1511,7 @@ const start = () => {
|
|
|
1341
1511
|
}).then((payloadBuffer: Buffer) => {
|
|
1342
1512
|
if (!config.replaceResponseBodyUrls) return payloadBuffer;
|
|
1343
1513
|
if (!payloadBuffer.length) return payloadBuffer;
|
|
1514
|
+
if (target.protocol === "config:") return payloadBuffer;
|
|
1344
1515
|
|
|
1345
1516
|
return replaceBody(payloadBuffer, outboundResponseHeaders, {
|
|
1346
1517
|
proxyHostnameAndPort,
|
|
@@ -1437,7 +1608,7 @@ const start = () => {
|
|
|
1437
1608
|
else inboundResponse.end();
|
|
1438
1609
|
const endTime = hrtime.bigint();
|
|
1439
1610
|
|
|
1440
|
-
|
|
1611
|
+
notifyLogsListeners({
|
|
1441
1612
|
randomId,
|
|
1442
1613
|
statusCode,
|
|
1443
1614
|
protocol: http2IsSupported ? "HTTP/2" : "HTTP1.1",
|
|
@@ -1485,28 +1656,67 @@ const start = () => {
|
|
|
1485
1656
|
} = determineMapping(request);
|
|
1486
1657
|
|
|
1487
1658
|
if (path === "/local-traffic-logs") {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
request.headers["sec-websocket-key"]
|
|
1491
|
-
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
|
|
1492
|
-
);
|
|
1493
|
-
const accept = shasum.digest("base64");
|
|
1494
|
-
upstreamSocket.allowHalfOpen = true;
|
|
1495
|
-
upstreamSocket.write(
|
|
1496
|
-
"HTTP/1.1 101 Switching Protocols\r\n" +
|
|
1497
|
-
`date: ${new Date().toUTCString()}\r\n` +
|
|
1498
|
-
"connection: upgrade\r\n" +
|
|
1499
|
-
"upgrade: websocket\r\n" +
|
|
1500
|
-
"server: local\r\n" +
|
|
1501
|
-
`sec-websocket-accept: ${accept}\r\n` +
|
|
1502
|
-
"\r\n",
|
|
1659
|
+
acknowledgeWebsocket(
|
|
1660
|
+
upstreamSocket,
|
|
1661
|
+
request.headers["sec-websocket-key"],
|
|
1503
1662
|
);
|
|
1504
1663
|
upstreamSocket.on("close", () => {
|
|
1505
1664
|
logsListeners = logsListeners.filter(
|
|
1506
|
-
oneLogsListener => upstreamSocket !== oneLogsListener,
|
|
1665
|
+
oneLogsListener => upstreamSocket !== oneLogsListener.stream,
|
|
1666
|
+
);
|
|
1667
|
+
});
|
|
1668
|
+
logsListeners.push({
|
|
1669
|
+
stream: upstreamSocket,
|
|
1670
|
+
wantsMask: !(
|
|
1671
|
+
request.headers["user-agent"]?.toString() ?? ""
|
|
1672
|
+
).includes("Chrome"),
|
|
1673
|
+
});
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
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,
|
|
1507
1685
|
);
|
|
1508
1686
|
});
|
|
1509
|
-
|
|
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
|
+
});
|
|
1510
1720
|
return;
|
|
1511
1721
|
}
|
|
1512
1722
|
|