Fix JSON binary encode and decode errors

Traverse the objects passed to JSON for encoding and decoding.
When binary data is seen during encode replace the binary
data with a dict {'__base64__' : base64_encoding_of_binary_value}.

On decode if a dict is seen whose single key is '__base64__' replace
that dict with the base64 decoded value of the key's value.

Thanks to Jason for the suggestion (no pun intended :-)
--
John Dennis <jden...@redhat.com>

Looking to carve out IT costs?
www.redhat.com/carveoutcosts/
>From f45be23b3c5f41ee012c51a6d6d3759f92ee48ee Mon Sep 17 00:00:00 2001
From: John Dennis <jden...@redhat.com>
Date: Mon, 1 Mar 2010 18:55:39 -0500
Subject: [PATCH] Fix JSON binary encode and decode errors
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit

Traverse the objects passed to JSON for encoding and decoding.
When binary data is seen during encode replace the binary
data with a dict {'__base64__' : base64_encoding_of_binary_value}.

On decode if a dict is seen whose single key is '__base64__' replace
that dict with the base64 decoded value of the key's value.
---
 ipaserver/rpcserver.py |  100 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 99 insertions(+), 1 deletions(-)

diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 4a5040e..96c4d29 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -31,7 +31,7 @@ from ipalib.request import context, Connection, destroy_context
 from ipalib.rpc import xml_dumps, xml_loads
 from ipalib.util import make_repr
 from ipalib.compat import json
-
+import base64
 
 def read_input(environ):
     """
@@ -216,6 +216,102 @@ class xmlserver(WSGIExecutioner):
         return xml_dumps(response, methodresponse=True)
 
 
+def json_encode_binary(val):
+    '''
+   JSON cannot encode binary values. We encode binary values in Python str
+   objects and text in Python unicode objects. In order to allow a binary
+   object to be passed through JSON we base64 encode it thus converting it to
+   text which JSON can transport. To assure we recognize the value is a base64
+   encoded representation of the original binary value and not confuse it with
+   other text we convert the binary value to a dict in this form:
+
+   {'__base64__' : base64_encoding_of_binary_value}
+
+   This modification of the original input value cannot be done "in place" as
+   one might first assume (e.g. replacing any binary items in a container
+   (e.g. list, tuple, dict) with the base64 dict because the container might be
+   an immutable object (i.e. a tuple). Therefore this function returns a copy
+   of any container objects it encounters with tuples replaced by lists. This
+   is O.K. because the JSON encoding will map both lists and tuples to JSON
+   arrays.
+   '''
+
+    if isinstance(val, dict):
+        new_dict = {}
+        for k,v in val.items():
+            if isinstance(v, str):
+                new_dict[k] = {'__base64__' : base64.b64encode(v)}
+            else:
+                new_dict[k] = json_encode_binary(v)
+        del val
+        return new_dict
+    elif isinstance(val, (list, tuple)):
+        new_list = []
+        n = len(val)
+        i = 0
+        while i < n:
+            v = val[i]
+            if isinstance(v, str):
+                new_list.append({'__base64__' : base64.b64encode(v)})
+            else:
+                new_list.append(json_encode_binary(v))
+            i += 1
+        del val
+        return new_list
+    elif isinstance(val, str):
+        return {'__base64__' : base64.b64encode(val)}
+    else:
+        return val
+
+def json_decode_binary(val):
+    '''
+    JSON cannot transport binary data. In order to transport binary data we
+    convert binary data to a form like this:
+
+   {'__base64__' : base64_encoding_of_binary_value}
+
+   see json_encode_binary()
+
+    After JSON had decoded the JSON stream back into a Python object we must
+    recursively scan the object looking for any dicts which might represent
+    binary values and replace the dict containing the base64 encoding of the
+    binary value with the decoded binary value. Unlike the encoding problem
+    where the input might consist of immutable object, all JSON decoded
+    container are mutable so the conversion could be done in place. However we
+    don't modifying objects in place has side effects which may be
+    dangerous. Thus we elect to spend a few more cycles and avoid the
+    possibility of unintended side effects in favor of robustness.
+    '''
+
+    if isinstance(val, dict):
+        if val.has_key('__base64__'):
+            return base64.b64decode(val['__base64__'])
+        else:
+            new_dict = {}
+            for k,v in val.items():
+                if isinstance(v, dict) and v.has_key('__base64__'):
+                        new_dict[k] = base64.b64decode(v['__base64__'])
+                else:
+                    new_dict[k] = json_decode_binary(v)
+            del val
+            return new_dict
+    elif isinstance(val, list):
+        new_list = []
+        n = len(val)
+        i = 0
+        while i < n:
+            v = val[i]
+            if isinstance(v, dict) and v.has_key('__base64__'):
+                binary_val = base64.b64decode(v['__base64__'])
+                new_list.append(binary_val)
+            else:
+                new_list.append(json_decode_binary(v))
+            i += 1
+        del val
+        return new_list
+    else:
+        return val
+
 class jsonserver(WSGIExecutioner):
     """
     JSON RPC server.
@@ -241,6 +337,7 @@ class jsonserver(WSGIExecutioner):
             error=error,
             id=_id,
         )
+        response = json_encode_binary(response)
         return json.dumps(response, sort_keys=True, indent=4)
 
     def unmarshal(self, data):
@@ -254,6 +351,7 @@ class jsonserver(WSGIExecutioner):
             raise JSONError(error='Request is missing "method"')
         if 'params' not in d:
             raise JSONError(error='Request is missing "params"')
+        d = json_decode_binary(d)
         method = d['method']
         params = d['params']
         _id = d.get('id')
-- 
1.6.6.1

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to