Commit 14745317 authored by Mygod's avatar Mygod

Implement native mode without binary copying

Plugins that support this mode only need to implement `NativePluginProvider.getExecutable`.
parent 9e6948ca
...@@ -159,7 +159,7 @@ trait BaseService extends Service { ...@@ -159,7 +159,7 @@ trait BaseService extends Service {
Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES)) Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES))
plugin = new PluginConfiguration(profile.plugin).selectedOptions plugin = new PluginConfiguration(profile.plugin).selectedOptions
pluginPath = PluginManager.initPlugin(plugin.id) pluginPath = PluginManager.init(plugin)
} }
def startRunner(profile: Profile) { def startRunner(profile: Profile) {
......
...@@ -5,6 +5,8 @@ import java.io.{File, FileNotFoundException, FileOutputStream, IOException} ...@@ -5,6 +5,8 @@ import java.io.{File, FileNotFoundException, FileOutputStream, IOException}
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.{BroadcastReceiver, ContentResolver, Intent} import android.content.{BroadcastReceiver, ContentResolver, Intent}
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.CloseUtils.autoClose import com.github.shadowsocks.utils.CloseUtils.autoClose
import com.github.shadowsocks.utils.{Commandline, IOUtils} import com.github.shadowsocks.utils.{Commandline, IOUtils}
...@@ -41,11 +43,11 @@ object PluginManager { ...@@ -41,11 +43,11 @@ object PluginManager {
// the following parts are meant to be used by :bg // the following parts are meant to be used by :bg
@throws[Throwable] @throws[Throwable]
def initPlugin(id: String): String = { def init(options: PluginOptions): String = {
if (id.isEmpty) return null if (options.id.isEmpty) return null
var throwable: Throwable = null var throwable: Throwable = null
try initNativePlugin(id) match { try initNative(options) match {
case null => case null =>
case path => return path case path => return path
} catch { } catch {
...@@ -56,44 +58,61 @@ object PluginManager { ...@@ -56,44 +58,61 @@ object PluginManager {
// add other plugin types here // add other plugin types here
throw if (throwable == null) throw if (throwable == null) new FileNotFoundException(
new FileNotFoundException(app.getString(com.github.shadowsocks.R.string.plugin_unknown, id)) else throwable app.getString(com.github.shadowsocks.R.string.plugin_unknown, options.id)) else throwable
} }
@throws[IOException] @throws[IOException]
@throws[IndexOutOfBoundsException] @throws[IndexOutOfBoundsException]
@throws[AssertionError] @throws[AssertionError]
private def initNativePlugin(id: String): String = { private def initNative(options: PluginOptions): String = {
val providers = app.getPackageManager.queryIntentContentProviders( 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) assert(providers.length == 1)
val builder = new Uri.Builder() val uri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT) .scheme(ContentResolver.SCHEME_CONTENT)
.authority(providers(0).providerInfo.authority) .authority(providers(0).providerInfo.authority)
.build()
val cr = app.getContentResolver val cr = app.getContentResolver
val cursor = cr.query(builder.build(), Array(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), try initNativeFast(cr, options, uri) catch {
null, null, null) case t: Throwable =>
if (cursor != null) { t.printStackTrace()
var initialized = false Log.w("PluginManager", "Initializing native plugin fast mode failed. Falling back to slow mode.")
val pluginDir = new File(app.getFilesDir, "plugin") initNativeSlow(cr, options, uri)
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
} }
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
}
} }
...@@ -30,6 +30,14 @@ public final class PluginContract { ...@@ -30,6 +30,14 @@ public final class PluginContract {
*/ */
public static final String ACTION_HELP = "com.github.shadowsocks.plugin.ACTION_HELP"; 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. * The lookup key for a string that provides the whole command as a string.
* *
...@@ -61,6 +69,8 @@ public final class PluginContract { ...@@ -61,6 +69,8 @@ public final class PluginContract {
*/ */
public static final String METADATA_KEY_DEFAULT_CONFIG = "com.github.shadowsocks.plugin.default_config"; 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. * Relative to the file to be copied. This column is required.
* *
......
...@@ -3,7 +3,7 @@ package com.github.shadowsocks.plugin ...@@ -3,7 +3,7 @@ package com.github.shadowsocks.plugin
import android.content.{ContentProvider, ContentValues} import android.content.{ContentProvider, ContentValues}
import android.database.{Cursor, MatrixCursor} import android.database.{Cursor, MatrixCursor}
import android.net.Uri 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 * 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 { ...@@ -52,12 +52,30 @@ abstract class NativePluginProvider extends ContentProvider {
result 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 def openFile(uri: Uri): ParcelFileDescriptor
override def openFile(uri: Uri, mode: String): ParcelFileDescriptor = { override def openFile(uri: Uri, mode: String): ParcelFileDescriptor = {
assert(mode == "r") assert(mode == "r")
openFile(uri) 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 // Methods that should not be used
override def update(uri: Uri, values: ContentValues, selection: String, selectionArgs: Array[String]): Int = ??? override def update(uri: Uri, values: ContentValues, selection: String, selectionArgs: Array[String]): Int = ???
override def insert(uri: Uri, values: ContentValues): Uri = ??? override def insert(uri: Uri, values: ContentValues): Uri = ???
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment