mirror of
https://github.com/gchq/CyberChef
synced 2024-12-26 04:23:10 +00:00
Added new 'pretty' recipe format to make URLs more readable
This commit is contained in:
parent
2a4c9afdf2
commit
cf1ba60a10
7 changed files with 175 additions and 11 deletions
|
@ -843,6 +843,139 @@ const Utils = {
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a URI fragment (#) or query (?) using a minimal amount of percent-encoding.
|
||||||
|
*
|
||||||
|
* RFC 3986 defines legal characters for the fragment and query parts of a URL to be as follows:
|
||||||
|
*
|
||||||
|
* fragment = *( pchar / "/" / "?" )
|
||||||
|
* query = *( pchar / "/" / "?" )
|
||||||
|
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
||||||
|
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||||
|
* pct-encoded = "%" HEXDIG HEXDIG
|
||||||
|
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
||||||
|
* / "*" / "+" / "," / ";" / "="
|
||||||
|
*
|
||||||
|
* Meaning that the list of characters that need not be percent-encoded are alphanumeric plus:
|
||||||
|
* -._~!$&'()*+,;=:@/?
|
||||||
|
*
|
||||||
|
* & and = are still escaped as they are used to serialise the key-value pairs in CyberChef
|
||||||
|
* fragments. + is also escaped so as to prevent it being decoded to a space.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
encodeURIFragment: function(str) {
|
||||||
|
const LEGAL_CHARS = {
|
||||||
|
"%2D": "-",
|
||||||
|
"%2E": ".",
|
||||||
|
"%5F": "_",
|
||||||
|
"%7E": "~",
|
||||||
|
"%21": "!",
|
||||||
|
"%24": "$",
|
||||||
|
//"%26": "&",
|
||||||
|
"%27": "'",
|
||||||
|
"%28": "(",
|
||||||
|
"%29": ")",
|
||||||
|
"%2A": "*",
|
||||||
|
//"%2B": "+",
|
||||||
|
"%2C": ",",
|
||||||
|
"%3B": ";",
|
||||||
|
//"%3D": "=",
|
||||||
|
"%3A": ":",
|
||||||
|
"%40": "@",
|
||||||
|
"%2F": "/",
|
||||||
|
"%3F": "?"
|
||||||
|
};
|
||||||
|
str = encodeURIComponent(str);
|
||||||
|
|
||||||
|
return str.replace(/%[0-9A-F]{2}/g, function (match) {
|
||||||
|
return LEGAL_CHARS[match] || match;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a "pretty" recipe format from a recipeConfig object.
|
||||||
|
*
|
||||||
|
* "Pretty" CyberChef recipe formats are designed to be included in the fragment (#) or query (?)
|
||||||
|
* parts of the URL. They can also be loaded into CyberChef through the 'Load' interface. In order
|
||||||
|
* to make this format as readable as possible, various special characters are used unescaped. This
|
||||||
|
* reduces the amount of percent-encoding included in the URL which is typically difficult to read,
|
||||||
|
* as well as substantially increasing the overall length. These characteristics can be quite
|
||||||
|
* offputting for users.
|
||||||
|
*
|
||||||
|
* @param {Object[]} recipeConfig
|
||||||
|
* @param {boolean} newline - whether to add a newline after each operation
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
generatePrettyRecipe: function(recipeConfig, newline) {
|
||||||
|
let prettyConfig = "",
|
||||||
|
name = "",
|
||||||
|
args = "",
|
||||||
|
disabled = "",
|
||||||
|
bp = "";
|
||||||
|
|
||||||
|
recipeConfig.forEach(op => {
|
||||||
|
name = op.op.replace(/ /g, "_");
|
||||||
|
args = JSON.stringify(op.args)
|
||||||
|
.slice(1, -1) // Remove [ and ] as they are implied
|
||||||
|
// We now need to switch double-quoted (") strings to single-quotes (') as these do not
|
||||||
|
// need to be percent-encoded.
|
||||||
|
.replace(/'/g, "\\'") // Escape single quotes
|
||||||
|
.replace(/\\"/g, '"') // Unescape double quotes
|
||||||
|
.replace(/(^|,)"/g, "$1'") // Replace opening " with '
|
||||||
|
.replace(/"(,|$)/g, "'$1"); // Replace closing " with '
|
||||||
|
|
||||||
|
disabled = op.disabled ? "/disabled": "";
|
||||||
|
bp = op.breakpoint ? "/breakpoint" : "";
|
||||||
|
prettyConfig += `${name}(${args}${disabled}${bp})`;
|
||||||
|
if (newline) prettyConfig += "\n";
|
||||||
|
});
|
||||||
|
return prettyConfig;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a recipe string to the JSON representation of the recipe.
|
||||||
|
* Accepts either stringified JSON or bespoke "pretty" recipe format.
|
||||||
|
*
|
||||||
|
* @param {string} recipe
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
|
parseRecipeConfig: function(recipe) {
|
||||||
|
recipe = recipe.trim();
|
||||||
|
if (recipe.length === 0) return [];
|
||||||
|
if (recipe[0] === "[") return JSON.parse(recipe);
|
||||||
|
|
||||||
|
// Parse bespoke recipe format
|
||||||
|
recipe = recipe.replace(/\n/g, "");
|
||||||
|
let m,
|
||||||
|
recipeRegex = /([^(]+)\(((?:'[^'\\]*(?:\\.[^'\\]*)*'|[^)/])*)(\/[^)]+)?\)/g,
|
||||||
|
recipeConfig = [],
|
||||||
|
args;
|
||||||
|
|
||||||
|
while ((m = recipeRegex.exec(recipe))) {
|
||||||
|
// Translate strings in args back to double-quotes
|
||||||
|
args = m[2]
|
||||||
|
.replace(/"/g, '\\"') // Escape double quotes
|
||||||
|
.replace(/(^|,)'/g, '$1"') // Replace opening ' with "
|
||||||
|
.replace(/([^\\])'(,|$)/g, '$1"$2') // Replace closing ' with "
|
||||||
|
.replace(/\\'/g, "'"); // Unescape single quotes
|
||||||
|
args = "[" + args + "]";
|
||||||
|
|
||||||
|
let op = {
|
||||||
|
op: m[1].replace(/_/g, " "),
|
||||||
|
args: JSON.parse(args)
|
||||||
|
};
|
||||||
|
if (m[3] && m[3].indexOf("disabled") > 0) op.disabled = true;
|
||||||
|
if (m[3] && m[3].indexOf("breakpoint") > 0) op.breakpoint = true;
|
||||||
|
recipeConfig.push(op);
|
||||||
|
}
|
||||||
|
return recipeConfig;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expresses a number of milliseconds in a human readable format.
|
* Expresses a number of milliseconds in a human readable format.
|
||||||
*
|
*
|
||||||
|
|
|
@ -411,7 +411,7 @@ App.prototype.loadURIParams = function() {
|
||||||
// Read in recipe from URI params
|
// Read in recipe from URI params
|
||||||
if (this.uriParams.recipe) {
|
if (this.uriParams.recipe) {
|
||||||
try {
|
try {
|
||||||
const recipeConfig = JSON.parse(this.uriParams.recipe);
|
const recipeConfig = Utils.parseRecipeConfig(this.uriParams.recipe);
|
||||||
this.setRecipeConfig(recipeConfig);
|
this.setRecipeConfig(recipeConfig);
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
} else if (this.uriParams.op) {
|
} else if (this.uriParams.op) {
|
||||||
|
|
|
@ -170,7 +170,7 @@ ControlsWaiter.prototype.generateStateUrl = function(includeRecipe, includeInput
|
||||||
const link = baseURL || window.location.protocol + "//" +
|
const link = baseURL || window.location.protocol + "//" +
|
||||||
window.location.host +
|
window.location.host +
|
||||||
window.location.pathname;
|
window.location.pathname;
|
||||||
const recipeStr = JSON.stringify(recipeConfig);
|
const recipeStr = Utils.generatePrettyRecipe(recipeConfig);
|
||||||
const inputStr = Utils.toBase64(this.app.getInput(), "A-Za-z0-9+/"); // B64 alphabet with no padding
|
const inputStr = Utils.toBase64(this.app.getInput(), "A-Za-z0-9+/"); // B64 alphabet with no padding
|
||||||
|
|
||||||
includeRecipe = includeRecipe && (recipeConfig.length > 0);
|
includeRecipe = includeRecipe && (recipeConfig.length > 0);
|
||||||
|
@ -184,7 +184,7 @@ ControlsWaiter.prototype.generateStateUrl = function(includeRecipe, includeInput
|
||||||
|
|
||||||
const hash = params
|
const hash = params
|
||||||
.filter(v => v)
|
.filter(v => v)
|
||||||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
.map(([key, value]) => `${key}=${Utils.encodeURIFragment(value)}`)
|
||||||
.join("&");
|
.join("&");
|
||||||
|
|
||||||
if (hash) {
|
if (hash) {
|
||||||
|
@ -198,9 +198,9 @@ ControlsWaiter.prototype.generateStateUrl = function(includeRecipe, includeInput
|
||||||
/**
|
/**
|
||||||
* Handler for changes made to the save dialog text area. Re-initialises the save link.
|
* Handler for changes made to the save dialog text area. Re-initialises the save link.
|
||||||
*/
|
*/
|
||||||
ControlsWaiter.prototype.saveTextChange = function() {
|
ControlsWaiter.prototype.saveTextChange = function(e) {
|
||||||
try {
|
try {
|
||||||
const recipeConfig = JSON.parse(document.getElementById("save-text").value);
|
const recipeConfig = Utils.parseRecipeConfig(e.target.value);
|
||||||
this.initialiseSaveLink(recipeConfig);
|
this.initialiseSaveLink(recipeConfig);
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
};
|
};
|
||||||
|
@ -211,9 +211,16 @@ ControlsWaiter.prototype.saveTextChange = function() {
|
||||||
*/
|
*/
|
||||||
ControlsWaiter.prototype.saveClick = function() {
|
ControlsWaiter.prototype.saveClick = function() {
|
||||||
const recipeConfig = this.app.getRecipeConfig();
|
const recipeConfig = this.app.getRecipeConfig();
|
||||||
const recipeStr = JSON.stringify(recipeConfig).replace(/},{/g, "},\n{");
|
const recipeStr = JSON.stringify(recipeConfig);
|
||||||
|
|
||||||
document.getElementById("save-text").value = recipeStr;
|
document.getElementById("save-text-chef").value = Utils.generatePrettyRecipe(recipeConfig, true);
|
||||||
|
document.getElementById("save-text-clean").value = JSON.stringify(recipeConfig, null, 2)
|
||||||
|
.replace(/{\n\s+"/g, "{ \"")
|
||||||
|
.replace(/\[\n\s{3,}/g, "[")
|
||||||
|
.replace(/\n\s{3,}]/g, "]")
|
||||||
|
.replace(/\s*\n\s*}/g, " }")
|
||||||
|
.replace(/\n\s{6,}/g, " ");
|
||||||
|
document.getElementById("save-text-compact").value = recipeStr;
|
||||||
|
|
||||||
this.initialiseSaveLink(recipeConfig);
|
this.initialiseSaveLink(recipeConfig);
|
||||||
$("#save-modal").modal();
|
$("#save-modal").modal();
|
||||||
|
@ -339,7 +346,7 @@ ControlsWaiter.prototype.loadNameChange = function(e) {
|
||||||
*/
|
*/
|
||||||
ControlsWaiter.prototype.loadButtonClick = function() {
|
ControlsWaiter.prototype.loadButtonClick = function() {
|
||||||
try {
|
try {
|
||||||
const recipeConfig = JSON.parse(document.getElementById("load-text").value);
|
const recipeConfig = Utils.parseRecipeConfig(document.getElementById("load-text").value);
|
||||||
this.app.setRecipeConfig(recipeConfig);
|
this.app.setRecipeConfig(recipeConfig);
|
||||||
|
|
||||||
$("#rec-list [data-toggle=popover]").popover();
|
$("#rec-list [data-toggle=popover]").popover();
|
||||||
|
|
|
@ -102,7 +102,7 @@ Manager.prototype.initialiseEventListeners = function() {
|
||||||
document.getElementById("load-name").addEventListener("change", this.controls.loadNameChange.bind(this.controls));
|
document.getElementById("load-name").addEventListener("change", this.controls.loadNameChange.bind(this.controls));
|
||||||
document.getElementById("load-button").addEventListener("click", this.controls.loadButtonClick.bind(this.controls));
|
document.getElementById("load-button").addEventListener("click", this.controls.loadButtonClick.bind(this.controls));
|
||||||
document.getElementById("support").addEventListener("click", this.controls.supportButtonClick.bind(this.controls));
|
document.getElementById("support").addEventListener("click", this.controls.supportButtonClick.bind(this.controls));
|
||||||
this.addMultiEventListener("#save-text", "keyup paste", this.controls.saveTextChange, this.controls);
|
this.addMultiEventListeners("#save-texts textarea", "keyup paste", this.controls.saveTextChange, this.controls);
|
||||||
|
|
||||||
// Operations
|
// Operations
|
||||||
this.addMultiEventListener("#search", "keyup paste search", this.ops.searchOperations, this.ops);
|
this.addMultiEventListener("#search", "keyup paste search", this.ops.searchOperations, this.ops);
|
||||||
|
|
|
@ -295,6 +295,9 @@ RecipeWaiter.prototype.getConfig = function() {
|
||||||
option: ingList[j].previousSibling.children[0].textContent.slice(0, -1),
|
option: ingList[j].previousSibling.children[0].textContent.slice(0, -1),
|
||||||
string: ingList[j].value
|
string: ingList[j].value
|
||||||
};
|
};
|
||||||
|
} else if (ingList[j].getAttribute("type") === "number") {
|
||||||
|
// number
|
||||||
|
ingredients[j] = parseFloat(ingList[j].value, 10);
|
||||||
} else {
|
} else {
|
||||||
// all others
|
// all others
|
||||||
ingredients[j] = ingList[j].value;
|
ingredients[j] = ingList[j].value;
|
||||||
|
|
|
@ -215,7 +215,22 @@
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="save-text">Save your recipe to local storage or copy the following string to load later</label>
|
<label for="save-text">Save your recipe to local storage or copy the following string to load later</label>
|
||||||
<textarea class="form-control" id="save-text" rows="5"></textarea>
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
|
<li role="presentation" class="active"><a href="#chef-format" role="tab" data-toggle="tab">Chef format</a></li>
|
||||||
|
<li role="presentation"><a href="#clean-json" role="tab" data-toggle="tab">Clean JSON</a></li>
|
||||||
|
<li role="presentation"><a href="#compact-json" role="tab" data-toggle="tab">Compact JSON</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content" id="save-texts">
|
||||||
|
<div role="tabpanel" class="tab-pane active" id="chef-format">
|
||||||
|
<textarea class="form-control" id="save-text-chef" rows="5"></textarea>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="clean-json">
|
||||||
|
<textarea class="form-control" id="save-text-clean" rows="5"></textarea>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="compact-json">
|
||||||
|
<textarea class="form-control" id="save-text-compact" rows="5"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="save-name">Recipe name</label>
|
<label for="save-name">Recipe name</label>
|
||||||
|
|
|
@ -78,7 +78,13 @@
|
||||||
font-family: var(--primary-font-family);
|
font-family: var(--primary-font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
#save-text,
|
#save-texts textarea,
|
||||||
#load-text {
|
#load-text {
|
||||||
font-family: var(--fixed-width-font-family);
|
font-family: var(--fixed-width-font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#save-texts textarea {
|
||||||
|
border-top: none;
|
||||||
|
box-shadow: none;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue