On 01/15/2013 12:36 PM, Petr Viktorin wrote:
I meant to hold this patch a while longer to let it mature, but from
what Brian Smith asked on the user list it seems it could help him.

Design: http://freeipa.org/page/V3/JSON-RPC
Ticket: https://fedorahosted.org/freeipa/ticket/3299

See the design page for what the patch does.


As much as I've tried to avoid them, the code includes some workarounds:
It extends xmlrpclib to also support JSON. This is rather intrusive, but
to not do that I'd need to write a parallel stack for JSON, without the
help of a standard library.
The registration of either jsonclient or xmlclient as "rpcclient" in the
API also needs a bit of magic, since the framework requires the class
name to match the attribute.


To prevent backwards compatibility problems, we need to ensure that all
official JSON clients send the API version, so this patch should be
applied after my patches 0104-0106.


Updating to current master.


--
PetrĀ³
From 87bade151fd8865c29a16d817c841074754a0d26 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Wed, 19 Dec 2012 04:25:24 -0500
Subject: [PATCH] Switch client to JSON-RPC

Modify ipalib.rpc to support JSON-RPC in addition to XML-RPC.
This is done by subclassing and extending xmlrpclib, because
our existing code relies on xmlrpclib internals.

The URI to use is given in the new jsonrpc_uri env variable. When
it is not given, it is generated from xmlrpc_uri by replacing
/xml with /json.

The rpc_json_uri env variable existed before, but was unused,
undocumented and not set the install scripts.
This patch removes it in favor of jsonrpc_uri (for consistency
with xmlrpc_uri).

Add the rpc_protocol env variable to control the protocol
IPA uses. rpc_protocol defaults to 'jsonrpc', but may be changed
to 'xmlrpc'.
Make backend.Executioner and tests use the backend specified by
rpc_protocol.

For compatibility with unwrap_xml, decoding JSON now gives tuples
instead of lists.

Design: http://freeipa.org/page/V3/JSON-RPC
Ticket: https://fedorahosted.org/freeipa/ticket/3299
---
 ipa-client/man/default.conf.5                 |    8 +-
 ipalib/backend.py                             |    2 +-
 ipalib/config.py                              |   21 ++-
 ipalib/constants.py                           |    7 +-
 ipalib/frontend.py                            |    4 +-
 ipalib/plugins/{xmlclient.py => rpcclient.py} |   24 ++-
 ipalib/rpc.py                                 |  271 +++++++++++++++++++++----
 ipaserver/rpcserver.py                        |  113 +---------
 tests/test_cmdline/test_cli.py                |    4 +-
 tests/test_ipaserver/test_rpcserver.py        |    4 +-
 tests/test_xmlrpc/test_dns_plugin.py          |    4 +-
 tests/test_xmlrpc/test_trust_plugin.py        |    4 +-
 tests/test_xmlrpc/xmlrpc_test.py              |    8 +-
 13 files changed, 312 insertions(+), 162 deletions(-)
 rename ipalib/plugins/{xmlclient.py => rpcclient.py} (58%)

diff --git a/ipa-client/man/default.conf.5 b/ipa-client/man/default.conf.5
index a0804e39f946b3202acc32cc6a7ccbdb8f2626e6..edaa16ee15cdf74caa19910854e5abcabbd525de 100644
--- a/ipa-client/man/default.conf.5
+++ b/ipa-client/man/default.conf.5
@@ -179,7 +179,13 @@ Used internally in the IPA source package to verify that the API has not changed
 When True provides more information. Specifically this sets the global log level to "info".
 .TP
 .B xmlrpc_uri <URI>
-Specifies the URI of the XML\-RPC server for a client. This is used by IPA and some external tools as well, such as ipa\-getcert. e.g. https://ipa.example.com/ipa/xml
+Specifies the URI of the XML\-RPC server for a client. This may be used by IPA, and is used by some external tools, such as ipa\-getcert. Example: https://ipa.example.com/ipa/xml
+.TP
+.B jsonrpc_uri <URI>
+Specifies the URI of the JSON server for a client. This is used by IPA. If not given, it is derived from xmlrpc_uri. Example: https://ipa.example.com/ipa/json
+.TP
+.B rpc_protocol <URI>
+Specifies the type of RPC calls IPA makes: 'jsonrpc' or 'xmlrpc'. Defaults to 'jsonrpc'.
 .TP
 The following define the containers for the IPA server. Containers define where in the DIT that objects can be found. The full location is the value of container + basedn.
   container_accounts: cn=accounts
diff --git a/ipalib/backend.py b/ipalib/backend.py
index 7be38ecc80faf03e735813fb1e2d0eba5c347800..b94264236795b65905ba425ef15e452e7a66625b 100644
--- a/ipalib/backend.py
+++ b/ipalib/backend.py
@@ -113,7 +113,7 @@ class Executioner(Backend):
         if self.env.in_server:
             self.Backend.ldap2.connect(ccache=ccache)
         else:
-            self.Backend.xmlclient.connect(verbose=(self.env.verbose >= 2),
+            self.Backend.rpcclient.connect(verbose=(self.env.verbose >= 2),
                 fallback=self.env.fallback, delegate=self.env.delegate)
         if client_ip is not None:
             setattr(context, "client_ip", client_ip)
diff --git a/ipalib/config.py b/ipalib/config.py
index 3c9aeaa2880cb67c7f230ecba4839d37e77b04f2..f86c0a5ea3885d2bf89712f91b0c0705dceedd32 100644
--- a/ipalib/config.py
+++ b/ipalib/config.py
@@ -29,17 +29,17 @@ of the process.
 For the per-request thread-local information, see `ipalib.request`.
 """
 
+import urlparse
 from ConfigParser import RawConfigParser, ParsingError
 from types import NoneType
 import os
 from os import path
 import sys
-from socket import getfqdn
 from ipapython.dn import DN
 
 from base import check_name
 from constants import CONFIG_SECTION
-from constants import TYPE_ERROR, OVERRIDE_ERROR, SET_ERROR, DEL_ERROR
+from constants import OVERRIDE_ERROR, SET_ERROR, DEL_ERROR
 
 
 class Env(object):
@@ -514,8 +514,8 @@ class Env(object):
                ``self.conf_default`` (if it exists) by calling
                `Env._merge_from_file()`.
 
-            4. Intelligently fill-in the *in_server* , *logdir*, and *log*
-               variables if they haven't already been set.
+            4. Intelligently fill-in the *in_server* , *logdir*, *log*, and
+               *jsonrpc_uri* variables if they haven't already been set.
 
             5. Merge-in the variables in ``defaults`` by calling `Env._merge()`.
                In normal circumstances ``defaults`` will simply be those
@@ -556,6 +556,19 @@ class Env(object):
         if 'log' not in self:
             self.log = self._join('logdir', '%s.log' % self.context)
 
+        # Derive jsonrpc_uri from xmlrpc_uri
+        if 'jsonrpc_uri' not in self:
+            if 'xmlrpc_uri' in self:
+                xmlrpc_uri = self.xmlrpc_uri
+            else:
+                xmlrpc_uri = defaults.get('xmlrpc_uri')
+            if xmlrpc_uri:
+                (scheme, netloc, uripath, params, query, fragment
+                        ) = urlparse.urlparse(xmlrpc_uri)
+                uripath = uripath.replace('/xml', '/json', 1)
+                self.jsonrpc_uri = urlparse.urlunparse((
+                        scheme, netloc, uripath, params, query, fragment))
+
         self._merge(**defaults)
 
     def _finalize(self, **lastchance):
diff --git a/ipalib/constants.py b/ipalib/constants.py
index e6d951440f45d0a37b6c595d87ec568130c1f793..054f746ed100ca3ceba024d86406d1526a36b8bc 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -110,10 +110,12 @@ DEFAULT_CONFIG = (
     ('container_dna_posix_ids', DN(('cn', 'posix-ids'), ('cn', 'dna'), ('cn', 'ipa'), ('cn', 'etc'))),
 
     # Ports, hosts, and URIs:
-    # FIXME: let's renamed xmlrpc_uri to rpc_xml_uri
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
-    ('rpc_json_uri', 'http://localhost:8888/ipa/json'),
+    # jsonrpc_uri is set in Env._finalize_core()
     ('ldap_uri', 'ldap://localhost:389'),
+
+    ('rpc_protocol', 'jsonrpc'),
+
     # Time to wait for a service to start, in seconds
     ('startup_timeout', 120),
 
@@ -198,5 +200,6 @@ DEFAULT_CONFIG = (
     ('in_server', object),  # Whether or not running in-server (bool)
     ('logdir', object),  # Directory containing log files
     ('log', object),  # Path to context specific log file
+    ('jsonrpc_uri', object),  # derived from xmlrpc_uri in Env._finalize_core()
 
 )
diff --git a/ipalib/frontend.py b/ipalib/frontend.py
index 52ddf28c756344b0d873004484ed483c0edda1c5..aff44fd2b239177cc0f2b62567478c5054ca95e8 100644
--- a/ipalib/frontend.py
+++ b/ipalib/frontend.py
@@ -764,9 +764,9 @@ class Command(HasParam):
 
     def forward(self, *args, **kw):
         """
-        Forward call over XML-RPC to this same command on server.
+        Forward call over JSON-RPC to this same command on server.
         """
-        return self.Backend.xmlclient.forward(self.name, *args, **kw)
+        return self.Backend.rpcclient.forward(self.name, *args, **kw)
 
     def _on_finalize(self):
         """
diff --git a/ipalib/plugins/xmlclient.py b/ipalib/plugins/rpcclient.py
similarity index 58%
rename from ipalib/plugins/xmlclient.py
rename to ipalib/plugins/rpcclient.py
index 21ba47e2e0afca7d76efd54c5a2bd04261197aa1..cafe1d719ab96f9af4bc94516939a684fba1306d 100644
--- a/ipalib/plugins/xmlclient.py
+++ b/ipalib/plugins/rpcclient.py
@@ -19,11 +19,31 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-XML-RPC client plugin.
+RPC client plugins.
 """
 
 from ipalib import api
 
 if 'in_server' in api.env and api.env.in_server is False:
-    from ipalib.rpc import xmlclient
+    from ipalib.rpc import xmlclient, jsonclient
     api.register(xmlclient)
+    api.register(jsonclient)
+
+    # FIXME: api.register only looks at the class name, so we need to create
+    # trivial subclasses with the desired name.
+    if api.env.rpc_protocol == 'xmlrpc':
+
+        class rpcclient(xmlclient):
+            """xmlclient renamed to 'rpcclient'"""
+            pass
+        api.register(rpcclient)
+
+    elif api.env.rpc_protocol == 'jsonrpc':
+
+        class rpcclient(jsonclient):
+            """jsonclient renamed to 'rpcclient'"""
+            pass
+        api.register(rpcclient)
+
+    else:
+        raise ValueError('unknown rpc_protocol: %s' % api.env.rpc_protocol)
diff --git a/ipalib/rpc.py b/ipalib/rpc.py
index 077d99ebe3b53b046c6a41ea45112a7b7e133054..0dc7dd3c59552915b0d4082c35f42bc874a1b08c 100644
--- a/ipalib/rpc.py
+++ b/ipalib/rpc.py
@@ -32,41 +32,43 @@ Also see the `ipaserver.rpcserver` module.
 
 from types import NoneType
 from decimal import Decimal
-import threading
 import sys
 import os
-import errno
 import locale
-import datetime
+import base64
+import urllib
+import json
+import socket
+from urllib2 import urlparse
+
 from xmlrpclib import (Binary, Fault, dumps, loads, ServerProxy, Transport,
         ProtocolError, MININT, MAXINT)
 import kerberos
 from dns import resolver, rdatatype
 from dns.exception import DNSException
+from nss.error import NSPRError
 
 from ipalib.backend import Connectible
-from ipalib.errors import public_errors, PublicError, UnknownError, NetworkError, KerberosError, XMLRPCMarshallError
+from ipalib.errors import (public_errors, UnknownError, NetworkError,
+    KerberosError, XMLRPCMarshallError, JSONError, ConversionError)
 from ipalib import errors
 from ipalib.request import context, Connection
 from ipalib.util import get_current_principal
 from ipapython.ipa_log_manager import root_logger
 from ipapython import ipautil
 from ipapython import kernel_keyring
 from ipapython.cookie import Cookie
 from ipalib.text import _
-
-import httplib
-import socket
 from ipapython.nsslib import NSSHTTPS, NSSConnection
-from nss.error import NSPRError
-from urllib2 import urlparse
 from ipalib.krb_utils import KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, KRB5KRB_AP_ERR_TKT_EXPIRED, \
                              KRB5_FCC_PERM, KRB5_FCC_NOFILE, KRB5_CC_FORMAT, KRB5_REALM_CANT_RESOLVE
 from ipapython.dn import DN
 
 COOKIE_NAME = 'ipa_session'
 KEYRING_COOKIE_NAME = '%s_cookie:%%s' % COOKIE_NAME
 
+errors_by_code = dict((e.errno, e) for e in public_errors)
+
 
 def client_session_keyring_keyname(principal):
     '''
@@ -228,6 +230,84 @@ def xml_dumps(params, methodname=None, methodresponse=False, encoding='UTF-8'):
     )
 
 
+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():
+            new_dict[k] = json_encode_binary(v)
+        return new_dict
+    elif isinstance(val, (list, tuple)):
+        new_list = [json_encode_binary(v) for v in val]
+        return new_list
+    elif isinstance(val, str):
+        return {'__base64__': base64.b64encode(val)}
+    elif isinstance(val, Decimal):
+        return {'__base64__': base64.b64encode(str(val))}
+    elif isinstance(val, DN):
+        return str(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 modify objects in place because of 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 '__base64__' in val:
+            return base64.b64decode(val['__base64__'])
+        else:
+            return dict((k, json_decode_binary(v)) for k, v in val.items())
+    elif isinstance(val, list):
+        return tuple(json_decode_binary(v) for v in val)
+    else:
+        if isinstance(val, basestring):
+            try:
+                return val.decode('utf-8')
+            except UnicodeDecodeError:
+                raise ConversionError(
+                    name=val,
+                    error='incorrect type'
+                )
+        else:
+            return val
+
+
 def decode_fault(e, encoding='UTF-8'):
     assert isinstance(e, Fault)
     if type(e.faultString) is str:
@@ -265,10 +345,45 @@ def xml_loads(data, encoding='UTF-8'):
         raise decode_fault(e)
 
 
-class LanguageAwareTransport(Transport):
+class DummyParser(object):
+    def __init__(self):
+        self.data = ''
+
+    def feed(self, data):
+        self.data += data
+
+    def close(self):
+        return self.data
+
+
+class MultiProtocolTransport(Transport):
+    """Transport that handles both XML-RPC and JSON"""
+    def __init__(self, protocol):
+        Transport.__init__(self)
+        self.protocol = protocol
+
+    def getparser(self):
+        if self.protocol == 'json':
+            parser = DummyParser()
+            return parser, parser
+        else:
+            return Transport.getparser(self)
+
+    def send_content(self, connection, request_body):
+        if self.protocol == 'json':
+            connection.putheader("Content-Type", "application/json")
+        else:
+            connection.putheader("Content-Type", "text/xml")
+
+        connection.putheader("Content-Length", str(len(request_body)))
+        connection.endheaders(request_body)
+
+
+class LanguageAwareTransport(MultiProtocolTransport):
     """Transport sending Accept-Language header"""
     def get_host_info(self, host):
-        (host, extra_headers, x509) = Transport.get_host_info(self, host)
+        host, extra_headers, x509 = MultiProtocolTransport.get_host_info(
+            self, host)
 
         try:
             lang = locale.setlocale(locale.LC_ALL, '').split('.')[0].lower()
@@ -466,23 +581,27 @@ class DelegatedKerbTransport(KerbTransport):
     flags = kerberos.GSS_C_DELEG_FLAG |  kerberos.GSS_C_MUTUAL_FLAG | \
             kerberos.GSS_C_SEQUENCE_FLAG
 
-class xmlclient(Connectible):
+
+class RPCClient(Connectible):
     """
     Forwarding backend plugin for XML-RPC client.
 
     Also see the `ipaserver.rpcserver.xmlserver` plugin.
     """
 
-    def __init__(self):
-        super(xmlclient, self).__init__()
-        self.__errors = dict((e.errno, e) for e in public_errors)
+    # Values to set on subclasses:
+    session_path = None
+    server_proxy_class = ServerProxy
+    protocol = None
+    wrap_functions = None
 
-    def get_url_list(self, xmlrpc_uri):
+    def get_url_list(self, rpc_uri):
         """
         Create a list of urls consisting of the available IPA servers.
         """
         # the configured URL defines what we use for the discovered servers
-        (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(xmlrpc_uri)
+        (scheme, netloc, path, params, query, fragment
+            ) = urlparse.urlparse(rpc_uri)
         servers = []
         name = '_ldap._tcp.%s.' % self.env.domain
 
@@ -498,7 +617,7 @@ class xmlclient(Connectible):
         servers = list(set(servers))
         # the list/set conversion won't preserve order so stick in the
         # local config file version here.
-        cfg_server = xmlrpc_uri
+        cfg_server = rpc_uri
         if cfg_server in servers:
             # make sure the configured master server is there just once and
             # it is the first one
@@ -591,52 +710,56 @@ class xmlclient(Connectible):
 
         # Form the session URL by substituting the session path into the original URL
         scheme, netloc, path, params, query, fragment = urlparse.urlparse(original_url)
-        path = '/ipa/session/xml'
+        path = self.session_path
         session_url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
 
         return session_url
 
+    def rpc_uri_from_env(self, env):
+        raise NotImplementedError('RPCClient.rpc_uri_from_env')
+
     def create_connection(self, ccache=None, verbose=False, fallback=True,
                           delegate=False):
         try:
-            xmlrpc_uri = self.env.xmlrpc_uri
+            rpc_uri = self.rpc_uri_from_env(self.env)
             principal = get_current_principal()
             setattr(context, 'principal', principal)
             # We have a session cookie, try using the session URI to see if it
             # is still valid
             if not delegate:
-                xmlrpc_uri = self.apply_session_cookie(xmlrpc_uri)
+                rpc_uri = self.apply_session_cookie(rpc_uri)
         except ValueError:
             # No session key, do full Kerberos auth
             pass
-        urls = self.get_url_list(xmlrpc_uri)
+        urls = self.get_url_list(rpc_uri)
         serverproxy = None
         for url in urls:
             kw = dict(allow_none=True, encoding='UTF-8')
             kw['verbose'] = verbose
             if url.startswith('https://'):
                 if delegate:
-                    kw['transport'] = DelegatedKerbTransport()
+                    transport_class = DelegatedKerbTransport
                 else:
-                    kw['transport'] = KerbTransport()
+                    transport_class = KerbTransport
             else:
-                kw['transport'] = LanguageAwareTransport()
+                transport_class = LanguageAwareTransport
+            kw['transport'] = transport_class(protocol=self.protocol)
             self.log.info('trying %s' % url)
             setattr(context, 'request_url', url)
-            serverproxy = ServerProxy(url, **kw)
+            serverproxy = self.server_proxy_class(url, **kw)
             if len(urls) == 1:
                 # if we have only 1 server and then let the
                 # main requester handle any errors. This also means it
                 # must handle a 401 but we save a ping.
                 return serverproxy
             try:
                 command = getattr(serverproxy, 'ping')
                 try:
-                    response = command()
+                    response = command([], {})
                 except Fault, e:
                     e = decode_fault(e)
-                    if e.faultCode in self.__errors:
-                        error = self.__errors[e.faultCode]
+                    if e.faultCode in errors_by_code:
+                        error = errors_by_code[e.faultCode]
                         raise error(message=e.faultString)
                     else:
                         raise UnknownError(
@@ -665,8 +788,6 @@ class xmlclient(Connectible):
             except Exception, e:
                 if not fallback:
                     raise
-                else:
-                    self.log.info('Connection to %s failed with %s', url, e)
                 serverproxy = None
 
         if serverproxy is None:
@@ -697,18 +818,19 @@ class xmlclient(Connectible):
                 '%s.forward(): %r not in api.Command' % (self.name, name)
             )
         server = getattr(context, 'request_url', None)
-        self.info('Forwarding %r to server %r', name, server)
+        self.info('Forwarding %r to %s server %r', name, self.protocol, server)
         command = getattr(self.conn, name)
         params = [args, kw]
+        wrap, unwrap = self.wrap_functions
         try:
-            response = command(*xml_wrap(params))
-            return xml_unwrap(response)
+            response = command(*wrap(params))
+            return unwrap(response)
         except Fault, e:
             e = decode_fault(e)
             self.debug('Caught fault %d from server %s: %s', e.faultCode,
                 server, e.faultString)
-            if e.faultCode in self.__errors:
-                error = self.__errors[e.faultCode]
+            if e.faultCode in errors_by_code:
+                error = errors_by_code[e.faultCode]
                 raise error(message=e.faultString)
             raise UnknownError(
                 code=e.faultCode,
@@ -754,3 +876,80 @@ class xmlclient(Connectible):
             raise NetworkError(uri=server, error=str(e))
         except (OverflowError, TypeError), e:
             raise XMLRPCMarshallError(error=str(e))
+
+
+class xmlclient(RPCClient):
+    session_path = '/ipa/session/xml'
+    server_proxy_class = ServerProxy
+    protocol = 'xml'
+    wrap_functions = xml_wrap, xml_unwrap
+
+    def rpc_uri_from_env(self, env):
+        return env.xmlrpc_uri
+
+
+class JSONServerProxy(object):
+    def __init__(self, uri, transport, encoding, verbose, allow_none):
+        type, uri = urllib.splittype(uri)
+        if type not in ("http", "https"):
+            raise IOError("unsupported XML-RPC protocol")
+        self.__host, self.__handler = urllib.splithost(uri)
+        self.__transport = transport
+
+        assert encoding == 'UTF-8'
+        assert allow_none
+        self.__verbose = verbose
+
+        # FIXME: Some of our code requires ServerProxy internals.
+        # But, xmlrpclib.ServerProxy's _ServerProxy__transport can be accessed
+        # by calling serverproxy('transport')
+        self._ServerProxy__transport = transport
+
+    def __request(self, name, args):
+        payload = {'method': unicode(name), 'params': args, 'id': 0}
+
+        response = self.__transport.request(
+            self.__host,
+            self.__handler,
+            json.dumps(json_encode_binary(payload)),
+            verbose=self.__verbose,
+        )
+
+        try:
+            response = json_decode_binary(json.loads(response))
+        except ValueError, e:
+            raise JSONError(str(e))
+
+        error = response.get('error')
+        if error:
+            try:
+                error_class = errors_by_code[error['code']]
+            except KeyError:
+                raise UnknownError(
+                    code=error.get('code'),
+                    error=error.get('message'),
+                    server=self.__host,
+                )
+            else:
+                raise error_class(message=error['message'])
+
+        return response['result']
+
+    def __getattr__(self, name):
+        def _call(*args):
+            return self.__request(name, args)
+        return _call
+
+
+def identity(x):
+    return x
+
+
+class jsonclient(RPCClient):
+    session_path = '/ipa/session/json'
+    server_proxy_class = JSONServerProxy
+    protocol = 'json'
+    wrap_functions = identity, identity
+
+    def rpc_uri_from_env(self, env):
+        return env.jsonrpc_uri
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 581c30b4ca6b3b753fd27a43b9acc9f49f1f1f73..2368b5d08b7a894efb65274e105dbafbb9867e31 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -25,27 +25,29 @@ Also see the `ipalib.rpc` module.
 
 from xml.sax.saxutils import escape
 from xmlrpclib import Fault
-from wsgiref.util import shift_path_info
-import base64
 import os
-import string
 import datetime
-from decimal import Decimal
 import urlparse
 import time
 import json
 
 from ipalib import plugable
 from ipalib.backend import Executioner
-from ipalib.errors import PublicError, InternalError, CommandError, JSONError, ConversionError, CCacheError, RefererError, InvalidSessionPassword, NotFound, ACIError, ExecutionError
-from ipalib.request import context, Connection, destroy_context
-from ipalib.rpc import xml_dumps, xml_loads
+from ipalib.errors import (PublicError, InternalError, CommandError, JSONError,
+    CCacheError, RefererError, InvalidSessionPassword, NotFound, ACIError,
+    ExecutionError)
+from ipalib.request import context, destroy_context
+from ipalib.rpc import (xml_dumps, xml_loads,
+    json_encode_binary, json_decode_binary)
 from ipalib.util import parse_time_duration, normalize_name
 from ipapython.dn import DN
 from ipaserver.plugins.ldap2 import ldap2
-from ipalib.session import session_mgr, AuthManager, get_ipa_ccache_name, load_ccache_data, bind_ipa_ccache, release_ipa_ccache, fmt_time, default_max_session_duration
+from ipalib.session import (session_mgr, AuthManager, get_ipa_ccache_name,
+    load_ccache_data, bind_ipa_ccache, release_ipa_ccache, fmt_time,
+    default_max_session_duration)
 from ipalib.backend import Backend
-from ipalib.krb_utils import krb5_parse_ccache, KRB5_CCache, krb_ticket_expiration_threshold, krb5_format_principal_name
+from ipalib.krb_utils import (
+    KRB5_CCache, krb_ticket_expiration_threshold, krb5_format_principal_name)
 from ipapython import ipautil
 from ipapython.version import VERSION
 from ipalib.text import _
@@ -397,99 +399,6 @@ class WSGIExecutioner(Executioner):
         raise NotImplementedError('%s.marshal()' % self.fullname)
 
 
-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():
-            new_dict[k] = json_encode_binary(v)
-        return new_dict
-    elif isinstance(val, (list, tuple)):
-        new_list = [json_encode_binary(v) for v in val]
-        return new_list
-    elif isinstance(val, str):
-        return {'__base64__' : base64.b64encode(val)}
-    elif isinstance(val, Decimal):
-        return {'__base64__' : base64.b64encode(str(val))}
-    elif isinstance(val, DN):
-        return str(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 modify objects in place because of 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)
-            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
-        return new_list
-    else:
-        if isinstance(val, basestring):
-            try:
-                return val.decode('utf-8')
-            except UnicodeDecodeError:
-                raise ConversionError(
-                    name=val,
-                    error='incorrect type'
-                )
-        else:
-            return val
-
 class jsonserver(WSGIExecutioner, HTTP_Status):
     """
     JSON RPC server.
diff --git a/tests/test_cmdline/test_cli.py b/tests/test_cmdline/test_cli.py
index 06c6124bb1d3ac57add9e44d1754238063039c6d..f76180c1a57e3732f589b7aa4a8bf502c8ad2399 100644
--- a/tests/test_cmdline/test_cli.py
+++ b/tests/test_cmdline/test_cli.py
@@ -24,8 +24,8 @@ class TestCLIParsing(object):
 
     def run_command(self, command_name, **kw):
         """Run a command on the server"""
-        if not api.Backend.xmlclient.isconnected():
-            api.Backend.xmlclient.connect(fallback=False)
+        if not api.Backend.rpcclient.isconnected():
+            api.Backend.rpcclient.connect(fallback=False)
         try:
             api.Command[command_name](**kw)
         except errors.NetworkError:
diff --git a/tests/test_ipaserver/test_rpcserver.py b/tests/test_ipaserver/test_rpcserver.py
index a75a85e0961096bb57ea1bf8e520c908a6a75482..5632bcd75cdecbd67ded2bd4bde1bed860906ca8 100644
--- a/tests/test_ipaserver/test_rpcserver.py
+++ b/tests/test_ipaserver/test_rpcserver.py
@@ -241,7 +241,7 @@ class test_jsonserver(PluginTester):
         assert unicode(e.error) == 'params[1] (aka options) must be a dict'
 
         # Test with valid values:
-        args = [u'jdoe']
+        args = (u'jdoe', )
         options = dict(givenname=u'John', sn='Doe')
-        d = dict(method=u'user_add', params=[args, options], id=18)
+        d = dict(method=u'user_add', params=(args, options), id=18)
         assert o.unmarshal(json.dumps(d)) == (u'user_add', args, options, 18)
diff --git a/tests/test_xmlrpc/test_dns_plugin.py b/tests/test_xmlrpc/test_dns_plugin.py
index 1902484949aeb0c96a0f2cda294fd3e6ae6e086f..9ae016d2e5020163a5e4439b66be118fedd8c66e 100644
--- a/tests/test_xmlrpc/test_dns_plugin.py
+++ b/tests/test_xmlrpc/test_dns_plugin.py
@@ -60,8 +60,8 @@ class test_dns(Declarative):
     def setUpClass(cls):
         super(test_dns, cls).setUpClass()
 
-        if not api.Backend.xmlclient.isconnected():
-            api.Backend.xmlclient.connect(fallback=False)
+        if not api.Backend.rpcclient.isconnected():
+            api.Backend.rpcclient.connect(fallback=False)
         try:
            api.Command['dnszone_add'](dnszone1,
                idnssoamname = dnszone1_mname,
diff --git a/tests/test_xmlrpc/test_trust_plugin.py b/tests/test_xmlrpc/test_trust_plugin.py
index 7627be748137be181ce561aa774a1258b0ba253f..61e902c15ff6b8f01bc1333dca6a5aeebce59579 100644
--- a/tests/test_xmlrpc/test_trust_plugin.py
+++ b/tests/test_xmlrpc/test_trust_plugin.py
@@ -41,8 +41,8 @@ class test_trustconfig(Declarative):
     @classmethod
     def setUpClass(cls):
         super(test_trustconfig, cls).setUpClass()
-        if not api.Backend.xmlclient.isconnected():
-            api.Backend.xmlclient.connect(fallback=False)
+        if not api.Backend.rpcclient.isconnected():
+            api.Backend.rpcclient.connect(fallback=False)
         try:
            api.Command['trustconfig_show'](trust_type=u'ad')
         except errors.NotFound:
diff --git a/tests/test_xmlrpc/xmlrpc_test.py b/tests/test_xmlrpc/xmlrpc_test.py
index 610fa97c56639331c542c9f0d1da8be839a11a2c..1dc5dfaa4b1d0dc3ab622ef86dd6383571c6da7f 100644
--- a/tests/test_xmlrpc/xmlrpc_test.py
+++ b/tests/test_xmlrpc/xmlrpc_test.py
@@ -76,8 +76,8 @@ fuzzy_dergeneralizedtime = Fuzzy('^[0-9]{14}Z$')
 fuzzy_string = Fuzzy(type=basestring)
 
 try:
-    if not api.Backend.xmlclient.isconnected():
-        api.Backend.xmlclient.connect(fallback=False)
+    if not api.Backend.rpcclient.isconnected():
+        api.Backend.rpcclient.connect(fallback=False)
     res = api.Command['user_show'](u'notfound')
 except errors.NetworkError:
     server_available = False
@@ -139,8 +139,8 @@ class XMLRPC_test(object):
                                 (cls.__module__, api.env.xmlrpc_uri))
 
     def setUp(self):
-        if not api.Backend.xmlclient.isconnected():
-            api.Backend.xmlclient.connect(fallback=False)
+        if not api.Backend.rpcclient.isconnected():
+            api.Backend.rpcclient.connect(fallback=False)
 
     def tearDown(self):
         """
-- 
1.7.7.6

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

Reply via email to