mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-27 10:44:33 +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
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""FreedomBox app to configure Tor."""
|
"""FreedomBox app to configure Tor."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from plinth import action_utils
|
from plinth import action_utils
|
||||||
from plinth import app as app_module
|
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,
|
from plinth.daemon import (Daemon, app_is_running, diagnose_netcat,
|
||||||
diagnose_port_listening)
|
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.backups.components import BackupRestore
|
||||||
from plinth.modules.firewall.components import Firewall
|
from plinth.modules.firewall.components import Firewall
|
||||||
from plinth.modules.names.components import DomainType
|
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.modules.users.components import UsersAndGroups
|
||||||
from plinth.package import Packages
|
from plinth.package import Packages
|
||||||
from plinth.signals import domain_added, domain_removed
|
from plinth.signals import domain_added, domain_removed
|
||||||
from plinth.utils import format_lazy
|
|
||||||
|
|
||||||
from . import manifest, privileged, utils
|
from . import manifest, privileged, utils
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_description = [
|
_description = [
|
||||||
_('Tor is an anonymous communication system. You can learn more '
|
_('Tor is an anonymous communication system. You can learn more '
|
||||||
'about it from the <a href="https://www.torproject.org/">Tor '
|
'about it from the <a href="https://www.torproject.org/">Tor '
|
||||||
@ -26,9 +31,6 @@ _description = [
|
|||||||
'Tor Project recommends that you use the '
|
'Tor Project recommends that you use the '
|
||||||
'<a href="https://www.torproject.org/download/download-easy.html.en">'
|
'<a href="https://www.torproject.org/download/download-easy.html.en">'
|
||||||
'Tor Browser</a>.'),
|
'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'
|
app_id = 'tor'
|
||||||
|
|
||||||
_version = 6
|
_version = 7
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Create components for the app."""
|
"""Create components for the app."""
|
||||||
@ -57,28 +59,20 @@ class TorApp(app_module.App):
|
|||||||
parent_url_name='apps')
|
parent_url_name='apps')
|
||||||
self.add(menu_item)
|
self.add(menu_item)
|
||||||
|
|
||||||
packages = Packages('packages-tor', [
|
packages = Packages('packages-tor',
|
||||||
'tor', 'tor-geoipdb', 'torsocks', 'obfs4proxy', 'apt-transport-tor'
|
['tor', 'tor-geoipdb', 'obfs4proxy'])
|
||||||
])
|
|
||||||
self.add(packages)
|
self.add(packages)
|
||||||
|
|
||||||
domain_type = DomainType('domain-type-tor', _('Tor Onion Service'),
|
domain_type = DomainType('domain-type-tor', _('Tor Onion Service'),
|
||||||
'tor:index', can_have_certificate=False)
|
'tor:index', can_have_certificate=False)
|
||||||
self.add(domain_type)
|
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'),
|
firewall = Firewall('firewall-tor-relay', _('Tor Bridge Relay'),
|
||||||
ports=['tor-orport', 'tor-obfs3',
|
ports=['tor-orport', 'tor-obfs3',
|
||||||
'tor-obfs4'], is_external=True)
|
'tor-obfs4'], is_external=True)
|
||||||
self.add(firewall)
|
self.add(firewall)
|
||||||
|
|
||||||
daemon = Daemon(
|
daemon = Daemon('daemon-tor', 'tor@plinth', strict_check=True)
|
||||||
'daemon-tor', 'tor@plinth', strict_check=True,
|
|
||||||
listen_ports=[(9050, 'tcp4'), (9050, 'tcp6'), (9040, 'tcp4'),
|
|
||||||
(9040, 'tcp6'), (9053, 'udp4'), (9053, 'udp6')])
|
|
||||||
self.add(daemon)
|
self.add(daemon)
|
||||||
|
|
||||||
webserver = Webserver('webserver-onion-location',
|
webserver = Webserver('webserver-onion-location',
|
||||||
@ -113,8 +107,7 @@ class TorApp(app_module.App):
|
|||||||
update_hidden_service_domain()
|
update_hidden_service_domain()
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
"""Disable APT use of Tor before disabling."""
|
"""Disable the app and remove HS domain."""
|
||||||
privileged.configure(apt_transport_tor=False)
|
|
||||||
super().disable()
|
super().disable()
|
||||||
update_hidden_service_domain()
|
update_hidden_service_domain()
|
||||||
|
|
||||||
@ -166,21 +159,12 @@ class TorApp(app_module.App):
|
|||||||
'passed' if len(hs_hostname) == 56 else 'failed'
|
'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
|
return results
|
||||||
|
|
||||||
def setup(self, old_version):
|
def setup(self, old_version):
|
||||||
"""Install and configure the app."""
|
"""Install and configure the app."""
|
||||||
super().setup(old_version)
|
super().setup(old_version)
|
||||||
privileged.setup(old_version)
|
privileged.setup(old_version)
|
||||||
if not old_version:
|
|
||||||
privileged.configure(apt_transport_tor=True)
|
|
||||||
|
|
||||||
update_hidden_service_domain(utils.get_status())
|
update_hidden_service_domain(utils.get_status())
|
||||||
|
|
||||||
# Enable/disable Onion-Location component based on app 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')
|
daemon_component = self.get_component('daemon-tor')
|
||||||
component = self.get_component('webserver-onion-location')
|
component = self.get_component('webserver-onion-location')
|
||||||
if daemon_component.is_enabled():
|
if daemon_component.is_enabled():
|
||||||
|
logger.info('Enabling Onion-Location component')
|
||||||
component.enable()
|
component.enable()
|
||||||
else:
|
else:
|
||||||
|
logger.info('Disabling Onion-Location component')
|
||||||
component.disable()
|
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:
|
if not old_version:
|
||||||
|
logger.info('Enabling Tor app')
|
||||||
self.enable()
|
self.enable()
|
||||||
|
|
||||||
def uninstall(self):
|
def uninstall(self):
|
||||||
@ -234,23 +233,3 @@ def _diagnose_control_port():
|
|||||||
negate=negate))
|
negate=negate))
|
||||||
|
|
||||||
return results
|
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
|
raise validation_error
|
||||||
|
|
||||||
|
|
||||||
class TorForm(forms.Form): # pylint: disable=W0232
|
class TorCommonForm(forms.Form):
|
||||||
"""Tor configuration form."""
|
"""Tor common configuration form."""
|
||||||
use_upstream_bridges = forms.BooleanField(
|
use_upstream_bridges = forms.BooleanField(
|
||||||
label=_('Use upstream bridges to connect to Tor network'),
|
label=_('Use upstream bridges to connect to Tor network'),
|
||||||
required=False, help_text=_(
|
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 '
|
'https://bridges.torproject.org/</a> and copy/paste the bridge '
|
||||||
'information here. Currently supported transports are none, '
|
'information here. Currently supported transports are none, '
|
||||||
'obfs3, obfs4 and scamblesuit.'), validators=[bridges_validator])
|
'obfs3, obfs4 and scamblesuit.'), validators=[bridges_validator])
|
||||||
|
|
||||||
|
|
||||||
|
class TorForm(TorCommonForm):
|
||||||
|
"""Tor configuration form."""
|
||||||
relay_enabled = forms.BooleanField(
|
relay_enabled = forms.BooleanField(
|
||||||
label=_('Enable Tor relay'), required=False, help_text=format_lazy(
|
label=_('Enable Tor relay'), required=False, help_text=format_lazy(
|
||||||
_('When enabled, your {box_name} will run a Tor relay and donate '
|
_('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 '
|
'services (such as wiki or chat) without revealing its '
|
||||||
'location. Do not use this for strong anonymity yet.'),
|
'location. Do not use this for strong anonymity yet.'),
|
||||||
box_name=_(cfg.box_name)))
|
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):
|
def clean(self):
|
||||||
"""Validate the form for cross-field integrity."""
|
"""Validate the form for cross-field integrity."""
|
||||||
|
|||||||
@ -44,11 +44,11 @@ clients = [{
|
|||||||
|
|
||||||
backup = {
|
backup = {
|
||||||
'config': {
|
'config': {
|
||||||
'directories': ['/etc/tor/'],
|
'directories': ['/etc/tor/instances/plinth/'],
|
||||||
'files': [str(privileged.TOR_APACHE_SITE)]
|
'files': [str(privileged.TOR_APACHE_SITE)]
|
||||||
},
|
},
|
||||||
'secrets': {
|
'secrets': {
|
||||||
'directories': ['/var/lib/tor/', '/var/lib/tor-instances/']
|
'directories': ['/var/lib/tor-instances/plinth/']
|
||||||
},
|
},
|
||||||
'services': ['tor@plinth']
|
'services': ['tor@plinth']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
"""Configure Tor service."""
|
"""Configure Tor service."""
|
||||||
|
|
||||||
import codecs
|
import codecs
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
@ -15,20 +16,29 @@ import augeas
|
|||||||
|
|
||||||
from plinth import action_utils
|
from plinth import action_utils
|
||||||
from plinth.actions import privileged
|
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'
|
SERVICE_FILE = '/etc/firewalld/services/tor-{0}.xml'
|
||||||
TOR_CONFIG = '/files/etc/tor/instances/plinth/torrc'
|
SERVICE_NAME = f'tor@{INSTANCE_NAME}'
|
||||||
TOR_STATE_FILE = '/var/lib/tor-instances/plinth/state'
|
TOR_CONFIG = f'/etc/tor/instances/{INSTANCE_NAME}/torrc'
|
||||||
TOR_AUTH_COOKIE = '/var/run/tor-instances/plinth/control.authcookie'
|
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'
|
TOR_APACHE_SITE = '/etc/apache2/conf-available/onion-location-freedombox.conf'
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@privileged
|
@privileged
|
||||||
def setup(old_version: int):
|
def setup(old_version: int):
|
||||||
"""Setup Tor configuration after installing it."""
|
"""Setup Tor configuration after installing it."""
|
||||||
if old_version and old_version <= 4:
|
if old_version:
|
||||||
_upgrade_orport_value()
|
if old_version <= 4:
|
||||||
|
_upgrade_orport_value()
|
||||||
|
|
||||||
|
if old_version <= 6:
|
||||||
|
_remove_proxy()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
_first_time_setup()
|
_first_time_setup()
|
||||||
@ -37,50 +47,39 @@ def setup(old_version: int):
|
|||||||
|
|
||||||
def _first_time_setup():
|
def _first_time_setup():
|
||||||
"""Setup Tor configuration for the first time setting defaults."""
|
"""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
|
# Disable default tor service. We will use tor@plinth instance
|
||||||
# instead.
|
# instead.
|
||||||
_disable_apt_transport_tor()
|
action_utils.service_disable('tor@default')
|
||||||
action_utils.service_disable('tor')
|
|
||||||
|
|
||||||
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
|
# Remove line starting with +SocksPort, since our augeas lens
|
||||||
# doesn't handle it correctly.
|
# doesn't handle it correctly.
|
||||||
with open('/etc/tor/instances/plinth/torrc', 'r',
|
with open(TOR_CONFIG, 'r', encoding='utf-8') as torrc:
|
||||||
encoding='utf-8') as torrc:
|
|
||||||
torrc_lines = torrc.readlines()
|
torrc_lines = torrc.readlines()
|
||||||
with open('/etc/tor/instances/plinth/torrc', 'w',
|
with open(TOR_CONFIG, 'w', encoding='utf-8') as torrc:
|
||||||
encoding='utf-8') as torrc:
|
|
||||||
for line in torrc_lines:
|
for line in torrc_lines:
|
||||||
if not line.startswith('+'):
|
if not line.startswith('+'):
|
||||||
torrc.write(line)
|
torrc.write(line)
|
||||||
|
|
||||||
aug = augeas_load()
|
aug = augeas_load()
|
||||||
|
|
||||||
aug.set(TOR_CONFIG + '/SocksPort[1]', '[::]:9050')
|
aug.set(TOR_CONFIG_AUG + '/ControlPort', '9051')
|
||||||
aug.set(TOR_CONFIG + '/SocksPort[2]', '0.0.0.0:9050')
|
|
||||||
aug.set(TOR_CONFIG + '/ControlPort', '9051')
|
|
||||||
_enable_relay(relay=True, bridge=True, aug=aug)
|
_enable_relay(relay=True, bridge=True, aug=aug)
|
||||||
aug.set(TOR_CONFIG + '/ExitPolicy[1]', 'reject *:*')
|
aug.set(TOR_CONFIG_AUG + '/ExitPolicy[1]', 'reject *:*')
|
||||||
aug.set(TOR_CONFIG + '/ExitPolicy[2]', 'reject6 *:*')
|
aug.set(TOR_CONFIG_AUG + '/ExitPolicy[2]', 'reject6 *:*')
|
||||||
|
|
||||||
aug.set(TOR_CONFIG + '/VirtualAddrNetworkIPv4', '10.192.0.0/10')
|
aug.set(TOR_CONFIG_AUG + '/HiddenServiceDir',
|
||||||
aug.set(TOR_CONFIG + '/AutomapHostsOnResolve', '1')
|
f'/var/lib/tor-instances/{INSTANCE_NAME}/hidden_service')
|
||||||
aug.set(TOR_CONFIG + '/TransPort[1]', '127.0.0.1:9040')
|
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[1]', '22 127.0.0.1:22')
|
||||||
aug.set(TOR_CONFIG + '/TransPort[2]', '[::1]:9040')
|
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[2]', '80 127.0.0.1:80')
|
||||||
aug.set(TOR_CONFIG + '/DNSPort[1]', '127.0.0.1:9053')
|
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[3]', '443 127.0.0.1:443')
|
||||||
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.save()
|
aug.save()
|
||||||
|
|
||||||
action_utils.service_enable('tor@plinth')
|
action_utils.service_enable(SERVICE_NAME)
|
||||||
action_utils.service_restart('tor@plinth')
|
action_utils.service_restart(SERVICE_NAME)
|
||||||
_update_ports()
|
_update_ports()
|
||||||
|
|
||||||
# wait until hidden service information is available
|
# 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.
|
443 is not possible in FreedomBox due it is use for other purposes.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
logger.info('Upgrading ORPort value for Tor')
|
||||||
aug = augeas_load()
|
aug = augeas_load()
|
||||||
|
|
||||||
if _is_relay_enabled(aug):
|
if _is_relay_enabled(aug):
|
||||||
aug.set(TOR_CONFIG + '/ORPort[1]', '9001')
|
aug.set(TOR_CONFIG_AUG + '/ORPort[1]', '9001')
|
||||||
aug.set(TOR_CONFIG + '/ORPort[2]', '[::]:9001')
|
aug.set(TOR_CONFIG_AUG + '/ORPort[2]', '[::]:9001')
|
||||||
|
|
||||||
aug.save()
|
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
|
# Tor may not be running, don't try to read/update all ports
|
||||||
_update_port('orport', 9001)
|
_update_port('orport', 9001)
|
||||||
action_utils.service_restart('firewalld')
|
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
|
@privileged
|
||||||
def configure(use_upstream_bridges: Optional[bool] = None,
|
def configure(use_upstream_bridges: Optional[bool] = None,
|
||||||
upstream_bridges: Optional[str] = None,
|
upstream_bridges: Optional[str] = None,
|
||||||
relay: Optional[bool] = None,
|
relay: Optional[bool] = None,
|
||||||
bridge_relay: Optional[bool] = None,
|
bridge_relay: Optional[bool] = None,
|
||||||
hidden_service: Optional[bool] = None,
|
hidden_service: Optional[bool] = None):
|
||||||
apt_transport_tor: Optional[bool] = None):
|
|
||||||
"""Configure Tor."""
|
"""Configure Tor."""
|
||||||
aug = augeas_load()
|
aug = augeas_load()
|
||||||
|
|
||||||
@ -151,11 +167,6 @@ def configure(use_upstream_bridges: Optional[bool] = None,
|
|||||||
elif hidden_service is not None:
|
elif hidden_service is not None:
|
||||||
_disable_hs(aug=aug)
|
_disable_hs(aug=aug)
|
||||||
|
|
||||||
if apt_transport_tor:
|
|
||||||
_enable_apt_transport_tor()
|
|
||||||
elif apt_transport_tor is not None:
|
|
||||||
_disable_apt_transport_tor()
|
|
||||||
|
|
||||||
|
|
||||||
@privileged
|
@privileged
|
||||||
def update_ports():
|
def update_ports():
|
||||||
@ -166,12 +177,12 @@ def update_ports():
|
|||||||
@privileged
|
@privileged
|
||||||
def restart():
|
def restart():
|
||||||
"""Restart Tor."""
|
"""Restart Tor."""
|
||||||
if (action_utils.service_is_enabled('tor@plinth', strict_check=True)
|
if (action_utils.service_is_enabled(SERVICE_NAME, strict_check=True)
|
||||||
and action_utils.service_is_running('tor@plinth')):
|
and action_utils.service_is_running(SERVICE_NAME)):
|
||||||
action_utils.service_restart('tor@plinth')
|
action_utils.service_restart(SERVICE_NAME)
|
||||||
|
|
||||||
aug = augeas_load()
|
aug = augeas_load()
|
||||||
if aug.get(TOR_CONFIG + '/HiddenServiceDir'):
|
if aug.get(TOR_CONFIG_AUG + '/HiddenServiceDir'):
|
||||||
# wait until hidden service information is available
|
# wait until hidden service information is available
|
||||||
tries = 0
|
tries = 0
|
||||||
while not _get_hidden_service()['enabled']:
|
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:
|
def _are_upstream_bridges_enabled(aug) -> bool:
|
||||||
"""Return whether upstream bridges are being used."""
|
"""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'
|
return use_bridges == '1'
|
||||||
|
|
||||||
|
|
||||||
def _get_upstream_bridges(aug) -> str:
|
def _get_upstream_bridges(aug) -> str:
|
||||||
"""Return upstream bridges separated by newlines."""
|
"""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]
|
bridges = [aug.get(match) for match in matches]
|
||||||
return '\n'.join(bridges)
|
return '\n'.join(bridges)
|
||||||
|
|
||||||
|
|
||||||
def _is_relay_enabled(aug) -> bool:
|
def _is_relay_enabled(aug) -> bool:
|
||||||
"""Return whether a relay is enabled."""
|
"""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'
|
return bool(orport) and orport != '0'
|
||||||
|
|
||||||
|
|
||||||
def _is_bridge_relay_enabled(aug) -> bool:
|
def _is_bridge_relay_enabled(aug) -> bool:
|
||||||
"""Return whether bridge relay is enabled."""
|
"""Return whether bridge relay is enabled."""
|
||||||
bridge = aug.get(TOR_CONFIG + '/BridgeRelay')
|
bridge = aug.get(TOR_CONFIG_AUG + '/BridgeRelay')
|
||||||
return bridge == '1'
|
return bridge == '1'
|
||||||
|
|
||||||
|
|
||||||
@ -272,8 +283,8 @@ def _get_hidden_service(aug=None) -> dict[str, Any]:
|
|||||||
if not aug:
|
if not aug:
|
||||||
aug = augeas_load()
|
aug = augeas_load()
|
||||||
|
|
||||||
hs_dir = aug.get(TOR_CONFIG + '/HiddenServiceDir')
|
hs_dir = aug.get(TOR_CONFIG_AUG + '/HiddenServiceDir')
|
||||||
hs_port_paths = aug.match(TOR_CONFIG + '/HiddenServicePort')
|
hs_port_paths = aug.match(TOR_CONFIG_AUG + '/HiddenServicePort')
|
||||||
|
|
||||||
for hs_port_path in hs_port_paths:
|
for hs_port_path in hs_port_paths:
|
||||||
port_info = aug.get(hs_port_path).split()
|
port_info = aug.get(hs_port_path).split()
|
||||||
@ -300,14 +311,13 @@ def _get_hidden_service(aug=None) -> dict[str, Any]:
|
|||||||
|
|
||||||
def _enable():
|
def _enable():
|
||||||
"""Enable and start the service."""
|
"""Enable and start the service."""
|
||||||
action_utils.service_enable('tor@plinth')
|
action_utils.service_enable(SERVICE_NAME)
|
||||||
_update_ports()
|
_update_ports()
|
||||||
|
|
||||||
|
|
||||||
def _disable():
|
def _disable():
|
||||||
"""Disable and stop the service."""
|
"""Disable and stop the service."""
|
||||||
_disable_apt_transport_tor()
|
action_utils.service_disable(SERVICE_NAME)
|
||||||
action_utils.service_disable('tor@plinth')
|
|
||||||
|
|
||||||
|
|
||||||
def _use_upstream_bridges(use_upstream_bridges: Optional[bool] = None,
|
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()
|
aug = augeas_load()
|
||||||
|
|
||||||
if use_upstream_bridges:
|
if use_upstream_bridges:
|
||||||
aug.set(TOR_CONFIG + '/UseBridges', '1')
|
aug.set(TOR_CONFIG_AUG + '/UseBridges', '1')
|
||||||
else:
|
else:
|
||||||
aug.set(TOR_CONFIG + '/UseBridges', '0')
|
aug.set(TOR_CONFIG_AUG + '/UseBridges', '0')
|
||||||
|
|
||||||
aug.save()
|
aug.save()
|
||||||
|
|
||||||
@ -335,16 +345,16 @@ def _set_upstream_bridges(upstream_bridges=None, aug=None):
|
|||||||
if not aug:
|
if not aug:
|
||||||
aug = augeas_load()
|
aug = augeas_load()
|
||||||
|
|
||||||
aug.remove(TOR_CONFIG + '/Bridge')
|
aug.remove(TOR_CONFIG_AUG + '/Bridge')
|
||||||
if upstream_bridges:
|
if upstream_bridges:
|
||||||
bridges = [bridge.strip() for bridge in upstream_bridges.split('\n')]
|
bridges = [bridge.strip() for bridge in upstream_bridges.split('\n')]
|
||||||
bridges = [bridge for bridge in bridges if bridge]
|
bridges = [bridge for bridge in bridges if bridge]
|
||||||
for bridge in bridges:
|
for bridge in bridges:
|
||||||
parts = [part for part in bridge.split() if part]
|
parts = [part for part in bridge.split() if part]
|
||||||
bridge = ' '.join(parts)
|
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')
|
'obfs3,scramblesuit,obfs4 exec /usr/bin/obfs4proxy')
|
||||||
|
|
||||||
aug.save()
|
aug.save()
|
||||||
@ -362,20 +372,20 @@ def _enable_relay(relay: Optional[bool], bridge: Optional[bool],
|
|||||||
use_upstream_bridges = _are_upstream_bridges_enabled(aug)
|
use_upstream_bridges = _are_upstream_bridges_enabled(aug)
|
||||||
|
|
||||||
if relay and not use_upstream_bridges:
|
if relay and not use_upstream_bridges:
|
||||||
aug.set(TOR_CONFIG + '/ORPort[1]', '9001')
|
aug.set(TOR_CONFIG_AUG + '/ORPort[1]', '9001')
|
||||||
aug.set(TOR_CONFIG + '/ORPort[2]', '[::]:9001')
|
aug.set(TOR_CONFIG_AUG + '/ORPort[2]', '[::]:9001')
|
||||||
elif relay is not None:
|
elif relay is not None:
|
||||||
aug.remove(TOR_CONFIG + '/ORPort')
|
aug.remove(TOR_CONFIG_AUG + '/ORPort')
|
||||||
|
|
||||||
if bridge and not use_upstream_bridges:
|
if bridge and not use_upstream_bridges:
|
||||||
aug.set(TOR_CONFIG + '/BridgeRelay', '1')
|
aug.set(TOR_CONFIG_AUG + '/BridgeRelay', '1')
|
||||||
aug.set(TOR_CONFIG + '/ServerTransportPlugin',
|
aug.set(TOR_CONFIG_AUG + '/ServerTransportPlugin',
|
||||||
'obfs3,obfs4 exec /usr/bin/obfs4proxy')
|
'obfs3,obfs4 exec /usr/bin/obfs4proxy')
|
||||||
aug.set(TOR_CONFIG + '/ExtORPort', 'auto')
|
aug.set(TOR_CONFIG_AUG + '/ExtORPort', 'auto')
|
||||||
elif bridge is not None:
|
elif bridge is not None:
|
||||||
aug.remove(TOR_CONFIG + '/BridgeRelay')
|
aug.remove(TOR_CONFIG_AUG + '/BridgeRelay')
|
||||||
aug.remove(TOR_CONFIG + '/ServerTransportPlugin')
|
aug.remove(TOR_CONFIG_AUG + '/ServerTransportPlugin')
|
||||||
aug.remove(TOR_CONFIG + '/ExtORPort')
|
aug.remove(TOR_CONFIG_AUG + '/ExtORPort')
|
||||||
|
|
||||||
aug.save()
|
aug.save()
|
||||||
|
|
||||||
@ -388,11 +398,11 @@ def _enable_hs(aug=None):
|
|||||||
if _get_hidden_service(aug)['enabled']:
|
if _get_hidden_service(aug)['enabled']:
|
||||||
return
|
return
|
||||||
|
|
||||||
aug.set(TOR_CONFIG + '/HiddenServiceDir',
|
aug.set(TOR_CONFIG_AUG + '/HiddenServiceDir',
|
||||||
'/var/lib/tor-instances/plinth/hidden_service')
|
f'/var/lib/tor-instances/{INSTANCE_NAME}/hidden_service')
|
||||||
aug.set(TOR_CONFIG + '/HiddenServicePort[1]', '22 127.0.0.1:22')
|
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[1]', '22 127.0.0.1:22')
|
||||||
aug.set(TOR_CONFIG + '/HiddenServicePort[2]', '80 127.0.0.1:80')
|
aug.set(TOR_CONFIG_AUG + '/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 + '/HiddenServicePort[3]', '443 127.0.0.1:443')
|
||||||
aug.save()
|
aug.save()
|
||||||
_set_onion_header(_get_hidden_service(aug))
|
_set_onion_header(_get_hidden_service(aug))
|
||||||
|
|
||||||
@ -405,39 +415,12 @@ def _disable_hs(aug=None):
|
|||||||
if not _get_hidden_service(aug)['enabled']:
|
if not _get_hidden_service(aug)['enabled']:
|
||||||
return
|
return
|
||||||
|
|
||||||
aug.remove(TOR_CONFIG + '/HiddenServiceDir')
|
aug.remove(TOR_CONFIG_AUG + '/HiddenServiceDir')
|
||||||
aug.remove(TOR_CONFIG + '/HiddenServicePort')
|
aug.remove(TOR_CONFIG_AUG + '/HiddenServicePort')
|
||||||
aug.save()
|
aug.save()
|
||||||
_set_onion_header(None)
|
_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):
|
def _update_port(name, number):
|
||||||
"""Update firewall service information for single port."""
|
"""Update firewall service information for single port."""
|
||||||
lines = """<?xml version="1.0" encoding="utf-8"?>
|
lines = """<?xml version="1.0" encoding="utf-8"?>
|
||||||
@ -485,14 +468,14 @@ def augeas_load():
|
|||||||
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
|
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
|
||||||
augeas.Augeas.NO_MODL_AUTOLOAD)
|
augeas.Augeas.NO_MODL_AUTOLOAD)
|
||||||
aug.set('/augeas/load/Tor/lens', 'Tor.lns')
|
aug.set('/augeas/load/Tor/lens', 'Tor.lns')
|
||||||
aug.set('/augeas/load/Tor/incl[last() + 1]',
|
aug.set('/augeas/load/Tor/incl[last() + 1]', TOR_CONFIG)
|
||||||
'/etc/tor/instances/plinth/torrc')
|
|
||||||
aug.load()
|
aug.load()
|
||||||
return aug
|
return aug
|
||||||
|
|
||||||
|
|
||||||
def _set_onion_header(hidden_service):
|
def _set_onion_header(hidden_service):
|
||||||
"""Set Apache configuration for the Onion-Location header."""
|
"""Set Apache configuration for the Onion-Location header."""
|
||||||
|
logger.info('Setting Onion-Location header for Apache')
|
||||||
config_file = pathlib.Path(TOR_APACHE_SITE)
|
config_file = pathlib.Path(TOR_APACHE_SITE)
|
||||||
if hidden_service and hidden_service['enabled']:
|
if hidden_service and hidden_service['enabled']:
|
||||||
# https://community.torproject.org/onion-services/advanced/onion-location/
|
# https://community.torproject.org/onion-services/advanced/onion-location/
|
||||||
@ -512,10 +495,13 @@ def _set_onion_header(hidden_service):
|
|||||||
|
|
||||||
@privileged
|
@privileged
|
||||||
def uninstall():
|
def uninstall():
|
||||||
"""Remove create instances."""
|
"""Remove plinth instance."""
|
||||||
directories = [
|
directories = [
|
||||||
'/etc/tor/instances/', '/var/lib/tor-instances/',
|
f'/etc/tor/instances/{INSTANCE_NAME}/',
|
||||||
'/var/run/tor-instances/'
|
f'/var/lib/tor-instances/{INSTANCE_NAME}/',
|
||||||
|
f'/var/run/tor-instances/{INSTANCE_NAME}/'
|
||||||
]
|
]
|
||||||
for directory in directories:
|
for directory in directories:
|
||||||
shutil.rmtree(directory, ignore_errors=True)
|
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',
|
'relay': 'tor-relay_enabled',
|
||||||
'bridge-relay': 'tor-bridge_relay_enabled',
|
'bridge-relay': 'tor-bridge_relay_enabled',
|
||||||
'hidden-services': 'tor-hs_enabled',
|
'hidden-services': 'tor-hs_enabled',
|
||||||
'software': 'tor-apt_transport_tor_enabled'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pytestmark = [pytest.mark.apps, pytest.mark.domain, pytest.mark.tor]
|
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):
|
class TestTorApp(functional.BaseAppTests):
|
||||||
app_name = 'tor'
|
app_name = 'tor'
|
||||||
has_service = True
|
has_service = False
|
||||||
has_web = 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):
|
def test_set_tor_relay_configuration(self, session_browser):
|
||||||
"""Test setting Tor relay configuration."""
|
"""Test setting Tor relay configuration."""
|
||||||
@ -52,13 +48,6 @@ class TestTorApp(functional.BaseAppTests):
|
|||||||
enabled=True)
|
enabled=True)
|
||||||
_assert_hidden_services(session_browser)
|
_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
|
# TODO: Test more thoroughly by checking same hidden service is restored
|
||||||
# and by actually connecting using Tor.
|
# and by actually connecting using Tor.
|
||||||
@pytest.mark.backups
|
@pytest.mark.backups
|
||||||
|
|||||||
@ -14,14 +14,6 @@ from plinth.modules.tor import forms, utils
|
|||||||
class TestTor:
|
class TestTor:
|
||||||
"""Test cases for testing the Tor module."""
|
"""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
|
@staticmethod
|
||||||
@patch('plinth.app.App.get')
|
@patch('plinth.app.App.get')
|
||||||
@pytest.mark.usefixtures('needs_root', 'load_cfg')
|
@pytest.mark.usefixtures('needs_root', 'load_cfg')
|
||||||
@ -29,7 +21,7 @@ class TestTor:
|
|||||||
"""Test that get_status does not raise any unhandled exceptions.
|
"""Test that get_status does not raise any unhandled exceptions.
|
||||||
|
|
||||||
This should work regardless of whether tor is installed, or
|
This should work regardless of whether tor is installed, or
|
||||||
/etc/tor/torrc exists.
|
/etc/tor/instances/plinth/torrc exists.
|
||||||
"""
|
"""
|
||||||
utils.get_status()
|
utils.get_status()
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,12 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Tor utility functions."""
|
"""Tor utility functions."""
|
||||||
|
|
||||||
import itertools
|
|
||||||
|
|
||||||
import augeas
|
|
||||||
|
|
||||||
from plinth import app as app_module
|
from plinth import app as app_module
|
||||||
from plinth.daemon import app_is_running
|
from plinth.daemon import app_is_running
|
||||||
from plinth.modules.names.components import DomainName
|
from plinth.modules.names.components import DomainName
|
||||||
|
|
||||||
from . import privileged
|
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):
|
def get_status(initialized=True):
|
||||||
"""Return current Tor status."""
|
"""Return current Tor status."""
|
||||||
@ -53,50 +44,4 @@ def get_status(initialized=True):
|
|||||||
'hs_hostname': hs_info['hostname'],
|
'hs_hostname': hs_info['hostname'],
|
||||||
'hs_ports': hs_info['ports'],
|
'hs_ports': hs_info['ports'],
|
||||||
'hs_services': hs_services,
|
'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']
|
arguments['hidden_service'] = new_status['hs_enabled']
|
||||||
needs_restart = True
|
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'] != \
|
if old_status['use_upstream_bridges'] != \
|
||||||
new_status['use_upstream_bridges']:
|
new_status['use_upstream_bridges']:
|
||||||
arguments['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