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='&nbsp;', PATH_INFO='<script>do_bad_stuff();</script>')
+    assert_equal(
+        f(d, s),
+        [t % dict(url='&amp;nbsp;&lt;script&gt;do_bad_stuff();&lt;/script&gt;')]
+    )
+    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

Reply via email to