From 0ffae32a4474d075f5af02e2b4a24be1ff54b80d Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 16 Jan 2017 22:27:41 +0800 Subject: [PATCH] Implement plugin interface in mobile app --- .../github/shadowsocks/utils/Commandline.java | 27 +++++--- .../com/github/shadowsocks/BaseService.scala | 27 +++++++- .../github/shadowsocks/GuardedProcess.scala | 9 +-- .../shadowsocks/ShadowsocksNatService.scala | 69 ++++--------------- .../shadowsocks/ShadowsocksVpnService.scala | 38 +++------- .../shadowsocks/plugin/PluginManager.scala | 56 ++++++++++++++- .../github/shadowsocks/utils/Constants.scala | 1 - .../github/shadowsocks/utils/IOUtils.scala | 8 ++- .../plugin/NativePluginProvider.scala | 4 +- 9 files changed, 136 insertions(+), 103 deletions(-) diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java b/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java index 112df2d5..e364c567 100644 --- a/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java +++ b/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java @@ -1,6 +1,7 @@ package com.github.shadowsocks.utils; import java.util.ArrayList; +import java.util.Arrays; import java.util.StringTokenizer; /** @@ -32,23 +33,21 @@ public final class Commandline { /** * Quote the parts of the given array in way that makes them * usable as command line arguments. - * @param line the list of arguments to quote. + * @param args the list of arguments to quote. * @return empty string for null or no command, else every argument split * by spaces and quoted by quoting rules. */ - public static String toString(String[] line) { + public static String toString(Iterable args) { // empty path return empty string - if (line == null || line.length == 0) { + if (args == null) { return ""; } // path containing one or more elements final StringBuilder result = new StringBuilder(); - for (int i = 0; i < line.length; i++) { - if (i > 0) { - result.append(' '); - } - for (int j = 0; j < line[i].length(); ++j) { - char ch = line[i].charAt(j); + for (String arg : args) { + if (result.length() > 0) result.append(' '); + for (int j = 0; j < arg.length(); ++j) { + char ch = arg.charAt(j); switch (ch) { case ' ': case '\\': case '"': case '\'': result.append('\\'); // intentionally no break default: result.append(ch); @@ -57,6 +56,16 @@ public final class Commandline { } return result.toString(); } + /** + * Quote the parts of the given array in way that makes them + * usable as command line arguments. + * @param args the list of arguments to quote. + * @return empty string for null or no command, else every argument split + * by spaces and quoted by quoting rules. + */ + public static String toString(String[] args) { + return toString(Arrays.asList(args)); // thanks to Java, arrays aren't iterable + } /** * Crack a command line. diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala index 59af31ae..ccc13390 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -24,7 +24,7 @@ import java.io.IOException import java.net.InetAddress import java.util import java.util.concurrent.TimeUnit -import java.util.{Timer, TimerTask} +import java.util.{Locale, Timer, TimerTask} import android.app.Service import android.content.{BroadcastReceiver, Context, Intent, IntentFilter} @@ -36,6 +36,7 @@ import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback} import com.github.shadowsocks.database.Profile +import com.github.shadowsocks.plugin.{PluginConfiguration, PluginManager, PluginOptions} import com.github.shadowsocks.utils._ import okhttp3.{Dns, FormBody, OkHttpClient, Request} @@ -46,6 +47,8 @@ trait BaseService extends Service { @volatile private var state = State.STOPPED @volatile protected var profile: Profile = _ + @volatile private var plugin: PluginOptions = _ + @volatile private var pluginPath: String = _ case class NameNotResolvedException() extends IOException case class NullConnectionException() extends NullPointerException @@ -149,8 +152,12 @@ trait BaseService extends Service { profile.password = proxy(2).trim profile.method = proxy(3).trim } + if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES)) + + plugin = new PluginConfiguration(profile.plugin).selectedOptions + pluginPath = PluginManager.initPlugin(plugin.id) } def startRunner(profile: Profile) { @@ -304,4 +311,22 @@ trait BaseService extends Service { case _: Exception => "exclude = " + default + ";" } } + + protected def buildShadowsocksConfig(file: String, localPortOffset: Int = 0): String = { + val builder = new StringBuilder( + """{"server": "%s", "server_port": %d, "local_port": %d, "password": "%s", "method": "%s"""" + .formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort + localPortOffset, + profile.password, profile.method)) + if (profile.auth) builder.append(""", "auth": true""") + if (pluginPath != null) { + builder.append(""", "plugin": """") + builder.append(pluginPath) + builder.append("""", "plugin_opts": """") + builder.append(plugin.toString) + builder.append('"') + } + builder.append('}') + IOUtils.writeString(file, builder.toString) + file + } } diff --git a/mobile/src/main/scala/com/github/shadowsocks/GuardedProcess.scala b/mobile/src/main/scala/com/github/shadowsocks/GuardedProcess.scala index bdbf77b6..ca117acb 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/GuardedProcess.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/GuardedProcess.scala @@ -26,6 +26,7 @@ import java.util.concurrent.Semaphore import android.util.Log import com.github.shadowsocks.utils.CloseUtils._ +import com.github.shadowsocks.utils.Commandline import scala.collection.JavaConversions._ import scala.collection.immutable.Stream @@ -57,7 +58,7 @@ class GuardedProcess(cmd: Seq[String]) { try { var callback: () => Unit = null while (!isDestroyed) { - Log.i(TAG, "start process: " + cmd) + Log.i(TAG, "start process: " + Commandline.toString(cmd)) val startTime = currentTimeMillis process = new ProcessBuilder(cmd).redirectErrorStream(true).start @@ -75,7 +76,7 @@ class GuardedProcess(cmd: Seq[String]) { isRestart = false } else { if (currentTimeMillis - startTime < 1000) { - Log.w(TAG, "process exit too fast, stop guard: " + cmd) + Log.w(TAG, "process exit too fast, stop guard: " + Commandline.toString(cmd)) isDestroyed = true } } @@ -83,11 +84,11 @@ class GuardedProcess(cmd: Seq[String]) { } } catch { case _: InterruptedException => - Log.i(TAG, "thread interrupt, destroy process: " + cmd) + Log.i(TAG, "thread interrupt, destroy process: " + Commandline.toString(cmd)) process.destroy() case e: IOException => ioException = e } finally semaphore.release() - }, "GuardThread-" + cmd) + }, "GuardThread-" + cmd.head) guardThread.start() semaphore.acquire() diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala index 48a4a366..fa29c7dc 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala @@ -53,21 +53,11 @@ class ShadowsocksNatService extends BaseService { var su: Shell.Interactive = _ def startShadowsocksDaemon() { - val conf = - ConfigUtils - .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort, - profile.password, profile.method, 600) - Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-nat.conf"))(p => { - p.println(conf) - }) - val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-local.so" , "-b" , "127.0.0.1" , "-t" , "600" , "-P", getApplicationInfo.dataDir - , "-c" , getApplicationInfo.dataDir + "/ss-local-nat.conf") - - if (profile.auth) cmd += "-A" + , "-c" , buildShadowsocksConfig(getApplicationInfo.dataDir + "/ss-local-nat.conf")) if (TcpFastOpen.sendEnabled) cmd += "--fast-open" @@ -76,55 +66,24 @@ class ShadowsocksNatService extends BaseService { cmd += Acl.getPath(profile.route) } - if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) + if (BuildConfig.DEBUG) Log.d(TAG, Commandline.toString(cmd)) sslocalProcess = new GuardedProcess(cmd).start() } def startDNSTunnel() { - if (profile.udpdns) { - val conf = ConfigUtils - .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort + 53, - profile.password, profile.method, 10) - Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf"))(p => { - p.println(conf) - }) - val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-tunnel.so" - , "-u" - , "-t" , "10" - , "-b" , "127.0.0.1" - , "-l" , (profile.localPort + 53).toString - , "-L" , if (profile.remoteDns == null) "8.8.8.8:53" else profile.remoteDns + ":53" - , "-P" , getApplicationInfo.dataDir - , "-c" , getApplicationInfo.dataDir + "/ss-tunnel-nat.conf") + val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-tunnel.so" + , "-t" , "10" + , "-b" , "127.0.0.1" + , "-l" , (profile.localPort + 53).toString + , "-L" , if (profile.remoteDns == null) "8.8.8.8:53" else profile.remoteDns + ":53" + , "-P", getApplicationInfo.dataDir + , "-c" , buildShadowsocksConfig(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf", 53)) - if (profile.auth) cmd += "-A" + if (profile.udpdns) cmd.append("-u") - if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) + if (BuildConfig.DEBUG) Log.d(TAG, Commandline.toString(cmd)) - sstunnelProcess = new GuardedProcess(cmd).start() - - } else { - val conf = - ConfigUtils - .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort + 63, - profile.password, profile.method, 10) - Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf"))(p => { - p.println(conf) - }) - val cmdBuf = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-tunnel.so" - , "-t" , "10" - , "-b" , "127.0.0.1" - , "-l" , (profile.localPort + 63).toString - , "-L" , if (profile.remoteDns == null) "8.8.8.8:53" else profile.remoteDns + ":53" - , "-P", getApplicationInfo.dataDir - , "-c" , getApplicationInfo.dataDir + "/ss-tunnel-nat.conf") - - if (profile.auth) cmdBuf += "-A" - - if (BuildConfig.DEBUG) Log.d(TAG, cmdBuf.mkString(" ")) - - sstunnelProcess = new GuardedProcess(cmdBuf).start() - } + sstunnelProcess = new GuardedProcess(cmd).start() } def startDnsDaemon() { @@ -150,7 +109,7 @@ class ShadowsocksNatService extends BaseService { }) val cmd = Array(getApplicationInfo.nativeLibraryDir + "/libpdnsd.so", "-c", getApplicationInfo.dataDir + "/pdnsd-nat.conf") - if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) + if (BuildConfig.DEBUG) Log.d(TAG, Commandline.toString(cmd)) pdnsdProcess = new GuardedProcess(cmd).start() } @@ -162,7 +121,7 @@ class ShadowsocksNatService extends BaseService { p.println(conf) }) - if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) + if (BuildConfig.DEBUG) Log.d(TAG, Commandline.toString(cmd)) redsocksProcess = new GuardedProcess(cmd).start() } diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala index 46b334e9..a9b8550a 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala @@ -30,10 +30,11 @@ import android.net.VpnService import android.os._ import android.util.Log import com.github.shadowsocks.ShadowsocksApplication.app -import com.github.shadowsocks.acl.{AclSyncJob, Acl} +import com.github.shadowsocks.acl.{Acl, AclSyncJob} import com.github.shadowsocks.database.Profile import com.github.shadowsocks.utils._ +import scala.collection.JavaConversions._ import scala.collection.mutable.ArrayBuffer class ShadowsocksVpnService extends VpnService with BaseService { @@ -162,21 +163,11 @@ class ShadowsocksVpnService extends VpnService with BaseService { } def startShadowsocksDaemon() { - val conf = - ConfigUtils - .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort, - profile.password, profile.method, 600) - Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-vpn.conf"))(p => { - p.println(conf) - }) - val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-local.so", "-V" , "-b", "127.0.0.1" , "-t", "600" , "-P", getApplicationInfo.dataDir - , "-c", getApplicationInfo.dataDir + "/ss-local-vpn.conf") - - if (profile.auth) cmd += "-A" + , "-c", buildShadowsocksConfig(getApplicationInfo.dataDir + "/ss-local-vpn.conf")) if (profile.udpdns) cmd += "-u" @@ -187,31 +178,20 @@ class ShadowsocksVpnService extends VpnService with BaseService { if (TcpFastOpen.sendEnabled) cmd += "--fast-open" - if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) + if (BuildConfig.DEBUG) Log.d(TAG, Commandline.toString(cmd)) sslocalProcess = new GuardedProcess(cmd).start() } def startDnsTunnel() { - val conf = - ConfigUtils - .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort + 63, - profile.password, profile.method, 10) - Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf"))(p => { - p.println(conf) - }) - val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-tunnel.so" + val cmd = Array[String](getApplicationInfo.nativeLibraryDir + "/libss-tunnel.so" , "-V" , "-t", "10" , "-b", "127.0.0.1" , "-L" , if (profile.remoteDns == null) "8.8.8.8:53" else profile.remoteDns + ":53" , "-P", getApplicationInfo.dataDir - , "-c", getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf") - - if (profile.auth) cmd += "-A" - - if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) - + , "-c", buildShadowsocksConfig(getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf", 53)) + if (BuildConfig.DEBUG) Log.d(TAG, Commandline.toString(cmd)) sstunnelProcess = new GuardedProcess(cmd).start() } @@ -236,7 +216,7 @@ class ShadowsocksVpnService extends VpnService with BaseService { }) val cmd = Array(getApplicationInfo.nativeLibraryDir + "/libpdnsd.so", "-c", getApplicationInfo.dataDir + "/pdnsd-vpn.conf") - if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) + if (BuildConfig.DEBUG) Log.d(TAG, Commandline.toString(cmd)) pdnsdProcess = new GuardedProcess(cmd).start() } @@ -310,7 +290,7 @@ class ShadowsocksVpnService extends VpnService with BaseService { cmd += ("--dnsgw", "%s:%d".formatLocal(Locale.ENGLISH, PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"), profile.localPort + 53)) - if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) + if (BuildConfig.DEBUG) Log.d(TAG, Commandline.toString(cmd)) tun2socksProcess = new GuardedProcess(cmd).start(() => sendFd(fd)) 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 e6c80095..568e964a 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala @@ -1,8 +1,13 @@ package com.github.shadowsocks.plugin +import java.io.{File, FileNotFoundException, FileOutputStream, IOException} + import android.content.pm.PackageManager -import android.content.{BroadcastReceiver, Intent} +import android.content.{BroadcastReceiver, ContentResolver, Intent} +import android.net.Uri import com.github.shadowsocks.ShadowsocksApplication.app +import com.github.shadowsocks.utils.CloseUtils.autoClose +import com.github.shadowsocks.utils.IOUtils import scala.collection.JavaConverters._ @@ -22,4 +27,53 @@ object PluginManager { }) cachedPlugins } + + // the following parts are meant to be used by :bg + @throws[Throwable] + def initPlugin(id: String): String = { + if (id.isEmpty) return null + var throwable: Throwable = null + + try initNativePlugin(id) match { + case null => + case path => return path + } catch { + case t: Throwable => if (throwable == null) throwable = t + } + + // add other plugin types here + + throw if (throwable == null) + new FileNotFoundException(app.getString(com.github.shadowsocks.R.string.plugin_unknown, id)) else throwable + } + + @throws[IOException] + @throws[IndexOutOfBoundsException] + @throws[AssertionError] + private def initNativePlugin(id: String): String = { + val builder = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(PluginInterface.getAuthority(id)) + val cr = app.getContentResolver + val cursor = cr.query(builder.build(), Array(PluginInterface.COLUMN_PATH), null, null, null) + if (cursor != null) { + var initialized = false + val pluginDir = new File(app.getFilesDir, "plugin") + if (cursor.moveToFirst()) { + IOUtils.deleteRecursively(pluginDir) + if (!pluginDir.mkdirs()) throw new FileNotFoundException("Unable to create plugin directory") + val pluginDirPath = pluginDir.getAbsolutePath + '/' + 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))) + if (path == id) initialized = true + } while (cursor.moveToNext()) + } + if (!initialized) throw new IndexOutOfBoundsException("Plugin entry binary not found") + new File(pluginDir, id).getAbsolutePath + } else null + } } diff --git a/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala index 2b2d1929..7d7bea16 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala @@ -31,7 +31,6 @@ object Executable { } object ConfigUtils { - val SHADOWSOCKS = "{\"server\": \"%s\", \"server_port\": %d, \"local_port\": %d, \"password\": \"%s\", \"method\":\"%s\", \"timeout\": %d}" val REDSOCKS = "base {\n" + " log_debug = off;\n" + " log_info = off;\n" + diff --git a/mobile/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala index ae04ef85..8ef7101c 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala @@ -21,7 +21,7 @@ package com.github.shadowsocks.utils import com.github.shadowsocks.utils.CloseUtils._ -import java.io.{FileWriter, InputStream, OutputStream} +import java.io._ /** * @author Mygod @@ -39,4 +39,10 @@ object IOUtils { def writeString(file: String, content: String): Unit = autoClose(new FileWriter(file))(writer => writer.write(content)) + + @throws[FileNotFoundException] + def deleteRecursively(file: File) { + if (file.isDirectory) file.listFiles().foreach(deleteRecursively) + if (!file.delete()) throw new FileNotFoundException("Failed to delete: " + file.getAbsolutePath) + } } 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 1fdc7845..78d5b57d 100644 --- a/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala +++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala @@ -46,7 +46,7 @@ abstract class NativePluginProvider extends ContentProvider { override def query(uri: Uri, projection: Array[String], selection: String, selectionArgs: Array[String], sortOrder: String): Cursor = { - if (selection != null || selectionArgs != null || sortOrder != null) ??? + assert(selection == null && selectionArgs == null && sortOrder == null) val result = new MatrixCursor(projection.filter(_ == PluginInterface.COLUMN_PATH)) populateFiles(new PathProvider(uri, result)) result @@ -54,7 +54,7 @@ abstract class NativePluginProvider extends ContentProvider { def openFile(uri: Uri): ParcelFileDescriptor override def openFile(uri: Uri, mode: String): ParcelFileDescriptor = { - if (mode != "r") ??? + assert(mode == "r") openFile(uri) }