Files
shadowsocks-android/mobile/src/main/java/com/github/shadowsocks/bg/BaseService.kt
T
Mygod d7a11eaa3b Implement direct boot awareness
To do direct boot, important files need to be moved to device storage (i.e. not as safe storage). The only thing sensitive is user credentials (server settings in each profile) and the config file passed to libss-*.so. In order to implement direct boot, at least the active profile has to be moved/copied to device storage.

This commit keeps profile information (and temporary data used by ProfileConfigFragment) and Shadowsocks config in credential storage and move everything else to device storage. In order to make direct boot work, active profile is also copied into device storage and Shadowsocks config file is created in device storage during direct boot. Cleanup of those information is also implemented.

Fixes #1457.

NB:

* `android:directBootAware` can't be used on application element. That seems to be used by system apps.
* Works fine with multi-user;
* Old GSM files are not migrated (see App.kt) to device storage.
2017-12-15 15:36:19 -08:00

422 lines
18 KiB
Kotlin

/*******************************************************************************
* *
* 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.bg
import android.annotation.TargetApi
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.IBinder
import android.os.RemoteCallbackList
import android.support.v4.os.UserManagerCompat
import android.text.TextUtils
import android.util.Base64
import android.util.Log
import android.widget.Toast
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.R
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.acl.AclSyncJob
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.plugin.PluginOptions
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.*
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.io.File
import java.net.UnknownHostException
import java.security.MessageDigest
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.reflect.KClass
/**
* This object uses WeakMap to simulate the effects of multi-inheritance.
*/
object BaseService {
/**
* IDLE state is only used by UI and will never be returned by BaseService.
*/
const val IDLE = 0
const val CONNECTING = 1
const val CONNECTED = 2
const val STOPPING = 3
const val STOPPED = 4
const val CONFIG_FILE = "shadowsocks.conf"
class Data internal constructor(private val service: Interface) {
@Volatile var profile: Profile? = null
@Volatile var state = STOPPED
@Volatile var plugin = PluginOptions()
@Volatile var pluginPath: String? = null
var sslocalProcess: GuardedProcess? = null
var timer: Timer? = null
var trafficMonitorThread: TrafficMonitorThread? = null
val callbacks = RemoteCallbackList<IShadowsocksServiceCallback>()
val bandwidthListeners = HashSet<IBinder>() // the binder is the real identifier
var notification: ServiceNotification? = null
val closeReceiver = broadcastReceiver { _, intent ->
when (intent.action) {
Action.RELOAD -> service.forceLoad()
else -> {
Toast.makeText(service as Context, R.string.stopping, Toast.LENGTH_SHORT).show()
service.stopRunner(true)
}
}
}
var closeReceiverRegistered = false
val binder = object : IShadowsocksService.Stub() {
override fun getState(): Int = this@Data.state
override fun getProfileName(): String = profile?.formattedName ?: "Idle"
override fun registerCallback(cb: IShadowsocksServiceCallback) {
callbacks.register(cb)
}
override fun startListeningForBandwidth(cb: IShadowsocksServiceCallback) {
if (bandwidthListeners.add(cb.asBinder())) {
if (timer == null) {
val t = Timer(true)
t.schedule(object : TimerTask() {
override fun run() {
if (state == CONNECTED && TrafficMonitor.updateRate()) app.handler.post {
if (bandwidthListeners.isNotEmpty()) {
val txRate = TrafficMonitor.txRate
val rxRate = TrafficMonitor.rxRate
val txTotal = TrafficMonitor.txTotal
val rxTotal = TrafficMonitor.rxTotal
val n = callbacks.beginBroadcast()
for (i in 0 until n) try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder()))
item.trafficUpdated(profile!!.id, txRate, rxRate, txTotal, rxTotal)
} catch (_: Exception) { } // ignore
callbacks.finishBroadcast()
}
}
}
}, 1000, 1000)
timer = t
}
TrafficMonitor.updateRate()
if (state == CONNECTED) cb.trafficUpdated(profile!!.id,
TrafficMonitor.txRate, TrafficMonitor.rxRate,
TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
}
}
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
if (bandwidthListeners.remove(cb.asBinder()) && bandwidthListeners.isEmpty()) {
timer!!.cancel()
timer = null
}
}
override fun unregisterCallback(cb: IShadowsocksServiceCallback) {
stopListeningForBandwidth(cb) // saves an RPC, and safer
callbacks.unregister(cb)
}
}
internal fun updateTrafficTotal(tx: Long, rx: Long) {
val profile = profile ?: return
val p = ProfileManager.getProfile(profile.id) ?: return // profile may have host, etc. modified
p.tx += tx
p.rx += rx
ProfileManager.updateProfile(p)
app.handler.post {
if (bandwidthListeners.isNotEmpty()) {
val n = callbacks.beginBroadcast()
for (i in 0 until n) {
try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(p.id)
} catch (_: Exception) { } // ignore
}
callbacks.finishBroadcast()
}
}
}
internal var shadowsocksConfigFile: File? = null
internal fun buildShadowsocksConfig(): File {
val profile = profile!!
val config = JSONObject()
.put("server", profile.host)
.put("server_port", profile.remotePort)
.put("password", profile.password)
.put("method", profile.method)
val pluginPath = pluginPath
if (pluginPath != null) {
val pluginCmd = arrayListOf(pluginPath)
if (TcpFastOpen.sendEnabled) pluginCmd.add("--fast-open")
config
.put("plugin", Commandline.toString(service.buildAdditionalArguments(pluginCmd)))
.put("plugin_opts", plugin.toString())
}
// sensitive Shadowsocks config is stored in
val file = File((if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) {
app.deviceContext.noBackupFilesDir // only API 24+ will be in locked state
}), CONFIG_FILE)
shadowsocksConfigFile = file
file.writeText(config.toString())
return file
}
val aclFile: File? get() {
val route = profile!!.route
return if (route == Acl.ALL) null else
Acl.getFile(if (route == Acl.CUSTOM_RULES) Acl.CUSTOM_RULES_FLATTENED else route)
}
fun changeState(s: Int, msg: String? = null) {
if (state == s && msg == null) return
if (callbacks.registeredCallbackCount > 0) app.handler.post {
val n = callbacks.beginBroadcast()
for (i in 0 until n) try {
callbacks.getBroadcastItem(i).stateChanged(s, binder.profileName, msg)
} catch (_: Exception) { } // ignore
callbacks.finishBroadcast()
}
state = s
}
}
interface Interface {
val tag: String
fun onBind(intent: Intent): IBinder? = if (intent.action == Action.SERVICE) data.binder else null
fun checkProfile(profile: Profile): Boolean =
if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
stopRunner(true, (this as Context).getString(R.string.proxy_empty))
false
} else true
fun forceLoad() {
val p = app.currentProfile
?: return stopRunner(true, (this as Context).getString(R.string.profile_empty))
if (!checkProfile(p)) return
val s = data.state
when (s) {
STOPPED -> startRunner()
CONNECTED -> {
stopRunner(false)
startRunner()
}
else -> Log.w(tag, "Illegal state when invoking use: $s")
}
}
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
fun startNativeProcesses() {
val data = data
val cmd = buildAdditionalArguments(arrayListOf(
File((this as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
"-u",
"-b", "127.0.0.1",
"-l", DataStore.portProxy.toString(),
"-t", "600",
"-c", data.buildShadowsocksConfig().absolutePath))
val acl = data.aclFile
if (acl != null) {
cmd += "--acl"
cmd += acl.absolutePath
}
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
data.sslocalProcess = GuardedProcess(cmd).start()
}
fun createNotification(): ServiceNotification
fun startRunner() {
this as Context
if (Build.VERSION.SDK_INT >= 26) startForegroundService(Intent(this, javaClass))
else startService(Intent(this, javaClass))
}
fun killProcesses() {
val data = data
data.sslocalProcess?.destroy()
data.sslocalProcess = null
}
fun stopRunner(stopService: Boolean, msg: String? = null) {
// channge the state
val data = data
data.changeState(STOPPING)
app.track(tag, "stop")
killProcesses()
// clean up recevier
this as Service
if (data.closeReceiverRegistered) {
unregisterReceiver(data.closeReceiver)
data.closeReceiverRegistered = false
}
data.shadowsocksConfigFile?.delete() // remove old config possibly in device storage
data.shadowsocksConfigFile = null
data.notification?.destroy()
data.notification = null
// Make sure update total traffic when stopping the runner
data.updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
TrafficMonitor.reset()
data.trafficMonitorThread?.stopThread()
data.trafficMonitorThread = null
// change the state
data.changeState(STOPPED, msg)
// stop the service if nothing has bound to it
if (stopService) stopSelf()
data.profile = null
}
val data: Data get() = instances[this]!!
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = data
if (data.state != STOPPED) return Service.START_NOT_STICKY
val profile = app.currentProfile
this as Context
if (profile == null) {
stopRunner(true, getString(R.string.profile_empty))
return Service.START_NOT_STICKY
}
data.profile = profile
profile.name = profile.formattedName
TrafficMonitor.reset()
val thread = TrafficMonitorThread()
thread.start()
data.trafficMonitorThread = thread
if (!data.closeReceiverRegistered) {
// register close receiver
val filter = IntentFilter()
filter.addAction(Action.RELOAD)
filter.addAction(Intent.ACTION_SHUTDOWN)
filter.addAction(Action.CLOSE)
registerReceiver(data.closeReceiver, filter)
data.closeReceiverRegistered = true
}
data.notification = createNotification()
app.track(tag, "start")
data.changeState(CONNECTING)
thread {
try {
if (profile.host == "198.199.101.152") {
val client = OkHttpClient.Builder()
.dns {
listOf((Dns.resolve(it, false) ?: throw UnknownHostException())
.parseNumericAddress())
}
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(app.info.signatures[0].toByteArray())
val requestBody = FormBody.Builder()
.add("sig", String(Base64.encode(mdg.digest(), 0)))
.build()
val request = Request.Builder()
.url(app.remoteConfig.getString("proxy_url"))
.post(requestBody)
.build()
val proxies = client.newCall(request).execute()
.body()!!.string().split('|').toMutableList()
proxies.shuffle()
val proxy = proxies.first().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)
Acl.save(Acl.CUSTOM_RULES_FLATTENED, Acl.customRules.flatten(10))
data.plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
data.pluginPath = PluginManager.init(data.plugin)
// Clean up
killProcesses()
if (!profile.host.isNumericAddress())
profile.host = Dns.resolve(profile.host, true) ?: throw UnknownHostException()
startNativeProcesses()
if (profile.route !in arrayOf(Acl.ALL, Acl.CUSTOM_RULES)) AclSyncJob.schedule(profile.route)
data.changeState(CONNECTED)
} catch (_: UnknownHostException) {
stopRunner(true, getString(R.string.invalid_server))
} catch (_: VpnService.NullConnectionException) {
stopRunner(true, getString(R.string.reboot_required))
} catch (exc: Throwable) {
stopRunner(true, "${getString(R.string.service_failed)}: ${exc.message}")
app.track(exc)
}
}
return Service.START_NOT_STICKY
}
}
private val instances = WeakHashMap<Interface, Data>()
internal fun register(instance: Interface) = instances.put(instance, Data(instance))
val usingVpnMode: Boolean get() = DataStore.serviceMode == Key.modeVpn
val serviceClass: KClass<out Any> get() = when (DataStore.serviceMode) {
Key.modeProxy -> ProxyService::class
Key.modeVpn -> VpnService::class
Key.modeTransproxy -> TransproxyService::class
else -> throw UnknownError()
}
}