@sandeepk1729/porter 1.1.0 → 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 +809 -23
- 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,9 +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)
|
|
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.`))
|
|
28
38
|
.on("upgrade", (0, agent_1.upgradeHandler)(localPort))
|
|
29
39
|
.end();
|
|
30
40
|
});
|
|
@@ -92,6 +102,728 @@ if (!process.argv.slice(2).length) {
|
|
|
92
102
|
command_1.default.parse(process.argv);
|
|
93
103
|
|
|
94
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
|
+
|
|
95
827
|
/***/ }),
|
|
96
828
|
|
|
97
829
|
/***/ 257:
|
|
@@ -107,6 +839,7 @@ exports.upgradeHandler = void 0;
|
|
|
107
839
|
const node_http_1 = __importDefault(__nccwpck_require__(67));
|
|
108
840
|
const buffer_1 = __nccwpck_require__(88);
|
|
109
841
|
const config_1 = __nccwpck_require__(750);
|
|
842
|
+
const events_1 = __nccwpck_require__(265);
|
|
110
843
|
const requests = new Map();
|
|
111
844
|
const upgradeHandler = (localPort) => (res, socket) => {
|
|
112
845
|
let tunnelId = null;
|
|
@@ -136,51 +869,68 @@ const upgradeHandler = (localPort) => (res, socket) => {
|
|
|
136
869
|
// console.log(`➡️ Incoming request for tunnel ${tunnelId}: `, options);
|
|
137
870
|
// Using ANSI escape codes
|
|
138
871
|
console.log(`- \x1b[32m${options.method}\x1b[0m \x1b[34m${options.path}\x1b[0m`);
|
|
872
|
+
tunnelFrame({
|
|
873
|
+
requestId: frame.requestId,
|
|
874
|
+
type: buffer_1.FrameType.REQUEST_START,
|
|
875
|
+
payload: frame.payload,
|
|
876
|
+
});
|
|
139
877
|
const proxy = node_http_1.default.request(options, (res) => {
|
|
140
878
|
// send response start
|
|
141
|
-
|
|
142
|
-
type: buffer_1.FrameType.RESPONSE_START,
|
|
879
|
+
tunnelFrame({
|
|
143
880
|
requestId: frame.requestId,
|
|
881
|
+
type: buffer_1.FrameType.RESPONSE_START,
|
|
144
882
|
payload: {
|
|
145
883
|
status: res.statusCode || 500,
|
|
146
884
|
headers: res.headers,
|
|
147
885
|
},
|
|
148
|
-
})
|
|
886
|
+
});
|
|
149
887
|
// pipe response data
|
|
150
|
-
res.on("data", (c) =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
});
|
|
155
896
|
// response end
|
|
156
897
|
res.on("end", () => {
|
|
157
|
-
|
|
158
|
-
type: buffer_1.FrameType.RESPONSE_END,
|
|
898
|
+
tunnelFrame({
|
|
159
899
|
requestId: frame.requestId,
|
|
160
|
-
|
|
900
|
+
type: buffer_1.FrameType.RESPONSE_END,
|
|
901
|
+
});
|
|
161
902
|
});
|
|
162
903
|
});
|
|
163
904
|
proxy.on("error", (err) => {
|
|
164
|
-
|
|
165
|
-
type: buffer_1.FrameType.RESPONSE_START,
|
|
905
|
+
tunnelFrame({
|
|
166
906
|
requestId: frame.requestId,
|
|
907
|
+
type: buffer_1.FrameType.RESPONSE_START,
|
|
167
908
|
payload: {
|
|
168
909
|
status: 502,
|
|
169
910
|
headers: {},
|
|
170
911
|
body: "Bad Gateway: " + err.message,
|
|
171
912
|
},
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
type: buffer_1.FrameType.RESPONSE_END,
|
|
913
|
+
});
|
|
914
|
+
tunnelFrame({
|
|
175
915
|
requestId: frame.requestId,
|
|
176
|
-
|
|
916
|
+
type: buffer_1.FrameType.RESPONSE_END,
|
|
917
|
+
});
|
|
177
918
|
});
|
|
178
919
|
requests.set(frame.requestId, proxy);
|
|
179
920
|
}
|
|
180
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
|
+
});
|
|
181
927
|
requests.get(frame.requestId)?.write(frame.payload);
|
|
182
928
|
}
|
|
183
929
|
else if (frame.type === buffer_1.FrameType.REQUEST_END) {
|
|
930
|
+
tunnelFrame({
|
|
931
|
+
requestId: frame.requestId,
|
|
932
|
+
type: buffer_1.FrameType.REQUEST_END,
|
|
933
|
+
});
|
|
184
934
|
requests.get(frame.requestId)?.end();
|
|
185
935
|
requests.delete(frame.requestId);
|
|
186
936
|
}
|
|
@@ -192,6 +942,19 @@ const upgradeHandler = (localPort) => (res, socket) => {
|
|
|
192
942
|
socket.on("close", () => {
|
|
193
943
|
console.log("🚪 Agent disconnected");
|
|
194
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
|
+
};
|
|
195
958
|
};
|
|
196
959
|
exports.upgradeHandler = upgradeHandler;
|
|
197
960
|
const sanitizeHeaders = (headers, port) => {
|
|
@@ -222,11 +985,8 @@ const sanitizeHeaders = (headers, port) => {
|
|
|
222
985
|
"use strict";
|
|
223
986
|
|
|
224
987
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
225
|
-
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;
|
|
226
989
|
const node_buffer_1 = __nccwpck_require__(573);
|
|
227
|
-
const LENGTH = {
|
|
228
|
-
LENGTH_FIELD: 4,
|
|
229
|
-
};
|
|
230
990
|
var FrameType;
|
|
231
991
|
(function (FrameType) {
|
|
232
992
|
// Tunnel initialization
|
|
@@ -240,6 +1000,32 @@ var FrameType;
|
|
|
240
1000
|
FrameType[FrameType["RESPONSE_DATA"] = 5] = "RESPONSE_DATA";
|
|
241
1001
|
FrameType[FrameType["RESPONSE_END"] = 6] = "RESPONSE_END";
|
|
242
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;
|
|
243
1029
|
/**
|
|
244
1030
|
* Frame format:
|
|
245
1031
|
* [4 bytes length][1 byte type][8 bytes requestId][payload...]
|
|
@@ -4628,7 +5414,7 @@ exports.suggestSimilar = suggestSimilar;
|
|
|
4628
5414
|
/***/ ((module) => {
|
|
4629
5415
|
|
|
4630
5416
|
"use strict";
|
|
4631
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@sandeepk1729/porter","version":"1.
|
|
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"}}');
|
|
4632
5418
|
|
|
4633
5419
|
/***/ })
|
|
4634
5420
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandeepk1729/porter",
|
|
3
|
-
"version": "1.
|
|
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'"
|