diff --git a/actions/quassel b/actions/quassel
new file mode 100755
index 000000000..5beacb7d2
--- /dev/null
+++ b/actions/quassel
@@ -0,0 +1,55 @@
+#!/usr/bin/python3
+#
+# This file is part of FreedomBox.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+"""
+Configuration helper for Quassel.
+"""
+
+import argparse
+import pathlib
+
+
+def parse_arguments():
+ """Return parsed command line arguments as dictionary."""
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
+
+ subparser = subparsers.add_parser('set-domain',
+ help='Setup Cockpit configuration')
+ subparser.add_argument('domain_name', help='Domain name to be allowed')
+
+ subparsers.required = True
+ return parser.parse_args()
+
+
+def subcommand_set_domain(arguments):
+ """Write a file containing domain name."""
+ domain_file = pathlib.Path('/var/lib/quassel/domain-freedombox')
+ domain_file.write_text(arguments.domain_name)
+
+
+def main():
+ """Parse arguments and perform all duties."""
+ arguments = parse_arguments()
+
+ subcommand = arguments.subcommand.replace('-', '_')
+ subcommand_method = globals()['subcommand_' + subcommand]
+ subcommand_method(arguments)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/plinth/modules/quassel/__init__.py b/plinth/modules/quassel/__init__.py
index dadde0996..dcdbdcf4a 100644
--- a/plinth/modules/quassel/__init__.py
+++ b/plinth/modules/quassel/__init__.py
@@ -18,16 +18,19 @@
FreedomBox app for Quassel.
"""
+import pathlib
+
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
-from plinth import action_utils
+from plinth import action_utils, 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.firewall.components import Firewall
+from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.utils import format_lazy
-from plinth.views import AppView
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -37,6 +40,8 @@ managed_services = ['quasselcore']
managed_packages = ['quassel-core']
+managed_paths = [pathlib.Path('/var/lib/quassel/')]
+
name = _('Quassel')
short_description = _('IRC Client')
@@ -92,6 +97,15 @@ class QuasselApp(app_module.App):
is_external=True)
self.add(firewall)
+ letsencrypt = LetsEncrypt(
+ 'letsencrypt-quassel', domains=get_domains,
+ daemons=managed_services, should_copy_certificates=True,
+ private_key_path='/var/lib/quassel/quasselCert.pem',
+ certificate_path='/var/lib/quassel/quasselCert.pem',
+ user_owner='quasselcore', group_owner='quassel',
+ managing_app='quassel')
+ self.add(letsencrypt)
+
daemon = Daemon('daemon-quassel', managed_services[0])
self.add(daemon)
@@ -106,20 +120,11 @@ def init():
app.set_enabled(True)
-class QuasselAppView(AppView):
- app_id = 'quassel'
- diagnostics_module_name = 'quassel'
- name = name
- description = description
- clients = clients
- manual_page = manual_page
- port_forwarding_info = port_forwarding_info
-
-
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
helper.call('post', app.enable)
+ app.get_component('letsencrypt-quassel').setup_certificates()
def diagnose():
@@ -130,3 +135,40 @@ def diagnose():
results.append(action_utils.diagnose_port_listening(4242, 'tcp6'))
return results
+
+
+def get_available_domains():
+ """Return an iterator with all domains able to have a certificate."""
+ return (domain.name for domain in names.components.DomainName.list()
+ if domain.domain_type.can_have_certificate)
+
+
+def set_domain(domain):
+ """Set the TLS domain by writing a file to data directory."""
+ if domain:
+ actions.superuser_run('quassel', ['set-domain', domain])
+
+
+def get_domain():
+ """Read TLS domain from config file select first available if none."""
+ domain = None
+ try:
+ with open('/var/lib/quassel/domain-freedombox') as file_handle:
+ domain = file_handle.read().strip()
+ except FileNotFoundError:
+ pass
+
+ if not domain:
+ domain = next(get_available_domains(), None)
+ set_domain(domain)
+
+ return domain
+
+
+def get_domains():
+ """Return a list with the configured domain for quassel."""
+ domain = get_domain()
+ if domain:
+ return [domain]
+
+ return []
diff --git a/plinth/modules/quassel/forms.py b/plinth/modules/quassel/forms.py
new file mode 100644
index 000000000..5455b7fc1
--- /dev/null
+++ b/plinth/modules/quassel/forms.py
@@ -0,0 +1,42 @@
+#
+# This file is part of FreedomBox.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+"""
+Forms for Quassel app.
+"""
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from plinth.forms import AppForm
+from plinth.modules import quassel
+
+
+def get_domain_choices():
+ """Double domain entries for inclusion in the choice field."""
+ return ((domain, domain) for domain in quassel.get_available_domains())
+
+
+class QuasselForm(AppForm):
+ """Form to select a TLS domain for Quassel."""
+
+ domain = forms.ChoiceField(
+ choices=get_domain_choices,
+ label=_('TLS domain'),
+ help_text=_(
+ 'Select a domain to use TLS with. If the list is empty, please '
+ 'configure at least one domain with certificates.'),
+ )
diff --git a/plinth/modules/quassel/urls.py b/plinth/modules/quassel/urls.py
index a2dc6f9c0..fb64091d5 100644
--- a/plinth/modules/quassel/urls.py
+++ b/plinth/modules/quassel/urls.py
@@ -20,7 +20,7 @@ URLs for the quassel module.
from django.conf.urls import url
-from plinth.modules.quassel import QuasselAppView
+from .views import QuasselAppView
urlpatterns = [
url(r'^apps/quassel/$', QuasselAppView.as_view(), name='index'),
diff --git a/plinth/modules/quassel/views.py b/plinth/modules/quassel/views.py
new file mode 100644
index 000000000..62e018079
--- /dev/null
+++ b/plinth/modules/quassel/views.py
@@ -0,0 +1,48 @@
+#
+# This file is part of FreedomBox.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+from plinth.modules import quassel
+from plinth.views import AppView
+
+from .forms import QuasselForm
+
+
+class QuasselAppView(AppView):
+ app_id = 'quassel'
+ diagnostics_module_name = 'quassel'
+ name = quassel.name
+ description = quassel.description
+ clients = quassel.clients
+ manual_page = quassel.manual_page
+ port_forwarding_info = quassel.port_forwarding_info
+ form_class = QuasselForm
+
+ def get_initial(self):
+ """Return the values to fill in the form."""
+ initial = super().get_initial()
+ initial['domain'] = quassel.get_domain()
+ return initial
+
+ def form_valid(self, form):
+ """Change the access control of Radicale service."""
+ data = form.cleaned_data
+ if quassel.get_domain() != data['domain']:
+ quassel.set_domain(data['domain'])
+ quassel.app.get_component(
+ 'letsencrypt-quassel').setup_certificates()
+
+ return super().form_valid(form)