Commit 0ffae32a authored by Mygod's avatar Mygod

Implement plugin interface in mobile app

parent c54745e2
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<String> 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.
......
......@@ -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
}
}
......@@ -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()
......
......@@ -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")
if (profile.auth) cmd += "-A"
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
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")
, "-c" , buildShadowsocksConfig(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf", 53))
if (profile.auth) cmdBuf += "-A"
if (profile.udpdns) cmd.append("-u")
if (BuildConfig.DEBUG) Log.d(TAG, cmdBuf.mkString(" "))
if (BuildConfig.DEBUG) Log.d(TAG, Commandline.toString(cmd))
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()
}
......
......@@ -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))
......
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
}
}
......@@ -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" +
......
......@@ -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)
}
}
......@@ -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)
}
......
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