From 6bf4eb148318a49d8624d99fe8f701f16a43391f Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Sat, 11 Nov 2017 16:14:27 -0500 Subject: [PATCH] shadowsocks: Add shadowsocks client with socks5 proxy Signed-off-by: James Valleroy Reviewed-by: Sunil Mohan Adapa --- LICENSES | 1 + actions/shadowsocks | 97 ++++++++++++++ data/etc/plinth/modules-enabled/shadowsocks | 1 + .../services/shadowsocks-local-plinth.xml | 6 + plinth/modules/shadowsocks/__init__.py | 126 ++++++++++++++++++ plinth/modules/shadowsocks/forms.py | 61 +++++++++ plinth/modules/shadowsocks/tests/__init__.py | 0 plinth/modules/shadowsocks/urls.py | 29 ++++ plinth/modules/shadowsocks/views.py | 82 ++++++++++++ static/themes/default/icons/shadowsocks.png | Bin 0 -> 16487 bytes 10 files changed, 403 insertions(+) create mode 100755 actions/shadowsocks create mode 100644 data/etc/plinth/modules-enabled/shadowsocks create mode 100644 data/usr/lib/firewalld/services/shadowsocks-local-plinth.xml create mode 100644 plinth/modules/shadowsocks/__init__.py create mode 100644 plinth/modules/shadowsocks/forms.py create mode 100644 plinth/modules/shadowsocks/tests/__init__.py create mode 100644 plinth/modules/shadowsocks/urls.py create mode 100644 plinth/modules/shadowsocks/views.py create mode 100644 static/themes/default/icons/shadowsocks.png diff --git a/LICENSES b/LICENSES index 7957c93e8..dc86c91c7 100644 --- a/LICENSES +++ b/LICENSES @@ -58,6 +58,7 @@ otherwise. - static/themes/default/icons/repro.png :: [[https://www.resiprocate.org/Main_Page][BSD-3-clause]] - static/themes/default/icons/roundcube.png :: [[https://roundcube.net/][GPL-3+]] - static/themes/default/icons/shaarli.png :: [[https://github.com/shaarli/Shaarli][zlib/libpng]] +- static/themes/default/icons/shadowsocks.png :: [[https://commons.wikimedia.org/wiki/File:Shadowsocks_logo.png][Apache 2.0]] - static/themes/default/icons/syncthing.png :: [[https://github.com/syncthing/syncthing/][Mozilla Public License Version 2.0]] - static/themes/default/icons/tahoe.png :: [[https://github.com/thekishanraval/Logos][GPLv3+]] - static/themes/default/icons/transmission.png :: [[https://transmissionbt.com/][GPL]] diff --git a/actions/shadowsocks b/actions/shadowsocks new file mode 100755 index 000000000..bfbb8e67a --- /dev/null +++ b/actions/shadowsocks @@ -0,0 +1,97 @@ +#!/usr/bin/python3 +# -*- mode: python -*- +# +# This file is part of Plinth. +# +# 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 . +# +""" +Helper script for configuring Shadowsocks. +""" + +import argparse +import json +import sys + +from plinth import action_utils +from plinth.modules import shadowsocks +from plinth.modules.shadowsocks.views import SHADOWSOCKS_CONFIG + + +def parse_arguments(): + """Return parsed command line arguments as dictionary.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') + + subparsers.add_parser('setup', + help='Perform initial setup steps') + subparsers.add_parser('enable', + help='Enable Shadowsocks client socks5 proxy') + subparsers.add_parser('disable', + help='Disable Shadowsocks client socks5 proxy') + 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.""" + # Only client socks5 proxy is supported for now. Disable the + # server component. + action_utils.service_disable('shadowsocks-libev') + + +def subcommand_enable(_): + """Enable Shadowsocks client socks5 proxy.""" + action_utils.service_enable(shadowsocks.managed_services[0]) + + +def subcommand_disable(_): + """Disable Shadowsocks client socks5 proxy.""" + action_utils.service_disable(shadowsocks.managed_services[0]) + + +def subcommand_merge_config(arguments): + """Configure Shadowsocks.""" + config = sys.stdin.read() + config = json.loads(config) + + try: + current_config = open(SHADOWSOCKS_CONFIG, 'r').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, 'w').write(new_config) + + action_utils.service_reload(shadowsocks.managed_services[0]) + + +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() diff --git a/data/etc/plinth/modules-enabled/shadowsocks b/data/etc/plinth/modules-enabled/shadowsocks new file mode 100644 index 000000000..99f3b05e1 --- /dev/null +++ b/data/etc/plinth/modules-enabled/shadowsocks @@ -0,0 +1 @@ +plinth.modules.shadowsocks diff --git a/data/usr/lib/firewalld/services/shadowsocks-local-plinth.xml b/data/usr/lib/firewalld/services/shadowsocks-local-plinth.xml new file mode 100644 index 000000000..93e10f2be --- /dev/null +++ b/data/usr/lib/firewalld/services/shadowsocks-local-plinth.xml @@ -0,0 +1,6 @@ + + + Shadowsocks client socks5 proxy + Shadowsocks is a lightweight and secure socks5 proxy, designed to protect your Internet traffic. Enable this service if you are running a Shadowsocks client, and want to provide socks5 proxy. + + diff --git a/plinth/modules/shadowsocks/__init__.py b/plinth/modules/shadowsocks/__init__.py new file mode 100644 index 000000000..5164d880e --- /dev/null +++ b/plinth/modules/shadowsocks/__init__.py @@ -0,0 +1,126 @@ +# +# This file is part of Plinth. +# +# 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 . +# + +""" +Plinth module to configure Shadowsocks. +""" + +from django.urls import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from plinth import actions +from plinth import action_utils +from plinth import frontpage +from plinth import service as service_module +from plinth.menu import main_menu + + +version = 1 + +name = _('Shadowsocks') + +short_description = _('Socks5 Proxy') + +service = None + +managed_services = ['shadowsocks-libev-local@freedombox'] + +managed_packages = ['shadowsocks-libev'] + +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.'), + _('Your FreedomBox can run a Shadowsocks client, that can connect ' + 'to a Shadowsocks server. The FreedomBox will also run a socks5 ' + 'server. Local devices can connect to the socks5 server, and ' + 'their data will be encrypted and proxied through the Shadowsocks ' + 'server.'), +] + + +def init(): + """Intialize the module.""" + menu = main_menu.get('apps') + menu.add_urlname(name, 'glyphicon-send', 'shadowsocks:index', + short_description) + + global service + setup_helper = globals()['setup_helper'] + if setup_helper.get_state() != 'needs-setup': + service = service_module.Service( + 'shadowsocks', name, + ports=['shadowsocks-local-plinth'], is_external=False, + is_enabled=is_enabled, is_running=is_running, + enable=enable, disable=disable) + + if service.is_enabled(): + add_shortcut() + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(managed_packages) + helper.call('post', actions.superuser_run, 'shadowsocks', ['setup']) + global service + if service is None: + service = service_module.Service( + 'shadowsocks', name, + ports=['shadowsocks-local-plinth'], is_external=False, + is_enabled=is_enabled, is_running=is_running, + enable=enable, disable=disable) + + +def add_shortcut(): + """Helper method to add a shortcut to the frontpage.""" + frontpage.add_shortcut('shadowsocks', name, + short_description=short_description, + details=description, + configure_url=reverse_lazy('shadowsocks:index'), + login_required=False) + + +def is_enabled(): + """Return whether service is enabled.""" + return action_utils.service_is_enabled(managed_services[0]) + + +def is_running(): + """Return whether service is running.""" + return action_utils.service_is_running(managed_services[0]) + + +def enable(): + """Enable service.""" + actions.superuser_run('shadowsocks', ['enable']) + add_shortcut() + + +def disable(): + """Disable service.""" + actions.superuser_run('shadowsocks', ['disable']) + frontpage.remove_shortcut('shadowsocks') + + +def diagnose(): + """Run diagnostics and return the results.""" + results = [] + + results.append(action_utils.diagnose_port_listening(1080, 'tcp4')) + results.append(action_utils.diagnose_port_listening(1080, 'tcp6')) + + return results diff --git a/plinth/modules/shadowsocks/forms.py b/plinth/modules/shadowsocks/forms.py new file mode 100644 index 000000000..f6a2f1777 --- /dev/null +++ b/plinth/modules/shadowsocks/forms.py @@ -0,0 +1,61 @@ +# +# This file is part of Plinth. +# +# 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 . +# + +""" +Plinth module for configuring Shadowsocks. +""" + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from plinth.forms import ServiceForm + +METHODS = ['chacha20-ietf-poly1305', 'aes-256-gcm'] + + +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(TrimmedCharField, self).clean(value) + + +class ShadowsocksForm(ServiceForm): + """Shadowsocks configuration form""" + server = TrimmedCharField( + label=_('Server'), + help_text=_('Server hostname or IP address')) + + server_port = forms.IntegerField( + label=_('Server port'), + min_value=0, + max_value=65535, + help_text=_('Server port number')) + + password = forms.CharField( + label=_('Password'), + help_text=_('Password used to encrypt data. ' + 'Must match server password.')) + + method = forms.ChoiceField( + label=_('Method'), + choices=[(x, x) for x in METHODS], + help_text=_('Encryption method. Must match setting on server.')) diff --git a/plinth/modules/shadowsocks/tests/__init__.py b/plinth/modules/shadowsocks/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plinth/modules/shadowsocks/urls.py b/plinth/modules/shadowsocks/urls.py new file mode 100644 index 000000000..a4e2d5e71 --- /dev/null +++ b/plinth/modules/shadowsocks/urls.py @@ -0,0 +1,29 @@ +# +# This file is part of Plinth. +# +# 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 . +# + +""" +URLs for the Shadowsocks module. +""" + +from django.conf.urls import url + +from .views import ShadowsocksServiceView + +urlpatterns = [ + url(r'^apps/shadowsocks/$', ShadowsocksServiceView.as_view(), + name='index'), +] diff --git a/plinth/modules/shadowsocks/views.py b/plinth/modules/shadowsocks/views.py new file mode 100644 index 000000000..e861e0443 --- /dev/null +++ b/plinth/modules/shadowsocks/views.py @@ -0,0 +1,82 @@ +# +# This file is part of Plinth. +# +# 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 . +# + +""" +Plinth module for configuring Shadowsocks. +""" + +import json +from django.contrib import messages +from django.utils.translation import ugettext_lazy as _ + +from .forms import ShadowsocksForm +from plinth import actions +from plinth import views +from plinth.modules import shadowsocks + +SHADOWSOCKS_CONFIG = '/etc/shadowsocks-libev/freedombox.json' + + +class ShadowsocksServiceView(views.ServiceView): + """Configuration view for Shadowsocks local socks5 proxy.""" + service_id = 'shadowsocks' + diagnostics_module_name = 'shadowsocks' + form_class = ShadowsocksForm + description = shadowsocks.description + + def get_initial(self, *args, **kwargs): + """Get initial values for form.""" + try: + configuration = open(SHADOWSOCKS_CONFIG, 'r').read() + status = json.loads(configuration) + except (OSError, json.JSONDecodeError): + status = { + 'server': '', + 'server_port': 8388, + 'password': '', + 'method': 'chacha20-ietf-poly1305', + } + + status['is_enabled'] = self.service.is_enabled() + status['is_running'] = self.service.is_running() + + return status + + def form_valid(self, form): + """Configure Shadowsocks.""" + old_status = form.initial + new_status = form.cleaned_data + + if (old_status['server'] != new_status['server'] or + old_status['server_port'] != new_status['server_port'] or + old_status['password'] != new_status['password'] or + old_status['method'] != new_status['method']): + new_config = { + 'local_address': '::0', + 'local_port': 1080, + 'server': new_status['server'], + 'server_port': new_status['server_port'], + 'password': new_status['password'], + 'method': new_status['method'], + } + + actions.superuser_run( + 'shadowsocks', ['merge-config'], + input=json.dumps(new_config).encode()) + messages.success(self.request, _('Configuration updated')) + + return super().form_valid(form) diff --git a/static/themes/default/icons/shadowsocks.png b/static/themes/default/icons/shadowsocks.png new file mode 100644 index 0000000000000000000000000000000000000000..ef038ac138703a9aace2df15a7cbdae9d25aeea0 GIT binary patch literal 16487 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 Uw;|=he;@!ME2$(=BW4WxA7O8|tpET3 literal 0 HcmV?d00001