packetsnitch 1.5.599

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.
Files changed (54) hide show
  1. package/.eslintrc.json +28 -0
  2. package/.webpack/x64/main/index.js +2 -0
  3. package/.webpack/x64/main/index.js.map +1 -0
  4. package/.webpack/x64/renderer/assets/css/rubikglitch.woff2 +0 -0
  5. package/.webpack/x64/renderer/assets/css/style.css +1916 -0
  6. package/.webpack/x64/renderer/assets/images/loading.gif +0 -0
  7. package/.webpack/x64/renderer/assets/images/logo.webp +0 -0
  8. package/.webpack/x64/renderer/assets/images/packet-snitch-tag.webp +0 -0
  9. package/.webpack/x64/renderer/main_window/index.html +3 -0
  10. package/.webpack/x64/renderer/main_window/index.js +3 -0
  11. package/.webpack/x64/renderer/main_window/index.js.LICENSE.txt +36 -0
  12. package/.webpack/x64/renderer/main_window/index.js.map +1 -0
  13. package/.webpack/x64/renderer/main_window/preload.js +2 -0
  14. package/.webpack/x64/renderer/main_window/preload.js.map +1 -0
  15. package/backend/common/GeoLite2-City.mmdb +0 -0
  16. package/backend/common/mac-vendors-export.csv +56923 -0
  17. package/backend/common/service-names-port-numbers.csv +15368 -0
  18. package/backend/requirements.txt +14 -0
  19. package/backend/snitch.py +3611 -0
  20. package/forge.config.js +80 -0
  21. package/package.json +102 -0
  22. package/ps-icon.ico +0 -0
  23. package/snitch.spec +44 -0
  24. package/src/assets/css/rubikglitch.woff2 +0 -0
  25. package/src/assets/css/style.css +1916 -0
  26. package/src/assets/images/loading.gif +0 -0
  27. package/src/assets/images/logo.webp +0 -0
  28. package/src/assets/images/packet-snitch-tag.webp +0 -0
  29. package/src/back-comm.js +70 -0
  30. package/src/decoders.js +579 -0
  31. package/src/filter.js +461 -0
  32. package/src/front.js +10 -0
  33. package/src/index.html +1036 -0
  34. package/src/logging.js +150 -0
  35. package/src/main.js +571 -0
  36. package/src/preload.js +73 -0
  37. package/src/renderer.js +30 -0
  38. package/src/ui/common-frontend.js +13 -0
  39. package/src/ui/context-menu.js +88 -0
  40. package/src/ui/decoders.js +1 -0
  41. package/src/ui/main-frontend.js +4957 -0
  42. package/src/ui/panels/crypt-panel.js +565 -0
  43. package/src/ui/panels/data-panel.js +151 -0
  44. package/src/ui/panels/data-tools-panel.js +939 -0
  45. package/src/ui/panels/install-screen.js +59 -0
  46. package/src/ui/panels/keystore-panel.js +1248 -0
  47. package/src/ui/panels/list-panel.js +403 -0
  48. package/src/ui/panels/stats-panel.js +351 -0
  49. package/src/ui/panels/summary-panel.js +63 -0
  50. package/webpack.main.config.js +11 -0
  51. package/webpack.plugins.js +13 -0
  52. package/webpack.preload.config.js +7 -0
  53. package/webpack.renderer.config.js +30 -0
  54. package/webpack.rules.js +35 -0
@@ -0,0 +1,4957 @@
1
+ import "../assets/css/style.css";
2
+ const CryptoJS = require("crypto-js");
3
+ const { sha3_256, sha3_512 } = require("js-sha3");
4
+ const whirlpool = require("whirlpool-js");
5
+ const { filterPackets, validateFilterSyntax } = require("../filter");
6
+ const { initializeLogging } = require("../logging");
7
+ const { initializeContextMenu } = require("./context-menu");
8
+ const {
9
+ createTable,
10
+ renderDnsTable,
11
+ renderIcmpTable,
12
+ renderSnmpTable,
13
+ renderDhcpTable,
14
+ renderNtpTable,
15
+ renderSipTable,
16
+ renderHttpTable,
17
+ renderFtpTable,
18
+ renderSmtpTable,
19
+ renderPop3Table,
20
+ renderImapTable,
21
+ renderTelnetTable,
22
+ renderIrcTable,
23
+ renderMtpTable,
24
+ renderLdapTable,
25
+ renderMysqlTable,
26
+ renderPostgresqlTable,
27
+ renderXmppTable,
28
+ renderSmbTable,
29
+ renderMqttTable,
30
+ renderRtspTable,
31
+ renderTftpTable,
32
+ renderBgpTable,
33
+ renderHttp2Table,
34
+ renderNntpTable,
35
+ renderRadiusTable,
36
+ } = require("./decoders");
37
+ const { createCryptPanel } = require("./panels/crypt-panel");
38
+ const {
39
+ createKeystorePanel,
40
+ CRYPT_KEYSTORE_MODE_SESSION,
41
+ CRYPT_KEYSTORE_MODE_PERSISTENT,
42
+ SESSION_KEYCHAIN_LABEL,
43
+ } = require("./panels/keystore-panel");
44
+ const { createStatsPanel } = require("./panels/stats-panel");
45
+ const { createListPanel } = require("./panels/list-panel");
46
+ const { createSummaryPanel } = require("./panels/summary-panel");
47
+ const { initializeInstallScreen } = require("./panels/install-screen");
48
+ const { createDataPanel } = require("./panels/data-panel");
49
+ const psVer = require("../../package.json").version;
50
+ const {
51
+ initConvPanel,
52
+ CONV_CONVERSIONS_SUBTAB,
53
+ CONV_HASHES_SUBTAB,
54
+ CONV_DECODES_SUBTAB,
55
+ VALID_CONV_SUBTABS,
56
+ DATA_TOOLS_CONTEXT_BASE64_MIN_LENGTH,
57
+ DATA_TOOLS_TEXT_MIME_PRINTABLE_THRESHOLD,
58
+ DATA_TOOLS_ENTROPY_HIGH_THRESHOLD,
59
+ DATA_TOOLS_ENTROPY_MEDIUM_THRESHOLD,
60
+ DATA_TOOLS_MAX_DECIMAL_INTEGER_BYTES,
61
+ getActiveConvSubtab,
62
+ getActiveDataToolsProtoResult,
63
+ setConvSubtab,
64
+ runDataToolsHashesFromInput,
65
+ } = require("./panels/data-tools-panel");
66
+
67
+ // Cache frequently accessed DOM elements to avoid repeated lookups
68
+ const domCache = {};
69
+ function getCachedElement(id) {
70
+ if (!domCache[id]) {
71
+ domCache[id] = document.getElementById(id);
72
+ }
73
+ return domCache[id];
74
+ }
75
+
76
+ const SESSION_FILE_SCHEMA_VERSION = 1;
77
+ const SESSION_CAPTURE_KEY = "Capture Data";
78
+ const SESSION_STATE_KEY = "Session State";
79
+ const MAIN_TAB_SUMMARY = "summary";
80
+ const MAIN_TAB_DATA = "data";
81
+ const MAIN_TAB_STATS = "stats";
82
+ const MAIN_TAB_LIST = "list";
83
+ const MAIN_TAB_NOTES = "notes";
84
+ const MAIN_TAB_DATA_TOOLS = "data-tools";
85
+ const MAIN_TAB_CRYPT = "crypt";
86
+ const MAIN_TAB_KEYSTORE = "keystore";
87
+ const NOTE_DEFAULT_COLOR = "#4caf50";
88
+ const NOTE_FALLBACK_COLORS = [
89
+ "#4caf50",
90
+ "#ff9800",
91
+ "#2196f3",
92
+ "#9c27b0",
93
+ "#e91e63",
94
+ "#ffc107",
95
+ ];
96
+
97
+ // Global variables for DOM elements and state
98
+ let capturedPackets = {}; // Stores parsed packet data from JSON
99
+ let jsonCapture = ""; // Stringified JSON capture for pretty display
100
+ let currentIp;
101
+ let finalSummary = ""; // Stores the summary section from JSON
102
+ const status = getCachedElement("status"); // Status bar element
103
+ let hostsList = ["0.0.0.0"]; // List of hosts found in capture
104
+ const hostFilterEl = getCachedElement("host_filter"); // Host filter dropdown
105
+ let packetsForHost = []; // Packets for the currently selected host
106
+ let index = 0; // Navigation index for packets
107
+ let activePacketCursor = 0;
108
+ let bookmarkList = []; // List of bookmarks (host:packet index)
109
+ let activeBookmark = {}; // Current bookmark object
110
+ let isFileLoaded = false;
111
+ let jsonOfPackets;
112
+ let filteredPackets;
113
+ let currentPacketKey;
114
+ let startTime;
115
+ const filterInputEl = getCachedElement("filterStr");
116
+ const filterHighlightEl = getCachedElement("filterStr-highlight");
117
+ const filterClearButtonEl = getCachedElement("filterStr-clear");
118
+ const filterHistorySelectEl = getCachedElement("filter-history-select");
119
+ const filterHistory = [];
120
+ const CONTEXT_IPV4_REGEX =
121
+ /\b(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}\b/;
122
+ const STRICT_IPV4_REGEX =
123
+ /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
124
+ const CONTEXT_MAC_REGEX = /\b([0-9A-Fa-f]{2}([-:])){5}[0-9A-Fa-f]{2}\b/;
125
+ const CONTEXT_MIME_REGEX = /^[\w.+-]+\/[\w.+-]+$/;
126
+ const CRYPT_SSL_SUBTAB = "ssl";
127
+ const CRYPT_PGP_SUBTAB = "pgp";
128
+ const CRYPT_OPENSSH_SUBTAB = "openssh";
129
+ const VALID_MAIN_TABS = [
130
+ MAIN_TAB_SUMMARY,
131
+ MAIN_TAB_DATA,
132
+ MAIN_TAB_STATS,
133
+ MAIN_TAB_LIST,
134
+ MAIN_TAB_NOTES,
135
+ MAIN_TAB_DATA_TOOLS,
136
+ MAIN_TAB_CRYPT,
137
+ MAIN_TAB_KEYSTORE,
138
+ ];
139
+ const VALID_CRYPT_SUBTABS = [CRYPT_SSL_SUBTAB, CRYPT_PGP_SUBTAB, CRYPT_OPENSSH_SUBTAB];
140
+ let activeMainTab = MAIN_TAB_SUMMARY;
141
+ let activeCryptSubtab = CRYPT_SSL_SUBTAB;
142
+ let activeDataToolsProtoResult = null;
143
+ let keystorePanel;
144
+ let notesList = [];
145
+ let selectedNoteId = null;
146
+ let noteIdCounter = 0;
147
+
148
+ initializeInstallScreen({
149
+ installapi: window.installapi,
150
+ documentRef: document,
151
+ });
152
+
153
+ const {
154
+ initializeActivityLog,
155
+ writeLogEntry,
156
+ writeBackendErrorLogEntry,
157
+ logErrorEntry,
158
+ } = initializeLogging({
159
+ logapi: window.logapi,
160
+ documentRef: document,
161
+ consoleRef: console,
162
+ });
163
+
164
+ const { showStats } = createStatsPanel({
165
+ documentRef: document,
166
+ statusUpdate,
167
+ writeLogEntry,
168
+ setActiveMainTab: (tab) => {
169
+ activeMainTab = tab;
170
+ },
171
+ mainTabStats: MAIN_TAB_STATS,
172
+ getJsonCapture: () => jsonCapture,
173
+ getCapturedPackets: () => capturedPackets,
174
+ filterInputEl,
175
+ syncFilterHighlight,
176
+ runFilterQuery,
177
+ getFilteredPackets: () => filteredPackets,
178
+ setPacketsForHost: (packets) => {
179
+ packetsForHost = packets;
180
+ },
181
+ });
182
+
183
+ const summaryPanel = createSummaryPanel({
184
+ documentRef: document,
185
+ getJsonCapture: () => jsonCapture,
186
+ getFinalSummary: () => finalSummary,
187
+ setActiveMainTab: (tab) => {
188
+ activeMainTab = tab;
189
+ },
190
+ mainTabSummary: MAIN_TAB_SUMMARY,
191
+ statusUpdate,
192
+ fileLoaded,
193
+ });
194
+
195
+ const { showSummary, showSummaryLoading, clearSummaryContent } = summaryPanel;
196
+ const { initializeDataView, bindDataPanelEvents, logCurrentPacketDisplay } =
197
+ createDataPanel({
198
+ constants: {
199
+ MAIN_TAB_DATA,
200
+ },
201
+ documentRef: document,
202
+ statusUpdate,
203
+ writeLogEntry,
204
+ doError,
205
+ getIsFileLoaded: () => isFileLoaded,
206
+ getJsonCapture: () => jsonCapture,
207
+ getHostFilterValue: () => hostFilterEl.value,
208
+ getHostsList: () => hostsList,
209
+ getFilterInputValue: () => filterInputEl.value,
210
+ getFilteredPackets: () => filteredPackets,
211
+ getPacketsForHost: () => packetsForHost,
212
+ setActiveMainTab: (tab) => {
213
+ activeMainTab = tab;
214
+ },
215
+ handlePacketNavigation: (navAction, navBookmark) =>
216
+ handlePacketNavigation(navAction, navBookmark),
217
+ getIndex: () => index,
218
+ setIndex: (nextIndex) => {
219
+ index = nextIndex;
220
+ },
221
+ setActivePacketCursor,
222
+ setCurrentIp: (nextIp) => {
223
+ currentIp = nextIp;
224
+ },
225
+ setCurrentPacketKey: (nextPacketKey) => {
226
+ currentPacketKey = nextPacketKey;
227
+ },
228
+ getCurrentPacketKey: () => currentPacketKey,
229
+ syncBookmarkDropdown,
230
+ infoPanel,
231
+ popHexGrid,
232
+ populateDataTypes,
233
+ });
234
+ bindDataPanelEvents();
235
+
236
+ function getPacketTimeframe() {
237
+ if (!capturedPackets || typeof capturedPackets !== "object") return null;
238
+ const packetTimes = [];
239
+ if (!capturedPackets["Host"]) return null;
240
+ for (const host of Object.keys(capturedPackets["Host"])) {
241
+ const hostPackets = capturedPackets["Host"][host];
242
+ if (!Array.isArray(hostPackets)) continue;
243
+ hostPackets.forEach((packet) => {
244
+ const packetTime = packet?.["Packet Info"]?.["Packet Timestamp"];
245
+ if (packetTime) {
246
+ packetTimes.push(packetTime);
247
+ }
248
+ });
249
+ }
250
+ if (packetTimes.length === 0) return null;
251
+ const parsedTimes = packetTimes
252
+ .map((time) => ({
253
+ raw: time,
254
+ value: Date.parse(time),
255
+ }))
256
+ .filter((item) => !Number.isNaN(item.value))
257
+ .sort((a, b) => a.value - b.value);
258
+ if (parsedTimes.length < 1) return null;
259
+ return {
260
+ first: parsedTimes[0].raw,
261
+ last: parsedTimes[parsedTimes.length - 1].raw,
262
+ };
263
+ }
264
+
265
+ void initializeActivityLog();
266
+
267
+ popHexGrid("00".repeat(256));
268
+ // Set up file upload handler for JSON capture
269
+ document
270
+ .getElementById("json-upload")
271
+ .addEventListener("change", function (event) {
272
+ const file = event.target.files[0];
273
+ if (file) {
274
+ startTime = performance.now();
275
+ statusUpdate("Processing file: " + file.name);
276
+ writeLogEntry(
277
+ `User selected JSON file name=${file.name} size_bytes=${file.size}`,
278
+ );
279
+ processFile(file);
280
+ isFileLoaded = true;
281
+ event.target.value = ""; // Reset so the same file can be loaded again
282
+ }
283
+ });
284
+
285
+ document
286
+ .getElementById("pcap-filename")
287
+ .addEventListener("click", function (event) {
288
+ window.getfileapi
289
+ .selectFile()
290
+ .then((filePath) => {
291
+ if (filePath) {
292
+ writeLogEntry(`User selected PCAP file path=${filePath}`);
293
+ window.fsize
294
+ .getFSize()
295
+ .then((fileSize) => {
296
+ // Update the UI with the file size
297
+ const fileSizeKb = (fileSize / 1024).toFixed(2);
298
+ document.getElementById("pcap-size").textContent =
299
+ `PCAP size: ${fileSizeKb}kb`;
300
+ writeLogEntry(
301
+ `Capture size recorded bytes=${fileSize} kilobytes=${fileSizeKb}`,
302
+ );
303
+ })
304
+ .catch((error) => {
305
+ // Handle any errors (e.g., file not found)
306
+ console.error("Error fetching file size:", error);
307
+ logErrorEntry("file-size-fetch", error);
308
+ });
309
+
310
+ runSnitch(filePath);
311
+ }
312
+ })
313
+ .catch((error) => {
314
+ doError("Error selecting PCAP file!");
315
+ logErrorEntry("pcap-select", error);
316
+ });
317
+ });
318
+
319
+ function isValidJson(str) {
320
+ try {
321
+ JSON.parse(str);
322
+ return true;
323
+ } catch (e) {
324
+ return false;
325
+ }
326
+ }
327
+
328
+ // Chunked JSON parsing for large files to avoid blocking the UI
329
+ function parseJsonChunked(jsonString, chunkSize = 65536) {
330
+ return new Promise((resolve, reject) => {
331
+ try {
332
+ // For smaller files, parse directly
333
+ if (jsonString.length < chunkSize * 2) {
334
+ resolve(JSON.parse(jsonString));
335
+ return;
336
+ }
337
+
338
+ // For large files, use setTimeout to yield to the main thread
339
+ let position = 0;
340
+ const length = jsonString.length;
341
+ let result = "";
342
+ const stack = [];
343
+ let inString = false;
344
+ let escape = false;
345
+
346
+ function processChunk() {
347
+ const end = Math.min(position + chunkSize, length);
348
+
349
+ for (; position < end; position++) {
350
+ const char = jsonString[position];
351
+ if (escape) {
352
+ escape = false;
353
+ } else if (char === "\\") {
354
+ escape = true;
355
+ } else if (char === '"') {
356
+ inString = !inString;
357
+ } else if (!inString) {
358
+ if (char === "{" || char === "[") {
359
+ stack.push(char);
360
+ } else if (char === "}" || char === "]") {
361
+ stack.pop();
362
+ }
363
+ }
364
+ result += char;
365
+ }
366
+
367
+ if (position < length) {
368
+ // Yield to main thread and continue
369
+ setTimeout(processChunk, 0);
370
+ } else {
371
+ resolve(JSON.parse(result));
372
+ }
373
+ }
374
+
375
+ setTimeout(processChunk, 0);
376
+ } catch (e) {
377
+ reject(e);
378
+ }
379
+ });
380
+ }
381
+
382
+ function fileLoaded(isLoaded) {
383
+ isFileLoaded = isLoaded;
384
+ if (isLoaded) {
385
+ const loadEndTime = performance.now();
386
+ document.getElementById("load-time").textContent =
387
+ "Load time: " +
388
+ ((loadEndTime - startTime) / 1000).toFixed(2) +
389
+ " seconds";
390
+ filterInputEl.disabled = false;
391
+ filterHistorySelectEl.disabled = false;
392
+ document.getElementById("summary-btn").style.opacity = "1";
393
+ document.getElementById("data-btn").style.opacity = "1";
394
+ document.getElementById("data-tools-btn").style.opacity = "1";
395
+ document.getElementById("crypt-btn").style.opacity = "1";
396
+ document.getElementById("keystore-btn").style.opacity = "1";
397
+ document.getElementById("tab-btns").style.opacity = "1";
398
+ document.getElementById("prev-btn").style.opacity = "1";
399
+ document.getElementById("next-btn").style.opacity = "1";
400
+ document.getElementById("log-btn").style.opacity = "1";
401
+ document.getElementById("stats-btn").style.opacity = "1";
402
+ document.getElementById("list-btn").style.opacity = "1";
403
+ document.getElementById("notes-btn").style.opacity = "1";
404
+ document.getElementById("json-lab").style.display = "none";
405
+ document.getElementById("pcap-lab").style.display = "none";
406
+ document.getElementById("llm-toggle").style.display = "none";
407
+ writeLogEntry(
408
+ `Initial file load completed seconds=${((loadEndTime - startTime) / 1000).toFixed(2)}`,
409
+ );
410
+ } else {
411
+ filterInputEl.disabled = true;
412
+ filterHistorySelectEl.disabled = true;
413
+ document.getElementById("json-lab").style.display = "block";
414
+ document.getElementById("pcap-lab").style.display = "block";
415
+ document.getElementById("log-btn").style.opacity = "0";
416
+ document.getElementById("stats-btn").style.opacity = "0";
417
+ document.getElementById("list-btn").style.opacity = "0";
418
+ document.getElementById("notes-btn").style.opacity = "0";
419
+ document.getElementById("crypt-btn").style.opacity = "0";
420
+ document.getElementById("keystore-btn").style.opacity = "0";
421
+ }
422
+ updateFilterClearButtonState();
423
+ }
424
+
425
+ function escapeHtml(text) {
426
+ return text
427
+ .replace(/&/g, "&amp;")
428
+ .replace(/</g, "&lt;")
429
+ .replace(/>/g, "&gt;")
430
+ .replace(/"/g, "&quot;")
431
+ .replace(/'/g, "&#39;");
432
+ }
433
+
434
+ function decorateExpressionSegment(segmentText) {
435
+ if (!segmentText) return "";
436
+
437
+ const colonIndex = segmentText.indexOf(":");
438
+ if (colonIndex === -1) {
439
+ return `<span class="query-token-value">${escapeHtml(segmentText)}</span>`;
440
+ }
441
+
442
+ const keyText = segmentText.slice(0, colonIndex);
443
+ const valueText = segmentText.slice(colonIndex + 1);
444
+ const cmpMatch = valueText.match(/^(\s*)(>=|<=|==|!=|>|<)(\s*)(.*)$/);
445
+
446
+ let valueHtml = "";
447
+ if (cmpMatch) {
448
+ valueHtml =
449
+ escapeHtml(cmpMatch[1]) +
450
+ `<span class="query-token-operator">${escapeHtml(cmpMatch[2])}</span>` +
451
+ escapeHtml(cmpMatch[3]) +
452
+ `<span class="query-token-value">${escapeHtml(cmpMatch[4])}</span>`;
453
+ } else {
454
+ valueHtml = `<span class="query-token-value">${escapeHtml(valueText)}</span>`;
455
+ }
456
+
457
+ return (
458
+ `<span class="query-token-key">${escapeHtml(keyText)}</span>` +
459
+ '<span class="query-token-colon">:</span>' +
460
+ valueHtml
461
+ );
462
+ }
463
+
464
+ function renderHighlightedQuery(query) {
465
+ const source = query || "";
466
+ if (!source) return "&nbsp;";
467
+
468
+ // Query grammar tokens: logical OR/AND operators and grouping parentheses.
469
+ const tokenRegex = /(\|\||&&|\(|\)|!(?!=))/g;
470
+ let cursor = 0;
471
+ let html = "";
472
+ let tokenMatch = tokenRegex.exec(source);
473
+
474
+ while (tokenMatch !== null) {
475
+ const segmentText = source.slice(cursor, tokenMatch.index);
476
+ html += decorateExpressionSegment(segmentText);
477
+
478
+ const tokenText = tokenMatch[0];
479
+ const tokenClass =
480
+ tokenText === "(" || tokenText === ")" ? "paren" : "logic";
481
+ html += `<span class="query-token-${tokenClass}">${escapeHtml(tokenText)}</span>`;
482
+ cursor = tokenRegex.lastIndex;
483
+ tokenMatch = tokenRegex.exec(source);
484
+ }
485
+
486
+ html += decorateExpressionSegment(source.slice(cursor));
487
+ return html;
488
+ }
489
+
490
+ function syncFilterHighlight() {
491
+ filterHighlightEl.innerHTML = renderHighlightedQuery(filterInputEl.value);
492
+ syncFilterHighlightScroll();
493
+ updateFilterClearButtonState();
494
+ }
495
+
496
+ function syncFilterHighlightScroll() {
497
+ filterHighlightEl.scrollLeft = filterInputEl.scrollLeft;
498
+ }
499
+
500
+ function updateFilterClearButtonState() {
501
+ filterClearButtonEl.disabled = !canClearFilterQuery();
502
+ }
503
+
504
+ function canClearFilterQuery() {
505
+ return !filterInputEl.disabled && filterInputEl.value.trim() !== "";
506
+ }
507
+
508
+ function renderFilterHistory() {
509
+ filterHistorySelectEl.replaceChildren();
510
+
511
+ const placeholderOption = document.createElement("option");
512
+ placeholderOption.value = "";
513
+ placeholderOption.textContent = filterHistory.length
514
+ ? "Previous queries"
515
+ : "No previous queries";
516
+ placeholderOption.selected = true;
517
+ filterHistorySelectEl.appendChild(placeholderOption);
518
+
519
+ filterHistory.forEach((query) => {
520
+ const queryOption = document.createElement("option");
521
+ queryOption.value = query;
522
+ queryOption.textContent = query;
523
+ queryOption.title = query;
524
+ filterHistorySelectEl.appendChild(queryOption);
525
+ });
526
+ filterHistorySelectEl.value = "";
527
+ filterHistorySelectEl.disabled = !isFileLoaded;
528
+ }
529
+
530
+ function addFilterHistory(query) {
531
+ const normalizedQuery = query.trim();
532
+ if (!normalizedQuery) return;
533
+ const existingIndex = filterHistory.indexOf(normalizedQuery);
534
+ if (existingIndex !== -1) {
535
+ filterHistory.splice(existingIndex, 1);
536
+ }
537
+ filterHistory.unshift(normalizedQuery);
538
+ renderFilterHistory();
539
+ }
540
+
541
+ function runFilterQuery(filterQuery, options = {}) {
542
+ const { trackHistory = true } = options;
543
+ try {
544
+ validateFilterSyntax(filterQuery);
545
+ } catch (error) {
546
+ logErrorEntry("filter-syntax", error);
547
+ writeLogEntry(`User query rejected query="${filterQuery}"`);
548
+ doError(`Invalid filter syntax: ${error.message}`);
549
+ statusUpdate("Status: Invalid filter syntax");
550
+ return;
551
+ }
552
+
553
+ if (trackHistory) {
554
+ addFilterHistory(filterQuery);
555
+ }
556
+ filteredPackets = filterPackets(capturedPackets, filterQuery);
557
+ writeLogEntry(`User executed query="${filterQuery}"`);
558
+
559
+ if (filteredPackets === undefined || filteredPackets.length === 0) {
560
+ hideAllData();
561
+ statusUpdate("Status: No packets match the filter criteria");
562
+ writeLogEntry("User query returned 0 packets");
563
+ } else {
564
+ statusUpdate(
565
+ "Status: Displaying " +
566
+ filteredPackets.length +
567
+ " packets matching filter",
568
+ );
569
+ writeLogEntry(`User query returned packets=${filteredPackets.length}`);
570
+ handlePacketNavigation("filtered", null);
571
+ }
572
+ }
573
+
574
+ function clearFilterQuery() {
575
+ if (!canClearFilterQuery()) {
576
+ return;
577
+ }
578
+ filterInputEl.value = "";
579
+ syncFilterHighlight();
580
+ filterHistorySelectEl.value = "";
581
+ filterInputEl.focus();
582
+ runFilterQuery("");
583
+ }
584
+
585
+ function deepCloneSessionData(value, fallback) {
586
+ try {
587
+ return JSON.parse(JSON.stringify(value));
588
+ } catch {
589
+ return fallback;
590
+ }
591
+ }
592
+
593
+ function normalizeNoteColor(colorValue) {
594
+ const normalized =
595
+ typeof colorValue === "string" ? colorValue.trim().toLowerCase() : "";
596
+ if (/^#[0-9a-f]{6}$/.test(normalized)) return normalized;
597
+ return NOTE_DEFAULT_COLOR;
598
+ }
599
+
600
+ function generateNoteId() {
601
+ if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") {
602
+ return globalThis.crypto.randomUUID();
603
+ }
604
+ noteIdCounter += 1;
605
+ return `note-${Date.now()}-${noteIdCounter}`;
606
+ }
607
+
608
+ function createNoteEntry(text = "", color = NOTE_DEFAULT_COLOR) {
609
+ return {
610
+ id: generateNoteId(),
611
+ text: typeof text === "string" ? text : String(text || ""),
612
+ color: normalizeNoteColor(color),
613
+ };
614
+ }
615
+
616
+ function getSelectedNoteEntry() {
617
+ return notesList.find((entry) => entry.id === selectedNoteId) || null;
618
+ }
619
+
620
+ function renderNotesList() {
621
+ const notesSelectEl = document.getElementById("notes-select");
622
+ const notesEditorEl = document.getElementById("notes-editor");
623
+ const newNoteColorEl = document.getElementById("notes-new-color");
624
+ if (!notesSelectEl || !notesEditorEl || !newNoteColorEl) return;
625
+
626
+ notesSelectEl.replaceChildren();
627
+ if (!notesList.length) {
628
+ selectedNoteId = null;
629
+ notesEditorEl.value = "";
630
+ notesEditorEl.disabled = true;
631
+ return;
632
+ }
633
+
634
+ if (!getSelectedNoteEntry()) {
635
+ selectedNoteId = notesList[0].id;
636
+ }
637
+
638
+ notesList.forEach((noteEntry, noteIndex) => {
639
+ const optionEl = document.createElement("option");
640
+ const previewText = String(noteEntry.text || "")
641
+ .replace(/\s+/g, " ")
642
+ .trim();
643
+ optionEl.value = noteEntry.id;
644
+ optionEl.textContent = `${noteIndex + 1}. ${previewText || "(empty note)"}`;
645
+ optionEl.style.borderLeft = `8px solid ${normalizeNoteColor(noteEntry.color)}`;
646
+ notesSelectEl.appendChild(optionEl);
647
+ });
648
+
649
+ notesSelectEl.value = selectedNoteId;
650
+ const selectedNoteEntry = getSelectedNoteEntry();
651
+ notesEditorEl.disabled = !selectedNoteEntry;
652
+ notesEditorEl.value = selectedNoteEntry ? selectedNoteEntry.text : "";
653
+ newNoteColorEl.value = selectedNoteEntry
654
+ ? normalizeNoteColor(selectedNoteEntry.color)
655
+ : NOTE_DEFAULT_COLOR;
656
+ }
657
+
658
+ function addNote(text, color = NOTE_DEFAULT_COLOR, sourceLabel = "manual") {
659
+ const normalizedText =
660
+ typeof text === "string" ? text.trim() : String(text || "").trim();
661
+ if (!normalizedText) {
662
+ statusUpdate("Status: No note text to add");
663
+ return false;
664
+ }
665
+ const noteEntry = createNoteEntry(normalizedText, color);
666
+ notesList.unshift(noteEntry);
667
+ selectedNoteId = noteEntry.id;
668
+ renderNotesList();
669
+ statusUpdate("Status: Note added");
670
+ writeLogEntry(`Note added source=${sourceLabel} length=${normalizedText.length}`);
671
+ return true;
672
+ }
673
+
674
+ function removeSelectedNote() {
675
+ const selectedNoteEntry = getSelectedNoteEntry();
676
+ if (!selectedNoteEntry) {
677
+ statusUpdate("Status: No note selected to remove");
678
+ return;
679
+ }
680
+ const selectedIndex = notesList.findIndex((entry) => entry.id === selectedNoteEntry.id);
681
+ notesList = notesList.filter((entry) => entry.id !== selectedNoteEntry.id);
682
+ if (notesList.length === 0) {
683
+ selectedNoteId = null;
684
+ } else if (selectedIndex >= notesList.length) {
685
+ selectedNoteId = notesList[notesList.length - 1].id;
686
+ } else {
687
+ selectedNoteId = notesList[Math.max(0, selectedIndex)].id;
688
+ }
689
+ renderNotesList();
690
+ statusUpdate("Status: Note removed");
691
+ writeLogEntry(`Note removed id=${selectedNoteEntry.id}`);
692
+ }
693
+
694
+ function formatNotesForExport() {
695
+ if (!Array.isArray(notesList) || notesList.length === 0) return "";
696
+ return notesList
697
+ .map((noteEntry, noteIndex) => {
698
+ const noteText = String(noteEntry.text || "").trim();
699
+ return [
700
+ `Note ${noteIndex + 1}`,
701
+ `Color: ${normalizeNoteColor(noteEntry.color)}`,
702
+ noteText,
703
+ ].join("\n");
704
+ })
705
+ .join("\n\n---\n\n");
706
+ }
707
+
708
+ async function saveNotesToDisk() {
709
+ const notesExportText = formatNotesForExport();
710
+ if (!notesExportText) {
711
+ statusUpdate("Status: No notes available to save");
712
+ return;
713
+ }
714
+ const result = await window.saveapi.saveNotes(notesExportText);
715
+ if (result?.canceled) {
716
+ statusUpdate("Status: Save cancelled");
717
+ } else if (result?.success) {
718
+ statusUpdate("Status: Notes saved successfully");
719
+ writeLogEntry(`Notes saved entries=${notesList.length}`);
720
+ } else {
721
+ const errorMessage =
722
+ result && typeof result === "object" && "error" in result
723
+ ? result.error
724
+ : "unknown";
725
+ doError("Notes save failed");
726
+ logErrorEntry("save-notes", errorMessage || "unknown");
727
+ statusUpdate("Status: Notes save failed – " + (errorMessage || "unknown error"));
728
+ }
729
+ }
730
+
731
+ function buildConvConvertedOutputNoteText() {
732
+ const outputFields = [
733
+ ["Hex", "data-tools-hex-output"],
734
+ ["Binary", "data-tools-binary-output"],
735
+ ["Decimal bytes", "data-tools-decimal-output"],
736
+ ["Decimal integer", "data-tools-decimal-integer-output"],
737
+ ["ASCII", "data-tools-ascii-output"],
738
+ ["Base64", "data-tools-base64-output"],
739
+ ];
740
+ const lines = outputFields
741
+ .map(([label, id]) => {
742
+ const value = document.getElementById(id)?.value?.trim() || "";
743
+ return value ? `${label}: ${value}` : "";
744
+ })
745
+ .filter(Boolean);
746
+ return lines.length > 0 ? lines.join("\n") : "";
747
+ }
748
+
749
+ function buildConvHashesNoteText() {
750
+ const hashFields = [
751
+ ["Input", "data-tools-hash-input-reading"],
752
+ ["MD5", "data-tools-md5-output"],
753
+ ["SHA-1", "data-tools-sha1-output"],
754
+ ["SHA-256", "data-tools-sha256-output"],
755
+ ["SHA-384", "data-tools-sha384-output"],
756
+ ["SHA-512", "data-tools-sha512-output"],
757
+ ["SHA3-256", "data-tools-sha3-256-output"],
758
+ ["SHA3-512", "data-tools-sha3-512-output"],
759
+ ["RIPEMD-160", "data-tools-ripemd160-output"],
760
+ ["Whirlpool", "data-tools-whirlpool-output"],
761
+ ];
762
+ const lines = hashFields
763
+ .map(([label, id]) => {
764
+ const value = document.getElementById(id)?.value?.trim() || "";
765
+ return value ? `${label}: ${value}` : "";
766
+ })
767
+ .filter(Boolean);
768
+ return lines.length > 0 ? lines.join("\n") : "";
769
+ }
770
+
771
+ function sendTextToNotesFromContextMenu(text, sourceLabel) {
772
+ hideConvertContextMenu();
773
+ const didAdd = addNote(text, NOTE_DEFAULT_COLOR, sourceLabel);
774
+ if (!didAdd) return;
775
+ showNotesWorkspace();
776
+ }
777
+
778
+ function initializeNotesPanel() {
779
+ const addButtonEl = document.getElementById("notes-add-btn");
780
+ const removeButtonEl = document.getElementById("notes-remove-btn");
781
+ const saveButtonEl = document.getElementById("notes-save-btn");
782
+ const newNoteInputEl = document.getElementById("notes-new-input");
783
+ const newNoteColorEl = document.getElementById("notes-new-color");
784
+ const notesSelectEl = document.getElementById("notes-select");
785
+ const notesEditorEl = document.getElementById("notes-editor");
786
+ if (
787
+ !addButtonEl ||
788
+ !removeButtonEl ||
789
+ !saveButtonEl ||
790
+ !newNoteInputEl ||
791
+ !newNoteColorEl ||
792
+ !notesSelectEl ||
793
+ !notesEditorEl
794
+ ) {
795
+ return;
796
+ }
797
+ newNoteColorEl.value = NOTE_DEFAULT_COLOR;
798
+
799
+ addButtonEl.addEventListener("click", () => {
800
+ const didAdd = addNote(newNoteInputEl.value, newNoteColorEl.value, "notes-panel");
801
+ if (didAdd) {
802
+ newNoteInputEl.value = "";
803
+ newNoteInputEl.focus();
804
+ }
805
+ });
806
+ saveButtonEl.addEventListener("click", () => {
807
+ void saveNotesToDisk();
808
+ });
809
+ removeButtonEl.addEventListener("click", removeSelectedNote);
810
+ newNoteInputEl.addEventListener("keydown", (event) => {
811
+ if (!(event.ctrlKey || event.metaKey) || event.key !== "Enter") return;
812
+ event.preventDefault();
813
+ const didAdd = addNote(newNoteInputEl.value, newNoteColorEl.value, "notes-panel");
814
+ if (didAdd) {
815
+ newNoteInputEl.value = "";
816
+ newNoteInputEl.focus();
817
+ }
818
+ });
819
+ notesSelectEl.addEventListener("change", () => {
820
+ selectedNoteId = notesSelectEl.value || null;
821
+ renderNotesList();
822
+ });
823
+ notesEditorEl.addEventListener("input", () => {
824
+ const selectedNoteEntry = getSelectedNoteEntry();
825
+ if (!selectedNoteEntry) return;
826
+ selectedNoteEntry.text = notesEditorEl.value;
827
+ const selectedOptionEl =
828
+ notesSelectEl.options[notesSelectEl.selectedIndex] || null;
829
+ if (selectedOptionEl) {
830
+ const previewText = String(selectedNoteEntry.text || "")
831
+ .replace(/\s+/g, " ")
832
+ .trim();
833
+ selectedOptionEl.textContent = `${notesSelectEl.selectedIndex + 1}. ${previewText || "(empty note)"}`;
834
+ }
835
+ });
836
+ newNoteColorEl.addEventListener("input", () => {
837
+ const selectedNoteEntry = getSelectedNoteEntry();
838
+ if (!selectedNoteEntry) return;
839
+ selectedNoteEntry.color = normalizeNoteColor(newNoteColorEl.value);
840
+ const selectedOptionEl =
841
+ notesSelectEl.options[notesSelectEl.selectedIndex] || null;
842
+ if (selectedOptionEl) {
843
+ selectedOptionEl.style.borderLeft = `8px solid ${selectedNoteEntry.color}`;
844
+ }
845
+ });
846
+
847
+ renderNotesList();
848
+ }
849
+
850
+ function normalizeLoadedSessionPayload(parsedPayload) {
851
+ if (!parsedPayload || typeof parsedPayload !== "object") {
852
+ return null;
853
+ }
854
+
855
+ const hasWrappedCapture =
856
+ parsedPayload[SESSION_CAPTURE_KEY] &&
857
+ typeof parsedPayload[SESSION_CAPTURE_KEY] === "object";
858
+ const captureData = hasWrappedCapture
859
+ ? parsedPayload[SESSION_CAPTURE_KEY]
860
+ : parsedPayload;
861
+ const sessionState =
862
+ hasWrappedCapture && parsedPayload[SESSION_STATE_KEY]
863
+ ? parsedPayload[SESSION_STATE_KEY]
864
+ : null;
865
+
866
+ if (
867
+ !captureData ||
868
+ typeof captureData !== "object" ||
869
+ !captureData["Host"] ||
870
+ typeof captureData["Host"] !== "object"
871
+ ) {
872
+ return null;
873
+ }
874
+
875
+ return {
876
+ captureData,
877
+ sessionState:
878
+ sessionState && typeof sessionState === "object" ? sessionState : null,
879
+ };
880
+ }
881
+
882
+ function rebuildBookmarkDropdown() {
883
+ const selectBookmarkEl = document.getElementById("selectBookmark");
884
+ while (selectBookmarkEl.options.length > 1) {
885
+ selectBookmarkEl.remove(1);
886
+ }
887
+ bookmarkList.forEach((bookmarkKey) => {
888
+ selectBookmarkEl.appendChild(new Option(bookmarkKey, bookmarkKey));
889
+ });
890
+ }
891
+
892
+ function getSessionPacketViewMode() {
893
+ if (
894
+ Array.isArray(filteredPackets) &&
895
+ filteredPackets.length > 0 &&
896
+ packetsForHost === filteredPackets
897
+ ) {
898
+ return "filtered";
899
+ }
900
+ return "host";
901
+ }
902
+
903
+ function buildSessionStateSnapshot() {
904
+ const listSearchEl = document.getElementById("list-search");
905
+ const listGroupStreamsEl = document.getElementById("list-group-streams");
906
+ return {
907
+ schemaVersion: SESSION_FILE_SCHEMA_VERSION,
908
+ savedAt: new Date().toISOString(),
909
+ currentFilterQuery: filterInputEl.value || "",
910
+ filterHistory: [...filterHistory],
911
+ currentPacketKey: currentPacketKey || null,
912
+ activePacketCursor: getActivePacketCursor(),
913
+ packetViewMode: getSessionPacketViewMode(),
914
+ selectedHost:
915
+ document.getElementById("target_hosts")?.value || hostFilterEl.value || "",
916
+ bookmarkList: [...bookmarkList],
917
+ sessionKeychainEntries: deepCloneSessionData(
918
+ keystorePanel.getSessionKeychainEntries(),
919
+ [],
920
+ ),
921
+ keystoreMode: keystorePanel.getKeystoreMode(),
922
+ notes: deepCloneSessionData(notesList, []),
923
+ tabs: {
924
+ main: activeMainTab,
925
+ conv: getActiveConvSubtab(),
926
+ crypt: activeCryptSubtab,
927
+ listSearch: listSearchEl ? listSearchEl.value : "",
928
+ listGroupStreams: listGroupStreamsEl ? Boolean(listGroupStreamsEl.checked) : false,
929
+ },
930
+ };
931
+ }
932
+
933
+ function buildSessionFilePayload() {
934
+ return JSON.stringify(
935
+ {
936
+ [SESSION_CAPTURE_KEY]: capturedPackets,
937
+ [SESSION_STATE_KEY]: buildSessionStateSnapshot(),
938
+ },
939
+ null,
940
+ 2,
941
+ );
942
+ }
943
+
944
+ async function persistSessionToDisk(sourceLabel = "manual-save") {
945
+ if (
946
+ !capturedPackets ||
947
+ !capturedPackets["Host"] ||
948
+ typeof capturedPackets["Host"] !== "object"
949
+ ) {
950
+ statusUpdate("Status: No data loaded to save");
951
+ return { success: false, error: "No loaded capture" };
952
+ }
953
+ const sessionJsonData = buildSessionFilePayload();
954
+ const result = await window.saveapi.saveJson(sessionJsonData);
955
+ if (result.canceled) {
956
+ statusUpdate("Status: Save cancelled");
957
+ } else if (result.success) {
958
+ statusUpdate("Status: Session saved successfully");
959
+ writeLogEntry(`Session saved source=${sourceLabel}`);
960
+ } else {
961
+ const errorMessage =
962
+ result && typeof result === "object" && "error" in result
963
+ ? result.error
964
+ : "unknown";
965
+ doError("Save failed");
966
+ logErrorEntry("save-session", errorMessage || "unknown");
967
+ statusUpdate("Status: Save failed – " + (errorMessage || "unknown error"));
968
+ console.error("Save failed:", errorMessage);
969
+ }
970
+ return result;
971
+ }
972
+
973
+ async function maybePromptSaveSessionOnExit() {
974
+ if (!isFileLoaded || !capturedPackets || !capturedPackets["Host"]) {
975
+ return "discard";
976
+ }
977
+ const dialogEl = document.getElementById("save-session-dialog");
978
+ if (!dialogEl) {
979
+ return window.confirm("Save session before exit?") ? "save" : "cancel";
980
+ }
981
+ dialogEl.hidden = false;
982
+ return new Promise((resolve) => {
983
+ function cleanup(result) {
984
+ dialogEl.hidden = true;
985
+ document
986
+ .getElementById("save-session-save-btn")
987
+ .removeEventListener("click", onSave);
988
+ document
989
+ .getElementById("save-session-discard-btn")
990
+ .removeEventListener("click", onDiscard);
991
+ document
992
+ .getElementById("save-session-cancel-btn")
993
+ .removeEventListener("click", onCancel);
994
+ resolve(result);
995
+ }
996
+ function onSave() { cleanup("save"); }
997
+ function onDiscard() { cleanup("discard"); }
998
+ function onCancel() { cleanup("cancel"); }
999
+ document.getElementById("save-session-save-btn").addEventListener("click", onSave);
1000
+ document.getElementById("save-session-discard-btn").addEventListener("click", onDiscard);
1001
+ document.getElementById("save-session-cancel-btn").addEventListener("click", onCancel);
1002
+ });
1003
+ }
1004
+
1005
+ async function requestApplicationClose() {
1006
+ const exitAction = await maybePromptSaveSessionOnExit();
1007
+ if (exitAction === "cancel") {
1008
+ statusUpdate("Status: Exit cancelled");
1009
+ return;
1010
+ }
1011
+ if (exitAction === "save") {
1012
+ const saveResult = await persistSessionToDisk("exit-prompt");
1013
+ if (!saveResult?.success) {
1014
+ if (saveResult?.canceled) {
1015
+ statusUpdate("Status: Exit cancelled");
1016
+ } else {
1017
+ statusUpdate("Status: Exit cancelled due to save failure");
1018
+ }
1019
+ return;
1020
+ }
1021
+ }
1022
+ window.quitapi.quitApp();
1023
+ }
1024
+
1025
+ function restoreSessionState(sessionState) {
1026
+ if (!sessionState || typeof sessionState !== "object") return;
1027
+
1028
+ const loadedHistory = Array.isArray(sessionState.filterHistory)
1029
+ ? sessionState.filterHistory
1030
+ .filter((query) => typeof query === "string")
1031
+ .map((query) => query.trim())
1032
+ .filter(Boolean)
1033
+ : [];
1034
+ filterHistory.splice(0, filterHistory.length, ...loadedHistory);
1035
+ renderFilterHistory();
1036
+
1037
+ const loadedBookmarks = Array.isArray(sessionState.bookmarkList)
1038
+ ? sessionState.bookmarkList.filter(
1039
+ (bookmark) => typeof bookmark === "string" && bookmark.trim() !== "",
1040
+ )
1041
+ : [];
1042
+ bookmarkList = loadedBookmarks;
1043
+ rebuildBookmarkDropdown();
1044
+
1045
+ const loadedSessionEntries = Array.isArray(sessionState.sessionKeychainEntries)
1046
+ ? sessionState.sessionKeychainEntries.filter(
1047
+ (entry) => entry && typeof entry === "object",
1048
+ )
1049
+ : [];
1050
+ keystorePanel.restoreSessionState(
1051
+ deepCloneSessionData(loadedSessionEntries, []),
1052
+ sessionState.keystoreMode,
1053
+ );
1054
+
1055
+ const loadedNotes = Array.isArray(sessionState.notes)
1056
+ ? sessionState.notes
1057
+ .filter((note) => note && typeof note === "object")
1058
+ .map((note) => ({
1059
+ id:
1060
+ typeof note.id === "string" && note.id.trim()
1061
+ ? note.id
1062
+ : generateNoteId(),
1063
+ text: typeof note.text === "string" ? note.text : String(note.text || ""),
1064
+ color: normalizeNoteColor(note.color),
1065
+ }))
1066
+ : [];
1067
+ notesList = loadedNotes;
1068
+ selectedNoteId = notesList.length > 0 ? notesList[0].id : null;
1069
+ renderNotesList();
1070
+
1071
+ const selectedHost = String(sessionState.selectedHost || "").trim();
1072
+ if (selectedHost && capturedPackets?.["Host"]?.[selectedHost]) {
1073
+ const targetHostsEl = document.getElementById("target_hosts");
1074
+ if (targetHostsEl) {
1075
+ targetHostsEl.value = selectedHost;
1076
+ }
1077
+ hostFilterEl.value = selectedHost;
1078
+ }
1079
+
1080
+ const restoredFilterQuery =
1081
+ typeof sessionState.currentFilterQuery === "string"
1082
+ ? sessionState.currentFilterQuery
1083
+ : "";
1084
+ filterInputEl.value = restoredFilterQuery;
1085
+ syncFilterHighlight();
1086
+ if (restoredFilterQuery.trim()) {
1087
+ runFilterQuery(restoredFilterQuery, { trackHistory: false });
1088
+ } else {
1089
+ filteredPackets = [];
1090
+ document.getElementById("filter-returned").textContent = "Filtered Packets: 0";
1091
+ }
1092
+
1093
+ currentPacketKey =
1094
+ typeof sessionState.currentPacketKey === "string"
1095
+ ? sessionState.currentPacketKey
1096
+ : null;
1097
+ setActivePacketCursor(sessionState.activePacketCursor);
1098
+
1099
+ const navAction =
1100
+ sessionState.packetViewMode === "filtered" &&
1101
+ Array.isArray(filteredPackets) &&
1102
+ filteredPackets.length > 0
1103
+ ? "filtered"
1104
+ : "first-load";
1105
+ handlePacketNavigation(navAction);
1106
+
1107
+ const tabState = sessionState.tabs && typeof sessionState.tabs === "object"
1108
+ ? sessionState.tabs
1109
+ : {};
1110
+ const savedMainTab =
1111
+ typeof tabState.main === "string" && VALID_MAIN_TABS.includes(tabState.main)
1112
+ ? tabState.main
1113
+ : MAIN_TAB_DATA;
1114
+ const savedConvTab = VALID_CONV_SUBTABS.includes(tabState.conv)
1115
+ ? tabState.conv
1116
+ : CONV_CONVERSIONS_SUBTAB;
1117
+ const savedCryptTab = VALID_CRYPT_SUBTABS.includes(tabState.crypt)
1118
+ ? tabState.crypt
1119
+ : CRYPT_SSL_SUBTAB;
1120
+
1121
+ if (savedMainTab === MAIN_TAB_SUMMARY) {
1122
+ showSummary();
1123
+ } else if (savedMainTab === MAIN_TAB_STATS) {
1124
+ showStats();
1125
+ } else if (savedMainTab === MAIN_TAB_LIST) {
1126
+ showPacketList();
1127
+ const listSearchEl = document.getElementById("list-search");
1128
+ const listGroupStreamsEl = document.getElementById("list-group-streams");
1129
+ if (listSearchEl && typeof tabState.listSearch === "string") {
1130
+ listSearchEl.value = tabState.listSearch;
1131
+ listSearchEl.dispatchEvent(new Event("input"));
1132
+ }
1133
+ if (listGroupStreamsEl && typeof tabState.listGroupStreams === "boolean") {
1134
+ listGroupStreamsEl.checked = tabState.listGroupStreams;
1135
+ listGroupStreamsEl.dispatchEvent(new Event("change"));
1136
+ }
1137
+ } else if (savedMainTab === MAIN_TAB_NOTES) {
1138
+ showNotesWorkspace();
1139
+ } else if (savedMainTab === MAIN_TAB_DATA_TOOLS) {
1140
+ showDataTools(savedConvTab);
1141
+ } else if (savedMainTab === MAIN_TAB_CRYPT) {
1142
+ showCryptWorkspace(savedCryptTab);
1143
+ } else if (savedMainTab === MAIN_TAB_KEYSTORE && keystorePanel.isUnlocked()) {
1144
+ keystorePanel.showKeystoreWorkspace();
1145
+ } else {
1146
+ initializeDataView();
1147
+ }
1148
+
1149
+ if (savedMainTab !== MAIN_TAB_DATA_TOOLS) {
1150
+ setConvSubtab(savedConvTab);
1151
+ }
1152
+ if (savedMainTab !== MAIN_TAB_CRYPT) {
1153
+ setCryptSubtab(savedCryptTab);
1154
+ }
1155
+
1156
+ writeLogEntry("Session state restored from JSON");
1157
+ statusUpdate("Status: Session restored");
1158
+ }
1159
+
1160
+ /**
1161
+ * Reads and parses the JSON file, updates UI and state.
1162
+ * Uses chunked parsing for large files to avoid UI blocking.
1163
+ */
1164
+ function processFile(file) {
1165
+ let loadedSessionState = null;
1166
+ const reader = new FileReader();
1167
+ reader.onload = (event) => {
1168
+ const mainPanel = getCachedElement("main");
1169
+ if (isValidJson(event.target.result) == false) {
1170
+ console.log("Invalid JSON file");
1171
+ doError("Invalid JSON file, please upload a valid JSON capture!");
1172
+ fileLoaded(false);
1173
+ return;
1174
+ }
1175
+ fileLoaded(true);
1176
+ jsonOfPackets = event.target.result;
1177
+ getCachedElement("error-container").style.display = "none";
1178
+
1179
+ // Use chunked parsing for large files (>1MB)
1180
+ const fileSize = event.target.result.length;
1181
+ if (fileSize > 1024 * 1024) {
1182
+ statusUpdate(
1183
+ "Status: Parsing large file (" +
1184
+ (fileSize / 1024 / 1024).toFixed(2) +
1185
+ "MB)...",
1186
+ );
1187
+ parseJsonChunked(event.target.result)
1188
+ .then((parsed) => {
1189
+ const normalizedPayload = normalizeLoadedSessionPayload(parsed);
1190
+ if (!normalizedPayload) {
1191
+ doError("Invalid JSON file, please upload a valid capture/session file!");
1192
+ fileLoaded(false);
1193
+ return;
1194
+ }
1195
+ capturedPackets = normalizedPayload.captureData;
1196
+ loadedSessionState = normalizedPayload.sessionState;
1197
+ jsonCapture = JSON.stringify(capturedPackets, null, 2);
1198
+ finalSummary = capturedPackets["Final Summary"] ?? "";
1199
+ finishProcessingFile();
1200
+ })
1201
+ .catch((e) => {
1202
+ console.error("JSON parse error:", e);
1203
+ logErrorEntry("json-parse", e);
1204
+ doError("Error parsing JSON file!");
1205
+ });
1206
+ } else {
1207
+ const normalizedPayload = normalizeLoadedSessionPayload(
1208
+ JSON.parse(event.target.result),
1209
+ );
1210
+ if (!normalizedPayload) {
1211
+ doError("Invalid JSON file, please upload a valid capture/session file!");
1212
+ fileLoaded(false);
1213
+ return;
1214
+ }
1215
+ capturedPackets = normalizedPayload.captureData;
1216
+ loadedSessionState = normalizedPayload.sessionState;
1217
+ jsonCapture = JSON.stringify(capturedPackets, null, 2);
1218
+ finalSummary = capturedPackets["Final Summary"] ?? "";
1219
+ finishProcessingFile();
1220
+ }
1221
+ };
1222
+
1223
+ function finishProcessingFile() {
1224
+ getCachedElement("target_hosts").hidden = false;
1225
+ getCachedElement("summary-btn").style.display = "block";
1226
+ // Reset host list and dropdowns for the new file
1227
+ hostsList = ["0.0.0.0"];
1228
+ const targetHostsDropdown = getCachedElement("target_hosts");
1229
+ while (targetHostsDropdown.options.length > 0) {
1230
+ targetHostsDropdown.remove(0);
1231
+ }
1232
+ bookmarkList = [];
1233
+ notesList = [];
1234
+ selectedNoteId = null;
1235
+ renderNotesList();
1236
+ const selectBookmarkEl = document.getElementById("selectBookmark");
1237
+ while (selectBookmarkEl.options.length > 1) {
1238
+ selectBookmarkEl.remove(1);
1239
+ }
1240
+ // Populate host dropdown with hosts from JSON
1241
+ for (const host in capturedPackets["Host"]) {
1242
+ hostsList.push(host);
1243
+ const newhost = document.createElement("option");
1244
+ newhost.textContent = host;
1245
+ newhost.value = host;
1246
+ targetHostsDropdown.appendChild(newhost);
1247
+ isFileLoaded = true;
1248
+ }
1249
+ writeLogEntry(`Hosts targeted discovered count=${hostsList.length - 1}`);
1250
+ const keystoreEntryCount = keystorePanel.rebuildSessionEntries();
1251
+ writeLogEntry(
1252
+ `Session keychain auto-populated entries=${keystoreEntryCount}`,
1253
+ );
1254
+ const timeframe = getPacketTimeframe();
1255
+ if (timeframe) {
1256
+ writeLogEntry(
1257
+ `Packet timeframe start="${timeframe.first}" end="${timeframe.last}"`,
1258
+ );
1259
+ }
1260
+ writeLogEntry(`Total packet count=${totalPacketCount()}`);
1261
+ showSummary();
1262
+ initializeDataView();
1263
+ if (loadedSessionState) {
1264
+ restoreSessionState(loadedSessionState);
1265
+ }
1266
+ }
1267
+ reader.onerror = (error) => {
1268
+ status.textContent = "Status: Error reading file: " + error;
1269
+ logErrorEntry("file-read", error);
1270
+ doError("Error reading file!");
1271
+ };
1272
+ reader.readAsText(file);
1273
+ }
1274
+
1275
+ /**
1276
+ * Updates the status bar with a message, then resets after 6 seconds.
1277
+ */
1278
+ function statusUpdate(message) {
1279
+ status.textContent = message;
1280
+ setTimeout(() => {
1281
+ status.textContent = "PacketSnitch " + psVer + ": Ready";
1282
+ }, 6000);
1283
+ }
1284
+
1285
+ /**
1286
+ * Loads all capturedPackets for a given host IP into packetsForHost.
1287
+ */
1288
+ function hostPacketInfo(currentIp) {
1289
+ const selected = currentIp;
1290
+ packetsForHost = [];
1291
+ const hostPackets = capturedPackets["Host"][selected];
1292
+ for (const packet in hostPackets) {
1293
+ packetsForHost.push(hostPackets[packet]);
1294
+ }
1295
+ }
1296
+
1297
+ // Use event delegation for dynamically created elements
1298
+ // and cache static elements at module load
1299
+ const navButtons = {
1300
+ prev: getCachedElement("prev-btn"),
1301
+ next: getCachedElement("next-btn"),
1302
+ summary: getCachedElement("summary-btn"),
1303
+ data: getCachedElement("data-btn"),
1304
+ setBookmark: getCachedElement("setBookmark"),
1305
+ };
1306
+
1307
+ // Update host filter when a new host is selected from dropdown
1308
+ getCachedElement("target_hosts").addEventListener("change", function () {
1309
+ const selected = getCachedElement("target_hosts").value;
1310
+ let hostFilterEl = getCachedElement("host_filter");
1311
+ filteredPackets = []; // reset filter when host changes
1312
+ writeLogEntry(`Host target changed host=${selected}`);
1313
+ if (hostFilterEl.value !== selected) {
1314
+ hostFilterEl.value = selected;
1315
+ }
1316
+ });
1317
+
1318
+ getCachedElement("target_hosts").addEventListener("click", function () {
1319
+ const selected = getCachedElement("target_hosts").value;
1320
+ filteredPackets = filterPackets(
1321
+ capturedPackets,
1322
+ "ip.src.addr: " + selected + "|| ip.dst.addr: " + selected,
1323
+ );
1324
+ writeLogEntry(
1325
+ `Host target clicked host=${selected} packets_returned=${filteredPackets.length}`,
1326
+ );
1327
+ handlePacketNavigation("filtered", null);
1328
+ });
1329
+
1330
+ function parseDataToolsInput(format, rawInput) {
1331
+ if (!rawInput || rawInput.trim() === "") {
1332
+ throw new Error("Enter input data first.");
1333
+ }
1334
+
1335
+ if (format === "hex") {
1336
+ const normalized = rawInput
1337
+ .replace(/0x/gi, "")
1338
+ .replace(/[\s,:;-]+/g, "")
1339
+ .trim();
1340
+ if (!normalized) throw new Error("No hex bytes were found.");
1341
+ if (!/^[0-9a-fA-F]+$/.test(normalized)) {
1342
+ throw new Error("Hex input can only contain 0-9 and A-F.");
1343
+ }
1344
+ if (normalized.length % 2 !== 0) {
1345
+ throw new Error("Hex input must contain an even number of characters.");
1346
+ }
1347
+ const bytes = new Uint8Array(normalized.length / 2);
1348
+ for (let i = 0; i < normalized.length; i += 2) {
1349
+ bytes[i / 2] = parseInt(normalized.slice(i, i + 2), 16);
1350
+ }
1351
+ return bytes;
1352
+ }
1353
+
1354
+ if (format === "binary") {
1355
+ const normalized = rawInput.replace(/\s+/g, "");
1356
+ if (!normalized) throw new Error("No binary bits were found.");
1357
+ if (!/^[01]+$/.test(normalized)) {
1358
+ throw new Error("Binary input can only contain 0 and 1.");
1359
+ }
1360
+ if (normalized.length % 8 !== 0) {
1361
+ throw new Error("Binary input must be grouped into full 8-bit bytes.");
1362
+ }
1363
+ const bytes = new Uint8Array(normalized.length / 8);
1364
+ for (let i = 0; i < normalized.length; i += 8) {
1365
+ bytes[i / 8] = parseInt(normalized.slice(i, i + 8), 2);
1366
+ }
1367
+ return bytes;
1368
+ }
1369
+
1370
+ if (format === "base64") {
1371
+ const normalized = rawInput
1372
+ .trim()
1373
+ .replace(/^data:[^;]+;base64,/i, "")
1374
+ .replace(/\s+/g, "");
1375
+ if (!normalized) throw new Error("No base64 content was found.");
1376
+ let decoded = "";
1377
+ try {
1378
+ decoded = atob(normalized);
1379
+ } catch {
1380
+ throw new Error("Invalid base64 input.");
1381
+ }
1382
+ const bytes = new Uint8Array(decoded.length);
1383
+ for (let i = 0; i < decoded.length; i++) {
1384
+ bytes[i] = decoded.charCodeAt(i);
1385
+ }
1386
+ return bytes;
1387
+ }
1388
+
1389
+ if (format === "decimal") {
1390
+ const tokens = rawInput.split(/[\s,]+/).filter(Boolean);
1391
+ if (!tokens.length) throw new Error("No decimal byte values were found.");
1392
+ const values = tokens.map((token) => {
1393
+ const parsed = Number(token);
1394
+ if (!/^\d+$/.test(token) || parsed > 255) {
1395
+ throw new Error(
1396
+ "Each decimal value must be a non-negative integer between 0 and 255.",
1397
+ );
1398
+ }
1399
+ return parsed;
1400
+ });
1401
+ return Uint8Array.from(values);
1402
+ }
1403
+
1404
+ // ascii / utf-8 fallback
1405
+ return new TextEncoder().encode(rawInput);
1406
+ }
1407
+
1408
+ function bytesToBase64(bytes) {
1409
+ let binary = "";
1410
+ bytes.forEach((byte) => {
1411
+ binary += String.fromCharCode(byte);
1412
+ });
1413
+ return btoa(binary);
1414
+ }
1415
+
1416
+ function bytesToPrintableAscii(bytes) {
1417
+ return [...bytes]
1418
+ .map((byte) =>
1419
+ byte >= 32 && byte <= 126 ? String.fromCharCode(byte) : ".",
1420
+ )
1421
+ .join("");
1422
+ }
1423
+
1424
+ function bytesToBigIntDecimal(bytes) {
1425
+ let total = 0n;
1426
+ bytes.forEach((byte) => {
1427
+ total = (total << 8n) + BigInt(byte);
1428
+ });
1429
+ return total.toString(10);
1430
+ }
1431
+
1432
+ function calculateShannonEntropy(bytes) {
1433
+ if (!bytes.length) return 0;
1434
+ const counts = new Array(256).fill(0);
1435
+ bytes.forEach((byte) => {
1436
+ counts[byte] += 1;
1437
+ });
1438
+ let entropy = 0;
1439
+ counts.forEach((count) => {
1440
+ if (!count) return;
1441
+ const p = count / bytes.length;
1442
+ entropy -= p * Math.log2(p);
1443
+ });
1444
+ return entropy;
1445
+ }
1446
+
1447
+ function inferMimeType(bytes) {
1448
+ if (!bytes || !bytes.length) return "application/octet-stream";
1449
+
1450
+ const startsWith = (signature) =>
1451
+ signature.every((value, index) => bytes[index] === value);
1452
+ if (startsWith([0x89, 0x50, 0x4e, 0x47])) return "image/png";
1453
+ if (startsWith([0xff, 0xd8, 0xff])) return "image/jpeg";
1454
+ if (startsWith([0x47, 0x49, 0x46, 0x38])) return "image/gif";
1455
+ if (startsWith([0x25, 0x50, 0x44, 0x46])) return "application/pdf";
1456
+ if (startsWith([0x50, 0x4b, 0x03, 0x04])) return "application/zip";
1457
+ if (startsWith([0x1f, 0x8b])) return "application/gzip";
1458
+ if (startsWith([0x7f, 0x45, 0x4c, 0x46])) return "application/x-elf";
1459
+
1460
+ const utf8Text = new TextDecoder().decode(bytes);
1461
+ const trimmed = utf8Text.trim();
1462
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
1463
+ try {
1464
+ JSON.parse(trimmed);
1465
+ return "application/json";
1466
+ } catch {
1467
+ // Keep evaluating as plain text/binary.
1468
+ }
1469
+ }
1470
+
1471
+ const printableChars = [...utf8Text].filter((ch) => {
1472
+ const code = ch.charCodeAt(0);
1473
+ return (
1474
+ (code >= 32 && code <= 126) || ch === "\n" || ch === "\r" || ch === "\t"
1475
+ );
1476
+ }).length;
1477
+ if (
1478
+ utf8Text.length > 0 &&
1479
+ printableChars / utf8Text.length > DATA_TOOLS_TEXT_MIME_PRINTABLE_THRESHOLD
1480
+ ) {
1481
+ return "text/plain; charset=utf-8";
1482
+ }
1483
+
1484
+ return "application/octet-stream";
1485
+ }
1486
+
1487
+ function getEntropyLabel(entropy) {
1488
+ if (entropy >= DATA_TOOLS_ENTROPY_HIGH_THRESHOLD) return "High";
1489
+ if (entropy >= DATA_TOOLS_ENTROPY_MEDIUM_THRESHOLD) return "Medium";
1490
+ return "Low";
1491
+ }
1492
+
1493
+ function resetDataToolsOutputs() {
1494
+ document.getElementById("data-tools-hex-output").value = "";
1495
+ document.getElementById("data-tools-binary-output").value = "";
1496
+ document.getElementById("data-tools-decimal-output").value = "";
1497
+ document.getElementById("data-tools-decimal-integer-output").value = "";
1498
+ document.getElementById("data-tools-ascii-output").value = "";
1499
+ document.getElementById("data-tools-base64-output").value = "";
1500
+ document.getElementById("data-tools-byte-length").textContent =
1501
+ "Byte Length: 0";
1502
+ document.getElementById("data-tools-mime-type").textContent =
1503
+ "MIME Type: Unknown";
1504
+ document.getElementById("data-tools-entropy").textContent =
1505
+ "Shannon Entropy: 0.00 (Low)";
1506
+ resetHashOutputs();
1507
+ clearProtoDecoderOutput();
1508
+ }
1509
+
1510
+ const HASH_IDS = [
1511
+ "data-tools-md5-output",
1512
+ "data-tools-sha1-output",
1513
+ "data-tools-sha256-output",
1514
+ "data-tools-sha384-output",
1515
+ "data-tools-sha512-output",
1516
+ "data-tools-sha3-256-output",
1517
+ "data-tools-sha3-512-output",
1518
+ "data-tools-ripemd160-output",
1519
+ "data-tools-whirlpool-output",
1520
+ ];
1521
+
1522
+ function resetHashOutputs() {
1523
+ for (const id of HASH_IDS) {
1524
+ document.getElementById(id).value = "";
1525
+ }
1526
+ }
1527
+
1528
+ function bytesToCharString(bytes) {
1529
+ const CHUNK_SIZE = 0x8000;
1530
+ let result = "";
1531
+ for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
1532
+ const chunk = bytes.subarray(i, i + CHUNK_SIZE);
1533
+ result += String.fromCharCode(...chunk);
1534
+ }
1535
+ return result;
1536
+ }
1537
+
1538
+ function computeDataToolsHashes(bytes) {
1539
+ const wordArray = CryptoJS.lib.WordArray.create(bytes);
1540
+ const byteString = bytesToCharString(bytes);
1541
+
1542
+ document.getElementById("data-tools-md5-output").value =
1543
+ CryptoJS.MD5(wordArray).toString(CryptoJS.enc.Hex);
1544
+ document.getElementById("data-tools-sha1-output").value =
1545
+ CryptoJS.SHA1(wordArray).toString(CryptoJS.enc.Hex);
1546
+ document.getElementById("data-tools-sha256-output").value =
1547
+ CryptoJS.SHA256(wordArray).toString(CryptoJS.enc.Hex);
1548
+ document.getElementById("data-tools-sha384-output").value =
1549
+ CryptoJS.SHA384(wordArray).toString(CryptoJS.enc.Hex);
1550
+ document.getElementById("data-tools-sha512-output").value =
1551
+ CryptoJS.SHA512(wordArray).toString(CryptoJS.enc.Hex);
1552
+ document.getElementById("data-tools-sha3-256-output").value = sha3_256(bytes);
1553
+ document.getElementById("data-tools-sha3-512-output").value = sha3_512(bytes);
1554
+ document.getElementById("data-tools-ripemd160-output").value =
1555
+ CryptoJS.RIPEMD160(wordArray).toString(CryptoJS.enc.Hex);
1556
+ const whirlpoolHash =
1557
+ bytes.length > 0 ? whirlpool.encSync(byteString, "hex") : "";
1558
+ document.getElementById("data-tools-whirlpool-output").value = whirlpoolHash;
1559
+ }
1560
+
1561
+ function runDataToolsConversion() {
1562
+ const inputEl = document.getElementById("data-tools-input");
1563
+ const formatEl = document.getElementById("data-tools-format");
1564
+ const errorEl = document.getElementById("data-tools-error");
1565
+
1566
+ try {
1567
+ const bytes = parseDataToolsInput(formatEl.value, inputEl.value);
1568
+ const hexSpaced = [...bytes]
1569
+ .map((byte) => byte.toString(16).padStart(2, "0").toUpperCase())
1570
+ .join(" ");
1571
+ const binarySpaced = [...bytes]
1572
+ .map((byte) => byte.toString(2).padStart(8, "0"))
1573
+ .join(" ");
1574
+ const decimalBytes = [...bytes].join(" ");
1575
+ const asciiPreview = bytesToPrintableAscii(bytes);
1576
+ const base64Value = bytesToBase64(bytes);
1577
+ const entropy = calculateShannonEntropy(bytes);
1578
+ const entropyLabel = getEntropyLabel(entropy);
1579
+ const decimalInteger =
1580
+ bytes.length > DATA_TOOLS_MAX_DECIMAL_INTEGER_BYTES
1581
+ ? `Input exceeds ${DATA_TOOLS_MAX_DECIMAL_INTEGER_BYTES} bytes for decimal integer display`
1582
+ : bytesToBigIntDecimal(bytes);
1583
+
1584
+ document.getElementById("data-tools-hex-output").value = hexSpaced;
1585
+ document.getElementById("data-tools-binary-output").value = binarySpaced;
1586
+ document.getElementById("data-tools-decimal-output").value = decimalBytes;
1587
+ document.getElementById("data-tools-decimal-integer-output").value =
1588
+ decimalInteger;
1589
+ document.getElementById("data-tools-ascii-output").value = asciiPreview;
1590
+ document.getElementById("data-tools-base64-output").value = base64Value;
1591
+ document.getElementById("data-tools-byte-length").textContent =
1592
+ `Byte Length: ${bytes.length}`;
1593
+ document.getElementById("data-tools-mime-type").textContent =
1594
+ `MIME Type: ${inferMimeType(bytes)}`;
1595
+ document.getElementById("data-tools-entropy").textContent =
1596
+ `Shannon Entropy: ${entropy.toFixed(2)} (${entropyLabel})`;
1597
+ errorEl.textContent = "";
1598
+ computeDataToolsHashes(bytes);
1599
+ runProtoDecoder(bytes);
1600
+ } catch (error) {
1601
+ resetDataToolsOutputs();
1602
+ errorEl.textContent =
1603
+ error && typeof error === "object" && "message" in error
1604
+ ? error.message
1605
+ : String(error);
1606
+ }
1607
+ }
1608
+
1609
+ // ── Protocol decoders for the Conv tab ───────────────────────────────────────
1610
+
1611
+ function decodeHttpFromBytes(bytes) {
1612
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
1613
+ const lines = text.split(/\r?\n/);
1614
+ if (!lines.length) return null;
1615
+ const firstLine = lines[0].trim();
1616
+ const requestMatch = firstLine.match(
1617
+ /^([A-Z]+)\s+(\S+)\s+(HTTP\/[\d.]+)$/,
1618
+ );
1619
+ const responseMatch = firstLine.match(/^(HTTP\/[\d.]+)\s+(\d{3})\s*(.*)/);
1620
+ if (!requestMatch && !responseMatch) return null;
1621
+
1622
+ const emptyLineIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "");
1623
+ const headerLines = lines.slice(
1624
+ 1,
1625
+ emptyLineIdx > 0 ? emptyLineIdx : lines.length,
1626
+ );
1627
+ const headers = {};
1628
+ headerLines.forEach((hl) => {
1629
+ const idx = hl.indexOf(":");
1630
+ if (idx > 0) {
1631
+ headers[hl.slice(0, idx).trim()] = hl.slice(idx + 1).trim();
1632
+ }
1633
+ });
1634
+
1635
+ const fields = [];
1636
+ if (requestMatch) {
1637
+ fields.push(
1638
+ { name: "Type", value: "Request" },
1639
+ { name: "Method", value: requestMatch[1] },
1640
+ { name: "URL", value: requestMatch[2] },
1641
+ { name: "Version", value: requestMatch[3] },
1642
+ );
1643
+ [
1644
+ "Host",
1645
+ "User-Agent",
1646
+ "Content-Type",
1647
+ "Content-Length",
1648
+ "Accept",
1649
+ "Accept-Encoding",
1650
+ "Connection",
1651
+ "Authorization",
1652
+ "Referer",
1653
+ "Cookie",
1654
+ ].forEach((h) => {
1655
+ if (headers[h]) fields.push({ name: h, value: headers[h] });
1656
+ });
1657
+ } else {
1658
+ fields.push(
1659
+ { name: "Type", value: "Response" },
1660
+ { name: "Version", value: responseMatch[1] },
1661
+ { name: "Status Code", value: responseMatch[2] },
1662
+ { name: "Status Message", value: responseMatch[3] || "—" },
1663
+ );
1664
+ [
1665
+ "Server",
1666
+ "Content-Type",
1667
+ "Content-Length",
1668
+ "Content-Encoding",
1669
+ "Transfer-Encoding",
1670
+ "Connection",
1671
+ "Location",
1672
+ "Set-Cookie",
1673
+ "Cache-Control",
1674
+ "Date",
1675
+ ].forEach((h) => {
1676
+ if (headers[h]) fields.push({ name: h, value: headers[h] });
1677
+ });
1678
+ }
1679
+ if (emptyLineIdx > 0 && emptyLineIdx < lines.length - 1) {
1680
+ const body = lines
1681
+ .slice(emptyLineIdx + 1)
1682
+ .join("\n")
1683
+ .trim();
1684
+ if (body) {
1685
+ fields.push({
1686
+ name: "Body (preview)",
1687
+ value: body.length > 200 ? body.slice(0, 200) + "…" : body,
1688
+ });
1689
+ }
1690
+ }
1691
+ return { protocol: "HTTP", fields };
1692
+ }
1693
+
1694
+ function decodeTelnetFromBytes(bytes) {
1695
+ const IAC = 0xff;
1696
+ const WILL = 0xfb,
1697
+ WONT = 0xfc,
1698
+ DO = 0xfd,
1699
+ DONT = 0xfe;
1700
+ const SB = 0xfa,
1701
+ SE = 0xf0;
1702
+ const optionNames = {
1703
+ 0: "Binary",
1704
+ 1: "Echo",
1705
+ 3: "Suppress Go Ahead",
1706
+ 5: "Status",
1707
+ 24: "Terminal Type",
1708
+ 31: "Window Size",
1709
+ 32: "Terminal Speed",
1710
+ 34: "Linemode",
1711
+ 39: "New Environment",
1712
+ };
1713
+ const negotiations = [];
1714
+ let text = "";
1715
+ let i = 0;
1716
+ let hasIac = false;
1717
+ while (i < bytes.length) {
1718
+ if (bytes[i] === IAC) {
1719
+ hasIac = true;
1720
+ i++;
1721
+ if (i >= bytes.length) break;
1722
+ const cmd = bytes[i++];
1723
+ if (cmd === WILL || cmd === WONT || cmd === DO || cmd === DONT) {
1724
+ if (i < bytes.length) {
1725
+ const opt = bytes[i++];
1726
+ const cmdName =
1727
+ cmd === WILL
1728
+ ? "WILL"
1729
+ : cmd === WONT
1730
+ ? "WONT"
1731
+ : cmd === DO
1732
+ ? "DO"
1733
+ : "DONT";
1734
+ negotiations.push(`${cmdName} ${optionNames[opt] ?? `Option ${opt}`}`);
1735
+ }
1736
+ } else if (cmd === SB) {
1737
+ while (i < bytes.length) {
1738
+ if (bytes[i] === IAC && i + 1 < bytes.length && bytes[i + 1] === SE) {
1739
+ i += 2;
1740
+ break;
1741
+ }
1742
+ i++;
1743
+ }
1744
+ }
1745
+ } else {
1746
+ const b = bytes[i++];
1747
+ if (b >= 32 && b < 127) text += String.fromCharCode(b);
1748
+ else if (b === 10) text += "\n";
1749
+ else if (b === 13) text += "\r";
1750
+ }
1751
+ }
1752
+ if (!hasIac && !text.trim()) return null;
1753
+ const fields = [];
1754
+ if (negotiations.length) {
1755
+ fields.push({ name: "Negotiations", value: negotiations.join(", ") });
1756
+ }
1757
+ if (text.trim()) {
1758
+ const t = text.trim();
1759
+ fields.push({
1760
+ name: "Text",
1761
+ value: t.length > 500 ? t.slice(0, 500) + "…" : t,
1762
+ });
1763
+ }
1764
+ if (!fields.length) return null;
1765
+ return { protocol: "Telnet", fields };
1766
+ }
1767
+
1768
+ function decodeSshFromBytes(bytes) {
1769
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(
1770
+ bytes.slice(0, 512),
1771
+ );
1772
+ const bannerMatch = text.match(/^SSH-([\S]+)\r?\n/);
1773
+ if (!bannerMatch) return null;
1774
+ const versionStr = bannerMatch[1];
1775
+ const dashIdx = versionStr.indexOf("-");
1776
+ const protocolVersion =
1777
+ dashIdx >= 0 ? versionStr.slice(0, dashIdx) : versionStr;
1778
+ const softwareVersion = dashIdx >= 0 ? versionStr.slice(dashIdx + 1) : "—";
1779
+ const fields = [
1780
+ { name: "Protocol Version", value: protocolVersion },
1781
+ { name: "Software Version", value: softwareVersion },
1782
+ ];
1783
+ const bannerEnd = text.indexOf("\n");
1784
+ if (bannerEnd > 0 && bytes.length > bannerEnd + 1) {
1785
+ fields.push({
1786
+ name: "Additional Data",
1787
+ value: `${bytes.length - bannerEnd - 1} bytes (key exchange)`,
1788
+ });
1789
+ }
1790
+ return { protocol: "SSH / OpenSSH", fields };
1791
+ }
1792
+
1793
+ function decodePop3FromBytes(bytes) {
1794
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
1795
+ const lines = text.split(/\r?\n/).filter((l) => l.trim());
1796
+ if (!lines.length) return null;
1797
+ const POP3_COMMANDS = new Set([
1798
+ "USER",
1799
+ "PASS",
1800
+ "STAT",
1801
+ "LIST",
1802
+ "RETR",
1803
+ "DELE",
1804
+ "NOOP",
1805
+ "RSET",
1806
+ "QUIT",
1807
+ "APOP",
1808
+ "TOP",
1809
+ "UIDL",
1810
+ ]);
1811
+ const fields = [];
1812
+ let detected = false;
1813
+ for (const line of lines) {
1814
+ if (line.startsWith("+OK")) {
1815
+ fields.push({ name: "Response", value: "+OK" });
1816
+ const msg = line.slice(3).trim();
1817
+ if (msg) fields.push({ name: "Message", value: msg });
1818
+ detected = true;
1819
+ } else if (line.startsWith("-ERR")) {
1820
+ fields.push({ name: "Response", value: "-ERR" });
1821
+ const msg = line.slice(4).trim();
1822
+ if (msg) fields.push({ name: "Error", value: msg });
1823
+ detected = true;
1824
+ } else {
1825
+ const parts = line.split(/\s+/);
1826
+ const cmd = parts[0].toUpperCase();
1827
+ if (POP3_COMMANDS.has(cmd)) {
1828
+ fields.push({ name: "Command", value: cmd });
1829
+ if (parts.length > 1) {
1830
+ fields.push({ name: "Argument", value: parts.slice(1).join(" ") });
1831
+ }
1832
+ detected = true;
1833
+ }
1834
+ }
1835
+ if (fields.length >= 10) break;
1836
+ }
1837
+ if (!detected) return null;
1838
+ return { protocol: "POP3", fields };
1839
+ }
1840
+
1841
+ function decodeImapFromBytes(bytes) {
1842
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
1843
+ const lines = text.split(/\r?\n/).filter((l) => l.trim());
1844
+ if (!lines.length) return null;
1845
+ const IMAP_STATUSES = new Set(["OK", "NO", "BAD", "PREAUTH", "BYE"]);
1846
+ const IMAP_COMMANDS = new Set([
1847
+ "CAPABILITY",
1848
+ "NOOP",
1849
+ "LOGOUT",
1850
+ "AUTHENTICATE",
1851
+ "LOGIN",
1852
+ "SELECT",
1853
+ "EXAMINE",
1854
+ "CREATE",
1855
+ "DELETE",
1856
+ "RENAME",
1857
+ "SUBSCRIBE",
1858
+ "UNSUBSCRIBE",
1859
+ "LIST",
1860
+ "LSUB",
1861
+ "STATUS",
1862
+ "APPEND",
1863
+ "CHECK",
1864
+ "CLOSE",
1865
+ "EXPUNGE",
1866
+ "SEARCH",
1867
+ "FETCH",
1868
+ "STORE",
1869
+ "COPY",
1870
+ "UID",
1871
+ "IDLE",
1872
+ ]);
1873
+ const fields = [];
1874
+ let detected = false;
1875
+ for (const line of lines) {
1876
+ if (line.startsWith("* ")) {
1877
+ const val = line.slice(2).trim();
1878
+ fields.push({
1879
+ name: "Untagged",
1880
+ value: val.length > 100 ? val.slice(0, 100) + "…" : val,
1881
+ });
1882
+ detected = true;
1883
+ } else if (line.startsWith("+ ")) {
1884
+ fields.push({ name: "Continuation", value: line.slice(2).trim() });
1885
+ detected = true;
1886
+ } else {
1887
+ const m = line.match(/^(\S+)\s+(\S+)\s*(.*)/);
1888
+ if (m) {
1889
+ const tag = m[1];
1890
+ const word = m[2].toUpperCase();
1891
+ const rest = m[3];
1892
+ if (IMAP_STATUSES.has(word)) {
1893
+ const val = `${word} ${rest}`.trim();
1894
+ fields.push({
1895
+ name: `[${tag}] Status`,
1896
+ value: val.length > 100 ? val.slice(0, 100) + "…" : val,
1897
+ });
1898
+ detected = true;
1899
+ } else if (IMAP_COMMANDS.has(word)) {
1900
+ fields.push({ name: `[${tag}] Command`, value: word });
1901
+ if (rest) {
1902
+ fields.push({
1903
+ name: "Arguments",
1904
+ value: rest.length > 100 ? rest.slice(0, 100) + "…" : rest,
1905
+ });
1906
+ }
1907
+ detected = true;
1908
+ }
1909
+ }
1910
+ }
1911
+ if (fields.length >= 12) break;
1912
+ }
1913
+ if (!detected) return null;
1914
+ return { protocol: "IMAP", fields };
1915
+ }
1916
+
1917
+ function decodeSmtpFromBytes(bytes) {
1918
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
1919
+ const lines = text.split(/\r?\n/).filter((l) => l.trim());
1920
+ if (!lines.length) return null;
1921
+ const SMTP_COMMANDS = new Set([
1922
+ "HELO",
1923
+ "EHLO",
1924
+ "MAIL",
1925
+ "RCPT",
1926
+ "DATA",
1927
+ "RSET",
1928
+ "VRFY",
1929
+ "EXPN",
1930
+ "NOOP",
1931
+ "QUIT",
1932
+ "AUTH",
1933
+ "STARTTLS",
1934
+ ]);
1935
+ const fields = [];
1936
+ let detected = false;
1937
+ for (const line of lines) {
1938
+ const rm = line.match(/^(\d{3})([\s-])(.*)/);
1939
+ if (rm) {
1940
+ const label = `Response ${rm[1]}${rm[2] === "-" ? " (cont.)" : ""}`;
1941
+ fields.push({ name: label, value: rm[3] });
1942
+ detected = true;
1943
+ } else {
1944
+ const parts = line.split(/\s+/);
1945
+ const cmd = parts[0].toUpperCase();
1946
+ if (SMTP_COMMANDS.has(cmd)) {
1947
+ fields.push({ name: "Command", value: cmd });
1948
+ if (parts.length > 1) {
1949
+ const arg = parts.slice(1).join(" ");
1950
+ fields.push({
1951
+ name: "Argument",
1952
+ value: arg.length > 100 ? arg.slice(0, 100) + "…" : arg,
1953
+ });
1954
+ }
1955
+ detected = true;
1956
+ }
1957
+ }
1958
+ if (fields.length >= 12) break;
1959
+ }
1960
+ if (!detected) return null;
1961
+ return { protocol: "SMTP", fields };
1962
+ }
1963
+
1964
+ function autoDetectProtoFromBytes(bytes) {
1965
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(
1966
+ bytes.slice(0, 256),
1967
+ );
1968
+ if (/^SSH-/.test(text)) return "ssh";
1969
+ if (
1970
+ /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\s/.test(text) ||
1971
+ /^HTTP\/[\d.]+ \d{3}/.test(text)
1972
+ )
1973
+ return "http";
1974
+ if (
1975
+ /^(HELO|EHLO|MAIL FROM|RCPT TO|DATA|QUIT)\b/i.test(text) ||
1976
+ /^\d{3}[\s-]/.test(text)
1977
+ )
1978
+ return "smtp";
1979
+ if (
1980
+ /^\+OK/.test(text) ||
1981
+ /^-ERR/.test(text) ||
1982
+ /^(USER|PASS|STAT|LIST|RETR|DELE|QUIT)\b/i.test(text)
1983
+ )
1984
+ return "pop3";
1985
+ if (
1986
+ /^\* /.test(text) ||
1987
+ /^\+ /.test(text) ||
1988
+ /^\S+ (OK|NO|BAD|PREAUTH|BYE)\b/i.test(text) ||
1989
+ /^\S+ (SELECT|LOGIN|FETCH|AUTHENTICATE)\b/i.test(text)
1990
+ )
1991
+ return "imap";
1992
+ // Telnet: require IAC (0xFF) followed by a valid command byte (0xF0–0xFF)
1993
+ const TELNET_COMMANDS = new Set([
1994
+ 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb,
1995
+ 0xfc, 0xfd, 0xfe, 0xff,
1996
+ ]);
1997
+ for (let i = 0; i + 1 < bytes.length; i++) {
1998
+ if (bytes[i] === 0xff && TELNET_COMMANDS.has(bytes[i + 1])) return "telnet";
1999
+ }
2000
+ return null;
2001
+ }
2002
+
2003
+ function renderProtoDecoderOutput(result, selectedProtocol, protocol) {
2004
+ const protoOutput = document.getElementById("data-tools-proto-output");
2005
+ if (!protoOutput) return;
2006
+ activeDataToolsProtoResult = result || null;
2007
+ protoOutput.innerHTML = "";
2008
+ if (!result) {
2009
+ const span = document.createElement("span");
2010
+ span.className = "data-tools-proto-none";
2011
+ span.textContent =
2012
+ selectedProtocol === "auto"
2013
+ ? "No known protocol detected"
2014
+ : `Could not decode as ${(protocol || selectedProtocol).toUpperCase()}`;
2015
+ protoOutput.appendChild(span);
2016
+ return;
2017
+ }
2018
+ const table = document.createElement("table");
2019
+ table.className = "data-tools-proto-table";
2020
+ const headerRow = document.createElement("tr");
2021
+ const th1 = document.createElement("th");
2022
+ th1.textContent = `${result.protocol} Field`;
2023
+ const th2 = document.createElement("th");
2024
+ th2.textContent = "Value";
2025
+ headerRow.appendChild(th1);
2026
+ headerRow.appendChild(th2);
2027
+ table.appendChild(headerRow);
2028
+ result.fields.forEach((field) => {
2029
+ const tr = document.createElement("tr");
2030
+ const tdName = document.createElement("td");
2031
+ tdName.textContent = field.name;
2032
+ const tdVal = document.createElement("td");
2033
+ tdVal.textContent = field.value;
2034
+ tr.appendChild(tdName);
2035
+ tr.appendChild(tdVal);
2036
+ table.appendChild(tr);
2037
+ });
2038
+ protoOutput.appendChild(table);
2039
+ }
2040
+
2041
+ function runProtoDecoder(bytes) {
2042
+ const selectEl = document.getElementById("data-tools-proto-select");
2043
+ const selectedProtocol = selectEl ? selectEl.value : "auto";
2044
+ let protocol = selectedProtocol;
2045
+ if (protocol === "auto") {
2046
+ protocol = autoDetectProtoFromBytes(bytes);
2047
+ }
2048
+ let result = null;
2049
+ switch (protocol) {
2050
+ case "http":
2051
+ result = decodeHttpFromBytes(bytes);
2052
+ break;
2053
+ case "telnet":
2054
+ result = decodeTelnetFromBytes(bytes);
2055
+ break;
2056
+ case "ssh":
2057
+ result = decodeSshFromBytes(bytes);
2058
+ break;
2059
+ case "pop3":
2060
+ result = decodePop3FromBytes(bytes);
2061
+ break;
2062
+ case "imap":
2063
+ result = decodeImapFromBytes(bytes);
2064
+ break;
2065
+ case "smtp":
2066
+ result = decodeSmtpFromBytes(bytes);
2067
+ break;
2068
+ default:
2069
+ protocol = null;
2070
+ }
2071
+ renderProtoDecoderOutput(result, selectedProtocol, protocol);
2072
+ }
2073
+
2074
+ function clearProtoDecoderOutput() {
2075
+ const protoOutput = document.getElementById("data-tools-proto-output");
2076
+ if (protoOutput) protoOutput.innerHTML = "";
2077
+ }
2078
+
2079
+ // ─────────────────────────────────────────────────────────────────────────────
2080
+
2081
+ function showDataTools(tabName = CONV_CONVERSIONS_SUBTAB) {
2082
+ activeMainTab = MAIN_TAB_DATA_TOOLS;
2083
+ statusUpdate("Status: Displaying data conversion tools");
2084
+ writeLogEntry("User opened data conversion tools view");
2085
+ document.getElementById("prev-btn").style.display = "none";
2086
+ document.getElementById("next-btn").style.display = "none";
2087
+ document.getElementById("packetInfoPane").style.display = "none";
2088
+ document.getElementById("packetPayloadPane").style.display = "none";
2089
+ document.getElementById("summary_box").style.display = "none";
2090
+ document.getElementById("stats_box").style.display = "none";
2091
+ document.getElementById("list_box").style.display = "none";
2092
+ document.getElementById("notes_box").style.display = "none";
2093
+ document.getElementById("crypt_box").style.display = "none";
2094
+ document.getElementById("keystore_box").style.display = "none";
2095
+ document.getElementById("rightside").style.display = "none";
2096
+ document.getElementById("data_tools_box").style.display = "flex";
2097
+ setConvSubtab(tabName);
2098
+ }
2099
+
2100
+ function showNotesWorkspace() {
2101
+ activeMainTab = MAIN_TAB_NOTES;
2102
+ statusUpdate("Status: Displaying session notes");
2103
+ writeLogEntry("User opened notes workspace");
2104
+ document.getElementById("prev-btn").style.display = "none";
2105
+ document.getElementById("next-btn").style.display = "none";
2106
+ document.getElementById("packetInfoPane").style.display = "none";
2107
+ document.getElementById("packetPayloadPane").style.display = "none";
2108
+ document.getElementById("summary_box").style.display = "none";
2109
+ document.getElementById("stats_box").style.display = "none";
2110
+ document.getElementById("list_box").style.display = "none";
2111
+ document.getElementById("data_tools_box").style.display = "none";
2112
+ document.getElementById("crypt_box").style.display = "none";
2113
+ document.getElementById("keystore_box").style.display = "none";
2114
+ document.getElementById("notes_box").style.display = "flex";
2115
+ document.getElementById("rightside").style.display = "block";
2116
+ const rightsideDataEl = document.getElementById("rightside-data");
2117
+ const rightsideNotesEl = document.getElementById("rightside-notes");
2118
+ if (rightsideDataEl) rightsideDataEl.hidden = true;
2119
+ if (rightsideNotesEl) rightsideNotesEl.hidden = false;
2120
+ renderNotesList();
2121
+ }
2122
+
2123
+ function getFirstLineOrFallback(elementId, fallback = "") {
2124
+ const text = document.getElementById(elementId)?.textContent || "";
2125
+ const firstLine = text.split("\n")[0]?.trim();
2126
+ return firstLine || fallback;
2127
+ }
2128
+
2129
+
2130
+ const cryptPanel = createCryptPanel({
2131
+ constants: {
2132
+ MAIN_TAB_CRYPT,
2133
+ CRYPT_SSL_SUBTAB,
2134
+ CRYPT_PGP_SUBTAB,
2135
+ CRYPT_OPENSSH_SUBTAB,
2136
+ SESSION_KEYCHAIN_LABEL,
2137
+ STRICT_IPV4_REGEX,
2138
+ },
2139
+ getCapturedPackets: () => capturedPackets,
2140
+ getJsonCapture: () => jsonCapture,
2141
+ setActiveMainTab: (tabName) => {
2142
+ activeMainTab = tabName;
2143
+ },
2144
+ setActiveCryptSubtab: (tabName) => {
2145
+ activeCryptSubtab = tabName;
2146
+ },
2147
+ statusUpdate,
2148
+ writeLogEntry,
2149
+ doError,
2150
+ logErrorEntry,
2151
+ filterInputEl,
2152
+ syncFilterHighlight,
2153
+ runFilterQuery,
2154
+ addSessionKeystoreEntry: (...args) => keystorePanel.addSessionKeystoreEntry(...args),
2155
+ getFirstLineOrFallback,
2156
+ sendDecryptedToConv: ({ hexValue, utf8Value, sourceLabel }) => {
2157
+ const inputEl = document.getElementById("data-tools-input");
2158
+ const formatEl = document.getElementById("data-tools-format");
2159
+ const normalizedHex = String(hexValue || "").trim();
2160
+ const normalizedUtf8 = String(utf8Value || "");
2161
+ if (normalizedHex) {
2162
+ inputEl.value = normalizedHex;
2163
+ formatEl.value = "hex";
2164
+ } else {
2165
+ inputEl.value = normalizedUtf8;
2166
+ formatEl.value = "ascii";
2167
+ }
2168
+ showDataTools(CONV_CONVERSIONS_SUBTAB);
2169
+ runDataToolsConversion();
2170
+ },
2171
+ });
2172
+
2173
+ const {
2174
+ setCryptSubtab,
2175
+ applyCryptCertificateText,
2176
+ applyCryptPrivateKeyText,
2177
+ readCryptTextFile,
2178
+ applyCryptFilterForActiveEntry,
2179
+ loadEncounteredCertificateIntoCrypt,
2180
+ refreshCryptEncounteredEntries,
2181
+ showCryptWorkspace,
2182
+ decryptActiveEntryWithLoadedKey,
2183
+ sendDecryptedPayloadToConvTab,
2184
+ clearCryptDecryptionOutput,
2185
+ } = cryptPanel;
2186
+
2187
+ const listPanel = createListPanel({
2188
+ constants: {
2189
+ MAIN_TAB_LIST,
2190
+ },
2191
+ getJsonCapture: () => jsonCapture,
2192
+ getCapturedPackets: () => capturedPackets,
2193
+ getBookmarkList: () => bookmarkList,
2194
+ setActiveMainTab: (tabName) => {
2195
+ activeMainTab = tabName;
2196
+ },
2197
+ statusUpdate,
2198
+ writeLogEntry,
2199
+ hostFilterEl,
2200
+ filterInputEl,
2201
+ syncFilterHighlight,
2202
+ runFilterQuery,
2203
+ getFilteredPackets: () => filteredPackets,
2204
+ setPacketsForHost: (packets) => {
2205
+ packetsForHost = packets;
2206
+ },
2207
+ setIndex: (nextIndex) => {
2208
+ index = nextIndex;
2209
+ },
2210
+ setCurrentIp: (nextCurrentIp) => {
2211
+ currentIp = nextCurrentIp;
2212
+ },
2213
+ setCurrentPacketKey: (packetKey) => {
2214
+ currentPacketKey = packetKey;
2215
+ },
2216
+ syncBookmarkDropdown,
2217
+ setActivePacketCursor,
2218
+ showAllData,
2219
+ infoPanel,
2220
+ popHexGrid,
2221
+ populateDataTypes,
2222
+ });
2223
+
2224
+ const { showPacketList } = listPanel;
2225
+ let activeContextConversionText = "";
2226
+ let activeContextTarget = null;
2227
+ let activeContextPasteTarget = null;
2228
+ let activeContextFilterQueries = {};
2229
+ let activeContextCookieJarText = "";
2230
+ const convertContextMenuEl = getCachedElement("convert-context-menu");
2231
+ const convertContextButtons = {
2232
+ copy: getCachedElement("ctx-copy"),
2233
+ paste: getCachedElement("ctx-paste"),
2234
+ saveJson: getCachedElement("ctx-save-json"),
2235
+ exportPacket: getCachedElement("ctx-export-packet"),
2236
+ exportPayload: getCachedElement("ctx-export-payload"),
2237
+ hex: getCachedElement("convert-context-hex"),
2238
+ binary: getCachedElement("convert-context-binary"),
2239
+ base64: getCachedElement("convert-context-base64"),
2240
+ decimal: getCachedElement("convert-context-decimal"),
2241
+ ascii: getCachedElement("convert-context-ascii"),
2242
+ loadCursorAscii: getCachedElement("convert-context-load-cursor-ascii"),
2243
+ loadPayload: getCachedElement("convert-context-load-payload"),
2244
+ copyHex: getCachedElement("convert-context-copy-hex"),
2245
+ copyAscii: getCachedElement("convert-context-copy-ascii"),
2246
+ copyRaw: getCachedElement("convert-context-copy-raw"),
2247
+ filterIp: getCachedElement("ctx-filter-ip"),
2248
+ filterPort: getCachedElement("ctx-filter-port"),
2249
+ filterMac: getCachedElement("ctx-filter-mac"),
2250
+ filterProtocol: getCachedElement("ctx-filter-protocol"),
2251
+ filterMime: getCachedElement("ctx-filter-mime"),
2252
+ filterOrIp: getCachedElement("ctx-filter-or-ip"),
2253
+ filterOrPort: getCachedElement("ctx-filter-or-port"),
2254
+ filterOrMac: getCachedElement("ctx-filter-or-mac"),
2255
+ filterOrProtocol: getCachedElement("ctx-filter-or-protocol"),
2256
+ filterOrMime: getCachedElement("ctx-filter-or-mime"),
2257
+ filterNotIp: getCachedElement("ctx-filter-not-ip"),
2258
+ filterNotPort: getCachedElement("ctx-filter-not-port"),
2259
+ filterNotMac: getCachedElement("ctx-filter-not-mac"),
2260
+ filterNotProtocol: getCachedElement("ctx-filter-not-protocol"),
2261
+ filterNotMime: getCachedElement("ctx-filter-not-mime"),
2262
+ filterParenOpen: getCachedElement("ctx-filter-paren-open"),
2263
+ filterParenClose: getCachedElement("ctx-filter-paren-close"),
2264
+ filterParenWrap: getCachedElement("ctx-filter-paren-wrap"),
2265
+ filterClearIp: getCachedElement("ctx-filter-clear-ip"),
2266
+ filterClearPort: getCachedElement("ctx-filter-clear-port"),
2267
+ filterClearMac: getCachedElement("ctx-filter-clear-mac"),
2268
+ filterClearProtocol: getCachedElement("ctx-filter-clear-protocol"),
2269
+ filterClearMime: getCachedElement("ctx-filter-clear-mime"),
2270
+ keystorePasswordSession: getCachedElement("ctx-keystore-password-session"),
2271
+ keystorePasswordPersistent: getCachedElement("ctx-keystore-password-persistent"),
2272
+ keystoreKeySession: getCachedElement("ctx-keystore-key-session"),
2273
+ keystoreKeyPersistent: getCachedElement("ctx-keystore-key-persistent"),
2274
+ keystoreCertSession: getCachedElement("ctx-keystore-cert-session"),
2275
+ keystoreCertPersistent: getCachedElement("ctx-keystore-cert-persistent"),
2276
+ keystoreCookieSession: getCachedElement("ctx-keystore-cookie-session"),
2277
+ keystoreCookiePersistent: getCachedElement("ctx-keystore-cookie-persistent"),
2278
+ keystoreUriSession: getCachedElement("ctx-keystore-uri-session"),
2279
+ keystoreUriPersistent: getCachedElement("ctx-keystore-uri-persistent"),
2280
+ copyCookieJar: getCachedElement("ctx-copy-cookie-jar"),
2281
+ saveCookieJar: getCachedElement("ctx-save-cookie-jar"),
2282
+ notesSendData: getCachedElement("ctx-notes-send-data"),
2283
+ notesSendConvOutput: getCachedElement("ctx-notes-send-conv-output"),
2284
+ notesSendConvHashes: getCachedElement("ctx-notes-send-conv-hashes"),
2285
+ httpFileSave: getCachedElement("ctx-http-file-save"),
2286
+ httpFileLoad: getCachedElement("ctx-http-file-load"),
2287
+ httpFilePreview: getCachedElement("ctx-http-file-preview"),
2288
+ };
2289
+ const convertContextSubmenus = {
2290
+ copy: getCachedElement("ctx-copy-submenu"),
2291
+ convert: getCachedElement("ctx-convert-submenu"),
2292
+ filter: getCachedElement("ctx-filter-submenu"),
2293
+ filterAnd: getCachedElement("ctx-filter-and-submenu"),
2294
+ filterOr: getCachedElement("ctx-filter-or-submenu"),
2295
+ filterNot: getCachedElement("ctx-filter-not-submenu"),
2296
+ filterParentheses: getCachedElement("ctx-filter-parentheses-submenu"),
2297
+ filterClear: getCachedElement("ctx-filter-clear-submenu"),
2298
+ notes: getCachedElement("ctx-notes-submenu"),
2299
+ export: getCachedElement("ctx-export-submenu"),
2300
+ keystore: getCachedElement("ctx-keystore-submenu"),
2301
+ keystorePassword: getCachedElement("ctx-keystore-password-submenu"),
2302
+ keystoreKey: getCachedElement("ctx-keystore-key-submenu"),
2303
+ keystoreCert: getCachedElement("ctx-keystore-cert-submenu"),
2304
+ keystoreCookie: getCachedElement("ctx-keystore-cookie-submenu"),
2305
+ keystoreUri: getCachedElement("ctx-keystore-uri-submenu"),
2306
+ httpFile: getCachedElement("ctx-http-file-submenu"),
2307
+ };
2308
+ const convertContextDividerEl = getCachedElement("convert-context-divider");
2309
+ const convertContextSaveDividerEl = getCachedElement(
2310
+ "convert-context-save-divider",
2311
+ );
2312
+ const convertContextSubmenuEls = Array.from(
2313
+ convertContextMenuEl.querySelectorAll(".ctx-submenu"),
2314
+ );
2315
+
2316
+ function resetConvertContextSubmenuPositions() {
2317
+ convertContextSubmenuEls.forEach((submenuEl) => {
2318
+ submenuEl.classList.remove("ctx-submenu-flip-x", "ctx-submenu-flip-y");
2319
+ });
2320
+ }
2321
+
2322
+ function updateConvertContextSubmenuPositions() {
2323
+ const viewportPadding = 8;
2324
+ resetConvertContextSubmenuPositions();
2325
+
2326
+ convertContextSubmenuEls.forEach((submenuEl) => {
2327
+ if (submenuEl.style.display === "none") return;
2328
+ // Use :scope > to get only the direct child panel, not a grandchild's.
2329
+ const submenuPanelEl = submenuEl.querySelector(":scope > .ctx-submenu-panel");
2330
+ if (!submenuPanelEl) return;
2331
+
2332
+ // Temporarily reveal every ancestor .ctx-submenu-panel so that this
2333
+ // element has a real viewport position when getBoundingClientRect() is
2334
+ // called. Without this, panels at depth > 1 are inside a hidden
2335
+ // ancestor and always return zero-area rects, making the overflow
2336
+ // calculations completely wrong for those levels.
2337
+ const revealedAncestors = [];
2338
+ let node = submenuEl.parentElement;
2339
+ while (node && node !== convertContextMenuEl) {
2340
+ if (
2341
+ node.classList.contains("ctx-submenu-panel") &&
2342
+ node.style.display !== "block"
2343
+ ) {
2344
+ revealedAncestors.push({
2345
+ el: node,
2346
+ previousDisplay: node.style.display,
2347
+ previousVisibility: node.style.visibility,
2348
+ previousPointerEvents: node.style.pointerEvents,
2349
+ });
2350
+ node.style.display = "block";
2351
+ node.style.visibility = "hidden";
2352
+ node.style.pointerEvents = "none";
2353
+ }
2354
+ node = node.parentElement;
2355
+ }
2356
+
2357
+ const previousDisplay = submenuPanelEl.style.display;
2358
+ const previousVisibility = submenuPanelEl.style.visibility;
2359
+ const previousPointerEvents = submenuPanelEl.style.pointerEvents;
2360
+ submenuPanelEl.style.display = "block";
2361
+ submenuPanelEl.style.visibility = "hidden";
2362
+ submenuPanelEl.style.pointerEvents = "none";
2363
+
2364
+ const submenuRect = submenuEl.getBoundingClientRect();
2365
+ const panelRect = submenuPanelEl.getBoundingClientRect();
2366
+ const wouldOverflowRight =
2367
+ submenuRect.right + panelRect.width > window.innerWidth - viewportPadding;
2368
+ const wouldOverflowBottom =
2369
+ submenuRect.top + panelRect.height > window.innerHeight - viewportPadding;
2370
+ const hasRoomAbove =
2371
+ submenuRect.bottom - panelRect.height >= viewportPadding;
2372
+
2373
+ if (wouldOverflowRight) {
2374
+ submenuEl.classList.add("ctx-submenu-flip-x");
2375
+ }
2376
+ if (wouldOverflowBottom && hasRoomAbove) {
2377
+ submenuEl.classList.add("ctx-submenu-flip-y");
2378
+ }
2379
+
2380
+ submenuPanelEl.style.display = previousDisplay;
2381
+ submenuPanelEl.style.visibility = previousVisibility;
2382
+ submenuPanelEl.style.pointerEvents = previousPointerEvents;
2383
+
2384
+ // Restore ancestor panels in reverse order (innermost first).
2385
+ for (let i = revealedAncestors.length - 1; i >= 0; i--) {
2386
+ const ancestor = revealedAncestors[i];
2387
+ ancestor.el.style.display = ancestor.previousDisplay;
2388
+ ancestor.el.style.visibility = ancestor.previousVisibility;
2389
+ ancestor.el.style.pointerEvents = ancestor.previousPointerEvents;
2390
+ }
2391
+ });
2392
+ }
2393
+
2394
+ function hideConvertContextMenu() {
2395
+ activeContextConversionText = "";
2396
+ activeContextTarget = null;
2397
+ activeContextPasteTarget = null;
2398
+ activeContextFilterQueries = {};
2399
+ activeContextCookieJarText = "";
2400
+ resetConvertContextSubmenuPositions();
2401
+ convertContextMenuEl.hidden = true;
2402
+ }
2403
+
2404
+ function normalizeContextToken(value) {
2405
+ if (value === null || value === undefined) return "";
2406
+ return String(value).replace(/\s+/g, " ").trim();
2407
+ }
2408
+
2409
+ function extractContextIp(value) {
2410
+ const normalized = normalizeContextToken(value);
2411
+ const match = normalized.match(CONTEXT_IPV4_REGEX);
2412
+ return match ? match[0] : "";
2413
+ }
2414
+
2415
+ function extractContextPort(value, allowStandaloneNumber = false) {
2416
+ const normalized = normalizeContextToken(value);
2417
+ const ipPortMatch = normalized.match(
2418
+ /\b(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}:(\d{1,5})\b/,
2419
+ );
2420
+ if (ipPortMatch) {
2421
+ const ipPortValue = Number.parseInt(ipPortMatch[4], 10);
2422
+ return ipPortValue >= 0 && ipPortValue <= 65535 ? String(ipPortValue) : "";
2423
+ }
2424
+ if (!allowStandaloneNumber) return "";
2425
+ const portMatch = normalized.match(/^\d{1,5}$/);
2426
+ if (!portMatch) return "";
2427
+ const portValue = Number.parseInt(normalized, 10);
2428
+ return portValue >= 0 && portValue <= 65535 ? String(portValue) : "";
2429
+ }
2430
+
2431
+ function extractContextMac(value) {
2432
+ const normalized = normalizeContextToken(value);
2433
+ const match = normalized.match(CONTEXT_MAC_REGEX);
2434
+ return match ? match[0].toLowerCase() : "";
2435
+ }
2436
+
2437
+ function extractContextMimeType(value) {
2438
+ const normalized = normalizeContextToken(value);
2439
+ const labelStripped = normalized
2440
+ .replace(/^mime(?:\s+type)?\s*:\s*/i, "")
2441
+ .trim();
2442
+ if (!labelStripped) return "";
2443
+ const mimeBase = labelStripped.split(";")[0].trim();
2444
+ return CONTEXT_MIME_REGEX.test(mimeBase) ? mimeBase.toLowerCase() : "";
2445
+ }
2446
+
2447
+ function extractContextProtocol(value) {
2448
+ const normalized = normalizeContextToken(value);
2449
+ const labelStripped = normalized
2450
+ .replace(/^protocol(?:\s+name)?\s*:\s*/i, "")
2451
+ .replace(/^app(?:lication)?\s+protocol\s*:\s*/i, "")
2452
+ .replace(/^transport\s+protocol\s*:\s*/i, "")
2453
+ .trim();
2454
+ if (!labelStripped) return "";
2455
+ const protocolMatch = labelStripped.match(/^[a-z][a-z0-9+_-]*$/i);
2456
+ return protocolMatch ? labelStripped.toLowerCase() : "";
2457
+ }
2458
+
2459
+ function sanitizeFilterTerm(value) {
2460
+ return normalizeContextToken(value)
2461
+ .replace(/[^a-zA-Z0-9:./+-]/g, "")
2462
+ .trim();
2463
+ }
2464
+
2465
+ function buildContextFilterQueries(target, selectedText, conversionText) {
2466
+ const candidates = [];
2467
+ const addCandidate = (value) => {
2468
+ const normalized = normalizeContextToken(value);
2469
+ if (!normalized) return;
2470
+ if (!candidates.includes(normalized)) candidates.push(normalized);
2471
+ };
2472
+
2473
+ addCandidate(selectedText);
2474
+ addCandidate(conversionText);
2475
+
2476
+ let rowName = "";
2477
+ let rowPortEligible = false;
2478
+ const row = target?.closest?.("tr");
2479
+ if (row) {
2480
+ const cells = row.querySelectorAll("td");
2481
+ rowName = normalizeContextToken(cells[0]?.textContent);
2482
+ const rowValue = normalizeContextToken(cells[1]?.textContent);
2483
+ addCandidate(rowValue);
2484
+ rowPortEligible = /\bport\b/i.test(rowName);
2485
+ if (/^ip\s*:?\s*port$/i.test(rowName) && rowValue) {
2486
+ const bracketedIpv6Match = rowValue.match(/^\[([^\]]+)\]:(\d{1,5})$/);
2487
+ if (bracketedIpv6Match) {
2488
+ addCandidate(bracketedIpv6Match[1]);
2489
+ addCandidate(bracketedIpv6Match[2]);
2490
+ } else {
2491
+ const ipv4PortMatch = rowValue.match(
2492
+ /^((?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}):(\d{1,5})$/,
2493
+ );
2494
+ if (ipv4PortMatch) {
2495
+ addCandidate(ipv4PortMatch[1]);
2496
+ addCandidate(ipv4PortMatch[2]);
2497
+ } else {
2498
+ const lastColonIndex = rowValue.lastIndexOf(":");
2499
+ if (lastColonIndex > 0) {
2500
+ const maybePort = rowValue.slice(lastColonIndex + 1).trim();
2501
+ if (/^\d{1,5}$/.test(maybePort)) {
2502
+ addCandidate(maybePort);
2503
+ }
2504
+ }
2505
+ }
2506
+ }
2507
+ }
2508
+ }
2509
+
2510
+ const filterQueries = {};
2511
+ const skipProtocol = /^network\s+class$/i.test(rowName);
2512
+ for (const candidate of candidates) {
2513
+ if (!filterQueries.ip) {
2514
+ const ip = extractContextIp(candidate);
2515
+ if (ip) {
2516
+ const safeIp = sanitizeFilterTerm(ip);
2517
+ filterQueries.ip = `ip.src.addr: ${safeIp} || ip.dst.addr: ${safeIp}`;
2518
+ }
2519
+ }
2520
+ if (!filterQueries.port) {
2521
+ const port = extractContextPort(candidate, rowPortEligible);
2522
+ if (port) {
2523
+ const safePort = sanitizeFilterTerm(port);
2524
+ filterQueries.port =
2525
+ `tcp.src.port: ${safePort} || tcp.dst.port: ${safePort}` +
2526
+ ` || udp.src.port: ${safePort} || udp.dst.port: ${safePort}`;
2527
+ }
2528
+ }
2529
+ if (!filterQueries.mac) {
2530
+ const mac = extractContextMac(candidate);
2531
+ if (mac) {
2532
+ const safeMac = sanitizeFilterTerm(mac);
2533
+ filterQueries.mac = `ether.src.mac.addr: ${safeMac} || ether.dst.mac.addr: ${safeMac}`;
2534
+ }
2535
+ }
2536
+ if (!filterQueries.protocol && !skipProtocol) {
2537
+ const protocol = extractContextProtocol(candidate);
2538
+ if (protocol) {
2539
+ const safeProtocol = sanitizeFilterTerm(protocol);
2540
+ filterQueries.protocol = `wire.proto: ${safeProtocol} || tcp.proto: ${safeProtocol}`;
2541
+ }
2542
+ }
2543
+ if (!filterQueries.mime) {
2544
+ const mimeType = extractContextMimeType(candidate);
2545
+ if (mimeType) {
2546
+ const safeMimeType = sanitizeFilterTerm(mimeType);
2547
+ filterQueries.mime = `mime.type: ${safeMimeType}`;
2548
+ }
2549
+ }
2550
+ }
2551
+
2552
+ return filterQueries;
2553
+ }
2554
+
2555
+ function getTrimmedSelectionText() {
2556
+ return window.getSelection()?.toString().trim() || "";
2557
+ }
2558
+
2559
+ function looksLikeBase64(text) {
2560
+ const normalized = text.replace(/\s+/g, "");
2561
+ return (
2562
+ normalized.length >= DATA_TOOLS_CONTEXT_BASE64_MIN_LENGTH &&
2563
+ normalized.length % 4 === 0 &&
2564
+ /^[A-Za-z0-9+/]*={0,2}$/.test(normalized) &&
2565
+ normalized.replace(/=/g, "").length > 0
2566
+ );
2567
+ }
2568
+
2569
+ function detectConvertibleFormats(text) {
2570
+ const formats = [];
2571
+ const value = text.trim();
2572
+ if (!value) return formats;
2573
+
2574
+ const canParse = (format) => {
2575
+ try {
2576
+ parseDataToolsInput(format, value);
2577
+ return true;
2578
+ } catch {
2579
+ return false;
2580
+ }
2581
+ };
2582
+
2583
+ if (canParse("hex")) formats.push("hex");
2584
+ if (canParse("binary")) formats.push("binary");
2585
+ if (canParse("decimal")) formats.push("decimal");
2586
+ if (looksLikeBase64(value) && canParse("base64")) formats.push("base64");
2587
+ if (formats.length > 0) formats.push("ascii");
2588
+
2589
+ return formats;
2590
+ }
2591
+
2592
+ function splitCookieHeaderEntries(headerValue) {
2593
+ if (typeof headerValue !== "string" || !headerValue.trim()) return [];
2594
+ const entries = [];
2595
+ let currentEntry = "";
2596
+ let inQuotes = false;
2597
+ let isEscaped = false;
2598
+
2599
+ for (const character of headerValue) {
2600
+ if (isEscaped) {
2601
+ currentEntry += character;
2602
+ isEscaped = false;
2603
+ continue;
2604
+ }
2605
+ if (character === "\\" && inQuotes) {
2606
+ currentEntry += character;
2607
+ isEscaped = true;
2608
+ continue;
2609
+ }
2610
+ if (character === '"') {
2611
+ inQuotes = !inQuotes;
2612
+ currentEntry += character;
2613
+ continue;
2614
+ }
2615
+ if (character === ";" && !inQuotes) {
2616
+ const trimmedEntry = currentEntry.trim();
2617
+ if (trimmedEntry) entries.push(trimmedEntry);
2618
+ currentEntry = "";
2619
+ continue;
2620
+ }
2621
+ currentEntry += character;
2622
+ }
2623
+
2624
+ const trimmedEntry = currentEntry.trim();
2625
+ if (trimmedEntry) entries.push(trimmedEntry);
2626
+ return entries;
2627
+ }
2628
+
2629
+ function extractCookieJarEntriesFromHttpFields(fields) {
2630
+ if (!Array.isArray(fields)) return [];
2631
+ const cookieEntries = [];
2632
+ const addCookieEntry = (entry) => {
2633
+ const normalizedEntry =
2634
+ typeof entry === "string" ? entry.trim() : String(entry || "").trim();
2635
+ if (!normalizedEntry || !normalizedEntry.includes("=")) return;
2636
+ if (!cookieEntries.includes(normalizedEntry)) {
2637
+ cookieEntries.push(normalizedEntry);
2638
+ }
2639
+ };
2640
+
2641
+ fields.forEach((field) => {
2642
+ const fieldName = String(field?.name || "").trim().toLowerCase();
2643
+ const fieldValue = typeof field?.value === "string" ? field.value.trim() : "";
2644
+ if (!fieldValue) return;
2645
+ if (fieldName === "cookie") {
2646
+ splitCookieHeaderEntries(fieldValue).forEach((cookieEntry) => {
2647
+ addCookieEntry(cookieEntry);
2648
+ });
2649
+ return;
2650
+ }
2651
+ if (fieldName === "set-cookie") {
2652
+ addCookieEntry(fieldValue);
2653
+ }
2654
+ });
2655
+
2656
+ return cookieEntries;
2657
+ }
2658
+
2659
+ keystorePanel = createKeystorePanel({
2660
+ statusUpdate,
2661
+ writeLogEntry,
2662
+ doError,
2663
+ logErrorEntry,
2664
+ getCapturedPackets: () => capturedPackets,
2665
+ getJsonCapture: () => jsonCapture,
2666
+ setActiveMainTab: (tabName) => {
2667
+ activeMainTab = tabName;
2668
+ },
2669
+ MAIN_TAB_KEYSTORE,
2670
+ parseDataToolsInput,
2671
+ decodeHttpFromBytes,
2672
+ extractCookieJarEntriesFromHttpFields,
2673
+ getTrimmedSelectionText,
2674
+ hideConvertContextMenu,
2675
+ getActiveContextConversionText: () => activeContextConversionText,
2676
+ getApplyCryptCertificateText: () => applyCryptCertificateText,
2677
+ getApplyCryptPrivateKeyText: () => applyCryptPrivateKeyText,
2678
+ openExternalUrl: (url) => window.browserapi.openExternalUrl(url),
2679
+ });
2680
+
2681
+ function buildCookieJarTextFromHttpFields(fields) {
2682
+ return extractCookieJarEntriesFromHttpFields(fields).join("\n");
2683
+ }
2684
+
2685
+ function getCookieJarTextForCurrentPacket() {
2686
+ const payloadHex = getCurrentRawPayloadHex();
2687
+ if (!payloadHex) return "";
2688
+ try {
2689
+ const bytes = parseDataToolsInput("hex", payloadHex);
2690
+ const decodedHttp = decodeHttpFromBytes(bytes);
2691
+ return decodedHttp?.protocol === "HTTP"
2692
+ ? buildCookieJarTextFromHttpFields(decodedHttp.fields)
2693
+ : "";
2694
+ } catch {
2695
+ // Context-menu cookie actions are best-effort for the active packet.
2696
+ // Fallback to no cookie jar when payload decode fails.
2697
+ return "";
2698
+ }
2699
+ }
2700
+
2701
+ function getCookieJarTextForContextTarget(target) {
2702
+ if (target?.closest?.("#data-tools-proto-output")) {
2703
+ const dataToolsCookieJarText =
2704
+ getActiveDataToolsProtoResult()?.protocol === "HTTP"
2705
+ ? buildCookieJarTextFromHttpFields(getActiveDataToolsProtoResult().fields)
2706
+ : "";
2707
+ if (dataToolsCookieJarText) return dataToolsCookieJarText;
2708
+ }
2709
+ return getCookieJarTextForCurrentPacket();
2710
+ }
2711
+
2712
+ function getConversionTextFromTarget(target) {
2713
+ const selectedText = window.getSelection()?.toString().trim();
2714
+ if (selectedText) return selectedText;
2715
+
2716
+ const directValue =
2717
+ target && "value" in target && typeof target.value === "string"
2718
+ ? target.value.trim()
2719
+ : "";
2720
+ if (directValue) return directValue;
2721
+
2722
+ if (target?.classList?.contains("griditem")) {
2723
+ return target.textContent.trim();
2724
+ }
2725
+
2726
+ const textContent = target?.textContent ? target.textContent.trim() : "";
2727
+ if (!textContent) return "";
2728
+
2729
+ if (textContent.includes(":")) {
2730
+ const prefix = textContent.split(":")[0]?.trim();
2731
+ const looksLikeLabel = /^[A-Za-z][\w\s-]*$/.test(prefix);
2732
+ // Keep full suffix so values containing additional colons (IPv6/timestamps)
2733
+ // are preserved, e.g. "Label: fe80::1" or "Time: 12:34:56".
2734
+ if (looksLikeLabel) {
2735
+ const suffix = textContent.split(":").slice(1).join(":").trim();
2736
+ if (suffix) return suffix;
2737
+ }
2738
+ }
2739
+
2740
+ return textContent;
2741
+ }
2742
+
2743
+ function getPasteTargetFromContextTarget(target) {
2744
+ if (!(target instanceof Element)) return null;
2745
+ const editableTarget = target.closest(
2746
+ 'input, textarea, [contenteditable="true"], [contenteditable=""]',
2747
+ );
2748
+ if (!editableTarget) return null;
2749
+
2750
+ if ("readOnly" in editableTarget && editableTarget.readOnly) return null;
2751
+ if ("disabled" in editableTarget && editableTarget.disabled) return null;
2752
+
2753
+ if (editableTarget.tagName === "INPUT") {
2754
+ const disallowedInputTypes = new Set([
2755
+ "button",
2756
+ "checkbox",
2757
+ "color",
2758
+ "file",
2759
+ "hidden",
2760
+ "image",
2761
+ "radio",
2762
+ "range",
2763
+ "reset",
2764
+ "submit",
2765
+ ]);
2766
+ const inputType = (editableTarget.type || "text").toLowerCase();
2767
+ if (disallowedInputTypes.has(inputType)) return null;
2768
+ }
2769
+
2770
+ return editableTarget;
2771
+ }
2772
+
2773
+ function showConvertContextMenu(
2774
+ x,
2775
+ y,
2776
+ sourceText,
2777
+ formats,
2778
+ {
2779
+ isHexViewTarget = false,
2780
+ target = null,
2781
+ pasteTarget = null,
2782
+ showCopySelection = false,
2783
+ showPaste = true,
2784
+ showSaveJson = true,
2785
+ filterQueries = {},
2786
+ cookieJarText = "",
2787
+ showManualKeystoreUri = false,
2788
+ } = {},
2789
+ ) {
2790
+ activeContextConversionText = sourceText;
2791
+ activeContextTarget = target;
2792
+ activeContextPasteTarget = pasteTarget;
2793
+ activeContextFilterQueries = filterQueries;
2794
+ activeContextCookieJarText = cookieJarText;
2795
+
2796
+ convertContextButtons.copy.style.display = showCopySelection
2797
+ ? "block"
2798
+ : "none";
2799
+ convertContextButtons.paste.style.display = showPaste ? "block" : "none";
2800
+ convertContextButtons.saveJson.style.display = showSaveJson
2801
+ ? "block"
2802
+ : "none";
2803
+ const hasPacketToExport = Boolean(
2804
+ getCurrentPacketForExport(packetsForHost, getActivePacketCursor()),
2805
+ );
2806
+ const currentPayloadHex = getCurrentRawPayloadHex();
2807
+ const hasPayloadToExport = Boolean(currentPayloadHex);
2808
+ const hasHttpBody = Boolean(getCurrentHttpBodyHex());
2809
+ convertContextButtons.exportPacket.style.display = hasPacketToExport
2810
+ ? "block"
2811
+ : "none";
2812
+ convertContextButtons.exportPayload.style.display = hasPayloadToExport
2813
+ ? "block"
2814
+ : "none";
2815
+ convertContextButtons.httpFileSave.style.display = hasHttpBody
2816
+ ? "block"
2817
+ : "none";
2818
+ convertContextButtons.httpFileLoad.style.display = hasHttpBody
2819
+ ? "block"
2820
+ : "none";
2821
+ convertContextButtons.httpFilePreview.style.display = hasHttpBody
2822
+ ? "block"
2823
+ : "none";
2824
+
2825
+ ["hex", "binary", "base64", "decimal", "ascii"].forEach((format) => {
2826
+ convertContextButtons[format].style.display = formats.includes(format)
2827
+ ? "block"
2828
+ : "none";
2829
+ });
2830
+ convertContextButtons.copyHex.style.display = isHexViewTarget
2831
+ ? "block"
2832
+ : "none";
2833
+ convertContextButtons.copyAscii.style.display = isHexViewTarget
2834
+ ? "block"
2835
+ : "none";
2836
+ convertContextButtons.copyRaw.style.display = isHexViewTarget
2837
+ ? "block"
2838
+ : "none";
2839
+ convertContextButtons.copyCookieJar.style.display = cookieJarText
2840
+ ? "block"
2841
+ : "none";
2842
+ convertContextButtons.saveCookieJar.style.display = cookieJarText
2843
+ ? "block"
2844
+ : "none";
2845
+ convertContextButtons.loadPayload.style.display = hasPayloadToExport
2846
+ ? "block"
2847
+ : "none";
2848
+ const cursorByteIndex = Number.parseInt(target?.dataset?.byteIndex ?? "-1", 10);
2849
+ const hasCursorAsciiValue = Boolean(
2850
+ target?.classList?.contains("griditem") &&
2851
+ getCursorAsciiContextLoadData(currentPayloadHex, cursorByteIndex),
2852
+ );
2853
+ convertContextButtons.loadCursorAscii.style.display = hasCursorAsciiValue
2854
+ ? "block"
2855
+ : "none";
2856
+ convertContextButtons.filterIp.style.display = filterQueries.ip
2857
+ ? "block"
2858
+ : "none";
2859
+ convertContextButtons.filterPort.style.display = filterQueries.port
2860
+ ? "block"
2861
+ : "none";
2862
+ convertContextButtons.filterMac.style.display = filterQueries.mac
2863
+ ? "block"
2864
+ : "none";
2865
+ convertContextButtons.filterProtocol.style.display = filterQueries.protocol
2866
+ ? "block"
2867
+ : "none";
2868
+ convertContextButtons.filterMime.style.display = filterQueries.mime
2869
+ ? "block"
2870
+ : "none";
2871
+ convertContextButtons.filterOrIp.style.display = filterQueries.ip
2872
+ ? "block"
2873
+ : "none";
2874
+ convertContextButtons.filterOrPort.style.display = filterQueries.port
2875
+ ? "block"
2876
+ : "none";
2877
+ convertContextButtons.filterOrMac.style.display = filterQueries.mac
2878
+ ? "block"
2879
+ : "none";
2880
+ convertContextButtons.filterOrProtocol.style.display = filterQueries.protocol
2881
+ ? "block"
2882
+ : "none";
2883
+ convertContextButtons.filterOrMime.style.display = filterQueries.mime
2884
+ ? "block"
2885
+ : "none";
2886
+ convertContextButtons.filterNotIp.style.display = filterQueries.ip
2887
+ ? "block"
2888
+ : "none";
2889
+ convertContextButtons.filterNotPort.style.display = filterQueries.port
2890
+ ? "block"
2891
+ : "none";
2892
+ convertContextButtons.filterNotMac.style.display = filterQueries.mac
2893
+ ? "block"
2894
+ : "none";
2895
+ convertContextButtons.filterNotProtocol.style.display = filterQueries.protocol
2896
+ ? "block"
2897
+ : "none";
2898
+ convertContextButtons.filterNotMime.style.display = filterQueries.mime
2899
+ ? "block"
2900
+ : "none";
2901
+ convertContextButtons.filterParenOpen.style.display = "block";
2902
+ convertContextButtons.filterParenClose.style.display = "block";
2903
+ convertContextButtons.filterParenWrap.style.display = "block";
2904
+ convertContextButtons.filterClearIp.style.display = filterQueries.ip
2905
+ ? "block"
2906
+ : "none";
2907
+ convertContextButtons.filterClearPort.style.display = filterQueries.port
2908
+ ? "block"
2909
+ : "none";
2910
+ convertContextButtons.filterClearMac.style.display = filterQueries.mac
2911
+ ? "block"
2912
+ : "none";
2913
+ convertContextButtons.filterClearProtocol.style.display =
2914
+ filterQueries.protocol ? "block" : "none";
2915
+ convertContextButtons.filterClearMime.style.display = filterQueries.mime
2916
+ ? "block"
2917
+ : "none";
2918
+ const hasCookieActions = Boolean(cookieJarText);
2919
+ const hasContextDataForNotes = Boolean(
2920
+ (sourceText && sourceText.trim()) || getTrimmedSelectionText(),
2921
+ );
2922
+ const hasConvOutputForNotes = Boolean(buildConvConvertedOutputNoteText());
2923
+ const hasConvHashesForNotes = Boolean(buildConvHashesNoteText());
2924
+ convertContextButtons.notesSendData.style.display = hasContextDataForNotes
2925
+ ? "block"
2926
+ : "none";
2927
+ convertContextButtons.notesSendConvOutput.style.display = hasConvOutputForNotes
2928
+ ? "block"
2929
+ : "none";
2930
+ convertContextButtons.notesSendConvHashes.style.display = hasConvHashesForNotes
2931
+ ? "block"
2932
+ : "none";
2933
+ const hasNotesActions =
2934
+ hasContextDataForNotes || hasConvOutputForNotes || hasConvHashesForNotes;
2935
+ const hasCopyActions = showCopySelection || isHexViewTarget || hasCookieActions;
2936
+ const hasClipboardActions = hasCopyActions || showPaste;
2937
+ const hasGeneralActions = hasClipboardActions;
2938
+ const hasDataTypeActions =
2939
+ formats.length > 0 || hasPayloadToExport || hasCursorAsciiValue;
2940
+ const hasFilterActions = Object.values(filterQueries).some(Boolean);
2941
+ const hasContextTextKeystoreActions = showCopySelection || Boolean(sourceText);
2942
+ const hasKeystoreActions = hasContextTextKeystoreActions || showManualKeystoreUri;
2943
+ const hasExportActions =
2944
+ showSaveJson || hasPacketToExport || hasPayloadToExport || hasCookieActions;
2945
+ convertContextSubmenus.copy.style.display = hasCopyActions ? "block" : "none";
2946
+ convertContextSubmenus.convert.style.display = hasDataTypeActions
2947
+ ? "block"
2948
+ : "none";
2949
+ convertContextSubmenus.filter.style.display = hasFilterActions
2950
+ ? "block"
2951
+ : "none";
2952
+ convertContextSubmenus.filterAnd.style.display = hasFilterActions
2953
+ ? "block"
2954
+ : "none";
2955
+ convertContextSubmenus.filterOr.style.display = hasFilterActions
2956
+ ? "block"
2957
+ : "none";
2958
+ convertContextSubmenus.filterNot.style.display = hasFilterActions
2959
+ ? "block"
2960
+ : "none";
2961
+ convertContextSubmenus.filterParentheses.style.display = hasFilterActions
2962
+ ? "block"
2963
+ : "none";
2964
+ convertContextSubmenus.filterClear.style.display = hasFilterActions
2965
+ ? "block"
2966
+ : "none";
2967
+ convertContextSubmenus.notes.style.display = hasNotesActions ? "block" : "none";
2968
+ convertContextSubmenus.keystore.style.display = hasKeystoreActions
2969
+ ? "block"
2970
+ : "none";
2971
+ convertContextSubmenus.keystorePassword.style.display = hasContextTextKeystoreActions
2972
+ ? "block"
2973
+ : "none";
2974
+ convertContextSubmenus.keystoreKey.style.display = hasContextTextKeystoreActions
2975
+ ? "block"
2976
+ : "none";
2977
+ convertContextSubmenus.keystoreCert.style.display = hasContextTextKeystoreActions
2978
+ ? "block"
2979
+ : "none";
2980
+ convertContextSubmenus.keystoreCookie.style.display = hasContextTextKeystoreActions
2981
+ ? "block"
2982
+ : "none";
2983
+ convertContextSubmenus.keystoreUri.style.display = showManualKeystoreUri
2984
+ ? "block"
2985
+ : "none";
2986
+ convertContextSubmenus.export.style.display = hasExportActions
2987
+ ? "block"
2988
+ : "none";
2989
+ convertContextSubmenus.httpFile.style.display = hasHttpBody ? "block" : "none";
2990
+ if (
2991
+ !hasGeneralActions &&
2992
+ !hasDataTypeActions &&
2993
+ !isHexViewTarget &&
2994
+ !hasFilterActions &&
2995
+ !hasCookieActions &&
2996
+ !hasNotesActions &&
2997
+ !hasKeystoreActions &&
2998
+ !hasExportActions &&
2999
+ !hasHttpBody
3000
+ ) {
3001
+ hideConvertContextMenu();
3002
+ return;
3003
+ }
3004
+ convertContextDividerEl.style.display =
3005
+ hasClipboardActions &&
3006
+ (hasDataTypeActions ||
3007
+ isHexViewTarget ||
3008
+ hasFilterActions ||
3009
+ hasExportActions ||
3010
+ hasHttpBody)
3011
+ ? "block"
3012
+ : "none";
3013
+ convertContextSaveDividerEl.style.display =
3014
+ (hasExportActions || hasHttpBody) &&
3015
+ (hasClipboardActions ||
3016
+ hasDataTypeActions ||
3017
+ isHexViewTarget ||
3018
+ hasFilterActions ||
3019
+ hasCookieActions ||
3020
+ hasKeystoreActions)
3021
+ ? "block"
3022
+ : "none";
3023
+
3024
+ convertContextMenuEl.hidden = false;
3025
+ const menuWidth = convertContextMenuEl.offsetWidth;
3026
+ const menuHeight = convertContextMenuEl.offsetHeight;
3027
+ const boundedX = Math.max(8, Math.min(x, window.innerWidth - menuWidth - 8));
3028
+ const boundedY = Math.max(
3029
+ 8,
3030
+ Math.min(y, window.innerHeight - menuHeight - 8),
3031
+ );
3032
+ convertContextMenuEl.style.left = `${boundedX}px`;
3033
+ convertContextMenuEl.style.top = `${boundedY}px`;
3034
+ updateConvertContextSubmenuPositions();
3035
+ }
3036
+
3037
+ function loadContextValueIntoDataTools(format) {
3038
+ if (!activeContextConversionText) return;
3039
+ const inputEl = document.getElementById("data-tools-input");
3040
+ const formatEl = document.getElementById("data-tools-format");
3041
+ inputEl.value = activeContextConversionText;
3042
+ formatEl.value = format;
3043
+ showDataTools();
3044
+ runDataToolsConversion();
3045
+ hideConvertContextMenu();
3046
+ writeLogEntry(`Context conversion loaded format=${format}`);
3047
+ }
3048
+
3049
+ function loadRawPayloadIntoDataToolsFromContextMenu() {
3050
+ const payloadHex = getCurrentRawPayloadHex();
3051
+ hideConvertContextMenu();
3052
+ if (!payloadHex) {
3053
+ statusUpdate("Status: No raw payload available to load");
3054
+ return;
3055
+ }
3056
+ const inputEl = document.getElementById("data-tools-input");
3057
+ const formatEl = document.getElementById("data-tools-format");
3058
+ inputEl.value = payloadHex;
3059
+ formatEl.value = "hex";
3060
+ showDataTools();
3061
+ runDataToolsConversion();
3062
+ writeLogEntry("Context conversion loaded raw payload into Conv tab");
3063
+ }
3064
+
3065
+ function getCursorAsciiContextLoadData(payloadHex, byteIndex) {
3066
+ if (byteIndex < 0 || !payloadHex) return null;
3067
+ const decodedAscii = hexToAscii(payloadHex);
3068
+ let printableSequence = "";
3069
+ for (let i = byteIndex; i < decodedAscii.length; i++) {
3070
+ const charCode = decodedAscii.charCodeAt(i);
3071
+ if (!isPrintable(charCode)) break;
3072
+ printableSequence += decodedAscii[i];
3073
+ }
3074
+ if (printableSequence) {
3075
+ return { value: printableSequence, format: "ascii" };
3076
+ }
3077
+ const hexOffset = byteIndex * 2;
3078
+ const hexPair = payloadHex.slice(hexOffset, hexOffset + 2);
3079
+ if (hexPair.length !== 2 || !/^[0-9A-Fa-f]{2}$/.test(hexPair)) return null;
3080
+ return { value: hexPair.toUpperCase(), format: "hex" };
3081
+ }
3082
+
3083
+ function loadCursorAsciiIntoDataToolsFromContextMenu() {
3084
+ const payloadHex = getCurrentRawPayloadHex();
3085
+ const byteIndex = Number.parseInt(
3086
+ activeContextTarget?.dataset?.byteIndex ?? "-1",
3087
+ 10,
3088
+ );
3089
+ const cursorAsciiLoadData = getCursorAsciiContextLoadData(payloadHex, byteIndex);
3090
+ hideConvertContextMenu();
3091
+ if (!cursorAsciiLoadData) {
3092
+ statusUpdate("Status: No cursor ASCII value available to load");
3093
+ return;
3094
+ }
3095
+ const inputEl = document.getElementById("data-tools-input");
3096
+ const formatEl = document.getElementById("data-tools-format");
3097
+ inputEl.value = cursorAsciiLoadData.value;
3098
+ formatEl.value = cursorAsciiLoadData.format;
3099
+ showDataTools();
3100
+ runDataToolsConversion();
3101
+ writeLogEntry(
3102
+ `Context conversion loaded cursor ASCII into Conv tab format=${cursorAsciiLoadData.format}`,
3103
+ );
3104
+ }
3105
+
3106
+ function getActivePacketCursor() {
3107
+ return Number.isInteger(activePacketCursor) && activePacketCursor >= 0
3108
+ ? activePacketCursor
3109
+ : null;
3110
+ }
3111
+
3112
+ function setActivePacketCursor(nextIndex) {
3113
+ const parsedIndex = Number.parseInt(nextIndex, 10);
3114
+ activePacketCursor =
3115
+ Number.isNaN(parsedIndex) || parsedIndex < 0 ? null : parsedIndex;
3116
+ return activePacketCursor;
3117
+ }
3118
+
3119
+ function getCurrentRawPayloadHex() {
3120
+ const packetCursor = getActivePacketCursor();
3121
+ const payloadHex =
3122
+ packetsForHost?.[packetCursor]?.["Packet Info"]?.["Raw data"]?.[
3123
+ "Payload"
3124
+ ]?.["Hex Encoded"];
3125
+ return typeof payloadHex === "string" ? payloadHex : "";
3126
+ }
3127
+
3128
+ function getCurrentHttpData() {
3129
+ const cursor = getActivePacketCursor();
3130
+ if (cursor === null) return null;
3131
+ const packetInfo = packetsForHost?.[cursor]?.["Packet Info"];
3132
+ if (!packetInfo) return null;
3133
+ const protocol = packetInfo["Protocol"] || "TCP";
3134
+ return packetInfo[protocol]?.["HTTP"] || null;
3135
+ }
3136
+
3137
+ function extractHttpBodyHex(payloadHex) {
3138
+ if (!payloadHex) return "";
3139
+ // Locate the HTTP header/body separator in hex space.
3140
+ // RFC 7230 mandates \r\n\r\n which encodes as "0d0a0d0a".
3141
+ const lower = payloadHex.toLowerCase();
3142
+ const sepIdx = lower.indexOf("0d0a0d0a");
3143
+ if (sepIdx === -1) return "";
3144
+ const bodyStart = sepIdx + 8; // skip past the 4-byte CRLFCRLF separator
3145
+ if (bodyStart >= payloadHex.length) return "";
3146
+ return payloadHex.slice(bodyStart);
3147
+ }
3148
+
3149
+ function getCurrentHttpBodyHex() {
3150
+ return extractHttpBodyHex(getCurrentRawPayloadHex());
3151
+ }
3152
+
3153
+ function getCurrentPacketForExport(packetSet, packetIndex) {
3154
+ if (!Number.isInteger(packetIndex) || packetIndex < 0) {
3155
+ return null;
3156
+ }
3157
+ return packetSet?.[packetIndex] || null;
3158
+ }
3159
+
3160
+ async function copyTextToClipboard(text, label) {
3161
+ if (!text) {
3162
+ statusUpdate(`Status: No ${label.toLowerCase()} available to copy`);
3163
+ return;
3164
+ }
3165
+
3166
+ try {
3167
+ await navigator.clipboard.writeText(text);
3168
+ } catch {
3169
+ const fallbackInput = document.createElement("textarea");
3170
+ fallbackInput.value = text;
3171
+ fallbackInput.style.position = "fixed";
3172
+ fallbackInput.style.left = "-9999px";
3173
+ document.body.appendChild(fallbackInput);
3174
+ fallbackInput.focus();
3175
+ fallbackInput.select();
3176
+ document.execCommand("copy");
3177
+ document.body.removeChild(fallbackInput);
3178
+ }
3179
+
3180
+ statusUpdate(`Status: Copied ${label} to clipboard`);
3181
+ writeLogEntry(`Copied ${label} length=${text.length}`);
3182
+ }
3183
+
3184
+ function getAsciiPreviewForHexOffset(payloadHex, byteIndex) {
3185
+ if (byteIndex < 0) return "";
3186
+ const decodedAscii = hexToAscii(payloadHex);
3187
+ let printableSequence = "";
3188
+ for (let i = byteIndex; i < decodedAscii.length; i++) {
3189
+ const charCode = decodedAscii.charCodeAt(i);
3190
+ if (!isPrintable(charCode)) break;
3191
+ printableSequence += decodedAscii[i];
3192
+ }
3193
+ if (printableSequence.length > 0) return printableSequence;
3194
+ const fallbackCode = decodedAscii.charCodeAt(byteIndex);
3195
+ if (Number.isNaN(fallbackCode)) return "";
3196
+ return isPrintable(fallbackCode) ? decodedAscii[byteIndex] : ".";
3197
+ }
3198
+
3199
+ async function copyHexFromContext() {
3200
+ const payloadHex = getCurrentRawPayloadHex();
3201
+ const hexValue = activeContextTarget?.classList?.contains("griditem")
3202
+ ? activeContextTarget.textContent.trim()
3203
+ : payloadHex;
3204
+ await copyTextToClipboard(hexValue, "Hex");
3205
+ hideConvertContextMenu();
3206
+ }
3207
+
3208
+ async function copyAsciiFromContext() {
3209
+ const payloadHex = getCurrentRawPayloadHex();
3210
+ const byteIndex = Number.parseInt(
3211
+ activeContextTarget?.dataset?.byteIndex ?? "-1",
3212
+ 10,
3213
+ );
3214
+ const fullPayloadAscii = payloadHex
3215
+ ? bytesToPrintableAscii(parseDataToolsInput("hex", payloadHex))
3216
+ : "";
3217
+ const asciiValue = activeContextTarget?.classList?.contains("griditem")
3218
+ ? getAsciiPreviewForHexOffset(payloadHex, byteIndex)
3219
+ : fullPayloadAscii;
3220
+ await copyTextToClipboard(asciiValue, "ASCII");
3221
+ hideConvertContextMenu();
3222
+ }
3223
+
3224
+ async function copyRawPayloadFromContext() {
3225
+ await copyTextToClipboard(getCurrentRawPayloadHex(), "Raw payload");
3226
+ hideConvertContextMenu();
3227
+ }
3228
+
3229
+ function copySelectedTextFromContextMenu() {
3230
+ const selectedText = getTrimmedSelectionText();
3231
+ hideConvertContextMenu();
3232
+ if (!selectedText) {
3233
+ statusUpdate("Status: No text selected to copy");
3234
+ return;
3235
+ }
3236
+ navigator.clipboard
3237
+ .writeText(selectedText)
3238
+ .then(() => {
3239
+ statusUpdate("Status: Copied selected text to clipboard");
3240
+ writeLogEntry(`Copied selected text length=${selectedText.length}`);
3241
+ })
3242
+ .catch((error) => {
3243
+ console.error("Copy failed:", error);
3244
+ statusUpdate("Status: Copy failed – clipboard access denied");
3245
+ });
3246
+ }
3247
+
3248
+ async function copyCookieJarFromContextMenu() {
3249
+ const cookieJarText = activeContextCookieJarText;
3250
+ hideConvertContextMenu();
3251
+ await copyTextToClipboard(cookieJarText, "Cookie Jar");
3252
+ }
3253
+
3254
+ function pasteTextFromContextMenu() {
3255
+ const pasteTarget = activeContextPasteTarget;
3256
+ hideConvertContextMenu();
3257
+ if (!pasteTarget) {
3258
+ statusUpdate("Status: Paste unavailable for this target");
3259
+ return;
3260
+ }
3261
+ navigator.clipboard
3262
+ .readText()
3263
+ .then((text) => {
3264
+ if (
3265
+ pasteTarget.tagName === "INPUT" ||
3266
+ pasteTarget.tagName === "TEXTAREA"
3267
+ ) {
3268
+ const hasSelectionRange =
3269
+ typeof pasteTarget.selectionStart === "number" &&
3270
+ typeof pasteTarget.selectionEnd === "number";
3271
+ const start = hasSelectionRange
3272
+ ? pasteTarget.selectionStart
3273
+ : pasteTarget.value.length;
3274
+ const end = hasSelectionRange
3275
+ ? pasteTarget.selectionEnd
3276
+ : pasteTarget.value.length;
3277
+ const current = pasteTarget.value;
3278
+ pasteTarget.value =
3279
+ current.substring(0, start) + text + current.substring(end);
3280
+ if (hasSelectionRange) {
3281
+ pasteTarget.selectionStart = pasteTarget.selectionEnd =
3282
+ start + text.length;
3283
+ }
3284
+ pasteTarget.dispatchEvent(new Event("input", { bubbles: true }));
3285
+ return;
3286
+ }
3287
+
3288
+ if (pasteTarget.isContentEditable) {
3289
+ pasteTarget.focus();
3290
+ const selection = window.getSelection();
3291
+ if (selection && selection.rangeCount > 0) {
3292
+ const range = selection.getRangeAt(0);
3293
+ range.deleteContents();
3294
+ const textNode = document.createTextNode(text);
3295
+ range.insertNode(textNode);
3296
+ range.setStartAfter(textNode);
3297
+ range.setEndAfter(textNode);
3298
+ selection.removeAllRanges();
3299
+ selection.addRange(range);
3300
+ } else {
3301
+ pasteTarget.textContent = (pasteTarget.textContent || "") + text;
3302
+ }
3303
+ pasteTarget.dispatchEvent(new Event("input", { bubbles: true }));
3304
+ }
3305
+ })
3306
+ .catch((error) => {
3307
+ console.error("Paste failed:", error);
3308
+ statusUpdate("Status: Paste failed – clipboard access denied");
3309
+ });
3310
+ }
3311
+
3312
+ function saveJsonFromContextMenu() {
3313
+ hideConvertContextMenu();
3314
+ void persistSessionToDisk("context-menu");
3315
+ }
3316
+
3317
+ function exportCurrentPacketFromContextMenu() {
3318
+ hideConvertContextMenu();
3319
+ const currentPacket = getCurrentPacketForExport(
3320
+ packetsForHost,
3321
+ getActivePacketCursor(),
3322
+ );
3323
+ if (!currentPacket) {
3324
+ statusUpdate("Status: No packet selected to export");
3325
+ return;
3326
+ }
3327
+ window.saveapi.savePacket(currentPacket).then((result) => {
3328
+ if (result.canceled) {
3329
+ statusUpdate("Status: Export cancelled");
3330
+ } else if (result.success) {
3331
+ statusUpdate("Status: Packet exported successfully");
3332
+ writeLogEntry("Context menu packet export completed");
3333
+ } else {
3334
+ const errorMessage =
3335
+ result && typeof result === "object" && "error" in result
3336
+ ? result.error
3337
+ : "unknown";
3338
+ doError("Packet export failed");
3339
+ logErrorEntry("export-packet", errorMessage || "unknown");
3340
+ statusUpdate(
3341
+ "Status: Packet export failed – " + (errorMessage || "unknown error"),
3342
+ );
3343
+ console.error("Packet export failed:", errorMessage);
3344
+ }
3345
+ });
3346
+ }
3347
+
3348
+ function exportCurrentPayloadFromContextMenu() {
3349
+ hideConvertContextMenu();
3350
+ const payloadHex = getCurrentRawPayloadHex();
3351
+ if (!payloadHex) {
3352
+ statusUpdate("Status: No payload available to export");
3353
+ return;
3354
+ }
3355
+ window.saveapi.savePayload(payloadHex).then((result) => {
3356
+ if (result.canceled) {
3357
+ statusUpdate("Status: Export cancelled");
3358
+ } else if (result.success) {
3359
+ statusUpdate("Status: Payload exported successfully");
3360
+ writeLogEntry("Context menu payload export completed");
3361
+ } else {
3362
+ const errorMessage =
3363
+ result && typeof result === "object" && "error" in result
3364
+ ? result.error
3365
+ : "unknown";
3366
+ doError("Payload export failed");
3367
+ logErrorEntry("export-payload", errorMessage || "unknown");
3368
+ statusUpdate(
3369
+ "Status: Payload export failed – " + (errorMessage || "unknown error"),
3370
+ );
3371
+ console.error("Payload export failed:", errorMessage);
3372
+ }
3373
+ });
3374
+ }
3375
+
3376
+ function saveCookieJarFromContextMenu() {
3377
+ const cookieJarText = activeContextCookieJarText;
3378
+ hideConvertContextMenu();
3379
+ if (!cookieJarText) {
3380
+ statusUpdate("Status: No cookie jar available to save");
3381
+ return;
3382
+ }
3383
+ window.saveapi.saveCookieJar(cookieJarText).then((result) => {
3384
+ if (result.canceled) {
3385
+ statusUpdate("Status: Save cancelled");
3386
+ } else if (result.success) {
3387
+ statusUpdate("Status: Cookie jar saved successfully");
3388
+ writeLogEntry("Context menu cookie jar save completed");
3389
+ } else {
3390
+ const errorMessage =
3391
+ result && typeof result === "object" && "error" in result
3392
+ ? result.error
3393
+ : "unknown";
3394
+ doError("Cookie jar save failed");
3395
+ logErrorEntry("save-cookie-jar", errorMessage || "unknown");
3396
+ statusUpdate(
3397
+ "Status: Cookie jar save failed – " + (errorMessage || "unknown error"),
3398
+ );
3399
+ console.error("Cookie jar save failed:", errorMessage);
3400
+ }
3401
+ });
3402
+ }
3403
+
3404
+ function getHttpContentTypeForCurrentPacket() {
3405
+ const httpData = getCurrentHttpData();
3406
+ return (httpData && httpData["Content-Type"]) || "application/octet-stream";
3407
+ }
3408
+
3409
+ function saveHttpBodyFromContextMenu() {
3410
+ hideConvertContextMenu();
3411
+ const bodyHex = getCurrentHttpBodyHex();
3412
+ if (!bodyHex) {
3413
+ statusUpdate("Status: No HTTP body available to save");
3414
+ return;
3415
+ }
3416
+ const contentType = getHttpContentTypeForCurrentPacket();
3417
+ window.saveapi.saveHttpBody(bodyHex, contentType).then((result) => {
3418
+ if (result.canceled) {
3419
+ statusUpdate("Status: Save cancelled");
3420
+ } else if (result.success) {
3421
+ statusUpdate("Status: HTTP body saved successfully");
3422
+ writeLogEntry("Context menu HTTP body save completed");
3423
+ } else {
3424
+ const errorMessage =
3425
+ result && typeof result === "object" && "error" in result
3426
+ ? result.error
3427
+ : "unknown";
3428
+ doError("HTTP body save failed");
3429
+ logErrorEntry("http-body-save", errorMessage || "unknown");
3430
+ statusUpdate(
3431
+ "Status: HTTP body save failed – " + (errorMessage || "unknown error"),
3432
+ );
3433
+ console.error("HTTP body save failed:", errorMessage);
3434
+ }
3435
+ });
3436
+ }
3437
+
3438
+ function loadHttpBodyIntoConvTabFromContextMenu() {
3439
+ const bodyHex = getCurrentHttpBodyHex();
3440
+ hideConvertContextMenu();
3441
+ if (!bodyHex) {
3442
+ statusUpdate("Status: No HTTP body available to load");
3443
+ return;
3444
+ }
3445
+ const inputEl = document.getElementById("data-tools-input");
3446
+ const formatEl = document.getElementById("data-tools-format");
3447
+ inputEl.value = bodyHex;
3448
+ formatEl.value = "hex";
3449
+ showDataTools();
3450
+ runDataToolsConversion();
3451
+ writeLogEntry("Context menu loaded HTTP body into Conv tab");
3452
+ }
3453
+
3454
+ function previewHttpBodyInBrowserFromContextMenu() {
3455
+ hideConvertContextMenu();
3456
+ const bodyHex = getCurrentHttpBodyHex();
3457
+ if (!bodyHex) {
3458
+ statusUpdate("Status: No HTTP body available to preview");
3459
+ return;
3460
+ }
3461
+ const contentType = getHttpContentTypeForCurrentPacket();
3462
+ window.previewapi.previewHttpBody(bodyHex, contentType).then((result) => {
3463
+ if (result.success) {
3464
+ statusUpdate("Status: HTTP body opened in browser");
3465
+ writeLogEntry("Context menu HTTP body browser preview launched");
3466
+ } else {
3467
+ const errorMessage =
3468
+ result && typeof result === "object" && "error" in result
3469
+ ? result.error
3470
+ : "unknown";
3471
+ doError("HTTP body preview failed");
3472
+ logErrorEntry("http-body-preview", errorMessage || "unknown");
3473
+ statusUpdate(
3474
+ "Status: HTTP body preview failed – " + (errorMessage || "unknown error"),
3475
+ );
3476
+ console.error("HTTP body preview failed:", errorMessage);
3477
+ }
3478
+ });
3479
+ }
3480
+
3481
+ function appendFilterQueryFromContextMenu(
3482
+ type,
3483
+ joinOperator = "&&",
3484
+ negate = false,
3485
+ ) {
3486
+ const query = activeContextFilterQueries[type];
3487
+ hideConvertContextMenu();
3488
+ if (!query) {
3489
+ statusUpdate("Status: No matching filter value found for this selection");
3490
+ return;
3491
+ }
3492
+ if (joinOperator !== "&&" && joinOperator !== "||") {
3493
+ statusUpdate("Status: Could not add filter query — please try again");
3494
+ return;
3495
+ }
3496
+ const queryToInsert = negate ? `!(${query})` : query;
3497
+ const existingQuery = filterInputEl.value.trim();
3498
+ const wrappedQuery = negate
3499
+ ? queryToInsert
3500
+ : queryToInsert.includes("||") || queryToInsert.includes("&&")
3501
+ ? `(${queryToInsert})`
3502
+ : queryToInsert;
3503
+ if (!existingQuery) {
3504
+ filterInputEl.value = queryToInsert;
3505
+ } else if (/(?:\|\||&&)\s*$/.test(existingQuery)) {
3506
+ filterInputEl.value = `${existingQuery} ${wrappedQuery}`;
3507
+ } else {
3508
+ filterInputEl.value = `${existingQuery} ${joinOperator} ${wrappedQuery}`;
3509
+ }
3510
+ syncFilterHighlight();
3511
+ filterInputEl.focus();
3512
+ statusUpdate("Status: Filter query populated — press Enter to apply");
3513
+ writeLogEntry(
3514
+ `Context menu filter populated type=${type} negated=${negate} query="${filterInputEl.value}"`,
3515
+ );
3516
+ }
3517
+
3518
+ function clearAndFilterQueryFromContextMenu(type) {
3519
+ const query = activeContextFilterQueries[type];
3520
+ hideConvertContextMenu();
3521
+ if (!query) {
3522
+ statusUpdate("Status: No matching filter value found for this selection");
3523
+ return;
3524
+ }
3525
+ filterInputEl.value = query;
3526
+ syncFilterHighlight();
3527
+ filterInputEl.focus();
3528
+ statusUpdate("Status: Filter query populated — press Enter to apply");
3529
+ writeLogEntry(
3530
+ `Context menu filter cleared and populated type=${type} query="${filterInputEl.value}"`,
3531
+ );
3532
+ }
3533
+
3534
+ function appendParenthesisTokenFromContextMenu(token) {
3535
+ hideConvertContextMenu();
3536
+ if (token !== "(" && token !== ")") {
3537
+ statusUpdate("Status: Could not append parenthesis — please try again");
3538
+ return;
3539
+ }
3540
+ filterInputEl.value = `${filterInputEl.value}${token}`;
3541
+ syncFilterHighlight();
3542
+ filterInputEl.focus();
3543
+ statusUpdate("Status: Filter query updated — press Enter to apply");
3544
+ writeLogEntry(
3545
+ `Context menu filter appended token="${token}" query="${filterInputEl.value}"`,
3546
+ );
3547
+ }
3548
+
3549
+ function wrapCurrentFilterWithParenthesesFromContextMenu() {
3550
+ hideConvertContextMenu();
3551
+ const existingQuery = filterInputEl.value.trim();
3552
+ if (!existingQuery) {
3553
+ statusUpdate("Status: No filter query available to wrap");
3554
+ return;
3555
+ }
3556
+ filterInputEl.value = `(${existingQuery})`;
3557
+ syncFilterHighlight();
3558
+ filterInputEl.focus();
3559
+ statusUpdate("Status: Filter query updated — press Enter to apply");
3560
+ writeLogEntry(`Context menu filter wrapped query="${filterInputEl.value}"`);
3561
+ }
3562
+
3563
+ initConvPanel({
3564
+ writeLogEntry,
3565
+ statusUpdate,
3566
+ setActiveMainTab: (tab) => {
3567
+ activeMainTab = tab;
3568
+ },
3569
+ });
3570
+ initializeNotesPanel();
3571
+ document.getElementById("close-btn").addEventListener("click", () => {
3572
+ void requestApplicationClose();
3573
+ });
3574
+
3575
+ // Show capture stats when stats button is clicked
3576
+ document.getElementById("stats-btn").addEventListener("click", function () {
3577
+ if (!isFileLoaded) {
3578
+ doError("Please upload a JSON file before accessing packet statistics.");
3579
+ return;
3580
+ }
3581
+ showStats();
3582
+ });
3583
+
3584
+ // Show data conversion tools when data tools button is clicked
3585
+ document
3586
+ .getElementById("data-tools-btn")
3587
+ .addEventListener("click", function () {
3588
+ showDataTools();
3589
+ });
3590
+
3591
+ document.getElementById("crypt-btn").addEventListener("click", function () {
3592
+ if (!isFileLoaded) {
3593
+ doError("Please upload a JSON file before accessing crypt tools.");
3594
+ return;
3595
+ }
3596
+ showCryptWorkspace();
3597
+ });
3598
+
3599
+ document.getElementById("keystore-btn").addEventListener("click", async function () {
3600
+ if (!isFileLoaded) {
3601
+ doError("Please upload a JSON file before accessing the keystore.");
3602
+ return;
3603
+ }
3604
+ const unlocked = await keystorePanel.unlockPersistentKeystoreAndLoad();
3605
+ if (!unlocked) return;
3606
+ keystorePanel.showKeystoreWorkspace();
3607
+ });
3608
+ document
3609
+ .getElementById("crypt-keystore-unlock-confirm-btn")
3610
+ .addEventListener("click", keystorePanel.submitKeystoreUnlockDialog);
3611
+ document
3612
+ .getElementById("crypt-keystore-unlock-cancel-btn")
3613
+ .addEventListener("click", () => keystorePanel.resolveKeystoreUnlockPassword(null));
3614
+ document
3615
+ .getElementById("crypt-keystore-unlock-password")
3616
+ .addEventListener("keydown", (event) => {
3617
+ if (event.key !== "Enter") return;
3618
+ keystorePanel.submitKeystoreUnlockDialog();
3619
+ });
3620
+ document
3621
+ .getElementById("crypt-keystore-unlock-password-confirm")
3622
+ .addEventListener("keydown", (event) => {
3623
+ if (event.key !== "Enter") return;
3624
+ keystorePanel.submitKeystoreUnlockDialog();
3625
+ });
3626
+ document
3627
+ .getElementById("crypt-keystore-manual-uri-confirm-btn")
3628
+ .addEventListener("click", keystorePanel.submitManualUriFromContextMenuDialog);
3629
+ document
3630
+ .getElementById("crypt-keystore-manual-uri-cancel-btn")
3631
+ .addEventListener("click", () =>
3632
+ keystorePanel.resolveManualUriFromContextMenuDialog(null),
3633
+ );
3634
+ document
3635
+ .getElementById("crypt-keystore-manual-uri-input")
3636
+ .addEventListener("keydown", (event) => {
3637
+ if (event.key !== "Enter") return;
3638
+ keystorePanel.submitManualUriFromContextMenuDialog();
3639
+ });
3640
+
3641
+ // Show packet list when list button is clicked
3642
+ document.getElementById("list-btn").addEventListener("click", function () {
3643
+ if (!isFileLoaded) {
3644
+ doError("Please upload a JSON file before accessing the packet list.");
3645
+ return;
3646
+ }
3647
+ showPacketList();
3648
+ });
3649
+ document.getElementById("notes-btn").addEventListener("click", function () {
3650
+ if (!isFileLoaded) {
3651
+ doError("Please upload a JSON file before accessing notes.");
3652
+ return;
3653
+ }
3654
+ showNotesWorkspace();
3655
+ });
3656
+
3657
+ document
3658
+ .getElementById("conv-subtab-conversions")
3659
+ .addEventListener("click", () => setConvSubtab(CONV_CONVERSIONS_SUBTAB));
3660
+ document
3661
+ .getElementById("conv-subtab-hashes")
3662
+ .addEventListener("click", () => setConvSubtab(CONV_HASHES_SUBTAB));
3663
+ document
3664
+ .getElementById("conv-subtab-decodes")
3665
+ .addEventListener("click", () => setConvSubtab(CONV_DECODES_SUBTAB));
3666
+
3667
+ document
3668
+ .getElementById("crypt-subtab-ssl")
3669
+ .addEventListener("click", () => setCryptSubtab(CRYPT_SSL_SUBTAB));
3670
+ document
3671
+ .getElementById("crypt-subtab-pgp")
3672
+ .addEventListener("click", () => setCryptSubtab(CRYPT_PGP_SUBTAB));
3673
+ document
3674
+ .getElementById("crypt-subtab-openssh")
3675
+ .addEventListener("click", () => setCryptSubtab(CRYPT_OPENSSH_SUBTAB));
3676
+ document.getElementById("crypt-refresh-btn").addEventListener("click", () => {
3677
+ refreshCryptEncounteredEntries();
3678
+ });
3679
+ document
3680
+ .getElementById("crypt-encountered-list")
3681
+ .addEventListener("change", function () {
3682
+ const selectedIndex = Number(this.value);
3683
+ cryptPanel.selectEncounteredEntry(selectedIndex);
3684
+ });
3685
+ document
3686
+ .getElementById("crypt-apply-filter-btn")
3687
+ .addEventListener("click", applyCryptFilterForActiveEntry);
3688
+ document
3689
+ .getElementById("crypt-load-encountered-cert-btn")
3690
+ .addEventListener("click", loadEncounteredCertificateIntoCrypt);
3691
+
3692
+ document
3693
+ .getElementById("crypt-load-cert-file-btn")
3694
+ .addEventListener("click", () =>
3695
+ document.getElementById("crypt-cert-file-input").click(),
3696
+ );
3697
+ document
3698
+ .getElementById("crypt-cert-file-input")
3699
+ .addEventListener("change", function () {
3700
+ readCryptTextFile(this, applyCryptCertificateText);
3701
+ this.value = "";
3702
+ });
3703
+ document
3704
+ .getElementById("crypt-use-cert-input-btn")
3705
+ .addEventListener("click", () =>
3706
+ applyCryptCertificateText(
3707
+ document.getElementById("crypt-cert-input").value,
3708
+ "pasted text",
3709
+ ),
3710
+ );
3711
+ document.getElementById("crypt-clear-cert-btn").addEventListener("click", () => {
3712
+ applyCryptCertificateText("", "cleared");
3713
+ });
3714
+
3715
+ document
3716
+ .getElementById("crypt-load-key-file-btn")
3717
+ .addEventListener("click", () =>
3718
+ document.getElementById("crypt-key-file-input").click(),
3719
+ );
3720
+ document
3721
+ .getElementById("crypt-key-file-input")
3722
+ .addEventListener("change", function () {
3723
+ readCryptTextFile(this, applyCryptPrivateKeyText);
3724
+ this.value = "";
3725
+ });
3726
+ document
3727
+ .getElementById("crypt-use-key-input-btn")
3728
+ .addEventListener("click", () =>
3729
+ applyCryptPrivateKeyText(
3730
+ document.getElementById("crypt-key-input").value,
3731
+ "pasted text",
3732
+ ),
3733
+ );
3734
+ document.getElementById("crypt-clear-key-btn").addEventListener("click", () => {
3735
+ applyCryptPrivateKeyText("", "cleared");
3736
+ });
3737
+ document
3738
+ .getElementById("crypt-decrypt-entry-btn")
3739
+ .addEventListener("click", decryptActiveEntryWithLoadedKey);
3740
+ document
3741
+ .getElementById("crypt-send-decrypted-conv-btn")
3742
+ .addEventListener("click", sendDecryptedPayloadToConvTab);
3743
+ document
3744
+ .getElementById("crypt-clear-decrypted-btn")
3745
+ .addEventListener("click", clearCryptDecryptionOutput);
3746
+
3747
+ document
3748
+ .getElementById("crypt-save-cert-keystore-btn")
3749
+ .addEventListener("click", () => {
3750
+ void keystorePanel.addCryptKeystoreEntry({
3751
+ type: "certificate",
3752
+ label: document.getElementById("crypt-keystore-label").value,
3753
+ source: "crypt-certificate-loader",
3754
+ content: document.getElementById("crypt-cert-input").value,
3755
+ summary: getFirstLineOrFallback(
3756
+ "crypt-cert-preview",
3757
+ "Certificate from loader",
3758
+ ),
3759
+ });
3760
+ });
3761
+ document
3762
+ .getElementById("crypt-save-key-keystore-btn")
3763
+ .addEventListener("click", () => {
3764
+ void keystorePanel.addCryptKeystoreEntry({
3765
+ type: "private-key",
3766
+ label: document.getElementById("crypt-keystore-label").value,
3767
+ source: "crypt-private-key-loader",
3768
+ content: document.getElementById("crypt-key-input").value,
3769
+ summary: getFirstLineOrFallback(
3770
+ "crypt-key-preview",
3771
+ "Private key from loader",
3772
+ ),
3773
+ });
3774
+ });
3775
+ document
3776
+ .getElementById("crypt-save-secret-keystore-btn")
3777
+ .addEventListener("click", () => {
3778
+ void keystorePanel.addCryptKeystoreEntry({
3779
+ type: "secret",
3780
+ label: document.getElementById("crypt-keystore-label").value,
3781
+ source: "crypt-secret-input",
3782
+ content: document.getElementById("crypt-credential-input").value,
3783
+ summary: "Manual secret/credential entry",
3784
+ });
3785
+ });
3786
+ document
3787
+ .getElementById("crypt-keystore-mode")
3788
+ .addEventListener("change", function () {
3789
+ const selectedMode = String(this.value || CRYPT_KEYSTORE_MODE_SESSION);
3790
+ keystorePanel.setActiveMode(
3791
+ selectedMode === CRYPT_KEYSTORE_MODE_PERSISTENT
3792
+ ? CRYPT_KEYSTORE_MODE_PERSISTENT
3793
+ : CRYPT_KEYSTORE_MODE_SESSION,
3794
+ );
3795
+ });
3796
+ document
3797
+ .getElementById("crypt-keystore-list")
3798
+ .addEventListener("change", function () {
3799
+ const activeEntries = keystorePanel.getActiveCryptKeystoreEntries();
3800
+ const selectedIndex = Number(this.value);
3801
+ if (!Number.isFinite(selectedIndex) || !activeEntries[selectedIndex]) {
3802
+ return;
3803
+ }
3804
+ keystorePanel.renderCryptKeystoreDetails(activeEntries[selectedIndex]);
3805
+ });
3806
+ document
3807
+ .getElementById("crypt-load-keystore-entry-btn")
3808
+ .addEventListener("click", () => {
3809
+ void keystorePanel.loadSelectedCryptKeystoreEntry();
3810
+ });
3811
+ document
3812
+ .getElementById("crypt-send-to-persistent-btn")
3813
+ .addEventListener("click", () => {
3814
+ void keystorePanel.sendSelectedSessionEntryToPersistent();
3815
+ });
3816
+ document
3817
+ .getElementById("crypt-delete-keystore-entry-btn")
3818
+ .addEventListener("click", () => {
3819
+ void keystorePanel.deleteSelectedCryptKeystoreEntry();
3820
+ });
3821
+ document
3822
+ .getElementById("crypt-open-link-btn")
3823
+ .addEventListener("click", () => {
3824
+ void keystorePanel.openSelectedKeystoreLinkInBrowser();
3825
+ });
3826
+
3827
+ document
3828
+ .getElementById("data-tools-convert-btn")
3829
+ .addEventListener("click", runDataToolsConversion);
3830
+ document
3831
+ .getElementById("data-tools-hash-input-reading")
3832
+ .addEventListener("input", runDataToolsHashesFromInput);
3833
+ document
3834
+ .getElementById("data-tools-clear-btn")
3835
+ .addEventListener("click", () => {
3836
+ document.getElementById("data-tools-input").value = "";
3837
+ document.getElementById("data-tools-error").textContent = "";
3838
+ resetDataToolsOutputs();
3839
+ });
3840
+ document
3841
+ .getElementById("data-tools-proto-select")
3842
+ .addEventListener("change", () => {
3843
+ const inputEl = document.getElementById("data-tools-input");
3844
+ const formatEl = document.getElementById("data-tools-format");
3845
+ if (!inputEl.value.trim()) return;
3846
+ try {
3847
+ const bytes = parseDataToolsInput(formatEl.value, inputEl.value);
3848
+ runProtoDecoder(bytes);
3849
+ } catch {
3850
+ // ignore parse errors; the error will have been shown on convert
3851
+ }
3852
+ });
3853
+ convertContextButtons.hex.addEventListener("click", () =>
3854
+ loadContextValueIntoDataTools("hex"),
3855
+ );
3856
+ convertContextButtons.binary.addEventListener("click", () =>
3857
+ loadContextValueIntoDataTools("binary"),
3858
+ );
3859
+ convertContextButtons.base64.addEventListener("click", () =>
3860
+ loadContextValueIntoDataTools("base64"),
3861
+ );
3862
+ convertContextButtons.decimal.addEventListener("click", () =>
3863
+ loadContextValueIntoDataTools("decimal"),
3864
+ );
3865
+ convertContextButtons.ascii.addEventListener("click", () =>
3866
+ loadContextValueIntoDataTools("ascii"),
3867
+ );
3868
+ convertContextButtons.loadPayload.addEventListener("click", () => {
3869
+ loadRawPayloadIntoDataToolsFromContextMenu();
3870
+ });
3871
+ convertContextButtons.loadCursorAscii.addEventListener("click", () => {
3872
+ loadCursorAsciiIntoDataToolsFromContextMenu();
3873
+ });
3874
+ convertContextButtons.copyHex.addEventListener("click", () => {
3875
+ copyHexFromContext();
3876
+ });
3877
+ convertContextButtons.copyAscii.addEventListener("click", () => {
3878
+ copyAsciiFromContext();
3879
+ });
3880
+ convertContextButtons.copyRaw.addEventListener("click", () => {
3881
+ copyRawPayloadFromContext();
3882
+ });
3883
+ convertContextButtons.filterIp.addEventListener("click", () => {
3884
+ appendFilterQueryFromContextMenu("ip", "&&");
3885
+ });
3886
+ convertContextButtons.filterPort.addEventListener("click", () => {
3887
+ appendFilterQueryFromContextMenu("port", "&&");
3888
+ });
3889
+ convertContextButtons.filterMac.addEventListener("click", () => {
3890
+ appendFilterQueryFromContextMenu("mac", "&&");
3891
+ });
3892
+ convertContextButtons.filterProtocol.addEventListener("click", () => {
3893
+ appendFilterQueryFromContextMenu("protocol", "&&");
3894
+ });
3895
+ convertContextButtons.filterMime.addEventListener("click", () => {
3896
+ appendFilterQueryFromContextMenu("mime", "&&");
3897
+ });
3898
+ convertContextButtons.filterOrIp.addEventListener("click", () => {
3899
+ appendFilterQueryFromContextMenu("ip", "||");
3900
+ });
3901
+ convertContextButtons.filterOrPort.addEventListener("click", () => {
3902
+ appendFilterQueryFromContextMenu("port", "||");
3903
+ });
3904
+ convertContextButtons.filterOrMac.addEventListener("click", () => {
3905
+ appendFilterQueryFromContextMenu("mac", "||");
3906
+ });
3907
+ convertContextButtons.filterOrProtocol.addEventListener("click", () => {
3908
+ appendFilterQueryFromContextMenu("protocol", "||");
3909
+ });
3910
+ convertContextButtons.filterOrMime.addEventListener("click", () => {
3911
+ appendFilterQueryFromContextMenu("mime", "||");
3912
+ });
3913
+ convertContextButtons.filterNotIp.addEventListener("click", () => {
3914
+ appendFilterQueryFromContextMenu("ip", "&&", true);
3915
+ });
3916
+ convertContextButtons.filterNotPort.addEventListener("click", () => {
3917
+ appendFilterQueryFromContextMenu("port", "&&", true);
3918
+ });
3919
+ convertContextButtons.filterNotMac.addEventListener("click", () => {
3920
+ appendFilterQueryFromContextMenu("mac", "&&", true);
3921
+ });
3922
+ convertContextButtons.filterNotProtocol.addEventListener("click", () => {
3923
+ appendFilterQueryFromContextMenu("protocol", "&&", true);
3924
+ });
3925
+ convertContextButtons.filterNotMime.addEventListener("click", () => {
3926
+ appendFilterQueryFromContextMenu("mime", "&&", true);
3927
+ });
3928
+ convertContextButtons.filterParenOpen.addEventListener("click", () => {
3929
+ appendParenthesisTokenFromContextMenu("(");
3930
+ });
3931
+ convertContextButtons.filterParenClose.addEventListener("click", () => {
3932
+ appendParenthesisTokenFromContextMenu(")");
3933
+ });
3934
+ convertContextButtons.filterParenWrap.addEventListener("click", () => {
3935
+ wrapCurrentFilterWithParenthesesFromContextMenu();
3936
+ });
3937
+ convertContextButtons.filterClearIp.addEventListener("click", () => {
3938
+ clearAndFilterQueryFromContextMenu("ip");
3939
+ });
3940
+ convertContextButtons.filterClearPort.addEventListener("click", () => {
3941
+ clearAndFilterQueryFromContextMenu("port");
3942
+ });
3943
+ convertContextButtons.filterClearMac.addEventListener("click", () => {
3944
+ clearAndFilterQueryFromContextMenu("mac");
3945
+ });
3946
+ convertContextButtons.filterClearProtocol.addEventListener("click", () => {
3947
+ clearAndFilterQueryFromContextMenu("protocol");
3948
+ });
3949
+ convertContextButtons.filterClearMime.addEventListener("click", () => {
3950
+ clearAndFilterQueryFromContextMenu("mime");
3951
+ });
3952
+ convertContextButtons.copy.addEventListener(
3953
+ "click",
3954
+ copySelectedTextFromContextMenu,
3955
+ );
3956
+ convertContextButtons.copyCookieJar.addEventListener(
3957
+ "click",
3958
+ copyCookieJarFromContextMenu,
3959
+ );
3960
+ convertContextButtons.paste.addEventListener("click", pasteTextFromContextMenu);
3961
+ convertContextButtons.notesSendData.addEventListener("click", () => {
3962
+ const selectedText = getTrimmedSelectionText();
3963
+ sendTextToNotesFromContextMenu(
3964
+ selectedText || activeContextConversionText,
3965
+ "context-data",
3966
+ );
3967
+ });
3968
+ convertContextButtons.notesSendConvOutput.addEventListener("click", () => {
3969
+ sendTextToNotesFromContextMenu(
3970
+ buildConvConvertedOutputNoteText(),
3971
+ "context-conv-output",
3972
+ );
3973
+ });
3974
+ convertContextButtons.notesSendConvHashes.addEventListener("click", () => {
3975
+ sendTextToNotesFromContextMenu(
3976
+ buildConvHashesNoteText(),
3977
+ "context-conv-hashes",
3978
+ );
3979
+ });
3980
+ convertContextButtons.keystorePasswordSession.addEventListener("click", () => {
3981
+ keystorePanel.addToKeystoreFromContextMenu("password", CRYPT_KEYSTORE_MODE_SESSION);
3982
+ });
3983
+ convertContextButtons.keystorePasswordPersistent.addEventListener("click", () => {
3984
+ keystorePanel.addToKeystoreFromContextMenu("password", CRYPT_KEYSTORE_MODE_PERSISTENT);
3985
+ });
3986
+ convertContextButtons.keystoreKeySession.addEventListener("click", () => {
3987
+ keystorePanel.addToKeystoreFromContextMenu("key", CRYPT_KEYSTORE_MODE_SESSION);
3988
+ });
3989
+ convertContextButtons.keystoreKeyPersistent.addEventListener("click", () => {
3990
+ keystorePanel.addToKeystoreFromContextMenu("key", CRYPT_KEYSTORE_MODE_PERSISTENT);
3991
+ });
3992
+ convertContextButtons.keystoreCertSession.addEventListener("click", () => {
3993
+ keystorePanel.addToKeystoreFromContextMenu("cert", CRYPT_KEYSTORE_MODE_SESSION);
3994
+ });
3995
+ convertContextButtons.keystoreCertPersistent.addEventListener("click", () => {
3996
+ keystorePanel.addToKeystoreFromContextMenu("cert", CRYPT_KEYSTORE_MODE_PERSISTENT);
3997
+ });
3998
+ convertContextButtons.keystoreCookieSession.addEventListener("click", () => {
3999
+ keystorePanel.addToKeystoreFromContextMenu("cookie", CRYPT_KEYSTORE_MODE_SESSION);
4000
+ });
4001
+ convertContextButtons.keystoreCookiePersistent.addEventListener("click", () => {
4002
+ keystorePanel.addToKeystoreFromContextMenu("cookie", CRYPT_KEYSTORE_MODE_PERSISTENT);
4003
+ });
4004
+ convertContextButtons.keystoreUriSession.addEventListener("click", () => {
4005
+ void keystorePanel.addManualUriToKeystoreFromContextMenu(
4006
+ CRYPT_KEYSTORE_MODE_SESSION,
4007
+ );
4008
+ });
4009
+ convertContextButtons.keystoreUriPersistent.addEventListener("click", () => {
4010
+ void keystorePanel.addManualUriToKeystoreFromContextMenu(
4011
+ CRYPT_KEYSTORE_MODE_PERSISTENT,
4012
+ );
4013
+ });
4014
+ convertContextButtons.saveJson.addEventListener(
4015
+ "click",
4016
+ saveJsonFromContextMenu,
4017
+ );
4018
+ convertContextButtons.exportPacket.addEventListener(
4019
+ "click",
4020
+ exportCurrentPacketFromContextMenu,
4021
+ );
4022
+ convertContextButtons.exportPayload.addEventListener(
4023
+ "click",
4024
+ exportCurrentPayloadFromContextMenu,
4025
+ );
4026
+ convertContextButtons.saveCookieJar.addEventListener(
4027
+ "click",
4028
+ saveCookieJarFromContextMenu,
4029
+ );
4030
+ convertContextButtons.httpFileSave.addEventListener(
4031
+ "click",
4032
+ saveHttpBodyFromContextMenu,
4033
+ );
4034
+ convertContextButtons.httpFileLoad.addEventListener(
4035
+ "click",
4036
+ loadHttpBodyIntoConvTabFromContextMenu,
4037
+ );
4038
+ convertContextButtons.httpFilePreview.addEventListener(
4039
+ "click",
4040
+ previewHttpBodyInBrowserFromContextMenu,
4041
+ );
4042
+
4043
+ // Handle bookmark selection from dropdown
4044
+ document
4045
+ .getElementById("selectBookmark")
4046
+ .addEventListener("change", function () {
4047
+ const bookmarkHost = document
4048
+ .getElementById("selectBookmark")
4049
+ .value.split(":")[0];
4050
+ index = document.getElementById("selectBookmark").value.split(":")[1];
4051
+ setActivePacketCursor(index);
4052
+ packetsForHost = capturedPackets["Host"][bookmarkHost];
4053
+ activeBookmark["Host"] = bookmarkHost;
4054
+ activeBookmark["Packet"] = index;
4055
+ hostFilterEl.value = bookmarkHost;
4056
+ if (bookmarkHost == undefined || index == undefined) {
4057
+ statusUpdate("Invalid bookmark selection, missing host or packet index");
4058
+ doError("Invalid bookmark selection, missing host or packet index!");
4059
+ } else {
4060
+ document.getElementById("target_hosts").value = bookmarkHost;
4061
+ }
4062
+ handlePacketNavigation("bookmark", activeBookmark);
4063
+ });
4064
+
4065
+ // Add current packet as a bookmark
4066
+ document.getElementById("setBookmark").addEventListener("click", function () {
4067
+ if (!bookmarkList.includes(currentPacketKey)) {
4068
+ if (currentPacketKey != undefined) {
4069
+ bookmarkList.push(currentPacketKey);
4070
+ document
4071
+ .getElementById("selectBookmark")
4072
+ .appendChild(new Option(currentPacketKey, currentPacketKey));
4073
+ writeLogEntry(`Bookmark added key=${currentPacketKey}`);
4074
+ }
4075
+ }
4076
+ });
4077
+
4078
+ // Syncs the bookmark dropdown to reflect whether the given packet key is bookmarked
4079
+ function syncBookmarkDropdown(packetKey) {
4080
+ document.getElementById("selectBookmark").value = bookmarkList.includes(
4081
+ packetKey,
4082
+ )
4083
+ ? packetKey
4084
+ : "";
4085
+ }
4086
+
4087
+ // function that returns the total number of packets in the entire capture
4088
+ function totalPacketCount() {
4089
+ let totalCount = 0;
4090
+ if (capturedPackets["Host"] != undefined) {
4091
+ for (const host in capturedPackets["Host"]) {
4092
+ totalCount += capturedPackets["Host"][host].length;
4093
+ }
4094
+ } else {
4095
+ return 0;
4096
+ }
4097
+ return totalCount;
4098
+ }
4099
+
4100
+ /**
4101
+ * Returns the packet array index matching a `sourceIp:packetIndex` key.
4102
+ */
4103
+ function findPacketIndexByKey(packetSet, packetKey) {
4104
+ if (!Array.isArray(packetSet) || !packetKey || typeof packetKey !== "string") {
4105
+ return -1;
4106
+ }
4107
+
4108
+ const separatorIndex = packetKey.lastIndexOf(":");
4109
+ if (separatorIndex < 0) return -1;
4110
+
4111
+ const sourceIp = packetKey.slice(0, separatorIndex);
4112
+ const packetIndexValue = packetKey.slice(separatorIndex + 1);
4113
+ return packetSet.findIndex((packet) => {
4114
+ const packetInfo = packet?.["Packet Info"];
4115
+ if (!packetInfo) return false;
4116
+ const candidateSourceIp = packetInfo?.["IP"]?.["Source IP"];
4117
+ const candidatePacketIndex = packetInfo?.["Index"];
4118
+ return (
4119
+ String(candidateSourceIp) === sourceIp &&
4120
+ String(candidatePacketIndex) === packetIndexValue
4121
+ );
4122
+ });
4123
+ }
4124
+
4125
+ /**
4126
+ * Handles navigation between capturedPackets (next, prev, activeBookmark, first-load).
4127
+ * Updates UI and packet info accordingly.
4128
+ */
4129
+ function handlePacketNavigation(navAction, navBookmark) {
4130
+ activeMainTab = MAIN_TAB_DATA;
4131
+ const previousPacketKey = currentPacketKey;
4132
+ const previousCursor = getActivePacketCursor();
4133
+ document.getElementById("prev-btn").style.display = "block";
4134
+ document.getElementById("next-btn").style.display = "block";
4135
+ document.getElementById("loading-container").style.display = "none";
4136
+ document.getElementById("summary_box").style.display = "none";
4137
+ document.getElementById("stats_box").style.display = "none";
4138
+ document.getElementById("list_box").style.display = "none";
4139
+ document.getElementById("notes_box").style.display = "none";
4140
+ document.getElementById("data_tools_box").style.display = "none";
4141
+ document.getElementById("crypt_box").style.display = "none";
4142
+ document.getElementById("keystore_box").style.display = "none";
4143
+ document.getElementById("packetInfoPane").style.display = "block";
4144
+ document.getElementById("packetPayloadPane").style.display = "block";
4145
+ document.getElementById("welcome").style.display = "none";
4146
+ showAllData();
4147
+ const rightsideDataEl = document.getElementById("rightside-data");
4148
+ const rightsideNotesEl = document.getElementById("rightside-notes");
4149
+ if (rightsideDataEl) rightsideDataEl.hidden = false;
4150
+ if (rightsideNotesEl) rightsideNotesEl.hidden = true;
4151
+
4152
+ document.getElementById("total-packets").innerHTML =
4153
+ "Total Packets: " + totalPacketCount();
4154
+ if (navAction === undefined) {
4155
+ handlePacketNavigation("first-load");
4156
+ }
4157
+ let packetSet = capturedPackets["Host"][hostFilterEl.value];
4158
+ if (navAction === "filtered") {
4159
+ packetSet = [];
4160
+ document.getElementById("filter-returned").textContent =
4161
+ "Filtered Packets: " + filteredPackets.length;
4162
+ packetSet = filteredPackets;
4163
+ writeLogEntry(
4164
+ `Filtered packet navigation packets_returned=${packetSet.length}`,
4165
+ );
4166
+ }
4167
+
4168
+ if (navAction === "bookmark") {
4169
+ if (
4170
+ navBookmark["Host"] == undefined ||
4171
+ navBookmark["Packet"] == undefined
4172
+ ) {
4173
+ statusUpdate("Status: Invalid bookmark data, reverting to first packet");
4174
+ doError("Invalid bookmark data, missing host or packet index!");
4175
+ handlePacketNavigation("first-load");
4176
+ } else {
4177
+ index = navBookmark["Packet"] - 1;
4178
+ setActivePacketCursor(index);
4179
+
4180
+ statusUpdate(
4181
+ "Navigating to bookmark: " +
4182
+ navBookmark["Host"] +
4183
+ " packet " +
4184
+ navBookmark["Packet"],
4185
+ );
4186
+ writeLogEntry(
4187
+ `Navigating bookmark host=${navBookmark["Host"]} packet=${navBookmark["Packet"]}`,
4188
+ );
4189
+ }
4190
+ } else {
4191
+ const packetIndexFromKey = findPacketIndexByKey(packetSet, previousPacketKey);
4192
+ if (packetIndexFromKey >= 0) {
4193
+ index = packetIndexFromKey;
4194
+ } else if (
4195
+ Number.isInteger(previousCursor) &&
4196
+ previousCursor >= 0 &&
4197
+ previousCursor < packetSet?.length
4198
+ ) {
4199
+ index = previousCursor;
4200
+ } else {
4201
+ index = 0;
4202
+ }
4203
+ setActivePacketCursor(index);
4204
+ }
4205
+ if (!packetSet || packetSet.length === 0) {
4206
+ statusUpdate("Status: No packets");
4207
+ return;
4208
+ }
4209
+ if (
4210
+ packetSet != undefined &&
4211
+ (packetSet.length == 0 || packetSet[0] == undefined)
4212
+ ) {
4213
+ statusUpdate("Status: No packet information found for this host");
4214
+ document.getElementById("main").innerHTML = "Please select a json file!";
4215
+ }
4216
+ // in the data main secton, this is where we would
4217
+ // add the packet info for each packet, for now we just
4218
+ // dump the json, we'll format later
4219
+ // packetsForHost[index] is an array of all packet info
4220
+ // for the current host, we want to be able to navigate
4221
+ // through it with next and prev buttons
4222
+ if (packetSet == undefined || packetSet[index] == undefined) {
4223
+ statusUpdate("Status: No packet information found for this host");
4224
+ doError("No packet information found for this host!");
4225
+ return;
4226
+ } else {
4227
+ currentIp = packetSet[index]["Packet Info"]["IP"]["Source IP"];
4228
+ currentPacketKey =
4229
+ currentIp + ":" + packetSet[index]["Packet Info"]["Index"];
4230
+ syncBookmarkDropdown(currentPacketKey);
4231
+ console.log(packetSet[index]);
4232
+ const hexPayload =
4233
+ packetSet[index]["Packet Info"]["Raw data"]["Payload"]["Hex Encoded"];
4234
+ infoPanel(packetSet);
4235
+ popHexGrid(hexPayload);
4236
+ populateDataTypes(packetSet);
4237
+ logCurrentPacketDisplay(navAction || "first-load");
4238
+ }
4239
+ }
4240
+ function populateDataTypes(p) {
4241
+ const typesListEl = document.getElementById("types-list");
4242
+ typesListEl.textContent = "";
4243
+ const mimeTypeEl = document.getElementById("mime-type");
4244
+ const charsetEl = document.getElementById("charset");
4245
+ const encodingEl = document.getElementById("encoding");
4246
+ const languageEl = document.getElementById("language");
4247
+ encodingEl.textContent = "";
4248
+ languageEl.textContent = "";
4249
+ let encodingText = "";
4250
+ let languageText = "";
4251
+ // packetsForHost = capturedPackets["Host"][hostFilterEl.value];
4252
+ packetsForHost = p;
4253
+ let charsetText = JSON.parse(
4254
+ JSON.stringify(
4255
+ packetsForHost[index]["Extra Info"]["Traits"]["Characters"]["Charset"],
4256
+ ),
4257
+ );
4258
+ if (
4259
+ packetsForHost[index]["Extra Info"]["Traits"]["Characters"]["Encoding"] ==
4260
+ "Unavailable for high entropy data"
4261
+ ) {
4262
+ encodingText = JSON.parse(
4263
+ JSON.stringify(
4264
+ packetsForHost[index]["Extra Info"]["Traits"]["Characters"]["Encoding"],
4265
+ ),
4266
+ );
4267
+ } else {
4268
+ encodingText = JSON.stringify(
4269
+ packetsForHost[index]["Extra Info"]["Traits"]["Characters"]["Encoding"][
4270
+ "encoding"
4271
+ ],
4272
+ );
4273
+ languageText = JSON.stringify(
4274
+ packetsForHost[index]["Extra Info"]["Traits"]["Characters"]["Encoding"][
4275
+ "language"
4276
+ ],
4277
+ );
4278
+ }
4279
+
4280
+ const mimeTypeText = JSON.parse(
4281
+ JSON.stringify(packetsForHost[index]["Extra Info"]["MIME Type"]),
4282
+ );
4283
+ let dataItems = JSON.parse(
4284
+ JSON.stringify(packetsForHost[index]["Extra Info"]["Data Types"]),
4285
+ );
4286
+ let sslDetails = "";
4287
+ if (
4288
+ packetsForHost[index]["Extra Info"]["Traits"]["Server Info"][
4289
+ "Encryption Data"
4290
+ ] != "N/A" &&
4291
+ packetsForHost[index]["Extra Info"]["Traits"]["Server Info"][
4292
+ "Encryption Data"
4293
+ ] != undefined
4294
+ ) {
4295
+ sslDetails =
4296
+ packetsForHost[index]["Extra Info"]["Traits"]["Server Info"][
4297
+ "Encryption Data"
4298
+ ]["SSL Version"];
4299
+ const protoName =
4300
+ packetsForHost[index]["Extra Info"]["Traits"]["Network Data"][
4301
+ "Port Protcol"
4302
+ ];
4303
+ dataItems = [];
4304
+ dataItems.push(sslDetails + " encrypted stream");
4305
+ dataItems.push(protoName + " protocol data");
4306
+ }
4307
+
4308
+ mimeTypeEl.textContent = "MIME type: " + mimeTypeText;
4309
+ charsetText = charsetText == "" ? "Unknown" : charsetText;
4310
+ encodingText = encodingText == "" ? "Unknown" : encodingText;
4311
+ if (encodingText !== undefined) {
4312
+ encodingEl.textContent =
4313
+ "Payload Encoding: " + encodingText.replace(/"/g, "");
4314
+ }
4315
+ if (languageText !== undefined) {
4316
+ languageEl.textContent =
4317
+ "Payload Language: " + languageText.replace(/"/g, "");
4318
+ }
4319
+ dataItems.forEach((item) => {
4320
+ const listItem = document.createElement("li");
4321
+ listItem.textContent = item;
4322
+ typesListEl.appendChild(listItem);
4323
+ });
4324
+ }
4325
+ // this takes a char code and returns true if it's
4326
+ // a printable ASCII character, false otherwise
4327
+ function isPrintable(charCode) {
4328
+ // ASCII printable: 32 (space) to 126 (~)
4329
+ return charCode >= 32 && charCode <= 126;
4330
+ }
4331
+
4332
+ // this changes hex to ASCII
4333
+ function hexToAscii(hex) {
4334
+ let decodedAscii = "";
4335
+ for (let i = 0; i < hex.length; i += 2) {
4336
+ decodedAscii += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
4337
+ }
4338
+ return decodedAscii;
4339
+ }
4340
+
4341
+ // trunactes a string to a max length
4342
+ function truncate(str, maxLength) {
4343
+ if (str.length <= maxLength) return str;
4344
+ return str.slice(0, maxLength);
4345
+ }
4346
+
4347
+ // returns a 0 padded hex string of a number with a given length
4348
+ function decToHex(num, pad) {
4349
+ return num.toString(16).padStart(pad, "0");
4350
+ }
4351
+
4352
+ // clears the higlights (its called after the moouse leaves grid)
4353
+ function clearGridHighlights() {
4354
+ document
4355
+ .querySelectorAll(".griditem")
4356
+ .forEach((el) => el.classList.remove("highlight"));
4357
+ }
4358
+
4359
+ /**
4360
+ * Populates the hex grid display with the given hex string.
4361
+ */
4362
+ function popHexGrid(hex) {
4363
+ // swap it back to ASCII for the fade box
4364
+ const payloadAsciiBox = document.getElementById("payloadascii");
4365
+ const decodedAscii = hexToAscii(hex);
4366
+ document.getElementById("hexg").textContent = "";
4367
+ const hexGridContainer = document.getElementById("hexg");
4368
+ const hexPairs = hex.toUpperCase().match(/.{1,2}/g) || [];
4369
+ // this block populates the grid with boxes for hex codes
4370
+ hexPairs.forEach((hexPair, byteIndex) => {
4371
+ const item = document.createElement("div");
4372
+ item.classList.add("griditem");
4373
+ item.textContent = hexPair;
4374
+ item.dataset.byteIndex = String(byteIndex);
4375
+ hexGridContainer.appendChild(item);
4376
+ });
4377
+ function getPrintableSequence(startIndex) {
4378
+ let result = "";
4379
+ for (let i = startIndex; i < decodedAscii.length; i++) {
4380
+ if (!isPrintable(decodedAscii.charCodeAt(i))) break;
4381
+ result += String.fromCharCode(decodedAscii.charCodeAt(i));
4382
+ }
4383
+ return result;
4384
+ }
4385
+ // Attach event listeners to each grid item
4386
+ document.querySelectorAll(".griditem").forEach((item, idx) => {
4387
+ item.addEventListener("mouseenter", (e) => {
4388
+ //box fade in
4389
+ const hexOffsetDisplay = document.getElementById("asciiOffset");
4390
+ const asciiTextBox = document.getElementById("asciiText");
4391
+ payloadAsciiBox.style.top = e.clientY + 18 + "px";
4392
+ payloadAsciiBox.style.left = e.clientX + 18 + "px";
4393
+ payloadAsciiBox.classList.add("visible");
4394
+ asciiTextBox.innerHTML = "";
4395
+ const printable = getPrintableSequence(idx);
4396
+ window.currentPrintableSequence = printable;
4397
+ // adds only consecutive printable characters to the decodedAscii box
4398
+ asciiTextBox.textContent += truncate(printable, 32);
4399
+ for (let i = 0; i < truncate(printable, 32).length; i++) {
4400
+ const highlightedCell = document.querySelectorAll(".griditem")[idx + i];
4401
+ highlightedCell.classList.add("highlight");
4402
+ }
4403
+ const hexLen = parseInt(truncate(printable, 32).length, 10)
4404
+ .toString(16)
4405
+ .padStart(2, "0")
4406
+ .toUpperCase();
4407
+ const hexOffset = idx.toString(16).padStart(4, "0").toUpperCase();
4408
+ if (printable.length == 0) {
4409
+ asciiTextBox.textContent = "0x" + item.textContent;
4410
+ }
4411
+ hexOffsetDisplay.textContent = "0x" + hexOffset + ":" + hexLen;
4412
+ });
4413
+ });
4414
+ // this fades the box back out and calls the grid clear func
4415
+ document.querySelectorAll(".griditem").forEach((item) => {
4416
+ item.addEventListener("mouseleave", () => {
4417
+ payloadAsciiBox.classList.remove("visible");
4418
+ clearGridHighlights();
4419
+ });
4420
+ });
4421
+ }
4422
+
4423
+ /**
4424
+ * Utility to create a table from data and headers, and append to a container.
4425
+ */
4426
+ // probably should break this function up into smaller pieces,
4427
+ // but it works for now, it takes the current packet info and
4428
+ // populates the info panel with it, including the side tables
4429
+ // and the main info table, also updates the timestamp and
4430
+ // currentIp:port info at the top
4431
+ function infoPanel(pk) {
4432
+ const infoPaneEl = document.getElementById("packetInfoPane");
4433
+ document.getElementById("rightside").style.display = "block";
4434
+ document.getElementById("leftside").style.display = "block";
4435
+ const infoPaneOrigHtml = infoPaneEl.innerHTML;
4436
+ infoPaneEl.style.display = "block";
4437
+ const p = pk[index];
4438
+ let packetInfoData = p["Packet Info"];
4439
+ let extraInfoData = p["Extra Info"];
4440
+ let packetTimestamp = packetInfoData["Packet Timestamp"];
4441
+ let ipChecksum = packetInfoData["IP"]["IP Checksum"];
4442
+
4443
+ // Determine transport protocol (TCP or UDP); fall back to TCP for older captures
4444
+ const protocol = packetInfoData["Protocol"] || "TCP";
4445
+ const transportData = packetInfoData[protocol] || {};
4446
+
4447
+ const transportChecksum =
4448
+ protocol === "TCP"
4449
+ ? transportData["TCP checksum"]
4450
+ : protocol === "UDP"
4451
+ ? transportData["UDP checksum"]
4452
+ : protocol === "ICMP"
4453
+ ? transportData["ICMP Checksum"]
4454
+ : "N/A";
4455
+ const transportLayerLen =
4456
+ protocol === "TCP"
4457
+ ? transportData["TCP layer length"]
4458
+ : protocol === "UDP"
4459
+ ? transportData["UDP length"]
4460
+ : protocol === "ICMP"
4461
+ ? transportData["Wire length"]
4462
+ : "N/A";
4463
+ const tcpFlags =
4464
+ protocol === "TCP" && transportData["TCP Flag Data"]
4465
+ ? transportData["TCP Flag Data"]["Flags"]
4466
+ : "N/A";
4467
+
4468
+ const sourceIpPort =
4469
+ packetInfoData["IP"]["Source IP"] +
4470
+ ":" +
4471
+ (transportData["Source port"] ?? "?");
4472
+ const destIpPort =
4473
+ packetInfoData["IP"]["Destination IP"] +
4474
+ ":" +
4475
+ (transportData["Destination port"] ?? "?");
4476
+ const etherFrame =
4477
+ typeof packetInfoData["Ethernet Frame"] === "object" &&
4478
+ packetInfoData["Ethernet Frame"] !== null
4479
+ ? packetInfoData["Ethernet Frame"]
4480
+ : {};
4481
+ const srcMac = etherFrame["MAC Source"] ?? "N/A";
4482
+ const dstMac = etherFrame["MAC Destination"] ?? "N/A";
4483
+ const srcMacVendor = etherFrame["MAC Source Vendor"] ?? "N/A";
4484
+ const dstMacVendor = etherFrame["MAC Destination Vendor"] ?? "N/A";
4485
+ const ipLayerLen = packetInfoData["IP"]["IP layer length"];
4486
+ const wireLen = transportData["Wire length"];
4487
+ const payloadLen = packetInfoData["Raw data"]["Payload Length"];
4488
+ let sslCert = "";
4489
+ let sslVersion = "";
4490
+ let sslAlgos = "";
4491
+ if (
4492
+ extraInfoData["Traits"]["Server Info"]["Encryption Data"] == "N/A" ||
4493
+ extraInfoData["Traits"]["Server Info"].hasOwnProperty("Encryption Data") ==
4494
+ false
4495
+ ) {
4496
+ sslCert = "Not encrypted";
4497
+ sslVersion = "Not encrypted";
4498
+ sslAlgos = "";
4499
+ } else {
4500
+ sslCert =
4501
+ extraInfoData["Traits"]["Server Info"]["Encryption Data"]["SSL Cert"] ??
4502
+ "Not available";
4503
+ sslVersion =
4504
+ extraInfoData["Traits"]["Server Info"]["Encryption Data"][
4505
+ "SSL Version"
4506
+ ] ?? "Not available";
4507
+ sslAlgos =
4508
+ extraInfoData["Traits"]["Server Info"]["Encryption Data"][
4509
+ "Encrypted With"
4510
+ ].join("<br>Extra algo info: ") ?? "No algorithm information available";
4511
+ }
4512
+ const isDecompressed = extraInfoData["Decompressed"]["Decompressed"];
4513
+ function removeIps(ipList) {
4514
+ const ipRegex =
4515
+ /\b((25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\b/;
4516
+ return ipList.filter((item) => !ipRegex.test(item));
4517
+ }
4518
+
4519
+ let dnsHostsHtml;
4520
+ if (
4521
+ extraInfoData["Traits"]["Network Data"]["Hostnames"]["Hostnames"] ==
4522
+ undefined
4523
+ ) {
4524
+ dnsHostsHtml = "localhost";
4525
+ } else {
4526
+ dnsHostsHtml =
4527
+ "localhost<br>" +
4528
+ extraInfoData["Traits"]["Network Data"]["Hostnames"]["Hostnames"].join(
4529
+ "<br>",
4530
+ );
4531
+ }
4532
+ const filteredDnsHosts = removeIps(dnsHostsHtml.split("<br>")).join("<br>");
4533
+ dnsHostsHtml = filteredDnsHosts == "" ? "localhost" : filteredDnsHosts;
4534
+
4535
+ const pageTitle = extraInfoData["Traits"]["Server Info"]["Page Title"];
4536
+ const isEncrypted = extraInfoData["Traits"]["Server Info"]["Encrypted"];
4537
+ const protoName = extraInfoData["Traits"]["Network Data"]["Port Protcol"];
4538
+ const protoDescription =
4539
+ extraInfoData["Traits"]["Network Data"]["Port Description"];
4540
+ const srcNetClass =
4541
+ extraInfoData["Traits"]["Network Data"]["Source IP"]["Class"];
4542
+ const dstNetClass =
4543
+ extraInfoData["Traits"]["Network Data"]["Destination IP"]["Class"];
4544
+ document.getElementById("sidedatatable").textContent = "";
4545
+ document.getElementById("protoInfoSrc").textContent = "Source";
4546
+ document.getElementById("protoInfoDest").textContent = "Destination";
4547
+ document.getElementById("comp").textContent = "Unknown";
4548
+ if (isDecompressed == false || isDecompressed == undefined) {
4549
+ const types = extraInfoData["Data Types"];
4550
+
4551
+ types.forEach((type) => {
4552
+ if (type.includes("Zlib") || type.includes("zlib")) {
4553
+ document.getElementById("comp").textContent = "Compressed with zlib";
4554
+
4555
+ console.log("Data type identified: " + type);
4556
+ }
4557
+ if (type.includes("Gzip") || type.includes("gzip")) {
4558
+ document.getElementById("comp").textContent = "Compressed with gzip";
4559
+ }
4560
+ if (type.includes("Zip")) {
4561
+ document.getElementById("comp").textContent = "Compressed with zip";
4562
+ }
4563
+ });
4564
+ }
4565
+ if (isDecompressed == true) {
4566
+ document.getElementById("comp").textContent =
4567
+ "Not regonized as compressed data";
4568
+ }
4569
+ // wireLen
4570
+ if (pageTitle == undefined || pageTitle == "N/A") {
4571
+ document.getElementById("website").textContent =
4572
+ "Not available for this server";
4573
+ } else {
4574
+ document.getElementById("website").textContent = pageTitle;
4575
+ }
4576
+ //document.getElementById("crypt").textContent = isEncrypted;
4577
+ const dnsCollapsedList = dnsHostsHtml.replace(/(<br\s*\/?>\s*)+/gi, "<br>");
4578
+ document.getElementById("dns").innerHTML = dnsCollapsedList;
4579
+ if (sslAlgos == undefined || sslAlgos == "") {
4580
+ //document.getElementById("crypt").innerHTML = sslCert
4581
+ // ? "Encrypted with: " + sslVersion + "<br>" + sslAlgos
4582
+ // : "Not Encrypted";
4583
+ document.getElementById("crypt").innerHTML = "Not encrypted";
4584
+ } else {
4585
+ document.getElementById("crypt").innerHTML =
4586
+ "Encrypted with: " + sslVersion + "<br>" + sslAlgos;
4587
+ }
4588
+
4589
+ if (protoName == "Unknown") {
4590
+ document.getElementById("protocols").innerHTML = "Unknown";
4591
+ } else {
4592
+ document.getElementById("protocols").innerHTML =
4593
+ "Protocol Name: " +
4594
+ protoName +
4595
+ "<br>Protocol Description: " +
4596
+ protoDescription;
4597
+ }
4598
+ const checksumData = [
4599
+ { name: "IP Checksum", value: ipChecksum },
4600
+ { name: protocol + " Checksum", value: transportChecksum },
4601
+ { name: "Flags", value: tcpFlags },
4602
+ { name: "IP Length", value: ipLayerLen },
4603
+ { name: protocol + " Length", value: transportLayerLen },
4604
+ { name: "Wire Length", value: wireLen },
4605
+ { name: "Payload Length", value: payloadLen },
4606
+ ];
4607
+ const checksumHeaders = ["Protocol data", "Details"];
4608
+ createTable(checksumData, checksumHeaders, "sidedatatable");
4609
+
4610
+ // DNS info table (shown for UDP/DNS packets)
4611
+ renderDnsTable(transportData);
4612
+
4613
+ // ICMP info table (shown for ICMP packets)
4614
+ renderIcmpTable(protocol, transportData);
4615
+
4616
+ // SNMP info table (shown for SNMP packets on port 161/162)
4617
+ renderSnmpTable(transportData);
4618
+
4619
+ // DHCP info table (shown for DHCP packets on port 67/68)
4620
+ renderDhcpTable(transportData);
4621
+
4622
+ // NTP info table (shown for NTP packets on port 123)
4623
+ renderNtpTable(transportData);
4624
+
4625
+ // SIP info table (shown for SIP packets on port 5060/5061)
4626
+ renderSipTable(transportData);
4627
+
4628
+ // HTTP info table (shown for HTTP request/response packets)
4629
+ renderHttpTable(transportData);
4630
+
4631
+ // HTTP/2 info table (shown for HTTP/2 frames on any TCP port)
4632
+ renderHttp2Table(transportData);
4633
+
4634
+ // FTP info table (shown for FTP packets on port 20/21)
4635
+ renderFtpTable(transportData);
4636
+
4637
+ // SMTP info table (shown for SMTP packets on port 25/587/465)
4638
+ renderSmtpTable(transportData);
4639
+
4640
+ // POP3 info table (shown for POP3 packets on port 110/995)
4641
+ renderPop3Table(transportData);
4642
+
4643
+ // IMAP info table (shown for IMAP packets on port 143/993)
4644
+ renderImapTable(transportData);
4645
+
4646
+ // Telnet info table (shown for Telnet packets on port 23)
4647
+ renderTelnetTable(transportData);
4648
+
4649
+ // IRC info table (shown for IRC packets on port 6667/6668/6669)
4650
+ renderIrcTable(transportData);
4651
+
4652
+ // MTP info table (shown for MTP/MMS packets on port 1755)
4653
+ renderMtpTable(transportData);
4654
+
4655
+ // LDAP info table (shown for LDAP packets on port 389/636)
4656
+ renderLdapTable(transportData);
4657
+
4658
+ // MySQL info table (shown for MySQL packets on port 3306)
4659
+ renderMysqlTable(transportData);
4660
+
4661
+ // PostgreSQL info table (shown for PostgreSQL packets on port 5432)
4662
+ renderPostgresqlTable(transportData);
4663
+
4664
+ // XMPP info table (shown for XMPP packets on port 5222/5223)
4665
+ renderXmppTable(transportData);
4666
+
4667
+ // SMB info table (shown for SMB packets on port 139/445)
4668
+ renderSmbTable(transportData);
4669
+
4670
+ // MQTT info table (shown for MQTT packets on port 1883/8883)
4671
+ renderMqttTable(transportData);
4672
+
4673
+ // RTSP info table (shown for RTSP packets on port 554)
4674
+ renderRtspTable(transportData);
4675
+
4676
+ // TFTP info table (shown for TFTP packets on UDP port 69)
4677
+ renderTftpTable(transportData);
4678
+
4679
+ // BGP info table (shown for BGP packets on port 179)
4680
+ renderBgpTable(transportData);
4681
+
4682
+ // NNTP info table (shown for NNTP packets on port 119)
4683
+ renderNntpTable(transportData);
4684
+
4685
+ // RADIUS info table (shown for RADIUS packets on port 1812/1813/1645/1646)
4686
+ renderRadiusTable(transportData);
4687
+
4688
+ const ipTableHeaders = ["Packet", "Data"];
4689
+ const srcIpData = [
4690
+ { name: "IP:Port", value: sourceIpPort },
4691
+ { name: "MAC", value: srcMac },
4692
+ { name: "MAC Vendor", value: srcMacVendor },
4693
+ { name: "Network Class", value: srcNetClass },
4694
+ ];
4695
+ createTable(srcIpData, ipTableHeaders, "protoInfoSrc");
4696
+ const dstIpData = [
4697
+ { name: "IP:Port", value: destIpPort },
4698
+ { name: "MAC", value: dstMac },
4699
+ { name: "MAC Vendor", value: dstMacVendor },
4700
+ { name: "Network Class", value: dstNetClass },
4701
+ ];
4702
+ createTable(dstIpData, ipTableHeaders, "protoInfoDest");
4703
+ const entropyValue = extraInfoData["Traits"]["Shannon Entropy"];
4704
+ document.getElementById("timestamp").textContent =
4705
+ "Timestamp " + packetTimestamp;
4706
+ //document.getElementById("ip2ip").textContent = sourceIpPort + " ~ " + destIpPort;
4707
+ document.getElementById("sideloctable").textContent = "";
4708
+ document.getElementById("entropybox").textContent =
4709
+ "\u096F " + entropyValue.toFixed(2);
4710
+ const entropyBoxEl = document.getElementById("entropybox");
4711
+ if (entropyValue >= 6.8) {
4712
+ entropyBoxEl.className = "high";
4713
+ } else if (entropyValue >= 4.5) {
4714
+ entropyBoxEl.className = "med";
4715
+ } else {
4716
+ entropyBoxEl.className = "low";
4717
+ }
4718
+ const secondColumnCells = document.querySelectorAll(
4719
+ "table tr td:nth-child(1), table tr th:nth-child(1)",
4720
+ );
4721
+ secondColumnCells.forEach((cell) => {
4722
+ cell.style.width = "23%";
4723
+ });
4724
+ if (
4725
+ extraInfoData["Traits"]["Network Data"]["Source IP"]["Location"]["City"] ==
4726
+ undefined
4727
+ ) {
4728
+ const localnetData = [{ name: "Location", value: "Localnet" }];
4729
+ const localnetHeaders = ["Source Host", "Location"];
4730
+ createTable(localnetData, localnetHeaders, "sideloctable");
4731
+ } else {
4732
+ const srcLocData = [
4733
+ {
4734
+ name: "Country",
4735
+ value:
4736
+ extraInfoData["Traits"]["Network Data"]["Source IP"]["Location"][
4737
+ "Country"
4738
+ ],
4739
+ },
4740
+ {
4741
+ name: "City",
4742
+ value:
4743
+ extraInfoData["Traits"]["Network Data"]["Source IP"]["Location"][
4744
+ "City"
4745
+ ],
4746
+ },
4747
+ {
4748
+ name: "Timezone",
4749
+ value:
4750
+ extraInfoData["Traits"]["Network Data"]["Source IP"]["Location"][
4751
+ "Time Zone"
4752
+ ],
4753
+ },
4754
+ ];
4755
+ const srcLocHeaders = ["Source Host", "Location"];
4756
+ createTable(srcLocData, srcLocHeaders, "sideloctable");
4757
+ }
4758
+ if (
4759
+ extraInfoData["Traits"]["Network Data"]["Destination IP"]["Location"][
4760
+ "City"
4761
+ ] == undefined
4762
+ ) {
4763
+ const localnetData = [{ name: "Location", value: "Localnet" }];
4764
+ const localnetHeaders = ["Destination Host", "Location"];
4765
+ createTable(localnetData, localnetHeaders, "sideloctable");
4766
+ } else {
4767
+ const dstLocData = [
4768
+ {
4769
+ name: "Country",
4770
+ value:
4771
+ extraInfoData["Traits"]["Network Data"]["Destination IP"]["Location"][
4772
+ "Country"
4773
+ ],
4774
+ },
4775
+ {
4776
+ name: "City",
4777
+ value:
4778
+ extraInfoData["Traits"]["Network Data"]["Destination IP"]["Location"][
4779
+ "City"
4780
+ ],
4781
+ },
4782
+ {
4783
+ name: "Timezone",
4784
+
4785
+ value:
4786
+ extraInfoData["Traits"]["Network Data"]["Destination IP"]["Location"][
4787
+ "Time Zone"
4788
+ ],
4789
+ },
4790
+ ];
4791
+ const dstLocHeaders = ["Destination Host", "Location"];
4792
+ createTable(dstLocData, dstLocHeaders, "sideloctable");
4793
+ }
4794
+ }
4795
+
4796
+ // Save the currently loaded capture plus session state to disk
4797
+ document.getElementById("save-json-btn").addEventListener("click", function () {
4798
+ void persistSessionToDisk("sidebar-button");
4799
+ });
4800
+
4801
+ // the next two have hooks into IPC handlers for main.js
4802
+ // data transactions
4803
+
4804
+ // when the main.js returns our json data from snitch.py
4805
+ window.jsonapi.onJsonData((jsonData) => {
4806
+ document.getElementById("loading-container").style.display = "block";
4807
+ document.getElementById("error-container").style.display = "none";
4808
+ statusUpdate("Loaded data from backend, processing...");
4809
+ writeLogEntry("Backend JSON payload received for processing");
4810
+ processFile(
4811
+ new File([jsonData], "capture.json", { type: "application/json" }),
4812
+ );
4813
+ document.getElementById("loading-container").style.display = "none";
4814
+ const loadEndTime = performance.now();
4815
+ document.getElementById("load-time").textContent =
4816
+ "Load time: " + ((loadEndTime - startTime) / 1000).toFixed(2) + " seconds";
4817
+ });
4818
+
4819
+ // here we create the backend process and hook it to the handler
4820
+ function runSnitch(file) {
4821
+ document.getElementById("loading-container").style.display = "block";
4822
+ showSummaryLoading();
4823
+ document.getElementById("status").textContent =
4824
+ "Status: Running snitch backend, this may take a few minutes...";
4825
+ document.getElementById("error-container").style.display = "none";
4826
+ startTime = performance.now();
4827
+ const useLLM = document.getElementById("use-llm").checked;
4828
+ const fileLabel = typeof file === "string" ? file : file?.name || "unknown";
4829
+ writeLogEntry(
4830
+ `Backend analysis started file=${fileLabel} llm_enabled=${useLLM}`,
4831
+ );
4832
+ window.snitchapi
4833
+ .runBackendCommand(file, useLLM)
4834
+ .then((output) => {})
4835
+ .catch((error) => {
4836
+ doError("Backend run error!", { backend: true });
4837
+ logErrorEntry("backend-run", error);
4838
+ });
4839
+ }
4840
+
4841
+ function doError(message, { backend = false } = {}) {
4842
+ console.error("Error from backend:", message);
4843
+ if (backend) {
4844
+ writeBackendErrorLogEntry(`Error shown message="${message}"`);
4845
+ } else {
4846
+ writeLogEntry(`Error shown message="${message}"`);
4847
+ }
4848
+ const loadingContainerEl = document.getElementById("loading-container");
4849
+ const errorContainerEl = document.getElementById("error-container");
4850
+ clearSummaryContent();
4851
+ loadingContainerEl.style.display = "none";
4852
+ errorContainerEl.style.display = "block";
4853
+ errorContainerEl.textContent = message;
4854
+ errorContainerEl.addEventListener("click", () => {
4855
+ errorContainerEl.style.display = "none";
4856
+ loadingContainerEl.style.display = "none";
4857
+ });
4858
+ }
4859
+
4860
+ function hideAllData() {
4861
+ // document.getElementById("packetInfoPane").textContent =
4862
+ // "No matching packets found.";
4863
+ doError("No packets match the filter criteria!");
4864
+ statusUpdate("Status: No packets match the filter criteria");
4865
+ document.getElementById("data-types").style.display = "none";
4866
+ document.getElementById("protoInfo").style.display = "none";
4867
+ document.getElementById("timestamp").style.display = "none";
4868
+ document.getElementById("rightside").style.display = "none";
4869
+ document.getElementById("active-recon").style.display = "none";
4870
+ document.getElementById("prev-btn").style.opacity = "0";
4871
+ document.getElementById("next-btn").style.opacity = "0";
4872
+ popHexGrid("00".repeat(1));
4873
+ }
4874
+ function showAllData() {
4875
+ document.getElementById("prev-btn").style.opacity = "1";
4876
+ document.getElementById("next-btn").style.opacity = "1";
4877
+ document.getElementById("data-types").style.display = "block";
4878
+ document.getElementById("protoInfo").style.display = "block";
4879
+ document.getElementById("timestamp").style.display = "block";
4880
+ document.getElementById("rightside").style.display = "block";
4881
+ document.getElementById("active-recon").style.display = "block";
4882
+ document.getElementById("hexg").hidden = false;
4883
+ document.getElementById("error-container").style.display = "none";
4884
+ }
4885
+
4886
+ document
4887
+ .getElementById("filterStr")
4888
+ .addEventListener("keydown", function (event) {
4889
+ if (event.key === "Enter") {
4890
+ const filterQuery = filterInputEl.value;
4891
+ runFilterQuery(filterQuery);
4892
+ filterHistorySelectEl.value = "";
4893
+ }
4894
+ });
4895
+
4896
+ filterClearButtonEl.addEventListener("click", clearFilterQuery);
4897
+
4898
+ initializeContextMenu({
4899
+ documentRef: document,
4900
+ windowRef: window,
4901
+ convertContextMenuEl,
4902
+ getPasteTargetFromContextTarget,
4903
+ getTrimmedSelectionText,
4904
+ getConversionTextFromTarget,
4905
+ detectConvertibleFormats,
4906
+ buildContextFilterQueries,
4907
+ getCookieJarTextForContextTarget,
4908
+ showConvertContextMenu,
4909
+ hideConvertContextMenu,
4910
+ });
4911
+
4912
+ filterInputEl.addEventListener("input", syncFilterHighlight);
4913
+ filterInputEl.addEventListener("scroll", syncFilterHighlightScroll);
4914
+
4915
+ filterHistorySelectEl.addEventListener("change", () => {
4916
+ const selectedQuery = filterHistorySelectEl.value;
4917
+ if (!selectedQuery) return;
4918
+ filterInputEl.value = selectedQuery;
4919
+ syncFilterHighlight();
4920
+ runFilterQuery(selectedQuery);
4921
+ filterHistorySelectEl.value = "";
4922
+ });
4923
+
4924
+ renderFilterHistory();
4925
+ syncFilterHighlight();
4926
+
4927
+ window.onerror = (message, source, lineno, colno, error) => {
4928
+ doError(message + " at " + source + ":" + lineno + ":" + colno);
4929
+ };
4930
+
4931
+ window.onunhandledrejection = (event) => {
4932
+ doError("Unhandled promise error! " + event.reason);
4933
+ };
4934
+
4935
+ window.api.onError((msg) => {
4936
+ console.error("Error from backend:", msg);
4937
+ // Show alert or UI message
4938
+ doError(msg, { backend: true });
4939
+ });
4940
+
4941
+ // On page load, hide packet info and payload panes
4942
+ onload = function () {
4943
+ // document.getElementById("selectBookmark").style.display = "none";
4944
+ hideConvertContextMenu();
4945
+ keystorePanel.resetKeystoreState();
4946
+ setCryptSubtab(CRYPT_SSL_SUBTAB);
4947
+ setConvSubtab(CONV_CONVERSIONS_SUBTAB);
4948
+ document.getElementById("packetInfoPane").style.display = "none";
4949
+ document.getElementById("packetPayloadPane").style.display = "none";
4950
+ document.getElementById("rightside").style.display = "none";
4951
+ const rightsideDataEl = document.getElementById("rightside-data");
4952
+ const rightsideNotesEl = document.getElementById("rightside-notes");
4953
+ if (rightsideDataEl) rightsideDataEl.hidden = false;
4954
+ if (rightsideNotesEl) rightsideNotesEl.hidden = true;
4955
+ document.getElementById("leftside").style.display = "none";
4956
+ document.getElementById("loading-container").style.display = "none";
4957
+ };