summaryrefslogtreecommitdiffstats
path: root/ipaserver/rpcserver.py
diff options
context:
space:
mode:
Diffstat (limited to 'ipaserver/rpcserver.py')
-rw-r--r--ipaserver/rpcserver.py193
1 files changed, 191 insertions, 2 deletions
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 68d8215f9..06fb5aeea 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -23,11 +23,25 @@ RPC server.
Also see the `ipalib.rpc` module.
"""
+from urlparse import parse_qs
from xmlrpclib import Fault
from ipalib.backend import Executioner
-from ipalib.errors import PublicError, InternalError, CommandError
+from ipalib.errors import PublicError, InternalError, CommandError, JSONError
+from ipalib.request import context, Connection, destroy_context
from ipalib.rpc import xml_dumps, xml_loads
from ipalib.util import make_repr
+import json
+
+
+def read_input(environ):
+ """
+ Read the request body from environ['wsgi.input'].
+ """
+ try:
+ length = int(environ.get('CONTENT_LENGTH'))
+ except (ValueError, TypeError):
+ return
+ return environ['wsgi.input'].read(length)
def params_2_args_options(params):
@@ -39,13 +53,116 @@ def params_2_args_options(params):
return (params, dict())
-class xmlserver(Executioner):
+def nicify_query(query, encoding='utf-8'):
+ if not query:
+ return
+ for (key, value) in query.iteritems():
+ if len(value) == 0:
+ yield (key, None)
+ elif len(value) == 1:
+ yield (key, value[0].decode(encoding))
+ else:
+ yield (key, tuple(v.decode(encoding) for v in value))
+
+
+def extract_query(environ):
+ """
+ Return the query as a ``dict``, or ``None`` if no query is presest.
+ """
+ qstr = None
+ if environ['REQUEST_METHOD'] == 'POST':
+ if environ['CONTENT_TYPE'] == 'application/x-www-form-urlencoded':
+ qstr = read_input(environ)
+ elif environ['REQUEST_METHOD'] == 'GET':
+ qstr = environ['QUERY_STRING']
+ if qstr:
+ query = dict(nicify_query(
+ parse_qs(qstr, keep_blank_values=True)
+ ))
+ else:
+ query = {}
+ environ['wsgi.query'] = query
+ return query
+
+
+class WSGIExecutioner(Executioner):
+ """
+ Base class for execution backends with a WSGI application interface.
+ """
+
+ def finalize(self):
+ url = self.env['mount_' + self.name]
+ if url.startswith('/'):
+ self.url = url
+ else:
+ self.url = self.env.mount_ipa + url
+ super(WSGIExecutioner, self).finalize()
+
+ def execute(self, environ):
+ result = None
+ error = None
+ _id = None
+ try:
+ self.create_context(ccache=environ.get('KRB5CCNAME'))
+ if (
+ environ.get('CONTENT_TYPE', '').startswith(self.content_type)
+ and environ['REQUEST_METHOD'] == 'POST'
+ ):
+ data = read_input(environ)
+ (name, args, options, _id) = self.unmarshal(data)
+ else:
+ (name, args, options, _id) = self.simple_unmarshal(environ)
+ if name not in self.Command:
+ raise CommandError(name=name)
+ result = self.Command[name](*args, **options)
+ except PublicError, e:
+ error = e
+ except StandardError, e:
+ self.exception(
+ 'non-public: %s: %s', e.__class__.__name__, str(e)
+ )
+ error = InternalError()
+ finally:
+ destroy_context()
+ return self.marshal(result, error, _id)
+
+ def simple_unmarshal(self, environ):
+ name = environ['PATH_INFO'].strip('/')
+ options = extract_query(environ)
+ return (name, tuple(), options, None)
+
+ def __call__(self, environ, start_response):
+ """
+ WSGI application for execution.
+ """
+ try:
+ status = '200 OK'
+ response = self.execute(environ)
+ headers = [('Content-Type', self.content_type + '; charset=utf-8')]
+ except StandardError, e:
+ self.exception('%s.__call__():', self.name)
+ status = '500 Internal Server Error'
+ response = status
+ headers = [('Content-Type', 'text/plain')]
+ start_response(status, headers)
+ return [response]
+
+ def unmarshal(self, data):
+ raise NotImplementedError('%s.unmarshal()' % self.fullname)
+
+ def marshal(self, result, error, _id=None):
+ raise NotImplementedError('%s.marshal()' % self.fullname)
+
+
+class xmlserver(WSGIExecutioner):
"""
Execution backend plugin for XML-RPC server.
Also see the `ipalib.rpc.xmlclient` plugin.
"""
+ content_type = 'text/xml'
+
def finalize(self):
self.__system = {
'system.listMethods': self.listMethods,
@@ -79,3 +196,75 @@ class xmlserver(Executioner):
self.info('response: %s: %s', e.__class__.__name__, str(e))
response = Fault(e.errno, e.strerror)
return xml_dumps(response, methodresponse=True)
+
+ def unmarshal(self, data):
+ (params, name) = xml_loads(data)
+ (args, options) = params_2_args_options(params)
+ return (name, args, options, None)
+
+ def marshal(self, result, error, _id=None):
+ if error:
+ response = Fault(error.errno, error.strerror)
+ else:
+ response = (result,)
+ return xml_dumps(response, methodresponse=True)
+
+
+class jsonserver(WSGIExecutioner):
+ """
+ JSON RPC server.
+
+ For information on the JSON-RPC spec, see:
+
+ http://json-rpc.org/wiki/specification
+ """
+
+ content_type = 'application/json'
+
+ def marshal(self, result, error, _id=None):
+ if error:
+ assert isinstance(error, PublicError)
+ error = dict(
+ code=error.errno,
+ message=error.strerror,
+ name=error.__class__.__name__,
+ kw=dict(error.kw),
+ )
+ response = dict(
+ result=result,
+ error=error,
+ id=_id,
+ )
+ return json.dumps(response, sort_keys=True, indent=4)
+
+ def unmarshal(self, data):
+ try:
+ d = json.loads(data)
+ except ValueError, e:
+ raise JSONError(error=e)
+ if not isinstance(d, dict):
+ raise JSONError(error='Request must be a dict')
+ if 'method' not in d:
+ raise JSONError(error='Request is missing "method"')
+ if 'params' not in d:
+ raise JSONError(error='Request is missing "params"')
+ method = d['method']
+ params = d['params']
+ _id = d.get('id')
+ if not isinstance(params, (list, tuple)):
+ raise JSONError(error='params must be a list')
+ if len(params) != 2:
+ raise JSONError(
+ error='params must contain [args, options]'
+ )
+ args = params[0]
+ if not isinstance(args, (list, tuple)):
+ raise JSONError(
+ error='params[0] (aka args) must be a list'
+ )
+ options = params[1]
+ if not isinstance(options, dict):
+ raise JSONError(
+ error='params[1] (aka options) must be a dict'
+ )
+ return (method, args, options, _id)