mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
@@ -3,11 +3,12 @@ package ru.fromchat.api.instance
|
|||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import ru.fromchat.api.ApiClient
|
import ru.fromchat.api.ApiClient
|
||||||
import ru.fromchat.api.local.WebSocketManager
|
import ru.fromchat.api.local.WebSocketManager
|
||||||
import ru.fromchat.api.local.db.store.InstanceRegistryStore
|
|
||||||
import ru.fromchat.config.ServerConfigData
|
|
||||||
import ru.fromchat.api.local.cache.CacheContext
|
import ru.fromchat.api.local.cache.CacheContext
|
||||||
|
import ru.fromchat.api.local.db.store.InstanceRegistryStore
|
||||||
import ru.fromchat.config.ServerConfig
|
import ru.fromchat.config.ServerConfig
|
||||||
|
import ru.fromchat.config.ServerConfigData
|
||||||
import ru.fromchat.legal.DocumentRepository
|
import ru.fromchat.legal.DocumentRepository
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
|
|
||||||
sealed interface ServerProbeResult {
|
sealed interface ServerProbeResult {
|
||||||
@@ -51,20 +52,26 @@ fun Throwable.isSslProtocolError(): Boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun probeCallsReachable(config: ServerConfigData): Boolean {
|
suspend fun probeCallsReachable(config: ServerConfigData) = runCatching {
|
||||||
val urlScheme = if (config.httpsEnabled) "https" else "http"
|
withTimeout(CALLS_PROBE_MS.milliseconds) {
|
||||||
val host = config.serverIp.trim()
|
ApiClient.probeHttpGet(
|
||||||
val authorityHost = host.removePrefix("[").removeSuffix("]").ifEmpty { host }
|
buildString {
|
||||||
// Caddy rewrites this to LiveKit "/" (plain "OK"). GET /rtc is always 404 without a WS upgrade.
|
append(if (config.httpsEnabled) "https" else "http")
|
||||||
val url = "$urlScheme://$authorityHost:${config.apiPort}/livekit-health"
|
append("://")
|
||||||
return runCatching {
|
append(
|
||||||
withTimeout(CALLS_PROBE_MS) {
|
config.serverIp.trim().let {
|
||||||
ApiClient.probeHttpGet(url)
|
it.removePrefix("[").removeSuffix("]").ifEmpty { it }
|
||||||
}
|
}
|
||||||
}.getOrDefault(false)
|
)
|
||||||
}
|
append(":")
|
||||||
|
append(config.apiPort)
|
||||||
|
append("/livekit-health")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.getOrDefault(false)
|
||||||
|
|
||||||
suspend fun probeServer(config: ServerConfigData): ServerProbeResult =
|
suspend fun probeServer(config: ServerConfigData) =
|
||||||
probeServerEndpoint(
|
probeServerEndpoint(
|
||||||
host = config.serverIp,
|
host = config.serverIp,
|
||||||
port = config.apiPort,
|
port = config.apiPort,
|
||||||
@@ -82,44 +89,51 @@ suspend fun probeServerEndpoint(
|
|||||||
httpsPreferred: Boolean,
|
httpsPreferred: Boolean,
|
||||||
schemeExplicit: Boolean,
|
schemeExplicit: Boolean,
|
||||||
): Pair<ServerConfigData, ServerProbeResult> {
|
): Pair<ServerConfigData, ServerProbeResult> {
|
||||||
val schemes = when {
|
|
||||||
schemeExplicit -> listOf(httpsPreferred)
|
|
||||||
else -> listOf(true, false)
|
|
||||||
}
|
|
||||||
var lastConfig = ServerConfigData(
|
var lastConfig = ServerConfigData(
|
||||||
serverIp = host,
|
serverIp = host,
|
||||||
apiPort = port,
|
apiPort = port,
|
||||||
callsPort = port,
|
callsPort = port,
|
||||||
httpsEnabled = httpsPreferred,
|
httpsEnabled = httpsPreferred,
|
||||||
)
|
)
|
||||||
|
|
||||||
var lastResult: ServerProbeResult = ServerProbeResult.Unreachable
|
var lastResult: ServerProbeResult = ServerProbeResult.Unreachable
|
||||||
for ((index, https) in schemes.withIndex()) {
|
|
||||||
val config = ServerConfigData(
|
when {
|
||||||
serverIp = host,
|
schemeExplicit -> listOf(httpsPreferred)
|
||||||
apiPort = port,
|
else -> listOf(true, false)
|
||||||
callsPort = port,
|
}.apply {
|
||||||
httpsEnabled = https,
|
forEachIndexed { index, https ->
|
||||||
)
|
val config = ServerConfigData(
|
||||||
lastConfig = config
|
serverIp = host,
|
||||||
val (result, failure) = probeServerOnce(config)
|
apiPort = port,
|
||||||
lastResult = result
|
callsPort = port,
|
||||||
when (result) {
|
httpsEnabled = https,
|
||||||
is ServerProbeResult.Supported,
|
)
|
||||||
ServerProbeResult.Unsupported,
|
|
||||||
-> return config to result
|
lastConfig = config
|
||||||
ServerProbeResult.Timeout,
|
val (result, failure) = probeServerOnce(config)
|
||||||
ServerProbeResult.Unreachable,
|
lastResult = result
|
||||||
-> {
|
|
||||||
val hasHttpFallback = !schemeExplicit && https && index < schemes.lastIndex
|
when (result) {
|
||||||
if (!hasHttpFallback) return config to result
|
is ServerProbeResult.Supported,
|
||||||
// Browser-like: only fall back to HTTP after a TLS/protocol failure.
|
ServerProbeResult.Unsupported -> return config to result
|
||||||
if (failure?.isSslProtocolError() == true) continue
|
ServerProbeResult.Timeout,
|
||||||
if (result is ServerProbeResult.Timeout) return config to result
|
ServerProbeResult.Unreachable -> {
|
||||||
// Non-SSL Unreachable (e.g. LAN cleartext / connection refused on 443): try HTTP.
|
if (!(!schemeExplicit && https && index < lastIndex)) return config to result
|
||||||
continue
|
|
||||||
|
// Browser-like: only fall back to HTTP after a TLS/protocol failure.
|
||||||
|
if (failure?.isSslProtocolError() == true)
|
||||||
|
return@forEachIndexed
|
||||||
|
if (result is ServerProbeResult.Timeout)
|
||||||
|
return config to result
|
||||||
|
|
||||||
|
// Non-SSL Unreachable (e.g. LAN cleartext / connection refused on 443): try HTTP.
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lastConfig to lastResult
|
return lastConfig to lastResult
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,33 +141,38 @@ private suspend fun probeServerOnce(
|
|||||||
config: ServerConfigData,
|
config: ServerConfigData,
|
||||||
): Pair<ServerProbeResult, Throwable?> {
|
): Pair<ServerProbeResult, Throwable?> {
|
||||||
val apiBase = apiBaseUrlFor(config)
|
val apiBase = apiBaseUrlFor(config)
|
||||||
val mark = TimeSource.Monotonic.markNow()
|
val time = TimeSource.Monotonic.markNow()
|
||||||
InstanceIdGuard.probeConfig = config
|
InstanceIdGuard.probeConfig = config
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val resolve = resolveInstanceId(
|
val pingMs = time.elapsedNow().inWholeMilliseconds.toInt().coerceAtLeast(0)
|
||||||
config = config,
|
|
||||||
apiBaseUrl = apiBase,
|
return ServerProbeResult.Supported(
|
||||||
forceNetwork = true,
|
when (
|
||||||
allowCachedOnFailure = false,
|
val resolve = resolveInstanceId(
|
||||||
)
|
config = config,
|
||||||
val pingMs = mark.elapsedNow().inWholeMilliseconds.toInt().coerceAtLeast(0)
|
apiBaseUrl = apiBase,
|
||||||
val instanceId = when (resolve) {
|
forceNetwork = true,
|
||||||
is InstanceIdResolveResult.Cached -> resolve.instanceId
|
allowCachedOnFailure = false,
|
||||||
is InstanceIdResolveResult.Fetched -> resolve.instanceId
|
)
|
||||||
is InstanceIdResolveResult.InstanceIdChanged -> resolve.newId
|
) {
|
||||||
InstanceIdResolveResult.Unsupported ->
|
is InstanceIdResolveResult.Cached -> resolve.instanceId
|
||||||
return ServerProbeResult.Unsupported to null
|
is InstanceIdResolveResult.Fetched -> resolve.instanceId
|
||||||
InstanceIdResolveResult.Timeout ->
|
is InstanceIdResolveResult.InstanceIdChanged -> resolve.newId
|
||||||
return ServerProbeResult.Timeout to null
|
InstanceIdResolveResult.Unsupported ->
|
||||||
InstanceIdResolveResult.Unreachable -> {
|
return ServerProbeResult.Unsupported to null
|
||||||
val failure = runCatching {
|
InstanceIdResolveResult.Timeout ->
|
||||||
ApiClient.fetchServerInstanceId(apiBase)
|
return ServerProbeResult.Timeout to null
|
||||||
}.exceptionOrNull()
|
InstanceIdResolveResult.Unreachable -> {
|
||||||
return ServerProbeResult.Unreachable to failure
|
val failure = runCatching {
|
||||||
}
|
ApiClient.fetchServerInstanceId(apiBase)
|
||||||
}
|
}.exceptionOrNull()
|
||||||
val callsOk = probeCallsReachable(config)
|
return ServerProbeResult.Unreachable to failure
|
||||||
return ServerProbeResult.Supported(instanceId, callsOk, pingMs) to null
|
}
|
||||||
|
},
|
||||||
|
probeCallsReachable(config),
|
||||||
|
pingMs
|
||||||
|
) to null
|
||||||
} finally {
|
} finally {
|
||||||
InstanceIdGuard.probeConfig = null
|
InstanceIdGuard.probeConfig = null
|
||||||
}
|
}
|
||||||
@@ -182,13 +201,15 @@ suspend fun applyServerAndNavigate(
|
|||||||
): ApplyServerResult {
|
): ApplyServerResult {
|
||||||
val apiBase = apiBaseUrlFor(config)
|
val apiBase = apiBaseUrlFor(config)
|
||||||
val token = bearer.trim()
|
val token = bearer.trim()
|
||||||
|
|
||||||
if (token.isEmpty()) {
|
if (token.isEmpty()) {
|
||||||
applyServerConfig(config, probe.instanceId, probe.callsOk)
|
applyServerConfig(config, probe.instanceId, probe.callsOk)
|
||||||
WebSocketManager.disconnect()
|
WebSocketManager.disconnect()
|
||||||
onNavigateLogin()
|
onNavigateLogin()
|
||||||
return ApplyServerResult.Applied
|
return ApplyServerResult.Applied
|
||||||
}
|
}
|
||||||
when (val auth = ApiClient.checkAuthAt(apiBase, token)) {
|
|
||||||
|
when (ApiClient.checkAuthAt(apiBase, token)) {
|
||||||
ApiClient.CheckAuthResult.Authenticated -> {
|
ApiClient.CheckAuthResult.Authenticated -> {
|
||||||
applyServerConfig(config, probe.instanceId, probe.callsOk)
|
applyServerConfig(config, probe.instanceId, probe.callsOk)
|
||||||
WebSocketManager.disconnect()
|
WebSocketManager.disconnect()
|
||||||
@@ -196,9 +217,11 @@ suspend fun applyServerAndNavigate(
|
|||||||
onNavigateChat()
|
onNavigateChat()
|
||||||
return ApplyServerResult.Applied
|
return ApplyServerResult.Applied
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiClient.CheckAuthResult.Unreachable -> {
|
ApiClient.CheckAuthResult.Unreachable -> {
|
||||||
return ApplyServerResult.ServerUnreachable
|
return ApplyServerResult.ServerUnreachable
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiClient.CheckAuthResult.NotAuthenticated -> {
|
ApiClient.CheckAuthResult.NotAuthenticated -> {
|
||||||
onLogoutOldHost()
|
onLogoutOldHost()
|
||||||
applyServerConfig(config, probe.instanceId, probe.callsOk)
|
applyServerConfig(config, probe.instanceId, probe.callsOk)
|
||||||
|
|||||||
Reference in New Issue
Block a user