mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-07-29 12:09:37 +00:00
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 <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
372ecdcda9
commit
c163601b6c
@ -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():
|
||||
|
||||
@ -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 <a href="{users_url}">any user</a> 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)
|
||||
|
||||
@ -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
|
||||
</Location>
|
||||
|
||||
<Location /_cockpit/cockpit/socket>
|
||||
ProxyPass ws://localhost:9090/_cockpit/socket
|
||||
ProxyPass wss://localhost:9090/_cockpit/socket
|
||||
ProxyPreserveHost On
|
||||
</Location>
|
||||
|
||||
<ProxyMatch "^(https|wss)://localhost:9090/_cockpit/.*">
|
||||
SSLProxyEngine on
|
||||
SSLProxyVerify none
|
||||
SSLProxyCheckPeerName off
|
||||
</ProxyMatch>
|
||||
|
||||
@ -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 }}
|
||||
|
||||
<h3>{% trans "Access" %}</h3>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Cockpit will only work when accessed using the following URLs.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<ol>
|
||||
{% for url_ in urls %}
|
||||
<li>{{ url_ }}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endblock %}
|
||||
@ -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'),
|
||||
]
|
||||
|
||||
@ -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]
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user