From 14745317d05e13286d1d524a791526634dc795e5 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 22 Jan 2017 20:51:16 +0800 Subject: [PATCH] Implement native mode without binary copying Plugins that support this mode only need to implement `NativePluginProvider.getExecutable`. --- .../com/github/shadowsocks/BaseService.scala | 2 +- .../shadowsocks/plugin/PluginManager.scala | 83 ++++++++++++------- .../shadowsocks/plugin/PluginContract.java | 10 +++ .../plugin/NativePluginProvider.scala | 20 ++++- 4 files changed, 81 insertions(+), 34 deletions(-) diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala index 8dfca684..3b3d2ccb 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -159,7 +159,7 @@ trait BaseService extends Service { Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES)) plugin = new PluginConfiguration(profile.plugin).selectedOptions - pluginPath = PluginManager.initPlugin(plugin.id) + pluginPath = PluginManager.init(plugin) } def startRunner(profile: Profile) { diff --git a/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala b/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala index 5be8ccfc..00a66e7d 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala @@ -5,6 +5,8 @@ import java.io.{File, FileNotFoundException, FileOutputStream, IOException} import android.content.pm.PackageManager import android.content.{BroadcastReceiver, ContentResolver, Intent} import android.net.Uri +import android.os.Bundle +import android.util.Log import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.utils.CloseUtils.autoClose import com.github.shadowsocks.utils.{Commandline, IOUtils} @@ -41,11 +43,11 @@ object PluginManager { // the following parts are meant to be used by :bg @throws[Throwable] - def initPlugin(id: String): String = { - if (id.isEmpty) return null + def init(options: PluginOptions): String = { + if (options.id.isEmpty) return null var throwable: Throwable = null - try initNativePlugin(id) match { + try initNative(options) match { case null => case path => return path } catch { @@ -56,44 +58,61 @@ object PluginManager { // add other plugin types here - throw if (throwable == null) - new FileNotFoundException(app.getString(com.github.shadowsocks.R.string.plugin_unknown, id)) else throwable + throw if (throwable == null) new FileNotFoundException( + app.getString(com.github.shadowsocks.R.string.plugin_unknown, options.id)) else throwable } @throws[IOException] @throws[IndexOutOfBoundsException] @throws[AssertionError] - private def initNativePlugin(id: String): String = { + private def initNative(options: PluginOptions): String = { val providers = app.getPackageManager.queryIntentContentProviders( - new Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(id)), 0) + new Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0) assert(providers.length == 1) - val builder = new Uri.Builder() + val uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(providers(0).providerInfo.authority) + .build() val cr = app.getContentResolver - val cursor = cr.query(builder.build(), Array(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), - null, null, null) - if (cursor != null) { - var initialized = false - val pluginDir = new File(app.getFilesDir, "plugin") - def entryNotFound() = throw new IndexOutOfBoundsException("Plugin entry binary not found") - if (!cursor.moveToFirst()) entryNotFound() - IOUtils.deleteRecursively(pluginDir) - if (!pluginDir.mkdirs()) throw new FileNotFoundException("Unable to create plugin directory") - val pluginDirPath = pluginDir.getAbsolutePath + '/' - val list = new ListBuffer[String] - do { - val path = cursor.getString(0) - val file = new File(pluginDir, path) - assert(file.getAbsolutePath.startsWith(pluginDirPath)) - autoClose(cr.openInputStream(builder.path(path).build()))(in => - autoClose(new FileOutputStream(file))(out => IOUtils.copy(in, out))) - list += Commandline.toString(Array("chmod", cursor.getString(1), file.getAbsolutePath)) - if (path == id) initialized = true - } while (cursor.moveToNext()) - if (!initialized) entryNotFound() - Shell.SH.run(list) - new File(pluginDir, id).getAbsolutePath - } else null + try initNativeFast(cr, options, uri) catch { + case t: Throwable => + t.printStackTrace() + Log.w("PluginManager", "Initializing native plugin fast mode failed. Falling back to slow mode.") + initNativeSlow(cr, options, uri) + } } + + private def initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String = { + val out = new Bundle() + out.putString(PluginContract.EXTRA_OPTIONS, options.id) + val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out).getString(PluginContract.EXTRA_ENTRY) + assert(new File(result).canExecute) + result + } + + private def initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String = + cr.query(uri, Array(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null) match { + case null => null + case cursor => + var initialized = false + val pluginDir = new File(app.getFilesDir, "plugin") + def entryNotFound() = throw new IndexOutOfBoundsException("Plugin entry binary not found") + if (!cursor.moveToFirst()) entryNotFound() + IOUtils.deleteRecursively(pluginDir) + if (!pluginDir.mkdirs()) throw new FileNotFoundException("Unable to create plugin directory") + val pluginDirPath = pluginDir.getAbsolutePath + '/' + val list = new ListBuffer[String] + do { + val path = cursor.getString(0) + val file = new File(pluginDir, path) + assert(file.getAbsolutePath.startsWith(pluginDirPath)) + autoClose(cr.openInputStream(uri.buildUpon().path(path).build()))(in => + autoClose(new FileOutputStream(file))(out => IOUtils.copy(in, out))) + list += Commandline.toString(Array("chmod", cursor.getString(1), file.getAbsolutePath)) + if (path == options.id) initialized = true + } while (cursor.moveToNext()) + if (!initialized) entryNotFound() + Shell.SH.run(list) + new File(pluginDir, options.id).getAbsolutePath + } } diff --git a/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java b/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java index 90e274dc..1bd3732f 100644 --- a/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java +++ b/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java @@ -30,6 +30,14 @@ public final class PluginContract { */ public static final String ACTION_HELP = "com.github.shadowsocks.plugin.ACTION_HELP"; + /** + * The lookup key for a string that provides the plugin entry binary. + * + * Example: "/data/data/com.github.shadowsocks.plugin.obfs_local/lib/libobfs-local.so" + * + * Constant Value: "com.github.shadowsocks.plugin.EXTRA_ENTRY" + */ + public static final String EXTRA_ENTRY = "com.github.shadowsocks.plugin.EXTRA_ENTRY"; /** * The lookup key for a string that provides the whole command as a string. * @@ -61,6 +69,8 @@ public final class PluginContract { */ public static final String METADATA_KEY_DEFAULT_CONFIG = "com.github.shadowsocks.plugin.default_config"; + static final String METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable"; + /** * Relative to the file to be copied. This column is required. * diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala index 5c1357e1..982c7f37 100644 --- a/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala +++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala @@ -3,7 +3,7 @@ package com.github.shadowsocks.plugin import android.content.{ContentProvider, ContentValues} import android.database.{Cursor, MatrixCursor} import android.net.Uri -import android.os.ParcelFileDescriptor +import android.os.{Bundle, ParcelFileDescriptor} /** * Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required @@ -52,12 +52,30 @@ abstract class NativePluginProvider extends ContentProvider { 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. + */ + def getExecutable: String = throw new UnsupportedOperationException + def openFile(uri: Uri): ParcelFileDescriptor override def openFile(uri: Uri, mode: String): ParcelFileDescriptor = { assert(mode == "r") openFile(uri) } + override def call(method: String, arg: String, extras: Bundle): Bundle = method match { + case PluginContract.METHOD_GET_EXECUTABLE => + val out = new Bundle() + out.putString(PluginContract.EXTRA_ENTRY, getExecutable) + out + case _ => super.call(method, arg, extras) + } + // Methods that should not be used override def update(uri: Uri, values: ContentValues, selection: String, selectionArgs: Array[String]): Int = ??? override def insert(uri: Uri, values: ContentValues): Uri = ???