diff --git a/plinth/modules/tor/forms.py b/plinth/modules/tor/forms.py index 9958f1bbd..0d4d268a7 100644 --- a/plinth/modules/tor/forms.py +++ b/plinth/modules/tor/forms.py @@ -24,56 +24,64 @@ from django.core.exceptions import ValidationError from django.core.validators import validate_ipv46_address from django.forms import widgets from django.utils.translation import ugettext_lazy as _ +import re from plinth import cfg from plinth.utils import format_lazy -BRIDGE_VALIDATION_ERROR_MESSAGE = _('Enter a valid bridge with this format: ' - '[transport] IP:ORPort [fingerprint]') - - class TrimmedCharField(forms.CharField): """Trim the contents of a CharField""" def clean(self, value): """Clean and validate the field value""" if value: value = value.strip() + value = value.replace('\r\n', '\n') return super(TrimmedCharField, self).clean(value) def bridges_validator(bridges): """Validate upstream bridges entries.""" - for bridge in bridges.split('\n'): - parts = bridge.split() + validation_error = ValidationError( + _('Enter a valid bridge with this format: ' + '[transport] IP:ORPort [fingerprint]'), code='invalid') + + bridges = [bridge.strip() for bridge in bridges.split('\n')] + bridges = [bridge for bridge in bridges if bridge] + if not bridges: + raise validation_error + + for bridge in bridges: + parts = [part for part in bridge.split() if part] # IP:ORPort is required, transport and fingerprint are optional. # Transports may have additional options after the fingerprint. - if len(parts) < 1: - raise ValidationError( - BRIDGE_VALIDATION_ERROR_MESSAGE, code='invalid') - - # May start with transport or IP:ORPort. try: - ip_info = parts[0].split(':') - validate_ipv46_address(ip_info[0]) + ip_port_part = parts[0] + if re.match('[a-z_][a-z0-9_]*', parts[0]): + ip_port_part = parts[1] + except IndexError: + raise validation_error + + match = re.match('\[([a-fA-F0-9:]+)\](?::([0-9]+))?', ip_port_part) + if match: + ip_address = match.group(1) + port = match.group(2) + else: + ip_parts = ip_port_part.rsplit(':', maxsplit=1) + ip_address = ip_parts[0] + port = ip_parts[1] if len(ip_parts) > 1 else None + + try: + validate_ipv46_address(ip_address) except ValidationError: - try: - ip_info = parts[1].split(':') - validate_ipv46_address(ip_info[0]) - except (ValidationError, IndexError): - raise ValidationError( - BRIDGE_VALIDATION_ERROR_MESSAGE, code='invalid') + raise validation_error - try: - port = int(ip_info[1]) - except ValueError: - raise ValidationError( - BRIDGE_VALIDATION_ERROR_MESSAGE, code='invalid') - if port < 0 or port > 65535: - raise ValidationError( - BRIDGE_VALIDATION_ERROR_MESSAGE, code='invalid') + if port: + port = int(port) + if port < 0 or port > 65535: + raise validation_error class TorForm(forms.Form): # pylint: disable=W0232 @@ -131,3 +139,16 @@ class TorForm(forms.Form): # pylint: disable=W0232 '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.""" + cleaned_data = super().clean() + use_upstream_bridges = cleaned_data.get('use_upstream_bridges') + upstream_bridges = cleaned_data.get('upstream_bridges') + + if use_upstream_bridges and not upstream_bridges: + self.add_error('upstream_bridges', ValidationError(_( + 'Specify at least one upstream bridge to use upstream ' + 'bridges.'), code='invalid')) + + return cleaned_data diff --git a/plinth/modules/tor/tests/test_tor.py b/plinth/modules/tor/tests/test_tor.py index bb4e4b23d..c0489c9f4 100644 --- a/plinth/modules/tor/tests/test_tor.py +++ b/plinth/modules/tor/tests/test_tor.py @@ -19,10 +19,12 @@ Tests for Tor module. """ +from django.core.exceptions import ValidationError import os import unittest from plinth.modules.tor import utils +from plinth.modules.tor import forms euid = os.geteuid() @@ -44,3 +46,51 @@ class TestTor(unittest.TestCase): /etc/tor/torrc exists. """ utils.get_status() + + +class TestTorForm(unittest.TestCase): + """Test whether Tor configration form works.""" + def test_bridge_validator(self): + """Test upstream bridges' form field validator.""" + validator = forms.bridges_validator + + # Just IP:port + validator('73.237.165.184:9001') + validator('73.237.165.184') + validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443') + validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]') + + # With fingerprint + validator('73.237.165.184:9001 ' + '0D04F10F497E68D2AF32375BB763EC3458A908C8') + + # With transport type + validator('obfs4 73.237.165.184:9001 ' + '0D04F10F497E68D2AF32375BB763EC3458A908C8') + + # With transport type and extra options + validator('obfs4 10.1.1.1:30000 ' + '0123456789ABCDEF0123456789ABCDEF01234567 ' + 'cert=A/b+1 iat-mode=0') + + # Leading, trailing spaces and empty lines + validator('\n' + ' \n' + '73.237.165.184:9001 ' + '0D04F10F497E68D2AF32375BB763EC3458A908C8' + ' \n' + '73.237.165.184:9001 ' + '0D04F10F497E68D2AF32375BB763EC3458A908C8' + ' \n' + '\n') + + # Invalid number for parts + self.assertRaises(ValidationError, validator, ' ') + + # Invalid IP address/port + self.assertRaises(ValidationError, validator, '73.237.165.384:9001') + self.assertRaises(ValidationError, validator, '73.237.165.184:90001') + self.assertRaises(ValidationError, validator, + '[a2001:db8:85a3:8d3:1319:8a2e:370:7348]:443') + self.assertRaises(ValidationError, validator, + '[2001:db8:85a3:8d3:1319:8a2e:370:7348]:90443')