mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-04-01 09:30:29 +00:00
torproxy: Add separate app for Tor Proxy
- Includes SocksPort and "Download software packages over Tor" feature,
as well as setting upstream bridges.
- "Download software packages over Tor" option is enabled by default.
- When upgrading, if Tor app was enabled and "Download software
packages over Tor" was enabled, then Tor Proxy will be installed.
- The default tor instance is now called tor@default. The "tor" service
is an multi-instance master that has Wants relation all instances.
Tests:
- Tests for Tor and Tor Proxy passed.
- Enable Tor, and run the tests for Tor Proxy. Afterwards, Tor is still
enabled and running.
- Enable Tor Proxy, and run the tests for Tor. Afterwards, Tor Proxy is
still enabled and running.
- Test setting upstream bridges for Tor and Tor Proxy.
- Install FreedomBox 23.11 in a VM and install Tor with default
settings. Install new FreedomBox version with Tor Proxy. After
install, both Tor and Tor Proxy apps are installed and running.
/etc/tor/instances/{plinth,fbxproxy}/torrc both have expected content.
Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
64d6356c2f
commit
b0c75b7849
@ -1,24 +1,29 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app to configure Tor."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth import action_utils
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, menu
|
||||
from plinth import menu
|
||||
from plinth import setup as setup_module
|
||||
from plinth.daemon import (Daemon, app_is_running, diagnose_netcat,
|
||||
diagnose_port_listening)
|
||||
from plinth.modules.apache.components import Webserver, diagnose_url
|
||||
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 '
|
||||
@ -26,9 +31,6 @@ _description = [
|
||||
'Tor Project recommends that you use the '
|
||||
'<a href="https://www.torproject.org/download/download-easy.html.en">'
|
||||
'Tor Browser</a>.'),
|
||||
format_lazy(
|
||||
_('A Tor SOCKS port is available on your {box_name} for internal '
|
||||
'networks on TCP port 9050.'), box_name=_(cfg.box_name))
|
||||
]
|
||||
|
||||
|
||||
@ -37,7 +39,7 @@ class TorApp(app_module.App):
|
||||
|
||||
app_id = 'tor'
|
||||
|
||||
_version = 6
|
||||
_version = 7
|
||||
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
@ -57,28 +59,20 @@ class TorApp(app_module.App):
|
||||
parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
packages = Packages('packages-tor', [
|
||||
'tor', 'tor-geoipdb', 'torsocks', 'obfs4proxy', 'apt-transport-tor'
|
||||
])
|
||||
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-socks', _('Tor Socks Proxy'),
|
||||
ports=['tor-socks'], is_external=False)
|
||||
self.add(firewall)
|
||||
|
||||
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,
|
||||
listen_ports=[(9050, 'tcp4'), (9050, 'tcp6'), (9040, 'tcp4'),
|
||||
(9040, 'tcp6'), (9053, 'udp4'), (9053, 'udp6')])
|
||||
daemon = Daemon('daemon-tor', 'tor@plinth', strict_check=True)
|
||||
self.add(daemon)
|
||||
|
||||
webserver = Webserver('webserver-onion-location',
|
||||
@ -113,8 +107,7 @@ class TorApp(app_module.App):
|
||||
update_hidden_service_domain()
|
||||
|
||||
def disable(self):
|
||||
"""Disable APT use of Tor before disabling."""
|
||||
privileged.configure(apt_transport_tor=False)
|
||||
"""Disable the app and remove HS domain."""
|
||||
super().disable()
|
||||
update_hidden_service_domain()
|
||||
|
||||
@ -166,21 +159,12 @@ class TorApp(app_module.App):
|
||||
'passed' if len(hs_hostname) == 56 else 'failed'
|
||||
])
|
||||
|
||||
results.append(_diagnose_url_via_tor('http://www.debian.org', '4'))
|
||||
results.append(_diagnose_url_via_tor('http://www.debian.org', '6'))
|
||||
|
||||
results.append(_diagnose_tor_use('https://check.torproject.org', '4'))
|
||||
results.append(_diagnose_tor_use('https://check.torproject.org', '6'))
|
||||
|
||||
return results
|
||||
|
||||
def setup(self, old_version):
|
||||
"""Install and configure the app."""
|
||||
super().setup(old_version)
|
||||
privileged.setup(old_version)
|
||||
if not old_version:
|
||||
privileged.configure(apt_transport_tor=True)
|
||||
|
||||
update_hidden_service_domain(utils.get_status())
|
||||
|
||||
# Enable/disable Onion-Location component based on app status.
|
||||
@ -189,11 +173,26 @@ class TorApp(app_module.App):
|
||||
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 the "Download
|
||||
# software packages using Tor" option was enabled, then install and
|
||||
# enable Tor Proxy, to avoid any issues for apt.
|
||||
if old_version and old_version < 7:
|
||||
if self.is_enabled() and is_apt_transport_tor_enabled():
|
||||
logger.info(
|
||||
'Tor Proxy app will be installed for apt-transport-tor')
|
||||
# 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):
|
||||
@ -234,23 +233,3 @@ def _diagnose_control_port():
|
||||
negate=negate))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _diagnose_url_via_tor(url, kind=None):
|
||||
"""Diagnose whether a URL is reachable via Tor."""
|
||||
result = diagnose_url(url, kind=kind, wrapper='torsocks')
|
||||
result[0] = _('Access URL {url} on tcp{kind} via Tor') \
|
||||
.format(url=url, kind=kind)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _diagnose_tor_use(url, kind=None):
|
||||
"""Diagnose whether webpage at URL reports that we are using Tor."""
|
||||
expected_output = 'Congratulations. This browser is configured to use Tor.'
|
||||
result = diagnose_url(url, kind=kind, wrapper='torsocks',
|
||||
expected_output=expected_output)
|
||||
result[0] = _('Confirm Tor usage at {url} on tcp{kind}') \
|
||||
.format(url=url, kind=kind)
|
||||
|
||||
return result
|
||||
|
||||
@ -70,8 +70,8 @@ def bridges_validator(bridges):
|
||||
raise validation_error
|
||||
|
||||
|
||||
class TorForm(forms.Form): # pylint: disable=W0232
|
||||
"""Tor configuration form."""
|
||||
class TorCommonForm(forms.Form):
|
||||
"""Tor common configuration form."""
|
||||
use_upstream_bridges = forms.BooleanField(
|
||||
label=_('Use upstream bridges to connect to Tor network'),
|
||||
required=False, help_text=_(
|
||||
@ -87,6 +87,10 @@ class TorForm(forms.Form): # pylint: disable=W0232
|
||||
'https://bridges.torproject.org/</a> and copy/paste the bridge '
|
||||
'information here. Currently supported transports are none, '
|
||||
'obfs3, obfs4 and scamblesuit.'), validators=[bridges_validator])
|
||||
|
||||
|
||||
class TorForm(TorCommonForm):
|
||||
"""Tor configuration form."""
|
||||
relay_enabled = forms.BooleanField(
|
||||
label=_('Enable Tor relay'), required=False, help_text=format_lazy(
|
||||
_('When enabled, your {box_name} will run a Tor relay and donate '
|
||||
@ -107,12 +111,6 @@ class TorForm(forms.Form): # pylint: disable=W0232
|
||||
'services (such as wiki or chat) without revealing its '
|
||||
'location. Do not use this for strong anonymity yet.'),
|
||||
box_name=_(cfg.box_name)))
|
||||
apt_transport_tor_enabled = forms.BooleanField(
|
||||
label=_('Download software packages over Tor'), required=False,
|
||||
help_text=_('When enabled, software will be downloaded over the Tor '
|
||||
'network for installations and upgrades. This adds a '
|
||||
'degree of privacy and security during software '
|
||||
'downloads.'))
|
||||
|
||||
def clean(self):
|
||||
"""Validate the form for cross-field integrity."""
|
||||
|
||||
@ -44,11 +44,11 @@ clients = [{
|
||||
|
||||
backup = {
|
||||
'config': {
|
||||
'directories': ['/etc/tor/'],
|
||||
'directories': ['/etc/tor/instances/plinth/'],
|
||||
'files': [str(privileged.TOR_APACHE_SITE)]
|
||||
},
|
||||
'secrets': {
|
||||
'directories': ['/var/lib/tor/', '/var/lib/tor-instances/']
|
||||
'directories': ['/var/lib/tor-instances/plinth/']
|
||||
},
|
||||
'services': ['tor@plinth']
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"""Configure Tor service."""
|
||||
|
||||
import codecs
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
@ -15,20 +16,29 @@ import augeas
|
||||
|
||||
from plinth import action_utils
|
||||
from plinth.actions import privileged
|
||||
from plinth.modules.tor.utils import APT_TOR_PREFIX, get_augeas, iter_apt_uris
|
||||
|
||||
INSTANCE_NAME = 'plinth'
|
||||
SERVICE_FILE = '/etc/firewalld/services/tor-{0}.xml'
|
||||
TOR_CONFIG = '/files/etc/tor/instances/plinth/torrc'
|
||||
TOR_STATE_FILE = '/var/lib/tor-instances/plinth/state'
|
||||
TOR_AUTH_COOKIE = '/var/run/tor-instances/plinth/control.authcookie'
|
||||
SERVICE_NAME = f'tor@{INSTANCE_NAME}'
|
||||
TOR_CONFIG = f'/etc/tor/instances/{INSTANCE_NAME}/torrc'
|
||||
TOR_CONFIG_AUG = f'/files/{TOR_CONFIG}'
|
||||
TOR_STATE_FILE = f'/var/lib/tor-instances/{INSTANCE_NAME}/state'
|
||||
TOR_AUTH_COOKIE = f'/var/run/tor-instances/{INSTANCE_NAME}/control.authcookie'
|
||||
TOR_APACHE_SITE = '/etc/apache2/conf-available/onion-location-freedombox.conf'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@privileged
|
||||
def setup(old_version: int):
|
||||
"""Setup Tor configuration after installing it."""
|
||||
if old_version and old_version <= 4:
|
||||
_upgrade_orport_value()
|
||||
if old_version:
|
||||
if old_version <= 4:
|
||||
_upgrade_orport_value()
|
||||
|
||||
if old_version <= 6:
|
||||
_remove_proxy()
|
||||
|
||||
return
|
||||
|
||||
_first_time_setup()
|
||||
@ -37,50 +47,39 @@ def setup(old_version: int):
|
||||
|
||||
def _first_time_setup():
|
||||
"""Setup Tor configuration for the first time setting defaults."""
|
||||
logger.info('Performing first time setup for Tor')
|
||||
# Disable default tor service. We will use tor@plinth instance
|
||||
# instead.
|
||||
_disable_apt_transport_tor()
|
||||
action_utils.service_disable('tor')
|
||||
action_utils.service_disable('tor@default')
|
||||
|
||||
subprocess.run(['tor-instance-create', 'plinth'], check=True)
|
||||
subprocess.run(['tor-instance-create', INSTANCE_NAME], check=True)
|
||||
|
||||
# Remove line starting with +SocksPort, since our augeas lens
|
||||
# doesn't handle it correctly.
|
||||
with open('/etc/tor/instances/plinth/torrc', 'r',
|
||||
encoding='utf-8') as torrc:
|
||||
with open(TOR_CONFIG, 'r', encoding='utf-8') as torrc:
|
||||
torrc_lines = torrc.readlines()
|
||||
with open('/etc/tor/instances/plinth/torrc', 'w',
|
||||
encoding='utf-8') as torrc:
|
||||
with open(TOR_CONFIG, 'w', encoding='utf-8') as torrc:
|
||||
for line in torrc_lines:
|
||||
if not line.startswith('+'):
|
||||
torrc.write(line)
|
||||
|
||||
aug = augeas_load()
|
||||
|
||||
aug.set(TOR_CONFIG + '/SocksPort[1]', '[::]:9050')
|
||||
aug.set(TOR_CONFIG + '/SocksPort[2]', '0.0.0.0:9050')
|
||||
aug.set(TOR_CONFIG + '/ControlPort', '9051')
|
||||
aug.set(TOR_CONFIG_AUG + '/ControlPort', '9051')
|
||||
_enable_relay(relay=True, bridge=True, aug=aug)
|
||||
aug.set(TOR_CONFIG + '/ExitPolicy[1]', 'reject *:*')
|
||||
aug.set(TOR_CONFIG + '/ExitPolicy[2]', 'reject6 *:*')
|
||||
aug.set(TOR_CONFIG_AUG + '/ExitPolicy[1]', 'reject *:*')
|
||||
aug.set(TOR_CONFIG_AUG + '/ExitPolicy[2]', 'reject6 *:*')
|
||||
|
||||
aug.set(TOR_CONFIG + '/VirtualAddrNetworkIPv4', '10.192.0.0/10')
|
||||
aug.set(TOR_CONFIG + '/AutomapHostsOnResolve', '1')
|
||||
aug.set(TOR_CONFIG + '/TransPort[1]', '127.0.0.1:9040')
|
||||
aug.set(TOR_CONFIG + '/TransPort[2]', '[::1]:9040')
|
||||
aug.set(TOR_CONFIG + '/DNSPort[1]', '127.0.0.1:9053')
|
||||
aug.set(TOR_CONFIG + '/DNSPort[2]', '[::1]:9053')
|
||||
|
||||
aug.set(TOR_CONFIG + '/HiddenServiceDir',
|
||||
'/var/lib/tor-instances/plinth/hidden_service')
|
||||
aug.set(TOR_CONFIG + '/HiddenServicePort[1]', '22 127.0.0.1:22')
|
||||
aug.set(TOR_CONFIG + '/HiddenServicePort[2]', '80 127.0.0.1:80')
|
||||
aug.set(TOR_CONFIG + '/HiddenServicePort[3]', '443 127.0.0.1:443')
|
||||
aug.set(TOR_CONFIG_AUG + '/HiddenServiceDir',
|
||||
f'/var/lib/tor-instances/{INSTANCE_NAME}/hidden_service')
|
||||
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[1]', '22 127.0.0.1:22')
|
||||
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[2]', '80 127.0.0.1:80')
|
||||
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[3]', '443 127.0.0.1:443')
|
||||
|
||||
aug.save()
|
||||
|
||||
action_utils.service_enable('tor@plinth')
|
||||
action_utils.service_restart('tor@plinth')
|
||||
action_utils.service_enable(SERVICE_NAME)
|
||||
action_utils.service_restart(SERVICE_NAME)
|
||||
_update_ports()
|
||||
|
||||
# wait until hidden service information is available
|
||||
@ -110,28 +109,45 @@ def _upgrade_orport_value():
|
||||
443 is not possible in FreedomBox due it is use for other purposes.
|
||||
|
||||
"""
|
||||
logger.info('Upgrading ORPort value for Tor')
|
||||
aug = augeas_load()
|
||||
|
||||
if _is_relay_enabled(aug):
|
||||
aug.set(TOR_CONFIG + '/ORPort[1]', '9001')
|
||||
aug.set(TOR_CONFIG + '/ORPort[2]', '[::]:9001')
|
||||
aug.set(TOR_CONFIG_AUG + '/ORPort[1]', '9001')
|
||||
aug.set(TOR_CONFIG_AUG + '/ORPort[2]', '[::]:9001')
|
||||
|
||||
aug.save()
|
||||
|
||||
action_utils.service_try_restart('tor@plinth')
|
||||
action_utils.service_try_restart(SERVICE_NAME)
|
||||
|
||||
# Tor may not be running, don't try to read/update all ports
|
||||
_update_port('orport', 9001)
|
||||
action_utils.service_restart('firewalld')
|
||||
|
||||
|
||||
def _remove_proxy():
|
||||
"""Remove SocksProxy from configuration.
|
||||
|
||||
This functionality was split off to a separate app, Tor Proxy.
|
||||
"""
|
||||
logger.info('Removing SocksProxy from Tor configuration')
|
||||
aug = augeas_load()
|
||||
for config in [
|
||||
'SocksPort', 'VirtualAddrNetworkIPv4', 'AutomapHostsOnResolve',
|
||||
'TransPort', 'DNSPort'
|
||||
]:
|
||||
aug.remove(TOR_CONFIG_AUG + '/' + config)
|
||||
|
||||
aug.save()
|
||||
action_utils.service_try_restart(SERVICE_NAME)
|
||||
|
||||
|
||||
@privileged
|
||||
def configure(use_upstream_bridges: Optional[bool] = None,
|
||||
upstream_bridges: Optional[str] = None,
|
||||
relay: Optional[bool] = None,
|
||||
bridge_relay: Optional[bool] = None,
|
||||
hidden_service: Optional[bool] = None,
|
||||
apt_transport_tor: Optional[bool] = None):
|
||||
hidden_service: Optional[bool] = None):
|
||||
"""Configure Tor."""
|
||||
aug = augeas_load()
|
||||
|
||||
@ -151,11 +167,6 @@ def configure(use_upstream_bridges: Optional[bool] = None,
|
||||
elif hidden_service is not None:
|
||||
_disable_hs(aug=aug)
|
||||
|
||||
if apt_transport_tor:
|
||||
_enable_apt_transport_tor()
|
||||
elif apt_transport_tor is not None:
|
||||
_disable_apt_transport_tor()
|
||||
|
||||
|
||||
@privileged
|
||||
def update_ports():
|
||||
@ -166,12 +177,12 @@ def update_ports():
|
||||
@privileged
|
||||
def restart():
|
||||
"""Restart Tor."""
|
||||
if (action_utils.service_is_enabled('tor@plinth', strict_check=True)
|
||||
and action_utils.service_is_running('tor@plinth')):
|
||||
action_utils.service_restart('tor@plinth')
|
||||
if (action_utils.service_is_enabled(SERVICE_NAME, strict_check=True)
|
||||
and action_utils.service_is_running(SERVICE_NAME)):
|
||||
action_utils.service_restart(SERVICE_NAME)
|
||||
|
||||
aug = augeas_load()
|
||||
if aug.get(TOR_CONFIG + '/HiddenServiceDir'):
|
||||
if aug.get(TOR_CONFIG_AUG + '/HiddenServiceDir'):
|
||||
# wait until hidden service information is available
|
||||
tries = 0
|
||||
while not _get_hidden_service()['enabled']:
|
||||
@ -197,26 +208,26 @@ def get_status() -> dict[str, Union[bool, str, dict[str, Any]]]:
|
||||
|
||||
def _are_upstream_bridges_enabled(aug) -> bool:
|
||||
"""Return whether upstream bridges are being used."""
|
||||
use_bridges = aug.get(TOR_CONFIG + '/UseBridges')
|
||||
use_bridges = aug.get(TOR_CONFIG_AUG + '/UseBridges')
|
||||
return use_bridges == '1'
|
||||
|
||||
|
||||
def _get_upstream_bridges(aug) -> str:
|
||||
"""Return upstream bridges separated by newlines."""
|
||||
matches = aug.match(TOR_CONFIG + '/Bridge')
|
||||
matches = aug.match(TOR_CONFIG_AUG + '/Bridge')
|
||||
bridges = [aug.get(match) for match in matches]
|
||||
return '\n'.join(bridges)
|
||||
|
||||
|
||||
def _is_relay_enabled(aug) -> bool:
|
||||
"""Return whether a relay is enabled."""
|
||||
orport = aug.get(TOR_CONFIG + '/ORPort[1]')
|
||||
orport = aug.get(TOR_CONFIG_AUG + '/ORPort[1]')
|
||||
return bool(orport) and orport != '0'
|
||||
|
||||
|
||||
def _is_bridge_relay_enabled(aug) -> bool:
|
||||
"""Return whether bridge relay is enabled."""
|
||||
bridge = aug.get(TOR_CONFIG + '/BridgeRelay')
|
||||
bridge = aug.get(TOR_CONFIG_AUG + '/BridgeRelay')
|
||||
return bridge == '1'
|
||||
|
||||
|
||||
@ -272,8 +283,8 @@ def _get_hidden_service(aug=None) -> dict[str, Any]:
|
||||
if not aug:
|
||||
aug = augeas_load()
|
||||
|
||||
hs_dir = aug.get(TOR_CONFIG + '/HiddenServiceDir')
|
||||
hs_port_paths = aug.match(TOR_CONFIG + '/HiddenServicePort')
|
||||
hs_dir = aug.get(TOR_CONFIG_AUG + '/HiddenServiceDir')
|
||||
hs_port_paths = aug.match(TOR_CONFIG_AUG + '/HiddenServicePort')
|
||||
|
||||
for hs_port_path in hs_port_paths:
|
||||
port_info = aug.get(hs_port_path).split()
|
||||
@ -300,14 +311,13 @@ def _get_hidden_service(aug=None) -> dict[str, Any]:
|
||||
|
||||
def _enable():
|
||||
"""Enable and start the service."""
|
||||
action_utils.service_enable('tor@plinth')
|
||||
action_utils.service_enable(SERVICE_NAME)
|
||||
_update_ports()
|
||||
|
||||
|
||||
def _disable():
|
||||
"""Disable and stop the service."""
|
||||
_disable_apt_transport_tor()
|
||||
action_utils.service_disable('tor@plinth')
|
||||
action_utils.service_disable(SERVICE_NAME)
|
||||
|
||||
|
||||
def _use_upstream_bridges(use_upstream_bridges: Optional[bool] = None,
|
||||
@ -320,9 +330,9 @@ def _use_upstream_bridges(use_upstream_bridges: Optional[bool] = None,
|
||||
aug = augeas_load()
|
||||
|
||||
if use_upstream_bridges:
|
||||
aug.set(TOR_CONFIG + '/UseBridges', '1')
|
||||
aug.set(TOR_CONFIG_AUG + '/UseBridges', '1')
|
||||
else:
|
||||
aug.set(TOR_CONFIG + '/UseBridges', '0')
|
||||
aug.set(TOR_CONFIG_AUG + '/UseBridges', '0')
|
||||
|
||||
aug.save()
|
||||
|
||||
@ -335,16 +345,16 @@ def _set_upstream_bridges(upstream_bridges=None, aug=None):
|
||||
if not aug:
|
||||
aug = augeas_load()
|
||||
|
||||
aug.remove(TOR_CONFIG + '/Bridge')
|
||||
aug.remove(TOR_CONFIG_AUG + '/Bridge')
|
||||
if upstream_bridges:
|
||||
bridges = [bridge.strip() for bridge in upstream_bridges.split('\n')]
|
||||
bridges = [bridge for bridge in bridges if bridge]
|
||||
for bridge in bridges:
|
||||
parts = [part for part in bridge.split() if part]
|
||||
bridge = ' '.join(parts)
|
||||
aug.set(TOR_CONFIG + '/Bridge[last() + 1]', bridge.strip())
|
||||
aug.set(TOR_CONFIG_AUG + '/Bridge[last() + 1]', bridge.strip())
|
||||
|
||||
aug.set(TOR_CONFIG + '/ClientTransportPlugin',
|
||||
aug.set(TOR_CONFIG_AUG + '/ClientTransportPlugin',
|
||||
'obfs3,scramblesuit,obfs4 exec /usr/bin/obfs4proxy')
|
||||
|
||||
aug.save()
|
||||
@ -362,20 +372,20 @@ def _enable_relay(relay: Optional[bool], bridge: Optional[bool],
|
||||
use_upstream_bridges = _are_upstream_bridges_enabled(aug)
|
||||
|
||||
if relay and not use_upstream_bridges:
|
||||
aug.set(TOR_CONFIG + '/ORPort[1]', '9001')
|
||||
aug.set(TOR_CONFIG + '/ORPort[2]', '[::]:9001')
|
||||
aug.set(TOR_CONFIG_AUG + '/ORPort[1]', '9001')
|
||||
aug.set(TOR_CONFIG_AUG + '/ORPort[2]', '[::]:9001')
|
||||
elif relay is not None:
|
||||
aug.remove(TOR_CONFIG + '/ORPort')
|
||||
aug.remove(TOR_CONFIG_AUG + '/ORPort')
|
||||
|
||||
if bridge and not use_upstream_bridges:
|
||||
aug.set(TOR_CONFIG + '/BridgeRelay', '1')
|
||||
aug.set(TOR_CONFIG + '/ServerTransportPlugin',
|
||||
aug.set(TOR_CONFIG_AUG + '/BridgeRelay', '1')
|
||||
aug.set(TOR_CONFIG_AUG + '/ServerTransportPlugin',
|
||||
'obfs3,obfs4 exec /usr/bin/obfs4proxy')
|
||||
aug.set(TOR_CONFIG + '/ExtORPort', 'auto')
|
||||
aug.set(TOR_CONFIG_AUG + '/ExtORPort', 'auto')
|
||||
elif bridge is not None:
|
||||
aug.remove(TOR_CONFIG + '/BridgeRelay')
|
||||
aug.remove(TOR_CONFIG + '/ServerTransportPlugin')
|
||||
aug.remove(TOR_CONFIG + '/ExtORPort')
|
||||
aug.remove(TOR_CONFIG_AUG + '/BridgeRelay')
|
||||
aug.remove(TOR_CONFIG_AUG + '/ServerTransportPlugin')
|
||||
aug.remove(TOR_CONFIG_AUG + '/ExtORPort')
|
||||
|
||||
aug.save()
|
||||
|
||||
@ -388,11 +398,11 @@ def _enable_hs(aug=None):
|
||||
if _get_hidden_service(aug)['enabled']:
|
||||
return
|
||||
|
||||
aug.set(TOR_CONFIG + '/HiddenServiceDir',
|
||||
'/var/lib/tor-instances/plinth/hidden_service')
|
||||
aug.set(TOR_CONFIG + '/HiddenServicePort[1]', '22 127.0.0.1:22')
|
||||
aug.set(TOR_CONFIG + '/HiddenServicePort[2]', '80 127.0.0.1:80')
|
||||
aug.set(TOR_CONFIG + '/HiddenServicePort[3]', '443 127.0.0.1:443')
|
||||
aug.set(TOR_CONFIG_AUG + '/HiddenServiceDir',
|
||||
f'/var/lib/tor-instances/{INSTANCE_NAME}/hidden_service')
|
||||
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[1]', '22 127.0.0.1:22')
|
||||
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[2]', '80 127.0.0.1:80')
|
||||
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[3]', '443 127.0.0.1:443')
|
||||
aug.save()
|
||||
_set_onion_header(_get_hidden_service(aug))
|
||||
|
||||
@ -405,39 +415,12 @@ def _disable_hs(aug=None):
|
||||
if not _get_hidden_service(aug)['enabled']:
|
||||
return
|
||||
|
||||
aug.remove(TOR_CONFIG + '/HiddenServiceDir')
|
||||
aug.remove(TOR_CONFIG + '/HiddenServicePort')
|
||||
aug.remove(TOR_CONFIG_AUG + '/HiddenServiceDir')
|
||||
aug.remove(TOR_CONFIG_AUG + '/HiddenServicePort')
|
||||
aug.save()
|
||||
_set_onion_header(None)
|
||||
|
||||
|
||||
def _enable_apt_transport_tor():
|
||||
"""Enable package download over Tor."""
|
||||
aug = get_augeas()
|
||||
for uri_path in iter_apt_uris(aug):
|
||||
uri = aug.get(uri_path)
|
||||
if uri.startswith('http://') or uri.startswith('https://'):
|
||||
aug.set(uri_path, APT_TOR_PREFIX + uri)
|
||||
|
||||
aug.save()
|
||||
|
||||
|
||||
def _disable_apt_transport_tor():
|
||||
"""Disable package download over Tor."""
|
||||
try:
|
||||
aug = get_augeas()
|
||||
except Exception:
|
||||
# Disable what we can, so APT is not unusable.
|
||||
pass
|
||||
|
||||
for uri_path in iter_apt_uris(aug):
|
||||
uri = aug.get(uri_path)
|
||||
if uri.startswith(APT_TOR_PREFIX):
|
||||
aug.set(uri_path, uri[len(APT_TOR_PREFIX):])
|
||||
|
||||
aug.save()
|
||||
|
||||
|
||||
def _update_port(name, number):
|
||||
"""Update firewall service information for single port."""
|
||||
lines = """<?xml version="1.0" encoding="utf-8"?>
|
||||
@ -485,14 +468,14 @@ def augeas_load():
|
||||
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
|
||||
augeas.Augeas.NO_MODL_AUTOLOAD)
|
||||
aug.set('/augeas/load/Tor/lens', 'Tor.lns')
|
||||
aug.set('/augeas/load/Tor/incl[last() + 1]',
|
||||
'/etc/tor/instances/plinth/torrc')
|
||||
aug.set('/augeas/load/Tor/incl[last() + 1]', TOR_CONFIG)
|
||||
aug.load()
|
||||
return aug
|
||||
|
||||
|
||||
def _set_onion_header(hidden_service):
|
||||
"""Set Apache configuration for the Onion-Location header."""
|
||||
logger.info('Setting Onion-Location header for Apache')
|
||||
config_file = pathlib.Path(TOR_APACHE_SITE)
|
||||
if hidden_service and hidden_service['enabled']:
|
||||
# https://community.torproject.org/onion-services/advanced/onion-location/
|
||||
@ -512,10 +495,13 @@ def _set_onion_header(hidden_service):
|
||||
|
||||
@privileged
|
||||
def uninstall():
|
||||
"""Remove create instances."""
|
||||
"""Remove plinth instance."""
|
||||
directories = [
|
||||
'/etc/tor/instances/', '/var/lib/tor-instances/',
|
||||
'/var/run/tor-instances/'
|
||||
f'/etc/tor/instances/{INSTANCE_NAME}/',
|
||||
f'/var/lib/tor-instances/{INSTANCE_NAME}/',
|
||||
f'/var/run/tor-instances/{INSTANCE_NAME}/'
|
||||
]
|
||||
for directory in directories:
|
||||
shutil.rmtree(directory, ignore_errors=True)
|
||||
|
||||
os.unlink(f'/var/run/tor-instances/{INSTANCE_NAME}.defaults')
|
||||
|
||||
@ -11,7 +11,6 @@ _TOR_FEATURE_TO_ELEMENT = {
|
||||
'relay': 'tor-relay_enabled',
|
||||
'bridge-relay': 'tor-bridge_relay_enabled',
|
||||
'hidden-services': 'tor-hs_enabled',
|
||||
'software': 'tor-apt_transport_tor_enabled'
|
||||
}
|
||||
|
||||
pytestmark = [pytest.mark.apps, pytest.mark.domain, pytest.mark.tor]
|
||||
@ -19,11 +18,8 @@ pytestmark = [pytest.mark.apps, pytest.mark.domain, pytest.mark.tor]
|
||||
|
||||
class TestTorApp(functional.BaseAppTests):
|
||||
app_name = 'tor'
|
||||
has_service = True
|
||||
has_service = False
|
||||
has_web = False
|
||||
# TODO: Investigate why accessing IPv6 sites through Tor fails in
|
||||
# container.
|
||||
check_diagnostics = False
|
||||
|
||||
def test_set_tor_relay_configuration(self, session_browser):
|
||||
"""Test setting Tor relay configuration."""
|
||||
@ -52,13 +48,6 @@ class TestTorApp(functional.BaseAppTests):
|
||||
enabled=True)
|
||||
_assert_hidden_services(session_browser)
|
||||
|
||||
def test_set_download_software_packages_over_tor(self, session_browser):
|
||||
"""Test setting download software packages over Tor."""
|
||||
functional.app_enable(session_browser, 'tor')
|
||||
_feature_enable(session_browser, 'software', should_enable=True)
|
||||
_feature_enable(session_browser, 'software', should_enable=False)
|
||||
_assert_feature_enabled(session_browser, 'software', enabled=False)
|
||||
|
||||
# TODO: Test more thoroughly by checking same hidden service is restored
|
||||
# and by actually connecting using Tor.
|
||||
@pytest.mark.backups
|
||||
|
||||
@ -14,14 +14,6 @@ from plinth.modules.tor import forms, utils
|
||||
class TestTor:
|
||||
"""Test cases for testing the Tor module."""
|
||||
|
||||
@staticmethod
|
||||
@pytest.mark.usefixtures('needs_root')
|
||||
def test_is_apt_transport_tor_enabled():
|
||||
"""Test that is_apt_transport_tor_enabled does not raise any unhandled
|
||||
exceptions.
|
||||
"""
|
||||
utils.is_apt_transport_tor_enabled()
|
||||
|
||||
@staticmethod
|
||||
@patch('plinth.app.App.get')
|
||||
@pytest.mark.usefixtures('needs_root', 'load_cfg')
|
||||
@ -29,7 +21,7 @@ class TestTor:
|
||||
"""Test that get_status does not raise any unhandled exceptions.
|
||||
|
||||
This should work regardless of whether tor is installed, or
|
||||
/etc/tor/torrc exists.
|
||||
/etc/tor/instances/plinth/torrc exists.
|
||||
"""
|
||||
utils.get_status()
|
||||
|
||||
|
||||
@ -1,21 +1,12 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Tor utility functions."""
|
||||
|
||||
import itertools
|
||||
|
||||
import augeas
|
||||
|
||||
from plinth import app as app_module
|
||||
from plinth.daemon import app_is_running
|
||||
from plinth.modules.names.components import DomainName
|
||||
|
||||
from . import privileged
|
||||
|
||||
APT_SOURCES_URI_PATHS = ('/files/etc/apt/sources.list/*/uri',
|
||||
'/files/etc/apt/sources.list.d/*/*/uri',
|
||||
'/files/etc/apt/sources.list.d/*/*/URIs/*')
|
||||
APT_TOR_PREFIX = 'tor+'
|
||||
|
||||
|
||||
def get_status(initialized=True):
|
||||
"""Return current Tor status."""
|
||||
@ -53,50 +44,4 @@ def get_status(initialized=True):
|
||||
'hs_hostname': hs_info['hostname'],
|
||||
'hs_ports': hs_info['ports'],
|
||||
'hs_services': hs_services,
|
||||
'apt_transport_tor_enabled': is_apt_transport_tor_enabled()
|
||||
}
|
||||
|
||||
|
||||
def iter_apt_uris(aug):
|
||||
"""Iterate over all the APT source URIs."""
|
||||
return itertools.chain.from_iterable(
|
||||
[aug.match(path) for path in APT_SOURCES_URI_PATHS])
|
||||
|
||||
|
||||
def get_augeas():
|
||||
"""Return an instance of Augeaus for processing APT configuration."""
|
||||
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
|
||||
augeas.Augeas.NO_MODL_AUTOLOAD)
|
||||
aug.set('/augeas/load/Aptsources/lens', 'Aptsources.lns')
|
||||
aug.set('/augeas/load/Aptsources/incl[last() + 1]',
|
||||
'/etc/apt/sources.list')
|
||||
aug.set('/augeas/load/Aptsources/incl[last() + 1]',
|
||||
'/etc/apt/sources.list.d/*.list')
|
||||
aug.set('/augeas/load/Aptsources822/lens', 'Aptsources822.lns')
|
||||
aug.set('/augeas/load/Aptsources822/incl[last() + 1]',
|
||||
'/etc/apt/sources.list.d/*.sources')
|
||||
aug.load()
|
||||
|
||||
# Check for any errors in parsing sources lists.
|
||||
if aug.match('/augeas/files/etc/apt/sources.list/error') or \
|
||||
aug.match('/augeas/files/etc/apt/sources.list.d//error'):
|
||||
raise Exception('Error parsing sources list')
|
||||
|
||||
return aug
|
||||
|
||||
|
||||
def is_apt_transport_tor_enabled():
|
||||
"""Return whether APT is set to download packages over Tor."""
|
||||
try:
|
||||
aug = get_augeas()
|
||||
except Exception:
|
||||
# If there was an error with parsing.
|
||||
return False
|
||||
|
||||
for uri_path in iter_apt_uris(aug):
|
||||
uri = aug.get(uri_path)
|
||||
if not uri.startswith(APT_TOR_PREFIX) and \
|
||||
(uri.startswith('http://') or uri.startswith('https://')):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@ -98,11 +98,6 @@ def __apply_changes(old_status, new_status):
|
||||
arguments['hidden_service'] = new_status['hs_enabled']
|
||||
needs_restart = True
|
||||
|
||||
if old_status['apt_transport_tor_enabled'] != \
|
||||
new_status['apt_transport_tor_enabled']:
|
||||
arguments['apt_transport_tor'] = (
|
||||
is_enabled and new_status['apt_transport_tor_enabled'])
|
||||
|
||||
if old_status['use_upstream_bridges'] != \
|
||||
new_status['use_upstream_bridges']:
|
||||
arguments['use_upstream_bridges'] = new_status['use_upstream_bridges']
|
||||
|
||||
130
plinth/modules/torproxy/__init__.py
Normal file
130
plinth/modules/torproxy/__init__.py
Normal file
@ -0,0 +1,130 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app to configure Tor Proxy."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules.apache.components import diagnose_url
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.modules.users.components import UsersAndGroups
|
||||
from plinth.package import Packages
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from . import manifest, privileged
|
||||
|
||||
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>.'),
|
||||
format_lazy(
|
||||
_('A Tor SOCKS port is available on your {box_name} for internal '
|
||||
'networks on TCP port 9050.'), box_name=_(cfg.box_name))
|
||||
]
|
||||
|
||||
|
||||
class TorProxyApp(app_module.App):
|
||||
"""FreedomBox app for Tor Proxy."""
|
||||
|
||||
app_id = 'torproxy'
|
||||
|
||||
_version = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(app_id=self.app_id, version=self._version,
|
||||
name=_('Tor Proxy'), icon_filename='tor',
|
||||
short_description=_('Anonymity Network'),
|
||||
description=_description,
|
||||
manual_page='TorProxy',
|
||||
clients=manifest.clients,
|
||||
donation_url='https://donate.torproject.org/')
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-torproxy', info.name,
|
||||
info.short_description, info.icon_filename,
|
||||
'torproxy:index', parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
packages = Packages('packages-torproxy', [
|
||||
'tor', 'tor-geoipdb', 'torsocks', 'obfs4proxy', 'apt-transport-tor'
|
||||
])
|
||||
self.add(packages)
|
||||
|
||||
firewall = Firewall('firewall-torproxy-socks', _('Tor Socks Proxy'),
|
||||
ports=['tor-socks'], is_external=False)
|
||||
self.add(firewall)
|
||||
|
||||
daemon = Daemon(
|
||||
'daemon-torproxy', 'tor@fbxproxy', strict_check=True,
|
||||
listen_ports=[(9050, 'tcp4'), (9050, 'tcp6'), (9040, 'tcp4'),
|
||||
(9040, 'tcp6'), (9053, 'udp4'), (9053, 'udp6')])
|
||||
self.add(daemon)
|
||||
|
||||
users_and_groups = UsersAndGroups('users-and-groups-torproxy',
|
||||
reserved_usernames=['debian-tor'])
|
||||
self.add(users_and_groups)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-torproxy',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
def disable(self):
|
||||
"""Disable APT use of Tor before disabling."""
|
||||
privileged.configure(apt_transport_tor=False)
|
||||
super().disable()
|
||||
|
||||
def diagnose(self):
|
||||
"""Run diagnostics and return the results."""
|
||||
results = super().diagnose()
|
||||
results.append(_diagnose_url_via_tor('http://www.debian.org', '4'))
|
||||
results.append(_diagnose_url_via_tor('http://www.debian.org', '6'))
|
||||
results.append(_diagnose_tor_use('https://check.torproject.org', '4'))
|
||||
results.append(_diagnose_tor_use('https://check.torproject.org', '6'))
|
||||
return results
|
||||
|
||||
def setup(self, old_version):
|
||||
"""Install and configure the app."""
|
||||
super().setup(old_version)
|
||||
privileged.setup(old_version)
|
||||
if not old_version:
|
||||
logger.info('Enabling apt-transport-tor')
|
||||
privileged.configure(apt_transport_tor=True)
|
||||
logger.info('Enabling Tor Proxy app')
|
||||
self.enable()
|
||||
|
||||
def uninstall(self):
|
||||
"""De-configure and uninstall the app."""
|
||||
super().uninstall()
|
||||
privileged.uninstall()
|
||||
|
||||
|
||||
def _diagnose_url_via_tor(url, kind=None):
|
||||
"""Diagnose whether a URL is reachable via Tor."""
|
||||
result = diagnose_url(url, kind=kind, wrapper='torsocks')
|
||||
result[0] = _('Access URL {url} on tcp{kind} via Tor') \
|
||||
.format(url=url, kind=kind)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _diagnose_tor_use(url, kind=None):
|
||||
"""Diagnose whether webpage at URL reports that we are using Tor."""
|
||||
expected_output = 'Congratulations. This browser is configured to use Tor.'
|
||||
result = diagnose_url(url, kind=kind, wrapper='torsocks',
|
||||
expected_output=expected_output)
|
||||
result[0] = _('Confirm Tor usage at {url} on tcp{kind}') \
|
||||
.format(url=url, kind=kind)
|
||||
|
||||
return result
|
||||
@ -0,0 +1 @@
|
||||
plinth.modules.torproxy
|
||||
19
plinth/modules/torproxy/forms.py
Normal file
19
plinth/modules/torproxy/forms.py
Normal file
@ -0,0 +1,19 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Forms for configuring Tor Proxy.
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth.modules.tor.forms import TorCommonForm
|
||||
|
||||
|
||||
class TorProxyForm(TorCommonForm):
|
||||
"""Tor Proxy configuration form."""
|
||||
apt_transport_tor_enabled = forms.BooleanField(
|
||||
label=_('Download software packages over Tor'), required=False,
|
||||
help_text=_('When enabled, software will be downloaded over the Tor '
|
||||
'network for installations and upgrades. This adds a '
|
||||
'degree of privacy and security during software '
|
||||
'downloads.'))
|
||||
52
plinth/modules/torproxy/manifest.py
Normal file
52
plinth/modules/torproxy/manifest.py
Normal file
@ -0,0 +1,52 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""App manifest for Tor Proxy."""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth.clients import store_url
|
||||
|
||||
_ORBOT_PACKAGE_ID = 'org.torproject.android'
|
||||
_TOR_BROWSER_DOWNLOAD_URL = \
|
||||
'https://www.torproject.org/download/download-easy.html'
|
||||
|
||||
clients = [{
|
||||
'name':
|
||||
_('Tor Browser'),
|
||||
'platforms': [{
|
||||
'type': 'download',
|
||||
'os': 'windows',
|
||||
'url': _TOR_BROWSER_DOWNLOAD_URL,
|
||||
}, {
|
||||
'type': 'download',
|
||||
'os': 'gnu-linux',
|
||||
'url': _TOR_BROWSER_DOWNLOAD_URL,
|
||||
}, {
|
||||
'type': 'download',
|
||||
'os': 'macos',
|
||||
'url': _TOR_BROWSER_DOWNLOAD_URL,
|
||||
}]
|
||||
}, {
|
||||
'name':
|
||||
_('Orbot: Proxy with Tor'),
|
||||
'platforms': [{
|
||||
'type': 'store',
|
||||
'os': 'android',
|
||||
'store_name': 'google-play',
|
||||
'url': store_url('google-play', _ORBOT_PACKAGE_ID)
|
||||
}, {
|
||||
'type': 'store',
|
||||
'os': 'android',
|
||||
'store_name': 'f-droid',
|
||||
'url': store_url('f-droid', _ORBOT_PACKAGE_ID)
|
||||
}]
|
||||
}]
|
||||
|
||||
backup = {
|
||||
'config': {
|
||||
'directories': ['/etc/tor/instances/fbxproxy/'],
|
||||
},
|
||||
'secrets': {
|
||||
'directories': ['/var/lib/tor-instances/fbxproxy/']
|
||||
},
|
||||
'services': ['tor@fbxproxy']
|
||||
}
|
||||
219
plinth/modules/torproxy/privileged.py
Normal file
219
plinth/modules/torproxy/privileged.py
Normal file
@ -0,0 +1,219 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Configure Tor Proxy service."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import augeas
|
||||
|
||||
from plinth import action_utils
|
||||
from plinth.actions import privileged
|
||||
from plinth.modules.torproxy.utils import (APT_TOR_PREFIX, get_augeas,
|
||||
iter_apt_uris)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
INSTANCE_NAME = 'fbxproxy'
|
||||
SERVICE_FILE = '/etc/firewalld/services/tor-{0}.xml'
|
||||
SERVICE_NAME = f'tor@{INSTANCE_NAME}'
|
||||
TORPROXY_CONFIG = f'/etc/tor/instances/{INSTANCE_NAME}/torrc'
|
||||
TORPROXY_CONFIG_AUG = f'/files/{TORPROXY_CONFIG}'
|
||||
|
||||
|
||||
@privileged
|
||||
def setup(old_version: int):
|
||||
"""Setup Tor configuration after installing it."""
|
||||
_first_time_setup()
|
||||
|
||||
|
||||
def _first_time_setup():
|
||||
"""Setup Tor configuration for the first time setting defaults."""
|
||||
logger.info('Performing first time setup for Tor Proxy')
|
||||
# Disable default tor service. We will use tor@fbxproxy instance
|
||||
# instead.
|
||||
_disable_apt_transport_tor()
|
||||
action_utils.service_disable('tor@default')
|
||||
|
||||
subprocess.run(['tor-instance-create', INSTANCE_NAME], check=True)
|
||||
|
||||
# Remove line starting with +SocksPort, since our augeas lens
|
||||
# doesn't handle it correctly.
|
||||
with open(TORPROXY_CONFIG, 'r', encoding='utf-8') as torrc:
|
||||
torrc_lines = torrc.readlines()
|
||||
with open(TORPROXY_CONFIG, 'w', encoding='utf-8') as torrc:
|
||||
for line in torrc_lines:
|
||||
if not line.startswith('+'):
|
||||
torrc.write(line)
|
||||
|
||||
aug = augeas_load()
|
||||
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/SocksPort[1]', '[::]:9050')
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/SocksPort[2]', '0.0.0.0:9050')
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/ExitPolicy[1]', 'reject *:*')
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/ExitPolicy[2]', 'reject6 *:*')
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/VirtualAddrNetworkIPv4', '10.192.0.0/10')
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/AutomapHostsOnResolve', '1')
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/TransPort[1]', '127.0.0.1:9040')
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/TransPort[2]', '[::1]:9040')
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/DNSPort[1]', '127.0.0.1:9053')
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/DNSPort[2]', '[::1]:9053')
|
||||
|
||||
aug.save()
|
||||
|
||||
action_utils.service_enable(SERVICE_NAME)
|
||||
action_utils.service_restart(SERVICE_NAME)
|
||||
|
||||
|
||||
@privileged
|
||||
def configure(use_upstream_bridges: Optional[bool] = None,
|
||||
upstream_bridges: Optional[str] = None,
|
||||
apt_transport_tor: Optional[bool] = None):
|
||||
"""Configure Tor."""
|
||||
aug = augeas_load()
|
||||
|
||||
_use_upstream_bridges(use_upstream_bridges, aug=aug)
|
||||
|
||||
if upstream_bridges:
|
||||
_set_upstream_bridges(upstream_bridges, aug=aug)
|
||||
|
||||
if apt_transport_tor:
|
||||
_enable_apt_transport_tor()
|
||||
elif apt_transport_tor is not None:
|
||||
_disable_apt_transport_tor()
|
||||
|
||||
|
||||
@privileged
|
||||
def restart():
|
||||
"""Restart Tor."""
|
||||
if (action_utils.service_is_enabled(SERVICE_NAME, strict_check=True)
|
||||
and action_utils.service_is_running(SERVICE_NAME)):
|
||||
action_utils.service_restart(SERVICE_NAME)
|
||||
|
||||
|
||||
@privileged
|
||||
def get_status() -> dict[str, Union[bool, str, dict[str, Any]]]:
|
||||
"""Return dict with Tor Proxy status."""
|
||||
aug = augeas_load()
|
||||
return {
|
||||
'use_upstream_bridges': _are_upstream_bridges_enabled(aug),
|
||||
'upstream_bridges': _get_upstream_bridges(aug)
|
||||
}
|
||||
|
||||
|
||||
def _are_upstream_bridges_enabled(aug) -> bool:
|
||||
"""Return whether upstream bridges are being used."""
|
||||
use_bridges = aug.get(TORPROXY_CONFIG_AUG + '/UseBridges')
|
||||
return use_bridges == '1'
|
||||
|
||||
|
||||
def _get_upstream_bridges(aug) -> str:
|
||||
"""Return upstream bridges separated by newlines."""
|
||||
matches = aug.match(TORPROXY_CONFIG_AUG + '/Bridge')
|
||||
bridges = [aug.get(match) for match in matches]
|
||||
return '\n'.join(bridges)
|
||||
|
||||
|
||||
def _enable():
|
||||
"""Enable and start the service."""
|
||||
action_utils.service_enable(SERVICE_NAME)
|
||||
|
||||
|
||||
def _disable():
|
||||
"""Disable and stop the service."""
|
||||
_disable_apt_transport_tor()
|
||||
action_utils.service_disable(SERVICE_NAME)
|
||||
|
||||
|
||||
def _use_upstream_bridges(use_upstream_bridges: Optional[bool] = None,
|
||||
aug=None):
|
||||
"""Enable use of upstream bridges."""
|
||||
if use_upstream_bridges is None:
|
||||
return
|
||||
|
||||
if not aug:
|
||||
aug = augeas_load()
|
||||
|
||||
if use_upstream_bridges:
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/UseBridges', '1')
|
||||
else:
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/UseBridges', '0')
|
||||
|
||||
aug.save()
|
||||
|
||||
|
||||
def _set_upstream_bridges(upstream_bridges=None, aug=None):
|
||||
"""Set list of upstream bridges."""
|
||||
if upstream_bridges is None:
|
||||
return
|
||||
|
||||
if not aug:
|
||||
aug = augeas_load()
|
||||
|
||||
aug.remove(TORPROXY_CONFIG_AUG + '/Bridge')
|
||||
if upstream_bridges:
|
||||
bridges = [bridge.strip() for bridge in upstream_bridges.split('\n')]
|
||||
bridges = [bridge for bridge in bridges if bridge]
|
||||
for bridge in bridges:
|
||||
parts = [part for part in bridge.split() if part]
|
||||
bridge = ' '.join(parts)
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/Bridge[last() + 1]',
|
||||
bridge.strip())
|
||||
|
||||
aug.set(TORPROXY_CONFIG_AUG + '/ClientTransportPlugin',
|
||||
'obfs3,scramblesuit,obfs4 exec /usr/bin/obfs4proxy')
|
||||
|
||||
aug.save()
|
||||
|
||||
|
||||
def _enable_apt_transport_tor():
|
||||
"""Enable package download over Tor."""
|
||||
aug = get_augeas()
|
||||
for uri_path in iter_apt_uris(aug):
|
||||
uri = aug.get(uri_path)
|
||||
if uri.startswith('http://') or uri.startswith('https://'):
|
||||
aug.set(uri_path, APT_TOR_PREFIX + uri)
|
||||
|
||||
aug.save()
|
||||
|
||||
|
||||
def _disable_apt_transport_tor():
|
||||
"""Disable package download over Tor."""
|
||||
try:
|
||||
aug = get_augeas()
|
||||
except Exception:
|
||||
# Disable what we can, so APT is not unusable.
|
||||
pass
|
||||
|
||||
for uri_path in iter_apt_uris(aug):
|
||||
uri = aug.get(uri_path)
|
||||
if uri.startswith(APT_TOR_PREFIX):
|
||||
aug.set(uri_path, uri[len(APT_TOR_PREFIX):])
|
||||
|
||||
aug.save()
|
||||
|
||||
|
||||
def augeas_load():
|
||||
"""Initialize Augeas."""
|
||||
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
|
||||
augeas.Augeas.NO_MODL_AUTOLOAD)
|
||||
aug.set('/augeas/load/Tor/lens', 'Tor.lns')
|
||||
aug.set('/augeas/load/Tor/incl[last() + 1]', TORPROXY_CONFIG)
|
||||
aug.load()
|
||||
return aug
|
||||
|
||||
|
||||
@privileged
|
||||
def uninstall():
|
||||
"""Remove fbxproxy instance."""
|
||||
directories = [
|
||||
f'/etc/tor/instances/{INSTANCE_NAME}/',
|
||||
f'/var/lib/tor-instances/{INSTANCE_NAME}/',
|
||||
f'/var/run/tor-instances/{INSTANCE_NAME}/'
|
||||
]
|
||||
for directory in directories:
|
||||
shutil.rmtree(directory, ignore_errors=True)
|
||||
|
||||
os.unlink(f'/var/run/tor-instances/{INSTANCE_NAME}.defaults')
|
||||
BIN
plinth/modules/torproxy/static/icons/tor.png
Normal file
BIN
plinth/modules/torproxy/static/icons/tor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
112
plinth/modules/torproxy/static/icons/tor.svg
Normal file
112
plinth/modules/torproxy/static/icons/tor.svg
Normal file
@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
width="512"
|
||||
height="512"
|
||||
id="图层_1"
|
||||
class="mozwebext"
|
||||
sodipodi:docname="tor.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<metadata
|
||||
id="metadata27696">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs27694" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1956"
|
||||
inkscape:window-height="1329"
|
||||
id="namedview27692"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.41594518"
|
||||
inkscape:cx="-376.85156"
|
||||
inkscape:cy="-302.70084"
|
||||
inkscape:window-x="1463"
|
||||
inkscape:window-y="369"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="图层_1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<g
|
||||
transform="matrix(2.7708177,0,0,2.7708177,-452.25352,-177.84153)"
|
||||
id="layer3"
|
||||
style="display:inline">
|
||||
<g
|
||||
id="layer5">
|
||||
<path
|
||||
d="m 264.513,77.977773 -4.917,19.529001 c 6.965,-13.793001 18.027,-24.172001 30.729,-33.323001 -9.287,10.789 -17.754,21.579001 -22.944,32.368001 8.741,-12.292001 20.486,-19.120001 33.733,-23.627001 -17.618,15.706001 -31.60228,32.559277 -42.25528,49.494277 l -8.467,-3.687 c 1.501,-13.521 6.60928,-27.369276 14.12128,-40.754277 z"
|
||||
id="path2554"
|
||||
style="fill:#abcd03"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 241.90113,115.14152 16.116,6.68594 c 0,4.098 -0.33313,16.59703 2.22938,20.28403 26.80289,34.5191 22.29349,103.71329 -5.42951,105.48829 -42.21656,0 -58.317,-28.679 -58.317,-55.03801 0,-24.037 28.816,-40.016 46.025,-54.219 4.37,-3.824 3.61113,-12.27525 -0.62387,-23.20125 z"
|
||||
id="path2534"
|
||||
style="fill:#fffcdb"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 258.02197,121.58695 5.80803,2.96282 c -0.546,3.823 0.273,12.292 4.096,14.476 16.936,10.516 32.914,21.988 39.197,33.46 22.398,40.42601 -15.706,77.84601 -48.62,74.29501 17.891,-13.248 23.081,-40.42501 16.389,-70.06201 -2.731,-11.609 -6.966,-22.125 -14.478,-34.007 -3.25421,-5.83246 -2.11803,-13.06582 -2.39203,-21.12482 z"
|
||||
id="path2536"
|
||||
style="fill:#7d4698"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
id="layer4"
|
||||
style="display:inline">
|
||||
<path
|
||||
d="m 255.226,120.58877 12.018,1.639 c -3.551,11.745 6.966,19.939 10.38,21.852 7.64801,4.234 15.02301,8.604 20.89601,13.93 11.063,10.106 17.345,24.31 17.345,39.333 0,14.886 -6.829,29.226 -18.301,38.786 -10.789,9.014 -25.67501,12.838 -40.15201,12.838 -9.014,0 -17.072,-0.409 -25.812,-3.278 -19.939,-6.692 -34.826,-23.763 -36.055,-44.25 -1.093,-15.979 2.458,-28.134 14.887,-40.835 6.418,-6.692 19.393,-14.34 28.271,-20.486 4.371,-3.005 9.014,-11.473 0.136,-27.451 l 1.776,-1.366 13.15659,8.81203 -11.10759,-4.57803 c 0.956,1.366 3.551,7.512 4.098,9.287 1.229,5.053 0.683,9.971 -0.41,12.155 -5.599,10.107 -15.159,12.838 -22.124,18.574 -12.292,10.106 -25.676,18.164 -24.174,45.888 0.683,13.657 11.336,30.319 27.314,38.104 9.014,4.371 19.394,6.146 29.91,6.692 9.423,0.41 27.45101,-5.19 37.28401,-13.384 10.516,-8.74 16.389,-21.988 16.389,-35.508 0,-13.658 -5.463,-26.632 -15.706,-35.783 -5.873,-5.326 -15.56901,-11.745 -21.57801,-15.16 -6.009,-3.414 -13.521,-12.974 -11.063,-22.124 z"
|
||||
id="path2538"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 251.539,140.80177 c -1.229,6.283 -2.595,17.618 -8.058,21.852 -2.322,1.638 -4.644,3.278 -7.102,4.916 -9.833,6.693 -19.667,12.974 -24.173,29.09 -0.956,3.415 -0.136,7.102 0.684,10.516 2.458,9.833 9.423,20.486 14.886,26.769 0,0.273 1.093,0.956 1.093,1.229 4.507,5.327 5.873,6.829 22.944,10.652 l -0.41,1.913 c -10.243,-2.731 -18.71,-5.189 -24.037,-11.336 0,-0.136 -0.956,-1.093 -0.956,-1.093 -5.736,-6.556 -12.702,-17.481 -15.296,-27.724 -0.956,-4.098 -1.775,-7.238 -0.683,-11.473 4.643,-16.661 14.75,-23.217 24.993,-30.182 2.322,-1.502 5.053,-2.869 7.238,-4.644 4.233,-3.14 6.554,-12.701 8.877,-20.485 z"
|
||||
id="path2540"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 255.90625,166.74951 c 0.137,7.102 -0.55625,10.66475 1.21875,15.71875 1.092,3.004 4.782,7.1015 5.875,11.0625 1.502,5.327 3.138,11.19901 3,14.75001 0,4.09799 -0.25625,11.74249 -2.03125,19.93749 -1.35362,6.77108 -4.47323,12.58153 -9.71875,15.875 -5.37327,-1.10644 -11.68224,-2.99521 -15.40625,-6.1875 -7.238,-6.282 -13.64875,-16.7865 -14.46875,-25.9375 -0.682,-7.51099 6.27275,-18.5885 15.96875,-24.1875 8.194,-4.78 10.1,-10.22775 11.875,-18.96875 -2.458,7.648 -4.7665,14.05925 -12.6875,18.15625 -11.472,6.009 -17.3585,16.09626 -16.8125,25.65625 0.819,12.291 5.7415,20.6195 15.4375,27.3125 4.097,2.868 11.75125,5.89875 16.53125,6.71875 v -0.625 c 3.62493,-0.67888 8.31818,-6.63267 10.65625,-14.6875 2.049,-7.238 2.85675,-16.502 2.71875,-22.37499 -0.137,-3.414 -1.643,-10.80801 -4.375,-17.50001 -1.502,-3.687 -3.8095,-7.37375 -5.3125,-9.96875 -1.637,-2.597 -1.64875,-8.195 -2.46875,-14.75 z"
|
||||
id="path2542"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 255.09375,193.53076 c 0.136,4.78 2.056,10.90451 2.875,17.18751 0.684,4.64399 0.387,9.30824 0.25,13.40624 -0.13495,4.74323 -1.7152,13.24218 -3.875,17.375 -2.03673,-0.93403 -2.83294,-1.99922 -4.15625,-3.71875 -1.638,-2.322 -2.75075,-4.644 -3.84375,-7.375 -0.819,-2.049 -1.7765,-4.394 -2.1875,-7.125 -0.546,-4.097 -0.393,-10.5065 4.25,-17.06249 3.551,-5.19001 4.36475,-5.58476 5.59375,-11.59376 -1.64,5.326 -2.8625,5.869 -6.6875,10.37501 -4.233,4.917 -4.9375,12.15924 -4.9375,18.03124 0,2.459 0.9805,5.18725 1.9375,7.78125 1.092,2.732 2.02925,5.452 3.53125,7.5 2.25796,3.32082 5.14798,5.20922 6.5625,5.5625 0.009,0.002 0.022,-0.002 0.0312,0 0.0303,0.007 0.0649,0.0255 0.0937,0.0312 v -0.15625 c 2.64982,-2.95437 4.24444,-5.88934 4.78125,-8.84375 0.683,-3.551 0.84,-7.10975 1.25,-11.34375 0.409,-3.551 0.11225,-8.334 -0.84375,-13.24999 -1.365,-6.146 -3.669,-12.41226 -4.625,-16.78126 z"
|
||||
id="path2544"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 255.499,135.06577 c 0.137,7.101 0.683,20.35 2.595,25.539 0.546,1.775 5.599,9.56 9.149,18.983 2.459,6.556 3.005,12.565 3.415,14.34 1.639,7.785 -0.41,20.896 -3.142,33.324 -1.365,6.692 -6.009,15.023 -11.335,18.301 l -1.092,1.912 c 3.005,-0.137 10.379,-7.375 12.974,-16.389 4.371,-15.296 6.146,-22.398 4.098,-39.333 -0.273,-1.64 -0.956,-7.238 -3.551,-13.248 -3.824,-9.151 -9.287,-17.891 -9.969,-19.667 -1.23,-2.867 -2.869,-15.295 -3.142,-23.762 z"
|
||||
id="path2550"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 258.06151,125.35303 c -0.40515,7.29812 -0.51351,9.98574 0.85149,15.31174 1.502,5.873 9.151,14.34 12.292,24.037 6.009,18.574 4.507,42.884 0.136,61.867 -1.638,6.691 -9.424,16.389 -17.208,19.529 l 5.736,1.366 c 3.141,-0.137 11.198,-7.648 14.34,-16.252 5.052,-13.521 6.009,-29.636 3.96,-46.571 -0.137,-1.639 -2.869,-16.252 -5.463,-22.398 -3.688,-9.15 -10.244,-17.345 -10.926,-19.119 -1.228,-3.005 -3.92651,-9.24362 -3.71849,-17.77074 z"
|
||||
id="path2552"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
width="0.550412"
|
||||
height="126.01891"
|
||||
x="253.71959"
|
||||
y="120.21686"
|
||||
id="rect2556" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
33
plinth/modules/torproxy/static/torproxy.js
Normal file
33
plinth/modules/torproxy/static/torproxy.js
Normal file
@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
/**
|
||||
* @licstart The following is the entire license notice for the JavaScript
|
||||
* code in this page.
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @licend The above is the entire license notice for the JavaScript code
|
||||
* in this page.
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
$('#id_torproxy-use_upstream_bridges').change(function() {
|
||||
if ($('#id_torproxy-use_upstream_bridges').prop('checked')) {
|
||||
$('#id_torproxy-upstream_bridges').parent().parent().show('slow');
|
||||
} else {
|
||||
$('#id_torproxy-upstream_bridges').parent().parent().hide('slow');
|
||||
}
|
||||
}).change();
|
||||
})(jQuery);
|
||||
12
plinth/modules/torproxy/templates/torproxy.html
Normal file
12
plinth/modules/torproxy/templates/torproxy.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "app.html" %}
|
||||
{% comment %}
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block page_js %}
|
||||
<script type="text/javascript" src="{% static 'torproxy/torproxy.js' %}"></script>
|
||||
{% endblock %}
|
||||
0
plinth/modules/torproxy/tests/__init__.py
Normal file
0
plinth/modules/torproxy/tests/__init__.py
Normal file
64
plinth/modules/torproxy/tests/test_functional.py
Normal file
64
plinth/modules/torproxy/tests/test_functional.py
Normal file
@ -0,0 +1,64 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Functional, browser based tests for Tor Proxy app.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from plinth.tests import functional
|
||||
|
||||
_TOR_FEATURE_TO_ELEMENT = {'software': 'torproxy-apt_transport_tor_enabled'}
|
||||
|
||||
pytestmark = [pytest.mark.apps, pytest.mark.torproxy]
|
||||
|
||||
|
||||
class TestTorProxyApp(functional.BaseAppTests):
|
||||
"""Tests for the Tor Proxy app."""
|
||||
app_name = 'torproxy'
|
||||
has_service = True
|
||||
has_web = False
|
||||
# TODO: Investigate why accessing IPv6 sites through Tor fails in
|
||||
# container.
|
||||
check_diagnostics = False
|
||||
|
||||
def test_set_download_software_packages_over_tor(self, session_browser):
|
||||
"""Test setting download software packages over Tor."""
|
||||
functional.app_enable(session_browser, 'torproxy')
|
||||
_feature_enable(session_browser, 'software', should_enable=True)
|
||||
_feature_enable(session_browser, 'software', should_enable=False)
|
||||
_assert_feature_enabled(session_browser, 'software', enabled=False)
|
||||
|
||||
@pytest.mark.backups
|
||||
def test_backup_restore(self, session_browser):
|
||||
"""Test backup and restore of configuration."""
|
||||
functional.app_enable(session_browser, 'torproxy')
|
||||
# TODO: Check that upstream bridges are restored.
|
||||
functional.backup_create(session_browser, 'torproxy', 'test_torproxy')
|
||||
|
||||
functional.backup_restore(session_browser, 'torproxy', 'test_torproxy')
|
||||
|
||||
assert functional.service_is_running(session_browser, 'torproxy')
|
||||
|
||||
|
||||
def _feature_enable(browser, feature, should_enable):
|
||||
"""Enable/disable a Tor Proxy feature."""
|
||||
element_name = _TOR_FEATURE_TO_ELEMENT[feature]
|
||||
functional.nav_to_module(browser, 'torproxy')
|
||||
checkbox_element = browser.find_by_name(element_name).first
|
||||
if should_enable == checkbox_element.checked:
|
||||
return
|
||||
|
||||
if should_enable:
|
||||
checkbox_element.check()
|
||||
else:
|
||||
checkbox_element.uncheck()
|
||||
|
||||
functional.submit(browser, form_class='form-configuration')
|
||||
functional.wait_for_config_update(browser, 'torproxy')
|
||||
|
||||
|
||||
def _assert_feature_enabled(browser, feature, enabled):
|
||||
"""Assert whether Tor Proxy feature is enabled or disabled."""
|
||||
element_name = _TOR_FEATURE_TO_ELEMENT[feature]
|
||||
functional.nav_to_module(browser, 'torproxy')
|
||||
assert browser.find_by_name(element_name).first.checked == enabled
|
||||
33
plinth/modules/torproxy/tests/test_torproxy.py
Normal file
33
plinth/modules/torproxy/tests/test_torproxy.py
Normal file
@ -0,0 +1,33 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Tests for Tor Proxy module.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from plinth.modules.torproxy import utils
|
||||
|
||||
|
||||
class TestTorProxy:
|
||||
"""Test cases for testing the Tor Proxy module."""
|
||||
|
||||
@staticmethod
|
||||
@pytest.mark.usefixtures('needs_root')
|
||||
def test_is_apt_transport_tor_enabled():
|
||||
"""Test that is_apt_transport_tor_enabled does not raise any unhandled
|
||||
exceptions.
|
||||
"""
|
||||
utils.is_apt_transport_tor_enabled()
|
||||
|
||||
@staticmethod
|
||||
@patch('plinth.app.App.get')
|
||||
@pytest.mark.usefixtures('needs_root', 'load_cfg')
|
||||
def test_get_status(_app_get):
|
||||
"""Test that get_status does not raise any unhandled exceptions.
|
||||
|
||||
This should work regardless of whether tor is installed, or
|
||||
/etc/tor/instances/fbxproxy/torrc exists.
|
||||
"""
|
||||
utils.get_status()
|
||||
13
plinth/modules/torproxy/urls.py
Normal file
13
plinth/modules/torproxy/urls.py
Normal file
@ -0,0 +1,13 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
URLs for the Tor Proxy module.
|
||||
"""
|
||||
|
||||
from django.urls import re_path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^apps/torproxy/$', views.TorProxyAppView.as_view(),
|
||||
name='index'),
|
||||
]
|
||||
75
plinth/modules/torproxy/utils.py
Normal file
75
plinth/modules/torproxy/utils.py
Normal file
@ -0,0 +1,75 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Tor Proxy utility functions."""
|
||||
|
||||
import itertools
|
||||
|
||||
import augeas
|
||||
|
||||
from plinth import app as app_module
|
||||
from plinth.daemon import app_is_running
|
||||
|
||||
from . import privileged
|
||||
|
||||
APT_SOURCES_URI_PATHS = ('/files/etc/apt/sources.list/*/uri',
|
||||
'/files/etc/apt/sources.list.d/*/*/uri',
|
||||
'/files/etc/apt/sources.list.d/*/*/URIs/*')
|
||||
APT_TOR_PREFIX = 'tor+'
|
||||
|
||||
|
||||
def get_status(initialized=True):
|
||||
"""Return current Tor status."""
|
||||
status = privileged.get_status()
|
||||
|
||||
app = app_module.App.get('torproxy')
|
||||
return {
|
||||
'enabled': app.is_enabled() if initialized else False,
|
||||
'is_running': app_is_running(app) if initialized else False,
|
||||
'use_upstream_bridges': status['use_upstream_bridges'],
|
||||
'upstream_bridges': status['upstream_bridges'],
|
||||
'apt_transport_tor_enabled': is_apt_transport_tor_enabled()
|
||||
}
|
||||
|
||||
|
||||
def iter_apt_uris(aug):
|
||||
"""Iterate over all the APT source URIs."""
|
||||
return itertools.chain.from_iterable(
|
||||
[aug.match(path) for path in APT_SOURCES_URI_PATHS])
|
||||
|
||||
|
||||
def get_augeas():
|
||||
"""Return an instance of Augeaus for processing APT configuration."""
|
||||
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
|
||||
augeas.Augeas.NO_MODL_AUTOLOAD)
|
||||
aug.set('/augeas/load/Aptsources/lens', 'Aptsources.lns')
|
||||
aug.set('/augeas/load/Aptsources/incl[last() + 1]',
|
||||
'/etc/apt/sources.list')
|
||||
aug.set('/augeas/load/Aptsources/incl[last() + 1]',
|
||||
'/etc/apt/sources.list.d/*.list')
|
||||
aug.set('/augeas/load/Aptsources822/lens', 'Aptsources822.lns')
|
||||
aug.set('/augeas/load/Aptsources822/incl[last() + 1]',
|
||||
'/etc/apt/sources.list.d/*.sources')
|
||||
aug.load()
|
||||
|
||||
# Check for any errors in parsing sources lists.
|
||||
if aug.match('/augeas/files/etc/apt/sources.list/error') or \
|
||||
aug.match('/augeas/files/etc/apt/sources.list.d//error'):
|
||||
raise Exception('Error parsing sources list')
|
||||
|
||||
return aug
|
||||
|
||||
|
||||
def is_apt_transport_tor_enabled():
|
||||
"""Return whether APT is set to download packages over Tor."""
|
||||
try:
|
||||
aug = get_augeas()
|
||||
except Exception:
|
||||
# If there was an error with parsing.
|
||||
return False
|
||||
|
||||
for uri_path in iter_apt_uris(aug):
|
||||
uri = aug.get(uri_path)
|
||||
if not uri.startswith(APT_TOR_PREFIX) and \
|
||||
(uri.startswith('http://') or uri.startswith('https://')):
|
||||
return False
|
||||
|
||||
return True
|
||||
104
plinth/modules/torproxy/views.py
Normal file
104
plinth/modules/torproxy/views.py
Normal file
@ -0,0 +1,104 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app for configuring Tor Proxy."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils.translation import gettext_noop
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from plinth import app as app_module
|
||||
from plinth import operation as operation_module
|
||||
from plinth.views import AppView
|
||||
|
||||
from . import privileged
|
||||
from . import utils as tor_utils
|
||||
from .forms import TorProxyForm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TorProxyAppView(AppView):
|
||||
"""Show Tor Proxy app main page."""
|
||||
|
||||
app_id = 'torproxy'
|
||||
template_name = 'torproxy.html'
|
||||
form_class = TorProxyForm
|
||||
prefix = 'torproxy'
|
||||
|
||||
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('torproxy: 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:
|
||||
message = gettext_noop('Configuration updated.')
|
||||
|
||||
logger.info('torproxy: 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 = False
|
||||
arguments = {}
|
||||
|
||||
app = app_module.App.get('torproxy')
|
||||
is_enabled = app.is_enabled()
|
||||
|
||||
if old_status['apt_transport_tor_enabled'] != \
|
||||
new_status['apt_transport_tor_enabled']:
|
||||
arguments['apt_transport_tor'] = (
|
||||
is_enabled and new_status['apt_transport_tor_enabled'])
|
||||
|
||||
if old_status['use_upstream_bridges'] != \
|
||||
new_status['use_upstream_bridges']:
|
||||
arguments['use_upstream_bridges'] = new_status['use_upstream_bridges']
|
||||
needs_restart = True
|
||||
|
||||
if old_status['upstream_bridges'] != new_status['upstream_bridges']:
|
||||
arguments['upstream_bridges'] = new_status['upstream_bridges']
|
||||
needs_restart = True
|
||||
|
||||
if arguments:
|
||||
privileged.configure(**arguments)
|
||||
|
||||
if needs_restart and is_enabled:
|
||||
privileged.restart()
|
||||
Loading…
x
Reference in New Issue
Block a user