OutputWaiter.mjs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. /**
  2. * @author n1474335 [n1474335@gmail.com]
  3. * @copyright Crown Copyright 2016
  4. * @license Apache-2.0
  5. */
  6. import Utils from "../core/Utils";
  7. import FileSaver from "file-saver";
  8. /**
  9. * Waiter to handle events related to the output.
  10. */
  11. class OutputWaiter {
  12. /**
  13. * OutputWaiter constructor.
  14. *
  15. * @param {App} app - The main view object for CyberChef.
  16. * @param {Manager} manager - The CyberChef event manager.
  17. */
  18. constructor(app, manager) {
  19. this.app = app;
  20. this.manager = manager;
  21. this.dishBuffer = null;
  22. this.dishStr = null;
  23. }
  24. /**
  25. * Gets the output string from the output textarea.
  26. *
  27. * @returns {string}
  28. */
  29. get() {
  30. return document.getElementById("output-text").value;
  31. }
  32. /**
  33. * Sets the output in the output textarea.
  34. *
  35. * @param {string|ArrayBuffer} data - The output string/HTML/ArrayBuffer
  36. * @param {string} type - The data type of the output
  37. * @param {number} duration - The length of time (ms) it took to generate the output
  38. * @param {boolean} [preserveBuffer=false] - Whether to preserve the dishBuffer
  39. */
  40. async set(data, type, duration, preserveBuffer) {
  41. log.debug("Output type: " + type);
  42. const outputText = document.getElementById("output-text");
  43. const outputHtml = document.getElementById("output-html");
  44. const outputFile = document.getElementById("output-file");
  45. const outputHighlighter = document.getElementById("output-highlighter");
  46. const inputHighlighter = document.getElementById("input-highlighter");
  47. let scriptElements, lines, length;
  48. if (!preserveBuffer) {
  49. this.closeFile();
  50. this.dishStr = null;
  51. document.getElementById("show-file-overlay").style.display = "none";
  52. }
  53. switch (type) {
  54. case "html":
  55. outputText.style.display = "none";
  56. outputHtml.style.display = "block";
  57. outputFile.style.display = "none";
  58. outputHighlighter.display = "none";
  59. inputHighlighter.display = "none";
  60. outputText.value = "";
  61. outputHtml.innerHTML = data;
  62. // Execute script sections
  63. scriptElements = outputHtml.querySelectorAll("script");
  64. for (let i = 0; i < scriptElements.length; i++) {
  65. try {
  66. eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval
  67. } catch (err) {
  68. log.error(err);
  69. }
  70. }
  71. await this.getDishStr();
  72. length = this.dishStr.length;
  73. lines = this.dishStr.count("\n") + 1;
  74. break;
  75. case "ArrayBuffer":
  76. outputText.style.display = "block";
  77. outputHtml.style.display = "none";
  78. outputHighlighter.display = "none";
  79. inputHighlighter.display = "none";
  80. outputText.value = "";
  81. outputHtml.innerHTML = "";
  82. length = data.byteLength;
  83. this.setFile(data);
  84. break;
  85. case "string":
  86. default:
  87. outputText.style.display = "block";
  88. outputHtml.style.display = "none";
  89. outputFile.style.display = "none";
  90. outputHighlighter.display = "block";
  91. inputHighlighter.display = "block";
  92. outputText.value = Utils.printable(data, true);
  93. outputHtml.innerHTML = "";
  94. lines = data.count("\n") + 1;
  95. length = data.length;
  96. this.dishStr = data;
  97. break;
  98. }
  99. this.manager.highlighter.removeHighlights();
  100. this.setOutputInfo(length, lines, duration);
  101. this.backgroundMagic();
  102. }
  103. /**
  104. * Shows file details.
  105. *
  106. * @param {ArrayBuffer} buf
  107. */
  108. setFile(buf) {
  109. this.dishBuffer = buf;
  110. const file = new File([buf], "output.dat");
  111. // Display file overlay in output area with details
  112. const fileOverlay = document.getElementById("output-file"),
  113. fileSize = document.getElementById("output-file-size");
  114. fileOverlay.style.display = "block";
  115. fileSize.textContent = file.size.toLocaleString() + " bytes";
  116. // Display preview slice in the background
  117. const outputText = document.getElementById("output-text"),
  118. fileSlice = this.dishBuffer.slice(0, 4096);
  119. outputText.classList.add("blur");
  120. outputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
  121. }
  122. /**
  123. * Removes the output file and nulls its memory.
  124. */
  125. closeFile() {
  126. this.dishBuffer = null;
  127. document.getElementById("output-file").style.display = "none";
  128. document.getElementById("output-text").classList.remove("blur");
  129. }
  130. /**
  131. * Handler for file download events.
  132. */
  133. async downloadFile() {
  134. this.filename = window.prompt("Please enter a filename:", this.filename || "download.dat");
  135. await this.getDishBuffer();
  136. const file = new File([this.dishBuffer], this.filename);
  137. if (this.filename) FileSaver.saveAs(file, this.filename, false);
  138. }
  139. /**
  140. * Handler for file slice display events.
  141. */
  142. displayFileSlice() {
  143. const startTime = new Date().getTime(),
  144. showFileOverlay = document.getElementById("show-file-overlay"),
  145. sliceFromEl = document.getElementById("output-file-slice-from"),
  146. sliceToEl = document.getElementById("output-file-slice-to"),
  147. sliceFrom = parseInt(sliceFromEl.value, 10),
  148. sliceTo = parseInt(sliceToEl.value, 10),
  149. str = Utils.arrayBufferToStr(this.dishBuffer.slice(sliceFrom, sliceTo));
  150. document.getElementById("output-text").classList.remove("blur");
  151. showFileOverlay.style.display = "block";
  152. this.set(str, "string", new Date().getTime() - startTime, true);
  153. }
  154. /**
  155. * Handler for show file overlay events.
  156. *
  157. * @param {Event} e
  158. */
  159. showFileOverlayClick(e) {
  160. const outputFile = document.getElementById("output-file"),
  161. showFileOverlay = e.target;
  162. document.getElementById("output-text").classList.add("blur");
  163. outputFile.style.display = "block";
  164. showFileOverlay.style.display = "none";
  165. this.setOutputInfo(this.dishBuffer.byteLength, null, 0);
  166. }
  167. /**
  168. * Displays information about the output.
  169. *
  170. * @param {number} length - The length of the current output string
  171. * @param {number} lines - The number of the lines in the current output string
  172. * @param {number} duration - The length of time (ms) it took to generate the output
  173. */
  174. setOutputInfo(length, lines, duration) {
  175. let width = length.toString().length;
  176. width = width < 4 ? 4 : width;
  177. const lengthStr = length.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
  178. const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, "&nbsp;");
  179. let msg = "time: " + timeStr + "<br>length: " + lengthStr;
  180. if (typeof lines === "number") {
  181. const linesStr = lines.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
  182. msg += "<br>lines: " + linesStr;
  183. }
  184. document.getElementById("output-info").innerHTML = msg;
  185. document.getElementById("input-selection-info").innerHTML = "";
  186. document.getElementById("output-selection-info").innerHTML = "";
  187. }
  188. /**
  189. * Handler for save click events.
  190. * Saves the current output to a file.
  191. */
  192. saveClick() {
  193. this.downloadFile();
  194. }
  195. /**
  196. * Handler for copy click events.
  197. * Copies the output to the clipboard.
  198. */
  199. async copyClick() {
  200. await this.getDishStr();
  201. // Create invisible textarea to populate with the raw dish string (not the printable version that
  202. // contains dots instead of the actual bytes)
  203. const textarea = document.createElement("textarea");
  204. textarea.style.position = "fixed";
  205. textarea.style.top = 0;
  206. textarea.style.left = 0;
  207. textarea.style.width = 0;
  208. textarea.style.height = 0;
  209. textarea.style.border = "none";
  210. textarea.value = this.dishStr;
  211. document.body.appendChild(textarea);
  212. // Select and copy the contents of this textarea
  213. let success = false;
  214. try {
  215. textarea.select();
  216. success = this.dishStr && document.execCommand("copy");
  217. } catch (err) {
  218. success = false;
  219. }
  220. if (success) {
  221. this.app.alert("Copied raw output successfully.", 2000);
  222. } else {
  223. this.app.alert("Sorry, the output could not be copied.", 3000);
  224. }
  225. // Clean up
  226. document.body.removeChild(textarea);
  227. }
  228. /**
  229. * Handler for switch click events.
  230. * Moves the current output into the input textarea.
  231. */
  232. async switchClick() {
  233. this.switchOrigData = this.manager.input.get();
  234. document.getElementById("undo-switch").disabled = false;
  235. if (this.dishBuffer) {
  236. this.manager.input.setFile(new File([this.dishBuffer], "output.dat"));
  237. this.manager.input.handleLoaderMessage({
  238. data: {
  239. progress: 100,
  240. fileBuffer: this.dishBuffer
  241. }
  242. });
  243. } else {
  244. await this.getDishStr();
  245. this.app.setInput(this.dishStr);
  246. }
  247. }
  248. /**
  249. * Handler for undo switch click events.
  250. * Removes the output from the input and replaces the input that was removed.
  251. */
  252. undoSwitchClick() {
  253. this.app.setInput(this.switchOrigData);
  254. const undoSwitch = document.getElementById("undo-switch");
  255. undoSwitch.disabled = true;
  256. $(undoSwitch).tooltip("hide");
  257. }
  258. /**
  259. * Handler for maximise output click events.
  260. * Resizes the output frame to be as large as possible, or restores it to its original size.
  261. */
  262. maximiseOutputClick(e) {
  263. const el = e.target.id === "maximise-output" ? e.target : e.target.parentNode;
  264. if (el.getAttribute("data-original-title").indexOf("Maximise") === 0) {
  265. this.app.initialiseSplitter(true);
  266. this.app.columnSplitter.collapse(0);
  267. this.app.columnSplitter.collapse(1);
  268. this.app.ioSplitter.collapse(0);
  269. $(el).attr("data-original-title", "Restore output pane");
  270. el.querySelector("i").innerHTML = "fullscreen_exit";
  271. } else {
  272. $(el).attr("data-original-title", "Maximise output pane");
  273. el.querySelector("i").innerHTML = "fullscreen";
  274. this.app.initialiseSplitter(false);
  275. this.app.resetLayout();
  276. }
  277. }
  278. /**
  279. * Shows or hides the loading icon.
  280. *
  281. * @param {boolean} value
  282. */
  283. toggleLoader(value) {
  284. const outputLoader = document.getElementById("output-loader"),
  285. outputElement = document.getElementById("output-text");
  286. if (value) {
  287. this.manager.controls.hideStaleIndicator();
  288. this.bakingStatusTimeout = setTimeout(function() {
  289. outputElement.disabled = true;
  290. outputLoader.style.visibility = "visible";
  291. outputLoader.style.opacity = 1;
  292. this.manager.controls.toggleBakeButtonFunction(true);
  293. }.bind(this), 200);
  294. } else {
  295. clearTimeout(this.bakingStatusTimeout);
  296. outputElement.disabled = false;
  297. outputLoader.style.opacity = 0;
  298. outputLoader.style.visibility = "hidden";
  299. this.manager.controls.toggleBakeButtonFunction(false);
  300. this.setStatusMsg("");
  301. }
  302. }
  303. /**
  304. * Sets the baking status message value.
  305. *
  306. * @param {string} msg
  307. */
  308. setStatusMsg(msg) {
  309. const el = document.querySelector("#output-loader .loading-msg");
  310. el.textContent = msg;
  311. }
  312. /**
  313. * Returns true if the output contains carriage returns
  314. *
  315. * @returns {boolean}
  316. */
  317. async containsCR() {
  318. await this.getDishStr();
  319. return this.dishStr.indexOf("\r") >= 0;
  320. }
  321. /**
  322. * Retrieves the current dish as a string, returning the cached version if possible.
  323. *
  324. * @returns {string}
  325. */
  326. async getDishStr() {
  327. if (this.dishStr) return this.dishStr;
  328. this.dishStr = await new Promise(resolve => {
  329. this.manager.worker.getDishAs(this.app.dish, "string", r => {
  330. resolve(r.value);
  331. });
  332. });
  333. return this.dishStr;
  334. }
  335. /**
  336. * Retrieves the current dish as an ArrayBuffer, returning the cached version if possible.
  337. *
  338. * @returns {ArrayBuffer}
  339. */
  340. async getDishBuffer() {
  341. if (this.dishBuffer) return this.dishBuffer;
  342. this.dishBuffer = await new Promise(resolve => {
  343. this.manager.worker.getDishAs(this.app.dish, "ArrayBuffer", r => {
  344. resolve(r.value);
  345. });
  346. });
  347. return this.dishBuffer;
  348. }
  349. /**
  350. * Triggers the BackgroundWorker to attempt Magic on the current output.
  351. */
  352. backgroundMagic() {
  353. this.hideMagicButton();
  354. if (!this.app.options.autoMagic) return;
  355. const sample = this.dishStr ? this.dishStr.slice(0, 1000) :
  356. this.dishBuffer ? this.dishBuffer.slice(0, 1000) : "";
  357. if (sample.length) {
  358. this.manager.background.magic(sample);
  359. }
  360. }
  361. /**
  362. * Handles the results of a background Magic call.
  363. *
  364. * @param {Object[]} options
  365. */
  366. backgroundMagicResult(options) {
  367. if (!options.length ||
  368. !options[0].recipe.length)
  369. return;
  370. const currentRecipeConfig = this.app.getRecipeConfig();
  371. const newRecipeConfig = currentRecipeConfig.concat(options[0].recipe);
  372. const opSequence = options[0].recipe.map(o => o.op).join(", ");
  373. this.showMagicButton(opSequence, options[0].data, newRecipeConfig);
  374. }
  375. /**
  376. * Handler for Magic click events.
  377. *
  378. * Loads the Magic recipe.
  379. *
  380. * @fires Manager#statechange
  381. */
  382. magicClick() {
  383. const magicButton = document.getElementById("magic");
  384. this.app.setRecipeConfig(JSON.parse(magicButton.getAttribute("data-recipe")));
  385. window.dispatchEvent(this.manager.statechange);
  386. this.hideMagicButton();
  387. }
  388. /**
  389. * Displays the Magic button with a title and adds a link to a complete recipe.
  390. *
  391. * @param {string} opSequence
  392. * @param {string} result
  393. * @param {Object[]} recipeConfig
  394. */
  395. showMagicButton(opSequence, result, recipeConfig) {
  396. const magicButton = document.getElementById("magic");
  397. magicButton.setAttribute("data-original-title", `<i>${opSequence}</i> will produce <span class="data-text">"${Utils.escapeHtml(Utils.truncate(result), 30)}"</span>`);
  398. magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, "");
  399. magicButton.classList.remove("hidden");
  400. }
  401. /**
  402. * Hides the Magic button and resets its values.
  403. */
  404. hideMagicButton() {
  405. const magicButton = document.getElementById("magic");
  406. magicButton.classList.add("hidden");
  407. magicButton.setAttribute("data-recipe", "");
  408. magicButton.setAttribute("data-original-title", "Magic!");
  409. }
  410. /**
  411. * Handler for extract file events.
  412. *
  413. * @param {Event} e
  414. */
  415. async extractFileClick(e) {
  416. e.preventDefault();
  417. e.stopPropagation();
  418. const el = e.target.nodeName === "I" ? e.target.parentNode : e.target;
  419. const blobURL = el.getAttribute("blob-url");
  420. const fileName = el.getAttribute("file-name");
  421. const blob = await fetch(blobURL).then(r => r.blob());
  422. this.manager.input.loadFile(new File([blob], fileName, {type: blob.type}));
  423. }
  424. }
  425. export default OutputWaiter;