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.
422 lines
18 KiB
Kotlin
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()
|
|
}
|
|
}
|