@sandeepk1729/porter 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +915 -65
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const commander_1 = __nccwpck_require__(909);
|
|
|
15
15
|
const package_json_1 = __importDefault(__nccwpck_require__(330));
|
|
16
16
|
const agent_1 = __nccwpck_require__(257);
|
|
17
17
|
const config_1 = __nccwpck_require__(750);
|
|
18
|
+
const server_1 = __nccwpck_require__(127);
|
|
18
19
|
const porter = new commander_1.Command();
|
|
19
20
|
porter.name("porter").description(package_json_1.default.description).version(package_json_1.default.version); // <-- Dynamically injected
|
|
20
21
|
// 1. add alias
|
|
@@ -22,15 +23,18 @@ porter
|
|
|
22
23
|
.command("http")
|
|
23
24
|
.arguments("<local-port>")
|
|
24
25
|
.description("Add http port forwarding")
|
|
25
|
-
.
|
|
26
|
+
.option("--ui-port <port>", "Port for the web UI dashboard", "7676")
|
|
27
|
+
.action(async (localPort, options) => {
|
|
28
|
+
const uiPort = parseInt(options.uiPort, 10);
|
|
29
|
+
if (isNaN(uiPort) || uiPort < 1 || uiPort > 65535) {
|
|
30
|
+
console.error(`Invalid --ui-port value: "${options.uiPort}". Must be a number between 1 and 65535.`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
(0, server_1.startUIServer)(uiPort);
|
|
26
34
|
console.log(`Connecting to porter server and forwarding to local port ${localPort}`);
|
|
27
35
|
config_1.caller.request(config_1.REQ_BODY)
|
|
28
|
-
.on("
|
|
29
|
-
|
|
30
|
-
res.on("data", (chunk) => {
|
|
31
|
-
console.log("Response body:", chunk.toString());
|
|
32
|
-
});
|
|
33
|
-
})
|
|
36
|
+
.on("error", (err) => console.error(`Connection to porter server failed: ${err.message}\n` +
|
|
37
|
+
`Make sure the porter server is reachable and try again.`))
|
|
34
38
|
.on("upgrade", (0, agent_1.upgradeHandler)(localPort))
|
|
35
39
|
.end();
|
|
36
40
|
});
|
|
@@ -98,6 +102,728 @@ if (!process.argv.slice(2).length) {
|
|
|
98
102
|
command_1.default.parse(process.argv);
|
|
99
103
|
|
|
100
104
|
|
|
105
|
+
/***/ }),
|
|
106
|
+
|
|
107
|
+
/***/ 418:
|
|
108
|
+
/***/ ((__unused_webpack_module, exports) => {
|
|
109
|
+
|
|
110
|
+
"use strict";
|
|
111
|
+
|
|
112
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
113
|
+
class Channel {
|
|
114
|
+
clients;
|
|
115
|
+
/// Constructor
|
|
116
|
+
constructor() {
|
|
117
|
+
this.clients = new Set();
|
|
118
|
+
}
|
|
119
|
+
subscribe = (res) => {
|
|
120
|
+
this.clients.add(res);
|
|
121
|
+
};
|
|
122
|
+
unsubscribe(res) {
|
|
123
|
+
this.clients.delete(res);
|
|
124
|
+
}
|
|
125
|
+
broadcast = (event, html) => {
|
|
126
|
+
const payload = Array.isArray(html) ? html.join("\n") : html;
|
|
127
|
+
for (const client of this.clients) {
|
|
128
|
+
this.send(client, event, payload);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Write a single SSE message. Multi-line HTML is split into multiple
|
|
133
|
+
* `data:` lines so no double-newline accidentally terminates the frame.
|
|
134
|
+
*/
|
|
135
|
+
send = (client, event, html) => {
|
|
136
|
+
const safe = html.trim().replace(/\n{2,}/g, "\n");
|
|
137
|
+
const dataLines = safe
|
|
138
|
+
.split("\n")
|
|
139
|
+
.map((l) => `data: ${l}`)
|
|
140
|
+
.join("\n");
|
|
141
|
+
client.write(`event: ${event}\n${dataLines}\n\n`);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
exports["default"] = Channel;
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
/***/ }),
|
|
148
|
+
|
|
149
|
+
/***/ 265:
|
|
150
|
+
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
|
151
|
+
|
|
152
|
+
"use strict";
|
|
153
|
+
|
|
154
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
155
|
+
exports.agentEvents = void 0;
|
|
156
|
+
const node_events_1 = __nccwpck_require__(474);
|
|
157
|
+
const agentEvents = new node_events_1.EventEmitter();
|
|
158
|
+
exports.agentEvents = agentEvents;
|
|
159
|
+
agentEvents.setMaxListeners(100);
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
/***/ }),
|
|
163
|
+
|
|
164
|
+
/***/ 823:
|
|
165
|
+
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
|
166
|
+
|
|
167
|
+
"use strict";
|
|
168
|
+
|
|
169
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
170
|
+
exports.INDEX_HTML = exports.EMPTY_DETAIL_HTML = exports.EMPTY_MSG_HTML = void 0;
|
|
171
|
+
exports.rowHtml = rowHtml;
|
|
172
|
+
exports.countOobHtml = countOobHtml;
|
|
173
|
+
exports.detailHtml = detailHtml;
|
|
174
|
+
exports.detailInnerHtml = detailInnerHtml;
|
|
175
|
+
const utils_1 = __nccwpck_require__(448);
|
|
176
|
+
/**
|
|
177
|
+
* A single request row. If `oobSpec` is provided the element gets
|
|
178
|
+
* `hx-swap-oob` so HTMX applies it as an out-of-band DOM patch.
|
|
179
|
+
*
|
|
180
|
+
* Click behaviour is handled by a delegated listener on #list-panel
|
|
181
|
+
* (see the inline script at the bottom of the page) so that it works
|
|
182
|
+
* correctly even when rows are continuously added/replaced via SSE OOB
|
|
183
|
+
* swaps – per-element hx-* attributes would require htmx.process() to
|
|
184
|
+
* be called after every OOB swap, which the SSE extension does not
|
|
185
|
+
* guarantee.
|
|
186
|
+
*/
|
|
187
|
+
function rowHtml(r, oobSpec) {
|
|
188
|
+
const dur = r.endTime ? `${r.endTime - r.startTime}ms` : "…";
|
|
189
|
+
const stHtml = r.responseStatus
|
|
190
|
+
? `<div class="st ${(0, utils_1.stClass)(r.responseStatus)}">${r.responseStatus}</div>`
|
|
191
|
+
: `<div class="st st-p">…</div>`;
|
|
192
|
+
const oob = oobSpec ? ` hx-swap-oob="${(0, utils_1.esc)(oobSpec)}"` : "";
|
|
193
|
+
const cls = `req-row${r.done ? "" : " pending"}`;
|
|
194
|
+
return (`<div ${oob}>
|
|
195
|
+
<div id="row-${r.requestId}" class="${cls}">
|
|
196
|
+
<span class="mth ${(0, utils_1.mthClass)(r.method)}">${(0, utils_1.esc)(r.method)}</span>
|
|
197
|
+
<span class="req-path" title="${(0, utils_1.esc)(r.path)}">${(0, utils_1.esc)(r.path)}</span>
|
|
198
|
+
<div class="req-meta">
|
|
199
|
+
${stHtml}
|
|
200
|
+
<div class="dur">${dur}</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>`);
|
|
204
|
+
}
|
|
205
|
+
/** Request count badge, returned as an OOB outerHTML swap. */
|
|
206
|
+
function countOobHtml(n) {
|
|
207
|
+
return `<span id="req-count" hx-swap-oob="outerHTML">${n} ${n === 1 ? "request" : "requests"}</span>`;
|
|
208
|
+
}
|
|
209
|
+
/** Empty-state placeholder for #list-panel. */
|
|
210
|
+
const EMPTY_MSG_HTML = `<div id="empty-msg" class="empty-list">` +
|
|
211
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">` +
|
|
212
|
+
`<path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>` +
|
|
213
|
+
`</svg><span>Waiting for requests…</span></div>`;
|
|
214
|
+
exports.EMPTY_MSG_HTML = EMPTY_MSG_HTML;
|
|
215
|
+
/** Empty-state for #detail-panel (used by the clear response OOB). */
|
|
216
|
+
const EMPTY_DETAIL_HTML = `<div id="detail-panel" hx-swap-oob="outerHTML" class="center">` +
|
|
217
|
+
`<div class="ph">` +
|
|
218
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">` +
|
|
219
|
+
`<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>` +
|
|
220
|
+
`</svg><span>Select a request to view details</span></div></div>`;
|
|
221
|
+
exports.EMPTY_DETAIL_HTML = EMPTY_DETAIL_HTML;
|
|
222
|
+
/**
|
|
223
|
+
* Inner HTML of the detail panel for request `r`.
|
|
224
|
+
* Served both by `GET /request/:id` and as the payload of
|
|
225
|
+
* `response-end-{id}` SSE events (the `.detail-content` wrapper
|
|
226
|
+
* already has `sse-swap="response-end-{id}"` so HTMX replaces
|
|
227
|
+
* its innerHTML automatically when that event arrives).
|
|
228
|
+
*/
|
|
229
|
+
function detailInnerHtml(r) {
|
|
230
|
+
const dur = r.endTime ? `${r.endTime - r.startTime} ms` : "pending…";
|
|
231
|
+
const stHtml = r.responseStatus
|
|
232
|
+
? `<span class="st ${(0, utils_1.stClass)(r.responseStatus)}">${r.responseStatus}</span>`
|
|
233
|
+
: `<span class="st st-p">Pending…</span>`;
|
|
234
|
+
let html = `<div class="summary">` +
|
|
235
|
+
`<span class="mth ${(0, utils_1.mthClass)(r.method)}">${(0, utils_1.esc)(r.method)}</span>` +
|
|
236
|
+
`<span class="s-path">${(0, utils_1.esc)(r.path)}</span>` +
|
|
237
|
+
`${stHtml}<span class="s-dur">${dur}</span>` +
|
|
238
|
+
`</div>` +
|
|
239
|
+
`<div class="section"><div class="section-hdr">Request Headers</div>` +
|
|
240
|
+
`${(0, utils_1.headersHtml)(r.reqHeaders)}</div>` +
|
|
241
|
+
`<div class="section"><div class="section-hdr">Request Body</div>` +
|
|
242
|
+
`${(0, utils_1.bodyHtml)(r.reqBodyChunks)}</div>`;
|
|
243
|
+
if (r.responseStatus !== null) {
|
|
244
|
+
html +=
|
|
245
|
+
`<div class="section"><div class="section-hdr">Response Headers</div>` +
|
|
246
|
+
`${(0, utils_1.headersHtml)(r.resHeaders)}</div>` +
|
|
247
|
+
`<div class="section"><div class="section-hdr">Response Body</div>` +
|
|
248
|
+
`${(0, utils_1.bodyHtml)(r.resBodyChunks)}</div>`;
|
|
249
|
+
}
|
|
250
|
+
return html;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Full detail panel content for `GET /request/:id`.
|
|
254
|
+
* The `.detail-content` wrapper carries `sse-swap="response-end-{id}"`
|
|
255
|
+
* so HTMX's SSE extension auto-updates the detail panel when the
|
|
256
|
+
* response completes without any client-side JavaScript.
|
|
257
|
+
*/
|
|
258
|
+
function detailHtml(r) {
|
|
259
|
+
return (`<div class="detail-content"` +
|
|
260
|
+
` sse-swap="response-end-${r.requestId}"` +
|
|
261
|
+
` hx-swap="innerHTML">` +
|
|
262
|
+
detailInnerHtml(r) +
|
|
263
|
+
`</div>`);
|
|
264
|
+
}
|
|
265
|
+
// ── Embedded HTML dashboard ───────────────────────────────────────────────────
|
|
266
|
+
//
|
|
267
|
+
// Technology choices:
|
|
268
|
+
// HTMX (htmx.org) – SSE live-swap via hx-ext="sse", REST actions via
|
|
269
|
+
// hx-get / hx-delete, out-of-band (OOB) DOM patches from the server.
|
|
270
|
+
// htmx-ext-sse – official SSE extension for HTMX.
|
|
271
|
+
// Hyperscript (_hyperscript.org) – declarative DOM interactions via `_=`
|
|
272
|
+
// attributes: active-row selection, connection status dot, no JS block.
|
|
273
|
+
//
|
|
274
|
+
// All three libraries are loaded from unpkg CDN via <script> tags so the
|
|
275
|
+
// dashboard works without any bundled assets or local file serving.
|
|
276
|
+
//
|
|
277
|
+
// Key patterns:
|
|
278
|
+
// • <body hx-ext="sse" sse-connect="/events"> – body is the SSE root.
|
|
279
|
+
// • #sse-sink (hidden) absorbs `ui-update` SSE events; OOB fragments in
|
|
280
|
+
// those events patch #list-panel rows, #req-count, #empty-msg in-place.
|
|
281
|
+
// • A delegated click listener on #list-panel uses htmx.ajax() to fetch
|
|
282
|
+
// server-rendered detail HTML into #detail-panel when a row is clicked.
|
|
283
|
+
// Using delegation rather than per-row hx-get/hx-trigger avoids the
|
|
284
|
+
// need for htmx.process() to be called after every SSE OOB swap.
|
|
285
|
+
// • The .detail-content wrapper returned by GET /request/:id carries
|
|
286
|
+
// sse-swap="response-end-{id}" so HTMX auto-refreshes the detail panel
|
|
287
|
+
// when that per-request SSE event fires – zero client JS needed.
|
|
288
|
+
// • Clear button uses hx-delete="/requests"; the server response carries
|
|
289
|
+
// OOB patches to reset #detail-panel and #req-count.
|
|
290
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
291
|
+
const INDEX_HTML = `<!DOCTYPE html>
|
|
292
|
+
<html lang="en">
|
|
293
|
+
<head>
|
|
294
|
+
<meta charset="UTF-8">
|
|
295
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
296
|
+
<title>Porter Agent \u2014 Live Traffic</title>
|
|
297
|
+
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"
|
|
298
|
+
integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"
|
|
299
|
+
crossorigin="anonymous"><\/script>
|
|
300
|
+
<script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"
|
|
301
|
+
integrity="sha384-fw+eTlCc7suMV/1w/7fr2/PmwElUIt5i82bi+qTiLXvjRXZ2/FkiTNA/w0MhXnGI"
|
|
302
|
+
crossorigin="anonymous"><\/script>
|
|
303
|
+
<script src="https://unpkg.com/hyperscript.org@0.9.13/dist/_hyperscript.min.js"
|
|
304
|
+
integrity="sha384-5yQ5JTatiFEgeiEB4mfkRI3oTGtaNpbJGdcciZ4IEYFpLGt8yDsGAd7tKiMwnX9b"
|
|
305
|
+
crossorigin="anonymous"><\/script>
|
|
306
|
+
<style>
|
|
307
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
308
|
+
body{font-family:'Segoe UI',system-ui,sans-serif;background:#0f172a;color:#e2e8f0;height:100vh;display:flex;flex-direction:column;overflow:hidden}
|
|
309
|
+
header{background:#1e293b;padding:12px 20px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #334155;flex-shrink:0}
|
|
310
|
+
.logo{font-size:1.1rem;font-weight:700;color:#38bdf8;letter-spacing:-0.3px}
|
|
311
|
+
.conn-status{display:flex;align-items:center;gap:8px;font-size:0.8125rem;color:#94a3b8}
|
|
312
|
+
.dot{width:8px;height:8px;border-radius:50%;background:#22c55e}
|
|
313
|
+
.dot.off{background:#ef4444}
|
|
314
|
+
@keyframes blink{0%,100%{opacity:1}50%{opacity:0.4}}
|
|
315
|
+
.dot.live{animation:blink 1.8s ease-in-out infinite}
|
|
316
|
+
.toolbar{background:#1e293b;padding:6px 20px;display:flex;align-items:center;gap:10px;border-bottom:1px solid #334155;flex-shrink:0}
|
|
317
|
+
.btn{padding:5px 12px;border-radius:5px;border:1px solid #475569;background:transparent;color:#94a3b8;cursor:pointer;font-size:0.8rem;transition:all .15s}
|
|
318
|
+
.btn:hover{background:#334155;color:#e2e8f0}
|
|
319
|
+
#req-count{margin-left:auto;font-size:0.8rem;color:#475569}
|
|
320
|
+
main{display:flex;flex:1;overflow:hidden}
|
|
321
|
+
#list-panel{width:360px;min-width:240px;overflow-y:auto;border-right:1px solid #334155;flex-shrink:0}
|
|
322
|
+
.empty-list{display:flex;flex-direction:column;align-items:center;justify-content:center;height:200px;gap:10px;color:#475569;font-size:0.875rem;padding:20px;text-align:center}
|
|
323
|
+
.req-row{padding:8px 14px;border-bottom:1px solid #1e293b;cursor:pointer;display:grid;grid-template-columns:52px 1fr auto;gap:8px;align-items:center;transition:background .1s}
|
|
324
|
+
.req-row:hover{background:#1a2744}
|
|
325
|
+
.req-row.active{background:#1e3a5f;border-left:3px solid #38bdf8;padding-left:11px}
|
|
326
|
+
.req-row.pending{opacity:.75}
|
|
327
|
+
.mth{font-size:.65rem;font-weight:800;padding:2px 5px;border-radius:4px;text-align:center;letter-spacing:.4px;white-space:nowrap}
|
|
328
|
+
.mth-GET{background:#0c4a6e;color:#38bdf8}
|
|
329
|
+
.mth-POST{background:#14532d;color:#4ade80}
|
|
330
|
+
.mth-PUT{background:#78350f;color:#fbbf24}
|
|
331
|
+
.mth-PATCH{background:#4c1d95;color:#a78bfa}
|
|
332
|
+
.mth-DELETE{background:#7f1d1d;color:#f87171}
|
|
333
|
+
.mth-HEAD,.mth-OPTIONS{background:#1e3a5f;color:#93c5fd}
|
|
334
|
+
.mth-other{background:#1e293b;color:#94a3b8}
|
|
335
|
+
.req-path{font-size:.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#cbd5e1}
|
|
336
|
+
.req-meta{text-align:right;white-space:nowrap}
|
|
337
|
+
.st{font-size:.75rem;font-weight:600}
|
|
338
|
+
.st-2{color:#4ade80}.st-3{color:#60a5fa}.st-4{color:#fbbf24}.st-5{color:#f87171}.st-p{color:#64748b}
|
|
339
|
+
.dur{font-size:.7rem;color:#475569;margin-top:2px}
|
|
340
|
+
#detail-panel{flex:1;overflow-y:auto;padding:16px 20px}
|
|
341
|
+
#detail-panel.center{display:flex;align-items:center;justify-content:center}
|
|
342
|
+
.ph{display:flex;flex-direction:column;align-items:center;gap:10px;color:#475569;font-size:.9rem}
|
|
343
|
+
.summary{display:flex;align-items:center;gap:10px;background:#1e293b;padding:10px 14px;border-radius:8px;margin-bottom:16px;font-size:.875rem;flex-wrap:wrap}
|
|
344
|
+
.summary .s-path{color:#94a3b8;flex:1;word-break:break-all;font-family:monospace;font-size:.8rem}
|
|
345
|
+
.summary .s-dur{font-size:.75rem;color:#64748b}
|
|
346
|
+
.section{margin-bottom:18px}
|
|
347
|
+
.section-hdr{font-size:.75rem;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px;display:flex;align-items:center;gap:8px}
|
|
348
|
+
.section-hdr::after{content:"";flex:1;height:1px;background:#334155}
|
|
349
|
+
.hdr-tbl{width:100%;border-collapse:collapse;font-size:.78rem}
|
|
350
|
+
.hdr-tbl tr:nth-child(odd) td{background:#111827}
|
|
351
|
+
.hdr-tbl td{padding:4px 8px;vertical-align:top;word-break:break-word}
|
|
352
|
+
.hdr-tbl td:first-child{color:#7dd3fc;font-family:monospace;white-space:nowrap;width:36%;padding-right:12px}
|
|
353
|
+
.hdr-tbl td:last-child{color:#e2e8f0;font-family:monospace}
|
|
354
|
+
.body-box{background:#111827;border:1px solid #334155;border-radius:6px}
|
|
355
|
+
.body-pre{padding:10px 12px;font-family:'Cascadia Code',Consolas,monospace;font-size:.78rem;line-height:1.65;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow-y:auto;color:#e2e8f0}
|
|
356
|
+
.body-none{padding:10px 12px;color:#475569;font-style:italic;font-size:.8rem}
|
|
357
|
+
.no-data{color:#475569;font-size:.8rem;font-style:italic}
|
|
358
|
+
::-webkit-scrollbar{width:5px;height:5px}
|
|
359
|
+
::-webkit-scrollbar-track{background:#0f172a}
|
|
360
|
+
::-webkit-scrollbar-thumb{background:#334155;border-radius:3px}
|
|
361
|
+
::-webkit-scrollbar-thumb:hover{background:#475569}
|
|
362
|
+
</style>
|
|
363
|
+
</head>
|
|
364
|
+
<!--
|
|
365
|
+
hx-ext="sse" – enable the HTMX SSE extension on the whole page
|
|
366
|
+
sse-connect="/events" – open an EventSource to our /events endpoint
|
|
367
|
+
_="..." – Hyperscript: update the connection-status dot/label
|
|
368
|
+
when HTMX fires htmx:sseOpen / htmx:sseError on body
|
|
369
|
+
-->
|
|
370
|
+
<body hx-ext="sse" sse-connect="/events"
|
|
371
|
+
_="on htmx:sseOpen
|
|
372
|
+
remove .off from #dot
|
|
373
|
+
add .live to #dot
|
|
374
|
+
set the textContent of #conn-label to 'Connected'
|
|
375
|
+
on htmx:sseError
|
|
376
|
+
remove .live from #dot
|
|
377
|
+
add .off to #dot
|
|
378
|
+
set the textContent of #conn-label to 'Disconnected \u2014 retrying\u2026'">
|
|
379
|
+
|
|
380
|
+
<header>
|
|
381
|
+
<div class="logo">⚡ Porter Agent — Live Traffic</div>
|
|
382
|
+
<div class="conn-status">
|
|
383
|
+
<div class="dot off" id="dot"></div>
|
|
384
|
+
<span id="conn-label">Connecting…</span>
|
|
385
|
+
</div>
|
|
386
|
+
</header>
|
|
387
|
+
|
|
388
|
+
<div class="toolbar">
|
|
389
|
+
<!--
|
|
390
|
+
hx-delete="/requests" – DELETE /requests clears server state
|
|
391
|
+
hx-target / hx-swap – main response goes into #list-panel innerHTML
|
|
392
|
+
The server response also carries OOB patches for #detail-panel and
|
|
393
|
+
#req-count so everything resets in one round-trip, no JS needed.
|
|
394
|
+
-->
|
|
395
|
+
<button class="btn"
|
|
396
|
+
hx-delete="/requests"
|
|
397
|
+
hx-target="#list-panel"
|
|
398
|
+
hx-swap="innerHTML">Clear</button>
|
|
399
|
+
<span id="req-count">0 requests</span>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<main>
|
|
403
|
+
<div id="list-panel">
|
|
404
|
+
<div class="empty-list" id="empty-msg">
|
|
405
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
|
406
|
+
<span>Waiting for requests…</span>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
<div id="detail-panel" class="center">
|
|
410
|
+
<div class="ph">
|
|
411
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
412
|
+
<span>Select a request to view details</span>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</main>
|
|
416
|
+
|
|
417
|
+
<!--
|
|
418
|
+
Hidden SSE sink: absorbs "ui-update" events from the server.
|
|
419
|
+
Each "ui-update" message body carries one or more hx-swap-oob fragments
|
|
420
|
+
that HTMX applies directly to matching elements in the DOM
|
|
421
|
+
(new rows, count badge, row status patches, etc.).
|
|
422
|
+
-->
|
|
423
|
+
<div id="sse-sink" sse-swap="ui-update" hx-swap="innerHTML" style="display:none"></div>
|
|
424
|
+
|
|
425
|
+
<!--
|
|
426
|
+
Row click delegation
|
|
427
|
+
───────────────────
|
|
428
|
+
Rows are injected/replaced continuously via SSE OOB swaps. The SSE
|
|
429
|
+
extension does not guarantee that htmx.process() is called on every
|
|
430
|
+
new/replaced element, so per-element hx-get/hx-trigger attributes
|
|
431
|
+
are unreliable. A single delegated listener on the stable #list-panel
|
|
432
|
+
container works regardless of how many times rows are swapped.
|
|
433
|
+
-->
|
|
434
|
+
<script>
|
|
435
|
+
(function () {
|
|
436
|
+
document.getElementById('list-panel').addEventListener('click', function (e) {
|
|
437
|
+
var row = e.target.closest('[id^="row-"]');
|
|
438
|
+
if (!row) return;
|
|
439
|
+
document.querySelectorAll('.req-row').forEach(function (r) { r.classList.remove('active'); });
|
|
440
|
+
row.classList.add('active');
|
|
441
|
+
document.getElementById('detail-panel').classList.remove('center');
|
|
442
|
+
htmx.ajax('GET', '/request/' + row.id.replace(/^row-/, ''), {
|
|
443
|
+
target: '#detail-panel',
|
|
444
|
+
swap: 'innerHTML',
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
}());
|
|
448
|
+
<\/script>
|
|
449
|
+
|
|
450
|
+
</body>
|
|
451
|
+
</html>`;
|
|
452
|
+
exports.INDEX_HTML = INDEX_HTML;
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
/***/ }),
|
|
456
|
+
|
|
457
|
+
/***/ 366:
|
|
458
|
+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
|
459
|
+
|
|
460
|
+
"use strict";
|
|
461
|
+
|
|
462
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
463
|
+
if (k2 === undefined) k2 = k;
|
|
464
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
465
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
466
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
467
|
+
}
|
|
468
|
+
Object.defineProperty(o, k2, desc);
|
|
469
|
+
}) : (function(o, m, k, k2) {
|
|
470
|
+
if (k2 === undefined) k2 = k;
|
|
471
|
+
o[k2] = m[k];
|
|
472
|
+
}));
|
|
473
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
474
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
475
|
+
};
|
|
476
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
477
|
+
__exportStar(__nccwpck_require__(823), exports);
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
/***/ }),
|
|
481
|
+
|
|
482
|
+
/***/ 448:
|
|
483
|
+
/***/ ((__unused_webpack_module, exports) => {
|
|
484
|
+
|
|
485
|
+
"use strict";
|
|
486
|
+
|
|
487
|
+
// ── HTML rendering helpers ────────────────────────────────────────────────────
|
|
488
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
489
|
+
exports.bodyHtml = exports.headersHtml = exports.stClass = exports.mthClass = exports.esc = void 0;
|
|
490
|
+
const esc = (s) => {
|
|
491
|
+
return String(s ?? "")
|
|
492
|
+
.replace(/&/g, "&")
|
|
493
|
+
.replace(/</g, "<")
|
|
494
|
+
.replace(/>/g, ">")
|
|
495
|
+
.replace(/"/g, """)
|
|
496
|
+
.replace(/'/g, "'");
|
|
497
|
+
};
|
|
498
|
+
exports.esc = esc;
|
|
499
|
+
const mthClass = (m) => {
|
|
500
|
+
return ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].includes(m)
|
|
501
|
+
? `mth-${m}`
|
|
502
|
+
: "mth-other";
|
|
503
|
+
};
|
|
504
|
+
exports.mthClass = mthClass;
|
|
505
|
+
const stClass = (s) => {
|
|
506
|
+
if (!s)
|
|
507
|
+
return "st-p";
|
|
508
|
+
if (s >= 500)
|
|
509
|
+
return "st-5";
|
|
510
|
+
if (s >= 400)
|
|
511
|
+
return "st-4";
|
|
512
|
+
if (s >= 300)
|
|
513
|
+
return "st-3";
|
|
514
|
+
return "st-2";
|
|
515
|
+
};
|
|
516
|
+
exports.stClass = stClass;
|
|
517
|
+
const decodeBodyChunks = (chunks) => {
|
|
518
|
+
if (!chunks.length)
|
|
519
|
+
return "";
|
|
520
|
+
try {
|
|
521
|
+
return chunks
|
|
522
|
+
.map((c) => Buffer.from(c, "base64").toString("utf8"))
|
|
523
|
+
.join("");
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
return chunks.join("");
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
const prettyBody = (raw) => {
|
|
530
|
+
try {
|
|
531
|
+
return JSON.stringify(JSON.parse(raw), null, 2);
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
return raw;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
const headersHtml = (headers) => {
|
|
538
|
+
const entries = Object.entries(headers ?? {});
|
|
539
|
+
if (!entries.length) {
|
|
540
|
+
return `<span class="no-data">No headers</span>`;
|
|
541
|
+
}
|
|
542
|
+
const rows = entries
|
|
543
|
+
.map(([k, v]) => `<tr><td>${esc(k)}</td><td>${esc(String(v))}</td></tr>`)
|
|
544
|
+
.join("");
|
|
545
|
+
return `<table class="hdr-tbl"><tbody>${rows}</tbody></table>`;
|
|
546
|
+
};
|
|
547
|
+
exports.headersHtml = headersHtml;
|
|
548
|
+
const bodyHtml = (chunks) => {
|
|
549
|
+
const raw = decodeBodyChunks(chunks);
|
|
550
|
+
if (!raw) {
|
|
551
|
+
return `<div class="body-box"><div class="body-none">No body</div></div>`;
|
|
552
|
+
}
|
|
553
|
+
return `<div class="body-box"><pre class="body-pre">${esc(prettyBody(raw))}</pre></div>`;
|
|
554
|
+
};
|
|
555
|
+
exports.bodyHtml = bodyHtml;
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
/***/ }),
|
|
559
|
+
|
|
560
|
+
/***/ 127:
|
|
561
|
+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
|
562
|
+
|
|
563
|
+
"use strict";
|
|
564
|
+
|
|
565
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
566
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
567
|
+
};
|
|
568
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
569
|
+
exports.startUIServer = startUIServer;
|
|
570
|
+
const node_http_1 = __importDefault(__nccwpck_require__(67));
|
|
571
|
+
const events_1 = __nccwpck_require__(265);
|
|
572
|
+
const storage_1 = __importDefault(__nccwpck_require__(892));
|
|
573
|
+
const types_1 = __nccwpck_require__(888);
|
|
574
|
+
const channel_1 = __importDefault(__nccwpck_require__(418));
|
|
575
|
+
const buffer_1 = __nccwpck_require__(88);
|
|
576
|
+
const html_1 = __nccwpck_require__(366);
|
|
577
|
+
const channel = new channel_1.default();
|
|
578
|
+
const records = new storage_1.default();
|
|
579
|
+
const processEvent = (event, data) => {
|
|
580
|
+
const channelEvents = [];
|
|
581
|
+
switch (event) {
|
|
582
|
+
case "request-start": {
|
|
583
|
+
const record = new types_1.RequestRecord(data);
|
|
584
|
+
records.set(record.requestId, record);
|
|
585
|
+
channelEvents.push({
|
|
586
|
+
event: "ui-update",
|
|
587
|
+
fragments: [
|
|
588
|
+
// Prepend the new row into #list-panel
|
|
589
|
+
(0, html_1.rowHtml)(record, "afterbegin:#list-panel"),
|
|
590
|
+
// Update the request count badge
|
|
591
|
+
(0, html_1.countOobHtml)(records.size()),
|
|
592
|
+
],
|
|
593
|
+
});
|
|
594
|
+
// On the very first request hide the "waiting" placeholder
|
|
595
|
+
if (records.size() === 1) {
|
|
596
|
+
channelEvents.push({
|
|
597
|
+
event: "ui-update",
|
|
598
|
+
fragments: [
|
|
599
|
+
`<div id="empty-msg" hx-swap-oob="outerHTML" style="display:none"></div>`,
|
|
600
|
+
],
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
case "request-data": {
|
|
606
|
+
records.get(data.requestId)?.addRequestBody(data);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case "request-end": {
|
|
610
|
+
// Nothing to broadcast; detail updates happen on response-end
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
case "response-start": {
|
|
614
|
+
const r = records.get(data.requestId);
|
|
615
|
+
if (!r)
|
|
616
|
+
break;
|
|
617
|
+
r.setResponseStart(data);
|
|
618
|
+
// Patch the row in-place (status badge update)
|
|
619
|
+
channelEvents.push({
|
|
620
|
+
event: "ui-update",
|
|
621
|
+
fragments: [(0, html_1.rowHtml)(r, `outerHTML:#row-${r.requestId}`)],
|
|
622
|
+
});
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
case "response-data": {
|
|
626
|
+
records.get(data.requestId)?.addResponseBody(data);
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
case "response-end": {
|
|
630
|
+
const r = records.get(data.requestId);
|
|
631
|
+
if (!r)
|
|
632
|
+
break;
|
|
633
|
+
r.setResponseEnd(data);
|
|
634
|
+
// Patch the row (timing + remove .pending)
|
|
635
|
+
channelEvents.push({
|
|
636
|
+
event: "ui-update",
|
|
637
|
+
fragments: [(0, html_1.rowHtml)(r, `outerHTML:#row-${r.requestId}`)],
|
|
638
|
+
});
|
|
639
|
+
// Push updated detail content to any open detail panel for this request
|
|
640
|
+
channelEvents.push({
|
|
641
|
+
event: `response-end-${r.requestId}`,
|
|
642
|
+
fragments: [(0, html_1.detailHtml)(r)],
|
|
643
|
+
});
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
channelEvents.forEach(({ event, fragments }) => {
|
|
648
|
+
channel.broadcast(event, fragments);
|
|
649
|
+
});
|
|
650
|
+
};
|
|
651
|
+
Object.values(buffer_1.EventType).forEach((event) => {
|
|
652
|
+
events_1.agentEvents.on(event, (data) => processEvent(event, data));
|
|
653
|
+
});
|
|
654
|
+
// ── HTTP server ───────────────────────────────────────────────────────────────
|
|
655
|
+
function startUIServer(port = 7676) {
|
|
656
|
+
const server = node_http_1.default.createServer((req, res) => {
|
|
657
|
+
const url = req.url ?? "/";
|
|
658
|
+
// ── GET /events (SSE stream) ─────────────────────────────────────────
|
|
659
|
+
if (url === "/events") {
|
|
660
|
+
res.writeHead(200, {
|
|
661
|
+
"Content-Type": "text/event-stream",
|
|
662
|
+
"Cache-Control": "no-cache",
|
|
663
|
+
Connection: "keep-alive",
|
|
664
|
+
"Access-Control-Allow-Origin": "*",
|
|
665
|
+
});
|
|
666
|
+
res.write(": connected\n\n");
|
|
667
|
+
// Replay current state so late-joining browsers see existing traffic
|
|
668
|
+
if (records.hasData()) {
|
|
669
|
+
const rows = records.getValues()
|
|
670
|
+
.reverse()
|
|
671
|
+
.map((r) => (0, html_1.rowHtml)(r))
|
|
672
|
+
.join("\n");
|
|
673
|
+
channel.send(res, "ui-update", [
|
|
674
|
+
`<div id="list-panel" hx-swap-oob="innerHTML">${rows}</div>`,
|
|
675
|
+
(0, html_1.countOobHtml)(records.size()),
|
|
676
|
+
`<div id="empty-msg" hx-swap-oob="outerHTML" style="display:none"></div>`,
|
|
677
|
+
].join("\n"));
|
|
678
|
+
}
|
|
679
|
+
channel.subscribe(res);
|
|
680
|
+
req.on("close", () => channel.unsubscribe(res));
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
// ── GET /request/:id (detail panel fragment) ─────────────────────────
|
|
684
|
+
const detailMatch = url.match(/^\/request\/([a-f0-9]+)$/);
|
|
685
|
+
if (req.method === "GET" && detailMatch) {
|
|
686
|
+
const r = records.get(detailMatch[1] ?? "");
|
|
687
|
+
if (!r) {
|
|
688
|
+
res.writeHead(404);
|
|
689
|
+
res.end();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
693
|
+
res.end((0, html_1.detailHtml)(r));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
// ── DELETE /requests (clear all) ────────────────────────────────────
|
|
697
|
+
if (req.method === "DELETE" && url === "/requests") {
|
|
698
|
+
records.clear();
|
|
699
|
+
// Main response → replaces #list-panel innerHTML (hx-target on button)
|
|
700
|
+
// OOB responses → reset #detail-panel and #req-count in the same trip
|
|
701
|
+
const body = html_1.EMPTY_MSG_HTML +
|
|
702
|
+
html_1.EMPTY_DETAIL_HTML +
|
|
703
|
+
`<span id="req-count" hx-swap-oob="outerHTML">0 requests</span>`;
|
|
704
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
705
|
+
res.end(body);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
// ── GET / (main page) ────────────────────────────────────────────────
|
|
709
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
710
|
+
res.end(html_1.INDEX_HTML);
|
|
711
|
+
});
|
|
712
|
+
server.on("error", (err) => {
|
|
713
|
+
if (err.code === "EADDRINUSE") {
|
|
714
|
+
console.error(`⚠️ Web UI port ${port} is already in use. UI will not be available.`);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
console.error("Web UI server error:", err.message);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
server.listen(port, "127.0.0.1", () => {
|
|
721
|
+
console.log(`\uD83C\uDF10 Web UI available at http://localhost:${port}`);
|
|
722
|
+
});
|
|
723
|
+
return server;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
/***/ }),
|
|
728
|
+
|
|
729
|
+
/***/ 892:
|
|
730
|
+
/***/ ((__unused_webpack_module, exports) => {
|
|
731
|
+
|
|
732
|
+
"use strict";
|
|
733
|
+
|
|
734
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
735
|
+
class Storage {
|
|
736
|
+
store;
|
|
737
|
+
constructor() {
|
|
738
|
+
this.store = new Map();
|
|
739
|
+
}
|
|
740
|
+
size = () => {
|
|
741
|
+
return this.store.size;
|
|
742
|
+
};
|
|
743
|
+
hasData = () => {
|
|
744
|
+
return this.store.size > 0;
|
|
745
|
+
};
|
|
746
|
+
has = (key) => {
|
|
747
|
+
return this.store.has(key);
|
|
748
|
+
};
|
|
749
|
+
get = (key) => {
|
|
750
|
+
return this.store.get(key);
|
|
751
|
+
};
|
|
752
|
+
getValues = () => {
|
|
753
|
+
return Array.from(this.store.values());
|
|
754
|
+
};
|
|
755
|
+
set = (key, value) => {
|
|
756
|
+
this.store.set(key, value);
|
|
757
|
+
};
|
|
758
|
+
delete = (key) => {
|
|
759
|
+
this.store.delete(key);
|
|
760
|
+
};
|
|
761
|
+
clear = () => {
|
|
762
|
+
this.store.clear();
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
exports["default"] = Storage;
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
/***/ }),
|
|
769
|
+
|
|
770
|
+
/***/ 888:
|
|
771
|
+
/***/ ((__unused_webpack_module, exports) => {
|
|
772
|
+
|
|
773
|
+
"use strict";
|
|
774
|
+
|
|
775
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
776
|
+
exports.RequestRecord = void 0;
|
|
777
|
+
class RequestRecord {
|
|
778
|
+
requestId;
|
|
779
|
+
method;
|
|
780
|
+
path;
|
|
781
|
+
reqHeaders;
|
|
782
|
+
reqBodyChunks; // base64 encoded chunks
|
|
783
|
+
responseStatus;
|
|
784
|
+
resHeaders;
|
|
785
|
+
resBodyChunks; // base64 encoded chunks
|
|
786
|
+
startTime;
|
|
787
|
+
endTime;
|
|
788
|
+
done;
|
|
789
|
+
constructor({ requestId, payload, timestamp }) {
|
|
790
|
+
this.requestId = requestId;
|
|
791
|
+
this.method = payload.method;
|
|
792
|
+
this.path = payload.path;
|
|
793
|
+
this.reqHeaders = payload.headers;
|
|
794
|
+
this.startTime = timestamp;
|
|
795
|
+
this.reqBodyChunks = [];
|
|
796
|
+
this.resHeaders = {};
|
|
797
|
+
this.resBodyChunks = [];
|
|
798
|
+
this.responseStatus = null;
|
|
799
|
+
this.endTime = null;
|
|
800
|
+
this.done = false;
|
|
801
|
+
}
|
|
802
|
+
log(x) {
|
|
803
|
+
console.log(`[${this.requestId}]`, x, 'json:', JSON.stringify(x));
|
|
804
|
+
}
|
|
805
|
+
addRequestBody = ({ payload }) => {
|
|
806
|
+
if (!payload)
|
|
807
|
+
return;
|
|
808
|
+
this.reqBodyChunks.push(payload);
|
|
809
|
+
};
|
|
810
|
+
addResponseBody = ({ payload }) => {
|
|
811
|
+
if (!payload)
|
|
812
|
+
return;
|
|
813
|
+
this.resBodyChunks.push(payload);
|
|
814
|
+
};
|
|
815
|
+
setResponseStart = ({ payload }) => {
|
|
816
|
+
this.responseStatus = payload?.status;
|
|
817
|
+
this.resHeaders = payload?.headers || {};
|
|
818
|
+
};
|
|
819
|
+
setResponseEnd = ({ timestamp }) => {
|
|
820
|
+
this.endTime = timestamp;
|
|
821
|
+
this.done = true;
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
exports.RequestRecord = RequestRecord;
|
|
825
|
+
|
|
826
|
+
|
|
101
827
|
/***/ }),
|
|
102
828
|
|
|
103
829
|
/***/ 257:
|
|
@@ -113,6 +839,8 @@ exports.upgradeHandler = void 0;
|
|
|
113
839
|
const node_http_1 = __importDefault(__nccwpck_require__(67));
|
|
114
840
|
const buffer_1 = __nccwpck_require__(88);
|
|
115
841
|
const config_1 = __nccwpck_require__(750);
|
|
842
|
+
const events_1 = __nccwpck_require__(265);
|
|
843
|
+
const requests = new Map();
|
|
116
844
|
const upgradeHandler = (localPort) => (res, socket) => {
|
|
117
845
|
let tunnelId = null;
|
|
118
846
|
let buffer = Buffer.alloc(0);
|
|
@@ -127,51 +855,126 @@ const upgradeHandler = (localPort) => (res, socket) => {
|
|
|
127
855
|
const { frames, remaining } = (0, buffer_1.decodeFrames)(Buffer.concat([buffer, chunk]));
|
|
128
856
|
buffer = remaining;
|
|
129
857
|
frames.forEach((frame) => {
|
|
130
|
-
if (frame.type
|
|
858
|
+
if (frame.type < buffer_1.FrameType.REQUEST_START ||
|
|
859
|
+
frame.type > buffer_1.FrameType.REQUEST_END)
|
|
131
860
|
return;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
res.on("data", (c) => (body += c));
|
|
145
|
-
res.on("end", () => socket.write((0, buffer_1.encodeFrame)({
|
|
146
|
-
type: buffer_1.FrameType.RESPONSE,
|
|
861
|
+
if (frame.type === buffer_1.FrameType.REQUEST_START) {
|
|
862
|
+
const options = {
|
|
863
|
+
host: "localhost",
|
|
864
|
+
port: parseInt(localPort, 10),
|
|
865
|
+
method: frame.payload.method,
|
|
866
|
+
path: frame.payload.path,
|
|
867
|
+
headers: sanitizeHeaders(frame.payload.headers, localPort),
|
|
868
|
+
};
|
|
869
|
+
// console.log(`➡️ Incoming request for tunnel ${tunnelId}: `, options);
|
|
870
|
+
// Using ANSI escape codes
|
|
871
|
+
console.log(`- \x1b[32m${options.method}\x1b[0m \x1b[34m${options.path}\x1b[0m`);
|
|
872
|
+
tunnelFrame({
|
|
147
873
|
requestId: frame.requestId,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
874
|
+
type: buffer_1.FrameType.REQUEST_START,
|
|
875
|
+
payload: frame.payload,
|
|
876
|
+
});
|
|
877
|
+
const proxy = node_http_1.default.request(options, (res) => {
|
|
878
|
+
// send response start
|
|
879
|
+
tunnelFrame({
|
|
880
|
+
requestId: frame.requestId,
|
|
881
|
+
type: buffer_1.FrameType.RESPONSE_START,
|
|
882
|
+
payload: {
|
|
883
|
+
status: res.statusCode || 500,
|
|
884
|
+
headers: res.headers,
|
|
885
|
+
},
|
|
886
|
+
});
|
|
887
|
+
// pipe response data
|
|
888
|
+
res.on("data", (c) => {
|
|
889
|
+
// send response data
|
|
890
|
+
tunnelFrame({
|
|
891
|
+
requestId: frame.requestId,
|
|
892
|
+
type: buffer_1.FrameType.RESPONSE_DATA,
|
|
893
|
+
payload: c,
|
|
894
|
+
});
|
|
895
|
+
});
|
|
896
|
+
// response end
|
|
897
|
+
res.on("end", () => {
|
|
898
|
+
tunnelFrame({
|
|
899
|
+
requestId: frame.requestId,
|
|
900
|
+
type: buffer_1.FrameType.RESPONSE_END,
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
proxy.on("error", (err) => {
|
|
905
|
+
tunnelFrame({
|
|
906
|
+
requestId: frame.requestId,
|
|
907
|
+
type: buffer_1.FrameType.RESPONSE_START,
|
|
908
|
+
payload: {
|
|
909
|
+
status: 502,
|
|
910
|
+
headers: {},
|
|
911
|
+
body: "Bad Gateway: " + err.message,
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
tunnelFrame({
|
|
915
|
+
requestId: frame.requestId,
|
|
916
|
+
type: buffer_1.FrameType.RESPONSE_END,
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
requests.set(frame.requestId, proxy);
|
|
920
|
+
}
|
|
921
|
+
else if (frame.type === buffer_1.FrameType.REQUEST_DATA) {
|
|
922
|
+
tunnelFrame({
|
|
923
|
+
requestId: frame.requestId,
|
|
924
|
+
type: buffer_1.FrameType.REQUEST_DATA,
|
|
925
|
+
payload: frame.payload,
|
|
926
|
+
});
|
|
927
|
+
requests.get(frame.requestId)?.write(frame.payload);
|
|
928
|
+
}
|
|
929
|
+
else if (frame.type === buffer_1.FrameType.REQUEST_END) {
|
|
930
|
+
tunnelFrame({
|
|
931
|
+
requestId: frame.requestId,
|
|
932
|
+
type: buffer_1.FrameType.REQUEST_END,
|
|
933
|
+
});
|
|
934
|
+
requests.get(frame.requestId)?.end();
|
|
935
|
+
requests.delete(frame.requestId);
|
|
936
|
+
}
|
|
165
937
|
});
|
|
166
938
|
});
|
|
167
939
|
socket.on("error", (err) => {
|
|
168
940
|
console.error("Socket error:", err);
|
|
169
941
|
});
|
|
170
942
|
socket.on("close", () => {
|
|
171
|
-
console.log("Agent disconnected");
|
|
943
|
+
console.log("🚪 Agent disconnected");
|
|
172
944
|
});
|
|
945
|
+
const tunnelFrame = (frame) => {
|
|
946
|
+
// Only allow response frames to be sent back to the agent to prevent request spoofing
|
|
947
|
+
if ((frame.type === buffer_1.FrameType.RESPONSE_START ||
|
|
948
|
+
frame.type === buffer_1.FrameType.RESPONSE_DATA ||
|
|
949
|
+
frame.type === buffer_1.FrameType.RESPONSE_END)) {
|
|
950
|
+
socket.write((0, buffer_1.encodeFrame)(frame));
|
|
951
|
+
}
|
|
952
|
+
const eventPayload = {
|
|
953
|
+
...frame,
|
|
954
|
+
timestamp: Date.now(),
|
|
955
|
+
};
|
|
956
|
+
events_1.agentEvents.emit((0, buffer_1.getEventName)(frame.type), eventPayload);
|
|
957
|
+
};
|
|
173
958
|
};
|
|
174
959
|
exports.upgradeHandler = upgradeHandler;
|
|
960
|
+
const sanitizeHeaders = (headers, port) => {
|
|
961
|
+
const clean = {};
|
|
962
|
+
for (const [k, v] of Object.entries(headers || {})) {
|
|
963
|
+
const key = k.toLowerCase();
|
|
964
|
+
if (key.startsWith(":") ||
|
|
965
|
+
[
|
|
966
|
+
"connection",
|
|
967
|
+
"upgrade",
|
|
968
|
+
"content-length",
|
|
969
|
+
"accept-encoding",
|
|
970
|
+
"transfer-encoding",
|
|
971
|
+
].includes(key))
|
|
972
|
+
continue;
|
|
973
|
+
clean[key] = v;
|
|
974
|
+
}
|
|
975
|
+
clean["host"] = `localhost:${port}`;
|
|
976
|
+
return clean;
|
|
977
|
+
};
|
|
175
978
|
|
|
176
979
|
|
|
177
980
|
/***/ }),
|
|
@@ -182,47 +985,94 @@ exports.upgradeHandler = upgradeHandler;
|
|
|
182
985
|
"use strict";
|
|
183
986
|
|
|
184
987
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
185
|
-
exports.decodeTunnelId = exports.decodeFrames = exports.encodeFrame = exports.FrameType = void 0;
|
|
988
|
+
exports.decodeTunnelId = exports.decodeFrames = exports.encodeFrame = exports.getEventName = exports.EventType = exports.FrameType = void 0;
|
|
186
989
|
const node_buffer_1 = __nccwpck_require__(573);
|
|
187
|
-
const LENGTH = {
|
|
188
|
-
LENGTH_FIELD: 4,
|
|
189
|
-
};
|
|
190
990
|
var FrameType;
|
|
191
991
|
(function (FrameType) {
|
|
192
|
-
|
|
193
|
-
FrameType[FrameType["
|
|
194
|
-
|
|
992
|
+
// Tunnel initialization
|
|
993
|
+
FrameType[FrameType["TUNNEL_INIT"] = 0] = "TUNNEL_INIT";
|
|
994
|
+
// Requests
|
|
995
|
+
FrameType[FrameType["REQUEST_START"] = 1] = "REQUEST_START";
|
|
996
|
+
FrameType[FrameType["REQUEST_DATA"] = 2] = "REQUEST_DATA";
|
|
997
|
+
FrameType[FrameType["REQUEST_END"] = 3] = "REQUEST_END";
|
|
998
|
+
// Responses
|
|
999
|
+
FrameType[FrameType["RESPONSE_START"] = 4] = "RESPONSE_START";
|
|
1000
|
+
FrameType[FrameType["RESPONSE_DATA"] = 5] = "RESPONSE_DATA";
|
|
1001
|
+
FrameType[FrameType["RESPONSE_END"] = 6] = "RESPONSE_END";
|
|
195
1002
|
})(FrameType || (exports.FrameType = FrameType = {}));
|
|
1003
|
+
var EventType;
|
|
1004
|
+
(function (EventType) {
|
|
1005
|
+
EventType["REQUEST_START"] = "request-start";
|
|
1006
|
+
EventType["REQUEST_DATA"] = "request-data";
|
|
1007
|
+
EventType["REQUEST_END"] = "request-end";
|
|
1008
|
+
EventType["RESPONSE_START"] = "response-start";
|
|
1009
|
+
EventType["RESPONSE_DATA"] = "response-data";
|
|
1010
|
+
EventType["RESPONSE_END"] = "response-end";
|
|
1011
|
+
EventType["UNKNOWN"] = "unknown-event";
|
|
1012
|
+
})(EventType || (exports.EventType = EventType = {}));
|
|
1013
|
+
const FrameName = {
|
|
1014
|
+
[FrameType.TUNNEL_INIT]: EventType.REQUEST_START, // Not really an event, but we can reuse the payload structure
|
|
1015
|
+
[FrameType.REQUEST_START]: EventType.REQUEST_START,
|
|
1016
|
+
[FrameType.REQUEST_DATA]: EventType.REQUEST_DATA,
|
|
1017
|
+
[FrameType.REQUEST_END]: EventType.REQUEST_END,
|
|
1018
|
+
[FrameType.RESPONSE_START]: EventType.RESPONSE_START,
|
|
1019
|
+
[FrameType.RESPONSE_DATA]: EventType.RESPONSE_DATA,
|
|
1020
|
+
[FrameType.RESPONSE_END]: EventType.RESPONSE_END,
|
|
1021
|
+
};
|
|
1022
|
+
const getEventName = (type) => {
|
|
1023
|
+
if (type in FrameName) {
|
|
1024
|
+
return FrameName[type];
|
|
1025
|
+
}
|
|
1026
|
+
return EventType.UNKNOWN;
|
|
1027
|
+
};
|
|
1028
|
+
exports.getEventName = getEventName;
|
|
1029
|
+
/**
|
|
1030
|
+
* Frame format:
|
|
1031
|
+
* [4 bytes length][1 byte type][8 bytes requestId][payload...]
|
|
1032
|
+
*/
|
|
196
1033
|
const encodeFrame = (frame) => {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
1034
|
+
const payload = frame.payload instanceof node_buffer_1.Buffer
|
|
1035
|
+
? frame.payload
|
|
1036
|
+
: frame.payload
|
|
1037
|
+
? node_buffer_1.Buffer.from(JSON.stringify(frame.payload))
|
|
1038
|
+
: node_buffer_1.Buffer.alloc(0);
|
|
1039
|
+
const header = node_buffer_1.Buffer.alloc(13); // 4 + 1 + 8
|
|
1040
|
+
header.writeUInt32BE(payload.length + 9, 0); // total length = type(1) + requestId(8) + payload
|
|
1041
|
+
header.writeUInt8(frame.type, 4); // type
|
|
1042
|
+
header.write(frame.requestId, 5, 8, "hex"); // requestId
|
|
1043
|
+
return node_buffer_1.Buffer.concat([header, payload]); // final frame buffer
|
|
203
1044
|
};
|
|
204
1045
|
exports.encodeFrame = encodeFrame;
|
|
205
1046
|
const decodeFrames = (buffer) => {
|
|
206
|
-
let offset = 0;
|
|
207
|
-
let len = buffer.readInt32BE(0);
|
|
208
1047
|
const frames = [];
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
offset
|
|
213
|
-
if (buffer.length - offset < LENGTH.LENGTH_FIELD)
|
|
1048
|
+
let offset = 0;
|
|
1049
|
+
while (buffer.length - offset >= 4) {
|
|
1050
|
+
const len = buffer.readUInt32BE(offset);
|
|
1051
|
+
if (buffer.length - offset < len + 4)
|
|
214
1052
|
break;
|
|
215
|
-
|
|
1053
|
+
const type = buffer.readUInt8(offset + 4); // type
|
|
1054
|
+
const requestId = buffer
|
|
1055
|
+
.slice(offset + 5, offset + 13)
|
|
1056
|
+
.toString("hex"); //
|
|
1057
|
+
const payloadBuf = buffer.slice(offset + 13, offset + 4 + len);
|
|
1058
|
+
let payload = payloadBuf;
|
|
1059
|
+
if (type === FrameType.TUNNEL_INIT ||
|
|
1060
|
+
type === FrameType.REQUEST_START ||
|
|
1061
|
+
type === FrameType.RESPONSE_START) {
|
|
1062
|
+
payload = JSON.parse(payloadBuf.toString());
|
|
1063
|
+
}
|
|
1064
|
+
frames.push({ type, requestId, payload });
|
|
1065
|
+
offset += len + 4;
|
|
216
1066
|
}
|
|
217
1067
|
return { frames, remaining: buffer.slice(offset) };
|
|
218
1068
|
};
|
|
219
1069
|
exports.decodeFrames = decodeFrames;
|
|
220
1070
|
const decodeTunnelId = (buffer) => {
|
|
221
1071
|
const data = decodeFrames(buffer).frames[0];
|
|
222
|
-
if (data?.type !== FrameType.
|
|
1072
|
+
if (data?.type !== FrameType.TUNNEL_INIT) {
|
|
223
1073
|
throw new Error("Invalid frame type for tunnel ID");
|
|
224
1074
|
}
|
|
225
|
-
return data.tunnelId;
|
|
1075
|
+
return data.payload.tunnelId.toString();
|
|
226
1076
|
};
|
|
227
1077
|
exports.decodeTunnelId = decodeTunnelId;
|
|
228
1078
|
|
|
@@ -4564,7 +5414,7 @@ exports.suggestSimilar = suggestSimilar;
|
|
|
4564
5414
|
/***/ ((module) => {
|
|
4565
5415
|
|
|
4566
5416
|
"use strict";
|
|
4567
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@sandeepk1729/porter","version":"1.0
|
|
5417
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@sandeepk1729/porter","version":"1.2.0","description":"a port forwarding agent","main":"./dist/index.js","bin":{"porter":"dist/index.js"},"scripts":{"dev":"tsc -w & ncc build src/index.ts --out dist --watch","unlink":"npm unlink @sandeepk1729/porter -g","build":"tsc && ncc build src/index.ts --out dist","prepublishOnly":"npm run build","lint":"eslint \'src/**/*.ts\'"},"keywords":["port","agent","node","cli"],"homepage":"https://github.com/SandeepK1729/porter-agent#readme","bugs":{"url":"https://github.com/SandeepK1729/porter-agent/issues"},"repository":{"type":"git","url":"git+https://github.com/SandeepK1729/porter-agent.git"},"author":"SandeepK1729 <SandeepK1729+user@users.noreply.github.com>","license":"MIT","devDependencies":{"@types/node":"^24.3.0","@vercel/ncc":"^0.38.3","eslint":"^9.35.0","prettier":"^3.6.2","typescript":"^5.9.2"},"publishConfig":{"access":"public","directory":"dist","registry":"https://registry.npmjs.org"},"files":["dist/","README.md","package.json"],"dependencies":{"commander":"^14.0.0"}}');
|
|
4568
5418
|
|
|
4569
5419
|
/***/ })
|
|
4570
5420
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandeepk1729/porter",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "a port forwarding agent",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "tsc -w & ncc build src/index.ts --out dist --watch",
|
|
11
|
-
"unlink": "npm unlink @sandeepk1729/
|
|
11
|
+
"unlink": "npm unlink @sandeepk1729/porter -g",
|
|
12
12
|
"build": "tsc && ncc build src/index.ts --out dist",
|
|
13
13
|
"prepublishOnly": "npm run build",
|
|
14
14
|
"lint": "eslint 'src/**/*.ts'"
|