Skip to content

Commit

Permalink
Add shadowsocks-switch@Klavionik (linuxmint#5029)
Browse files Browse the repository at this point in the history
  • Loading branch information
Klavionik authored Sep 12, 2023
1 parent 22ee9a1 commit 6a7dea1
Show file tree
Hide file tree
Showing 12 changed files with 1,096 additions and 0 deletions.
39 changes: 39 additions & 0 deletions shadowsocks-switch@Klavionik/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Shadowsocks Switch
Shadowsocks Switch simplifies enabling and disabling Shadowsocks.

## Requirements
This applet acts as a wrapper around `ss-local` utility and therefore requires
`shadowsocks-libev` to be present. Run `apt-get install shadowsocks-libev` to install
it before using this applet.

## Rationale
Shadowsocks is a secure proxy that is made to bypass internet censorship, so the use
case is a lot like we use VPN nowadays. While other VPN protocols have Network Manager
plugins that add convenient GUI to quickly enable/disable the VPN connection, Shadowsocks
doesn't have one.

I'm using Shadowsocks myself, so I wanted a simple and convenient UI to quickly
enable/disable the Shadowsocks connection.

## How it works
Upon installation this applet adds an icon with a pop-up toggle in the system tray.

When you switch the proxy on, the applet does the following.

1. Reads and saves your current system proxy settings (proxy mode, SOCKS5 host and port).
2. Runs `ss-local -c <config>` in background (`<config>` defaults to
`/etc/shadowsocks-libev/config.json`).
3. Sets your system proxy mode to `Manual`, SOCKS5 host to `localhost` and SOCKS5 port
to the `local_port` value from `<config>`.

When you are done using Shadowsocks, you switch it off and the applet restores your
previous system proxy settings and stops the proxy.

## Settings
The applet uses a Shadowsocks configuration file to discover the local proxy port. The
default path to the configuration file is `/etc/shadowsocks-libev/config.json`, but
you can point the applet to any other configuration file via applet settings (click on
the cog icon in the Applets window).

## Credits
Shadowsocks tray icon is based on [Papirus Icon Theme](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme).

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Shadowsocks Switch
Shadowsocks Switch simplifies enabling and disabling Shadowsocks.

## Requirements
This applet acts as a wrapper around `ss-local` utility and therefore requires
`shadowsocks-libev` to be present. Run `apt-get install shadowsocks-libev` to install
it before using this applet.

## Rationale
Shadowsocks is a secure proxy that is made to bypass internet censorship, so the use
case is a lot like we use VPN nowadays. While other VPN protocols have Network Manager
plugins that add convenient GUI to quickly enable/disable the VPN connection, Shadowsocks
doesn't have one.

I'm using Shadowsocks myself, so I wanted a simple and convenient UI to quickly
enable/disable the Shadowsocks connection.

## How it works
Upon installation this applet adds an icon with a pop-up toggle in the system tray.

When you switch the proxy on, the applet does the following.

1. Reads and saves your current system proxy settings (proxy mode, SOCKS5 host and port).
2. Runs `ss-local -c <config>` in background (`<config>` defaults to
`/etc/shadowsocks-libev/config.json`).
3. Sets your system proxy mode to `Manual`, SOCKS5 host to `localhost` and SOCKS5 port
to the `local_port` value from `<config>`.

When you are done using Shadowsocks, you switch it off and the applet restores your
previous system proxy settings and stops the proxy.

## Settings
The applet uses a Shadowsocks configuration file to discover the local proxy port. The
default path to the configuration file is `/etc/shadowsocks-libev/config.json`, but
you can point the applet to any other configuration file via applet settings (click on
the cog icon in the Applets window).

## Credits
Shadowsocks tray icon is based on [Papirus Icon Theme](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme).
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
const Applet = imports.ui.applet
const Util = imports.misc.util
const PopupMenu = imports.ui.popupMenu
const Main = imports.ui.main
const Settings = imports.ui.settings
const GLib = imports.gi.GLib
const Gettext = imports.gettext

const UUID = "shadowsocks-switch@Klavionik"

Gettext.bindtextdomain(UUID, GLib.get_home_dir() + "/.local/share/locale")

function _(string) {
return Gettext.dgettext(UUID, string)
}

function logError(error) {
global.logError("\n[" + UUID + "]: " + error + "\n")
}

function fileURItoPath(uri) {
return uri.slice(7) // Remove file:// scheme.
}

function notify(msg) {
const title = _("Shadowsocks Switch")
Main.notify(title, msg)
}

class ShadowsocksAppletMenu extends Applet.AppletPopupMenu {
constructor(launcher, orientation) {
super(launcher, orientation)
this.toggleRef = this._makeToggle()
this.addMenuItem(this.toggleRef)
}

_makeToggle() {
const toggle = new PopupMenu.PopupSwitchMenuItem(_("Enable Shadowsocks"), false)
toggle.connect("toggled", () => (this.emit("toggle-proxy")))
return toggle
}
}

class ShadowsocksAppletError extends Error {}

class ShadowsocksApplet extends Applet.IconApplet {
constructor(metadata, orientation, panel_height, instance_id) {
super(orientation, panel_height, instance_id)
this._metadata = metadata
this._ssConfigFileURI = ""
this._host = "localhost"
this._port = -1
this._savedMode = ""
this._savedHost = ""
this._savedPort = -1
this._clientPid = -1
this._connected = false
this._settingsProvider = undefined

this._configureApplet()
this.menu = this._createMenu(orientation)
}

_configureApplet() {
this.set_applet_icon_path(this._metadata.path + "/icon.png")
this.set_applet_tooltip(_("Toggle Shadowsocks proxy"))

this._settingsProvider = new Settings.AppletSettings(this, this._metadata.uuid)
this._settingsProvider.bindProperty(
Settings.BindingDirection.IN,
"config",
"_ssConfigFileURI",
() => {}
)
}

_configureConnection() {
const {port} = this._loadConfig()
this._port = port
}

_createMenu(orientation) {
const menuManager = new PopupMenu.PopupMenuManager(this)
const menu = new ShadowsocksAppletMenu(this, orientation)

menu.connect("toggle-proxy", this._onToggleProxy.bind(this))
menuManager.addMenu(menu)
return menu
}

_onToggleProxy() {
this._connected ? this._disconnect() : this._connect()
}

_loadConfig() {
let content

try {
const [_, buffer] = GLib.file_get_contents(fileURItoPath(this._ssConfigFileURI))
content = buffer
} catch (e) {
const msg = _(`Cannot read Shadowsocks config.`)
notify(`${msg} ${String(e)}`)
throw new ShadowsocksAppletError(msg)
}

const config = JSON.parse(content)

if (!config.hasOwnProperty("local_port")) {
const msg = _("Shadowsocks config is missing local port.")
notify(msg)
throw new ShadowsocksAppletError(msg)
}

return {port: config.local_port}
}

_connect() {
try {
this._configureConnection()
this._saveProxySettings()
this._clientPid = this._runClient()
this._setupProxy()
} catch (e) {
this.menu.toggleRef.setToggleState(false)
this._restoreProxySettings()

if (this._clientPid !== -1) {
this._stopClient()
}

logError(String(e))
throw e
}

this._connected = true
this.set_applet_icon_path(this._metadata.path + "/icon-connected.png")
notify(_("Connection established."))
}

_runClient() {
const cmd = `ss-local -c ${fileURItoPath(this._ssConfigFileURI)}`
return Util.trySpawnCommandLine(cmd)
}

_stopClient() {
const cmd = `kill -s SIGINT ${this._clientPid}`
Util.spawnCommandLine(cmd)
this._clientPid = -1
}

_saveProxySettings() {
Util.spawnCommandLineAsyncIO("gsettings get org.gnome.system.proxy mode", (stdout) => {
this._savedMode = stdout
})

Util.spawnCommandLineAsyncIO("gsettings get org.gnome.system.proxy.socks host", (stdout) => {
this._savedHost = stdout
})

Util.spawnCommandLineAsyncIO("gsettings get org.gnome.system.proxy.socks port", (stdout) => {
this._savedPort = stdout
})
}

_restoreProxySettings() {
const restoreMode = `gsettings set org.gnome.system.proxy mode ${this._savedMode}`
const restoreHost = `gsettings set org.gnome.system.proxy.socks host ${this._savedHost}`
const restorePort = `gsettings set org.gnome.system.proxy.socks port ${this._savedPort}`

const cmd = `sh -c "${restoreMode} ${restoreHost} ${restorePort}"`
Util.trySpawnCommandLine(cmd)
}

_setupProxy() {
const setMode = "gsettings set org.gnome.system.proxy mode 'manual'"
const setHost = `gsettings set org.gnome.system.proxy.socks host ${this._host}`
const setPort = `gsettings set org.gnome.system.proxy.socks port ${this._port}`

const cmd = `sh -c "${setMode}; ${setHost}; ${setPort}"`
Util.trySpawnCommandLine(cmd)
}

_disconnect() {
this._restoreProxySettings()
this._stopClient()

this._connected = false
notify(_("Disconnected."))
this.set_applet_icon_path(this._metadata.path + "/icon.png")
}

on_applet_clicked() {
this.menu.toggle()
}

on_applet_removed_from_panel(deleteConfig) {
if (this._connected) {
this._disconnect()
}
}
}


function main(metadata, orientation, panel_height, instance_id) {
return new ShadowsocksApplet(metadata, orientation, panel_height, instance_id)
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"uuid": "shadowsocks-switch@Klavionik",
"name": "Shadowsocks Switch",
"description": "Quickly toggle Shadowsocks proxy from the system tray"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-12 00:22+0300\n"
"PO-Revision-Date: 2023-09-12 00:26+0300\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.3.2\n"

#. metadata.json->name
#: applet.js:26
msgid "Shadowsocks Switch"
msgstr "Переключатель Shadowsocks"

#: applet.js:38
msgid "Enable Shadowsocks"
msgstr "Включить Shadowsocks"

#: applet.js:66
msgid "Toggle Shadowsocks proxy"
msgstr "Управление Shadowsocks"

#: applet.js:102
msgid "Cannot read Shadowsocks config."
msgstr "Невозможно прочитать конфигурацию Shadowsocks."

#: applet.js:110
msgid "Shadowsocks config is missing local port."
msgstr "В конфигурации Shadowsocks отсутствует локальный порт."

#: applet.js:138
msgid "Connection established."
msgstr "Соединение установлено."

#: applet.js:189
msgid "Disconnected."
msgstr "Соединение разорвано."

#. metadata.json->description
msgid "Quickly toggle Shadowsocks proxy from the system tray"
msgstr "Быстрое управление Shadowsocks из системной панели"

#. settings-schema.json->section1->description
msgid "General"
msgstr "Общие"

#. settings-schema.json->config->description
msgid "Shadowsocks configuration"
msgstr "Конфигурация Shadowsocks"
Loading

0 comments on commit 6a7dea1

Please sign in to comment.