shadowsocks: Use privileged decorator for actions

Tests:

- Functional tests work
- Initial setup works
- Setting configuration works, correct configuration is updated in the
  configuration files and app shows the values correctly.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2022-08-26 10:45:43 -07:00 committed by James Valleroy
parent 97706cef8e
commit 6e7b31a3cf
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
4 changed files with 28 additions and 69 deletions

View File

@ -1,12 +1,9 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
""" """FreedomBox app to configure Shadowsocks."""
FreedomBox app to configure Shadowsocks.
"""
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from plinth import actions
from plinth import app as app_module from plinth import app as app_module
from plinth import cfg, frontpage, menu from plinth import cfg, frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
@ -15,7 +12,7 @@ from plinth.modules.firewall.components import Firewall
from plinth.package import Packages from plinth.package import Packages
from plinth.utils import format_lazy from plinth.utils import format_lazy
from . import manifest from . import manifest, privileged
_description = [ _description = [
_('Shadowsocks is a lightweight and secure SOCKS5 proxy, designed to ' _('Shadowsocks is a lightweight and secure SOCKS5 proxy, designed to '
@ -85,5 +82,5 @@ class ShadowsocksApp(app_module.App):
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)
actions.superuser_run('shadowsocks', ['setup']) privileged.setup()
self.enable() self.enable()

View File

@ -1,7 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
""" """FreedomBox app for configuring Shadowsocks."""
FreedomBox app for configuring Shadowsocks.
"""
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -22,10 +20,10 @@ METHODS = [('chacha20-ietf-poly1305',
class TrimmedCharField(forms.CharField): class TrimmedCharField(forms.CharField):
"""Trim the contents of a CharField""" """Trim the contents of a CharField."""
def clean(self, value): def clean(self, value):
"""Clean and validate the field value""" """Clean and validate the field value."""
if value: if value:
value = value.strip() value = value.strip()
@ -33,7 +31,8 @@ class TrimmedCharField(forms.CharField):
class ShadowsocksForm(forms.Form): class ShadowsocksForm(forms.Form):
"""Shadowsocks configuration form""" """Shadowsocks configuration form."""
server = TrimmedCharField(label=_('Server'), server = TrimmedCharField(label=_('Server'),
help_text=_('Server hostname or IP address')) help_text=_('Server hostname or IP address'))

View File

@ -1,42 +1,24 @@
#!/usr/bin/python3
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
""" """Configure Shadowsocks."""
Helper script for configuring Shadowsocks.
"""
import argparse
import json import json
import os import os
import pathlib import pathlib
import random import random
import string import string
import sys
from shutil import move from shutil import move
from typing import Union
from plinth import action_utils from plinth import action_utils
from plinth.modules.shadowsocks import ShadowsocksApp from plinth.actions import privileged
SHADOWSOCKS_CONFIG_SYMLINK = '/etc/shadowsocks-libev/freedombox.json' SHADOWSOCKS_CONFIG_SYMLINK = '/etc/shadowsocks-libev/freedombox.json'
SHADOWSOCKS_CONFIG_ACTUAL = \ SHADOWSOCKS_CONFIG_ACTUAL = \
'/var/lib/private/shadowsocks-libev/freedombox/freedombox.json' '/var/lib/private/shadowsocks-libev/freedombox/freedombox.json'
def parse_arguments(): @privileged
"""Return parsed command line arguments as dictionary.""" def setup():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
subparsers.add_parser('setup', help='Perform initial setup steps')
subparsers.add_parser('get-config',
help='Read and print JSON config to stdout')
subparsers.add_parser('merge-config',
help='Merge JSON config from stdin with existing')
subparsers.required = True
return parser.parse_args()
def subcommand_setup(_):
"""Perform initial setup steps.""" """Perform initial setup steps."""
# Only client socks5 proxy is supported for now. Disable the # Only client socks5 proxy is supported for now. Disable the
# server component. # server component.
@ -76,16 +58,16 @@ def subcommand_setup(_):
if not wrong_state_dir.is_symlink() and wrong_state_dir.is_dir(): if not wrong_state_dir.is_symlink() and wrong_state_dir.is_dir():
wrong_state_dir.rmdir() wrong_state_dir.rmdir()
from plinth.modules.shadowsocks import ShadowsocksApp
if action_utils.service_is_enabled(ShadowsocksApp.DAEMON): if action_utils.service_is_enabled(ShadowsocksApp.DAEMON):
action_utils.service_restart(ShadowsocksApp.DAEMON) action_utils.service_restart(ShadowsocksApp.DAEMON)
def subcommand_get_config(_): @privileged
def get_config() -> dict[str, Union[int, str]]:
"""Read and print Shadowsocks configuration.""" """Read and print Shadowsocks configuration."""
try: config = open(SHADOWSOCKS_CONFIG_SYMLINK, 'r', encoding='utf-8').read()
print(open(SHADOWSOCKS_CONFIG_SYMLINK, 'r', encoding='utf-8').read()) return json.loads(config)
except Exception:
sys.exit(1)
def _merge_config(config): def _merge_config(config):
@ -103,26 +85,13 @@ def _merge_config(config):
open(SHADOWSOCKS_CONFIG_SYMLINK, 'w', encoding='utf-8').write(new_config) open(SHADOWSOCKS_CONFIG_SYMLINK, 'w', encoding='utf-8').write(new_config)
def subcommand_merge_config(_): @privileged
def merge_config(config: dict[str, Union[int, str]]):
"""Configure Shadowsocks.""" """Configure Shadowsocks."""
config = sys.stdin.read()
config = json.loads(config)
_merge_config(config) _merge_config(config)
# Don't try_restart because initial configuration may not be valid so # Don't try_restart because initial configuration may not be valid so
# shadowsocks will not be running even when enabled. # shadowsocks will not be running even when enabled.
from . import ShadowsocksApp
if action_utils.service_is_enabled(ShadowsocksApp.DAEMON): if action_utils.service_is_enabled(ShadowsocksApp.DAEMON):
action_utils.service_restart(ShadowsocksApp.DAEMON) action_utils.service_restart(ShadowsocksApp.DAEMON)
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
if __name__ == '__main__':
main()

View File

@ -1,21 +1,18 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
""" """FreedomBox app for configuring Shadowsocks."""
FreedomBox app for configuring Shadowsocks.
"""
import json
from django.contrib import messages from django.contrib import messages
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from plinth import actions, views from plinth import views
from plinth.errors import ActionError
from . import privileged
from .forms import ShadowsocksForm from .forms import ShadowsocksForm
class ShadowsocksAppView(views.AppView): class ShadowsocksAppView(views.AppView):
"""Configuration view for Shadowsocks local socks5 proxy.""" """Configuration view for Shadowsocks local socks5 proxy."""
app_id = 'shadowsocks' app_id = 'shadowsocks'
form_class = ShadowsocksForm form_class = ShadowsocksForm
@ -23,10 +20,8 @@ class ShadowsocksAppView(views.AppView):
"""Get initial values for form.""" """Get initial values for form."""
status = super().get_initial() status = super().get_initial()
try: try:
configuration = actions.superuser_run('shadowsocks', status.update(privileged.get_config())
['get-config']) except Exception:
status.update(json.loads(configuration))
except ActionError:
status.update({ status.update({
'server': '', 'server': '',
'server_port': 8388, 'server_port': 8388,
@ -54,8 +49,7 @@ class ShadowsocksAppView(views.AppView):
'method': new_status['method'], 'method': new_status['method'],
} }
actions.superuser_run('shadowsocks', ['merge-config'], privileged.merge_config(new_config)
input=json.dumps(new_config).encode())
messages.success(self.request, _('Configuration updated')) messages.success(self.request, _('Configuration updated'))
return super().form_valid(form) return super().form_valid(form)