Explorar el Código

Added 'Parse TCP' operation

n1474335 hace 3 años
padre
commit
a895d1d82a

+ 2 - 1
Gruntfile.js

@@ -217,7 +217,8 @@ module.exports = function (grunt) {
                     client: {
                     client: {
                         logging: "error",
                         logging: "error",
                         overlay: true
                         overlay: true
-                    }
+                    },
+                    hot: "only"
                 },
                 },
                 plugins: [
                 plugins: [
                     new webpack.DefinePlugin(BUILD_CONSTANTS),
                     new webpack.DefinePlugin(BUILD_CONSTANTS),

+ 7 - 7
package-lock.json

@@ -98,7 +98,7 @@
         "autoprefixer": "^10.4.4",
         "autoprefixer": "^10.4.4",
         "babel-loader": "^8.2.4",
         "babel-loader": "^8.2.4",
         "babel-plugin-dynamic-import-node": "^2.3.3",
         "babel-plugin-dynamic-import-node": "^2.3.3",
-        "chromedriver": "^99.0.0",
+        "chromedriver": "^101.0.0",
         "cli-progress": "^3.10.0",
         "cli-progress": "^3.10.0",
         "colors": "^1.4.0",
         "colors": "^1.4.0",
         "copy-webpack-plugin": "^10.2.4",
         "copy-webpack-plugin": "^10.2.4",
@@ -4467,9 +4467,9 @@
       }
       }
     },
     },
     "node_modules/chromedriver": {
     "node_modules/chromedriver": {
-      "version": "99.0.0",
-      "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-99.0.0.tgz",
-      "integrity": "sha512-pyB+5LuyZdb7EBPL3i5D5yucZUD+SlkdiUtmpjaEnLd9zAXp+SvD/hP5xF4l/ZmWvUo/1ZLxAI1YBdhazGTpgA==",
+      "version": "101.0.0",
+      "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-101.0.0.tgz",
+      "integrity": "sha512-LkkWxy6KM/0YdJS8qBeg5vfkTZTRamhBfOttb4oic4echDgWvCU1E8QcBbUBOHqZpSrYMyi7WMKmKMhXFUaZ+w==",
       "dev": true,
       "dev": true,
       "hasInstallScript": true,
       "hasInstallScript": true,
       "dependencies": {
       "dependencies": {
@@ -19171,9 +19171,9 @@
       "dev": true
       "dev": true
     },
     },
     "chromedriver": {
     "chromedriver": {
-      "version": "99.0.0",
-      "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-99.0.0.tgz",
-      "integrity": "sha512-pyB+5LuyZdb7EBPL3i5D5yucZUD+SlkdiUtmpjaEnLd9zAXp+SvD/hP5xF4l/ZmWvUo/1ZLxAI1YBdhazGTpgA==",
+      "version": "101.0.0",
+      "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-101.0.0.tgz",
+      "integrity": "sha512-LkkWxy6KM/0YdJS8qBeg5vfkTZTRamhBfOttb4oic4echDgWvCU1E8QcBbUBOHqZpSrYMyi7WMKmKMhXFUaZ+w==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
         "@testim/chrome-version": "^1.1.2",
         "@testim/chrome-version": "^1.1.2",

+ 1 - 1
package.json

@@ -48,7 +48,7 @@
     "autoprefixer": "^10.4.4",
     "autoprefixer": "^10.4.4",
     "babel-loader": "^8.2.4",
     "babel-loader": "^8.2.4",
     "babel-plugin-dynamic-import-node": "^2.3.3",
     "babel-plugin-dynamic-import-node": "^2.3.3",
-    "chromedriver": "^99.0.0",
+    "chromedriver": "^101.0.0",
     "cli-progress": "^3.10.0",
     "cli-progress": "^3.10.0",
     "colors": "^1.4.0",
     "colors": "^1.4.0",
     "copy-webpack-plugin": "^10.2.4",
     "copy-webpack-plugin": "^10.2.4",

+ 1 - 0
src/core/config/Categories.json

@@ -190,6 +190,7 @@
             "Parse IP range",
             "Parse IP range",
             "Parse IPv6 address",
             "Parse IPv6 address",
             "Parse IPv4 header",
             "Parse IPv4 header",
+            "Parse TCP",
             "Parse UDP",
             "Parse UDP",
             "Parse SSH Host Key",
             "Parse SSH Host Key",
             "Parse URI",
             "Parse URI",

+ 9 - 5
src/core/lib/Binary.mjs

@@ -13,7 +13,7 @@ import OperationError from "../errors/OperationError.mjs";
 /**
 /**
  * Convert a byte array into a binary string.
  * Convert a byte array into a binary string.
  *
  *
- * @param {Uint8Array|byteArray} data
+ * @param {Uint8Array|byteArray|number} data
  * @param {string} [delim="Space"]
  * @param {string} [delim="Space"]
  * @param {number} [padding=8]
  * @param {number} [padding=8]
  * @returns {string}
  * @returns {string}
@@ -26,13 +26,17 @@ import OperationError from "../errors/OperationError.mjs";
  * toBinary([10,20,30], ":");
  * toBinary([10,20,30], ":");
  */
  */
 export function toBinary(data, delim="Space", padding=8) {
 export function toBinary(data, delim="Space", padding=8) {
-    if (!data) return "";
-
     delim = Utils.charRep(delim);
     delim = Utils.charRep(delim);
     let output = "";
     let output = "";
 
 
-    for (let i = 0; i < data.length; i++) {
-        output += data[i].toString(2).padStart(padding, "0") + delim;
+    if (data.length) { // array
+        for (let i = 0; i < data.length; i++) {
+            output += data[i].toString(2).padStart(padding, "0") + delim;
+        }
+    } else if (typeof data === "number") { // Single value
+        return data.toString(2).padStart(padding, "0");
+    } else {
+        return "";
     }
     }
 
 
     if (delim.length) {
     if (delim.length) {

+ 12 - 12
src/core/lib/FileSignatures.mjs

@@ -3778,8 +3778,8 @@ function parseDEFLATE(stream) {
 
 
     while (!finalBlock) {
     while (!finalBlock) {
         // Read header
         // Read header
-        finalBlock = stream.readBits(1);
-        const blockType = stream.readBits(2);
+        finalBlock = stream.readBits(1, "le");
+        const blockType = stream.readBits(2, "le");
 
 
         if (blockType === 0) {
         if (blockType === 0) {
             /* No compression */
             /* No compression */
@@ -3798,16 +3798,16 @@ function parseDEFLATE(stream) {
             /* Dynamic Huffman */
             /* Dynamic Huffman */
 
 
             // Read the number of liternal and length codes
             // Read the number of liternal and length codes
-            const hlit = stream.readBits(5) + 257;
+            const hlit = stream.readBits(5, "le") + 257;
             // Read the number of distance codes
             // Read the number of distance codes
-            const hdist = stream.readBits(5) + 1;
+            const hdist = stream.readBits(5, "le") + 1;
             // Read the number of code lengths
             // Read the number of code lengths
-            const hclen = stream.readBits(4) + 4;
+            const hclen = stream.readBits(4, "le") + 4;
 
 
             // Parse code lengths
             // Parse code lengths
             const codeLengths = new Uint8Array(huffmanOrder.length);
             const codeLengths = new Uint8Array(huffmanOrder.length);
             for (let i = 0; i < hclen; i++) {
             for (let i = 0; i < hclen; i++) {
-                codeLengths[huffmanOrder[i]] = stream.readBits(3);
+                codeLengths[huffmanOrder[i]] = stream.readBits(3, "le");
             }
             }
 
 
             // Parse length table
             // Parse length table
@@ -3819,16 +3819,16 @@ function parseDEFLATE(stream) {
                 code = readHuffmanCode(stream, codeLengthsTable);
                 code = readHuffmanCode(stream, codeLengthsTable);
                 switch (code) {
                 switch (code) {
                     case 16:
                     case 16:
-                        repeat = 3 + stream.readBits(2);
+                        repeat = 3 + stream.readBits(2, "le");
                         while (repeat--) lengthTable[i++] = prev;
                         while (repeat--) lengthTable[i++] = prev;
                         break;
                         break;
                     case 17:
                     case 17:
-                        repeat = 3 + stream.readBits(3);
+                        repeat = 3 + stream.readBits(3, "le");
                         while (repeat--) lengthTable[i++] = 0;
                         while (repeat--) lengthTable[i++] = 0;
                         prev = 0;
                         prev = 0;
                         break;
                         break;
                     case 18:
                     case 18:
-                        repeat = 11 + stream.readBits(7);
+                        repeat = 11 + stream.readBits(7, "le");
                         while (repeat--) lengthTable[i++] = 0;
                         while (repeat--) lengthTable[i++] = 0;
                         prev = 0;
                         prev = 0;
                         break;
                         break;
@@ -3886,11 +3886,11 @@ function parseHuffmanBlock(stream, litTab, distTab) {
         if (code < 256) continue;
         if (code < 256) continue;
 
 
         // Length code
         // Length code
-        stream.readBits(lengthExtraTable[code - 257]);
+        stream.readBits(lengthExtraTable[code - 257], "le");
 
 
         // Dist code
         // Dist code
         code = readHuffmanCode(stream, distTab);
         code = readHuffmanCode(stream, distTab);
-        stream.readBits(distanceExtraTable[code]);
+        stream.readBits(distanceExtraTable[code], "le");
     }
     }
 }
 }
 
 
@@ -3948,7 +3948,7 @@ function readHuffmanCode(stream, table) {
     const [codeTable, maxCodeLength] = table;
     const [codeTable, maxCodeLength] = table;
 
 
     // Read max length
     // Read max length
-    const bitsBuf = stream.readBits(maxCodeLength);
+    const bitsBuf = stream.readBits(maxCodeLength, "le");
     const codeWithLength = codeTable[bitsBuf & ((1 << maxCodeLength) - 1)];
     const codeWithLength = codeTable[bitsBuf & ((1 << maxCodeLength) - 1)];
     const codeLength = codeWithLength >>> 16;
     const codeLength = codeWithLength >>> 16;
 
 

+ 47 - 0
src/core/lib/Protocol.mjs

@@ -0,0 +1,47 @@
+/**
+ * Protocol parsing functions.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2022
+ * @license Apache-2.0
+ */
+
+import BigNumber from "bignumber.js";
+import {toHexFast} from "../lib/Hex.mjs";
+
+/**
+ * Recursively displays a JSON object as an HTML table
+ *
+ * @param {Object} obj
+ * @returns string
+ */
+export function objToTable(obj, nested=false) {
+    let html = `<table
+        class='table table-sm table-nonfluid ${nested ? "mb-0 table-borderless" : "table-bordered"}'
+        style='table-layout: fixed; ${nested ? "margin: -1px !important;" : ""}'>`;
+    if (!nested)
+        html += `<tr>
+            <th>Field</th>
+            <th>Value</th>
+        </tr>`;
+
+    for (const key in obj) {
+        html += `<tr><td style='word-wrap: break-word'>${key}</td>`;
+        if (typeof obj[key] === "object")
+            html += `<td style='padding: 0'>${objToTable(obj[key], true)}</td>`;
+        else
+            html += `<td>${obj[key]}</td>`;
+        html += "</tr>";
+    }
+    html += "</table>";
+    return html;
+}
+
+/**
+ * Converts bytes into a BigNumber string
+ * @param {Uint8Array} bs
+ * @returns {string}
+ */
+export function bytesToLargeNumber(bs) {
+    return BigNumber(toHexFast(bs), 16).toString();
+}

+ 22 - 12
src/core/lib/Stream.mjs

@@ -27,15 +27,17 @@ export default class Stream {
     }
     }
 
 
     /**
     /**
-     * Get a number of bytes from the current position.
+     * Get a number of bytes from the current position, or all remaining bytes.
      *
      *
-     * @param {number} numBytes
+     * @param {number} [numBytes=null]
      * @returns {Uint8Array}
      * @returns {Uint8Array}
      */
      */
-    getBytes(numBytes) {
+    getBytes(numBytes=null) {
         if (this.position > this.length) return undefined;
         if (this.position > this.length) return undefined;
 
 
-        const newPosition = this.position + numBytes;
+        const newPosition = numBytes !== null ?
+            this.position + numBytes :
+            this.length;
         const bytes = this.bytes.slice(this.position, newPosition);
         const bytes = this.bytes.slice(this.position, newPosition);
         this.position = newPosition;
         this.position = newPosition;
         this.bitPos = 0;
         this.bitPos = 0;
@@ -91,34 +93,40 @@ export default class Stream {
     }
     }
 
 
     /**
     /**
-     * Reads a number of bits from the buffer.
-     *
-     * @TODO Add endianness
+     * Reads a number of bits from the buffer in big or little endian.
      *
      *
      * @param {number} numBits
      * @param {number} numBits
+     * @param {string} [endianness="be"]
      * @returns {number}
      * @returns {number}
      */
      */
-    readBits(numBits) {
+    readBits(numBits, endianness="be") {
         if (this.position > this.length) return undefined;
         if (this.position > this.length) return undefined;
 
 
         let bitBuf = 0,
         let bitBuf = 0,
             bitBufLen = 0;
             bitBufLen = 0;
 
 
         // Add remaining bits from current byte
         // Add remaining bits from current byte
-        bitBuf = (this.bytes[this.position++] & bitMask(this.bitPos)) >>> this.bitPos;
+        bitBuf = this.bytes[this.position++] & bitMask(this.bitPos);
+        if (endianness !== "be") bitBuf >>>= this.bitPos;
         bitBufLen = 8 - this.bitPos;
         bitBufLen = 8 - this.bitPos;
         this.bitPos = 0;
         this.bitPos = 0;
 
 
         // Not enough bits yet
         // Not enough bits yet
         while (bitBufLen < numBits) {
         while (bitBufLen < numBits) {
-            bitBuf |= this.bytes[this.position++] << bitBufLen;
+            if (endianness === "be")
+                bitBuf = (bitBuf << bitBufLen) | this.bytes[this.position++];
+            else
+                bitBuf |= this.bytes[this.position++] << bitBufLen;
             bitBufLen += 8;
             bitBufLen += 8;
         }
         }
 
 
         // Reverse back to numBits
         // Reverse back to numBits
         if (bitBufLen > numBits) {
         if (bitBufLen > numBits) {
             const excess = bitBufLen - numBits;
             const excess = bitBufLen - numBits;
-            bitBuf &= (1 << numBits) - 1;
+            if (endianness === "be")
+                bitBuf >>>= excess;
+            else
+                bitBuf &= (1 << numBits) - 1;
             bitBufLen -= excess;
             bitBufLen -= excess;
             this.position--;
             this.position--;
             this.bitPos = 8 - excess;
             this.bitPos = 8 - excess;
@@ -133,7 +141,9 @@ export default class Stream {
          * @returns {number} The bit mask
          * @returns {number} The bit mask
          */
          */
         function bitMask(bitPos) {
         function bitMask(bitPos) {
-            return 256 - (1 << bitPos);
+            return endianness === "be" ?
+                (1 << (8 - bitPos)) - 1 :
+                256 - (1 << bitPos);
         }
         }
     }
     }
 
 

+ 245 - 0
src/core/operations/ParseTCP.mjs

@@ -0,0 +1,245 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2022
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import Stream from "../lib/Stream.mjs";
+import {toHexFast, fromHex} from "../lib/Hex.mjs";
+import {toBinary} from "../lib/Binary.mjs";
+import {objToTable, bytesToLargeNumber} from "../lib/Protocol.mjs";
+import Utils from "../Utils.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import BigNumber from "bignumber.js";
+
+/**
+ * Parse TCP operation
+ */
+class ParseTCP extends Operation {
+
+    /**
+     * ParseTCP constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Parse TCP";
+        this.module = "Default";
+        this.description = "Parses a TCP header and payload (if present).";
+        this.infoURL = "https://wikipedia.org/wiki/Transmission_Control_Protocol";
+        this.inputType = "string";
+        this.outputType = "json";
+        this.presentType = "html";
+        this.args = [
+            {
+                name: "Input format",
+                type: "option",
+                value: ["Hex", "Raw"]
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {html}
+     */
+    run(input, args) {
+        const format = args[0];
+
+        if (format === "Hex") {
+            input = fromHex(input);
+        } else if (format === "Raw") {
+            input = Utils.strToArrayBuffer(input);
+        } else {
+            throw new OperationError("Unrecognised input format.");
+        }
+
+        const s = new Stream(new Uint8Array(input));
+        if (s.length < 20) {
+            throw new OperationError("Need at least 20 bytes for a TCP Header");
+        }
+
+        // Parse Header
+        const TCPPacket = {
+            "Source port": s.readInt(2),
+            "Destination port": s.readInt(2),
+            "Sequence number": bytesToLargeNumber(s.getBytes(4)),
+            "Acknowledgement number": s.readInt(4),
+            "Data offset": s.readBits(4),
+            "Flags": {
+                "Reserved": toBinary(s.readBits(3), "", 3),
+                "NS": s.readBits(1),
+                "CWR": s.readBits(1),
+                "ECE": s.readBits(1),
+                "URG": s.readBits(1),
+                "ACK": s.readBits(1),
+                "PSH": s.readBits(1),
+                "RST": s.readBits(1),
+                "SYN": s.readBits(1),
+                "FIN": s.readBits(1),
+            },
+            "Window size": s.readInt(2),
+            "Checksum": "0x" + toHexFast(s.getBytes(2)),
+            "Urgent pointer": "0x" + toHexFast(s.getBytes(2))
+        };
+
+        // Parse options if present
+        let windowScaleShift = 0;
+        if (TCPPacket["Data offset"] > 5) {
+            let remainingLength = TCPPacket["Data offset"] * 4 - 20;
+
+            const options = {};
+            while (remainingLength > 0) {
+                const option = {
+                    "Kind": s.readInt(1)
+                };
+
+                let opt = { name: "Reserved", length: true };
+                if (Object.prototype.hasOwnProperty.call(TCP_OPTION_KIND_LOOKUP, option.Kind)) {
+                    opt = TCP_OPTION_KIND_LOOKUP[option.Kind];
+                }
+
+                // Add Length and Value fields
+                if (opt.length) {
+                    option.Length = s.readInt(1);
+
+                    if (option.Length > 2) {
+                        if (Object.prototype.hasOwnProperty.call(opt, "parser")) {
+                            option.Value = opt.parser(s.getBytes(option.Length - 2));
+                        } else {
+                            option.Value = option.Length <= 6 ?
+                                s.readInt(option.Length - 2):
+                                "0x" + toHexFast(s.getBytes(option.Length - 2));
+                        }
+
+                        // Store Window Scale shift for later
+                        if (option.Kind === 3 && option.Value) {
+                            windowScaleShift = option.Value["Shift count"];
+                        }
+                    }
+                }
+                options[opt.name] = option;
+
+                const length = option.Length || 1;
+                remainingLength -= length;
+            }
+            TCPPacket.Options = options;
+        }
+
+        if (s.hasMore()) {
+            TCPPacket.Data = "0x" + toHexFast(s.getBytes());
+        }
+
+        // Improve values
+        TCPPacket["Data offset"] = `${TCPPacket["Data offset"]} (${TCPPacket["Data offset"] * 4} bytes)`;
+        const trueWndSize = BigNumber(TCPPacket["Window size"]).multipliedBy(BigNumber(2).pow(BigNumber(windowScaleShift)));
+        TCPPacket["Window size"] = `${TCPPacket["Window size"]} (Scaled: ${trueWndSize})`;
+
+        return TCPPacket;
+    }
+
+    /**
+     * Displays the TCP Packet in a tabular style
+     * @param {Object} data
+     * @returns {html}
+     */
+    present(data) {
+        return objToTable(data);
+    }
+
+}
+
+// Taken from https://www.iana.org/assignments/tcp-parameters/tcp-parameters.xhtml
+// on 2022-05-30
+const TCP_OPTION_KIND_LOOKUP = {
+    0: { name: "End of Option List", length: false },
+    1: { name: "No-Operation", length: false },
+    2: { name: "Maximum Segment Size", length: true },
+    3: { name: "Window Scale", length: true, parser: windowScaleParser },
+    4: { name: "SACK Permitted", length: true },
+    5: { name: "SACK", length: true },
+    6: { name: "Echo (obsoleted by option 8)", length: true },
+    7: { name: "Echo Reply (obsoleted by option 8)", length: true },
+    8: { name: "Timestamps", length: true, parser: tcpTimestampParser },
+    9: { name: "Partial Order Connection Permitted (obsolete)", length: true },
+    10: { name: "Partial Order Service Profile (obsolete)", length: true },
+    11: { name: "CC (obsolete)", length: true },
+    12: { name: "CC.NEW (obsolete)", length: true },
+    13: { name: "CC.ECHO (obsolete)", length: true },
+    14: { name: "TCP Alternate Checksum Request (obsolete)", length: true, parser: tcpAlternateChecksumParser },
+    15: { name: "TCP Alternate Checksum Data (obsolete)", length: true },
+    16: { name: "Skeeter", length: true },
+    17: { name: "Bubba", length: true },
+    18: { name: "Trailer Checksum Option", length: true },
+    19: { name: "MD5 Signature Option (obsoleted by option 29)", length: true },
+    20: { name: "SCPS Capabilities", length: true },
+    21: { name: "Selective Negative Acknowledgements", length: true },
+    22: { name: "Record Boundaries", length: true },
+    23: { name: "Corruption experienced", length: true },
+    24: { name: "SNAP", length: true },
+    25: { name: "Unassigned (released 2000-12-18)", length: true },
+    26: { name: "TCP Compression Filter", length: true },
+    27: { name: "Quick-Start Response", length: true },
+    28: { name: "User Timeout Option (also, other known unauthorized use)", length: true },
+    29: { name: "TCP Authentication Option (TCP-AO)", length: true },
+    30: { name: "Multipath TCP (MPTCP)", length: true },
+    69: { name: "Encryption Negotiation (TCP-ENO)", length: true },
+    70: { name: "Reserved (known unauthorized use without proper IANA assignment)", length: true },
+    76: { name: "Reserved (known unauthorized use without proper IANA assignment)", length: true },
+    77: { name: "Reserved (known unauthorized use without proper IANA assignment)", length: true },
+    78: { name: "Reserved (known unauthorized use without proper IANA assignment)", length: true },
+    253: { name: "RFC3692-style Experiment 1 (also improperly used for shipping products) ", length: true },
+    254: { name: "RFC3692-style Experiment 2 (also improperly used for shipping products) ", length: true }
+};
+
+/**
+ * Parses the TCP Alternate Checksum Request field
+ * @param {Uint8Array} data
+ */
+function tcpAlternateChecksumParser(data) {
+    const lookup = {
+        0: "TCP Checksum",
+        1: "8-bit Fletchers's algorithm",
+        2: "16-bit Fletchers's algorithm",
+        3: "Redundant Checksum Avoidance"
+    }[data[0]];
+
+    return `${lookup} (0x${toHexFast(data)})`;
+}
+
+/**
+ * Parses the TCP Timestamp field
+ * @param {Uint8Array} data
+ */
+function tcpTimestampParser(data) {
+    const s = new Stream(data);
+
+    if (s.length !== 8)
+        return `Error: Timestamp field should be 8 bytes long (received 0x${toHexFast(data)})`;
+
+    const tsval = bytesToLargeNumber(s.getBytes(4)),
+        tsecr = bytesToLargeNumber(s.getBytes(4));
+
+    return {
+        "Current Timestamp": tsval,
+        "Echo Reply": tsecr
+    };
+}
+
+/**
+ * Parses the Window Scale field
+ * @param {Uint8Array} data
+ */
+function windowScaleParser(data) {
+    if (data.length !== 1)
+        return `Error: Window Scale should be one byte long (received 0x${toHexFast(data)})`;
+
+    return {
+        "Shift count": data[0],
+        "Multiplier": 1 << data[0]
+    };
+}
+
+export default ParseTCP;

+ 29 - 24
src/core/operations/ParseUDP.mjs

@@ -6,7 +6,9 @@
 
 
 import Operation from "../Operation.mjs";
 import Operation from "../Operation.mjs";
 import Stream from "../lib/Stream.mjs";
 import Stream from "../lib/Stream.mjs";
-import {toHex} from "../lib/Hex.mjs";
+import {toHexFast, fromHex} from "../lib/Hex.mjs";
+import {objToTable} from "../lib/Protocol.mjs";
+import Utils from "../Utils.mjs";
 import OperationError from "../errors/OperationError.mjs";
 import OperationError from "../errors/OperationError.mjs";
 
 
 /**
 /**
@@ -24,58 +26,61 @@ class ParseUDP extends Operation {
         this.module = "Default";
         this.module = "Default";
         this.description = "Parses a UDP header and payload (if present).";
         this.description = "Parses a UDP header and payload (if present).";
         this.infoURL = "https://wikipedia.org/wiki/User_Datagram_Protocol";
         this.infoURL = "https://wikipedia.org/wiki/User_Datagram_Protocol";
-        this.inputType = "ArrayBuffer";
+        this.inputType = "string";
         this.outputType = "json";
         this.outputType = "json";
         this.presentType = "html";
         this.presentType = "html";
-        this.args = [];
+        this.args = [
+            {
+                name: "Input format",
+                type: "option",
+                value: ["Hex", "Raw"]
+            }
+        ];
     }
     }
 
 
     /**
     /**
-     * @param {ArrayBuffer} input
+     * @param {string} input
+     * @param {Object[]} args
      * @returns {Object}
      * @returns {Object}
      */
      */
     run(input, args) {
     run(input, args) {
-        if (input.byteLength < 8) {
-            throw new OperationError("Need 8 bytes for a UDP Header");
+        const format = args[0];
+
+        if (format === "Hex") {
+            input = fromHex(input);
+        } else if (format === "Raw") {
+            input = Utils.strToArrayBuffer(input);
+        } else {
+            throw new OperationError("Unrecognised input format.");
         }
         }
 
 
         const s = new Stream(new Uint8Array(input));
         const s = new Stream(new Uint8Array(input));
+        if (s.length < 8) {
+            throw new OperationError("Need 8 bytes for a UDP Header");
+        }
+
         // Parse Header
         // Parse Header
         const UDPPacket = {
         const UDPPacket = {
             "Source port": s.readInt(2),
             "Source port": s.readInt(2),
             "Destination port": s.readInt(2),
             "Destination port": s.readInt(2),
             "Length": s.readInt(2),
             "Length": s.readInt(2),
-            "Checksum": toHex(s.getBytes(2), "")
+            "Checksum": "0x" + toHexFast(s.getBytes(2))
         };
         };
         // Parse data if present
         // Parse data if present
         if (s.hasMore()) {
         if (s.hasMore()) {
-            UDPPacket.Data = toHex(s.getBytes(UDPPacket.Length - 8), "");
+            UDPPacket.Data = "0x" + toHexFast(s.getBytes(UDPPacket.Length - 8));
         }
         }
 
 
         return UDPPacket;
         return UDPPacket;
     }
     }
 
 
     /**
     /**
-     * Displays the UDP Packet in a table style
+     * Displays the UDP Packet in a tabular style
      * @param {Object} data
      * @param {Object} data
      * @returns {html}
      * @returns {html}
      */
      */
     present(data) {
     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("");
+        return objToTable(data);
     }
     }
 
 
 }
 }

+ 5 - 1
src/web/waiters/OperationsWaiter.mjs

@@ -109,11 +109,15 @@ class OperationsWaiter {
         const matchedOps = [];
         const matchedOps = [];
         const matchedDescs = [];
         const matchedDescs = [];
 
 
+        // Create version with no whitespace for the fuzzy match
+        // Helps avoid missing matches e.g. query "TCP " would not find "Parse TCP"
+        const inStrNWS = inStr.replace(/\s/g, "");
+
         for (const opName in this.app.operations) {
         for (const opName in this.app.operations) {
             const op = this.app.operations[opName];
             const op = this.app.operations[opName];
 
 
             // Match op name using fuzzy match
             // Match op name using fuzzy match
-            const [nameMatch, score, idxs] = fuzzyMatch(inStr, opName);
+            const [nameMatch, score, idxs] = fuzzyMatch(inStrNWS, opName);
 
 
             // Match description based on exact match
             // Match description based on exact match
             const descPos = op.description.toLowerCase().indexOf(inStr.toLowerCase());
             const descPos = op.description.toLowerCase().indexOf(inStr.toLowerCase());

+ 1 - 0
tests/operations/index.mjs

@@ -96,6 +96,7 @@ import "./tests/Protobuf.mjs";
 import "./tests/ParseSSHHostKey.mjs";
 import "./tests/ParseSSHHostKey.mjs";
 import "./tests/DefangIP.mjs";
 import "./tests/DefangIP.mjs";
 import "./tests/ParseUDP.mjs";
 import "./tests/ParseUDP.mjs";
+import "./tests/ParseTCP.mjs";
 import "./tests/AvroToJSON.mjs";
 import "./tests/AvroToJSON.mjs";
 import "./tests/Lorenz.mjs";
 import "./tests/Lorenz.mjs";
 import "./tests/LuhnChecksum.mjs";
 import "./tests/LuhnChecksum.mjs";

+ 44 - 0
tests/operations/tests/ParseTCP.mjs

@@ -0,0 +1,44 @@
+/**
+ * Parse TCP tests.
+ *
+ * @author n1474335
+ * @copyright Crown Copyright 2022
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+    {
+        name: "Parse TCP: No options",
+        input: "c2eb0050a138132e70dc9fb9501804025ea70000",
+        expectedMatch: /1026 \(Scaled: 1026\)/,
+        recipeConfig: [
+            {
+                op: "Parse TCP",
+                args: ["Hex"],
+            }
+        ],
+    },
+    {
+        name: "Parse TCP: Options",
+        input: "c2eb0050a1380c1f000000008002faf080950000020405b40103030801010402",
+        expectedMatch: /1460/,
+        recipeConfig: [
+            {
+                op: "Parse TCP",
+                args: ["Hex"],
+            }
+        ],
+    },
+    {
+        name: "Parse TCP: Timestamps",
+        input: "9e90e11574d57b2c00000000a002ffffe5740000020405b40402080aa4e8c8f50000000001030308",
+        expectedMatch: /2766719221/,
+        recipeConfig: [
+            {
+                op: "Parse TCP",
+                args: ["Hex"],
+            }
+        ],
+    }
+]);

+ 5 - 18
tests/operations/tests/ParseUDP.mjs

@@ -2,7 +2,6 @@
  * Parse UDP tests.
  * Parse UDP tests.
  *
  *
  * @author h345983745
  * @author h345983745
- *
  * @copyright Crown Copyright 2019
  * @copyright Crown Copyright 2019
  * @license Apache-2.0
  * @license Apache-2.0
  */
  */
@@ -12,15 +11,11 @@ TestRegister.addTests([
     {
     {
         name: "Parse UDP: No Data - JSON",
         name: "Parse UDP: No Data - JSON",
         input: "04 89 00 35 00 2c 01 01",
         input: "04 89 00 35 00 2c 01 01",
-        expectedOutput: "{\"Source port\":1161,\"Destination port\":53,\"Length\":44,\"Checksum\":\"0101\"}",
+        expectedOutput: "{\"Source port\":1161,\"Destination port\":53,\"Length\":44,\"Checksum\":\"0x0101\"}",
         recipeConfig: [
         recipeConfig: [
-            {
-                op: "From Hex",
-                args: ["Auto"],
-            },
             {
             {
                 op: "Parse UDP",
                 op: "Parse UDP",
-                args: [],
+                args: ["Hex"],
             },
             },
             {
             {
                 op: "JSON Minify",
                 op: "JSON Minify",
@@ -30,15 +25,11 @@ TestRegister.addTests([
     }, {
     }, {
         name: "Parse UDP: With Data - JSON",
         name: "Parse UDP: With Data - JSON",
         input: "04 89 00 35 00 2c 01 01 02 02",
         input: "04 89 00 35 00 2c 01 01 02 02",
-        expectedOutput: "{\"Source port\":1161,\"Destination port\":53,\"Length\":44,\"Checksum\":\"0101\",\"Data\":\"0202\"}",
+        expectedOutput: "{\"Source port\":1161,\"Destination port\":53,\"Length\":44,\"Checksum\":\"0x0101\",\"Data\":\"0x0202\"}",
         recipeConfig: [
         recipeConfig: [
-            {
-                op: "From Hex",
-                args: ["Auto"],
-            },
             {
             {
                 op: "Parse UDP",
                 op: "Parse UDP",
-                args: [],
+                args: ["Hex"],
             },
             },
             {
             {
                 op: "JSON Minify",
                 op: "JSON Minify",
@@ -51,13 +42,9 @@ TestRegister.addTests([
         input: "04 89 00",
         input: "04 89 00",
         expectedOutput: "Need 8 bytes for a UDP Header",
         expectedOutput: "Need 8 bytes for a UDP Header",
         recipeConfig: [
         recipeConfig: [
-            {
-                op: "From Hex",
-                args: ["Auto"],
-            },
             {
             {
                 op: "Parse UDP",
                 op: "Parse UDP",
-                args: [],
+                args: ["Hex"],
             },
             },
             {
             {
                 op: "JSON Minify",
                 op: "JSON Minify",