From 9d55978063947d5865bb3fa4e0c2ebef78f78812 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Mon, 6 Nov 2023 08:14:27 +0000 Subject: epic18: Manage VPN services for Tor or Tracker control --- .../e/advancedprivacy/ipscrambler/KoinModule.kt | 2 +- .../ipscrambler/OrbotServiceSupervisor.kt | 308 -------------------- .../advancedprivacy/ipscrambler/OrbotSupervisor.kt | 312 +++++++++++++++++++++ 3 files changed, 313 insertions(+), 309 deletions(-) delete mode 100644 ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt create mode 100644 ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotSupervisor.kt (limited to 'ipscrambling/src/main/java/foundation') diff --git a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt index 79aeb05..d9ef0be 100644 --- a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt +++ b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt @@ -21,5 +21,5 @@ import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val ipScramblerModule = module { - singleOf(::OrbotServiceSupervisor) + singleOf(::OrbotSupervisor) } diff --git a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt deleted file mode 100644 index 8813948..0000000 --- a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2021 E FOUNDATION - * - * 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 foundation.e.advancedprivacy.ipscrambler - -import android.annotation.SuppressLint -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.net.VpnService -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.os.Message -import androidx.localbroadcastmanager.content.LocalBroadcastManager -import foundation.e.advancedprivacy.domain.entities.FeatureServiceState -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.pcap4j.packet.DnsPacket -import org.torproject.android.service.OrbotConstants -import org.torproject.android.service.OrbotConstants.ACTION_STOP_FOREGROUND_TASK -import org.torproject.android.service.OrbotService -import org.torproject.android.service.util.Prefs -import timber.log.Timber -import java.security.InvalidParameterException -import java.util.function.Function - -@SuppressLint("CommitPrefEdits") -class OrbotServiceSupervisor( - private val context: Context, - private val coroutineScope: CoroutineScope, -) { - private val _state = MutableStateFlow(FeatureServiceState.OFF) - val state: StateFlow = _state - - enum class Status { - OFF, ON, STARTING, STOPPING, START_DISABLED - } - companion object { - private val EXIT_COUNTRY_CODES = setOf("DE", "AT", "SE", "CH", "IS", "CA", "US", "ES", "FR", "BG", "PL", "AU", "BR", "CZ", "DK", "FI", "GB", "HU", "NL", "JP", "RO", "RU", "SG", "SK") - - // Key where exit country is stored by orbot service. - private const val PREFS_KEY_EXIT_NODES = "pref_exit_nodes" - // Copy of the package private OrbotService.NOTIFY_ID value. - // const val ORBOT_SERVICE_NOTIFY_ID_COPY = 1 - } - - private var currentStatus: Status? = null - - private val localBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action ?: return - if (action == OrbotConstants.ACTION_RUNNING_SYNC) { - try { - intent.getStringExtra(OrbotConstants.EXTRA_STATUS)?.let { - val newStatus = Status.valueOf(it) - currentStatus = newStatus - } - } catch (e: Exception) { - Timber.e("Can't parse Orbot service status.") - } - return - } - - val msg = messageHandler.obtainMessage() - msg.obj = action - msg.data = intent.extras - messageHandler.sendMessage(msg) - } - } - - private val messageHandler: Handler = object : Handler(Looper.getMainLooper()) { - override fun handleMessage(msg: Message) { - val action = msg.obj as? String ?: return - val data = msg.data - when (action) { - OrbotConstants.LOCAL_ACTION_PORTS -> { - httpProxyPort = data.getInt(OrbotService.EXTRA_HTTP_PROXY_PORT, -1) - socksProxyPort = data.getInt(OrbotService.EXTRA_SOCKS_PROXY_PORT, -1) - } - - OrbotConstants.LOCAL_ACTION_STATUS -> - data.getString(OrbotConstants.EXTRA_STATUS)?.let { - try { - val newStatus = Status.valueOf(it) - updateStatus(newStatus, force = true) - } catch (e: Exception) { - Timber.e("Can't parse Orbot service status.") - } - } - OrbotConstants.LOCAL_ACTION_LOG, - OrbotConstants.LOCAL_ACTION_BANDWIDTH -> {} // Unused in Advanced Privacy - } - super.handleMessage(msg) - } - } - - init { - Prefs.setContext(context) - - val lbm = LocalBroadcastManager.getInstance(context) - lbm.registerReceiver( - localBroadcastReceiver, - IntentFilter(OrbotConstants.LOCAL_ACTION_STATUS) - ) - lbm.registerReceiver( - localBroadcastReceiver, - IntentFilter(OrbotConstants.LOCAL_ACTION_BANDWIDTH) - ) - lbm.registerReceiver( - localBroadcastReceiver, - IntentFilter(OrbotConstants.LOCAL_ACTION_LOG) - ) - lbm.registerReceiver( - localBroadcastReceiver, - IntentFilter(OrbotConstants.LOCAL_ACTION_PORTS) - ) - lbm.registerReceiver( - localBroadcastReceiver, - IntentFilter(OrbotConstants.ACTION_RUNNING_SYNC) - ) - - Prefs.getSharedPrefs(context).edit() - .putInt(OrbotConstants.PREFS_DNS_PORT, OrbotConstants.TOR_DNS_PORT_DEFAULT) - .apply() - } - - private fun updateStatus(status: Status, force: Boolean = false) { - if (force || status != currentStatus) { - val newState = when (status) { - Status.OFF -> FeatureServiceState.OFF - Status.ON -> FeatureServiceState.ON - Status.STARTING -> FeatureServiceState.STARTING - Status.STOPPING, - Status.START_DISABLED -> FeatureServiceState.STOPPING - } - - coroutineScope.launch(Dispatchers.IO) { - _state.update { currentState -> - if (newState == FeatureServiceState.OFF && - currentState == FeatureServiceState.STOPPING - ) { - // Wait for orbot to relax before allowing user to reactivate it. - delay(1000) - } - newState - } - } - } - } - - private fun isServiceRunning(): Boolean { - // Reset status, and then ask to refresh it synchronously. - currentStatus = Status.OFF - LocalBroadcastManager.getInstance(context) - .sendBroadcastSync(Intent(OrbotConstants.ACTION_CHECK_RUNNING_SYNC)) - return currentStatus != Status.OFF - } - - private fun sendIntentToService(action: String, extra: Bundle? = null) { - val intent = Intent(context, OrbotService::class.java) - intent.action = action - extra?.let { intent.putExtras(it) } - context.startService(intent) - } - - @SuppressLint("ApplySharedPref") - private fun saveTorifiedApps(packageNames: Collection) { - packageNames.joinToString("|") - Prefs.getSharedPrefs(context).edit().putString( - OrbotConstants.PREFS_KEY_TORIFIED, packageNames.joinToString("|") - ).commit() - - if (isServiceRunning()) { - sendIntentToService(OrbotConstants.ACTION_RESTART_VPN) - } - } - - private fun getTorifiedApps(): Set { - val list = Prefs.getSharedPrefs(context).getString(OrbotConstants.PREFS_KEY_TORIFIED, "") - ?.split("|") - return if (list == null || list == listOf("")) { - emptySet() - } else { - list.toSet() - } - } - - @SuppressLint("ApplySharedPref") - suspend fun setExitCountryCode(countryCode: String) { - withContext(Dispatchers.IO) { - val countryParam = when { - countryCode.isEmpty() -> "" - countryCode in EXIT_COUNTRY_CODES -> "{$countryCode}" - else -> throw InvalidParameterException( - "Only these countries are available: ${EXIT_COUNTRY_CODES.joinToString { ", " }}" - ) - } - - if (isServiceRunning()) { - val extra = Bundle() - extra.putString("exit", countryParam) - sendIntentToService(OrbotConstants.CMD_SET_EXIT, extra) - } else { - Prefs.getSharedPrefs(context) - .edit().putString(PREFS_KEY_EXIT_NODES, countryParam) - .commit() - } - } - } - - fun getExitCountryCode(): String { - val raw = Prefs.getExitNodes() - return if (raw.isEmpty()) raw else raw.slice(1..2) - } - - fun prepareAndroidVpn(): Intent? { - return VpnService.prepare(context) - } - - fun setDNSFilter(shouldBlock: Function?) { - OrbotService.shouldBlock = shouldBlock - } - - fun start(enableNotification: Boolean) { - Prefs.enableNotification(enableNotification) - Prefs.putUseVpn(true) - Prefs.putStartOnBoot(true) - - sendIntentToService(OrbotConstants.ACTION_START) - sendIntentToService(OrbotConstants.ACTION_START_VPN) - } - - fun stop() { - if (!isServiceRunning()) return - - updateStatus(Status.STOPPING) - - Prefs.putUseVpn(false) - Prefs.putStartOnBoot(false) - - sendIntentToService(OrbotConstants.ACTION_STOP_VPN) - sendIntentToService( - action = OrbotConstants.ACTION_STOP, - extra = Bundle().apply { putBoolean(ACTION_STOP_FOREGROUND_TASK, true) } - ) - stoppingWatchdog(5) - } - - private fun stoppingWatchdog(countDown: Int) { - Handler(Looper.getMainLooper()).postDelayed( - { - if (isServiceRunning() && countDown > 0) { - stoppingWatchdog(countDown - 1) - } else { - updateStatus(Status.OFF, force = true) - } - }, - 500 - ) - } - - fun requestStatus() { - if (isServiceRunning()) { - sendIntentToService(OrbotConstants.ACTION_STATUS) - } else { - updateStatus(Status.OFF, force = true) - } - } - - var appList: Set - get() = getTorifiedApps() - set(value) = saveTorifiedApps(value) - - fun getAvailablesLocations(): Set = EXIT_COUNTRY_CODES - - var httpProxyPort: Int = -1 - private set - - var socksProxyPort: Int = -1 - private set - - fun onCleared() { - LocalBroadcastManager.getInstance(context).unregisterReceiver(localBroadcastReceiver) - } -} diff --git a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotSupervisor.kt b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotSupervisor.kt new file mode 100644 index 0000000..6e0e205 --- /dev/null +++ b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotSupervisor.kt @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2021 E FOUNDATION + * + * 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 foundation.e.advancedprivacy.ipscrambler + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.VpnService +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Message +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import foundation.e.advancedprivacy.domain.entities.FeatureState +import foundation.e.advancedprivacy.externalinterfaces.servicesupervisors.FeatureSupervisor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.pcap4j.packet.DnsPacket +import org.torproject.android.service.OrbotConstants +import org.torproject.android.service.OrbotConstants.ACTION_STOP_FOREGROUND_TASK +import org.torproject.android.service.OrbotService +import org.torproject.android.service.util.Prefs +import timber.log.Timber +import java.security.InvalidParameterException +import java.util.function.Function + +@SuppressLint("CommitPrefEdits") +class OrbotSupervisor( + private val context: Context, + private val coroutineScope: CoroutineScope, +) : FeatureSupervisor { + private val _state = MutableStateFlow(FeatureState.OFF) + override val state: StateFlow = _state + + enum class Status { + OFF, ON, STARTING, STOPPING, START_DISABLED + } + companion object { + private val EXIT_COUNTRY_CODES = setOf("DE", "AT", "SE", "CH", "IS", "CA", "US", "ES", "FR", "BG", "PL", "AU", "BR", "CZ", "DK", "FI", "GB", "HU", "NL", "JP", "RO", "RU", "SG", "SK") + + // Key where exit country is stored by orbot service. + private const val PREFS_KEY_EXIT_NODES = "pref_exit_nodes" + // Copy of the package private OrbotService.NOTIFY_ID value. + // const val ORBOT_SERVICE_NOTIFY_ID_COPY = 1 + } + + private var currentStatus: Status? = null + + private val localBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action ?: return + if (action == OrbotConstants.ACTION_RUNNING_SYNC) { + try { + intent.getStringExtra(OrbotConstants.EXTRA_STATUS)?.let { + val newStatus = Status.valueOf(it) + currentStatus = newStatus + } + } catch (e: Exception) { + Timber.e("Can't parse Orbot service status.") + } + return + } + + val msg = messageHandler.obtainMessage() + msg.obj = action + msg.data = intent.extras + messageHandler.sendMessage(msg) + } + } + + private val messageHandler: Handler = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + val action = msg.obj as? String ?: return + val data = msg.data + when (action) { + OrbotConstants.LOCAL_ACTION_PORTS -> { + httpProxyPort = data.getInt(OrbotService.EXTRA_HTTP_PROXY_PORT, -1) + socksProxyPort = data.getInt(OrbotService.EXTRA_SOCKS_PROXY_PORT, -1) + } + + OrbotConstants.LOCAL_ACTION_STATUS -> + data.getString(OrbotConstants.EXTRA_STATUS)?.let { + try { + val newStatus = Status.valueOf(it) + updateStatus(newStatus, force = true) + } catch (e: Exception) { + Timber.e("Can't parse Orbot service status.") + } + } + OrbotConstants.LOCAL_ACTION_LOG, + OrbotConstants.LOCAL_ACTION_BANDWIDTH -> {} // Unused in Advanced Privacy + } + super.handleMessage(msg) + } + } + + init { + Prefs.setContext(context) + + val lbm = LocalBroadcastManager.getInstance(context) + lbm.registerReceiver( + localBroadcastReceiver, + IntentFilter(OrbotConstants.LOCAL_ACTION_STATUS) + ) + lbm.registerReceiver( + localBroadcastReceiver, + IntentFilter(OrbotConstants.LOCAL_ACTION_BANDWIDTH) + ) + lbm.registerReceiver( + localBroadcastReceiver, + IntentFilter(OrbotConstants.LOCAL_ACTION_LOG) + ) + lbm.registerReceiver( + localBroadcastReceiver, + IntentFilter(OrbotConstants.LOCAL_ACTION_PORTS) + ) + lbm.registerReceiver( + localBroadcastReceiver, + IntentFilter(OrbotConstants.ACTION_RUNNING_SYNC) + ) + + Prefs.getSharedPrefs(context).edit() + .putInt(OrbotConstants.PREFS_DNS_PORT, OrbotConstants.TOR_DNS_PORT_DEFAULT) + .apply() + } + + private fun updateStatus(status: Status, force: Boolean = false) { + if (force || status != currentStatus) { + val newState = when (status) { + Status.OFF -> FeatureState.OFF + Status.ON -> FeatureState.ON + Status.STARTING -> FeatureState.STARTING + Status.STOPPING, + Status.START_DISABLED -> FeatureState.STOPPING + } + + coroutineScope.launch(Dispatchers.IO) { + _state.update { currentState -> + if (newState == FeatureState.OFF && + currentState == FeatureState.STOPPING + ) { + // Wait for orbot to relax before allowing user to reactivate it. + delay(1000) + } + newState + } + } + } + } + + private fun isServiceRunning(): Boolean { + // Reset status, and then ask to refresh it synchronously. + currentStatus = Status.OFF + LocalBroadcastManager.getInstance(context) + .sendBroadcastSync(Intent(OrbotConstants.ACTION_CHECK_RUNNING_SYNC)) + return currentStatus != Status.OFF + } + + private fun sendIntentToService(action: String, extra: Bundle? = null) { + val intent = Intent(context, OrbotService::class.java) + intent.action = action + extra?.let { intent.putExtras(it) } + context.startService(intent) + } + + @SuppressLint("ApplySharedPref") + private fun saveTorifiedApps(packageNames: Collection) { + packageNames.joinToString("|") + Prefs.getSharedPrefs(context).edit().putString( + OrbotConstants.PREFS_KEY_TORIFIED, packageNames.joinToString("|") + ).commit() + + if (isServiceRunning()) { + sendIntentToService(OrbotConstants.ACTION_RESTART_VPN) + } + } + + private fun getTorifiedApps(): Set { + val list = Prefs.getSharedPrefs(context).getString(OrbotConstants.PREFS_KEY_TORIFIED, "") + ?.split("|") + return if (list == null || list == listOf("")) { + emptySet() + } else { + list.toSet() + } + } + + @SuppressLint("ApplySharedPref") + suspend fun setExitCountryCode(countryCode: String) { + withContext(Dispatchers.IO) { + val countryParam = when { + countryCode.isEmpty() -> "" + countryCode in EXIT_COUNTRY_CODES -> "{$countryCode}" + else -> throw InvalidParameterException( + "Only these countries are available: ${EXIT_COUNTRY_CODES.joinToString { ", " }}" + ) + } + + if (isServiceRunning()) { + val extra = Bundle() + extra.putString("exit", countryParam) + sendIntentToService(OrbotConstants.CMD_SET_EXIT, extra) + } else { + Prefs.getSharedPrefs(context) + .edit().putString(PREFS_KEY_EXIT_NODES, countryParam) + .commit() + } + } + } + + fun getExitCountryCode(): String { + val raw = Prefs.getExitNodes() + return if (raw.isEmpty()) raw else raw.slice(1..2) + } + + fun prepareAndroidVpn(): Intent? { + return VpnService.prepare(context) + } + + fun setDNSFilter(shouldBlock: Function?) { + OrbotService.shouldBlock = shouldBlock + } + + override fun start(): Boolean { + val enableNotification = OrbotService.shouldBlock != null + Prefs.enableNotification(enableNotification) + Prefs.putUseVpn(true) + Prefs.putStartOnBoot(true) + + sendIntentToService(OrbotConstants.ACTION_START) + sendIntentToService(OrbotConstants.ACTION_START_VPN) + return true + } + + override fun stop(): Boolean { + if (!isServiceRunning()) return false + + updateStatus(Status.STOPPING) + + Prefs.putUseVpn(false) + Prefs.putStartOnBoot(false) + + sendIntentToService(OrbotConstants.ACTION_STOP_VPN) + sendIntentToService( + action = OrbotConstants.ACTION_STOP, + extra = Bundle().apply { putBoolean(ACTION_STOP_FOREGROUND_TASK, true) } + ) + stoppingWatchdog(5) + return true + } + + private fun stoppingWatchdog(countDown: Int) { + Handler(Looper.getMainLooper()).postDelayed( + { + if (isServiceRunning() && countDown > 0) { + stoppingWatchdog(countDown - 1) + } else { + updateStatus(Status.OFF, force = true) + } + }, + 500 + ) + } + + fun requestStatus() { + if (isServiceRunning()) { + sendIntentToService(OrbotConstants.ACTION_STATUS) + } else { + updateStatus(Status.OFF, force = true) + } + } + + var appList: Set + get() = getTorifiedApps() + set(value) = saveTorifiedApps(value) + + fun getAvailablesLocations(): Set = EXIT_COUNTRY_CODES + + var httpProxyPort: Int = -1 + private set + + var socksProxyPort: Int = -1 + private set + + fun onCleared() { + LocalBroadcastManager.getInstance(context).unregisterReceiver(localBroadcastReceiver) + } +} -- cgit v1.2.1