Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.darius.lionvpn.ui.home.Event
import com.darius.lionvpn.ui.home.HomeState
import com.darius.lionvpn.ui.model.Lang
import com.darius.lionvpn.ui.model.SavedConfig
import com.darius.lionvpn.ui.home.CertOperationResult
import com.darius.lionvpn.ui.home.CertOperationType
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
Expand Down Expand Up @@ -43,6 +45,9 @@ class AndroidAppViewModel : ViewModel() {
private val _showInstructionsDialog = MutableStateFlow(false)
val showInstructionsDialog: StateFlow<Boolean> = _showInstructionsDialog.asStateFlow()

private val _certOperationResult = MutableStateFlow<CertOperationResult?>(null)
private val _isCertBusy = MutableStateFlow(false)

// Expose dynamic HomeState compiled reactively from underlying flows using stateIn
@Suppress("UNCHECKED_CAST")
val homeState: StateFlow<HomeState> = combine(
Expand All @@ -52,7 +57,9 @@ class AndroidAppViewModel : ViewModel() {
_selectedConfigIndex,
_rawConfigJson,
_configResetTrigger,
_language
_language,
_certOperationResult,
_isCertBusy
) { array ->
val state = array[INDEX_VPN_STATE] as ConnectionState
val logs = array[INDEX_VPN_LOGS] as List<String>
Expand All @@ -61,6 +68,8 @@ class AndroidAppViewModel : ViewModel() {
val configJson = array[INDEX_RAW_CONFIG_JSON] as String
val resetTrigger = array[INDEX_CONFIG_RESET_TRIGGER] as Int
val lang = array[INDEX_LANGUAGE] as Lang
val certResult = array[INDEX_CERT_OPERATION_RESULT] as CertOperationResult?
val certBusy = array[INDEX_IS_CERT_BUSY] as Boolean

HomeState(
connectionState = state,
Expand All @@ -70,6 +79,9 @@ class AndroidAppViewModel : ViewModel() {
rawConfigJson = configJson,
configResetTrigger = resetTrigger,
language = lang,
certOperationResult = certResult,
isAndroid = true,
isCertBusy = certBusy
)
}.stateIn(
scope = viewModelScope,
Expand Down Expand Up @@ -97,7 +109,15 @@ class AndroidAppViewModel : ViewModel() {
when (event) {
is Event.Connect -> connectVpn()
is Event.InstallCertificate -> generateAndInstallCert()
is Event.UninstallCertificate -> { /* TODO */ }
is Event.UninstallCertificate -> {
_certOperationResult.value = null
_certOperationResult.value = CertOperationResult(
type = CertOperationType.UNINSTALL,
isSuccess = true,
timestamp = -1L // Special timestamp to trigger manual note Toast
)
_uiEffect.emit(AndroidUiEffect.UninstallCertificate)
}
is Event.ClearLogs -> ProxyService.clearLogs()
is Event.AddConfig -> {
addConfig(event.config)
Expand All @@ -122,6 +142,7 @@ class AndroidAppViewModel : ViewModel() {
_language.value = event.language
emitSaveSettings()
}
Event.ClearCertResult -> _certOperationResult.value = null
}
}
}
Expand Down Expand Up @@ -222,5 +243,7 @@ class AndroidAppViewModel : ViewModel() {
private const val INDEX_RAW_CONFIG_JSON = 4
private const val INDEX_CONFIG_RESET_TRIGGER = 5
private const val INDEX_LANGUAGE = 6
private const val INDEX_CERT_OPERATION_RESULT = 7
private const val INDEX_IS_CERT_BUSY = 8
}
}
19 changes: 19 additions & 0 deletions cmp/androidApp/src/main/kotlin/com/darius/lionvpn/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import org.koin.android.ext.android.getKoin
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.compose.koinInject
import android.content.Intent
import android.provider.Settings
import java.io.File

class MainActivity : ComponentActivity() {
Expand Down Expand Up @@ -154,6 +156,23 @@ class MainActivity : ComponentActivity() {
is AndroidUiEffect.CheckAndSaveCertificate -> {
handleCheckAndSaveCertificate()
}
is AndroidUiEffect.UninstallCertificate -> {
val intent = Intent(Settings.ACTION_SECURITY_SETTINGS)
try {
startActivity(intent)
} catch (e: Exception) {
try {
startActivity(Intent(Settings.ACTION_SETTINGS))
} catch (ex: Exception) {
ProxyService.addLogLine("Error opening Settings: ${ex.message}")
Toast.makeText(
this@MainActivity,
"Could not open system settings automatically.",
Toast.LENGTH_LONG
).show()
}
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ sealed interface AndroidUiEffect {
object ConnectVpn : AndroidUiEffect
object CheckAndSaveCertificate : AndroidUiEffect
object SaveSettings : AndroidUiEffect
object UninstallCertificate : AndroidUiEffect
}
Binary file modified cmp/desktopApp/src/macos/resources/MasterHttpRelayVPN
Binary file not shown.
59 changes: 54 additions & 5 deletions cmp/desktopApp/src/main/kotlin/com/darius/lionvpn/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import com.darius.lionvpn.config.saveSavedScripts
import com.darius.lionvpn.ui.home.ConnectionState
import com.darius.lionvpn.ui.home.Event
import com.darius.lionvpn.ui.home.HomeState
import com.darius.lionvpn.ui.home.CertOperationResult
import com.darius.lionvpn.ui.home.CertOperationType
import com.darius.lionvpn.ui.model.Lang
import com.darius.lionvpn.ui.model.SavedConfig
import kotlinx.coroutines.CoroutineExceptionHandler
Expand All @@ -26,6 +28,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.runBlocking

class AppViewModel : ViewModel() {

Expand All @@ -34,6 +37,17 @@ class AppViewModel : ViewModel() {
private val _rawConfigJson = MutableStateFlow("")
private val _configResetTrigger = MutableStateFlow(0)
private val _language = MutableStateFlow(loadLanguagePreference())
private val _certOperationResult = MutableStateFlow<CertOperationResult?>(null)
private val _isCertTrusted = MutableStateFlow(
try {
runBlocking(Dispatchers.IO) {
ProcessRunner.checkCertTrusted()
}
} catch (e: Exception) {
false
}
)
private val _isCertBusy = MutableStateFlow(false)

private val vpnState = ProcessRunner.vpnState
private val vpnLogs = ProcessRunner.vpnLogs
Expand All @@ -46,7 +60,10 @@ class AppViewModel : ViewModel() {
_selectedConfigIndex,
_rawConfigJson,
_configResetTrigger,
_language
_language,
_certOperationResult,
_isCertTrusted,
_isCertBusy
) { array ->
val state = array[INDEX_VPN_STATE] as ConnectionState
val logs = array[INDEX_VPN_LOGS] as List<String>
Expand All @@ -55,15 +72,20 @@ class AppViewModel : ViewModel() {
val configJson = array[INDEX_RAW_CONFIG_JSON] as String
val resetTrigger = array[INDEX_CONFIG_RESET_TRIGGER] as Int
val lang = array[INDEX_LANGUAGE] as Lang

val certResult = array[INDEX_CERT_OPERATION_RESULT] as CertOperationResult?
val trusted = array[INDEX_IS_CERT_TRUSTED] as Boolean
val certBusy = array[INDEX_IS_CERT_BUSY] as Boolean
HomeState(
connectionState = state,
log = logs,
savedConfigs = configs,
selectedConfigIndex = index,
rawConfigJson = configJson,
configResetTrigger = resetTrigger,
language = lang
language = lang,
certOperationResult = certResult,
isCertTrusted = trusted,
isCertBusy = certBusy
)
}.stateIn(
scope = viewModelScope,
Expand All @@ -86,6 +108,13 @@ class AppViewModel : ViewModel() {
}
}

private suspend fun checkCertStatus() {
val trusted = withContext(Dispatchers.IO) {
ProcessRunner.checkCertTrusted()
}
_isCertTrusted.value = trusted
}

private suspend fun loadConfigs() = withContext(Dispatchers.IO) {
val configs = loadSavedScripts()
val index = loadActiveScriptIndex()
Expand Down Expand Up @@ -181,14 +210,30 @@ class AppViewModel : ViewModel() {
viewModelScope.launch(errorHandler) {
when (event) {
Event.InstallCertificate -> {
withContext(Dispatchers.IO) {
_certOperationResult.value = null
_isCertBusy.value = true
val success = withContext(Dispatchers.IO) {
ProcessRunner.installCert()
}
_certOperationResult.value = CertOperationResult(
type = CertOperationType.INSTALL,
isSuccess = success
)
checkCertStatus()
_isCertBusy.value = false
}
Event.UninstallCertificate -> {
withContext(Dispatchers.IO) {
_certOperationResult.value = null
_isCertBusy.value = true
val success = withContext(Dispatchers.IO) {
ProcessRunner.uninstallCert()
}
_certOperationResult.value = CertOperationResult(
type = CertOperationType.UNINSTALL,
isSuccess = success
)
checkCertStatus()
_isCertBusy.value = false
}
Event.Connect -> {
withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -221,6 +266,7 @@ class AppViewModel : ViewModel() {
}
_language.value = lang
}
Event.ClearCertResult -> _certOperationResult.value = null
}
}
}
Expand All @@ -233,5 +279,8 @@ class AppViewModel : ViewModel() {
private const val INDEX_RAW_CONFIG_JSON = 4
private const val INDEX_CONFIG_RESET_TRIGGER = 5
private const val INDEX_LANGUAGE = 6
private const val INDEX_CERT_OPERATION_RESULT = 7
private const val INDEX_IS_CERT_TRUSTED = 8
private const val INDEX_IS_CERT_BUSY = 9
}
}
88 changes: 79 additions & 9 deletions cmp/desktopApp/src/main/kotlin/com/darius/lionvpn/ProcessRunner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import okio.IOException
import org.koin.mp.KoinPlatform.getKoin
import java.io.File
import java.lang.ProcessBuilder
import kotlin.collections.plus
import kotlin.concurrent.thread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

object ProcessRunner {

Expand All @@ -44,7 +45,7 @@ object ProcessRunner {
_vpnLogs.update { it.appendCappedLog(line) }
}

fun installCert() {
suspend fun installCert(): Boolean = withContext(Dispatchers.IO) {
println("Installing Certificate for: $binaryPath")

val configFile = File(getUserDataDirectory(), Constants.Config.FILE_NAME)
Expand All @@ -55,13 +56,29 @@ object ProcessRunner {
else -> ProcessBuilder("pkexec", binaryPath, "--config", configFile.absolutePath, "--install-cert")
}

processBuilder.runProcess { isSuccess ->
if (isSuccess) println("[VPN Process] Certificate was installed successfully!")
else println("[VPN Process] Something went wrong!")
val binaryFile = File(binaryPath)
if (binaryFile.parentFile != null) {
processBuilder.directory(binaryFile.parentFile)
}
processBuilder.redirectErrorStream(true)

try {
val process = processBuilder.start()
val exitCode = process.waitFor()
if (exitCode == 0) {
println("[VPN Process] Certificate was installed successfully!")
true
} else {
println("[VPN Process] Something went wrong! Exit code: $exitCode")
false
}
} catch (e: Exception) {
println("[VPN Process] Failed to start install-cert process: ${e.message}")
false
}
}

fun uninstallCert() {
suspend fun uninstallCert(): Boolean = withContext(Dispatchers.IO) {
println("Uninstalling Certificate for: $binaryPath")

val configFile = File(getUserDataDirectory(), Constants.Config.FILE_NAME)
Expand All @@ -72,9 +89,62 @@ object ProcessRunner {
else -> ProcessBuilder("pkexec", binaryPath, "--config", configFile.absolutePath, "--uninstall-cert")
}

processBuilder.runProcess { isSuccess ->
if (isSuccess) println("[VPN Process] Certificate was uninstalled successfully!")
else println("[VPN Process] Something went wrong!")
val binaryFile = File(binaryPath)
if (binaryFile.parentFile != null) {
processBuilder.directory(binaryFile.parentFile)
}
processBuilder.redirectErrorStream(true)

try {
val process = processBuilder.start()
val exitCode = process.waitFor()
if (exitCode == 0) {
println("[VPN Process] Certificate was uninstalled successfully!")
true
} else {
println("[VPN Process] Something went wrong! Exit code: $exitCode")
false
}
} catch (e: Exception) {
println("[VPN Process] Failed to start uninstall-cert process: ${e.message}")
false
}
}

suspend fun checkCertTrusted(): Boolean = withContext(Dispatchers.IO) {
println("Checking if CA Certificate is trusted via native platform CLI...")

when (platform.os) {
JvmPlatform.OS.WIN -> {
try {
val process = ProcessBuilder("certutil", "-user", "-store", "Root").start()
val output = process.inputStream.bufferedReader().readText()
process.waitFor()
output.contains("MasterHttpRelayVPN", ignoreCase = true)
} catch (e: Exception) {
println("[VPN Process] Windows trust check failed: ${e.message}")
false
}
}
JvmPlatform.OS.MAC -> {
try {
val process = ProcessBuilder("security", "find-certificate", "-a", "-c", "MasterHttpRelayVPN").start()
val output = process.inputStream.bufferedReader().readText()
process.waitFor()
output.trim().isNotEmpty()
} catch (e: Exception) {
println("[VPN Process] macOS trust check failed: ${e.message}")
false
}
}
else -> {
val anchors = listOf(
"/usr/local/share/ca-certificates/MasterHttpRelayVPN.crt",
"/etc/pki/ca-trust/source/anchors/MasterHttpRelayVPN.crt",
"/etc/ca-certificates/trust-source/anchors/MasterHttpRelayVPN.crt"
)
anchors.any { File(it).exists() }
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ actual fun isDebugBuild(): Boolean =
actual fun getCurrentTimeString(): String {
return java.time.LocalTime.now().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss"))
}

actual fun getCurrentTimeMillis(): Long {
return System.currentTimeMillis()
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,15 @@
<string name="stat_status_connecting">در حال اتصال</string>
<string name="stat_status_disconnected">قطع شده</string>

<!-- Certificate Operation Toasts -->
<string name="cert_install_success">گواهی با موفقیت نصب شد!</string>
<string name="cert_install_failure">نصب گواهی انجام نشد.</string>
<string name="cert_uninstall_success">گواهی با موفقیت حذف شد!</string>
<string name="cert_uninstall_failure">حذف گواهی انجام نشد.</string>
<string name="cert_uninstall_android_note">گواهی‌های CA باید به صورت دستی از تنظیمات امنیت سیستم حذف شوند.</string>
<string name="cert_status_trusted">گواهی CA مورد اعتماد و فعال است</string>
<string name="cert_status_not_trusted">گواهی CA مورد اعتماد سیستم نیست</string>

<!-- Limitations Dialog -->
<string name="stat_limitations_title">محدودیت‌ها</string>
<string name="stat_limitations_value">مشاهده</string>
Expand Down
Loading