____ _ _______ __
/ __ \ | /| / / __/ |/ /
/ /_/ / |/ |/ / _// /
\____/|__/|__/___/_||_/
owenclave / app/src/main/java/io/nekohasekai/sagernet/ui/compose/screens/ProbeCertScreen.kt
package io.nekohasekai.sagernet.ui.compose.screens
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.ui.compose.components.OwenclaveTopAppBar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import libexclavecore.Libexclavecore
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProbeCertScreen(
onBack: () -> Unit,
) {
val scope = rememberCoroutineScope()
var server by remember { mutableStateOf("example.com") }
var port by remember { mutableStateOf("443") }
var sni by remember { mutableStateOf("example.com") }
var alpn by remember { mutableStateOf("h2,http/1.1") }
var protocol by remember { mutableStateOf(0) }
var certResult by remember { mutableStateOf("") }
var verifyError by remember { mutableStateOf("") }
var error by remember { mutableStateOf("") }
var running by remember { mutableStateOf(false) }
var certHash by remember { mutableStateOf("") }
var hashType by remember { mutableStateOf(0) }
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
containerColor = MaterialTheme.colorScheme.surfaceContainer,
topBar = {
OwenclaveTopAppBar(
title = "Probe Certificate",
navigationIcon = Icons.AutoMirrored.Filled.ArrowBack,
onNavigationClick = onBack,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
Column {
Card(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
) {
Column(modifier = Modifier.padding(20.dp)) {
io.nekohasekai.sagernet.ui.compose.components.ExpressiveTextField(
value = server,
onValueChange = { server = it; if (sni.isEmpty()) sni = it },
label = "Server",
singleLine = true,
)
Spacer(Modifier.height(8.dp))
io.nekohasekai.sagernet.ui.compose.components.ExpressiveTextField(
value = port,
onValueChange = { port = it.filter { c -> c.isDigit() } },
label = "Port",
singleLine = true,
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
),
)
Spacer(Modifier.height(8.dp))
io.nekohasekai.sagernet.ui.compose.components.ExpressiveTextField(
value = sni,
onValueChange = { sni = it },
label = "SNI / Server Name",
singleLine = true,
)
Spacer(Modifier.height(8.dp))
io.nekohasekai.sagernet.ui.compose.components.ExpressiveTextField(
value = alpn,
onValueChange = { alpn = it },
label = "ALPN",
singleLine = true,
)
Spacer(Modifier.height(8.dp))
androidx.compose.foundation.layout.Row(
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
) {
androidx.compose.material3.RadioButton(
selected = protocol == 0,
onClick = { protocol = 0; alpn = "h2,http/1.1" },
)
Text("TLS", style = MaterialTheme.typography.bodyMedium)
Spacer(Modifier.width(16.dp))
androidx.compose.material3.RadioButton(
selected = protocol == 1,
onClick = { protocol = 1; alpn = "h3" },
)
Text("QUIC", style = MaterialTheme.typography.bodyMedium)
}
Spacer(Modifier.height(16.dp))
Button(
onClick = {
running = true
error = ""
certResult = ""
verifyError = ""
certHash = ""
scope.launch(Dispatchers.IO) {
try {
val portNum = port.toInt()
val certProber = Libexclavecore.newCertProber()
if (SagerNet.started && DataStore.startedProfile > 0) {
certProber.useUDS(
SagerNet.deviceStorage.noBackupFilesDir.toString() + "/ipc.sock"
)
}
val result = if (protocol == 0) {
certProber.probeTLS(server, portNum, sni, alpn)
} else {
certProber.probeQUIC(server, portNum, sni, alpn)
}
withContext(Dispatchers.Main) {
if (result.error.isNotEmpty()) {
error = result.error
} else {
certResult = result.cert
if (result.verifyError.isNotEmpty()) {
verifyError = result.verifyError
}
}
running = false
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
error = e.message ?: e.toString()
running = false
}
}
}
},
enabled = !running && server.isNotEmpty(),
modifier = Modifier.fillMaxWidth(),
) { Text(if (running) "Probing..." else "Probe") }
}
}
if (error.isNotEmpty()) {
Spacer(Modifier.height(16.dp))
Card(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.large) {
Text(
text = error,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(20.dp),
)
}
}
if (verifyError.isNotEmpty()) {
Spacer(Modifier.height(16.dp))
Card(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.large) {
Column(modifier = Modifier.padding(20.dp)) {
Text("Verify Error", style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.error)
Spacer(Modifier.height(8.dp))
Text(verifyError, style = MaterialTheme.typography.bodySmall)
}
}
}
if (certResult.isNotEmpty()) {
Spacer(Modifier.height(16.dp))
Card(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.large) {
Column(modifier = Modifier.padding(20.dp)) {
Text("Certificate (PEM)", style = MaterialTheme.typography.titleSmall)
Spacer(Modifier.height(8.dp))
Text(
text = certResult,
style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
)
}
}
Spacer(Modifier.height(16.dp))
Card(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.large) {
Column(modifier = Modifier.padding(20.dp)) {
Text("Certificate Hash", style = MaterialTheme.typography.titleSmall)
Spacer(Modifier.height(8.dp))
androidx.compose.foundation.layout.Row(
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
) {
androidx.compose.material3.RadioButton(selected = hashType == 0, onClick = { hashType = 0 })
Text("Cert SHA256", style = MaterialTheme.typography.bodySmall)
Spacer(Modifier.width(8.dp))
androidx.compose.material3.RadioButton(selected = hashType == 1, onClick = { hashType = 1 })
Text("PubKey SHA256", style = MaterialTheme.typography.bodySmall)
Spacer(Modifier.width(8.dp))
androidx.compose.material3.RadioButton(selected = hashType == 2, onClick = { hashType = 2 })
Text("Chain SHA256", style = MaterialTheme.typography.bodySmall)
}
Spacer(Modifier.height(8.dp))
val hash = remember(certResult, hashType) {
try {
when (hashType) {
0 -> Libexclavecore.calculatePEMCertSHA256Hash(certResult)
1 -> Libexclavecore.calculatePEMCertPublicKeySHA256Hash(certResult)
2 -> Libexclavecore.calculatePEMCertChainSHA256Hash(certResult)
else -> ""
}
} catch (_: Exception) { "" }
}
if (hash.isNotEmpty()) {
Text(
text = hash,
style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
)
}
}
}
}
}
}
}
}