/* * 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) } }