mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-28 08:03:36 +00:00
- Add tags to Info component of apps. Use only English tags for all operations. Localized tags are used for presentation to the user only. Add tags to all the apps. Conventions (English): 1. Tags describing use cases should be in kebab case. 2. Protocols in tag names should be in their canonical format. 3. Tags needn't be 100% technically correct. This can get in the way of comparing apps using a tag. Words that describe use cases that users can easily understand should be preferred over being pedantic. 4. Tags should be short, ideally not more than 2 words. Avoid conjunctions like "and", "or" in tags. 5. Avoid redundant words like "server", or "web-clients". Most apps on FreedomBox are either servers or web clients. 6. Keep your nouns singular in tags. - Use query params to filter the Apps page by tags. When all tags are removed, redirect to /apps. - Add UI elements to add and remove tag filters in the Apps page. Make the UI similar to GitLab issue tags. Since there are 40 apps, there will be at least 40 tags. Selecting a tag from a dropdown will be difficult on mobile devices. A fuzzy search is useful to find tags to add to the filter. Allow user to find the best match for the search term and highlight it visually. The user can then press Enter to select the highlighted tag. Make tag search case-insensitive. Make the dropdown menu scrollable with a fixed size. User input is debounced by 300 ms during search. - tests: Add missing mock in test_module_loader.py - Add functional tests [sunil] - 'list' can be used instead of 'List' for typing in recent Python versions. - Reserve tripe-quoted strings for docstrings. - Undo some changes in module initialization, use module_name for logging errors. - isort and yapf changes. - Encode parameters before adding them to the URL. Tests: - Tested the functionality of filtering by tag with one tag and two tags. Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net> Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
256 lines
10 KiB
Python
256 lines
10 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""FreedomBox app to configure Tor."""
|
|
|
|
import json
|
|
import logging
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.utils.translation import gettext_noop
|
|
|
|
from plinth import action_utils
|
|
from plinth import app as app_module
|
|
from plinth import cfg, kvstore, menu
|
|
from plinth import setup as setup_module_ # Not setup_module, for pytest
|
|
from plinth.daemon import (Daemon, app_is_running, diagnose_netcat,
|
|
diagnose_port_listening)
|
|
from plinth.diagnostic_check import DiagnosticCheck, Result
|
|
from plinth.modules import torproxy
|
|
from plinth.modules.apache.components import Webserver
|
|
from plinth.modules.backups.components import BackupRestore
|
|
from plinth.modules.firewall.components import Firewall
|
|
from plinth.modules.names.components import DomainType
|
|
from plinth.modules.torproxy.utils import is_apt_transport_tor_enabled
|
|
from plinth.modules.users.components import UsersAndGroups
|
|
from plinth.package import Packages
|
|
from plinth.signals import domain_added, domain_removed
|
|
from plinth.utils import format_lazy
|
|
|
|
from . import manifest, privileged, utils
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_description = [
|
|
_('Tor is an anonymous communication system. You can learn more '
|
|
'about it from the <a href="https://www.torproject.org/">Tor '
|
|
'Project</a> website. For best protection when web surfing, the '
|
|
'Tor Project recommends that you use the '
|
|
'<a href="https://www.torproject.org/download/download-easy.html.en">'
|
|
'Tor Browser</a>.'),
|
|
_('This app provides relay services to contribute to Tor network and help '
|
|
'others overcome censorship.'),
|
|
format_lazy(
|
|
_('This app provides an onion domain to expose {box_name} services '
|
|
'via the Tor network. Using Tor browser, one can access {box_name} '
|
|
'from the internet even when using an ISP that limits servers at '
|
|
'home.'), box_name=_(cfg.box_name)),
|
|
]
|
|
|
|
|
|
class TorApp(app_module.App):
|
|
"""FreedomBox app for Tor."""
|
|
|
|
app_id = 'tor'
|
|
|
|
_version = 7
|
|
|
|
def __init__(self) -> None:
|
|
"""Create components for the app."""
|
|
super().__init__()
|
|
|
|
info = app_module.Info(app_id=self.app_id, version=self._version,
|
|
depends=['names'
|
|
], name=_('Tor'), icon_filename='tor',
|
|
short_description=_('Anonymity Network'),
|
|
description=_description, manual_page='Tor',
|
|
clients=manifest.clients, tags=manifest.tags,
|
|
donation_url='https://donate.torproject.org/')
|
|
self.add(info)
|
|
|
|
menu_item = menu.Menu('menu-tor', info.name, info.short_description,
|
|
info.icon_filename, 'tor:index',
|
|
parent_url_name='apps')
|
|
self.add(menu_item)
|
|
|
|
packages = Packages('packages-tor',
|
|
['tor', 'tor-geoipdb', 'obfs4proxy'])
|
|
self.add(packages)
|
|
|
|
domain_type = DomainType('domain-type-tor', _('Tor Onion Service'),
|
|
'tor:index', can_have_certificate=False)
|
|
self.add(domain_type)
|
|
|
|
firewall = Firewall('firewall-tor-relay', _('Tor Bridge Relay'),
|
|
ports=['tor-orport', 'tor-obfs3',
|
|
'tor-obfs4'], is_external=True)
|
|
self.add(firewall)
|
|
|
|
daemon = Daemon('daemon-tor', 'tor@plinth', strict_check=True)
|
|
self.add(daemon)
|
|
|
|
webserver = Webserver('webserver-onion-location',
|
|
'onion-location-freedombox')
|
|
self.add(webserver)
|
|
|
|
users_and_groups = UsersAndGroups('users-and-groups-tor',
|
|
reserved_usernames=['debian-tor'])
|
|
self.add(users_and_groups)
|
|
|
|
backup_restore = BackupRestore('backup-restore-tor', **manifest.backup)
|
|
self.add(backup_restore)
|
|
|
|
def post_init(self):
|
|
"""Perform post initialization operations."""
|
|
# Register onion service name with Name Services module.
|
|
if (not self.needs_setup() and self.is_enabled()
|
|
and app_is_running(self)):
|
|
status = utils.get_status(initialized=False)
|
|
hostname = status['hs_hostname']
|
|
services = [int(port['virtport']) for port in status['hs_ports']]
|
|
|
|
if status['hs_enabled'] and status['hs_hostname']:
|
|
domain_added.send_robust(sender='tor',
|
|
domain_type='domain-type-tor',
|
|
name=hostname, services=services)
|
|
|
|
def enable(self):
|
|
"""Enable the app and update firewall ports."""
|
|
super().enable()
|
|
privileged.update_ports()
|
|
update_hidden_service_domain()
|
|
|
|
def disable(self):
|
|
"""Disable the app and remove HS domain."""
|
|
super().disable()
|
|
update_hidden_service_domain()
|
|
|
|
def diagnose(self) -> list[DiagnosticCheck]:
|
|
"""Run diagnostics and return the results."""
|
|
results = super().diagnose()
|
|
|
|
results.extend(_diagnose_control_port())
|
|
|
|
status = utils.get_status()
|
|
ports = status['ports']
|
|
|
|
if status['relay_enabled']:
|
|
results.append(
|
|
DiagnosticCheck(
|
|
'tor-port-relay', gettext_noop('Tor relay port available'),
|
|
Result.PASSED if 'orport' in ports else Result.FAILED))
|
|
if 'orport' in ports:
|
|
results.append(
|
|
diagnose_port_listening(int(ports['orport']), 'tcp4'))
|
|
results.append(
|
|
diagnose_port_listening(int(ports['orport']), 'tcp6'))
|
|
|
|
if status['bridge_relay_enabled']:
|
|
results.append(
|
|
DiagnosticCheck(
|
|
'tor-port-obfs3',
|
|
gettext_noop('Obfs3 transport registered'),
|
|
Result.PASSED if 'obfs3' in ports else Result.FAILED))
|
|
if 'obfs3' in ports:
|
|
results.append(
|
|
diagnose_port_listening(int(ports['obfs3']), 'tcp4'))
|
|
results.append(
|
|
diagnose_port_listening(int(ports['obfs3']), 'tcp6'))
|
|
|
|
results.append(
|
|
DiagnosticCheck(
|
|
'tor-port-obfs4',
|
|
gettext_noop('Obfs4 transport registered'),
|
|
Result.PASSED if 'obfs4' in ports else Result.FAILED))
|
|
if 'obfs4' in ports:
|
|
results.append(
|
|
diagnose_port_listening(int(ports['obfs4']), 'tcp4'))
|
|
results.append(
|
|
diagnose_port_listening(int(ports['obfs4']), 'tcp6'))
|
|
|
|
if status['hs_enabled']:
|
|
hs_hostname = status['hs_hostname'].split('.onion')[0]
|
|
results.append(
|
|
DiagnosticCheck(
|
|
'tor-onion-version',
|
|
gettext_noop('Onion service is version 3'), Result.PASSED
|
|
if len(hs_hostname) == 56 else Result.FAILED))
|
|
|
|
return results
|
|
|
|
def setup(self, old_version):
|
|
"""Install and configure the app."""
|
|
super().setup(old_version)
|
|
privileged.setup(old_version)
|
|
status = utils.get_status()
|
|
update_hidden_service_domain(status)
|
|
|
|
# Enable/disable Onion-Location component based on app status.
|
|
# Component was introduced in version 6.
|
|
if old_version and old_version < 6:
|
|
daemon_component = self.get_component('daemon-tor')
|
|
component = self.get_component('webserver-onion-location')
|
|
if daemon_component.is_enabled():
|
|
logger.info('Enabling Onion-Location component')
|
|
component.enable()
|
|
else:
|
|
logger.info('Disabling Onion-Location component')
|
|
component.disable()
|
|
|
|
# The SOCKS proxy and "Download software packages using Tor" features
|
|
# were moved into a new app, Tor Proxy, in version 7. If Tor is
|
|
# enabled, then store the relevant configuration, and install Tor
|
|
# Proxy.
|
|
if old_version and old_version < 7 and self.is_enabled():
|
|
logger.info('Tor Proxy app will be installed')
|
|
config = {
|
|
'use_upstream_bridges': status['use_upstream_bridges'],
|
|
'upstream_bridges': status['upstream_bridges'],
|
|
'apt_transport_tor': is_apt_transport_tor_enabled()
|
|
}
|
|
kvstore.set(torproxy.PREINSTALL_CONFIG_KEY, json.dumps(config))
|
|
# This creates the operation, which will run after the current
|
|
# operation (Tor setup) is completed.
|
|
setup_module_.run_setup_on_app('torproxy')
|
|
|
|
if not old_version:
|
|
logger.info('Enabling Tor app')
|
|
self.enable()
|
|
|
|
def uninstall(self):
|
|
"""De-configure and uninstall the app."""
|
|
super().uninstall()
|
|
privileged.uninstall()
|
|
|
|
|
|
def update_hidden_service_domain(status=None):
|
|
"""Update onion service domain with Name Services module."""
|
|
if not status:
|
|
status = utils.get_status()
|
|
|
|
domain_removed.send_robust(sender='tor', domain_type='domain-type-tor')
|
|
|
|
if status['enabled'] and status['is_running'] and \
|
|
status['hs_enabled'] and status['hs_hostname']:
|
|
services = [int(port['virtport']) for port in status['hs_ports']]
|
|
domain_added.send_robust(sender='tor', domain_type='domain-type-tor',
|
|
name=status['hs_hostname'], services=services)
|
|
|
|
|
|
def _diagnose_control_port() -> list[DiagnosticCheck]:
|
|
"""Diagnose whether Tor control port is open on 127.0.0.1 only."""
|
|
results = []
|
|
|
|
addresses = action_utils.get_ip_addresses()
|
|
for address in addresses:
|
|
if address['kind'] != '4':
|
|
continue
|
|
|
|
negate = True
|
|
if address['address'] == '127.0.0.1':
|
|
negate = False
|
|
|
|
results.append(
|
|
diagnose_netcat(str(address['address']), 9051,
|
|
remote_input='QUIT\n', negate=negate))
|
|
|
|
return results
|