tor: Use AppView and Operation for app page

- Use AppView for app page.

- Handle post enable/disable activities within the App class.

- Use Operation class to perform configuration instead of custom mechanism. Drop
all the older code for it.

Tests:

- DONE: Run functional tests
- DONE: Enabling Tor
  - DONE: Enables the service
  - DONE: Updates the firewall ports
  - DONE: Adds hidden service domain to names app
  - DONE: Shows app enabled
  - DONE: Firewall ports are opened
- DONE: Disabling Tor
  - DONE: Disables apt transport over Tor
  - DONE: Firewall ports are closed
  - DONE: Shows app disabled
  - DONE: Onion domain is removed from names app
- DONE: App page
  - DONE: Running/not-running status is shown properly based on whether tor
    daemon is running.
  - DONE: Port forwarding information is shown properly.
  - DONE: When hidden service is enabled, status of hidden services is shown
- DONE: Configuration update
  - DONE: Form shown correct status of the option
  - DONE: When configuration is being updated, operation progress is shown
  - DONE: Page refreshes once in 3 seconds during operation. Refresh stops after
    operation.
  - Once the operation is complete, success or error message is shown
  - DONE: Javascript to show/hide upstream bridges text box works
  - DONE: Javascript to enable/disable relay checkboxes works
  - DONE: Operation does not show notification.
  - DONE: Enabling apt over Tor does not work when app is disabled
  - DONE: When configuration is changed, the message 'Settings unchanged' is not
    shown.
  - DONE: If an error is thrown during configuration, an error message is shown
    properly.
  - DONE: Tor is restarted after configuration update and hidden service domains
    is updated.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2022-08-22 18:03:56 -07:00 committed by James Valleroy
parent 493071bfe0
commit 67860385d0
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
7 changed files with 122 additions and 169 deletions

View File

@ -40,8 +40,6 @@ def parse_arguments():
subparsers.add_parser('get-status', help='Get Tor status in JSON format')
configure = subparsers.add_parser('configure', help='Configure Tor')
configure.add_argument('--service', choices=['enable', 'disable'],
help='Configure Tor service')
configure.add_argument('--relay', choices=['enable', 'disable'],
help='Configure relay')
configure.add_argument('--bridge-relay', choices=['enable', 'disable'],
@ -57,6 +55,9 @@ def parse_arguments():
configure.add_argument('--upstream-bridges',
help='Set list of upstream bridges to use')
subparsers.add_parser('update-ports',
help='Update firewall ports based on what Tor uses')
subparsers.add_parser('restart', help='Restart Tor')
subparsers.required = True
@ -171,9 +172,6 @@ def subcommand_configure(arguments):
"""Configure Tor."""
aug = augeas_load()
if arguments.service == 'disable':
_disable()
_use_upstream_bridges(arguments.use_upstream_bridges, aug=aug)
if arguments.use_upstream_bridges == 'enable':
@ -190,15 +188,17 @@ def subcommand_configure(arguments):
elif arguments.hidden_service == 'disable':
_disable_hs(aug=aug)
if arguments.service == 'enable':
_enable()
if arguments.apt_transport_tor == 'enable':
_enable_apt_transport_tor()
elif arguments.apt_transport_tor == 'disable':
_disable_apt_transport_tor()
def subcommand_update_ports(_):
"""Update firewall ports based on what Tor uses."""
_update_ports()
def subcommand_restart(_):
"""Restart Tor."""
if (action_utils.service_is_enabled('tor@plinth', strict_check=True)

View File

@ -106,6 +106,19 @@ class TorApp(app_module.App):
domain_type='domain-type-tor',
name=hostname, services=services)
def enable(self):
"""Enable the app and update firewall ports."""
super().enable()
actions.superuser_run('tor', ['update-ports'])
update_hidden_service_domain()
def disable(self):
"""Disable APT use of Tor before disabling."""
actions.superuser_run('tor',
['configure', '--apt-transport-tor', 'disable'])
super().disable()
update_hidden_service_domain()
def diagnose(self):
"""Run diagnostics and return the results."""
results = super().diagnose()

View File

@ -72,7 +72,6 @@ def bridges_validator(bridges):
class TorForm(forms.Form): # pylint: disable=W0232
"""Tor configuration form."""
enabled = forms.BooleanField(label=_('Enable Tor'), required=False)
use_upstream_bridges = forms.BooleanField(
label=_('Use upstream bridges to connect to Tor network'),
required=False, help_text=_(

View File

@ -8,49 +8,36 @@
{% load static %}
{% block status %}
{% if config_running %}
<h3>{% trans "Status" %}</h3>
{{ block.super }}
<p class="running-status-parent">
<span class="running-status loading"></span>
{% trans "Tor configuration is being updated" %}
</p>
{% else %}
{{ block.super }}
{% if status.hs_enabled %}
<div class="table-responsive">
<table class="table tor-hs">
<thead>
<tr>
<th>{% trans "Onion Service" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Ports" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tor-hs-hostname">{{ status.hs_hostname }}</td>
<td class="tor-hs-status">{{ status.hs_status }}</td>
<td class="tor-hs-services">
{{ status.hs_services|join:', ' }}
</td>
</tr>
</tbody>
</table>
</div>
{% endif %}
{% if status.hs_enabled %}
<div class="table-responsive">
<table class="table tor-hs">
<thead>
<tr>
<th>{% trans "Onion Service" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Ports" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tor-hs-hostname">{{ status.hs_hostname }}</td>
<td class="tor-hs-status">{{ status.hs_status }}</td>
<td class="tor-hs-services">
{{ status.hs_services|join:', ' }}
</td>
</tr>
</tbody>
</table>
</div>
{% endif %}
{% endblock %}
{% block internal_zone %}
{% if not config_running %}
{{ block.super }}
{% endif %}
{{ block.super }}
{% endblock %}
{% block page_js %}
<script type="text/javascript" src="{% static 'tor/tor.js' %}"></script>
{% endblock %}

View File

@ -8,5 +8,5 @@ from django.urls import re_path
from . import views
urlpatterns = [
re_path(r'^apps/tor/$', views.index, name='index'),
re_path(r'^apps/tor/$', views.TorAppView.as_view(), name='index'),
]

View File

@ -2,84 +2,94 @@
"""
FreedomBox app for configuring Tor.
"""
from django.contrib import messages
from django.template.response import TemplateResponse
from django.utils.translation import gettext as _
import logging
from django.utils.translation import gettext_noop
from django.views.generic.edit import FormView
from plinth import actions
from plinth import app as app_module
from plinth.errors import ActionError
from plinth import operation as operation_module
from plinth.modules import tor
from plinth.modules.firewall.components import (Firewall,
get_port_forwarding_info)
from plinth.views import AppView
from . import utils as tor_utils
from .forms import TorForm
config_process = None
logger = logging.getLogger(__name__)
def index(request):
"""Serve configuration page."""
if config_process:
_collect_config_result(request)
class TorAppView(AppView):
"""Show Tor app main page."""
status = tor_utils.get_status()
form = None
app_id = 'tor'
template_name = 'tor.html'
form_class = TorForm
prefix = 'tor'
if request.method == 'POST':
form = TorForm(request.POST, prefix='tor')
# pylint: disable=E1101
if form.is_valid():
_apply_changes(request, status, form.cleaned_data)
status = tor_utils.get_status()
form = TorForm(initial=status, prefix='tor')
status = None
def get_initial(self):
"""Return the values to fill in the form."""
if not self.status:
self.status = tor_utils.get_status()
initial = super().get_initial()
initial.update(self.status)
return initial
def get_context_data(self, *args, **kwargs):
"""Add additional context data for template."""
if not self.status:
self.status = tor_utils.get_status()
context = super().get_context_data(*args, **kwargs)
context['status'] = self.status
return context
def form_valid(self, form):
"""Configure tor app on successful form submission."""
operation_module.manager.new(self.app_id,
gettext_noop('Updating configuration'),
_apply_changes,
[form.initial, form.cleaned_data],
show_notification=False)
# Skip check for 'Settings unchanged' message by calling grandparent
return super(FormView, self).form_valid(form)
def _apply_changes(old_status, new_status):
"""Try to apply changes and handle errors."""
logger.info('tor: applying configuration changes')
exception_to_update = None
message = None
try:
__apply_changes(old_status, new_status)
except Exception as exception:
exception_to_update = exception
message = gettext_noop('Error configuring app: {error}').format(
error=exception)
else:
form = TorForm(initial=status, prefix='tor')
message = gettext_noop('Configuration updated.')
logger.info('tor: configuration changes completed')
operation = operation_module.Operation.get_operation()
operation.on_update(message, exception_to_update)
def __apply_changes(old_status, new_status):
"""Apply the changes."""
needs_restart = True
arguments = []
app = app_module.App.get('tor')
return TemplateResponse(
request, 'tor.html', {
'app_id': 'tor',
'app_info': app.info,
'status': status,
'config_running': bool(config_process),
'form': form,
'firewall': app.get_components_of_type(Firewall),
'has_diagnostics': True,
'is_enabled': status['enabled'],
'is_running': status['is_running'],
'port_forwarding_info': get_port_forwarding_info(app),
'refresh_page_sec': 3 if bool(config_process) else None,
})
def _apply_changes(request, old_status, new_status):
"""Try to apply changes and handle errors."""
try:
__apply_changes(request, old_status, new_status)
except ActionError as exception:
messages.error(
request,
_('Action error: {0} [{1}] [{2}]').format(exception.args[0],
exception.args[1],
exception.args[2]))
def __apply_changes(request, old_status, new_status):
"""Apply the changes."""
global config_process
if config_process:
# Already running a configuration task
return
needs_restart = False
arguments = []
is_enabled = app.is_enabled()
if old_status['relay_enabled'] != new_status['relay_enabled']:
arg_value = 'enable' if new_status['relay_enabled'] else 'disable'
arguments.extend(['--relay', arg_value])
needs_restart = True
if old_status['bridge_relay_enabled'] != \
new_status['bridge_relay_enabled']:
@ -87,80 +97,33 @@ def __apply_changes(request, old_status, new_status):
if not new_status['bridge_relay_enabled']:
arg_value = 'disable'
arguments.extend(['--bridge-relay', arg_value])
needs_restart = True
if old_status['hs_enabled'] != new_status['hs_enabled']:
arg_value = 'enable' if new_status['hs_enabled'] else 'disable'
arguments.extend(['--hidden-service', arg_value])
needs_restart = True
if old_status['apt_transport_tor_enabled'] != \
new_status['apt_transport_tor_enabled']:
arg_value = 'disable'
if new_status['enabled'] and new_status['apt_transport_tor_enabled']:
if is_enabled and new_status['apt_transport_tor_enabled']:
arg_value = 'enable'
arguments.extend(['--apt-transport-tor', arg_value])
needs_restart = False
if old_status['use_upstream_bridges'] != \
new_status['use_upstream_bridges']:
arg_value = 'disable'
if new_status['enabled'] and new_status['use_upstream_bridges']:
arg_value = 'enable'
arg_value = 'enable' if new_status[
'use_upstream_bridges'] else 'disable'
arguments.extend(['--use-upstream-bridges', arg_value])
needs_restart = True
if old_status['upstream_bridges'] != new_status['upstream_bridges']:
arguments.extend(
['--upstream-bridges', new_status['upstream_bridges']])
needs_restart = True
if old_status['enabled'] != new_status['enabled']:
arg_value = 'enable' if new_status['enabled'] else 'disable'
arguments.extend(['--service', arg_value])
# XXX: Perform app enable/disable within the background process
app = app_module.App.get('tor')
if new_status['enabled']:
app.enable()
else:
app.disable()
config_process = actions.superuser_run('tor',
['configure'] + arguments,
run_in_background=True)
return
if arguments:
actions.superuser_run('tor', ['configure'] + arguments)
if not needs_restart:
messages.success(request, _('Configuration updated.'))
if needs_restart and new_status['enabled']:
config_process = actions.superuser_run('tor', ['restart'],
run_in_background=True)
if not arguments:
messages.info(request, _('Setting unchanged'))
def _collect_config_result(request):
"""Handle config process completion."""
global config_process
if not config_process:
return
return_code = config_process.poll()
# Config process is not complete yet
if return_code is None:
return
status = tor_utils.get_status()
tor.update_hidden_service_domain(status)
if not return_code:
messages.success(request, _('Configuration updated.'))
else:
messages.error(request, _('An error occurred during configuration.'))
config_process = None
if needs_restart and is_enabled:
actions.superuser_run('tor', ['restart'])
status = tor_utils.get_status()
tor.update_hidden_service_domain(status)

View File

@ -34,12 +34,9 @@ logger = logging.getLogger(__name__)
base_url = config['DEFAULT']['url']
_app_checkbox_id = {
'tor': 'id_tor-enabled',
'openvpn': 'id_openvpn-enabled',
}
_apps_with_loaders = ['tor']
# unlisted sites just use '/' + site_name as url
_site_url = {
'wiki': '/ikiwiki',
@ -228,9 +225,6 @@ def change_checkbox_status(browser, app_name, checkbox_id,
submit(browser, form_class='form-configuration')
if app_name in _apps_with_loaders:
wait_for_config_update(browser, app_name)
def wait_for_config_update(browser, app_name):
"""Wait until the configuration update progress goes away.
@ -241,7 +235,7 @@ def wait_for_config_update(browser, app_name):
"""
script = 'return (document.readyState == "complete") && ' \
'(!Boolean(document.querySelector(".running-status.loading")));'
'(!Boolean(document.querySelector(".app-operation")));'
while not browser.execute_script(script):
time.sleep(0.1)
@ -410,9 +404,6 @@ def _change_app_status(browser, app_name, change_status_to='enabled'):
change_checkbox_status(browser, app_name, checkbox_id,
change_status_to)
if app_name in _apps_with_loaders:
wait_for_config_update(browser, app_name)
def app_enable(browser, app_name):
nav_to_module(browser, app_name)