# Authors: # Jason Gerard DeRose # # Copyright (C) 2008 Red Hat # see file 'COPYING' for use and warranty information # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; version 2 only # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ RPC server. Also see the `ipalib.rpc` module. """ from cgi import parse_qs from xml.sax.saxutils import escape from xmlrpclib import Fault from ipalib.backend import Executioner 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 from ipalib.compat import json from wsgiref.util import shift_path_info _not_found_template = """ 404 Not Found

Not Found

The requested URL %(url)s was not found on this server.

""" def not_found(environ, start_response): """ Return a 404 Not Found error. """ status = '404 Not Found' response_headers = [('Content-Type', 'text/html')] start_response(status, response_headers) output = _not_found_template % dict( url=escape(environ['SCRIPT_NAME'] + environ['PATH_INFO']) ) return [output] 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): assert type(params) is tuple if len(params) == 0: return (tuple(), dict()) if type(params[-1]) is dict: return (params[:-1], params[-1]) return (params, dict()) 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 session(Executioner): """ WSGI routing middleware and entry point into IPA server. The `session` plugin is the entry point into the IPA server. It will create an LDAP connection (from a session cookie or the KRB5CCNAME header) and then dispatch the request to the appropriate application. In WSGI parlance, `session` is *middleware*. """ def __init__(self): super(session, self).__init__() self.__apps = {} def __iter__(self): for key in sorted(self.__apps): yield key def __getitem__(self, key): return self.__apps[key] def __contains__(self, key): return key in self.__apps def __call__(self, environ, start_response): try: self.create_context(ccache=environ.get('KRB5CCNAME')) return self.route(environ, start_response) finally: destroy_context() def finalize(self): self.url = self.env['mount_ipa'] super(session, self).finalize() def route(self, environ, start_response): key = shift_path_info(environ) if key in self.__apps: app = self.__apps[key] return app(environ, start_response) return not_found(environ, start_response) def mount(self, app, key): """ Mount the WSGI application *app* at *key*. """ # if self.__islocked__(): # raise StandardError('%s.mount(): locked, cannot mount %r at %r' % ( # self.name, app, key) # ) if key in self.__apps: raise StandardError('%s.mount(): cannot replace %r with %r at %r' % ( self.name, self.__apps[key], app, key) ) self.info('Mounting %r at %r', app, key) self.__apps[key] = app class WSGIExecutioner(Executioner): """ Base class for execution backends with a WSGI application interface. """ key = '' def set_api(self, api): super(WSGIExecutioner, self).set_api(api) if 'session' in self.api.Backend: self.api.Backend.session.mount(self, self.key) def finalize(self): self.url = self.env.mount_ipa + self.key super(WSGIExecutioner, self).finalize() def wsgi_execute(self, environ): result = None error = None _id = None try: 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() 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.wsgi_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' key = 'xml' def finalize(self): self.__system = { 'system.listMethods': self.listMethods, 'system.methodSignature': self.methodSignature, 'system.methodHelp': self.methodHelp, } super(xmlserver, self).finalize() def listMethods(self, *params): return tuple(name.decode('UTF-8') for name in self.Command) def methodSignature(self, *params): return u'methodSignature not implemented' def methodHelp(self, *params): return u'methodHelp not implemented' def marshaled_dispatch(self, data, ccache, client_ip): """ Execute the XML-RPC request contained in ``data``. """ try: self.create_context(ccache=ccache, client_ip=client_ip) (params, name) = xml_loads(data) if name in self.__system: response = (self.__system[name](*params),) else: (args, options) = params_2_args_options(params) response = (self.execute(name, *args, **options),) except PublicError, e: 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' key = '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' ) options = dict((str(k), v) for (k, v) in options.iteritems()) return (method, args, options, _id)