mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
shadowsocksserver: Add separate app for Shadowsocks server
Closes: #729. Tests: - Install Shadowsocks Server. Install Shadowsocks Client, and set the server to localhost, and set the same password as the server. Use curl to connect to local SOCKS proxy on port 1080 and fetch a website. Signed-off-by: James Valleroy <jvalleroy@mailbox.org> [sunil: Update some docstring comments for shadowsocks clients] [sunil: Use the term Censorship instead of network filters] [sunil: Prevent enabling both apps when setup is re-run] [sunil: Update typehint for a privileged method to be minimal] [sunil: Accept connections from external IPs too] Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
parent
1e905d8553
commit
e719b1ed49
2
debian/copyright
vendored
2
debian/copyright
vendored
@ -232,11 +232,13 @@ Comment: https://github.com/shaarli/Shaarli/blob/master/doc/md/images/doc-logo.s
|
||||
License: Zlib
|
||||
|
||||
Files: plinth/modules/shadowsocks/static/icons/shadowsocks.png
|
||||
plinth/modules/shadowsocksserver/static/icons/shadowsocks.png
|
||||
Copyright: 2014-2018 Symeon Huang (librehat) <hzwhuang@gmail.com>
|
||||
Comment: https://commons.wikimedia.org/wiki/File:Shadowsocks_logo.png
|
||||
License: LGPL-3+
|
||||
|
||||
Files: plinth/modules/shadowsocks/static/icons/shadowsocks.svg
|
||||
plinth/modules/shadowsocksserver/static/icons/shadowsocks.svg
|
||||
Copyright: Clowwindy
|
||||
Comment: https://commons.wikimedia.org/wiki/File:Shadowsocks-Logo.svg
|
||||
License: Apache-2.0
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app to configure Shadowsocks."""
|
||||
"""FreedomBox app to configure Shadowsocks Client."""
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -15,9 +15,13 @@ from plinth.utils import format_lazy
|
||||
from . import manifest, privileged
|
||||
|
||||
_description = [
|
||||
_('Shadowsocks is a lightweight and secure SOCKS5 proxy, designed to '
|
||||
'protect your Internet traffic. It can be used to bypass Internet '
|
||||
'filtering and censorship.'),
|
||||
_('Shadowsocks is a tool for securely forwarding network requests to a'
|
||||
' remote server. It consists of two parts: (1) a Shadowsocks server,'
|
||||
' and (2) a Shadowsocks client with a SOCKS5 proxy.'),
|
||||
_('Shadowsocks can be used to bypass Internet filtering and '
|
||||
'censorship. This requires that the Shadowsocks server is in a '
|
||||
'location where it can freely access the Internet, without '
|
||||
'filtering.'),
|
||||
format_lazy(
|
||||
_('Your {box_name} can run a Shadowsocks client, that can connect to '
|
||||
'a Shadowsocks server. It will also run a SOCKS5 proxy. Local '
|
||||
@ -30,7 +34,7 @@ _description = [
|
||||
|
||||
|
||||
class ShadowsocksApp(app_module.App):
|
||||
"""FreedomBox app for Shadowsocks."""
|
||||
"""FreedomBox app for Shadowsocks Client."""
|
||||
|
||||
app_id = 'shadowsocks'
|
||||
|
||||
@ -43,9 +47,9 @@ class ShadowsocksApp(app_module.App):
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(app_id=self.app_id, version=self._version,
|
||||
name=_('Shadowsocks'),
|
||||
name=_('Shadowsocks Client'),
|
||||
icon_filename='shadowsocks',
|
||||
short_description=_('Socks5 Proxy'),
|
||||
short_description=_('Bypass Censorship'),
|
||||
description=_description,
|
||||
manual_page='Shadowsocks')
|
||||
self.add(info)
|
||||
@ -83,7 +87,8 @@ class ShadowsocksApp(app_module.App):
|
||||
"""Install and configure the app."""
|
||||
super().setup(old_version)
|
||||
privileged.setup()
|
||||
self.enable()
|
||||
if not old_version:
|
||||
self.enable()
|
||||
|
||||
def uninstall(self):
|
||||
"""De-configure and uninstall the app."""
|
||||
|
||||
@ -1,22 +1,10 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app for configuring Shadowsocks."""
|
||||
"""FreedomBox app for configuring Shadowsocks Client."""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
METHODS = [('chacha20-ietf-poly1305',
|
||||
format_lazy('chacha20-ietf-poly1305 ({})', _('Recommended'))),
|
||||
('aes-256-gcm', format_lazy('aes-256-gcm ({})', _('Recommended'))),
|
||||
('aes-192-gcm', 'aes-192-gcm'), ('aes-128-gcm', 'aes-128-gcm'),
|
||||
('aes-128-ctr', 'aes-128-ctr'), ('aes-192-ctr', 'aes-192-ctr'),
|
||||
('aes-256-ctr', 'aes-256-ctr'), ('aes-128-cfb', 'aes-128-cfb'),
|
||||
('aes-192-cfb', 'aes-192-cfb'), ('aes-256-cfb', 'aes-256-cfb'),
|
||||
('camellia-128-cfb', 'camellia-128-cfb'),
|
||||
('camellia-192-cfb', 'camellia-192-cfb'),
|
||||
('camellia-256-cfb', 'camellia-256-cfb'),
|
||||
('chacha20-ietf', 'chacha20-ietf')]
|
||||
from plinth.modules.shadowsocksserver.forms import METHODS
|
||||
|
||||
|
||||
class TrimmedCharField(forms.CharField):
|
||||
@ -31,7 +19,7 @@ class TrimmedCharField(forms.CharField):
|
||||
|
||||
|
||||
class ShadowsocksForm(forms.Form):
|
||||
"""Shadowsocks configuration form."""
|
||||
"""Shadowsocks Client configuration form."""
|
||||
|
||||
server = TrimmedCharField(label=_('Server'),
|
||||
help_text=_('Server hostname or IP address'))
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Application manifest for shadowsocks.
|
||||
Application manifest for Shadowsocks Client.
|
||||
"""
|
||||
|
||||
backup = {
|
||||
|
||||
@ -87,7 +87,7 @@ def _merge_config(config):
|
||||
|
||||
@privileged
|
||||
def merge_config(config: dict[str, Union[int, str]]):
|
||||
"""Configure Shadowsocks."""
|
||||
"""Configure Shadowsocks Client."""
|
||||
_merge_config(config)
|
||||
|
||||
# Don't try_restart because initial configuration may not be valid so
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Functional, browser based tests for shadowsocks app.
|
||||
Functional, browser based tests for Shadowsocks Client app.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
URLs for the Shadowsocks module.
|
||||
URLs for the Shadowsocks Clients.
|
||||
"""
|
||||
|
||||
from django.urls import re_path
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app for configuring Shadowsocks."""
|
||||
"""FreedomBox app for configuring Shadowsocks Client."""
|
||||
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -11,7 +11,7 @@ from .forms import ShadowsocksForm
|
||||
|
||||
|
||||
class ShadowsocksAppView(views.AppView):
|
||||
"""Configuration view for Shadowsocks local socks5 proxy."""
|
||||
"""Configuration view for Shadowsocks Client and SOCKS5 proxy."""
|
||||
|
||||
app_id = 'shadowsocks'
|
||||
form_class = ShadowsocksForm
|
||||
|
||||
96
plinth/modules/shadowsocksserver/__init__.py
Normal file
96
plinth/modules/shadowsocksserver/__init__.py
Normal file
@ -0,0 +1,96 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app to configure Shadowsocks Server."""
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, frontpage, menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.package import Packages
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from . import manifest, privileged
|
||||
|
||||
_description = [
|
||||
_('Shadowsocks is a tool for securely forwarding network requests to a'
|
||||
' remote server. It consists of two parts: (1) a Shadowsocks server,'
|
||||
' and (2) a Shadowsocks client with a SOCKS5 proxy.'),
|
||||
_('Shadowsocks can be used to bypass Internet filtering and '
|
||||
'censorship. This requires that the Shadowsocks server is in a '
|
||||
'location where it can freely access the Internet, without '
|
||||
'filtering.'),
|
||||
format_lazy(
|
||||
_('Your {box_name} can run a Shadowsocks server, that allows '
|
||||
'Shadowsocks clients to connect to it. Clients\' data will be '
|
||||
'encrypted and proxied through this server.'),
|
||||
box_name=_(cfg.box_name)),
|
||||
]
|
||||
|
||||
|
||||
class ShadowsocksServerApp(app_module.App):
|
||||
"""FreedomBox app for Shadowsocks Server."""
|
||||
|
||||
app_id = 'shadowsocksserver'
|
||||
|
||||
_version = 1
|
||||
|
||||
DAEMON = 'shadowsocks-libev-server@fbxserver'
|
||||
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(
|
||||
app_id=self.app_id, version=self._version,
|
||||
name=_('Shadowsocks Server'), icon_filename='shadowsocks',
|
||||
short_description=_('Help Others Bypass Censorship'),
|
||||
description=_description, manual_page='ShadowsocksServer')
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-shadowsocks-server', info.name,
|
||||
info.short_description, info.icon_filename,
|
||||
'shadowsocksserver:index',
|
||||
parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut(
|
||||
'shortcut-shadowsocks-server', info.name,
|
||||
short_description=info.short_description, icon=info.icon_filename,
|
||||
description=info.description, manual_page=info.manual_page,
|
||||
configure_url=reverse_lazy('shadowsocksserver:index'),
|
||||
login_required=True)
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-shadowsocks-server',
|
||||
['shadowsocks-libev'])
|
||||
self.add(packages)
|
||||
|
||||
firewall = Firewall('firewall-shadowsocks-server', info.name,
|
||||
ports=['shadowsocks-server-freedombox'],
|
||||
is_external=True)
|
||||
self.add(firewall)
|
||||
|
||||
daemon = Daemon(
|
||||
'daemon-shadowsocks-server', self.DAEMON,
|
||||
listen_ports=[(8388, 'tcp4'), (8388, 'tcp6'), (8388, 'udp4'),
|
||||
(8388, 'udp6')])
|
||||
self.add(daemon)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-shadowsocks-server',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
def setup(self, old_version):
|
||||
"""Install and configure the app."""
|
||||
super().setup(old_version)
|
||||
privileged.setup()
|
||||
if not old_version:
|
||||
self.enable()
|
||||
|
||||
def uninstall(self):
|
||||
"""De-configure and uninstall the app."""
|
||||
super().uninstall()
|
||||
privileged.uninstall()
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<service>
|
||||
<short>Shadowsocks server</short>
|
||||
<description>Shadowsocks is a lightweight and secure SOCKS5 proxy, designed to protect your Internet traffic. Enable this service if you are running a Shadowsocks server, to provide secured tunnel service.</description>
|
||||
<port protocol="tcp" port="8388"/>
|
||||
<port protocol="udp" port="8388"/>
|
||||
</service>
|
||||
@ -0,0 +1,3 @@
|
||||
[Service]
|
||||
StateDirectory=shadowsocks-libev/%i
|
||||
DynamicUser=yes
|
||||
@ -0,0 +1 @@
|
||||
plinth.modules.shadowsocksserver
|
||||
43
plinth/modules/shadowsocksserver/forms.py
Normal file
43
plinth/modules/shadowsocksserver/forms.py
Normal file
@ -0,0 +1,43 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app for configuring Shadowsocks Server."""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
METHODS = [('chacha20-ietf-poly1305',
|
||||
format_lazy('chacha20-ietf-poly1305 ({})', _('Recommended'))),
|
||||
('aes-256-gcm', format_lazy('aes-256-gcm ({})', _('Recommended'))),
|
||||
('aes-192-gcm', 'aes-192-gcm'), ('aes-128-gcm', 'aes-128-gcm'),
|
||||
('aes-128-ctr', 'aes-128-ctr'), ('aes-192-ctr', 'aes-192-ctr'),
|
||||
('aes-256-ctr', 'aes-256-ctr'), ('aes-128-cfb', 'aes-128-cfb'),
|
||||
('aes-192-cfb', 'aes-192-cfb'), ('aes-256-cfb', 'aes-256-cfb'),
|
||||
('camellia-128-cfb', 'camellia-128-cfb'),
|
||||
('camellia-192-cfb', 'camellia-192-cfb'),
|
||||
('camellia-256-cfb', 'camellia-256-cfb'),
|
||||
('chacha20-ietf', 'chacha20-ietf')]
|
||||
|
||||
|
||||
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()
|
||||
|
||||
return super().clean(value)
|
||||
|
||||
|
||||
class ShadowsocksServerForm(forms.Form):
|
||||
"""Shadowsocks Server configuration form."""
|
||||
|
||||
password = forms.CharField(
|
||||
label=_('Password'),
|
||||
help_text=_('Password used to encrypt data. Clients must use the '
|
||||
'same password.'))
|
||||
|
||||
method = forms.ChoiceField(
|
||||
label=_('Method'), choices=METHODS,
|
||||
help_text=_('Encryption method. Clients must use the same setting.'))
|
||||
13
plinth/modules/shadowsocksserver/manifest.py
Normal file
13
plinth/modules/shadowsocksserver/manifest.py
Normal file
@ -0,0 +1,13 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Application manifest for Shadowsocks Server.
|
||||
"""
|
||||
|
||||
backup = {
|
||||
'secrets': {
|
||||
'files': [
|
||||
'/var/lib/private/shadowsocks-libev/fbxserver/fbxserver.json'
|
||||
]
|
||||
},
|
||||
'services': ['shadowsocks-libev-server@fbxserver']
|
||||
}
|
||||
87
plinth/modules/shadowsocksserver/privileged.py
Normal file
87
plinth/modules/shadowsocksserver/privileged.py
Normal file
@ -0,0 +1,87 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Configure Shadowsocks Server."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import random
|
||||
import string
|
||||
from typing import Union
|
||||
|
||||
from plinth import action_utils
|
||||
from plinth.actions import privileged
|
||||
|
||||
SHADOWSOCKS_CONFIG_SYMLINK = '/etc/shadowsocks-libev/fbxserver.json'
|
||||
SHADOWSOCKS_CONFIG_ACTUAL = \
|
||||
'/var/lib/private/shadowsocks-libev/fbxserver/fbxserver.json'
|
||||
|
||||
|
||||
@privileged
|
||||
def setup():
|
||||
"""Perform initial setup steps."""
|
||||
# Disable the default service, and use the templated service instead, so
|
||||
# that the configuration can be customized.
|
||||
action_utils.service_disable('shadowsocks-libev')
|
||||
|
||||
os.makedirs('/var/lib/private/shadowsocks-libev/fbxserver/', exist_ok=True)
|
||||
|
||||
if not os.path.islink(SHADOWSOCKS_CONFIG_SYMLINK):
|
||||
os.symlink(SHADOWSOCKS_CONFIG_ACTUAL, SHADOWSOCKS_CONFIG_SYMLINK)
|
||||
|
||||
if not os.path.isfile(SHADOWSOCKS_CONFIG_ACTUAL):
|
||||
password = ''.join(
|
||||
random.choice(string.ascii_letters) for _ in range(12))
|
||||
initial_config = {
|
||||
'server': ['::0', '0.0.0.0'], # As recommended in man page
|
||||
'mode': 'tcp_and_udp',
|
||||
'server_port': 8388,
|
||||
'password': password,
|
||||
'timeout': 86400,
|
||||
'method': 'chacha20-ietf-poly1305'
|
||||
}
|
||||
_merge_config(initial_config)
|
||||
|
||||
from plinth.modules.shadowsocksserver import ShadowsocksServerApp
|
||||
if action_utils.service_is_enabled(ShadowsocksServerApp.DAEMON):
|
||||
action_utils.service_restart(ShadowsocksServerApp.DAEMON)
|
||||
|
||||
|
||||
@privileged
|
||||
def get_config() -> dict[str, Union[int, str]]:
|
||||
"""Read and print Shadowsocks Server configuration."""
|
||||
config = open(SHADOWSOCKS_CONFIG_SYMLINK, 'r', encoding='utf-8').read()
|
||||
return json.loads(config)
|
||||
|
||||
|
||||
def _merge_config(config):
|
||||
"""Write merged configuration into file."""
|
||||
try:
|
||||
current_config = open(SHADOWSOCKS_CONFIG_SYMLINK, 'r',
|
||||
encoding='utf-8').read()
|
||||
current_config = json.loads(current_config)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
current_config = {}
|
||||
|
||||
new_config = current_config
|
||||
new_config.update(config)
|
||||
new_config = json.dumps(new_config, indent=4, sort_keys=True)
|
||||
open(SHADOWSOCKS_CONFIG_SYMLINK, 'w', encoding='utf-8').write(new_config)
|
||||
|
||||
|
||||
@privileged
|
||||
def merge_config(config: dict[str, str]):
|
||||
"""Configure Shadowsocks Server."""
|
||||
_merge_config(config)
|
||||
|
||||
# Don't try_restart because initial configuration may not be valid so
|
||||
# shadowsocks will not be running even when enabled.
|
||||
from . import ShadowsocksServerApp
|
||||
if action_utils.service_is_enabled(ShadowsocksServerApp.DAEMON):
|
||||
action_utils.service_restart(ShadowsocksServerApp.DAEMON)
|
||||
|
||||
|
||||
@privileged
|
||||
def uninstall():
|
||||
"""Remove configuration files."""
|
||||
for path in SHADOWSOCKS_CONFIG_SYMLINK, SHADOWSOCKS_CONFIG_ACTUAL:
|
||||
pathlib.Path(path).unlink(missing_ok=True)
|
||||
BIN
plinth/modules/shadowsocksserver/static/icons/shadowsocks.png
Normal file
BIN
plinth/modules/shadowsocksserver/static/icons/shadowsocks.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,62 @@
|
||||
<?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"
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 384 384"
|
||||
class="mozwebext"
|
||||
version="1.1"
|
||||
id="svg74980"
|
||||
sodipodi:docname="shadowsocks.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<metadata
|
||||
id="metadata74986">
|
||||
<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="defs74984" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1756"
|
||||
inkscape:window-height="1255"
|
||||
id="namedview74982"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:zoom="1.0858896"
|
||||
inkscape:cx="90"
|
||||
inkscape:cy="84.666667"
|
||||
inkscape:window-x="1287"
|
||||
inkscape:window-y="397"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg74980" />
|
||||
<path
|
||||
d="M 384,9.0045385 314.28744,301.79727 c 0,0 -107.47352,-30.49924 -146.68683,-43.57034 L 295.40696,102.53555 118.22088,242.5416 0,203.61876 Z M 168.47201,291.63086 l 44.73223,14.52345 -46.18457,68.84115 z"
|
||||
id="path74978"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#1a7dc0;stroke-width:2.90468979" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
0
plinth/modules/shadowsocksserver/tests/__init__.py
Normal file
0
plinth/modules/shadowsocksserver/tests/__init__.py
Normal file
52
plinth/modules/shadowsocksserver/tests/test_functional.py
Normal file
52
plinth/modules/shadowsocksserver/tests/test_functional.py
Normal file
@ -0,0 +1,52 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Functional, browser based tests for Shadowsocks Server app.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from plinth.tests import functional
|
||||
|
||||
pytestmark = [pytest.mark.apps, pytest.mark.shadowsocksserver]
|
||||
|
||||
|
||||
class TestShadowsocksServerApp(functional.BaseAppTests):
|
||||
app_name = 'shadowsocksserver'
|
||||
has_service = True
|
||||
has_web = False
|
||||
|
||||
@pytest.fixture(scope='class', autouse=True)
|
||||
def fixture_setup(self, session_browser):
|
||||
"""Setup the app."""
|
||||
functional.login(session_browser)
|
||||
functional.install(session_browser, 'shadowsocksserver')
|
||||
_configure(session_browser, 'fakepassword')
|
||||
|
||||
@pytest.mark.backups
|
||||
def test_backup_restore(self, session_browser):
|
||||
"""Test backup and restore of configuration."""
|
||||
_configure(session_browser, 'beforebackup123')
|
||||
functional.backup_create(session_browser, 'shadowsocksserver',
|
||||
'test_shadowsocksserver')
|
||||
|
||||
_configure(session_browser, 'afterbackup123')
|
||||
functional.backup_restore(session_browser, 'shadowsocksserver',
|
||||
'test_shadowsocksserver')
|
||||
|
||||
assert functional.service_is_running(session_browser,
|
||||
'shadowsocksserver')
|
||||
assert _get_configuration(session_browser) == 'beforebackup123'
|
||||
|
||||
|
||||
def _configure(browser, password):
|
||||
"""Configure Shadowsocks Server with given details."""
|
||||
functional.visit(browser, '/plinth/apps/shadowsocksserver/')
|
||||
browser.find_by_id('id_password').fill(password)
|
||||
functional.submit(browser, form_class='form-configuration')
|
||||
|
||||
|
||||
def _get_configuration(browser):
|
||||
"""Return the password currently configured in Shadowsocks Server."""
|
||||
functional.visit(browser, '/plinth/apps/shadowsocksserver/')
|
||||
password = browser.find_by_id('id_password').value
|
||||
return password
|
||||
13
plinth/modules/shadowsocksserver/urls.py
Normal file
13
plinth/modules/shadowsocksserver/urls.py
Normal file
@ -0,0 +1,13 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
URLs for the Shadowsocks Server module.
|
||||
"""
|
||||
|
||||
from django.urls import re_path
|
||||
|
||||
from .views import ShadowsocksServerAppView
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^apps/shadowsocksserver/$', ShadowsocksServerAppView.as_view(),
|
||||
name='index'),
|
||||
]
|
||||
54
plinth/modules/shadowsocksserver/views.py
Normal file
54
plinth/modules/shadowsocksserver/views.py
Normal file
@ -0,0 +1,54 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app for configuring Shadowsocks Server."""
|
||||
|
||||
import random
|
||||
import string
|
||||
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth import views
|
||||
|
||||
from . import privileged
|
||||
from .forms import ShadowsocksServerForm
|
||||
|
||||
|
||||
class ShadowsocksServerAppView(views.AppView):
|
||||
"""Configuration view for Shadowsocks Server."""
|
||||
|
||||
app_id = 'shadowsocksserver'
|
||||
form_class = ShadowsocksServerForm
|
||||
|
||||
def get_initial(self, *args, **kwargs):
|
||||
"""Get initial values for form."""
|
||||
status = super().get_initial()
|
||||
try:
|
||||
status.update(privileged.get_config())
|
||||
except Exception:
|
||||
# If we cannot read the configuration for some reason, generate a
|
||||
# new random password.
|
||||
password = ''.join(
|
||||
random.choice(string.ascii_letters) for _ in range(12))
|
||||
status.update({
|
||||
'password': password,
|
||||
'method': 'chacha20-ietf-poly1305',
|
||||
})
|
||||
|
||||
return status
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Configure Shadowsocks Server."""
|
||||
old_status = form.initial
|
||||
new_status = form.cleaned_data
|
||||
|
||||
if old_status['password'] != new_status['password'] or \
|
||||
old_status['method'] != new_status['method']:
|
||||
new_config = {
|
||||
'password': new_status['password'],
|
||||
'method': new_status['method'],
|
||||
}
|
||||
|
||||
privileged.merge_config(new_config)
|
||||
messages.success(self.request, _('Configuration updated'))
|
||||
|
||||
return super().form_valid(form)
|
||||
@ -57,6 +57,7 @@ markers = [
|
||||
"security",
|
||||
"shaarli",
|
||||
"shadowsocks",
|
||||
"shadowsocksserver",
|
||||
"sharing",
|
||||
"snapshot",
|
||||
"ssh",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user