Verified Commit eb82443d authored by Štěpán Henek's avatar Štěpán Henek 🌩

Foris was splitted into two apps (-a, --app wizard/config)

Still there are some regressions such as missing links from the wizard
parent 20ac8248
import argparse
import bottle
from foris.core import get_arg_parser, prepare_main_app
from foris.config_app import prepare_config_app
from foris.wizard_app import prepare_wizard_app
app_map = {
"config": prepare_config_app,
"wizard": prepare_wizard_app,
}
def get_arg_parser():
"""
Create ArgumentParser instance with Foris arguments.
:return: instance of ArgumentParser
"""
parser = argparse.ArgumentParser()
group = parser.add_argument_group("run server")
group.add_argument("-H", "--host", default="0.0.0.0")
group.add_argument("-p", "--port", type=int, default=8080)
group.add_argument("--session-timeout", type=int, default=900,
help="session timeout (in seconds)")
group.add_argument("-s", "--server", choices=["wsgiref", "flup", "cgi"], default="wsgiref")
group.add_argument("-d", "--debug", action="store_true")
group.add_argument("--noauth", action="store_true",
help="disable authentication (available only in debug mode)")
group.add_argument("--nucipath", help="path to Nuci binary")
parser.add_argument("-R", "--routes", action="store_true", help="print routes and exit")
group.add_argument(
"-S", "--static", action="store_true",
help="serve static files directly through foris app (should be used for debug only)"
)
group.add_argument(
"-a", "--app", choices=["config", "wizard"], default="config",
help="sets which app should be started (wizard/config)",
)
return parser
def main():
parser = get_arg_parser()
args = parser.parse_args()
main_app = prepare_main_app(args)
main_app = app_map[args.app](args)
if args.routes:
# routes should be printed and we can safely exit
......
......@@ -26,15 +26,19 @@ import time
from functools import wraps
from foris.nuci import client, filters
from foris.nuci.helpers import write_uci_lang, contract_valid, get_wizard_progress
from foris.nuci.helpers import write_uci_lang, contract_valid
from foris.caches import nuci_cache
from foris.utils import (
redirect_unauthenticated, is_safe_redirect, messages,
WIZARD_NEXT_STEP_ALLOWED_KEY, NUM_WIZARD_STEPS,
redirect_unauthenticated, is_safe_redirect, messages, login_required
)
from foris.middleware.bottle_csrf import update_csrf_token, CSRFValidationError, CSRFPlugin
from foris.utils.routing import reverse
from foris.middleware.bottle_csrf import update_csrf_token, CSRFValidationError
from foris.utils.translators import _, translations
from foris.utils.bottle_stuff import (
clickjacking_protection,
clear_lazy_cache,
disable_caching,
)
logger = logging.getLogger("foris.common")
......@@ -105,41 +109,6 @@ def foris_403_handler(error):
bottle.app().default_error_handler(error)
def login_redirect(step_num, wizard_finished=False):
if step_num >= NUM_WIZARD_STEPS or wizard_finished:
next = bottle.request.GET.get("next")
if next and is_safe_redirect(next, bottle.request.get_header('host')):
bottle.redirect(next)
bottle.redirect(reverse("config_index"))
elif step_num == 1:
bottle.redirect(reverse("wizard_index"))
else:
bottle.redirect(reverse("wizard_step", number=step_num))
@bottle.view("index")
def index():
session = bottle.request.environ['foris.session']
allowed_step_max, wizard_finished = get_wizard_progress(session)
if allowed_step_max == 1:
if session.is_anonymous:
session.recreate()
session["user_authenticated"] = True
else:
session[WIZARD_NEXT_STEP_ALLOWED_KEY] = str(allowed_step_max)
session["wizard_finished"] = wizard_finished
allowed_step_max = int(allowed_step_max)
session.save()
if session.get("user_authenticated"):
login_redirect(allowed_step_max, wizard_finished)
return dict(
luci_path="//%(host)s/%(path)s"
% {'host': bottle.request.get_header('host'), 'path': 'cgi-bin/luci'})
def render_js(filename):
""" Render javascript template to insert a translation
:param filename: name of the file to be translated
......@@ -234,3 +203,49 @@ def require_contract_valid(valid=True):
return func(*args, **kwargs)
return wrapper
return decorator
@login_required
def reboot():
client.reboot()
bottle.redirect(reverse("/"))
def init_default_app(index, include_static=False):
"""
Initialize top-level Foris app - register all routes etc.
:param include_static: include route to static files
:type include_static: bool
:return: instance of Foris Bottle application
"""
app = bottle.app()
app.install(CSRFPlugin())
app.route("/", name="index", callback=index)
app.route("/lang/<lang:re:\w{2}>", name="change_lang", callback=change_lang)
app.route("/", method="POST", name="login", callback=login)
app.route("/logout", name="logout", callback=logout)
app.route("/reboot", name="reboot", callback=reboot)
if include_static:
app.route('/static/<filename:re:.*>', name="static", callback=static)
app.route("/js/<filename:re:.*>", name="render_js", callback=render_js)
return app
def init_common_app(app, prefix):
"""
Initializes Foris application - use this method to apply properties etc.
that should be set to main app and all the mounted apps (i.e. to the
Bottle() instances).
:param app: instance of bottle application to mount
:param prefix: prefix which has been used to mount the application
"""
app.catchall = False # caught by ReportingMiddleware
app.error_handler[403] = foris_403_handler
app.add_hook('after_request', clickjacking_protection)
app.add_hook('after_request', disable_caching)
app.add_hook('after_request', clear_lazy_cache)
app.config['prefix'] = prefix
......@@ -33,9 +33,9 @@ from foris.config_handlers import (
from foris.nuci import client
from foris.nuci.client import filters
from foris.nuci.exceptions import ConfigRestoreError
from foris.nuci.helpers import contract_valid
from foris.nuci.helpers import contract_valid, get_wizard_progress
from foris.nuci.preprocessors import preproc_disabled_to_agreed
from foris.utils import login_required, messages
from foris.utils import login_required, messages, is_safe_redirect
from foris.middleware.bottle_csrf import CSRFPlugin
from foris.utils.routing import reverse
......@@ -168,10 +168,6 @@ class MaintenanceConfigPage(ConfigPageMixin, backups.MaintenanceHandler):
return bottle.static_file(filename, directory,
mimetype="application/x-bz2", download=True)
def _action_reboot(self):
client.reboot()
bottle.redirect(reverse("config_index"))
def _action_save_notifications(self):
if bottle.request.method != 'POST':
messages.error(_("Wrong HTTP method."))
......@@ -202,8 +198,6 @@ class MaintenanceConfigPage(ConfigPageMixin, backups.MaintenanceHandler):
def call_action(self, action):
if action == "config-backup":
return self._action_config_backup()
elif action == "reboot":
return self._action_reboot()
elif action == "save_notifications":
return self._action_save_notifications()
elif action == "test_notifications":
......@@ -654,3 +648,29 @@ def init_app():
callback=config_page_get)
bottle.SimpleTemplate.defaults['config_pages'] = config_page_map
return app
def login_redirect():
next_url = bottle.request.GET.get("next")
if next_url and is_safe_redirect(next_url, bottle.request.get_header('host')):
bottle.redirect(next_url)
bottle.redirect(reverse("config_index"))
@bottle.view("index")
def top_index():
session = bottle.request.environ['foris.session']
allowed_step_max, wizard_finished = get_wizard_progress(session)
if allowed_step_max == 1:
if session.is_anonymous:
session.recreate()
session["user_authenticated"] = True
session.save()
if session.get("user_authenticated"):
login_redirect()
return dict(
luci_path="//%(host)s/%(path)s"
% {'host': bottle.request.get_header('host'), 'path': 'cgi-bin/luci'})
......@@ -16,7 +16,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# builtins
import argparse
import logging
import os
......@@ -25,32 +24,25 @@ import bottle
from bottle_i18n import I18NMiddleware, I18NPlugin, i18n_defaults
# local
from foris.config import init_app as init_app_config
from foris.wizard import init_app as init_app_wizard
from foris.config import init_app as init_app_config, top_index
from foris.common import (
index, login, foris_403_handler, render_js_md5, render_js, logout, change_lang, static
)
from foris.common import render_js_md5, init_common_app, init_default_app
from foris.middleware.sessions import SessionMiddleware
from foris.middleware.reporting import ReportingMiddleware
from foris.nuci import client
from foris.nuci.helpers import contract_valid, read_uci_lang
from foris.langs import DEFAULT_LANGUAGE
from foris.plugins import ForisPluginLoader
from foris.middleware.bottle_csrf import CSRFPlugin
from foris.utils import messages
from foris.utils.translators import translations
from foris.utils.bottle_stuff import (
prepare_template_defaults,
clickjacking_protection,
clear_lazy_cache,
disable_caching,
route_list_cmdline,
route_list_debug,
)
logger = logging.getLogger("foris")
logger = logging.getLogger("foris.config")
BASE_DIR = os.path.dirname(__file__)
......@@ -59,71 +51,7 @@ bottle.SimpleTemplate.defaults['contract_valid'] = contract_valid
bottle.SimpleTemplate.defaults['js_md5'] = lambda filename: render_js_md5(filename)
def init_foris_app(app, prefix):
"""
Initializes Foris application - use this method to apply properties etc.
that should be set to main app and all the mounted apps (i.e. to the
Bottle() instances).
:param app: instance of bottle application to mount
:param prefix: prefix which has been used to mount the application
"""
app.catchall = False # caught by ReportingMiddleware
app.error_handler[403] = foris_403_handler
app.add_hook('after_request', clickjacking_protection)
app.add_hook('after_request', disable_caching)
app.add_hook('after_request', clear_lazy_cache)
app.config['prefix'] = prefix
def get_arg_parser():
"""
Create ArgumentParser instance with Foris arguments.
:return: instance of ArgumentParser
"""
parser = argparse.ArgumentParser()
group = parser.add_argument_group("run server")
group.add_argument("-H", "--host", default="0.0.0.0")
group.add_argument("-p", "--port", type=int, default=8080)
group.add_argument("--session-timeout", type=int, default=900,
help="session timeout (in seconds)")
group.add_argument("-s", "--server", choices=["wsgiref", "flup", "cgi"], default="wsgiref")
group.add_argument("-d", "--debug", action="store_true")
group.add_argument("--noauth", action="store_true",
help="disable authentication (available only in debug mode)")
group.add_argument("--nucipath", help="path to Nuci binary")
parser.add_argument("-R", "--routes", action="store_true", help="print routes and exit")
group.add_argument(
"-S", "--static", action="store_true",
help="serve static files directly through foris app (should be used for debug only)"
)
return parser
def init_default_app(include_static=False):
"""
Initialize top-level Foris app - register all routes etc.
:param include_static: include route to static files
:type include_static: bool
:return: instance of Foris Bottle application
"""
app = bottle.app()
app.install(CSRFPlugin())
app.route("/", name="index", callback=index)
app.route("/lang/<lang:re:\w{2}>", name="change_lang", callback=change_lang)
app.route("/", method="POST", name="login", callback=login)
app.route("/logout", name="logout", callback=logout)
if include_static:
app.route('/static/<filename:re:.*>', name="static", callback=static)
app.route("/js/<filename:re:.*>", name="render_js", callback=render_js)
return app
def prepare_main_app(args):
def prepare_config_app(args):
"""
Prepare Foris main application - i.e. apply CLI arguments, mount applications,
install hooks and middleware etc...
......@@ -141,7 +69,7 @@ def prepare_main_app(args):
# init messaging template
messages.set_template_defaults()
app = init_default_app(args.static)
app = init_default_app(top_index, args.static)
# basic and bottle settings
template_dir = os.path.join(BASE_DIR, "templates")
......@@ -150,7 +78,6 @@ def prepare_main_app(args):
# mount apps
app.mount("/config", init_app_config())
app.mount("/wizard", init_app_wizard())
if args.debug:
if args.noauth:
......@@ -158,12 +85,12 @@ def prepare_main_app(args):
app.config["no_auth"] = True
# set custom app attributes for main app and all mounted apps
init_foris_app(app, None)
init_common_app(app, None)
for route in app.routes:
if route.config.get("mountpoint"):
mounted = route.config['mountpoint.target']
prefix = route.config['mountpoint.prefix']
init_foris_app(mounted, prefix)
init_common_app(mounted, prefix)
if args.nucipath:
client.StaticNetconfConnection.set_bin_path(args.nucipath)
......
......@@ -20,7 +20,7 @@
{{! notification.escaped_body[lang()] }}
%if notification.requires_restart:
<div class="buttons">
<a href="{{ url("config_action", page_name="maintenance", action="reboot") }}" class="button reboot">{{ trans("Reboot now") }}</a>
<a href="{{ url("reboot") }}" class="button reboot">{{ trans("Reboot now") }}</a>
</div>
%elif notification.id:
<a href="#" class="dismiss" title="{{ trans("Dismiss") }}" data-id="{{ notification.id }}">&times;</a>
......
......@@ -57,7 +57,7 @@
<h2>{{ trans("Device reboot") }}</h2>
<p>{{ trans("If you need to reboot the device, click on the following button. The reboot process takes approximately 30 seconds, you will be required to log in again after the reboot.") }}</p>
<div>
<a href="{{ url("config_action", page_name="maintenance", action="reboot") }}" class="button">{{ trans("Reboot") }}</a>
<a href="{{ url("reboot") }}" class="button">{{ trans("Reboot") }}</a>
</div>
<script>
......
......@@ -18,7 +18,7 @@
<h1>{{ trans("Installation finished") }}</h1>
<p>{{! trans("You can change any of the previously configured settings in the <a href=\"%(url)s\">standard configuration interface</a>. In case you are interested in more advanced options, you can use the LuCI interface which is available from the <a href=\"%(url2)s\">Advanced administration tab</a>.") % {'url': url("config_index"), 'url2': url("config_page", page_name="system-password")} }}</p>
<p>{{! trans("You can change any of the previously configured settings in the <a href=\"%(url)s\">standard configuration interface</a>. In case you are interested in more advanced options, you can use the LuCI interface which is available from the <a href=\"%(url2)s\">Advanced administration tab</a>.") % {'url': "TODO", 'url2': "TODO"} }}</p>
<h2>{{ trans("What next?") }}</h2>
......@@ -28,10 +28,10 @@
{{ trans("Without the Updater, installed software will not be kept up to date and you will also not be able to install Updater's package lists.") }}
{{! trans('By enabling of the Updater, you can also join our research project: <a href="https://www.turris.cz/">Project Turris</a>.') }}
</p>
<p>{{! trans('You can enable the Updater any time on the <a href=\"%(url)s\">Updater</a> configuration page.') % dict(url=url("config_page", page_name="updater")) }}</p>
<p>{{! trans('You can enable the Updater any time on the <a href=\"%(url)s\">Updater</a> configuration page.') % dict(url="TODO") }}</p>
%else:
<p>{{! trans('With Turris Omnia you can join the research project called <a href="https://www.turris.cz/">Project Turris</a>. Thanks to this project, your router can become a probe that analyzes the traffic between internet and the home network and identifies suspicious data flows.') }}</p>
<p>{{! trans('You can enable these additional features by following the instructions on the <a href="%(url)s">Data collection</a> page.') % dict(url=url("config_page", page_name="data-collection")) }}</p>
<p>{{! trans('You can enable these additional features by following the instructions on the <a href="%(url)s">Data collection</a> page.') % dict(url="TODO") }}</p>
%end
%if len(notifications):
......@@ -40,4 +40,4 @@
%include("_notifications.tpl", notifications=notifications)
%end
<a class="button-next" href="{{ url("config_index") }}">{{ trans("Continue to the configuration interface") }}</a>
<a class="button-next" href="{{ "TODO" }}">{{ trans("Continue to the configuration interface") }}</a>
......@@ -14,7 +14,8 @@ from nose.tools import (assert_equal, assert_not_equal, assert_in,
assert_regexp_matches)
from webtest import TestApp as WebApp, Upload, Text
import foris.core
from foris import config_app
from foris.__main__ import get_arg_parser
from foris.nuci.client import StaticNetconfConnection
from . import test_data
......@@ -58,7 +59,7 @@ class ForisTest(TestCase):
StaticNetconfConnection.enable_test_environment(cls.config_directory)
# initialize Foris WSGI app
args = cls.make_args()
cls.app = WebApp(foris.core.prepare_main_app(args))
cls.app = WebApp(config_app.prepare_main_app(args))
@classmethod
def tearDownClass(cls):
......@@ -91,7 +92,7 @@ class ForisTest(TestCase):
@staticmethod
def make_args():
parser = foris.core.get_arg_parser()
parser = get_arg_parser()
args = parser.parse_args([])
return args
......
......@@ -29,7 +29,9 @@ from foris.nuci.helpers import (
from foris.nuci.modules.uci_raw import build_option_uci_tree
from foris.nuci.notifications import make_notification_title
from foris.nuci.preprocessors import preproc_disabled_to_agreed
from foris.utils import login_required, messages, WIZARD_NEXT_STEP_ALLOWED_KEY, NUM_WIZARD_STEPS
from foris.utils import (
login_required, messages, WIZARD_NEXT_STEP_ALLOWED_KEY, NUM_WIZARD_STEPS, is_safe_redirect
)
from foris.middleware.bottle_csrf import CSRFPlugin
from foris.utils.routing import reverse
from foris.utils.translators import gettext_dummy as gettext, _
......@@ -517,3 +519,38 @@ def init_app():
app.route("/step/<number:re:\d+>", method="POST", callback=step_post)
app.route("/skip", name="wizard_skip", callback=skip)
return app
def login_redirect(step_num, wizard_finished=False):
if step_num >= NUM_WIZARD_STEPS or wizard_finished:
next = bottle.request.GET.get("next")
if next and is_safe_redirect(next, bottle.request.get_header('host')):
bottle.redirect(next)
bottle.redirect(reverse("wizard_step", number=NUM_WIZARD_STEPS))
elif step_num == 1:
bottle.redirect(reverse("wizard_index"))
else:
bottle.redirect(reverse("wizard_step", number=step_num))
@bottle.view("index")
def top_index():
session = bottle.request.environ['foris.session']
allowed_step_max, wizard_finished = get_wizard_progress(session)
if allowed_step_max == 1:
if session.is_anonymous:
session.recreate()
session["user_authenticated"] = True
else:
session[WIZARD_NEXT_STEP_ALLOWED_KEY] = str(allowed_step_max)
session["wizard_finished"] = wizard_finished
allowed_step_max = int(allowed_step_max)
session.save()
if session.get("user_authenticated"):
login_redirect(allowed_step_max, wizard_finished)
return dict(
luci_path="//%(host)s/%(path)s"
% {'host': bottle.request.get_header('host'), 'path': 'cgi-bin/luci'})
# coding=utf-8
# Foris - web administration interface for OpenWrt based on NETCONF
# Copyright (C) 2017 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# 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.
#
# 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/>.
# builtins
import logging
import os
# 3rd party
import bottle
from bottle_i18n import I18NMiddleware, I18NPlugin, i18n_defaults
# local
from foris.wizard import init_app as init_app_wizard, top_index
from foris.common import render_js_md5, init_common_app, init_default_app
from foris.middleware.sessions import SessionMiddleware
from foris.middleware.reporting import ReportingMiddleware
from foris.nuci import client
from foris.nuci.helpers import contract_valid, read_uci_lang
from foris.langs import DEFAULT_LANGUAGE
from foris.plugins import ForisPluginLoader
from foris.utils import messages
from foris.utils.translators import translations
from foris.utils.bottle_stuff import (
prepare_template_defaults,
route_list_cmdline,
route_list_debug,
)
logger = logging.getLogger("foris.wizard")
BASE_DIR = os.path.dirname(__file__)
bottle.SimpleTemplate.defaults['contract_valid'] = contract_valid
bottle.SimpleTemplate.defaults['js_md5'] = lambda filename: render_js_md5(filename)
def prepare_wizard_app(args):
"""
Prepare Foris wizard application - i.e. apply CLI arguments, mount applications,
install hooks and middleware etc...
:param args: arguments received from ArgumentParser.parse_args().
:return: bottle.app() for Foris
"""
# internationalization
i18n_defaults(bottle.SimpleTemplate, bottle.request)
# setup default template defaults
prepare_template_defaults()
# init messaging template
messages.set_template_defaults()
app = init_default_app(top_index, args.static)
# basic and bottle settings
template_dir = os.path.join(BASE_DIR, "templates")
bottle.TEMPLATE_PATH.append(template_dir)
logging.basicConfig(level=logging.DEBUG if args.debug else logging.WARNING)
# mount apps
app.mount("/wizard", init_app_wizard())
if args.debug:
if args.noauth:
logger.warning("authentication disabled")
app.config["no_auth"] = True
# set custom app attributes for main app and all mounted apps
init_common_app(app, None)
for route in app.routes:
if route.config.get("mountpoint"):
mounted = route.config['mountpoint.target']
prefix = route.config['mountpoint.prefix']
init_common_app(mounted, prefix)
if args.nucipath:
client.StaticNetconfConnection.set_bin_path(args.nucipath)
# load Foris plugins before applying Bottle plugins to app
loader = ForisPluginLoader(app)
loader.autoload_plugins()
# print routes to console and exit
if args.routes:
routes = route_list_cmdline(app)
print("\n".join(sorted(set(routes))))
return app
# print routes in debug mode
if args.debug:
routes = route_list_debug(app)
logger.debug("Routes:\n%s", "\n".join(routes))
# read language saved in Uci
lang = read_uci_lang(DEFAULT_LANGUAGE)
# i18n middleware
if lang not in translations:
lang = DEFAULT_LANGUAGE
app = I18NMiddleware(app, I18NPlugin(
domain="messages", lang_code=lang, default=DEFAULT_LANGUAGE,
locale_dir=os.path.join(BASE_DIR, "locale")
))
# reporting middleware for all mounted apps
app = ReportingMiddleware(app, sensitive_params=("key", "pass", "*password*"))
app.install_dump_route(bottle.app())
app = SessionMiddleware(app, args.session_timeout)
return app
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment