From 25bd5347ebaf5647584e8a1dbf8aa70199e4f6e7 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 19 Aug 2016 15:23:43 +0800 Subject: [PATCH] Refine #816 and error handling in startRunner --- .../com/github/shadowsocks/BaseService.scala | 19 +++- .../shadowsocks/ShadowsocksNatService.scala | 69 +++++------- .../shadowsocks/ShadowsocksVpnService.scala | 101 +++++++----------- .../com/github/shadowsocks/utils/Utils.scala | 45 +++++++- 4 files changed, 125 insertions(+), 109 deletions(-) diff --git a/src/main/scala/com/github/shadowsocks/BaseService.scala b/src/main/scala/com/github/shadowsocks/BaseService.scala index e968a569..7f726718 100644 --- a/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -39,6 +39,7 @@ package com.github.shadowsocks +import java.io.IOException import java.util.{Timer, TimerTask} import android.app.Service @@ -48,7 +49,6 @@ import android.os.{Handler, RemoteCallbackList} import android.text.TextUtils import android.util.Log import android.widget.Toast - import com.github.kevinsawicki.http.HttpRequest import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback} import com.github.shadowsocks.utils._ @@ -60,6 +60,9 @@ trait BaseService extends Service { @volatile private var state = State.STOPPED @volatile protected var profile: Profile = _ + case class NameNotResolvedException() extends IOException + case class KcpcliParseException(cause: Throwable) extends Exception(cause) + var timer: Timer = _ var trafficMonitorThread: TrafficMonitorThread = _ @@ -136,7 +139,7 @@ trait BaseService extends Service { false } else true - def connect() = if (profile.host == "198.199.101.152") try { + def connect() = if (profile.host == "198.199.101.152") { val holder = app.containerHolder val container = holder.getContainer val url = container.getString("proxy_url") @@ -153,8 +156,6 @@ trait BaseService extends Service { profile.remotePort = proxy(1).trim.toInt profile.password = proxy(2).trim profile.method = proxy(3).trim - } catch { - case ex: Exception => stopRunner(true, getString(R.string.service_failed)) } def startRunner(profile: Profile) { @@ -188,7 +189,15 @@ trait BaseService extends Service { if (profile.isMethodUnsafe) handler.post(() => Toast.makeText(this, R.string.method_unsafe, Toast.LENGTH_LONG).show) - Utils.ThrowableFuture(connect) + Utils.ThrowableFuture(try connect catch { + case _: NameNotResolvedException => stopRunner(true, getString(R.string.invalid_server)) + case exc: KcpcliParseException => + stopRunner(true, getString(R.string.service_failed) + ": " + exc.cause.getMessage) + case exc: Throwable => + stopRunner(true, getString(R.string.service_failed) + ": " + exc.getMessage) + exc.printStackTrace() + app.track(exc) + }) } def stopRunner(stopService: Boolean, msg: String = null) { diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala index b38e8fd6..b2d1b5c8 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala @@ -97,8 +97,7 @@ class ShadowsocksNatService extends BaseService { p.println(conf) }) - val cmd = new ArrayBuffer[String] - cmd += (getApplicationInfo.dataDir + "/ss-local" + val cmd = ArrayBuffer[String](getApplicationInfo.dataDir + "/ss-local" , "-b" , "127.0.0.1" , "-t" , "600" , "-P", getApplicationInfo.dataDir @@ -120,15 +119,16 @@ class ShadowsocksNatService extends BaseService { def startKcptunDaemon() { if (profile.kcpcli == null) profile.kcpcli = "" - val cmd = new ArrayBuffer[String] - cmd += (getApplicationInfo.dataDir + "/kcptun" + val cmd = ArrayBuffer[String](getApplicationInfo.dataDir + "/kcptun" , "-r", profile.host + ":" + profile.kcpPort - , "-l", "127.0.0.1:" + (profile.localPort + 90) - , profile.kcpcli.replaceAll("^\\s*([\"'])(.*)\\1\\s*$", "$2")) + , "-l", "127.0.0.1:" + (profile.localPort + 90)) + try cmd ++= Utils.translateCommandline(profile.kcpcli) catch { + case exc: Exception => throw KcpcliParseException(exc) + } if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) - kcptunProcess = new GuardedProcess(cmd.mkString(" ").split("[ ]{1,}").toSeq).start() + kcptunProcess = new GuardedProcess(cmd).start() } def startDNSTunnel() { @@ -139,8 +139,7 @@ class ShadowsocksNatService extends BaseService { Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf"))(p => { p.println(conf) }) - val cmd = new ArrayBuffer[String] - cmd += (getApplicationInfo.dataDir + "/ss-tunnel" + val cmd = ArrayBuffer[String](getApplicationInfo.dataDir + "/ss-tunnel" , "-u" , "-t" , "10" , "-b" , "127.0.0.1" @@ -168,8 +167,7 @@ class ShadowsocksNatService extends BaseService { Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf"))(p => { p.println(conf) }) - val cmdBuf = new ArrayBuffer[String] - cmdBuf += (getApplicationInfo.dataDir + "/ss-tunnel" + val cmdBuf = ArrayBuffer[String](getApplicationInfo.dataDir + "/ss-tunnel" , "-t" , "10" , "-b" , "127.0.0.1" , "-l" , (profile.localPort + 63).toString @@ -198,27 +196,26 @@ class ShadowsocksNatService extends BaseService { Utils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-nat.conf"))(p => { p.println(conf) }) - val cmd = getApplicationInfo.dataDir + "/pdnsd -c " + getApplicationInfo.dataDir + "/pdnsd-nat.conf" + val cmd = Array(getApplicationInfo.dataDir + "/pdnsd", "-c", getApplicationInfo.dataDir + "/pdnsd-nat.conf") - if (BuildConfig.DEBUG) Log.d(TAG, cmd) + if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) - pdnsdProcess = new GuardedProcess(cmd.split(" ").toSeq).start() + pdnsdProcess = new GuardedProcess(cmd).start() } def startRedsocksDaemon() { val conf = ConfigUtils.REDSOCKS.formatLocal(Locale.ENGLISH, profile.localPort) - val cmd = "%s/redsocks -c %s/redsocks-nat.conf" - .formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, getApplicationInfo.dataDir) + val cmd = Array(getApplicationInfo.dataDir + "/redsocks", "-c", getApplicationInfo.dataDir + "/redsocks-nat.conf") Utils.printToFile(new File(getApplicationInfo.dataDir + "/redsocks-nat.conf"))(p => { p.println(conf) }) - if (BuildConfig.DEBUG) Log.d(TAG, cmd) - redsocksProcess = new GuardedProcess(cmd.split(" ").toSeq).start() + if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) + redsocksProcess = new GuardedProcess(cmd).start() } /** Called when the activity is first created. */ - def handleConnection { + def handleConnection() { startDNSTunnel() startRedsocksDaemon() @@ -323,31 +320,19 @@ class ShadowsocksNatService extends BaseService { override def connect() { super.connect() - if (profile != null) { + // Clean up + killProcesses() - // Clean up - killProcesses() - - var resolved = false - if (!Utils.isNumeric(profile.host)) { - Utils.resolve(profile.host, enableIPv6 = true) match { - case Some(a) => - profile.host = a - resolved = true - case None => resolved = false - } - } else { - resolved = true - } - - if (!resolved) stopRunner(true, getString(R.string.invalid_server)) else { - handleConnection - // Set DNS - su.addCommand(Utils.FLUSH_DNS) - changeState(State.CONNECTED) - notification = new ShadowsocksNotification(this, profile.name, true) - } + if (!Utils.isNumeric(profile.host)) Utils.resolve(profile.host, enableIPv6 = true) match { + case Some(a) => profile.host = a + case None => throw NameNotResolvedException() } + + handleConnection() + // Set DNS + su.addCommand(Utils.FLUSH_DNS) + changeState(State.CONNECTED) + notification = new ShadowsocksNotification(this, profile.name, true) } override def stopRunner(stopService: Boolean, msg: String = null) { diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala index 3fab3b74..65765bd9 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala @@ -148,42 +148,28 @@ class ShadowsocksVpnService extends VpnService with BaseService { override def connect() = { super.connect() - if (profile != null) { + vpnThread = new ShadowsocksVpnThread(this) + vpnThread.start() - vpnThread = new ShadowsocksVpnThread(this) - vpnThread.start() + // reset the context + killProcesses() - // reset the context - killProcesses() - - // Resolve the server address - var resolved: Boolean = false - if (!Utils.isNumeric(profile.host)) { - Utils.resolve(profile.host, enableIPv6 = true) match { - case Some(addr) => - profile.host = addr - resolved = true - case None => resolved = false - } - } else { - resolved = true - } - - if (!resolved) stopRunner(true, getString(R.string.invalid_server)) else if (handleConnection) { - changeState(State.CONNECTED) - notification = new ShadowsocksNotification(this, profile.name) - } else { - changeState(State.STOPPED, getString(R.string.service_failed)) - stopRunner(true) - } + // Resolve the server address + if (!Utils.isNumeric(profile.host)) Utils.resolve(profile.host, enableIPv6 = true) match { + case Some(addr) => profile.host = addr + case None => throw NameNotResolvedException() } + + handleConnection() + changeState(State.CONNECTED) + notification = new ShadowsocksNotification(this, profile.name) } /** Called when the activity is first created. */ - def handleConnection: Boolean = { + def handleConnection() { val fd = startVpn() - if (!sendFd(fd)) return false + if (!sendFd(fd)) throw new Exception("sendFd failed") if (profile.kcp) { startKcptunDaemon() @@ -199,24 +185,23 @@ class ShadowsocksVpnService extends VpnService with BaseService { startDnsDaemon() startDnsTunnel() } - - true } def startKcptunDaemon() { if (profile.kcpcli == null) profile.kcpcli = "" - val cmd = new ArrayBuffer[String] - cmd += (getApplicationInfo.dataDir + "/kcptun" + val cmd = ArrayBuffer(getApplicationInfo.dataDir + "/kcptun" , "-r", profile.host + ":" + profile.kcpPort , "-l", "127.0.0.1:" + (profile.localPort + 90) - , "--path", getApplicationInfo.dataDir + "/protect_path" - , profile.kcpcli.replaceAll("^\\s*([\"'])(.*)\\1\\s*$", "$2")) + , "--path", getApplicationInfo.dataDir + "/protect_path") + try cmd ++= Utils.translateCommandline(profile.kcpcli) catch { + case exc: Exception => throw KcpcliParseException(exc) + } if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) - kcptunProcess = new GuardedProcess(cmd.mkString(" ").split("[ ]{1,}").toSeq).start() + kcptunProcess = new GuardedProcess(cmd).start() } def startShadowsocksUDPDaemon() { @@ -227,8 +212,7 @@ class ShadowsocksVpnService extends VpnService with BaseService { p.println(conf) }) - val cmd = new ArrayBuffer[String] - cmd += (getApplicationInfo.dataDir + "/ss-local", "-V", "-U" + val cmd = ArrayBuffer[String](getApplicationInfo.dataDir + "/ss-local", "-V", "-U" , "-b", "127.0.0.1" , "-t", "600" , "-P", getApplicationInfo.dataDir @@ -267,8 +251,7 @@ class ShadowsocksVpnService extends VpnService with BaseService { p.println(conf) }) - val cmd = new ArrayBuffer[String] - cmd += (getApplicationInfo.dataDir + "/ss-local", "-V" + val cmd = ArrayBuffer[String](getApplicationInfo.dataDir + "/ss-local", "-V" , "-b", "127.0.0.1" , "-t", "600" , "-P", getApplicationInfo.dataDir @@ -303,8 +286,7 @@ class ShadowsocksVpnService extends VpnService with BaseService { Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf"))(p => { p.println(conf) }) - val cmd = new ArrayBuffer[String] - cmd += (getApplicationInfo.dataDir + "/ss-tunnel" + val cmd = ArrayBuffer[String](getApplicationInfo.dataDir + "/ss-tunnel" , "-V" , "-t", "10" , "-b", "127.0.0.1" @@ -333,11 +315,11 @@ class ShadowsocksVpnService extends VpnService with BaseService { Utils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-vpn.conf"))(p => { p.println(conf) }) - val cmd = getApplicationInfo.dataDir + "/pdnsd -c " + getApplicationInfo.dataDir + "/pdnsd-vpn.conf" + val cmd = Array(getApplicationInfo.dataDir + "/pdnsd", "-c", getApplicationInfo.dataDir + "/pdnsd-vpn.conf") - if (BuildConfig.DEBUG) Log.d(TAG, cmd) + if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) - pdnsdProcess = new GuardedProcess(cmd.split(" ").toSeq).start() + pdnsdProcess = new GuardedProcess(cmd).start() } def startVpn(): Int = { @@ -397,30 +379,27 @@ class ShadowsocksVpnService extends VpnService with BaseService { val fd = conn.getFd - var cmd = (getApplicationInfo.dataDir + - "/tun2socks --netif-ipaddr %s " - + "--netif-netmask 255.255.255.0 " - + "--socks-server-addr 127.0.0.1:%d " - + "--tunfd %d " - + "--tunmtu %d " - + "--sock-path %s " - + "--loglevel 3") - .formatLocal(Locale.ENGLISH, - PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "2"), - profile.localPort, fd, VPN_MTU, getApplicationInfo.dataDir + "/sock_path") + var cmd = ArrayBuffer[String](getApplicationInfo.dataDir + "/tun2socks", + "--netif-ipaddr", PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "2"), + "--netif-netmask", "255.255.255.0", + "--socks-server-addr", "127.0.0.1:" + profile.localPort, + "--tunfd", fd.toString, + "--tunmtu", VPN_MTU.toString, + "--sock-path", getApplicationInfo.dataDir + "/sock_path", + "--loglevel", "3") if (profile.ipv6) - cmd += " --netif-ip6addr " + PRIVATE_VLAN6.formatLocal(Locale.ENGLISH, "2") + cmd += ("--netif-ip6addr", PRIVATE_VLAN6.formatLocal(Locale.ENGLISH, "2")) if (profile.udpdns) - cmd += " --enable-udprelay" + cmd += "--enable-udprelay" else - cmd += " --dnsgw %s:%d".formatLocal(Locale.ENGLISH, PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"), - profile.localPort + 53) + cmd += ("--dnsgw", "%s:%d".formatLocal(Locale.ENGLISH, PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"), + profile.localPort + 53)) - if (BuildConfig.DEBUG) Log.d(TAG, cmd) + if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) - tun2socksProcess = new GuardedProcess(cmd.split(" ").toSeq).start(() => sendFd(fd)) + tun2socksProcess = new GuardedProcess(cmd).start(() => sendFd(fd)) fd } diff --git a/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/src/main/scala/com/github/shadowsocks/utils/Utils.scala index 691401ad..81f2c5f4 100644 --- a/src/main/scala/com/github/shadowsocks/utils/Utils.scala +++ b/src/main/scala/com/github/shadowsocks/utils/Utils.scala @@ -41,7 +41,7 @@ package com.github.shadowsocks.utils import java.io.File import java.net._ import java.security.MessageDigest -import java.util.Scanner +import java.util.{Scanner, StringTokenizer} import android.animation.{Animator, AnimatorListenerAdapter} import android.content.pm.PackageManager @@ -58,6 +58,7 @@ import eu.chainfire.libsuperuser.Shell import org.xbill.DNS._ import scala.collection.JavaConversions._ +import scala.collection.mutable.ArrayBuffer import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.util.{Failure, Try} @@ -127,6 +128,48 @@ object Utils { } } + /** + * Crack a command line. + * Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java#L471 + * @param toProcess the command line to process. + * @return the command line broken into strings. + * An empty or null toProcess parameter results in a zero sized ArrayBuffer. + */ + def translateCommandline(toProcess: String): ArrayBuffer[String] = { + if (toProcess == null || toProcess.length == 0) return ArrayBuffer[String]() + val tok = new StringTokenizer(toProcess, "\"' ", true) + val result = ArrayBuffer[String]() + val current = new StringBuilder() + var quote = ' ' + var last = " " + while (tok.hasMoreTokens) { + val nextTok = tok.nextToken + quote match { + case '\'' => nextTok match { + case "'" => quote = ' ' + case _ => current.append(nextTok) + } + case '"' => nextTok match { + case "\"" => quote = ' ' + case _ => current.append(nextTok) + } + case _ => nextTok match { + case "'" => quote = '\'' + case "\"" => quote = '"' + case " " => if (last != " ") { + result.append(current.toString) + current.setLength(0) + } + case _ => current.append(nextTok) + } + } + last = nextTok + } + if (current.nonEmpty) result.append(current.toString) + if (quote == '\'' || quote == '"') throw new Exception("Unbalanced quotes in " + toProcess) + result + } + // Because /sys/class/net/* isn't accessible since API level 24 final val FLUSH_DNS = "for if in /sys/class/net/*; do " + "if [ \"down\" != $(cat $if/operstate) ]; then " + // up or unknown