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