From c163601b6ca14e62df0d34a9e6364745e6395825 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 6 Jul 2022 14:51:49 -0700 Subject: [PATCH] cockpit: Reconfigure to allow any origin When Cockpit is not configured any origins, it uses the host and protocol of the incoming request to set the allowed origin for WebSocket connections. By ensuring that the original host/protocol is passed on to Cockpit from the browser, we can eliminate the need for configuring a pre-determined list of origins. Passing the host and protocol from the browser is done by setting ProxyPreserveHost and using https:// for proxying. For a cross-site request, Origin: and Host: entries won't match and '403 Forbidden' is thrown. So, this approach is still safe. Tests: - Without the patch, access Cockpit using IP address and it fails. Apply the patch. Cockpit setup should run. Origins= directive in the configuration file /etc/cockpit/cockpit.conf should get removed. Accessing with IP address and logging in succeeds. - Freshly setup a container with the patch and access Cockpit using IP address. This works and login succeeds. - Test on stable and testing containers. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- actions/cockpit | 71 ++++++------------- plinth/modules/cockpit/__init__.py | 46 ++---------- .../conf-available/cockpit-freedombox.conf | 12 +++- plinth/modules/cockpit/templates/cockpit.html | 24 ------- plinth/modules/cockpit/urls.py | 5 +- plinth/modules/cockpit/utils.py | 43 ----------- plinth/modules/cockpit/views.py | 18 ----- 7 files changed, 39 insertions(+), 180 deletions(-) delete mode 100644 plinth/modules/cockpit/templates/cockpit.html delete mode 100644 plinth/modules/cockpit/utils.py delete mode 100644 plinth/modules/cockpit/views.py diff --git a/actions/cockpit b/actions/cockpit index 3abc8770f..e3ff78d41 100755 --- a/actions/cockpit +++ b/actions/cockpit @@ -6,8 +6,11 @@ Configuration helper for Cockpit. import argparse +import augeas + from plinth import action_utils -from plinth.modules.cockpit import utils + +CONFIG_FILE = '/etc/cockpit/cockpit.conf' def parse_arguments(): @@ -15,65 +18,31 @@ def parse_arguments(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') - subparser = subparsers.add_parser('setup', - help='Setup Cockpit configuration') - subparser.add_argument('domain_names', nargs='*', - help='Domain names to be allowed') - subparser = subparsers.add_parser( - 'add-domain', - help='Allow a new domain to be origin for Cockpit\'s WebSocket') - subparser.add_argument('domain_name', help='Domain name to be allowed') - subparser = subparsers.add_parser( - 'remove-domain', - help='Disallow a new domain from being origin for Cockpit\'s ' - 'WebSocket') - subparser.add_argument('domain_name', help='Domain name to be removed') + subparsers.add_parser('setup', help='Setup Cockpit configuration') subparsers.required = True return parser.parse_args() +def _load_augeas(): + """Initialize Augeas.""" + aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + + augeas.Augeas.NO_MODL_AUTOLOAD) + aug.set('/augeas/load/inifile/lens', 'Puppet.lns') + aug.set('/augeas/load/inifile/incl[last() + 1]', CONFIG_FILE) + aug.load() + return aug + + def subcommand_setup(arguments): """Setup Cockpit configuration.""" - aug = utils.load_augeas() - origins = [ - utils.get_origin_from_domain(domain) - for domain in arguments.domain_names - ] - origins += ['https://localhost', 'https://localhost:4430'] - _set_origin_domains(aug, origins) - aug.set('/files' + utils.CONFIG_FILE + '/WebService/UrlRoot', '/_cockpit/') + aug = _load_augeas() + aug.set('/files' + CONFIG_FILE + '/WebService/UrlRoot', '/_cockpit/') + aug.remove('/files' + CONFIG_FILE + '/WebService/Origins') aug.save() - action_utils.service_restart('cockpit.socket') - - -def _set_origin_domains(aug, origins): - """Set the list of allowed origin domains.""" - aug.set('/files' + utils.CONFIG_FILE + '/WebService/Origins', - ' '.join(origins)) - - -def subcommand_add_domain(arguments): - """Allow a new domain to be origin for Cockpit's WebSocket.""" - aug = utils.load_augeas() - origins = utils.get_origin_domains(aug) - origins.add(utils.get_origin_from_domain(arguments.domain_name)) - _set_origin_domains(aug, origins) - aug.save() - - -def subcommand_remove_domain(arguments): - """Disallow a domain from being origin for Cockpit's WebSocket.""" - aug = utils.load_augeas() - origins = utils.get_origin_domains(aug) - try: - origins.remove(utils.get_origin_from_domain(arguments.domain_name)) - except KeyError: - pass - else: - _set_origin_domains(aug, origins) - aug.save() + # Accommodate changes in Apache configuration file from v1 to v2. + action_utils.service_reload('apache2') def main(): diff --git a/plinth/modules/cockpit/__init__.py b/plinth/modules/cockpit/__init__.py index 5e7abb6b8..f565a8a8c 100644 --- a/plinth/modules/cockpit/__init__.py +++ b/plinth/modules/cockpit/__init__.py @@ -10,15 +10,13 @@ from plinth import actions from plinth import app as app_module from plinth import cfg, frontpage, menu from plinth.daemon import Daemon -from plinth.modules import names from plinth.modules.apache.components import Webserver from plinth.modules.backups.components import BackupRestore from plinth.modules.firewall.components import Firewall from plinth.package import Packages -from plinth.signals import domain_added, domain_removed from plinth.utils import format_lazy -from . import manifest, utils +from . import manifest _description = [ format_lazy( @@ -36,10 +34,6 @@ _description = [ _('It can be accessed by any user on ' '{box_name} belonging to the admin group.'), box_name=_(cfg.box_name), users_url=reverse_lazy('users:index')), - format_lazy( - _('Cockpit requires that you access it through a domain name. ' - 'It will not work when accessed using an IP address as part' - ' of the URL.')), ] app = None @@ -50,9 +44,7 @@ class CockpitApp(app_module.App): app_id = 'cockpit' - _version = 1 - - DAEMON = 'cockpit.socket' + _version = 2 def __init__(self): """Create components for the app.""" @@ -91,43 +83,17 @@ class CockpitApp(app_module.App): urls=['https://{host}/_cockpit/']) self.add(webserver) - daemon = Daemon('daemon-cockpit', self.DAEMON) + daemon = Daemon('daemon-cockpit', 'cockpit.socket') self.add(daemon) backup_restore = BackupRestore('backup-restore-cockpit', **manifest.backup) self.add(backup_restore) - @staticmethod - def post_init(): - """Perform post initialization operations.""" - domain_added.connect(on_domain_added) - domain_removed.connect(on_domain_removed) - def setup(helper, old_version=None): """Install and configure the module.""" app.setup(old_version) - domains = names.components.DomainName.list_names('https') - helper.call('post', actions.superuser_run, 'cockpit', - ['setup'] + list(domains)) - helper.call('post', app.enable) - - -def on_domain_added(sender, domain_type, name='', description='', - services=None, **kwargs): - """Handle addition of a new domain.""" - if name and not app.needs_setup(): - if name not in utils.get_domains(): - actions.superuser_run('cockpit', ['add-domain', name]) - actions.superuser_run('service', - ['try-restart', CockpitApp.DAEMON]) - - -def on_domain_removed(sender, domain_type, name='', **kwargs): - """Handle removal of a domain.""" - if name and not app.needs_setup(): - if name in utils.get_domains(): - actions.superuser_run('cockpit', ['remove-domain', name]) - actions.superuser_run('service', - ['try-restart', CockpitApp.DAEMON]) + helper.call('post', actions.superuser_run, 'cockpit', ['setup']) + if not old_version: + helper.call('post', app.enable) diff --git a/plinth/modules/cockpit/data/etc/apache2/conf-available/cockpit-freedombox.conf b/plinth/modules/cockpit/data/etc/apache2/conf-available/cockpit-freedombox.conf index 495037bbe..db203d231 100644 --- a/plinth/modules/cockpit/data/etc/apache2/conf-available/cockpit-freedombox.conf +++ b/plinth/modules/cockpit/data/etc/apache2/conf-available/cockpit-freedombox.conf @@ -14,9 +14,17 @@ ReWriteCond %{HTTPS} !=on RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=302,L] - ProxyPass http://localhost:9090/_cockpit/ + ProxyPass https://localhost:9090/_cockpit/ + ProxyPreserveHost On - ProxyPass ws://localhost:9090/_cockpit/socket + ProxyPass wss://localhost:9090/_cockpit/socket + ProxyPreserveHost On + + + SSLProxyEngine on + SSLProxyVerify none + SSLProxyCheckPeerName off + diff --git a/plinth/modules/cockpit/templates/cockpit.html b/plinth/modules/cockpit/templates/cockpit.html deleted file mode 100644 index f962db2e9..000000000 --- a/plinth/modules/cockpit/templates/cockpit.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "app.html" %} -{% comment %} -# SPDX-License-Identifier: AGPL-3.0-or-later -{% endcomment %} -{% load bootstrap %} -{% load i18n %} - -{% block status %} - {{ block.super }} - -

{% trans "Access" %}

- -

- {% blocktrans trimmed %} - Cockpit will only work when accessed using the following URLs. - {% endblocktrans %} -

- -
    - {% for url_ in urls %} -
  1. {{ url_ }}
  2. - {% endfor %} -
-{% endblock %} diff --git a/plinth/modules/cockpit/urls.py b/plinth/modules/cockpit/urls.py index 041de1146..0b888270a 100644 --- a/plinth/modules/cockpit/urls.py +++ b/plinth/modules/cockpit/urls.py @@ -5,8 +5,9 @@ URLs for Cockpit module. from django.urls import re_path -from plinth.modules.cockpit.views import CockpitAppView +from plinth.views import AppView urlpatterns = [ - re_path(r'^sys/cockpit/$', CockpitAppView.as_view(), name='index'), + re_path(r'^sys/cockpit/$', AppView.as_view(app_id='cockpit'), + name='index'), ] diff --git a/plinth/modules/cockpit/utils.py b/plinth/modules/cockpit/utils.py deleted file mode 100644 index 385642174..000000000 --- a/plinth/modules/cockpit/utils.py +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -""" -Minor utility methods for Cockpit. -""" - -import urllib.parse - -import augeas - -CONFIG_FILE = '/etc/cockpit/cockpit.conf' - - -def load_augeas(): - """Initialize Augeas.""" - aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + - augeas.Augeas.NO_MODL_AUTOLOAD) - aug.set('/augeas/load/inifile/lens', 'Puppet.lns') - aug.set('/augeas/load/inifile/incl[last() + 1]', CONFIG_FILE) - aug.load() - return aug - - -def get_origin_domains(aug): - """Return the list of allowed origin domains.""" - origins = aug.get('/files' + CONFIG_FILE + '/WebService/Origins') - return set(origins.split()) if origins else set() - - -def get_origin_from_domain(domain): - """Return the origin that should be allowed for a domain.""" - return 'https://{domain}'.format(domain=domain) - - -def _get_domain_from_origin(origin): - """Return the domain from an origin URL.""" - return urllib.parse.urlparse(origin).netloc - - -def get_domains(): - """Return the domain name in origin URL.""" - aug = load_augeas() - origins = get_origin_domains(aug) - return [_get_domain_from_origin(origin) for origin in origins] diff --git a/plinth/modules/cockpit/views.py b/plinth/modules/cockpit/views.py deleted file mode 100644 index 231e6c67d..000000000 --- a/plinth/modules/cockpit/views.py +++ /dev/null @@ -1,18 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -""" -Views for the Cockpit module -""" -from plinth.modules.cockpit.utils import get_origin_domains, load_augeas -from plinth.views import AppView - - -class CockpitAppView(AppView): - app_id = 'cockpit' - template_name = 'cockpit.html' - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(**kwargs) - urls = get_origin_domains(load_augeas()) - context['urls'] = [url for url in urls if 'localhost' not in url] - - return context