diff options
Diffstat (limited to 'ipaserver')
-rw-r--r-- | ipaserver/plugins/xmlserver.py | 3 | ||||
-rw-r--r-- | ipaserver/rpcserver.py | 193 |
2 files changed, 193 insertions, 3 deletions
diff --git a/ipaserver/plugins/xmlserver.py b/ipaserver/plugins/xmlserver.py index 51b990ffb..10c94d452 100644 --- a/ipaserver/plugins/xmlserver.py +++ b/ipaserver/plugins/xmlserver.py @@ -25,5 +25,6 @@ XML-RPC client plugin. from ipalib import api if 'in_server' in api.env and api.env.in_server is True: - from ipaserver.rpcserver import xmlserver + from ipaserver.rpcserver import xmlserver, jsonserver api.register(xmlserver) + api.register(jsonserver) 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) |