/******************************************************************************* * * * Copyright (C) 2017 by Max Lv * * Copyright (C) 2017 by Mygod Studio * * * * 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 . * * * *******************************************************************************/ 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.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() val bandwidthListeners = HashSet() // 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(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 (profile.host.isEmpty() || profile.password.isEmpty()) { 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): ArrayList = 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(profileName: String): 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) { data.notification = createNotification("") // gracefully shutdown: https://stackoverflow.com/questions/47337857/context-startforegroundservice-did-not-then-call-service-startforeground-eve stopRunner(true, getString(R.string.profile_empty)) return Service.START_NOT_STICKY } data.profile = profile 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(profile.formattedName) 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, 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() internal fun register(instance: Interface) = instances.put(instance, Data(instance)) val usingVpnMode: Boolean get() = DataStore.serviceMode == Key.modeVpn val serviceClass: KClass get() = when (DataStore.serviceMode) { Key.modeProxy -> ProxyService::class Key.modeVpn -> VpnService::class Key.modeTransproxy -> TransproxyService::class else -> throw UnknownError() } }