URL: https://github.com/freeipa/freeipa/pull/407 Author: tiran Title: #407: New lite-server implementation Action: opened
PR body: """ The new development server depends on werkzeug instead of paste. The werkzeug WSGI server comes with some additional features, most noticeable multi-processing server. The IPA framework is not compatible with threaded servers. Werkzeug can serve static files easily and has a fast auto-reloader. The new lite-server implementation depends on PR 314 (privilege separation). For Python 3 support, it additionally depends on PR 393. Signed-off-by: Christian Heimes <chei...@redhat.com> """ To pull the PR as Git branch: git remote add ghfreeipa https://github.com/freeipa/freeipa git fetch ghfreeipa pull/407/head:pr407 git checkout pr407
From f7221e527d93ff1bf802c4406959fc7eaefd2da4 Mon Sep 17 00:00:00 2001 From: Christian Heimes <chei...@redhat.com> Date: Sat, 21 Jan 2017 19:34:12 +0100 Subject: [PATCH] New lite-server implementation The new development server depends on werkzeug instead of paste. The werkzeug WSGI server comes with some additional features, most noticeable multi-processing server. The IPA framework is not compatible with threaded servers. Werkzeug can serve static files easily and has a fast auto-reloader. The new lite-server implementation depends on PR 314 (privilege separation). For Python 3 support, it additionally depends on PR 393. Signed-off-by: Christian Heimes <chei...@redhat.com> --- lite-server.py | 249 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 139 insertions(+), 110 deletions(-) diff --git a/lite-server.py b/lite-server.py index cd4f09c..89482b4 100755 --- a/lite-server.py +++ b/lite-server.py @@ -1,125 +1,115 @@ -#!/usr/bin/python2 - -# Authors: -# Jason Gerard DeRose <jder...@redhat.com> -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information +#!/usr/bin/env python # -# 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, either version 3 of the License, or -# (at your option) any later version. +# Copyright (C) 2017 FreeIPA Contributors see COPYING for license # -# 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, see <http://www.gnu.org/licenses/>. +"""In-tree development server -""" -In-tree paste-based test server. +The dev server requires a Kerberos TGT and a file based credential cache: -This uses the *Python Paste* WSGI server. For more info, see: + $ mkdir -p ~/.ipa + $ export KRB5CCNAME=~/.ipa/ccache + $ kinit admin + $ ./lite-server.py - http://pythonpaste.org/ +Optionally you can set KRB5_CONFIG to use a custom Kerberos configuration +instead of /etc/krb5.conf. -Unfortunately, SSL support is broken under Python 2.6 with paste 1.7.2, see: +By default the dev server supports HTTP only. To switch to HTTPS, you can put +a PEM file at ~/.ipa/lite.pem. The PEM file must contain a server certificate, +its unencrypted private key and intermediate chain certs (if applicable). - http://trac.pythonpaste.org/pythonpaste/ticket/314 -""" +Prerequisite +------------ + +Additionally to build and runtime requirements of FreeIPA, the dev server +depends on the werkzeug framework and optionally watchdog for auto-reloading. +You may also have to enable a development COPR. + + $ sudo dnf install -y dnf-plugins-core + $ sudo dnf builddep --spec freeipa.spec.in + $ sudo dnf install -y python-werkzeug python2-watchdog \ + python3-werkzeug python3-watchdog + $ ./autogen.sh + $ make -from os import path, getcwd +For more information see + + * http://www.freeipa.org/page/Build + * http://www.freeipa.org/page/Testing + +""" +import os import optparse # pylint: disable=deprecated-module -from paste import httpserver -import paste.gzipper -from paste.urlmap import URLMap +import ssl +import sys + +from werkzeug.exceptions import NotFound +from werkzeug.serving import run_simple +from werkzeug.utils import redirect +from werkzeug.wsgi import DispatcherMiddleware, SharedDataMiddleware + + +HERE = os.path.dirname(os.path.abspath(__file__)) +STATIC_FILES = { + '/ipa/ui': os.path.join(HERE, 'install/ui'), + '/ipa/ui/js': os.path.join(HERE, 'install/ui/src'), + '/ipa/ui/js/dojo': os.path.join(HERE, 'install/ui/build/dojo'), + '/ipa/ui/fonts': '/usr/share/fonts', +} + +# import ipa Python packages from script directory +sys.path.insert(0, HERE) from ipalib import api -from subprocess import check_output, CalledProcessError -import re - -# Ugly hack for test purposes only. GSSAPI has no way to get default ccache -# name, but we don't need it outside test server -def get_default_ccache_name(): - try: - out = check_output(['klist']) - except CalledProcessError: - raise RuntimeError("Default ccache not found. Did you kinit?") - match = re.match(r'^Ticket cache:\s*(\S+)', out) - if not match: - raise RuntimeError("Cannot obtain ccache name") - return match.group(1) +from ipalib.krb_utils import krb5_parse_ccache, krb5_unparse_ccache + + +def get_ccname(): + """Retrieve and validate Kerberos credential cache + + Only FILE schema is supported. + """ + ccname = os.environ.get('KRB5CCNAME') + if ccname is None: + raise ValueError("KRB5CCNAME env var is not set.") + scheme, location = krb5_parse_ccache(ccname) + if scheme != 'FILE': # MEMORY makes no sense + raise ValueError("Unsupported KRB5CCNAME scheme {}".format(scheme)) + if not os.path.isfile(location): + raise ValueError("KRB5CCNAME file '{}' does not exit".format(location)) + return krb5_unparse_ccache(scheme, location) class KRBCheater(object): - def __init__(self, app): + """Add KRB5CCNAME to WSGI environ + """ + def __init__(self, app, ccname): self.app = app - self.url = app.url - self.ccname = get_default_ccache_name() + self.ccname = ccname def __call__(self, environ, start_response): environ['KRB5CCNAME'] = self.ccname return self.app(environ, start_response) -class WebUIApp(object): - INDEX_FILE = 'index.html' - EXTENSION_TO_MIME_MAP = { - 'xhtml': 'text/html', - 'html': 'text/html', - 'js': 'text/javascript', - 'inc': 'text/html', - 'css': 'text/css', - 'png': 'image/png', - 'json': 'text/javascript', - } - - def __init__(self): - self.url = '/ipa/ui' - - def __call__(self, environ, start_response): - path_info = environ['PATH_INFO'].lstrip('/') - if path_info == '': - path_info = self.INDEX_FILE - requested_file = path.join(getcwd(), 'install/ui/', path_info) - extension = requested_file.rsplit('.', 1)[-1] - - if extension not in self.EXTENSION_TO_MIME_MAP: - start_response('404 Not Found', [('Content-Type', 'text/plain')]) - return ['NOT FOUND'] - mime_type = self.EXTENSION_TO_MIME_MAP[extension] - - f = None - try: - f = open(requested_file, 'r') - api.log.info('Request file %s' % requested_file) - start_response('200 OK', [('Content-Type', mime_type)]) - return [f.read()] - except IOError: - start_response('404 Not Found', [('Content-Type', 'text/plain')]) - return ['NOT FOUND'] - finally: - if f is not None: - f.close() - api.log.info('Request done') - - -if __name__ == '__main__': +def init_api(): + """Initialize FreeIPA API from command line + """ parser = optparse.OptionParser() - parser.add_option('--dev', - help='Run WebUI in development mode (requires FireBug)', + parser.add_option( + '--dev', + help='Run WebUI in development mode', default=True, action='store_false', dest='prod', ) - parser.add_option('--host', + parser.add_option( + '--host', help='Listen on address HOST (default 127.0.0.1)', default='127.0.0.1', ) - parser.add_option('--port', + parser.add_option( + '--port', help='Listen on PORT (default 8888)', default=8888, type='int', @@ -127,7 +117,13 @@ def __call__(self, environ, start_response): api.env.in_server = True api.env.startup_traceback = True - (options, args) = api.bootstrap_with_global_options(parser, context='lite') + # workaround for RefererError in rpcserver + api.env.in_tree = True + # workaround: AttributeError: locked: cannot set ldap2.time_limit to None + api.env.mode = 'production' + + # pylint: disable=unused-variable + options, args = api.bootstrap_with_global_options(parser, context='lite') api.env._merge( lite_port=options.port, lite_host=options.host, @@ -136,23 +132,56 @@ def __call__(self, environ, start_response): ) api.finalize() - urlmap = URLMap() - apps = [ - ('IPA', KRBCheater(api.Backend.wsgi_dispatch)), - ('webUI', KRBCheater(WebUIApp())), - ] - for (name, app) in apps: - urlmap[app.url] = app - api.log.info('Mounting %s at %s', name, app.url) - - if path.isfile(api.env.lite_pem): - pem = api.env.lite_pem + +def redirect_ui(app): + """Redirect to /ipa/ui/index.html + """ + def wsgi(environ, start_response): + path_info = environ['PATH_INFO'] + if path_info in {'/', '/ipa', '/ipa/', '/ipa/ui', '/ipa/ui/index.html'}: + response = redirect('/ipa/ui/') + return response(environ, start_response) + if path_info == '/ipa/ui/test': + response = redirect('/ipa/ui/test/') + return response(environ, start_response) + if path_info == '/favicon.ico': + response = redirect('/ipa/ui/favicon.ico') + return response(environ, start_response) + # rewrite path, SharedDataMiddleware does not handle index.html + if path_info.startswith('/ipa/ui/') and path_info.endswith('/'): + environ['PATH_INFO'] = path_info + 'index.html' + return app(environ, start_response) + return wsgi + + +def main(): + ccname = get_ccname() + init_api() + + if os.path.isfile(api.env.lite_pem): + ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) + ctx.load_cert_chain(api.env.lite_pem) else: - api.log.info('To enable SSL, place PEM file at %r', api.env.lite_pem) - pem = None + ctx = None - httpserver.serve(paste.gzipper.middleware(urlmap), - host=api.env.lite_host, + app = NotFound() + app = DispatcherMiddleware(app, { + '/ipa': KRBCheater(api.Backend.wsgi_dispatch, ccname), + }) + app = SharedDataMiddleware(app, STATIC_FILES) + app = redirect_ui(app) + + run_simple( + hostname=api.env.lite_host, port=api.env.lite_port, - ssl_pem=pem, + application=app, + processes=5, + ssl_context=ctx, + use_reloader=True, + # debugger doesn't work because framework catches all exceptions + # use_debugger=not api.env.webui_prod, + # use_evalex=not api.env.webui_prod, ) + +if __name__ == '__main__': + main()
-- Manage your subscription for the Freeipa-devel mailing list: https://www.redhat.com/mailman/listinfo/freeipa-devel Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code