diff --git a/package-lock.json b/package-lock.json index f1eaf9c8..f8b73bdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,12 +113,12 @@ "@babel/plugin-syntax-import-assertions": "^7.28.6", "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.29.2", - "@babel/runtime": "^7.28.6", + "@babel/runtime": "^7.29.2", "@codemirror/commands": "^6.10.3", "@codemirror/language": "^6.12.2", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.5.4", - "@codemirror/view": "^6.39.17", + "@codemirror/view": "^6.40.0", "autoprefixer": "^10.4.27", "babel-loader": "^10.1.1", "base64-loader": "^1.0.0", @@ -127,7 +127,7 @@ "colors": "^1.4.0", "compression-webpack-plugin": "^11.1.0", "copy-webpack-plugin": "^13.0.1", - "core-js": "^3.48.0", + "core-js": "^3.49.0", "cspell": "^8.19.4", "css-loader": "7.1.4", "eslint": "^9.39.4", @@ -1755,9 +1755,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "dev": true, "license": "MIT", "engines": { @@ -1897,13 +1897,13 @@ } }, "node_modules/@codemirror/view": { - "version": "6.39.17", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.17.tgz", - "integrity": "sha512-Aim4lFqhbijnchl83RLfABWueSGs1oUCSv0mru91QdhpXQeNKprIdRO9LWA4cYkJvuYTKGJN7++9MXx8XW43ag==", + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz", + "integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==", "dev": true, "license": "MIT", "dependencies": { - "@codemirror/state": "^6.5.0", + "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" @@ -6820,9 +6820,9 @@ } }, "node_modules/core-js": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", - "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9634,9 +9634,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, diff --git a/package.json b/package.json index 2d40a586..2d4f9ff8 100644 --- a/package.json +++ b/package.json @@ -43,12 +43,12 @@ "@babel/plugin-syntax-import-assertions": "^7.28.6", "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.29.2", - "@babel/runtime": "^7.28.6", + "@babel/runtime": "^7.29.2", "@codemirror/commands": "^6.10.3", "@codemirror/language": "^6.12.2", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.5.4", - "@codemirror/view": "^6.39.17", + "@codemirror/view": "^6.40.0", "autoprefixer": "^10.4.27", "babel-loader": "^10.1.1", "base64-loader": "^1.0.0", @@ -57,7 +57,7 @@ "colors": "^1.4.0", "compression-webpack-plugin": "^11.1.0", "copy-webpack-plugin": "^13.0.1", - "core-js": "^3.48.0", + "core-js": "^3.49.0", "cspell": "^8.19.4", "css-loader": "7.1.4", "eslint": "^9.39.4", diff --git a/src/core/Chef.mjs b/src/core/Chef.mjs index ab8f83de..5be10868 100755 --- a/src/core/Chef.mjs +++ b/src/core/Chef.mjs @@ -55,8 +55,15 @@ class Chef { progress = await recipe.execute(this.dish, progress); } catch (err) { log.error(err); + + let displayStr; + if ("displayStr" in err) { + displayStr = err.displayStr; + } else { + displayStr = err.toString(); + } error = { - displayStr: err.displayStr, + displayStr: displayStr, }; progress = err.progress; } diff --git a/src/core/Ingredient.mjs b/src/core/Ingredient.mjs old mode 100755 new mode 100644 index 319dfb15..0dd31707 --- a/src/core/Ingredient.mjs +++ b/src/core/Ingredient.mjs @@ -5,7 +5,8 @@ */ import Utils from "./Utils.mjs"; -import {fromHex} from "./lib/Hex.mjs"; +import { fromHex } from "./lib/Hex.mjs"; +import OperationError from "./errors/OperationError.mjs"; /** * The arguments to operations. @@ -119,7 +120,9 @@ class Ingredient { number = parseFloat(data); if (isNaN(number)) { const sample = Utils.truncate(data.toString(), 10); - throw "Invalid ingredient value. Not a number: " + sample; + throw new OperationError( + "Invalid ingredient value. Not a number: " + sample, + ); } return number; default: diff --git a/src/core/Operation.mjs b/src/core/Operation.mjs index 24739d3f..09058766 100755 --- a/src/core/Operation.mjs +++ b/src/core/Operation.mjs @@ -5,6 +5,7 @@ */ import Dish from "./Dish.mjs"; +import OperationError from "./errors/OperationError.mjs"; import Ingredient from "./Ingredient.mjs"; /** @@ -223,7 +224,11 @@ class Operation { */ set ingValues(ingValues) { ingValues.forEach((val, i) => { - this._ingList[i].value = val; + try { + this._ingList[i].value = val; + } catch (err) { + throw new OperationError(`Failed to set value of ingredient '${this._ingList[i].name}': ${err}`); + } }); } diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs index 7824d1e8..b4a10e03 100755 --- a/src/core/Recipe.mjs +++ b/src/core/Recipe.mjs @@ -70,11 +70,15 @@ class Recipe { if (o instanceof Operation) { return o; } else { - const op = new modules[o.module][o.name](); - op.ingValues = o.ingValues; - op.breakpoint = o.breakpoint; - op.disabled = o.disabled; - return op; + try { + const op = new modules[o.module][o.name](); + op.ingValues = o.ingValues; + op.breakpoint = o.breakpoint; + op.disabled = o.disabled; + return op; + } catch (err) { + throw new Error(`Failed to hydrate operation '${o.name}': ${err}`); + } } }); } diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index f6fe1790..f9e0d684 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -249,6 +249,7 @@ "DNS over HTTPS", "Strip HTTP headers", "Dechunk HTTP response", + "Parse Ethernet frame", "Parse User Agent", "Parse IP range", "Parse IPv6 address", diff --git a/src/core/operations/Jq.mjs b/src/core/operations/Jq.mjs index c1e02b34..bc502957 100644 --- a/src/core/operations/Jq.mjs +++ b/src/core/operations/Jq.mjs @@ -30,7 +30,12 @@ class Jq extends Operation { name: "Query", type: "string", value: "" - } + }, + { + name: "Raw", + type: "boolean", + value: false + }, ]; } @@ -40,7 +45,7 @@ class Jq extends Operation { * @returns {string} */ run(input, args) { - const [query] = args; + const [query, raw] = args; let result; try { @@ -48,8 +53,11 @@ class Jq extends Operation { } catch (err) { throw new OperationError(`Invalid jq expression: ${err.message}`); } - - return JSON.stringify(result); + if (raw && typeof result === "string") { + return result; + } else { + return JSON.stringify(result); + } } } diff --git a/src/core/operations/ParseEthernetFrame.mjs b/src/core/operations/ParseEthernetFrame.mjs new file mode 100644 index 00000000..9dac5d57 --- /dev/null +++ b/src/core/operations/ParseEthernetFrame.mjs @@ -0,0 +1,115 @@ +/** + * @author tedk [tedk@ted.do] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import Utils from "../Utils.mjs"; +import {fromHex, toHex} from "../lib/Hex.mjs"; + +/** + * Parse Ethernet frame operation + */ +class ParseEthernetFrame extends Operation { + + /** + * ParseEthernetFrame constructor + */ + constructor() { + super(); + + this.name = "Parse Ethernet frame"; + this.module = "Default"; + this.description = "Parses an Ethernet frame and either shows the deduced values (Source and destination MAC, VLANs) or returns the packet data.

Good for use in conjunction with the Parse IPv4, and Parse TCP/UDP recipes."; + this.infoURL = "https://en.wikipedia.org/wiki/Ethernet_frame#Frame_%E2%80%93_data_link_layer"; + this.inputType = "string"; + this.outputType = "html"; + this.args = [ + { + name: "Input type", + type: "option", + value: [ + "Raw", "Hex" + ], + defaultIndex: 0, + }, + { + name: "Return type", + type: "option", + value: [ + "Text output", "Packet data", "Packet data (hex)", + ], + defaultIndex: 0, + } + ]; + } + + + /** + * @param {string} input + * @param {Object[]} args + * @returns {html} + */ + run(input, args) { + const format = args[0]; + const outputFormat = args[1]; + + if (format === "Hex") { + input = fromHex(input); + } else if (format === "Raw") { + input = new Uint8Array(Utils.strToArrayBuffer(input)); + } else { + throw new OperationError("Invalid input format selected."); + } + + const destinationMac = input.slice(0, 6); + const sourceMac = input.slice(6, 12); + + let offset = 12; + const vlans = []; + + while (offset < input.length) { + const ethType = Utils.byteArrayToChars(input.slice(offset, offset+2)); + offset += 2; + + + if (ethType === "\x08\x00") { + break; + } else if (ethType === "\x81\x00" || ethType === "\x88\xA8") { + // Parse the VLAN tag: + // [0000] 0000 0000 0000 + // ^^^ PRIO - Ignored + // ^ DEI - Ignored + // ^^^^ ^^^^ ^^^^ VLAN ID + const vlanTag = input.slice(offset+2, offset+4); + vlans.push((vlanTag[0] & 0b00001111) << 4 | vlanTag[1]); + + offset += 2; + } else { + break; + } + } + + const packetData = input.slice(offset); + + if (outputFormat === "Packet data") { + return Utils.byteArrayToChars(packetData); + } else if (outputFormat === "Packet data (hex)") { + return toHex(packetData); + } else if (outputFormat === "Text output") { + let retval = `Source MAC: ${toHex(sourceMac, ":")}\nDestination MAC: ${toHex(destinationMac, ":")}\n`; + if (vlans.length > 0) { + retval += `VLAN: ${vlans.join(", ")}\n`; + } + retval += `Data:\n${toHex(packetData)}`; + return retval; + } + + } + + +} + +export default ParseEthernetFrame; diff --git a/src/core/operations/ParseIPv4Header.mjs b/src/core/operations/ParseIPv4Header.mjs index 84351cdc..4eed5d46 100644 --- a/src/core/operations/ParseIPv4Header.mjs +++ b/src/core/operations/ParseIPv4Header.mjs @@ -33,6 +33,12 @@ class ParseIPv4Header extends Operation { "name": "Input format", "type": "option", "value": ["Hex", "Raw"] + }, + { + "name": "Output format", + "type": "option", + "value": ["Table", "Data (hex)", "Data (raw)"], + defaultIndex: 0, } ]; } @@ -44,6 +50,8 @@ class ParseIPv4Header extends Operation { */ run(input, args) { const format = args[0]; + const outputFormat = args[1]; + let output; if (format === "Hex") { @@ -98,7 +106,10 @@ class ParseIPv4Header extends Operation { checksumResult = givenChecksum + " (incorrect, should be " + correctChecksum + ")"; } - output = ` + const data = input.slice(ihl * 4); + + if (outputFormat === "Table") { + output = `
FieldValue
@@ -116,13 +127,19 @@ class ParseIPv4Header extends Operation { -`; + +`; - if (ihl > 5) { - output += ``; + if (ihl > 5) { + output += ``; + } + + return output + "
FieldValue
Version${version}
Internet Header Length (IHL)${ihl} (${ihl * 4} bytes)
Differentiated Services Code Point (DSCP)${dscp}
Protocol${protocol}, ${protocolInfo.protocol} (${protocolInfo.keyword})
Header checksum${checksumResult}
Source IP address${ipv4ToStr(srcIP)}
Destination IP address${ipv4ToStr(dstIP)}
Destination IP address${ipv4ToStr(dstIP)}
Data (hex)${toHex(data)}
Options${toHex(options)}
Options${toHex(options)}
"; + } else if (outputFormat === "Data (hex)") { + return toHex(data); + } else if (outputFormat === "Data (raw)") { + return Utils.byteArrayToChars(data); } - - return output + ""; } } diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index ae972a59..7cde638d 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -130,6 +130,7 @@ class Manager { // Controls document.getElementById("bake").addEventListener("click", this.controls.bakeClick.bind(this.controls)); document.getElementById("auto-bake").addEventListener("change", this.controls.autoBakeChange.bind(this.controls)); + document.getElementById("auto-bake").addEventListener("keydown", this.controls.autoBakeKeyboardHandler.bind(this.controls)); document.getElementById("step").addEventListener("click", this.controls.stepClick.bind(this.controls)); document.getElementById("clr-recipe").addEventListener("click", this.controls.clearRecipeClick.bind(this.controls)); document.getElementById("save").addEventListener("click", this.controls.saveClick.bind(this.controls)); diff --git a/src/web/html/index.html b/src/web/html/index.html index 38bf7ccc..2d4de4bd 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -145,7 +145,7 @@ -
+
@@ -584,22 +584,22 @@