This is part1 of the mod_wsgi transition. It provides a new plugin: api.Backend.session. This is a WSGI middleware component that will create the LDAP connection and then route the request to the appropriate WSGI application (/xml or /json or /ui).
The end result is that we have a single entry point (/ipa) instead of 3, and we also use the exact same code path to create and destroy the LDAP connection (which is obviously good for security). All this still is running under mod_python, but my next patch switches things to mod_wsgi (still have a few issues on that front).
>From 541616b0290d309a686bf66febb370ef0cade06a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose <jder...@redhat.com> Date: Tue, 23 Feb 2010 10:53:47 -0700 Subject: [PATCH] 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 b956293..f5987fb 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" <Location "/ipa"> @@ -32,34 +24,42 @@ Alias /ipa/crl "/var/lib/pki-ca/publish" KrbSaveCredentials on Require valid-user ErrorDocument 401 /ipa/errors/unauthorized.html -</Location> -<Location "/ipa/xml"> 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 -</Location> -<Location "/ipa/json"> - SetHandler python-program - PythonInterpreter main_interpreter - PythonHandler ipaserver::jsonrpc - PythonDebug Off - PythonOption SCRIPT_NAME /ipa/json - PythonAutoReload Off </Location> -<Location "/ipa/ui"> - SetHandler python-program - PythonInterpreter main_interpreter - PythonHandler ipaserver::webui - PythonDebug Off - PythonOption SCRIPT_NAME /ipa/ui - PythonAutoReload Off -</Location> +#<Location "/ipa/xml"> +# SetHandler python-program +# PythonInterpreter main_interpreter +# PythonHandler ipaserver::xmlrpc +# PythonDebug Off +# PythonOption SCRIPT_NAME /ipa/xml +# PythonAutoReload Off +#</Location> + +#<Location "/ipa/json"> +# SetHandler python-program +# PythonInterpreter main_interpreter +# PythonHandler ipaserver::jsonrpc +# PythonDebug Off +# PythonOption SCRIPT_NAME /ipa/json +# PythonAutoReload Off +#</Location> + +#<Location "/ipa/ui"> +# SetHandler python-program +# PythonInterpreter main_interpreter +# PythonHandler ipaserver::webui +# PythonDebug Off +# PythonOption SCRIPT_NAME /ipa/ui +# PythonAutoReload Off +#</Location> Alias /ipa-assets/ "/var/cache/ipa/assets/" <Directory "/var/cache/ipa/assets"> @@ -72,14 +72,39 @@ Alias /ipa-assets/ "/var/cache/ipa/assets/" </Directory> +<Location "/ipa/errors"> + SetHandler None +</Location> + +<Location "/ipa/config"> + SetHandler None +</Location> + +<Location "/ipa/crl"> + SetHandler None +</Location> + + +# 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 <Directory "/usr/share/ipa/html"> + SetHandler None AllowOverride None Satisfy Any Allow from all </Directory> + +# For CRL publishing +Alias /ipa/crl "/var/lib/pki-ca/publish" + <Directory "/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 79ddbca..a942076 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 1b62255..874ac3e 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 cbbf148..290bef6 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 4a5040e..ad402cd 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 = """<html> +<head> +<title>404 Not Found</title> +</head> +<body> +<h1>Not Found</h1> +<p> +The requested URL <strong>%(url)s</strong> was not found on this server. +</p> +</body> +</html>""" + + +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 037fc76..0e892d8 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 65fb555..ba7cfe3 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 12d37ca..294d349 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='<script>do_bad_stuff();</script>') + 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. -- 1.6.3.3
_______________________________________________ Freeipa-devel mailing list Freeipa-devel@redhat.com https://www.redhat.com/mailman/listinfo/freeipa-devel