feat: add Test Connection diagnostics to Namecoin settings
Add per-server connection testing with detailed error reporting: - Test Connection button tests each ElectrumX server individually - Shows streaming results: ✅ success with response time, ❌ failure with human-readable error (TLS handshake failed, connection refused, timeout, DNS failed, invalid response, etc.) - Diagnostic card shows: last test timestamp + success count, device info (manufacturer/model/Android/API), TLS version negotiated - ElectrumXClient.testServer() method for single-server diagnostics with TLS version capture from SSL session This gives users (and bug reporters) immediate visibility into why Namecoin resolution may be failing silently on their device.
This commit is contained in:
@@ -161,12 +161,14 @@ class AppModules(
|
||||
// Custom fetcher that considers tor settings and avoids forwarding.
|
||||
val nip05Fetcher = OkHttpNip05Fetcher(roleBasedHttpClientBuilder::okHttpClientForNip05)
|
||||
|
||||
val electrumXClient =
|
||||
ElectrumXClient(
|
||||
socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
|
||||
)
|
||||
|
||||
val namecoinResolver =
|
||||
NamecoinNameResolver(
|
||||
electrumxClient =
|
||||
ElectrumXClient(
|
||||
socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
|
||||
),
|
||||
electrumxClient = electrumXClient,
|
||||
serverListProvider = {
|
||||
// User-configured custom servers take priority
|
||||
namecoinPrefs.customServersOrNull
|
||||
|
||||
@@ -200,7 +200,7 @@ fun AppNavigation(
|
||||
composableFromEnd<Route.AccountBackup> { AccountBackupScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.SecurityFilters> { SecurityFiltersScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.PrivacyOptions> { PrivacyOptionsScreen(Amethyst.instance.torPrefs.value, nav) }
|
||||
composableFromEnd<Route.NamecoinSettings> { NamecoinSettingsScreen(Amethyst.instance.namecoinPrefs, nav) }
|
||||
composableFromEnd<Route.NamecoinSettings> { NamecoinSettingsScreen(Amethyst.instance.namecoinPrefs, Amethyst.instance.electrumXClient, nav) }
|
||||
composableFromEnd<Route.OtsSettings> { OtsSettingsScreen(Amethyst.instance.otsPrefs, Amethyst.instance.torPrefs.value, nav) }
|
||||
composableFromEnd<Route.Bookmarks> { BookmarkListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Drafts> { DraftListScreen(accountViewModel, nav) }
|
||||
|
||||
+3
@@ -38,12 +38,14 @@ import com.vitorpamplona.amethyst.model.preferences.NamecoinSharedPreferences
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumXClient
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NamecoinSettingsScreen(
|
||||
namecoinPrefs: NamecoinSharedPreferences,
|
||||
electrumXClient: ElectrumXClient,
|
||||
nav: INav,
|
||||
) {
|
||||
val namecoinSettings by namecoinPrefs.settings.collectAsState()
|
||||
@@ -75,6 +77,7 @@ fun NamecoinSettingsScreen(
|
||||
onReset = {
|
||||
scope.launch { namecoinPrefs.reset() }
|
||||
},
|
||||
onTestServer = { server -> electrumXClient.testServer(server) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+276
-1
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
@@ -41,6 +42,10 @@ import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -53,20 +58,29 @@ 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.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.namecoin.NamecoinSettings
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_SERVERS
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ServerTestResult
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Complete settings section for Namecoin ElectrumX server configuration.
|
||||
@@ -79,6 +93,7 @@ import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_S
|
||||
* @param onAddServer Called with `host:port[:tcp]` when user adds a server
|
||||
* @param onRemoveServer Called with the server string to remove
|
||||
* @param onReset Called when user resets to defaults
|
||||
* @param onTestServer Suspend function to test a single server
|
||||
*/
|
||||
@Composable
|
||||
fun NamecoinSettingsSection(
|
||||
@@ -87,6 +102,7 @@ fun NamecoinSettingsSection(
|
||||
onAddServer: (String) -> Unit,
|
||||
onRemoveServer: (String) -> Unit,
|
||||
onReset: () -> Unit,
|
||||
onTestServer: suspend (ElectrumxServer) -> ServerTestResult,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier.padding(16.dp)) {
|
||||
@@ -150,12 +166,271 @@ fun NamecoinSettingsSection(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// ── Test Connection ────────────────────────────────
|
||||
TestConnectionSection(
|
||||
settings = settings,
|
||||
onTestServer = onTestServer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sub-composables ────────────────────────────────────────────────────
|
||||
// ── Test Connection ────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun TestConnectionSection(
|
||||
settings: NamecoinSettings,
|
||||
onTestServer: suspend (ElectrumxServer) -> ServerTestResult,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var isTesting by remember { mutableStateOf(false) }
|
||||
var testResults by remember { mutableStateOf<List<ServerTestResult>>(emptyList()) }
|
||||
var lastTestTimestamp by remember { mutableStateOf<Long?>(null) }
|
||||
|
||||
val servers = settings.toElectrumxServers() ?: DEFAULT_ELECTRUMX_SERVERS
|
||||
|
||||
Column {
|
||||
// ── Test button ────────────────────────────────────────
|
||||
Button(
|
||||
onClick = {
|
||||
if (!isTesting) {
|
||||
isTesting = true
|
||||
testResults = emptyList()
|
||||
scope.launch {
|
||||
val results = mutableListOf<ServerTestResult>()
|
||||
for (server in servers) {
|
||||
val result = onTestServer(server)
|
||||
results.add(result)
|
||||
testResults = results.toList()
|
||||
}
|
||||
lastTestTimestamp = System.currentTimeMillis()
|
||||
isTesting = false
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !isTesting,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
if (isTesting) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.namecoin_testing))
|
||||
} else {
|
||||
Text(stringResource(R.string.namecoin_test_connection))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-server results ─────────────────────────────────
|
||||
if (testResults.isNotEmpty()) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
stringResource(R.string.namecoin_test_results),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
|
||||
testResults.forEach { result ->
|
||||
ServerTestResultRow(result)
|
||||
}
|
||||
|
||||
if (isTesting && testResults.size < servers.size) {
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(14.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
"Testing next server…",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Diagnostic card ────────────────────────────────────
|
||||
if (testResults.isNotEmpty() || lastTestTimestamp != null) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
DiagnosticCard(
|
||||
testResults = testResults,
|
||||
lastTestTimestamp = lastTestTimestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerTestResultRow(result: ServerTestResult) {
|
||||
val serverLabel = "${result.server.host}:${result.server.port}"
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 3.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Text(
|
||||
text = if (result.success) "✅" else "❌",
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(end = 6.dp, top = 1.dp),
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = serverLabel,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.namecoin_response_time, result.responseTimeMs),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
if (result.success) {
|
||||
Text(
|
||||
text = stringResource(R.string.namecoin_test_success),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color(0xFF2E8B57),
|
||||
)
|
||||
} else {
|
||||
val errorText = result.error
|
||||
if (errorText != null) {
|
||||
Text(
|
||||
text = errorText,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Diagnostic Card ────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun DiagnosticCard(
|
||||
testResults: List<ServerTestResult>,
|
||||
lastTestTimestamp: Long?,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.namecoin_diagnostics),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
// Last test timestamp
|
||||
if (lastTestTimestamp != null) {
|
||||
val formatted =
|
||||
remember(lastTestTimestamp) {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
.format(Date(lastTestTimestamp))
|
||||
}
|
||||
val successCount = testResults.count { it.success }
|
||||
val totalCount = testResults.size
|
||||
DiagnosticRow(
|
||||
label = stringResource(R.string.namecoin_last_test),
|
||||
value = "$formatted ($successCount/$totalCount OK)",
|
||||
)
|
||||
} else {
|
||||
DiagnosticRow(
|
||||
label = stringResource(R.string.namecoin_last_test),
|
||||
value = stringResource(R.string.namecoin_no_test_yet),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
// Device info
|
||||
DiagnosticRow(
|
||||
label = stringResource(R.string.namecoin_device_info),
|
||||
value = "${Build.MANUFACTURER} ${Build.MODEL}, Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})",
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
// TLS info from test results
|
||||
val tlsVersions =
|
||||
testResults
|
||||
.mapNotNull { it.tlsVersion }
|
||||
.distinct()
|
||||
val tlsDisplay =
|
||||
if (tlsVersions.isNotEmpty()) {
|
||||
tlsVersions.joinToString(", ")
|
||||
} else {
|
||||
"—"
|
||||
}
|
||||
DiagnosticRow(
|
||||
label = stringResource(R.string.namecoin_tls_info),
|
||||
value = tlsDisplay,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DiagnosticRow(
|
||||
label: String,
|
||||
value: String,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(0.35f),
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.weight(0.65f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Original Sub-composables ───────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(
|
||||
|
||||
@@ -1842,6 +1842,17 @@
|
||||
<string name="select_all">Select All</string>
|
||||
<string name="uptime">%1$d%% uptime</string>
|
||||
<string name="namecoin_settings">Namecoin Settings</string>
|
||||
<string name="namecoin_test_connection">Test Connection</string>
|
||||
<string name="namecoin_testing">Testing servers…</string>
|
||||
<string name="namecoin_test_success">Connected</string>
|
||||
<string name="namecoin_test_failed">Failed</string>
|
||||
<string name="namecoin_test_results">Test Results</string>
|
||||
<string name="namecoin_diagnostics">Diagnostics</string>
|
||||
<string name="namecoin_last_test">Last test</string>
|
||||
<string name="namecoin_device_info">Device Info</string>
|
||||
<string name="namecoin_tls_info">TLS Info</string>
|
||||
<string name="namecoin_no_test_yet">No test run yet</string>
|
||||
<string name="namecoin_response_time">%dms</string>
|
||||
<string name="event_sync_title">Relay Sync</string>
|
||||
<string name="event_sync_section">Relay Sync</string>
|
||||
<string name="event_sync_section_explainer">Re-publish your events across all known relays to keep your outbox, inbox, and DM relays up to date. Requires Wi-Fi — this may use a lot of data.</string>
|
||||
|
||||
+11
@@ -72,6 +72,17 @@ sealed class NamecoinLookupException(
|
||||
) : NamecoinLookupException("All ElectrumX servers unreachable", lastError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of testing connectivity to a single ElectrumX server.
|
||||
*/
|
||||
data class ServerTestResult(
|
||||
val server: ElectrumxServer,
|
||||
val success: Boolean,
|
||||
val responseTimeMs: Long,
|
||||
val error: String? = null,
|
||||
val tlsVersion: String? = null,
|
||||
)
|
||||
|
||||
/** Well-known public Namecoin ElectrumX servers (clearnet). */
|
||||
val DEFAULT_ELECTRUMX_SERVERS =
|
||||
listOf(
|
||||
|
||||
+135
@@ -150,6 +150,141 @@ class ElectrumXClient(
|
||||
throw NamecoinLookupException.ServersUnreachable(lastError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connectivity to a single ElectrumX server.
|
||||
*
|
||||
* Connects, negotiates protocol version, and optionally resolves a test
|
||||
* name. Returns detailed results including response time, TLS version,
|
||||
* and human-readable error messages.
|
||||
*
|
||||
* @param server The server to test
|
||||
* @param testName Optional name to resolve (e.g. "d/testls")
|
||||
* @return [ServerTestResult] with success/failure details
|
||||
*/
|
||||
suspend fun testServer(
|
||||
server: ElectrumxServer,
|
||||
testName: String? = "d/testls",
|
||||
): ServerTestResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
try {
|
||||
val socket = createSocket(server)
|
||||
socket.soTimeout = readTimeoutMs.toInt()
|
||||
|
||||
val tlsVersion =
|
||||
if (socket is javax.net.ssl.SSLSocket) {
|
||||
socket.session.protocol
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val writer = PrintWriter(socket.getOutputStream(), true)
|
||||
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
|
||||
|
||||
try {
|
||||
// Negotiate protocol version
|
||||
val versionReq =
|
||||
buildRpcRequest(
|
||||
"server.version",
|
||||
listOf("AmethystNMC/0.1", PROTOCOL_VERSION),
|
||||
)
|
||||
writer.println(versionReq)
|
||||
val versionResponse =
|
||||
reader.readLine()
|
||||
?: return@withContext ServerTestResult(
|
||||
server = server,
|
||||
success = false,
|
||||
responseTimeMs = System.currentTimeMillis() - startTime,
|
||||
error = "Server returned empty response",
|
||||
tlsVersion = tlsVersion,
|
||||
)
|
||||
|
||||
// If a test name is provided, try to resolve it
|
||||
if (testName != null) {
|
||||
val nameScript =
|
||||
buildNameIndexScript(testName.toByteArray(Charsets.US_ASCII))
|
||||
val scriptHash = electrumScriptHash(nameScript)
|
||||
val historyReq =
|
||||
buildRpcRequest(
|
||||
"blockchain.scripthash.get_history",
|
||||
listOf(scriptHash),
|
||||
)
|
||||
writer.println(historyReq)
|
||||
reader.readLine() // consume response
|
||||
}
|
||||
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
ServerTestResult(
|
||||
server = server,
|
||||
success = true,
|
||||
responseTimeMs = elapsed,
|
||||
tlsVersion = tlsVersion,
|
||||
)
|
||||
} finally {
|
||||
runCatching { writer.close() }
|
||||
runCatching { reader.close() }
|
||||
runCatching { socket.close() }
|
||||
}
|
||||
} catch (e: java.net.ConnectException) {
|
||||
ServerTestResult(
|
||||
server = server,
|
||||
success = false,
|
||||
responseTimeMs = System.currentTimeMillis() - startTime,
|
||||
error = "Connection refused",
|
||||
)
|
||||
} catch (e: java.net.SocketTimeoutException) {
|
||||
ServerTestResult(
|
||||
server = server,
|
||||
success = false,
|
||||
responseTimeMs = System.currentTimeMillis() - startTime,
|
||||
error = "Connection timed out after ${connectTimeoutMs / 1000}s",
|
||||
)
|
||||
} catch (e: java.net.UnknownHostException) {
|
||||
ServerTestResult(
|
||||
server = server,
|
||||
success = false,
|
||||
responseTimeMs = System.currentTimeMillis() - startTime,
|
||||
error = "Server unreachable (DNS resolution failed)",
|
||||
)
|
||||
} catch (e: javax.net.ssl.SSLHandshakeException) {
|
||||
val detail =
|
||||
if (e.message?.contains("self-signed", ignoreCase = true) == true ||
|
||||
e.message?.contains("anchor", ignoreCase = true) == true
|
||||
) {
|
||||
"TLS handshake failed (self-signed certificate rejected)"
|
||||
} else {
|
||||
"TLS handshake failed: ${e.message?.take(100) ?: "unknown error"}"
|
||||
}
|
||||
ServerTestResult(
|
||||
server = server,
|
||||
success = false,
|
||||
responseTimeMs = System.currentTimeMillis() - startTime,
|
||||
error = detail,
|
||||
)
|
||||
} catch (e: javax.net.ssl.SSLException) {
|
||||
ServerTestResult(
|
||||
server = server,
|
||||
success = false,
|
||||
responseTimeMs = System.currentTimeMillis() - startTime,
|
||||
error = "TLS error: ${e.message?.take(100) ?: "unknown"}",
|
||||
)
|
||||
} catch (e: java.io.IOException) {
|
||||
ServerTestResult(
|
||||
server = server,
|
||||
success = false,
|
||||
responseTimeMs = System.currentTimeMillis() - startTime,
|
||||
error = "I/O error: ${e.message?.take(100) ?: "unknown"}",
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
ServerTestResult(
|
||||
server = server,
|
||||
success = false,
|
||||
responseTimeMs = System.currentTimeMillis() - startTime,
|
||||
error = e.message?.take(150) ?: "Unknown error",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── internals ──────────────────────────────────────────────────────
|
||||
|
||||
private fun connectAndQuery(
|
||||
|
||||
Reference in New Issue
Block a user