Merge remote-tracking branch 'upstream/master' into render-markdown

This commit is contained in:
j433866 2019-08-29 13:23:37 +01:00
commit 45fccb94e1
49 changed files with 834 additions and 772 deletions

View file

@ -167,6 +167,7 @@
"Parse IP range",
"Parse IPv6 address",
"Parse IPv4 header",
"Parse UDP",
"Parse SSH Host Key",
"Parse URI",
"URL Encode",

View file

@ -109,7 +109,7 @@ export function mean(data) {
*/
export function median(data) {
if ((data.length % 2) === 0 && data.length > 0) {
data.sort(function(a, b){
data.sort(function(a, b) {
return a.minus(b);
});
const first = data[Math.floor(data.length / 2)];

View file

@ -327,13 +327,13 @@ export function convertCoordinates (input, inFormat, inDelim, outFormat, outDeli
* @param {string} input - The input data to be split
* @returns {number[]} An array of the different items in the string, stored as floats
*/
function splitInput (input){
function splitInput (input) {
const split = [];
input.split(/\s+/).forEach(item => {
// Remove any character that isn't a digit, decimal point or negative sign
item = item.replace(/[^0-9.-]/g, "");
if (item.length > 0){
if (item.length > 0) {
// Turn the item into a float
split.push(parseFloat(item));
}
@ -350,7 +350,7 @@ function splitInput (input){
* @param {number} precision - The precision the result should be rounded to
* @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string)
*/
function convDMSToDD (degrees, minutes, seconds, precision){
function convDMSToDD (degrees, minutes, seconds, precision) {
const absDegrees = Math.abs(degrees);
let conv = absDegrees + (minutes / 60) + (seconds / 3600);
let outString = round(conv, precision) + "°";
@ -566,7 +566,7 @@ export function findFormat (input, delim) {
// Test DMS/DDM/DD formats
if (testData !== undefined) {
const split = splitInput(testData);
switch (split.length){
switch (split.length) {
case 3:
return "Degrees Minutes Seconds";
case 2:

View file

@ -241,7 +241,7 @@ export function ipv6ListedRange(match, includeNetworkInfo) {
ipv6List = ipv6List.filter(function(str) {
return str.trim();
});
for (let i =0; i < ipv6List.length; i++){
for (let i =0; i < ipv6List.length; i++) {
ipv6List[i] = ipv6List[i].trim();
}
const ipv6CidrList = ipv6List.filter(function(a) {
@ -502,8 +502,8 @@ export function ipv6Compare(a, b) {
const a_ = strToIpv6(a),
b_ = strToIpv6(b);
for (let i = 0; i < a_.length; i++){
if (a_[i] !== b_[i]){
for (let i = 0; i < a_.length; i++) {
if (a_[i] !== b_[i]) {
return a_[i] - b_[i];
}
}

View file

@ -85,7 +85,7 @@ function getWords(length=3) {
const words = [];
let word;
let previousWord;
while (words.length < length){
while (words.length < length) {
do {
word = wordList[Math.floor(Math.random() * wordList.length)];
} while (previousWord === word);

View file

@ -71,8 +71,8 @@ class AESDecrypt extends Operation {
* @throws {OperationError} if cannot decrypt input or invalid key length
*/
run(input, args) {
const key = Utils.convertToByteArray(args[0].string, args[0].option),
iv = Utils.convertToByteArray(args[1].string, args[1].option),
const key = Utils.convertToByteString(args[0].string, args[0].option),
iv = Utils.convertToByteString(args[1].string, args[1].option),
mode = args[2],
inputType = args[3],
outputType = args[4],

View file

@ -64,7 +64,7 @@ class BlurImage extends Operation {
throw new OperationError(`Error loading image. (${err})`);
}
try {
switch (blurType){
switch (blurType) {
case "Fast":
if (isWorkerEnvironment())
self.sendStatusMessage("Fast blurring image...");

View file

@ -29,12 +29,12 @@ class ChangeIPFormat extends Operation {
{
"name": "Input format",
"type": "option",
"value": ["Dotted Decimal", "Decimal", "Hex"]
"value": ["Dotted Decimal", "Decimal", "Octal", "Hex"]
},
{
"name": "Output format",
"type": "option",
"value": ["Dotted Decimal", "Decimal", "Hex"]
"value": ["Dotted Decimal", "Decimal", "Octal", "Hex"]
}
];
}
@ -54,7 +54,6 @@ class ChangeIPFormat extends Operation {
if (lines[i] === "") continue;
let baIp = [];
let octets;
let decimal;
if (inFormat === outFormat) {
output += lines[i] + "\n";
@ -70,11 +69,10 @@ class ChangeIPFormat extends Operation {
}
break;
case "Decimal":
decimal = lines[i].toString();
baIp.push(decimal >> 24 & 255);
baIp.push(decimal >> 16 & 255);
baIp.push(decimal >> 8 & 255);
baIp.push(decimal & 255);
baIp = this.fromNumber(lines[i].toString(), 10);
break;
case "Octal":
baIp = this.fromNumber(lines[i].toString(), 8);
break;
case "Hex":
baIp = fromHex(lines[i]);
@ -100,6 +98,10 @@ class ChangeIPFormat extends Operation {
decIp = ((baIp[0] << 24) | (baIp[1] << 16) | (baIp[2] << 8) | baIp[3]) >>> 0;
output += decIp.toString() + "\n";
break;
case "Octal":
decIp = ((baIp[0] << 24) | (baIp[1] << 16) | (baIp[2] << 8) | baIp[3]) >>> 0;
output += "0" + decIp.toString(8) + "\n";
break;
case "Hex":
hexIp = "";
for (j = 0; j < baIp.length; j++) {
@ -115,6 +117,22 @@ class ChangeIPFormat extends Operation {
return output.slice(0, output.length-1);
}
/**
* Constructs an array of IP address octets from a numerical value.
* @param {string} value The value of the IP address
* @param {number} radix The numeral system to be used
* @returns {number[]}
*/
fromNumber(value, radix) {
const decimal = parseInt(value, radix);
const baIp = [];
baIp.push(decimal >> 24 & 255);
baIp.push(decimal >> 16 & 255);
baIp.push(decimal >> 8 & 255);
baIp.push(decimal & 255);
return baIp;
}
}
export default ChangeIPFormat;

View file

@ -111,7 +111,7 @@ class DNSOverHTTPS extends Operation {
* @returns {JSON}
*/
function extractData(data) {
if (typeof(data) == "undefined"){
if (typeof(data) == "undefined") {
return [];
} else {
const dataValues = [];

View file

@ -20,7 +20,7 @@ class ExtractDomains extends Operation {
this.name = "Extract domains";
this.module = "Regex";
this.description = "Extracts domain names.<br>Note that this will not include paths. Use <strong>Extract URLs</strong> to find entire URLs.";
this.description = "Extracts fully qualified domain names.<br>Note that this will not include paths. Use <strong>Extract URLs</strong> to find entire URLs.";
this.inputType = "string";
this.outputType = "string";
this.args = [

View file

@ -58,7 +58,7 @@ class FlipImage extends Operation {
try {
if (isWorkerEnvironment())
self.sendStatusMessage("Flipping image...");
switch (flipAxis){
switch (flipAxis) {
case "Horizontal":
image.flip(true, false);
break;

View file

@ -47,7 +47,7 @@ class GenerateLoremIpsum extends Operation {
*/
run(input, args) {
const [length, lengthType] = args;
if (length < 1){
if (length < 1) {
throw new OperationError("Length must be greater than 0");
}
switch (lengthType) {

View file

@ -48,7 +48,7 @@ class ImageFilter extends Operation {
*/
async run(input, args) {
const [filterType] = args;
if (!isImage(new Uint8Array(input))){
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}

View file

@ -34,7 +34,7 @@ class MicrosoftScriptDecoder extends Operation {
run(input, args) {
const matcher = /#@~\^.{6}==(.+).{6}==\^#~@/;
const encodedData = matcher.exec(input);
if (encodedData){
if (encodedData) {
return MicrosoftScriptDecoder._decode(encodedData[1]);
} else {
return "";

View file

@ -134,7 +134,7 @@ CMYK: ${cmyk}
static _hslToRgb(h, s, l) {
let r, g, b;
if (s === 0){
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = function hue2rgb(p, q, t) {

View file

@ -0,0 +1,84 @@
/**
* @author h345983745 []
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import Stream from "../lib/Stream.mjs";
import {toHex} from "../lib/Hex.mjs";
import OperationError from "../errors/OperationError.mjs";
/**
* Parse UDP operation
*/
class ParseUDP extends Operation {
/**
* ParseUDP constructor
*/
constructor() {
super();
this.name = "Parse UDP";
this.module = "Default";
this.description = "Parses a UDP header and payload (if present).";
this.infoURL = "https://wikipedia.org/wiki/User_Datagram_Protocol";
this.inputType = "ArrayBuffer";
this.outputType = "json";
this.presentType = "html";
this.args = [];
}
/**
* @param {ArrayBuffer} input
* @returns {Object}
*/
run(input, args) {
if (input.byteLength < 8) {
throw new OperationError("Need 8 bytes for a UDP Header");
}
const s = new Stream(new Uint8Array(input));
// Parse Header
const UDPPacket = {
"Source port": s.readInt(2),
"Destination port": s.readInt(2),
"Length": s.readInt(2),
"Checksum": toHex(s.getBytes(2), "")
};
// Parse data if present
if (s.hasMore()) {
UDPPacket.Data = toHex(s.getBytes(UDPPacket.Length - 8), "");
}
return UDPPacket;
}
/**
* Displays the UDP Packet in a table style
* @param {Object} data
* @returns {html}
*/
present(data) {
const html = [];
html.push("<table class='table table-hover table-sm table-bordered table-nonfluid' style='table-layout: fixed'>");
html.push("<tr>");
html.push("<th>Field</th>");
html.push("<th>Value</th>");
html.push("</tr>");
for (const key in data) {
html.push("<tr>");
html.push("<td style=\"word-wrap:break-word\">" + key + "</td>");
html.push("<td>" + data[key] + "</td>");
html.push("</tr>");
}
html.push("</table>");
return html.join("");
}
}
export default ParseUDP;

View file

@ -41,7 +41,7 @@ class ScanForEmbeddedFiles extends Operation {
* @returns {string}
*/
run(input, args) {
let output = "Scanning data for 'magic bytes' which may indicate embedded files. The following results may be false positives and should not be treat as reliable. Any suffiently long file is likely to contain these magic bytes coincidentally.\n",
let output = "Scanning data for 'magic bytes' which may indicate embedded files. The following results may be false positives and should not be treat as reliable. Any sufficiently long file is likely to contain these magic bytes coincidentally.\n",
numFound = 0;
const categories = [],
data = new Uint8Array(input);

View file

@ -62,7 +62,7 @@ class SharpenImage extends Operation {
async run(input, args) {
const [radius, amount, threshold] = args;
if (!isImage(new Uint8Array(input))){
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}

View file

@ -79,7 +79,7 @@ class SwapEndianness extends Operation {
const word = data.slice(i, i + wordLength);
// Pad word if too short
if (padIncompleteWords && word.length < wordLength){
if (padIncompleteWords && word.length < wordLength) {
for (j = word.length; j < wordLength; j++) {
word.push(0);
}

View file

@ -51,7 +51,7 @@ class UNIXTimestampToWindowsFiletime extends Operation {
input = new BigNumber(input);
if (units === "Seconds (s)"){
if (units === "Seconds (s)") {
input = input.multipliedBy(new BigNumber("10000000"));
} else if (units === "Milliseconds (ms)") {
input = input.multipliedBy(new BigNumber("10000"));
@ -65,7 +65,7 @@ class UNIXTimestampToWindowsFiletime extends Operation {
input = input.plus(new BigNumber("116444736000000000"));
if (format === "Hex"){
if (format === "Hex") {
return input.toString(16);
} else {
return input.toFixed();

View file

@ -57,7 +57,7 @@ class WindowsFiletimeToUNIXTimestamp extends Operation {
input = input.minus(new BigNumber("116444736000000000"));
if (units === "Seconds (s)"){
if (units === "Seconds (s)") {
input = input.dividedBy(new BigNumber("10000000"));
} else if (units === "Milliseconds (ms)") {
input = input.dividedBy(new BigNumber("10000"));

View file

@ -295,7 +295,7 @@ export function help(input) {
* bake [Wrapped] - Perform an array of operations on some input.
* @returns {Function}
*/
export function bake(){
export function bake() {
/**
* bake

View file

@ -633,7 +633,7 @@ class App {
* Pops up a message to the user and writes it to the console log.
*
* @param {string} str - The message to display (HTML supported)
* @param {number} timeout - The number of milliseconds before the alert closes automatically
* @param {number} [timeout=0] - The number of milliseconds before the alert closes automatically
* 0 for never (until the user closes it)
* @param {boolean} [silent=false] - Don't show the message in the popup, only print it to the
* console
@ -646,14 +646,12 @@ class App {
* // Pops up a box with the message "Happy Christmas!" that will disappear after 5 seconds.
* this.alert("Happy Christmas!", 5000);
*/
alert(str, timeout, silent) {
alert(str, timeout=0, silent=false) {
const time = new Date();
log.info("[" + time.toLocaleString() + "] " + str);
if (silent) return;
timeout = timeout || 0;
this.currentSnackbar = $.snackbar({
content: str,
timeout: timeout,
@ -670,18 +668,22 @@ class App {
*
* @param {string} title - The title of the box
* @param {string} body - The question (HTML supported)
* @param {string} accept - The text of the accept button
* @param {string} reject - The text of the reject button
* @param {function} callback - A function accepting one boolean argument which handles the
* response e.g. function(answer) {...}
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Pops up a box asking if the user would like a cookie. Prints the answer to the console.
* this.confirm("Question", "Would you like a cookie?", function(answer) {console.log(answer);});
* this.confirm("Question", "Would you like a cookie?", "Yes", "No", function(answer) {console.log(answer);});
*/
confirm(title, body, callback, scope) {
confirm(title, body, accept, reject, callback, scope) {
scope = scope || this;
document.getElementById("confirm-title").innerHTML = title;
document.getElementById("confirm-body").innerHTML = body;
document.getElementById("confirm-yes").innerText = accept;
document.getElementById("confirm-no").innerText = reject;
document.getElementById("confirm-modal").style.display = "block";
this.confirmClosed = false;
@ -694,9 +696,14 @@ class App {
callback.bind(scope)(true);
$("#confirm-modal").modal("hide");
}.bind(this))
.one("click", "#confirm-no", function() {
this.confirmClosed = true;
callback.bind(scope)(false);
}.bind(this))
.one("hide.bs.modal", function(e) {
if (!this.confirmClosed)
callback.bind(scope)(false);
if (!this.confirmClosed) {
callback.bind(scope)(undefined);
}
this.confirmClosed = true;
}.bind(this));
}

View file

@ -224,7 +224,7 @@ class Manager {
document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options));
document.getElementById("reset-options").addEventListener("click", this.options.resetOptionsClick.bind(this.options));
this.addDynamicListener(".option-item input[type=checkbox]", "change", this.options.switchChange, this.options);
this.addDynamicListener(".option-item input[type=checkbox]", "change", this.options.setWordWrap, this.options);
this.addDynamicListener(".option-item input[type=checkbox]#wordWrap", "change", this.options.setWordWrap, this.options);
this.addDynamicListener(".option-item input[type=checkbox]#useMetaKey", "change", this.bindings.updateKeybList, this.bindings);
this.addDynamicListener(".option-item input[type=number]", "keyup", this.options.numberChange, this.options);
this.addDynamicListener(".option-item input[type=number]", "change", this.options.numberChange, this.options);

View file

@ -123,9 +123,9 @@
document.getElementById("preloader-error").innerHTML =
"CyberChef encountered an error while loading.<br><br>" +
"The following browser versions are supported:" +
"<ul><li>Google Chrome 40+</li><li>Mozilla Firefox 35+</li><li>Microsoft Edge 14+</li></ul>" +
"<ul><li>Google Chrome 50+</li><li>Mozilla Firefox 38+</li></ul>" +
"Your user agent is:<br>" + escapeHtml(navigator.userAgent) + "<br><br>" +
"If your browser is supported, please <a href='https://github.com/gchq/CyberChef/issues/new'>" +
"If your browser is supported, please <a href='https://github.com/gchq/CyberChef/issues/new/choose'>" +
"raise an issue</a> including the following details:<br><br>" +
"<pre>" + escapeHtml(msg) + "</pre>";
};
@ -563,16 +563,23 @@
<div class="checkbox option-item">
<label for="imagePreview">
<input type="checkbox" option="imagePreview" id="imagePreview">
Render a preview of the input if it's detected to be an image.
Render a preview of the input if it's detected to be an image
</label>
</div>
<div class="checkbox option-item">
<label for="syncTabs">
<input type="checkbox" option="syncTabs" id="syncTabs">
Keep the current tab in sync between the input and output.
</label>
</div>
<label for="syncTabs">
<input type="checkbox" option="syncTabs" id="syncTabs">
Keep the current tab in sync between the input and output
</label>
</div>
<div class="checkbox option-item">
<label for="preserveCR" data-toggle="tooltip" data-placement="right" data-html="true" title="As HTML textareas don't support carriage returns, editing input must be turned off to preserve them.<br><br>When this option is enabled, editing is disabled for pasted text that contains carriage returns. Otherwise, editing will remain enabled but carriage returns will not be preserved.">
<input type="checkbox" option="preserveCR" id="preserveCR">
Preserve carriage returns when pasting an input
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="reset-options">Reset options to default</button>
@ -702,7 +709,7 @@
<br>
<pre id="report-bug-info"></pre>
<br>
<a class="btn btn-primary" href="https://github.com/gchq/CyberChef/issues/new" role="button">Raise issue on GitHub</a>
<a class="btn btn-primary" href="https://github.com/gchq/CyberChef/issues/new/choose" role="button">Raise issue on GitHub</a>
</div>
<div role="tabpanel" class="tab-pane" id="about" style="padding: 20px;">
<h5><strong>What</strong></h5>

View file

@ -53,7 +53,9 @@ function main() {
logLevel: "info",
autoMagic: true,
imagePreview: true,
syncTabs: true
syncTabs: true,
preserveCR: true,
userSetCR: false
};
document.removeEventListener("DOMContentLoaded", main, false);

View file

@ -337,6 +337,29 @@
fill: var(--primary-font-colour);
}
.pulse {
box-shadow: 0 0 0 0 rgba(90, 153, 212, .3);
animation: pulse 1.5s 1;
}
.pulse:hover {
animation-play-state: paused;
}
@keyframes pulse {
0% {
transform: scale(1);
}
70% {
transform: scale(1.1);
box-shadow: 0 0 0 20px rgba(90, 153, 212, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(90, 153, 212, 0);
}
}
#input-find-options,
#output-find-options {
display: flex;

View file

@ -39,10 +39,16 @@ div#output {
.split {
box-sizing: border-box;
overflow: auto;
/* overflow: auto;
Removed to enable Background Magic button pulse to overflow.
Replace this rule if it seems to be causing problems. */
position: relative;
}
#operations {
overflow: auto;
}
.split.split-horizontal, .gutter.gutter-horizontal {
height: 100%;
float: left;

View file

@ -222,8 +222,6 @@ class InputWaiter {
if (Object.prototype.hasOwnProperty.call(r, "progress") &&
Object.prototype.hasOwnProperty.call(r, "inputNum")) {
this.manager.tabs.updateInputTabProgress(r.inputNum, r.progress, 100);
} else if (Object.prototype.hasOwnProperty.call(r, "fileBuffer")) {
this.manager.tabs.updateInputTabProgress(r.inputNum, 100, 100);
}
const transferable = Object.prototype.hasOwnProperty.call(r, "fileBuffer") ? [r.fileBuffer] : undefined;
@ -305,6 +303,9 @@ class InputWaiter {
case "removeChefWorker":
this.removeChefWorker();
break;
case "fileLoaded":
this.fileLoaded(r.data.inputNum);
break;
default:
log.error(`Unknown action ${r.action}.`);
}
@ -331,7 +332,7 @@ class InputWaiter {
* @param {number} inputData.size - The size in bytes of the input file
* @param {string} inputData.type - The MIME type of the input file
* @param {number} inputData.progress - The load progress of the input file
* @param {boolean} [silent=false] - If true, fires the manager statechange event
* @param {boolean} [silent=false] - If false, fires the manager statechange event
*/
async set(inputData, silent=false) {
return new Promise(function(resolve, reject) {
@ -373,7 +374,7 @@ class InputWaiter {
if (!silent) window.dispatchEvent(this.manager.statechange);
} else {
this.setFile(inputData);
this.setFile(inputData, silent);
}
}.bind(this));
@ -389,8 +390,9 @@ class InputWaiter {
* @param {number} inputData.size - The size in bytes of the input file
* @param {string} inputData.type - The MIME type of the input file
* @param {number} inputData.progress - The load progress of the input file
* @param {boolean} [silent=true] - If false, fires the manager statechange event
*/
setFile(inputData) {
setFile(inputData, silent=true) {
const activeTab = this.manager.tabs.getActiveInputTab();
if (inputData.inputNum !== activeTab) return;
@ -414,6 +416,30 @@ class InputWaiter {
this.setInputInfo(inputData.size, null);
this.displayFilePreview(inputData);
if (!silent) window.dispatchEvent(this.manager.statechange);
}
/**
* Update file details when a file completes loading
*
* @param {number} inputNum - The inputNum of the input which has finished loading
*/
fileLoaded(inputNum) {
this.manager.tabs.updateInputTabProgress(inputNum, 100, 100);
const activeTab = this.manager.tabs.getActiveInputTab();
if (activeTab !== inputNum) return;
this.inputWorker.postMessage({
action: "setInput",
data: {
inputNum: inputNum,
silent: false
}
});
this.updateFileProgress(inputNum, 100);
}
/**
@ -495,19 +521,6 @@ class InputWaiter {
fileLoaded.textContent = progress + "%";
fileLoaded.style.color = "";
}
if (progress === 100 && progress !== oldProgress) {
// Don't set the input if the progress hasn't changed
this.inputWorker.postMessage({
action: "setInput",
data: {
inputNum: inputNum,
silent: false
}
});
window.dispatchEvent(this.manager.statechange);
}
}
/**
@ -711,33 +724,50 @@ class InputWaiter {
*
* @param {event} e
*/
inputPaste(e) {
const pastedData = e.clipboardData.getData("Text");
if (pastedData.length < (this.app.options.ioDisplayThreshold * 1024)) {
// Pasting normally fires the inputChange() event before
// changing the value, so instead change it here ourselves
// and manually fire inputChange()
e.preventDefault();
const inputText = document.getElementById("input-text");
const selStart = inputText.selectionStart;
const selEnd = inputText.selectionEnd;
const startVal = inputText.value.slice(0, selStart);
const endVal = inputText.value.slice(selEnd);
inputText.value = startVal + pastedData + endVal;
inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length);
this.debounceInputChange(e);
} else {
e.preventDefault();
e.stopPropagation();
async inputPaste(e) {
e.preventDefault();
e.stopPropagation();
const self = this;
/**
* Triggers the input file/binary data overlay
*
* @param {string} pastedData
*/
function triggerOverlay(pastedData) {
const file = new File([pastedData], "PastedData", {
type: "text/plain",
lastModified: Date.now()
});
this.loadUIFiles([file]);
self.loadUIFiles([file]);
}
const pastedData = e.clipboardData.getData("Text");
const inputText = document.getElementById("input-text");
const selStart = inputText.selectionStart;
const selEnd = inputText.selectionEnd;
const startVal = inputText.value.slice(0, selStart);
const endVal = inputText.value.slice(selEnd);
const val = startVal + pastedData + endVal;
if (val.length >= (this.app.options.ioDisplayThreshold * 1024)) {
// Data too large to display, use overlay
triggerOverlay(val);
return false;
} else if (await this.preserveCarriageReturns(val)) {
// Data contains a carriage return and the user doesn't wish to edit it, use overlay
// We check this in a separate condition to make sure it is not run unless absolutely
// necessary.
triggerOverlay(val);
return false;
} else {
// Pasting normally fires the inputChange() event before
// changing the value, so instead change it here ourselves
// and manually fire inputChange()
inputText.value = val;
inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length);
this.debounceInputChange(e);
}
}
@ -815,6 +845,46 @@ class InputWaiter {
}
}
/**
* Checks if an input contains carriage returns.
* If a CR is detected, checks if the preserve CR option has been set,
* and if not, asks the user for their preference.
*
* @param {string} input - The input to be checked
* @returns {boolean} - If true, the input contains a CR which should be
* preserved, so display an overlay so it can't be edited
*/
async preserveCarriageReturns(input) {
if (input.indexOf("\r") < 0) return false;
const optionsStr = "This behaviour can be changed in the <a href='#' onclick='document.getElementById(\"options\").click()'>Options pane</a>";
if (!this.app.options.userSetCR) {
// User has not set a CR preference yet
let preserve = await new Promise(function(resolve, reject) {
this.app.confirm(
"Carriage Return Detected",
"A <a href='https://wikipedia.org/wiki/Carriage_return'>carriage return</a> (<code>\\r</code>, <code>0x0d</code>) was detected in your input. As HTML textareas <a href='https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element'>can't display carriage returns</a>, editing must be turned off to preserve them. <br>Alternatively, you can enable editing but your carriage returns will not be preserved.<br><br>This preference will be saved but can be toggled in the options pane.",
"Preserve Carriage Returns",
"Enable Editing", resolve, this);
}.bind(this));
if (preserve === undefined) {
// The confirm pane was closed without picking a specific choice
this.app.alert(`Not preserving carriage returns.\n${optionsStr}`, 5000);
preserve = false;
}
this.manager.options.updateOption("preserveCR", preserve);
this.manager.options.updateOption("userSetCR", true);
} else {
if (this.app.options.preserveCR) {
this.app.alert(`A carriage return (\\r, 0x0d) was detected in your input, so editing has been disabled to preserve it.<br>${optionsStr}`, 10000);
} else {
this.app.alert(`A carriage return (\\r, 0x0d) was detected in your input. Editing is remaining enabled, but carriage returns will not be preserved.<br>${optionsStr}`, 10000);
}
}
return this.app.options.preserveCR;
}
/**
* Load files from the UI into the inputWorker
*

View file

@ -1,174 +1,180 @@
/**
* Waiter to handle events related to the CyberChef options.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
*/
const OptionsWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
};
/**
* Loads options and sets values of switches and inputs to match them.
*
* @param {Object} options
* Waiter to handle events related to the CyberChef options.
*/
OptionsWaiter.prototype.load = function(options) {
for (const option in options) {
this.app.options[option] = options[option];
class OptionsWaiter {
/**
* OptionsWaiter constructor.
*
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
constructor(app, manager) {
this.app = app;
this.manager = manager;
}
// Set options to match object
const cboxes = document.querySelectorAll("#options-body input[type=checkbox]");
let i;
for (i = 0; i < cboxes.length; i++) {
cboxes[i].checked = this.app.options[cboxes[i].getAttribute("option")];
}
/**
* Loads options and sets values of switches and inputs to match them.
*
* @param {Object} options
*/
load(options) {
for (const option in options) {
this.app.options[option] = options[option];
}
const nboxes = document.querySelectorAll("#options-body input[type=number]");
for (i = 0; i < nboxes.length; i++) {
nboxes[i].value = this.app.options[nboxes[i].getAttribute("option")];
nboxes[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
// Set options to match object
const cboxes = document.querySelectorAll("#options-body input[type=checkbox]");
let i;
for (i = 0; i < cboxes.length; i++) {
cboxes[i].checked = this.app.options[cboxes[i].getAttribute("option")];
}
const selects = document.querySelectorAll("#options-body select");
for (i = 0; i < selects.length; i++) {
const val = this.app.options[selects[i].getAttribute("option")];
if (val) {
selects[i].value = val;
selects[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
} else {
selects[i].selectedIndex = 0;
const nboxes = document.querySelectorAll("#options-body input[type=number]");
for (i = 0; i < nboxes.length; i++) {
nboxes[i].value = this.app.options[nboxes[i].getAttribute("option")];
nboxes[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
const selects = document.querySelectorAll("#options-body select");
for (i = 0; i < selects.length; i++) {
const val = this.app.options[selects[i].getAttribute("option")];
if (val) {
selects[i].value = val;
selects[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
} else {
selects[i].selectedIndex = 0;
}
}
}
};
/**
* Handler for options click events.
* Dispays the options pane.
*
* @param {event} e
*/
OptionsWaiter.prototype.optionsClick = function(e) {
e.preventDefault();
$("#options-modal").modal();
};
/**
* Handler for reset options click events.
* Resets options back to their default values.
*/
OptionsWaiter.prototype.resetOptionsClick = function() {
this.load(this.app.doptions);
};
/**
* Handler for switch change events.
* Modifies the option state and saves it to local storage.
*
* @param {event} e
*/
OptionsWaiter.prototype.switchChange = function(e) {
const el = e.target;
const option = el.getAttribute("option");
const state = el.checked;
log.debug(`Setting ${option} to ${state}`);
this.app.options[option] = state;
if (this.app.isLocalStorageAvailable())
localStorage.setItem("options", JSON.stringify(this.app.options));
};
/**
* Handler for number change events.
* Modifies the option value and saves it to local storage.
*
* @param {event} e
*/
OptionsWaiter.prototype.numberChange = function(e) {
const el = e.target;
const option = el.getAttribute("option");
const val = parseInt(el.value, 10);
log.debug(`Setting ${option} to ${val}`);
this.app.options[option] = val;
if (this.app.isLocalStorageAvailable())
localStorage.setItem("options", JSON.stringify(this.app.options));
};
/**
* Handler for select change events.
* Modifies the option value and saves it to local storage.
*
* @param {event} e
*/
OptionsWaiter.prototype.selectChange = function(e) {
const el = e.target;
const option = el.getAttribute("option");
log.debug(`Setting ${option} to ${el.value}`);
this.app.options[option] = el.value;
if (this.app.isLocalStorageAvailable())
localStorage.setItem("options", JSON.stringify(this.app.options));
};
/**
* Sets or unsets word wrap on the input and output depending on the wordWrap option value.
*/
OptionsWaiter.prototype.setWordWrap = function() {
document.getElementById("input-text").classList.remove("word-wrap");
document.getElementById("output-text").classList.remove("word-wrap");
document.getElementById("output-html").classList.remove("word-wrap");
document.getElementById("input-highlighter").classList.remove("word-wrap");
document.getElementById("output-highlighter").classList.remove("word-wrap");
if (!this.app.options.wordWrap) {
document.getElementById("input-text").classList.add("word-wrap");
document.getElementById("output-text").classList.add("word-wrap");
document.getElementById("output-html").classList.add("word-wrap");
document.getElementById("input-highlighter").classList.add("word-wrap");
document.getElementById("output-highlighter").classList.add("word-wrap");
/**
* Handler for options click events.
* Dispays the options pane.
*
* @param {event} e
*/
optionsClick(e) {
e.preventDefault();
$("#options-modal").modal();
}
};
/**
* Changes the theme by setting the class of the <html> element.
*
* @param {Event} e
*/
OptionsWaiter.prototype.themeChange = function (e) {
const themeClass = e.target.value;
document.querySelector(":root").className = themeClass;
};
/**
* Handler for reset options click events.
* Resets options back to their default values.
*/
resetOptionsClick() {
this.load(this.app.doptions);
}
/**
* Changes the console logging level.
*
* @param {Event} e
*/
OptionsWaiter.prototype.logLevelChange = function (e) {
const level = e.target.value;
log.setLevel(level, false);
this.manager.worker.setLogLevel();
this.manager.input.setLogLevel();
};
/**
* Handler for switch change events.
*
* @param {event} e
*/
switchChange(e) {
const el = e.target;
const option = el.getAttribute("option");
const state = el.checked;
this.updateOption(option, state);
}
/**
* Handler for number change events.
*
* @param {event} e
*/
numberChange(e) {
const el = e.target;
const option = el.getAttribute("option");
const val = parseInt(el.value, 10);
this.updateOption(option, val);
}
/**
* Handler for select change events.
*
* @param {event} e
*/
selectChange(e) {
const el = e.target;
const option = el.getAttribute("option");
this.updateOption(option, el.value);
}
/**
* Modifies an option value and saves it to local storage.
*
* @param {string} option - The option to be updated
* @param {string|number|boolean} value - The new value of the option
*/
updateOption(option, value) {
log.debug(`Setting ${option} to ${value}`);
this.app.options[option] = value;
if (this.app.isLocalStorageAvailable())
localStorage.setItem("options", JSON.stringify(this.app.options));
}
/**
* Sets or unsets word wrap on the input and output depending on the wordWrap option value.
*/
setWordWrap() {
document.getElementById("input-text").classList.remove("word-wrap");
document.getElementById("output-text").classList.remove("word-wrap");
document.getElementById("output-html").classList.remove("word-wrap");
document.getElementById("input-highlighter").classList.remove("word-wrap");
document.getElementById("output-highlighter").classList.remove("word-wrap");
if (!this.app.options.wordWrap) {
document.getElementById("input-text").classList.add("word-wrap");
document.getElementById("output-text").classList.add("word-wrap");
document.getElementById("output-html").classList.add("word-wrap");
document.getElementById("input-highlighter").classList.add("word-wrap");
document.getElementById("output-highlighter").classList.add("word-wrap");
}
}
/**
* Changes the theme by setting the class of the <html> element.
*
* @param {Event} e
*/
themeChange(e) {
const themeClass = e.target.value;
document.querySelector(":root").className = themeClass;
}
/**
* Changes the console logging level.
*
* @param {Event} e
*/
logLevelChange(e) {
const level = e.target.value;
log.setLevel(level, false);
this.manager.worker.setLogLevel();
this.manager.input.setLogLevel();
}
}
export default OptionsWaiter;

View file

@ -217,6 +217,9 @@ class OutputWaiter {
*/
removeAllOutputs() {
this.outputs = {};
this.resetSwitch();
const tabsList = document.getElementById("output-tabs");
const tabsListChildren = tabsList.children;
@ -516,9 +519,10 @@ class OutputWaiter {
this.app.alert("Could not find any output data to download. Has this output been baked?", 3000);
return;
}
let fileName = window.prompt("Please enter a filename: ", "download.dat");
const fileName = window.prompt("Please enter a filename: ", "download.dat");
if (fileName === null) fileName = "download.dat";
// Assume if the user clicks cancel they don't want to download
if (fileName === null) return;
const data = await dish.get(Dish.ARRAY_BUFFER),
file = new File([data], fileName);
@ -529,12 +533,22 @@ class OutputWaiter {
* Handler for save all click event
* Saves all outputs to a single archvie file
*/
saveAllClick() {
async saveAllClick() {
const downloadButton = document.getElementById("save-all-to-file");
if (downloadButton.firstElementChild.innerHTML === "archive") {
this.downloadAllFiles();
} else if (window.confirm("Cancel zipping of outputs?")) {
this.terminateZipWorker();
} else {
const cancel = await new Promise(function(resolve, reject) {
this.app.confirm(
"Cancel zipping?",
"The outputs are currently being zipped for download.<br>Cancel zipping?",
"Continue zipping",
"Cancel zipping",
resolve, this);
}.bind(this));
if (!cancel) {
this.terminateZipWorker();
}
}
}
@ -544,57 +558,61 @@ class OutputWaiter {
* be zipped for download
*/
async downloadAllFiles() {
return new Promise(resolve => {
const inputNums = Object.keys(this.outputs);
for (let i = 0; i < inputNums.length; i++) {
const iNum = inputNums[i];
if (this.outputs[iNum].status !== "baked" ||
this.outputs[iNum].bakeId !== this.manager.worker.bakeId) {
if (window.confirm("Not all outputs have been baked yet. Continue downloading outputs?")) {
break;
} else {
return;
}
const inputNums = Object.keys(this.outputs);
for (let i = 0; i < inputNums.length; i++) {
const iNum = inputNums[i];
if (this.outputs[iNum].status !== "baked" ||
this.outputs[iNum].bakeId !== this.manager.worker.bakeId) {
const continueDownloading = await new Promise(function(resolve, reject) {
this.app.confirm(
"Incomplete outputs",
"Not all outputs have been baked yet. Continue downloading outputs?",
"Download", "Cancel", resolve, this);
}.bind(this));
if (continueDownloading) {
break;
} else {
return;
}
}
}
let fileName = window.prompt("Please enter a filename: ", "download.zip");
let fileName = window.prompt("Please enter a filename: ", "download.zip");
if (fileName === null || fileName === "") {
// Don't zip the files if there isn't a filename
this.app.alert("No filename was specified.", 3000);
return;
}
if (fileName === null || fileName === "") {
// Don't zip the files if there isn't a filename
this.app.alert("No filename was specified.", 3000);
return;
}
if (!fileName.match(/.zip$/)) {
fileName += ".zip";
}
if (!fileName.match(/.zip$/)) {
fileName += ".zip";
}
let fileExt = window.prompt("Please enter a file extension for the files, or leave blank to detect automatically.", "");
let fileExt = window.prompt("Please enter a file extension for the files, or leave blank to detect automatically.", "");
if (fileExt === null) fileExt = "";
if (fileExt === null) fileExt = "";
if (this.zipWorker !== null) {
this.terminateZipWorker();
}
if (this.zipWorker !== null) {
this.terminateZipWorker();
}
const downloadButton = document.getElementById("save-all-to-file");
const downloadButton = document.getElementById("save-all-to-file");
downloadButton.classList.add("spin");
downloadButton.title = `Zipping ${inputNums.length} files...`;
downloadButton.setAttribute("data-original-title", `Zipping ${inputNums.length} files...`);
downloadButton.classList.add("spin");
downloadButton.title = `Zipping ${inputNums.length} files...`;
downloadButton.setAttribute("data-original-title", `Zipping ${inputNums.length} files...`);
downloadButton.firstElementChild.innerHTML = "autorenew";
downloadButton.firstElementChild.innerHTML = "autorenew";
log.debug("Creating ZipWorker");
this.zipWorker = new ZipWorker();
this.zipWorker.postMessage({
outputs: this.outputs,
filename: fileName,
fileExtension: fileExt
});
this.zipWorker.addEventListener("message", this.handleZipWorkerMessage.bind(this));
log.debug("Creating ZipWorker");
this.zipWorker = new ZipWorker();
this.zipWorker.postMessage({
outputs: this.outputs,
filename: fileName,
fileExtension: fileExt
});
this.zipWorker.addEventListener("message", this.handleZipWorkerMessage.bind(this));
}
/**
@ -1065,6 +1083,7 @@ class OutputWaiter {
magicButton.setAttribute("data-original-title", `<i>${opSequence}</i> will produce <span class="data-text">"${Utils.escapeHtml(Utils.truncate(result), 30)}"</span>`);
magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, "");
magicButton.classList.remove("hidden");
magicButton.classList.add("pulse");
}
@ -1074,6 +1093,7 @@ class OutputWaiter {
hideMagicButton() {
const magicButton = document.getElementById("magic");
magicButton.classList.add("hidden");
magicButton.classList.remove("pulse");
magicButton.setAttribute("data-recipe", "");
magicButton.setAttribute("data-original-title", "Magic!");
}
@ -1213,14 +1233,39 @@ class OutputWaiter {
* Moves the current output into the input textarea.
*/
async switchClick() {
const active = await this.getDishBuffer(this.getOutputDish(this.manager.tabs.getActiveOutputTab()));
const activeTab = this.manager.tabs.getActiveOutputTab();
const transferable = [];
const switchButton = document.getElementById("switch");
switchButton.classList.add("spin");
switchButton.disabled = true;
switchButton.firstElementChild.innerHTML = "autorenew";
$(switchButton).tooltip("hide");
let active = await this.getDishBuffer(this.getOutputDish(activeTab));
if (!this.outputExists(activeTab)) {
this.resetSwitchButton();
return;
}
if (this.outputs[activeTab].data.type === "string" &&
active.byteLength <= this.app.options.ioDisplayThreshold * 1024) {
const dishString = await this.getDishStr(this.getOutputDish(activeTab));
if (!await this.manager.input.preserveCarriageReturns(dishString)) {
active = dishString;
}
} else {
transferable.push(active);
}
this.manager.input.inputWorker.postMessage({
action: "inputSwitch",
data: {
inputNum: this.manager.tabs.getActiveInputTab(),
inputNum: activeTab,
outputData: active
}
}, [active]);
}, transferable);
}
/**
@ -1238,6 +1283,9 @@ class OutputWaiter {
inputSwitch(switchData) {
this.switchOrigData = switchData;
document.getElementById("undo-switch").disabled = false;
this.resetSwitchButton();
}
/**
@ -1246,17 +1294,35 @@ class OutputWaiter {
*/
undoSwitchClick() {
this.manager.input.updateInputObj(this.switchOrigData.inputNum, this.switchOrigData.data);
this.manager.input.fileLoaded(this.switchOrigData.inputNum);
this.resetSwitch();
}
/**
* Removes the switch data and resets the switch buttons
*/
resetSwitch() {
if (this.switchOrigData !== undefined) {
delete this.switchOrigData;
}
const undoSwitch = document.getElementById("undo-switch");
undoSwitch.disabled = true;
$(undoSwitch).tooltip("hide");
this.manager.input.inputWorker.postMessage({
action: "setInput",
data: {
inputNum: this.switchOrigData.inputNum,
silent: false
}
});
this.resetSwitchButton();
}
/**
* Resets the switch button to its usual state
*/
resetSwitchButton() {
const switchButton = document.getElementById("switch");
switchButton.classList.remove("spin");
switchButton.disabled = false;
switchButton.firstElementChild.innerHTML = "open_in_browser";
}
/**

View file

@ -202,6 +202,7 @@ self.bakeInput = function(inputNum, bakeId) {
if (inputObj === null ||
inputObj === undefined ||
inputObj.status !== "loaded") {
self.postMessage({
action: "queueInputError",
data: {
@ -441,7 +442,7 @@ self.updateTabHeader = function(inputNum) {
*
* @param {object} inputData
* @param {number} inputData.inputNum - The input to get the data for
* @param {boolean} inputData.silent - If false, the manager statechange event won't be fired
* @param {boolean} inputData.silent - If false, the manager statechange event will be fired
*/
self.setInput = function(inputData) {
const inputNum = inputData.inputNum;
@ -590,7 +591,7 @@ self.updateInputObj = function(inputData) {
const inputNum = inputData.inputNum;
const data = inputData.data;
if (self.getInputObj(inputNum) === -1) return;
if (self.getInputObj(inputNum) === undefined) return;
self.inputs[inputNum].data = data;
};
@ -663,11 +664,19 @@ self.handleLoaderMessage = function(r) {
if ("fileBuffer" in r) {
log.debug(`Input file ${inputNum} loaded.`);
self.loadingInputs--;
self.updateInputValue({
inputNum: inputNum,
value: r.fileBuffer
});
self.postMessage({
action: "fileLoaded",
data: {
inputNum: inputNum
}
});
const idx = self.getLoaderWorkerIdx(r.id);
self.loadNextFile(idx);
} else if ("progress" in r) {
@ -782,7 +791,7 @@ self.loadFiles = function(filesData) {
}
self.getLoadProgress();
self.setInput({inputNum: activeTab, silent: false});
self.setInput({inputNum: activeTab, silent: true});
};
/**
@ -1025,7 +1034,7 @@ self.inputSwitch = function(switchData) {
const currentData = currentInput.data;
if (currentInput === undefined || currentInput === null) return;
if (typeof switchData.outputData === "object") {
if (typeof switchData.outputData !== "string") {
const output = new Uint8Array(switchData.outputData),
types = detectFileType(output);
let type = "unknown",
@ -1036,15 +1045,22 @@ self.inputSwitch = function(switchData) {
}
// ArrayBuffer
currentInput.data = {
fileBuffer: switchData.outputData,
name: `output.${ext}`,
size: switchData.outputData.byteLength.toLocaleString(),
type: type
};
self.updateInputObj({
inputNum: switchData.inputNum,
data: {
fileBuffer: switchData.outputData,
name: `output.${ext}`,
size: switchData.outputData.byteLength.toLocaleString(),
type: type
}
});
} else {
// String
currentInput.data = switchData.outputData;
self.updateInputValue({
inputNum: switchData.inputNum,
value: switchData.outputData,
force: true
});
}
self.postMessage({
@ -1055,6 +1071,11 @@ self.inputSwitch = function(switchData) {
}
});
self.setInput({inputNum: switchData.inputNum, silent: false});
self.postMessage({
action: "fileLoaded",
data: {
inputNum: switchData.inputNum
}
});
};