From 93f9562c6b4a4aa53f18d2fd23e297ec7eec1c84 Mon Sep 17 00:00:00 2001 From: Yiqun Zhang Date: Thu, 1 Feb 2024 22:48:47 +0800 Subject: [PATCH] :sparkles: Implementing signal protocol for desktop device --- .../kotlin/com/clipevery/net/ClipClient.kt | 6 ++- .../DesktopClipeveryKoinApplication.kt | 2 + .../com/clipevery/net/DesktopClipClient.kt | 13 ++++- .../com/clipevery/net/DesktopConnectState.kt | 22 +++++--- .../net/clientapi/DesktopSyncClientApi.kt | 4 +- .../kotlin/com/clipevery/utils/NetUtils.kt | 43 --------------- .../kotlin/com/clipevery/utils/TelnetUtils.kt | 54 +++++++++++++++++++ 7 files changed, 89 insertions(+), 55 deletions(-) delete mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/utils/NetUtils.kt create mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/utils/TelnetUtils.kt diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/net/ClipClient.kt b/composeApp/src/commonMain/kotlin/com/clipevery/net/ClipClient.kt index f034ef738..e1a8c55c7 100644 --- a/composeApp/src/commonMain/kotlin/com/clipevery/net/ClipClient.kt +++ b/composeApp/src/commonMain/kotlin/com/clipevery/net/ClipClient.kt @@ -6,11 +6,13 @@ import io.ktor.http.URLBuilder interface ClipClient { suspend fun post( - urlBuilder: URLBuilder.(URLBuilder) -> Unit, message: ByteArray, + timeout: Long = 1000L, + urlBuilder: URLBuilder.(URLBuilder) -> Unit ): HttpResponse suspend fun get( - urlBuilder: URLBuilder.(URLBuilder) -> Unit + timeout: Long = 1000L, + urlBuilder: URLBuilder.(URLBuilder) -> Unit, ): HttpResponse } diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/DesktopClipeveryKoinApplication.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/DesktopClipeveryKoinApplication.kt index 2099af4eb..8d02d51d4 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/DesktopClipeveryKoinApplication.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/DesktopClipeveryKoinApplication.kt @@ -43,6 +43,7 @@ import com.clipevery.ui.DesktopThemeDetector import com.clipevery.ui.ThemeDetector import com.clipevery.utils.DesktopQRCodeGenerator import com.clipevery.utils.QRCodeGenerator +import com.clipevery.utils.TelnetUtils import org.koin.core.KoinApplication import org.koin.core.context.GlobalContext import org.koin.dsl.module @@ -79,6 +80,7 @@ object Dependencies { single> { lazy { get() } } single { DesktopClipBonjourService(get(), get()).registerService() } single { DesktopDeviceRefresher(get()) } + single { TelnetUtils(get()) } // signal component single { getClipIdentityKeyStoreFactory(get(), get()).createIdentityKeyStore() } diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopClipClient.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopClipClient.kt index d7039176f..174cdc5e3 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopClipClient.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopClipClient.kt @@ -3,6 +3,7 @@ package com.clipevery.net import com.clipevery.app.AppInfo import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.timeout import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.client.request.post @@ -16,20 +17,28 @@ class DesktopClipClient(private val appInfo: AppInfo): ClipClient { @OptIn(InternalAPI::class) override suspend fun post( - urlBuilder: URLBuilder.(URLBuilder) -> Unit, message: ByteArray, + timeout: Long, + urlBuilder: URLBuilder.(URLBuilder) -> Unit ): HttpResponse { return client.post { header("appInstanceId", appInfo.appInstanceId) + timeout { + requestTimeoutMillis = timeout + } body = message } } override suspend fun get( - urlBuilder: URLBuilder.(URLBuilder) -> Unit + timeout: Long, + urlBuilder: URLBuilder.(URLBuilder) -> Unit, ): HttpResponse { return client.get { header("appInstanceId", appInfo.appInstanceId) + timeout { + requestTimeoutMillis = timeout + } url { urlBuilder(this) } diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopConnectState.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopConnectState.kt index c86efbc8c..6b09b2b8a 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopConnectState.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopConnectState.kt @@ -1,6 +1,9 @@ package com.clipevery.net +import com.clipevery.Dependencies import com.clipevery.dao.sync.SyncRuntimeInfo +import com.clipevery.dao.sync.SyncState +import com.clipevery.utils.TelnetUtils class ConnectedState: ConnectState { override suspend fun autoResolve(syncRuntimeInfo: SyncRuntimeInfo) { @@ -33,14 +36,21 @@ class ConnectingState(private val clientHandler: ClientHandler): ConnectState { } override suspend fun next(): Boolean { - TODO("Not yet implemented") + return true } } class DisconnectedState(private val clientHandler: ClientHandler): ConnectState { + + private val telnetUtils = Dependencies.koinApplication.koin.get() + override suspend fun autoResolve(syncRuntimeInfo: SyncRuntimeInfo) { - TODO("Not yet implemented") + telnetUtils.switchHost(syncRuntimeInfo.hostInfoList, syncRuntimeInfo.port)?.let { hostInfo -> + clientHandler.updateSyncStateWithHostInfo(SyncState.Companion.CONNECTING, hostInfo, syncRuntimeInfo.port) + } ?: run { + clientHandler.updateSyncState(SyncState.Companion.UNMATCHED) + } } override suspend fun next(): Boolean { @@ -51,22 +61,22 @@ class DisconnectedState(private val clientHandler: ClientHandler): ConnectState class UnmatchedState: ConnectState { override suspend fun autoResolve(syncRuntimeInfo: SyncRuntimeInfo) { - TODO("Not yet implemented") + // do nothing } override suspend fun next(): Boolean { - TODO("Not yet implemented") + return false } } class UnverifiedState(private val clientHandler: ClientHandler): ConnectState { override suspend fun autoResolve(syncRuntimeInfo: SyncRuntimeInfo) { - TODO("Not yet implemented") + // do nothing } override suspend fun next(): Boolean { - TODO("Not yet implemented") + return false } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/net/clientapi/DesktopSyncClientApi.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/net/clientapi/DesktopSyncClientApi.kt index 633699897..2db0dea65 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/net/clientapi/DesktopSyncClientApi.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/net/clientapi/DesktopSyncClientApi.kt @@ -14,7 +14,7 @@ class DesktopSyncClientApi(private val clipClient: ClipClient): SyncClientApi { override suspend fun getPreKeyBundle(toUrl: URLBuilder.(URLBuilder) -> Unit): PreKeyBundle? { try { - val response = clipClient.get(toUrl) + val response = clipClient.get(urlBuilder = toUrl) if (response.status.value != 200) { return null } @@ -29,7 +29,7 @@ class DesktopSyncClientApi(private val clipClient: ClipClient): SyncClientApi { toUrl: URLBuilder.(URLBuilder) -> Unit): Boolean { try { val ciphertextMessage = sessionCipher.encrypt("exchange".toByteArray(Charsets.UTF_8)) - val response = clipClient.post(toUrl, ciphertextMessage.serialize()) + val response = clipClient.post(ciphertextMessage.serialize(), urlBuilder = toUrl) if (response.status.value != 200) { return false } diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/utils/NetUtils.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/utils/NetUtils.kt deleted file mode 100644 index 16af71019..000000000 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/utils/NetUtils.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.clipevery.utils - -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.selects.select -import java.net.InetSocketAddress -import java.net.Socket - -fun telnet(host: String, port: Int, timeout: Int): Boolean { - return try { - Socket().use { socket -> - socket.connect(InetSocketAddress(host, port), timeout) - true - } - } catch (e: Exception) { - false - } -} - -suspend fun telnet(hosts: List, port: Int, timeout: Int): String? { - val deferreds = coroutineScope { - hosts.map { host -> - async { - if (telnet(host, port, timeout)) host else null - } - } - } - - var result: String? = null - while (deferreds.isNotEmpty() && result == null) { - select { - deferreds.forEach { deferred -> - deferred.onAwait { hostResult -> - if (hostResult != null) { - result = hostResult - deferreds.forEach { it.cancel() } - } - } - } - } - } - return result -} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/utils/TelnetUtils.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/utils/TelnetUtils.kt new file mode 100644 index 000000000..269f171e0 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/utils/TelnetUtils.kt @@ -0,0 +1,54 @@ +package com.clipevery.utils + +import com.clipevery.dao.sync.HostInfo +import com.clipevery.net.ClipClient +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.async +import kotlinx.coroutines.selects.select +import kotlinx.coroutines.withContext + +class TelnetUtils(private val clipClient: ClipClient) { + + private val logger = KotlinLogging.logger {} + + suspend fun switchHost(hostInfoList: List, port: Int, timeout: Long = 500L): HostInfo? { + if (hostInfoList.isEmpty()) { + return null + } + val deferredArray = withContext(ioDispatcher) { + hostInfoList.map { hostInfo -> + async { + if (telnet(hostInfo, port, timeout)) hostInfo else null + } + } + } + + var result: HostInfo? = null + while (deferredArray.isNotEmpty() && result == null) { + select { + deferredArray.forEach { deferred -> + deferred.onAwait { hostInfo -> + if (hostInfo != null) { + result = hostInfo + deferredArray.forEach { it.cancel() } + } + } + } + } + } + return result + } + + private suspend fun telnet(hostInfo: HostInfo, port: Int, timeout: Long): Boolean { + return try { + val httpResponse = clipClient.get(timeout = timeout) { urlBuilder -> + urlBuilder.port = port + urlBuilder.host = hostInfo.hostAddress + } + httpResponse.status.value == 200 + } catch (e: Exception) { + logger.debug(e) { "telnet $hostInfo fail" } + false + } + } +} \ No newline at end of file