shadowsocks: Add shadowsocks client with socks5 proxy

Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
James Valleroy 2017-11-11 16:14:27 -05:00 committed by Sunil Mohan Adapa
parent bf1664aa9d
commit 6bf4eb1483
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
10 changed files with 403 additions and 0 deletions

View File

@ -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]]

97
actions/shadowsocks Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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()

View File

@ -0,0 +1 @@
plinth.modules.shadowsocks

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>Shadowsocks client socks5 proxy</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 client, and want to provide socks5 proxy.</description>
<port protocol="tcp" port="1080"/>
</service>

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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.'))

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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'),
]

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB