summaryrefslogtreecommitdiff
path: root/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt
diff options
context:
space:
mode:
Diffstat (limited to 'ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt')
-rw-r--r--ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt308
1 files changed, 308 insertions, 0 deletions
diff --git a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt
new file mode 100644
index 0000000..8813948
--- /dev/null
+++ b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt
@@ -0,0 +1,308 @@
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+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<FeatureServiceState> = _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<String>) {
+ 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<String> {
+ 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<DnsPacket?, DnsPacket?>?) {
+ 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<String>
+ get() = getTorifiedApps()
+ set(value) = saveTorifiedApps(value)
+
+ fun getAvailablesLocations(): Set<String> = EXIT_COUNTRY_CODES
+
+ var httpProxyPort: Int = -1
+ private set
+
+ var socksProxyPort: Int = -1
+ private set
+
+ fun onCleared() {
+ LocalBroadcastManager.getInstance(context).unregisterReceiver(localBroadcastReceiver)
+ }
+}