Repository: mitmproxy/mitmproxy

Issue:
Colorize JSON Contentview
I use mitmweb or mitmproxy to debug webhooks call to my api, often these webhooks are full of useful JSON data. So I think a JSON syntax coloration is a key feature for a proxy like that. May be I don't have it enabled or whatever and this feature is already implemented but not enabled by default ?




Discussion:
That sounds like a useful feature suggestion! We do have support for syntax highlighting for contentviews (see the hex view for example), if someone would extend the JSON view to do that, that'd be great! 😃 

https://github.com/mitmproxy/mitmproxy/blob/master/mitmproxy/contentviews/hex.py
https://github.com/mitmproxy/mitmproxy/blob/master/mitmproxy/contentviews/json.py

Resolving patch:
diff --git a/mitmproxy/contentviews/json.py b/mitmproxy/contentviews/json.py
--- a/mitmproxy/contentviews/json.py
+++ b/mitmproxy/contentviews/json.py
@@ -1,16 +1,39 @@
+import re
 import json
-from typing import Optional
+
+import typing
 
 from mitmproxy.contentviews import base
 
+PARSE_ERROR = object()
+
 
-def pretty_json(s: bytes) -> Optional[bytes]:
+def parse_json(s: bytes) -> typing.Any:
     try:
-        p = json.loads(s.decode('utf-8'))
+        return json.loads(s.decode('utf-8'))
     except ValueError:
-        return None
-    pretty = json.dumps(p, sort_keys=True, indent=4, ensure_ascii=False)
-    return pretty.encode("utf8", "strict")
+        return PARSE_ERROR
+
+
+def format_json(data: typing.Any) -> typing.Iterator[base.TViewLine]:
+    encoder = json.JSONEncoder(indent=4, sort_keys=True, ensure_ascii=False)
+    current_line: base.TViewLine = []
+    for chunk in encoder.iterencode(data):
+        if "\n" in chunk:
+            rest_of_last_line, chunk = chunk.split("\n", maxsplit=1)
+            # rest_of_last_line is a delimiter such as , or [
+            current_line.append(('text', rest_of_last_line))
+            yield current_line
+            current_line = []
+        if re.match(r'\s*"', chunk):
+            current_line.append(('json_string', chunk))
+        elif re.match(r'\s*\d', chunk):
+            current_line.append(('json_number', chunk))
+        elif re.match(r'\s*(true|null|false)', chunk):
+            current_line.append(('json_boolean', chunk))
+        else:
+            current_line.append(('text', chunk))
+    yield current_line
 
 
 class ViewJSON(base.View):
@@ -22,6 +45,6 @@ class ViewJSON(base.View):
     ]
 
     def __call__(self, data, **metadata):
-        pj = pretty_json(data)
-        if pj:
-            return "JSON", base.format_text(pj)
+        data = parse_json(data)
+        if data is not PARSE_ERROR:
+            return "JSON", format_json(data)
diff --git a/mitmproxy/tools/console/palettes.py b/mitmproxy/tools/console/palettes.py
--- a/mitmproxy/tools/console/palettes.py
+++ b/mitmproxy/tools/console/palettes.py
@@ -35,6 +35,9 @@ class Palette:
         # Hex view
         'offset',
 
+        # JSON view
+        'json_string', 'json_number', 'json_boolean',
+
         # Grid Editor
         'focusfield', 'focusfield_error', 'field_error', 'editfield',
 
@@ -170,6 +173,11 @@ class LowDark(Palette):
         # Hex view
         offset = ('dark cyan', 'default'),
 
+        # JSON view
+        json_string = ('dark blue', 'default'),
+        json_number = ('light magenta', 'default'),
+        json_boolean = ('dark magenta', 'default'),
+
         # Grid Editor
         focusfield = ('black', 'light gray'),
         focusfield_error = ('dark red', 'light gray'),
@@ -270,6 +278,11 @@ class LowLight(Palette):
         # Hex view
         offset = ('dark blue', 'default'),
 
+        # JSON view
+        json_string = ('dark blue', 'default'),
+        json_number = ('light magenta', 'default'),
+        json_boolean = ('dark magenta', 'default'),
+
         # Grid Editor
         focusfield = ('black', 'light gray'),
         focusfield_error = ('dark red', 'light gray'),
@@ -380,6 +393,11 @@ class SolarizedLight(LowLight):
         # Hex view
         offset = (sol_cyan, 'default'),
 
+        # JSON view
+        json_string = (sol_cyan, 'default'),
+        json_number = (sol_blue, 'default'),
+        json_boolean = (sol_magenta, 'default'),
+
         # Grid Editor
         focusfield = (sol_base00, sol_base2),
         focusfield_error = (sol_red, sol_base2),
@@ -454,6 +472,11 @@ class SolarizedDark(LowDark):
         # Hex view
         offset = (sol_cyan, 'default'),
 
+        # JSON view
+        json_string = (sol_cyan, 'default'),
+        json_number = (sol_blue, 'default'),
+        json_boolean = (sol_magenta, 'default'),
+
         # Grid Editor
         focusfield = (sol_base0, sol_base02),
         focusfield_error = (sol_red, sol_base02),


Test patch:
diff --git a/test/mitmproxy/contentviews/test_json.py b/test/mitmproxy/contentviews/test_json.py
--- a/test/mitmproxy/contentviews/test_json.py
+++ b/test/mitmproxy/contentviews/test_json.py
@@ -1,16 +1,43 @@
+from hypothesis import given
+from hypothesis.strategies import binary
+
 from mitmproxy.contentviews import json
 from . import full_eval
 
 
-def test_pretty_json():
-    assert json.pretty_json(b'{"foo": 1}')
-    assert not json.pretty_json(b"moo")
-    assert json.pretty_json(b'{"foo" : "\xe4\xb8\x96\xe7\x95\x8c"}')  # utf8 with chinese characters
-    assert not json.pretty_json(b'{"foo" : "\xFF"}')
+def test_parse_json():
+    assert json.parse_json(b'{"foo": 1}')
+    assert json.parse_json(b'null') is None
+    assert json.parse_json(b"moo") is json.PARSE_ERROR
+    assert json.parse_json(b'{"foo" : "\xe4\xb8\x96\xe7\x95\x8c"}')  # utf8 with chinese characters
+    assert json.parse_json(b'{"foo" : "\xFF"}') is json.PARSE_ERROR
+
+
+def test_format_json():
+    assert list(json.format_json({
+        "data": [
+            "str",
+            42,
+            True,
+            False,
+            None,
+            {},
+            []
+        ]
+    }))
 
 
 def test_view_json():
     v = full_eval(json.ViewJSON())
+    assert v(b"null")
     assert v(b"{}")
     assert not v(b"{")
     assert v(b"[1, 2, 3, 4, 5]")
+    assert v(b'{"foo" : 3}')
+    assert v(b'{"foo": true, "nullvalue": null}')
+
+
+@given(binary())
+def test_view_json_doesnt_crash(data):
+    v = full_eval(json.ViewJSON())
+    v(data)