From 942919bef77030b10a96cab66ab878a8a3d7ef10 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 23 Feb 2010 10:53:47 -0700 Subject: Consolidate to single WSGI entry point --- install/conf/ipa.conf | 81 +++++++++++------- ipalib/constants.py | 2 +- ipaserver/__init__.py | 4 + ipaserver/plugins/xmlserver.py | 10 +-- ipaserver/rpcserver.py | 149 ++++++++++++++++++++++++++------- ipawebui/__init__.py | 11 +-- lite-server.py | 6 +- tests/test_ipaserver/test_rpcserver.py | 96 ++++++++++++++++++++- 8 files changed, 276 insertions(+), 83 deletions(-) diff --git a/install/conf/ipa.conf b/install/conf/ipa.conf index b9562936f..f5987fbea 100644 --- a/install/conf/ipa.conf +++ b/install/conf/ipa.conf @@ -11,14 +11,6 @@ PythonImport ipaserver main_interpreter # This is required so the auto-configuration works with Firefox 2+ AddType application/java-archive jar -# This is where we redirect on failed auth -Alias /ipa/errors "/usr/share/ipa/html" - -# For the MIT Windows config files -Alias /ipa/config "/usr/share/ipa/html" - -# For CRL publishing -Alias /ipa/crl "/var/lib/pki-ca/publish" @@ -32,34 +24,42 @@ Alias /ipa/crl "/var/lib/pki-ca/publish" KrbSaveCredentials on Require valid-user ErrorDocument 401 /ipa/errors/unauthorized.html - - SetHandler python-program PythonInterpreter main_interpreter - PythonHandler ipaserver::xmlrpc + PythonHandler ipaserver::handler PythonDebug Off - PythonOption SCRIPT_NAME /ipa/xml + PythonOption SCRIPT_NAME /ipa PythonAutoReload Off - - - SetHandler python-program - PythonInterpreter main_interpreter - PythonHandler ipaserver::jsonrpc - PythonDebug Off - PythonOption SCRIPT_NAME /ipa/json - PythonAutoReload Off - - SetHandler python-program - PythonInterpreter main_interpreter - PythonHandler ipaserver::webui - PythonDebug Off - PythonOption SCRIPT_NAME /ipa/ui - PythonAutoReload Off - +# +# SetHandler python-program +# PythonInterpreter main_interpreter +# PythonHandler ipaserver::xmlrpc +# PythonDebug Off +# PythonOption SCRIPT_NAME /ipa/xml +# PythonAutoReload Off +# + +# +# SetHandler python-program +# PythonInterpreter main_interpreter +# PythonHandler ipaserver::jsonrpc +# PythonDebug Off +# PythonOption SCRIPT_NAME /ipa/json +# PythonAutoReload Off +# + +# +# SetHandler python-program +# PythonInterpreter main_interpreter +# PythonHandler ipaserver::webui +# PythonDebug Off +# PythonOption SCRIPT_NAME /ipa/ui +# PythonAutoReload Off +# Alias /ipa-assets/ "/var/cache/ipa/assets/" @@ -72,14 +72,39 @@ Alias /ipa-assets/ "/var/cache/ipa/assets/" + + SetHandler None + + + + SetHandler None + + + + SetHandler None + + + +# This is where we redirect on failed auth +Alias /ipa/errors "/usr/share/ipa/html" + +# For the MIT Windows config files +Alias /ipa/config "/usr/share/ipa/html" + # Do no authentication on the directory that contains error messages + SetHandler None AllowOverride None Satisfy Any Allow from all + +# For CRL publishing +Alias /ipa/crl "/var/lib/pki-ca/publish" + + SetHandler None AllowOverride None Options Indexes FollowSymLinks Satisfy Any diff --git a/ipalib/constants.py b/ipalib/constants.py index 79ddbca8f..a94207696 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -108,7 +108,7 @@ DEFAULT_CONFIG = ( ('mount_ipa', '/ipa/'), ('mount_xmlserver', 'xml'), ('mount_jsonserver', 'json'), - ('mount_webui', 'ui/'), + ('mount_webui', 'ui'), ('mount_webui_assets', '/ipa-assets/'), # WebUI stuff: diff --git a/ipaserver/__init__.py b/ipaserver/__init__.py index 1b6225536..874ac3e24 100644 --- a/ipaserver/__init__.py +++ b/ipaserver/__init__.py @@ -222,3 +222,7 @@ def webui(req): mod_python handler for web-UI requests (place holder). """ return adapter(req, ui) + + +def handler(req): + return adapter(req, api.Backend.session) diff --git a/ipaserver/plugins/xmlserver.py b/ipaserver/plugins/xmlserver.py index cbbf14893..290bef6a2 100644 --- a/ipaserver/plugins/xmlserver.py +++ b/ipaserver/plugins/xmlserver.py @@ -19,17 +19,13 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -XML-RPC client plugin. +Loads WSGI server plugins. """ from ipalib import api if 'in_server' in api.env and api.env.in_server is True: - from ipaserver.rpcserver import xmlserver, jsonserver - from ipalib.backend import Executioner + from ipaserver.rpcserver import session, xmlserver, jsonserver + api.register(session) api.register(xmlserver) api.register(jsonserver) - - class session(Executioner): - pass - api.register(session) diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index 4a5040e2f..ad402cdf8 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -24,6 +24,7 @@ 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 @@ -31,6 +32,33 @@ 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): @@ -85,17 +113,81 @@ def extract_query(environ): 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): - url = self.env['mount_' + self.name] - if url.startswith('/'): - self.url = url - else: - self.url = self.env.mount_ipa + url + self.url = self.env.mount_ipa + self.key super(WSGIExecutioner, self).finalize() def wsgi_execute(self, environ): @@ -103,28 +195,24 @@ class WSGIExecutioner(Executioner): error = None _id = None try: - 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() + 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): @@ -155,11 +243,6 @@ class WSGIExecutioner(Executioner): raise NotImplementedError('%s.marshal()' % self.fullname) - -class session(Executioner): - pass - - class xmlserver(WSGIExecutioner): """ Execution backend plugin for XML-RPC server. @@ -168,6 +251,7 @@ class xmlserver(WSGIExecutioner): """ content_type = 'text/xml' + key = 'xml' def finalize(self): self.__system = { @@ -226,6 +310,7 @@ class jsonserver(WSGIExecutioner): """ content_type = 'application/json' + key = 'json' def marshal(self, result, error, _id=None): if error: diff --git a/ipawebui/__init__.py b/ipawebui/__init__.py index 037fc7647..0e892d8a2 100644 --- a/ipawebui/__init__.py +++ b/ipawebui/__init__.py @@ -47,7 +47,6 @@ def join_url(base, url): class WebUI(Application): def __init__(self, api): self.api = api - self.session = api.Backend.session baseurl = api.env.mount_ipa assets = Assets( url=join_url(baseurl, api.env.mount_webui_assets), @@ -60,16 +59,8 @@ class WebUI(Application): widgets=create_widgets(), prod=api.env.webui_prod, ) + self.api.Backend.session.mount(self, api.env.mount_webui) - def __call__(self, environ, start_response): - self.session.create_context(ccache=environ.get('KRB5CCNAME')) - try: - query = extract_query(environ) - print query - response = super(WebUI, self).__call__(environ, start_response) - finally: - destroy_context() - return response def create_wsgi_app(api): diff --git a/lite-server.py b/lite-server.py index 65fb555a8..ba7cfe3d3 100755 --- a/lite-server.py +++ b/lite-server.py @@ -86,13 +86,11 @@ if __name__ == '__main__': urlmap = URLMap() apps = [ - ('XML RPC', api.Backend.xmlserver), - ('JSON RPC', api.Backend.jsonserver), + ('IPA', KRBCheater(api.Backend.session)), ('Assets', AssetsApp(ui.assets)), - ('Web UI', ui), ] for (name, app) in apps: - urlmap[app.url] = KRBCheater(app) + urlmap[app.url] = app api.log.info('Mounting %s at %s', name, app.url) if path.isfile(api.env.lite_pem): diff --git a/tests/test_ipaserver/test_rpcserver.py b/tests/test_ipaserver/test_rpcserver.py index 12d37ca30..294d349d3 100644 --- a/tests/test_ipaserver/test_rpcserver.py +++ b/tests/test_ipaserver/test_rpcserver.py @@ -21,13 +21,56 @@ Test the `ipaserver.rpc` module. """ -from tests.util import create_test_api, raises, PluginTester +from tests.util import create_test_api, assert_equal, raises, PluginTester from tests.data import unicode_str from ipalib import errors, Command from ipaserver import rpcserver from ipalib.compat import json +class StartResponse(object): + def __init__(self): + self.reset() + + def reset(self): + self.status = None + self.headers = None + + def __call__(self, status, headers): + assert self.status is None + assert self.headers is None + assert isinstance(status, str) + assert isinstance(headers, list) + self.status = status + self.headers = headers + + +def test_not_found(): + f = rpcserver.not_found + t = rpcserver._not_found_template + s = StartResponse() + + # Test with an innocent URL: + d = dict(SCRIPT_NAME='/ipa', PATH_INFO='/foo/stuff') + assert_equal( + f(d, s), + [t % dict(url='/ipa/foo/stuff')] + ) + assert s.status == '404 Not Found' + assert s.headers == [('Content-Type', 'text/html')] + + # Test when URL contains any of '<>&' + s.reset() + d = dict(SCRIPT_NAME=' ', PATH_INFO='') + assert_equal( + f(d, s), + [t % dict(url='&nbsp;<script>do_bad_stuff();</script>')] + ) + assert s.status == '404 Not Found' + assert s.headers == [('Content-Type', 'text/html')] + + + def test_params_2_args_options(): """ Test the `ipaserver.rpcserver.params_2_args_options` function. @@ -42,6 +85,57 @@ def test_params_2_args_options(): assert f((options,) + args) == ((options,) + args, dict()) +class test_session(object): + klass = rpcserver.session + + def test_route(self): + def app1(environ, start_response): + return ( + 'from 1', + [environ[k] for k in ('SCRIPT_NAME', 'PATH_INFO')] + ) + + def app2(environ, start_response): + return ( + 'from 2', + [environ[k] for k in ('SCRIPT_NAME', 'PATH_INFO')] + ) + + inst = self.klass() + inst.mount(app1, 'foo') + inst.mount(app2, 'bar') + + d = dict(SCRIPT_NAME='/ipa', PATH_INFO='/foo/stuff') + assert inst.route(d, None) == ('from 1', ['/ipa/foo', '/stuff']) + + d = dict(SCRIPT_NAME='/ipa', PATH_INFO='/bar') + assert inst.route(d, None) == ('from 2', ['/ipa/bar', '']) + + def test_mount(self): + def app1(environ, start_response): + pass + + def app2(environ, start_response): + pass + + # Test that mount works: + inst = self.klass() + inst.mount(app1, 'foo') + assert inst['foo'] is app1 + assert list(inst) == ['foo'] + + # Test that StandardError is raise if trying override a mount: + e = raises(StandardError, inst.mount, app2, 'foo') + assert str(e) == '%s.mount(): cannot replace %r with %r at %r' % ( + 'session', app1, app2, 'foo' + ) + + # Test mounting a second app: + inst.mount(app2, 'bar') + assert inst['bar'] is app2 + assert list(inst) == ['bar', 'foo'] + + class test_xmlserver(PluginTester): """ Test the `ipaserver.rpcserver.xmlserver` plugin. -- cgit