388 lines
14 KiB
Scala
388 lines
14 KiB
Scala
/*******************************************************************************/
|
|
/* */
|
|
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
|
|
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
|
|
/* */
|
|
/* This program is free software: you can redistribute it and/or modify */
|
|
/* it under the terms of the GNU General Public License as published by */
|
|
/* the Free Software Foundation, either version 3 of the License, or */
|
|
/* (at your option) any later version. */
|
|
/* */
|
|
/* This program is distributed in the hope that it will be useful, */
|
|
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
|
|
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
|
|
/* GNU General Public License for more details. */
|
|
/* */
|
|
/* You should have received a copy of the GNU General Public License */
|
|
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
|
/* */
|
|
/*******************************************************************************/
|
|
|
|
package com.github.shadowsocks
|
|
|
|
import java.io.{File, IOException}
|
|
import java.net.InetAddress
|
|
import java.util
|
|
import java.util.concurrent.TimeUnit
|
|
import java.util.{Timer, TimerTask}
|
|
|
|
import android.app.Service
|
|
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
|
import android.os.{Handler, IBinder, RemoteCallbackList}
|
|
import android.text.TextUtils
|
|
import android.util.Log
|
|
import android.widget.Toast
|
|
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}
|
|
import org.json.{JSONArray, JSONObject}
|
|
|
|
import scala.collection.JavaConversions._
|
|
import scala.collection.mutable
|
|
import scala.collection.mutable.ArrayBuffer
|
|
import scala.util.Random
|
|
|
|
trait BaseService extends Service {
|
|
|
|
@volatile private var state = State.STOPPED
|
|
@volatile protected var profile: Profile = _
|
|
@volatile private var plugin: PluginOptions = _
|
|
@volatile protected var pluginPath: String = _
|
|
|
|
case class NameNotResolvedException() extends IOException
|
|
case class NullConnectionException() extends NullPointerException
|
|
|
|
var timer: Timer = _
|
|
var trafficMonitorThread: TrafficMonitorThread = _
|
|
|
|
final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback]
|
|
private final val bandwidthListeners = new mutable.HashSet[IBinder]() // the binder is the real identifier
|
|
lazy val handler = new Handler(getMainLooper)
|
|
lazy val restartHanlder = new Handler(getMainLooper)
|
|
|
|
private val closeReceiver: BroadcastReceiver = (context: Context, _: Intent) => {
|
|
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
|
|
stopRunner(stopService = true)
|
|
}
|
|
var closeReceiverRegistered: Boolean = _
|
|
|
|
val binder = new IShadowsocksService.Stub {
|
|
override def getState: Int = {
|
|
state
|
|
}
|
|
|
|
override def getProfileName: String = if (profile == null) null else profile.getName
|
|
|
|
override def registerCallback(cb: IShadowsocksServiceCallback): Unit = callbacks.register(cb)
|
|
|
|
override def startListeningForBandwidth(cb: IShadowsocksServiceCallback): Unit =
|
|
if (bandwidthListeners.add(cb.asBinder)){
|
|
if (timer == null) {
|
|
timer = new Timer(true)
|
|
timer.schedule(new TimerTask {
|
|
def run(): Unit = if (state == State.CONNECTED && TrafficMonitor.updateRate()) updateTrafficRate()
|
|
}, 1000, 1000)
|
|
}
|
|
TrafficMonitor.updateRate()
|
|
if (state == State.CONNECTED) cb.trafficUpdated(profile.id,
|
|
TrafficMonitor.txRate, TrafficMonitor.rxRate, TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
|
|
}
|
|
|
|
override def stopListeningForBandwidth(cb: IShadowsocksServiceCallback): Unit =
|
|
if (bandwidthListeners.remove(cb.asBinder) && bandwidthListeners.isEmpty) {
|
|
timer.cancel()
|
|
timer = null
|
|
}
|
|
|
|
override def unregisterCallback(cb: IShadowsocksServiceCallback) {
|
|
stopListeningForBandwidth(cb) // saves an RPC, and safer
|
|
callbacks.unregister(cb)
|
|
}
|
|
|
|
override def use(profileId: Int): Unit = synchronized(if (profileId < 0) stopRunner(stopService = true) else {
|
|
val profile = app.profileManager.getProfile(profileId).orNull
|
|
if (profile == null) stopRunner(stopService = true, getString(R.string.profile_empty)) else state match {
|
|
case State.STOPPED => if (checkProfile(profile)) startRunner(profile)
|
|
case State.CONNECTED => if (profileId != BaseService.this.profile.id && checkProfile(profile)) {
|
|
stopRunner(stopService = false)
|
|
startRunner(profile)
|
|
}
|
|
case _ => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + state)
|
|
}
|
|
})
|
|
|
|
override def useSync(profileId: Int): Unit = use(profileId)
|
|
}
|
|
|
|
def checkProfile(profile: Profile): Boolean = if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
|
|
stopRunner(stopService = true, getString(R.string.proxy_empty))
|
|
false
|
|
} else true
|
|
|
|
def connect() {
|
|
if (profile.host == "198.199.101.152") {
|
|
val holder = app.containerHolder
|
|
val container = holder.getContainer
|
|
val url = container.getString("proxy_url")
|
|
val sig = Utils.getSignature(this)
|
|
|
|
val client = new OkHttpClient.Builder()
|
|
.dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
|
|
case Some(ip) => util.Arrays.asList(InetAddress.getByName(ip))
|
|
case _ => Dns.SYSTEM.lookup(hostname)
|
|
})
|
|
.connectTimeout(5, TimeUnit.SECONDS)
|
|
.writeTimeout(5, TimeUnit.SECONDS)
|
|
.readTimeout(5, TimeUnit.SECONDS)
|
|
.build()
|
|
val requestBody = new FormBody.Builder()
|
|
.add("sig", sig)
|
|
.build()
|
|
val request = new Request.Builder()
|
|
.url(url)
|
|
.post(requestBody)
|
|
.build()
|
|
|
|
val proxies = Random.shuffle(client.newCall(request).execute().body.string.split('|').toSeq)
|
|
val proxy = proxies.head.split(':')
|
|
profile.host = proxy(0).trim
|
|
profile.remotePort = proxy(1).trim.toInt
|
|
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.init(plugin)
|
|
}
|
|
|
|
def startRunner(profile: Profile) {
|
|
this.profile = profile
|
|
|
|
startService(new Intent(this, getClass))
|
|
TrafficMonitor.reset()
|
|
trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
|
|
trafficMonitorThread.start()
|
|
|
|
if (!closeReceiverRegistered) {
|
|
// register close receiver
|
|
val filter = new IntentFilter()
|
|
filter.addAction(Intent.ACTION_SHUTDOWN)
|
|
filter.addAction(Action.CLOSE)
|
|
registerReceiver(closeReceiver, filter)
|
|
closeReceiverRegistered = true
|
|
}
|
|
|
|
app.track(getClass.getSimpleName, "start")
|
|
|
|
changeState(State.CONNECTING)
|
|
|
|
Utils.ThrowableFuture(try connect() catch {
|
|
case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server))
|
|
case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required))
|
|
case exc: Throwable =>
|
|
stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage)
|
|
exc.printStackTrace()
|
|
app.track(exc)
|
|
})
|
|
}
|
|
|
|
def stopRunner(stopService: Boolean, msg: String = null) {
|
|
// clean up recevier
|
|
if (closeReceiverRegistered) {
|
|
unregisterReceiver(closeReceiver)
|
|
closeReceiverRegistered = false
|
|
}
|
|
|
|
// Make sure update total traffic when stopping the runner
|
|
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
|
|
|
|
TrafficMonitor.reset()
|
|
if (trafficMonitorThread != null) {
|
|
trafficMonitorThread.stopThread()
|
|
trafficMonitorThread = null
|
|
}
|
|
|
|
// change the state
|
|
changeState(State.STOPPED, msg)
|
|
|
|
// stop the service if nothing has bound to it
|
|
if (stopService) stopSelf()
|
|
|
|
profile = null
|
|
}
|
|
|
|
def updateTrafficTotal(tx: Long, rx: Long) {
|
|
val profile = this.profile // avoid race conditions without locking
|
|
if (profile != null) {
|
|
app.profileManager.getProfile(profile.id) match {
|
|
case Some(p) => // default profile may have host, etc. modified
|
|
p.tx += tx
|
|
p.rx += rx
|
|
app.profileManager.updateProfile(p)
|
|
handler.post(() => {
|
|
if (bandwidthListeners.nonEmpty) {
|
|
val n = callbacks.beginBroadcast()
|
|
for (i <- 0 until n) {
|
|
try {
|
|
val item = callbacks.getBroadcastItem(i)
|
|
if (bandwidthListeners.contains(item.asBinder)) item.trafficPersisted(p.id)
|
|
} catch {
|
|
case _: Exception => // Ignore
|
|
}
|
|
}
|
|
callbacks.finishBroadcast()
|
|
}
|
|
})
|
|
case None =>
|
|
}
|
|
}
|
|
}
|
|
|
|
def getState: Int = {
|
|
state
|
|
}
|
|
|
|
def updateTrafficRate() {
|
|
handler.post(() => {
|
|
if (bandwidthListeners.nonEmpty) {
|
|
val txRate = TrafficMonitor.txRate
|
|
val rxRate = TrafficMonitor.rxRate
|
|
val txTotal = TrafficMonitor.txTotal
|
|
val rxTotal = TrafficMonitor.rxTotal
|
|
val n = callbacks.beginBroadcast()
|
|
for (i <- 0 until n) {
|
|
try {
|
|
val item = callbacks.getBroadcastItem(i)
|
|
if (bandwidthListeners.contains(item.asBinder))
|
|
item.trafficUpdated(profile.id, txRate, rxRate, txTotal, rxTotal)
|
|
} catch {
|
|
case _: Exception => // Ignore
|
|
}
|
|
}
|
|
callbacks.finishBroadcast()
|
|
}
|
|
})
|
|
}
|
|
|
|
|
|
override def onCreate() {
|
|
super.onCreate()
|
|
app.refreshContainerHolder()
|
|
app.updateAssets()
|
|
}
|
|
|
|
// Service of shadowsocks should always be started explicitly
|
|
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = Service.START_NOT_STICKY
|
|
|
|
protected def changeState(s: Int, msg: String = null) {
|
|
val handler = new Handler(getMainLooper)
|
|
if (state != s || msg != null) {
|
|
if (callbacks.getRegisteredCallbackCount > 0) handler.post(() => {
|
|
val n = callbacks.beginBroadcast()
|
|
for (i <- 0 until n) {
|
|
try {
|
|
callbacks.getBroadcastItem(i).stateChanged(s, binder.getProfileName, msg)
|
|
} catch {
|
|
case _: Exception => // Ignore
|
|
}
|
|
}
|
|
callbacks.finishBroadcast()
|
|
})
|
|
state = s
|
|
}
|
|
}
|
|
|
|
def getBlackList: String = {
|
|
val default = getString(R.string.black_list)
|
|
try {
|
|
val container = app.containerHolder.getContainer
|
|
val update = container.getString("black_list_lite")
|
|
val list = if (update == null || update.isEmpty) default else update
|
|
"exclude = " + list + ";"
|
|
} catch {
|
|
case _: Exception => "exclude = " + default + ";"
|
|
}
|
|
}
|
|
|
|
protected def buildPluginCommandLine(): ArrayBuffer[String] = {
|
|
val result = ArrayBuffer(pluginPath)
|
|
if (TcpFastOpen.sendEnabled) result += "--fast-open"
|
|
result
|
|
}
|
|
protected final def buildShadowsocksConfig(file: String): String = {
|
|
val config = new JSONObject()
|
|
.put("server", profile.host)
|
|
.put("server_port", profile.remotePort)
|
|
.put("password", profile.password)
|
|
.put("method", profile.method)
|
|
if (pluginPath != null) config
|
|
.put("plugin", Commandline.toString(buildPluginCommandLine()))
|
|
.put("plugin_opts", plugin.toString)
|
|
IOUtils.writeString(new File(getFilesDir, file), config.toString)
|
|
file
|
|
}
|
|
|
|
protected final def buildOvertureConfig(file: String): String = {
|
|
val url = "http://icanhazip.com"
|
|
val client = new OkHttpClient.Builder()
|
|
.connectTimeout(10, TimeUnit.SECONDS)
|
|
.writeTimeout(10, TimeUnit.SECONDS)
|
|
.readTimeout(30, TimeUnit.SECONDS)
|
|
.build()
|
|
val request = new Request.Builder()
|
|
.url(url)
|
|
.build()
|
|
val response = client.newCall(request).execute()
|
|
val externalIp = if (response.isSuccessful) response.body.string.trim else ""
|
|
|
|
val config = new JSONObject()
|
|
.put("BindAddress", ":" + (profile.localPort + 53))
|
|
.put("RedirectIPv6Record", true)
|
|
.put("DomainBase64Decode", true)
|
|
.put("HostsFile", "hosts")
|
|
.put("MinimumTTL", 3600)
|
|
.put("CacheSize", 4096)
|
|
def makeDns(name: String, address: String, edns: Boolean = true) = {
|
|
val dns = new JSONObject()
|
|
.put("Name", name)
|
|
.put("Address", address + ":53")
|
|
.put("Timeout", 6)
|
|
if (edns) dns
|
|
.put("Protocol", "tcp")
|
|
.put("Socks5Address", "127.0.0.1:" + profile.localPort)
|
|
.put("EDNSClientSubnet", new JSONObject()
|
|
.put("Policy", "auto")
|
|
.put("ExternalIP", externalIp))
|
|
else dns
|
|
.put("Protocol", "udp")
|
|
.put("EDNSClientSubnet", new JSONObject().put("Policy", "disable"))
|
|
dns
|
|
}
|
|
profile.route match {
|
|
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES => config
|
|
.put("PrimaryDNS", new JSONArray(Array(
|
|
makeDns("Primary-1", "119.29.29.29", edns = false),
|
|
makeDns("Primary-2", "114.114.114.114", edns = false)
|
|
)))
|
|
.put("AlternativeDNS", new JSONArray().put(makeDns("Alternative", profile.remoteDns)))
|
|
.put("IPNetworkFile", "china_ip_list.txt")
|
|
.put("DomainFile", "gfwlist.txt")
|
|
case Acl.CHINALIST => config
|
|
.put("PrimaryDNS", new JSONArray().put(makeDns("Primary", "119.29.29.29")))
|
|
.put("AlternativeDNS", new JSONArray().put(makeDns("Alternative", profile.remoteDns)))
|
|
case _ => config
|
|
.put("PrimaryDNS", new JSONArray().put(makeDns("Primary", profile.remoteDns)))
|
|
.put("AlternativeDNS", new JSONArray().put(makeDns("Alternative", "208.67.222.222")))
|
|
}
|
|
IOUtils.writeString(new File(getFilesDir, file), config.toString)
|
|
file
|
|
}
|
|
}
|