|
| 1 | +from mcp.server.fastmcp import FastMCP |
| 2 | +import base64 |
| 3 | +import binascii |
| 4 | +import gzip |
| 5 | +import zlib |
| 6 | +import urllib.parse |
| 7 | +import html |
| 8 | +import quopri |
| 9 | +import codecs |
| 10 | +import json |
| 11 | +import string |
| 12 | + |
| 13 | +def _calculate_score(data: bytes) -> float: |
| 14 | + """Calculate a 'readability' score for bytes (0.0 to 1.0).""" |
| 15 | + if not data: |
| 16 | + return 0.0 |
| 17 | + try: |
| 18 | + text = data.decode('utf-8') |
| 19 | + printable = set(string.printable) |
| 20 | + count = sum(1 for c in text if c in printable) |
| 21 | + return count / len(text) |
| 22 | + except UnicodeDecodeError: |
| 23 | + # If not utf-8, check if it's mostly ASCII printable bytes |
| 24 | + printable_bytes = set(string.printable.encode('ascii')) |
| 25 | + count = sum(1 for b in data if b in printable_bytes) |
| 26 | + return (count / len(data)) * 0.5 # Penalty for non-utf8 |
| 27 | + |
| 28 | +def _try_decode(data: str, encoding: str): |
| 29 | + """Try to decode data with specific encoding, returning (success, result_bytes, error).""" |
| 30 | + try: |
| 31 | + if encoding == "base64": |
| 32 | + # Handle standard and url-safe base64, and padding |
| 33 | + missing_padding = len(data) % 4 |
| 34 | + if missing_padding: |
| 35 | + data += '=' * (4 - missing_padding) |
| 36 | + return True, base64.b64decode(data, validate=True), None |
| 37 | + |
| 38 | + elif encoding == "hex": |
| 39 | + # Remove spaces/colons/0x |
| 40 | + clean_data = data.replace(" ", "").replace(":", "").replace("0x", "") |
| 41 | + return True, binascii.unhexlify(clean_data), None |
| 42 | + |
| 43 | + elif encoding == "url": |
| 44 | + return True, urllib.parse.unquote_to_bytes(data), None |
| 45 | + |
| 46 | + elif encoding == "rot13": |
| 47 | + return True, codecs.decode(data, 'rot_13').encode('utf-8'), None |
| 48 | + |
| 49 | + elif encoding == "gzip": |
| 50 | + # Latin-1 allows 1:1 mapping of bytes to chars |
| 51 | + b = data.encode('latin-1') |
| 52 | + return True, gzip.decompress(b), None |
| 53 | + |
| 54 | + elif encoding == "deflate": |
| 55 | + b = data.encode('latin-1') |
| 56 | + # -15 for raw deflate (no header), standard zlib has header |
| 57 | + try: |
| 58 | + return True, zlib.decompress(b), None |
| 59 | + except: |
| 60 | + return True, zlib.decompress(b, -15), None |
| 61 | + |
| 62 | + elif encoding == "quopri": |
| 63 | + return True, quopri.decodestring(data.encode('utf-8')), None |
| 64 | + |
| 65 | + elif encoding == "html": |
| 66 | + return True, html.unescape(data).encode('utf-8'), None |
| 67 | + |
| 68 | + elif encoding == "unicode": |
| 69 | + # "Hello\u0020World" -> bytes |
| 70 | + return True, data.encode('utf-8').decode('unicode_escape').encode('utf-8'), None |
| 71 | + |
| 72 | + elif encoding == "ascii85": |
| 73 | + # Adobe Ascii85 usually delimited by <~ ~> |
| 74 | + d = data.strip() |
| 75 | + if d.startswith("<~"): d = d[2:] |
| 76 | + if d.endswith("~>"): d = d[:-2] |
| 77 | + return True, base64.a85decode(d), None |
| 78 | + |
| 79 | + except Exception as e: |
| 80 | + return False, None, str(e) |
| 81 | + |
| 82 | + return False, None, "Unknown encoding" |
| 83 | + |
| 84 | +def register_decode_tools(mcp: FastMCP): |
| 85 | + |
| 86 | + @mcp.tool() |
| 87 | + def wireshark_decode_payload(data: str, encoding: str = "auto") -> str: |
| 88 | + """ |
| 89 | + [Utils] Decode common encodings (Base64, Hex, URL, Gzip, etc.). |
| 90 | + |
| 91 | + Args: |
| 92 | + data: The string to decode. |
| 93 | + encoding: Target encoding. Supported: |
| 94 | + 'base64', 'hex', 'url', 'rot13', 'gzip', 'deflate', |
| 95 | + 'html', 'unicode', 'quopri', 'ascii85'. |
| 96 | + Use 'auto' to try all and sort by readability. |
| 97 | + |
| 98 | + Returns: |
| 99 | + Decoded string (or JSON in 'auto' mode). |
| 100 | + """ |
| 101 | + encodings = ["base64", "hex", "url", "rot13", "html", "unicode", "quopri", "ascii85"] |
| 102 | + # Exclude gzip/deflate from simple auto list, handled in chaining |
| 103 | + |
| 104 | + if encoding == "auto": |
| 105 | + results = [] |
| 106 | + |
| 107 | + # 1. Try single-step decodes |
| 108 | + for enc in encodings: |
| 109 | + success, res_bytes, _ = _try_decode(data, enc) |
| 110 | + if success: |
| 111 | + try: |
| 112 | + text = res_bytes.decode('utf-8') |
| 113 | + score = _calculate_score(res_bytes) |
| 114 | + # Filter out trivial results |
| 115 | + if text == data and enc in ["url", "html", "unicode", "rot13"]: |
| 116 | + continue |
| 117 | + if enc == "hex" and score < 0.1: |
| 118 | + continue |
| 119 | + |
| 120 | + results.append({ |
| 121 | + "encoding": enc, |
| 122 | + "result": text[:200] + "..." if len(text) > 200 else text, |
| 123 | + "score": round(score, 2), |
| 124 | + "is_text": True |
| 125 | + }) |
| 126 | + except: |
| 127 | + # Binary result |
| 128 | + results.append({ |
| 129 | + "encoding": enc, |
| 130 | + "result": "<binary_data>", |
| 131 | + "hex_preview": binascii.hexlify(res_bytes[:20]).decode('ascii'), |
| 132 | + "score": 0.0, |
| 133 | + "is_text": False |
| 134 | + }) |
| 135 | + |
| 136 | + # 2. Try Chained (e.g., Base64 -> Gzip) |
| 137 | + success, b64_bytes, _ = _try_decode(data, "base64") |
| 138 | + if success: |
| 139 | + try: |
| 140 | + gzip_bytes = gzip.decompress(b64_bytes) |
| 141 | + results.append({ |
| 142 | + "encoding": "base64+gzip", |
| 143 | + "result": gzip_bytes.decode('utf-8', errors='replace')[:200], |
| 144 | + "score": _calculate_score(gzip_bytes), |
| 145 | + "is_text": True |
| 146 | + }) |
| 147 | + except: pass |
| 148 | + |
| 149 | + try: |
| 150 | + zlib_bytes = zlib.decompress(b64_bytes) |
| 151 | + results.append({ |
| 152 | + "encoding": "base64+zlib", |
| 153 | + "result": zlib_bytes.decode('utf-8', errors='replace')[:200], |
| 154 | + "score": _calculate_score(zlib_bytes), |
| 155 | + "is_text": True |
| 156 | + }) |
| 157 | + except: pass |
| 158 | + |
| 159 | + # Sort by score desc |
| 160 | + results.sort(key=lambda x: x["score"], reverse=True) |
| 161 | + |
| 162 | + return json.dumps({ |
| 163 | + "success": True, |
| 164 | + "candidates": results[:5] # Return top 5 |
| 165 | + }, indent=2) |
| 166 | + |
| 167 | + else: |
| 168 | + success, res_bytes, err = _try_decode(data, encoding) |
| 169 | + if not success: |
| 170 | + return json.dumps({"success": False, "error": err}) |
| 171 | + |
| 172 | + try: |
| 173 | + return res_bytes.decode('utf-8') |
| 174 | + except UnicodeDecodeError: |
| 175 | + return f"[Binary Data] Hex: {binascii.hexlify(res_bytes).decode('ascii')}" |
0 commit comments