*
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU 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 General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package org.amnezia.vpn.shadowsocks.plugin
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.os.Bundle
+import android.os.ParcelFileDescriptor
+import androidx.core.os.bundleOf
+
+/**
+ * Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required
+ * to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this
+ * class, implement the abstract methods, and add it to your manifest like this:
+ *
+ * <manifest>
+ * ...
+ * <application>
+ * ...
+ * <provider android:name="com.kyle.shadowsocks.$PLUGIN_ID.BinaryProvider"
+ * android:authorities="com.kyle.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider">
+ * <intent-filter>
+ * <category android:name="com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" />
+ * </intent-filter>
+ * </provider>
+ * ...
+ * </application>
+ *</manifest>
+ */
+abstract class NativePluginProvider : ContentProvider() {
+ override fun getType(p0: Uri): String = "application/x-elf"
+
+ override fun onCreate(): Boolean = true
+
+ /**
+ * Provide all files needed for native plugin.
+ *
+ * @param provider A helper object to use to add files.
+ */
+ protected abstract fun populateFiles(provider: PathProvider)
+
+ override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?,
+ sortOrder: String?): Cursor {
+ check(selection == null && selectionArgs == null && sortOrder == null)
+ val result = MatrixCursor(projection)
+ populateFiles(PathProvider(uri, result))
+ return result
+ }
+
+ /**
+ * Returns executable entry absolute path. This is used if plugin is sharing UID with the host.
+ *
+ * Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the
+ * default behavior.
+ *
+ * @return Absolute path for executable entry.
+ */
+ open fun getExecutable(): String = throw UnsupportedOperationException()
+
+ abstract fun openFile(uri: Uri?): ParcelFileDescriptor
+ override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
+ check(mode == "r")
+ return openFile(uri)
+ }
+
+ override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = when (method) {
+ PluginContract.METHOD_GET_EXECUTABLE -> bundleOf(Pair(PluginContract.EXTRA_ENTRY, getExecutable()))
+ else -> super.call(method, arg, extras)
+ }
+
+ // Methods that should not be used
+ override fun insert(p0: Uri, p1: ContentValues?): Uri = throw UnsupportedOperationException()
+ override fun update(p0: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int =
+ throw UnsupportedOperationException()
+ override fun delete(uri: Uri, p1: String?, p2: Array?): Int = throw UnsupportedOperationException()
+}
diff --git a/client/android/src/org/amnezia/vpn/shadowsocks/core/widget/PathProvider.kt b/client/android/src/org/amnezia/vpn/shadowsocks/core/widget/PathProvider.kt
new file mode 100644
index 00000000..f7ebe1e4
--- /dev/null
+++ b/client/android/src/org/amnezia/vpn/shadowsocks/core/widget/PathProvider.kt
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * *
+ * Copyright (C) 2017 by Max Lv *
+ * Copyright (C) 2017 by Mygod Studio *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU 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 General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package org.amnezia.vpn.shadowsocks.plugin
+
+import android.database.MatrixCursor
+import android.net.Uri
+import java.io.File
+
+/**
+ * Helper class to provide relative paths of files to copy.
+ */
+class PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) {
+ private val basePath = baseUri.path?.trim('/') ?: ""
+
+ fun addPath(path: String, mode: Int = 0b110100100): PathProvider {
+ val trimmed = path.trim('/')
+ if (trimmed.startsWith(basePath)) cursor.newRow()
+ .add(PluginContract.COLUMN_PATH, trimmed)
+ .add(PluginContract.COLUMN_MODE, mode)
+ return this
+ }
+ fun addTo(file: File, to: String = "", mode: Int = 0b110100100): PathProvider {
+ var sub = to + file.name
+ if (basePath.startsWith(sub)) if (file.isDirectory) {
+ sub += '/'
+ file.listFiles().forEach { addTo(it, sub, mode) }
+ } else addPath(sub, mode)
+ return this
+ }
+ fun addAt(file: File, at: String = "", mode: Int = 0b110100100): PathProvider {
+ if (basePath.startsWith(at))
+ if (file.isDirectory) file.listFiles().forEach { addTo(it, at, mode) } else addPath(at, mode)
+ return this
+ }
+}
diff --git a/client/android/src/org/amnezia/vpn/shadowsocks/core/widget/PluginContract.kt b/client/android/src/org/amnezia/vpn/shadowsocks/core/widget/PluginContract.kt
new file mode 100644
index 00000000..fd980810
--- /dev/null
+++ b/client/android/src/org/amnezia/vpn/shadowsocks/core/widget/PluginContract.kt
@@ -0,0 +1,118 @@
+/*******************************************************************************
+ * *
+ * Copyright (C) 2017 by Max Lv *
+ * Copyright (C) 2017 by Mygod Studio *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU 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 General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package org.amnezia.vpn.shadowsocks.plugin
+
+/**
+ * The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc.
+ *
+ * This class is written in Java to keep Java interoperability.
+ */
+object PluginContract {
+ /**
+ * ContentProvider Action: Used for NativePluginProvider.
+ *
+ * Constant Value: "com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
+ */
+ const val ACTION_NATIVE_PLUGIN = "com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
+
+ /**
+ * Activity Action: Used for ConfigurationActivity.
+ *
+ * Constant Value: "com.kyle.shadowsocks.plugin.ACTION_CONFIGURE"
+ */
+ const val ACTION_CONFIGURE = "com.kyle.shadowsocks.plugin.ACTION_CONFIGURE"
+ /**
+ * Activity Action: Used for HelpActivity or HelpCallback.
+ *
+ * Constant Value: "com.kyle.shadowsocks.plugin.ACTION_HELP"
+ */
+ const val ACTION_HELP = "com.kyle.shadowsocks.plugin.ACTION_HELP"
+
+ /**
+ * The lookup key for a string that provides the plugin entry binary.
+ *
+ * Example: "/data/data/com.kyle.shadowsocks.plugin.obfs_local/lib/libobfs-local.so"
+ *
+ * Constant Value: "com.kyle.shadowsocks.plugin.EXTRA_ENTRY"
+ */
+ const val EXTRA_ENTRY = "com.kyle.shadowsocks.plugin.EXTRA_ENTRY"
+ /**
+ * The lookup key for a string that provides the options as a string.
+ *
+ * Example: "obfs=http;obfs-host=www.baidu.com"
+ *
+ * Constant Value: "com.kyle.shadowsocks.plugin.EXTRA_OPTIONS"
+ */
+ const val EXTRA_OPTIONS = "com.kyle.shadowsocks.plugin.EXTRA_OPTIONS"
+ /**
+ * The lookup key for a CharSequence that provides user relevant help message.
+ *
+ * Example: "obfs=|tls> Enable obfuscating: HTTP or TLS (Experimental).
+ * obfs-host= Hostname for obfuscating (Experimental)."
+ *
+ * Constant Value: "com.kyle.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
+ */
+ const val EXTRA_HELP_MESSAGE = "com.kyle.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
+
+ /**
+ * The metadata key to retrieve plugin id. Required for plugins.
+ *
+ * Constant Value: "com.kyle.shadowsocks.plugin.id"
+ */
+ const val METADATA_KEY_ID = "com.kyle.shadowsocks.plugin.id"
+ /**
+ * The metadata key to retrieve default configuration. Default value is empty.
+ *
+ * Constant Value: "com.kyle.shadowsocks.plugin.default_config"
+ */
+ const val METADATA_KEY_DEFAULT_CONFIG = "com.kyle.shadowsocks.plugin.default_config"
+
+ const val METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable"
+
+ /** ConfigurationActivity result: fallback to manual edit mode. */
+ const val RESULT_FALLBACK = 1
+
+ /**
+ * Relative to the file to be copied. This column is required.
+ *
+ * Example: "kcptun", "doc/help.txt"
+ *
+ * Type: String
+ */
+ const val COLUMN_PATH = "path"
+ /**
+ * File mode bits. Default value is "644".
+ *
+ * Example: "755"
+ *
+ * Type: String
+ */
+ const val COLUMN_MODE = "mode"
+
+ /**
+ * The scheme for general plugin actions.
+ */
+ const val SCHEME = "plugin"
+ /**
+ * The authority for general plugin actions.
+ */
+ const val AUTHORITY = "com.kyle.shadowsocks"
+}
diff --git a/client/android/src/org/amnezia/vpn/shadowsocks/core/widget/PluginOptions.kt b/client/android/src/org/amnezia/vpn/shadowsocks/core/widget/PluginOptions.kt
new file mode 100644
index 00000000..a11ffdd0
--- /dev/null
+++ b/client/android/src/org/amnezia/vpn/shadowsocks/core/widget/PluginOptions.kt
@@ -0,0 +1,110 @@
+/*******************************************************************************
+ * *
+ * Copyright (C) 2017 by Max Lv *
+ * Copyright (C) 2017 by Mygod Studio *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU 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 General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package org.amnezia.vpn.shadowsocks.plugin
+
+import java.util.*
+
+/**
+ * Helper class for processing plugin options.
+ *
+ * Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
+ */
+class PluginOptions : HashMap {
+ var id = ""
+
+ constructor() : super()
+ constructor(initialCapacity: Int) : super(initialCapacity)
+ constructor(initialCapacity: Int, loadFactor: Float) : super(initialCapacity, loadFactor)
+
+ private constructor(options: String?, parseId: Boolean) : this() {
+ @Suppress("NAME_SHADOWING")
+ var parseId = parseId
+ if (options.isNullOrEmpty()) return
+ check(options.all { !it.isISOControl() }) { "No control characters allowed." }
+ val tokenizer = StringTokenizer("$options;", "\\=;", true)
+ val current = StringBuilder()
+ var key: String? = null
+ while (tokenizer.hasMoreTokens()) when (val nextToken = tokenizer.nextToken()) {
+ "\\" -> current.append(tokenizer.nextToken())
+ "=" -> if (key == null) {
+ key = current.toString()
+ current.setLength(0)
+ } else current.append(nextToken)
+ ";" -> {
+ if (key != null) {
+ put(key, current.toString())
+ key = null
+ } else if (current.isNotEmpty())
+ if (parseId) id = current.toString() else put(current.toString(), null)
+ current.setLength(0)
+ parseId = false
+ }
+ else -> current.append(nextToken)
+ }
+ }
+
+ constructor(options: String?) : this(options, true)
+ constructor(id: String, options: String?) : this(options, false) {
+ this.id = id
+ }
+
+ /**
+ * Put but if value is null or default, the entry is deleted.
+ *
+ * @return Old value before put.
+ */
+ fun putWithDefault(key: String, value: String?, default: String? = null) =
+ if (value == null || value == default) remove(key) else put(key, value)
+
+ private fun append(result: StringBuilder, str: String) = (0 until str.length)
+ .map { str[it] }
+ .forEach {
+ when (it) {
+ '\\', '=', ';' -> {
+ result.append('\\') // intentionally no break
+ result.append(it)
+ }
+ else -> result.append(it)
+ }
+ }
+
+ fun toString(trimId: Boolean): String {
+ val result = StringBuilder()
+ if (!trimId) if (id.isEmpty()) return "" else append(result, id)
+ for ((key, value) in entries) {
+ if (result.isNotEmpty()) result.append(';')
+ append(result, key)
+ if (value != null) {
+ result.append('=')
+ append(result, value)
+ }
+ }
+ return result.toString()
+ }
+
+ override fun toString(): String = toString(true)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ return javaClass == other?.javaClass && super.equals(other) && id == (other as PluginOptions).id
+ }
+ override fun hashCode(): Int = Objects.hash(super.hashCode(), id)
+}
diff --git a/client/client.pro b/client/client.pro
index 6a8dd84f..644adba8 100644
--- a/client/client.pro
+++ b/client/client.pro
@@ -273,6 +273,7 @@ android {
ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libss-local.so
ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libtun2socks.so
+ ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libredsocks.so
}
}
@@ -370,5 +371,7 @@ ios {
# %{sourceDir}/client/ios/xcode_patcher.rb %{buildDir}/client/AmneziaVPN.xcodeproj 2.0 2.0.0 ios 1
}
+DISTFILES +=
+
diff --git a/client/containers/containers_defs.cpp b/client/containers/containers_defs.cpp
index 48d71d3f..63dd83fe 100644
--- a/client/containers/containers_defs.cpp
+++ b/client/containers/containers_defs.cpp
@@ -158,6 +158,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
switch (c) {
case DockerContainer::WireGuard: return true;
case DockerContainer::OpenVpn: return true;
+ case DockerContainer::ShadowSocks: return true;
default: return false;
}