Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔨 Uniformly use Json format as the data format protocol of the API #307

Merged
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
1 change: 1 addition & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ kotlin {
implementation(libs.jna.platform)
implementation(libs.jnativehook)
implementation(libs.koin.core)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.logging)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.clipevery.dto.sync

import com.clipevery.serializer.Base64MimeByteArraySerializer
import kotlinx.serialization.Serializable

@Serializable
data class ExchangePreKey(
@Serializable(with = Base64MimeByteArraySerializer::class) val data: ByteArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as ExchangePreKey

return data.contentEquals(other.data)
}

override fun hashCode(): Int {
return data.contentHashCode()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package com.clipevery.net

import io.ktor.client.statement.HttpResponse
import io.ktor.http.URLBuilder
import io.ktor.util.reflect.TypeInfo

interface ClipClient {

suspend fun post(
message: ByteArray,
suspend fun <T: Any> post(
message: T,
messageType: TypeInfo,
timeout: Long = 1000L,
urlBuilder: URLBuilder.(URLBuilder) -> Unit
): HttpResponse
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.clipevery.serializer

import com.clipevery.utils.base64Decode
import com.clipevery.utils.base64Encode
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

object Base64MimeByteArraySerializer: KSerializer<ByteArray> {

override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ByteArray") {}

override fun deserialize(decoder: Decoder): ByteArray {
return base64Decode(decoder.decodeString())
}

override fun serialize(encoder: Encoder, value: ByteArray) {
encoder.encodeString(base64Encode(value))
}
}
22 changes: 16 additions & 6 deletions composeApp/src/commonMain/kotlin/com/clipevery/utils/JsonUtils.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package com.clipevery.utils

import kotlinx.serialization.encodeToString
import com.clipevery.serializer.Base64MimeByteArraySerializer
import com.clipevery.serializer.IdentityKeySerializer
import com.clipevery.serializer.PreKeyBundleSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.serializersModuleOf
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.state.PreKeyBundle

inline fun <reified T> readJson(json: String): T {
return Json.decodeFromString<T>(json)
}
object JsonUtils {

val JSON: Json = Json {
serializersModule = SerializersModule {
serializersModuleOf(ByteArray::class, Base64MimeByteArraySerializer)
serializersModuleOf(PreKeyBundle::class, PreKeyBundleSerializer)
serializersModuleOf(IdentityKey::class, IdentityKeySerializer)
}
}

inline fun <reified T : Any> writeJson(obj: T): String {
return Json.encodeToString(obj)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package com.clipevery.net

import com.clipevery.app.AppInfo
import com.clipevery.utils.JsonUtils
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.timeout
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType
import io.ktor.http.URLBuilder
import io.ktor.util.InternalAPI
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import io.ktor.util.reflect.TypeInfo

class DesktopClipClient(private val appInfo: AppInfo): ClipClient {

Expand All @@ -20,11 +26,14 @@ class DesktopClipClient(private val appInfo: AppInfo): ClipClient {
requestTimeoutMillis = 1000
}
install(Logging)
install(ContentNegotiation) {
json(JsonUtils.JSON, ContentType.Application.Json)
}
}

@OptIn(InternalAPI::class)
override suspend fun post(
message: ByteArray,
override suspend fun <T: Any> post(
message: T,
messageType: TypeInfo,
timeout: Long,
urlBuilder: URLBuilder.(URLBuilder) -> Unit
): HttpResponse {
Expand All @@ -33,7 +42,11 @@ class DesktopClipClient(private val appInfo: AppInfo): ClipClient {
timeout {
requestTimeoutMillis = timeout
}
body = message
contentType(ContentType.Application.Json)
url {
urlBuilder(this)
}
setBody(message, messageType)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import com.clipevery.exception.StandardErrorCode
import com.clipevery.net.exception.signalExceptionHandler
import com.clipevery.net.plugin.SignalDecryption
import com.clipevery.routing.syncRouting
import com.clipevery.serializer.IdentityKeySerializer
import com.clipevery.serializer.PreKeyBundleSerializer
import com.clipevery.utils.JsonUtils
import com.clipevery.utils.failResponse
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.serialization.kotlinx.json.json
Expand All @@ -23,11 +22,6 @@ import io.ktor.server.request.httpMethod
import io.ktor.server.request.uri
import io.ktor.server.routing.routing
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.serializersModuleOf
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.state.PreKeyBundle
import java.net.BindException

class DesktopClipServer(private val configManager: ConfigManager,
Expand All @@ -42,12 +36,7 @@ class DesktopClipServer(private val configManager: ConfigManager,
private fun createServer(port: Int): NettyApplicationEngine {
return embeddedServer(Netty, port = port) {
install(ContentNegotiation) {
json(Json {
serializersModule = SerializersModule {
serializersModuleOf(PreKeyBundle::class, PreKeyBundleSerializer)
serializersModuleOf(IdentityKey::class, IdentityKeySerializer)
}
})
json(JsonUtils.JSON)
}
install(StatusPages) {
exception(Exception::class) { call, cause ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.clipevery.dao.sync.HostInfo
import com.clipevery.dao.sync.SyncRuntimeInfo
import com.clipevery.dao.sync.SyncState
import com.clipevery.utils.TelnetUtils
import com.clipevery.utils.buildUrl
import io.github.oshai.kotlinlogging.KotlinLogging

class ConnectedState: ConnectState {
Expand All @@ -25,47 +26,36 @@ class ConnectingState(private val clientHandler: ClientHandler): ConnectState {
override suspend fun autoResolve(syncRuntimeInfo: SyncRuntimeInfo) {
clientHandler.getHostInfo()?.let { hostInfo ->
if (clientHandler.isExistSession()) {
useSession(hostInfo, syncRuntimeInfo.port)
} else {
createSession(hostInfo, syncRuntimeInfo.port)
if (useSession(hostInfo, syncRuntimeInfo.port)) {
return
}
}
createSession(hostInfo, syncRuntimeInfo.port)
} ?: run {
logger.info { "${syncRuntimeInfo.platformName} to disconnected" }
clientHandler.updateSyncState(SyncState.DISCONNECTED)
}
}

private suspend fun useSession(hostInfo: HostInfo, port: Int) {
val syncClientApi = clientHandler.getSyncClientApi()
val sessionCipher = clientHandler.getSessionCipher()
private suspend fun useSession(hostInfo: HostInfo, port: Int): Boolean {
try {
if(syncClientApi.exchangePreKey(sessionCipher) { urlBuilder ->
urlBuilder.port = port
urlBuilder.host = hostInfo.hostAddress
}) {
return
}
return exchangePreKey(hostInfo, port)
} catch (e: Exception) {
logger.warn(e) { "useSession exchangePreKey fail" }
}
logger.info { "connect state to unmatched ${hostInfo.hostAddress} $port"}
clientHandler.updateSyncState(SyncState.UNMATCHED)
return false
}

private suspend fun createSession(hostInfo: HostInfo, port: Int) {
val syncClientApi = clientHandler.getSyncClientApi()
val sessionCipher = clientHandler.getSessionCipher()
try {
syncClientApi.getPreKeyBundle { urlBuilder ->
urlBuilder.port = port
urlBuilder.host = hostInfo.hostAddress
buildUrl(urlBuilder, hostInfo, port, "sync", "preKeyBundle")
}?.let { preKeyBundle ->
val sessionBuilder = clientHandler.createSessionBuilder()
sessionBuilder.process(preKeyBundle)
try {
if (syncClientApi.exchangePreKey(sessionCipher) { urlBuilder ->
urlBuilder.port = port
urlBuilder.host = hostInfo.hostAddress
}) {
if (exchangePreKey(hostInfo, port)) {
return
}
} catch (e: Exception) {
Expand All @@ -79,6 +69,19 @@ class ConnectingState(private val clientHandler: ClientHandler): ConnectState {
clientHandler.updateSyncState(SyncState.UNMATCHED)
}

private suspend fun exchangePreKey(hostInfo: HostInfo, port: Int): Boolean {
val syncClientApi = clientHandler.getSyncClientApi()
val sessionCipher = clientHandler.getSessionCipher()
return if (syncClientApi.exchangePreKey(sessionCipher) { urlBuilder ->
buildUrl(urlBuilder, hostInfo, port, "sync", "exchangePreKey")
}) {
clientHandler.updateSyncState(SyncState.CONNECTED)
true
} else {
false
}
}

override suspend fun next(): Boolean {
return true
}
Expand All @@ -87,12 +90,16 @@ class ConnectingState(private val clientHandler: ClientHandler): ConnectState {

class DisconnectedState(private val clientHandler: ClientHandler): ConnectState {

private val logger = KotlinLogging.logger {}

private val telnetUtils = Dependencies.koinApplication.koin.get<TelnetUtils>()

override suspend fun autoResolve(syncRuntimeInfo: SyncRuntimeInfo) {
telnetUtils.switchHost(syncRuntimeInfo.hostInfoList, syncRuntimeInfo.port)?.let { hostInfo ->
logger.info { "${hostInfo.hostAddress} to connecting" }
clientHandler.updateSyncStateWithHostInfo(SyncState.Companion.CONNECTING, hostInfo, syncRuntimeInfo.port)
} ?: run {
logger.info { "${syncRuntimeInfo.platformName} to disconnected" }
clientHandler.updateSyncState(SyncState.Companion.DISCONNECTED)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.clipevery.net.clientapi

import com.clipevery.dto.sync.ExchangePreKey
import com.clipevery.net.ClipClient
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.call.body
import io.ktor.http.URLBuilder
import io.ktor.util.reflect.typeInfo
import org.signal.libsignal.protocol.SessionCipher
import org.signal.libsignal.protocol.message.SignalMessage
import org.signal.libsignal.protocol.state.PreKeyBundle
Expand All @@ -29,12 +31,15 @@ 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(ciphertextMessage.serialize(), urlBuilder = toUrl)

val exchangePreKey = ExchangePreKey(data = ciphertextMessage.serialize())

val response = clipClient.post(exchangePreKey, typeInfo<ExchangePreKey>() , urlBuilder = toUrl)
if (response.status.value != 200) {
return false
}
val bytes = response.body<ByteArray>()
val signalMessage = SignalMessage(bytes)
val getExchangePreKey = response.body<ExchangePreKey>()
val signalMessage = SignalMessage(getExchangePreKey.data)
val decrypt = sessionCipher.decrypt(signalMessage)
return String(decrypt, Charsets.UTF_8) == "exchange"
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.clipevery.net.plugin

import com.clipevery.Dependencies
import com.clipevery.utils.base64mimeDecode
import com.clipevery.serializer.Base64MimeByteArraySerializer
import com.clipevery.utils.JsonUtils
import io.ktor.server.application.ApplicationPlugin
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.application.hooks.ReceiveRequestBytes
import io.ktor.server.request.path
import io.ktor.util.KtorDsl
import io.ktor.utils.io.core.readBytes
import io.ktor.utils.io.writer
Expand All @@ -26,11 +28,11 @@ val SignalDecryption: ApplicationPlugin<SignalDecryptionConfig> = createApplicat
headers["appInstanceId"]?.let { appInstanceId ->
headers["signal"]?.let { signal ->
if (signal == "1") {
call.request.path()
return@on application.writer {
val base64Content = body.readRemaining().readBytes()
val originalString = String(base64Content, Charsets.UTF_8)
val base64String = originalString.substring(1, originalString.length - 1)
val encryptedContent = base64mimeDecode(base64String)
val encryptedContent = JsonUtils.JSON.decodeFromString(Base64MimeByteArraySerializer, originalString)
val signalProtocolAddress = SignalProtocolAddress(appInstanceId, 1)
val signalMessage = SignalMessage(encryptedContent)
val sessionCipher = SessionCipher(signalProtocolStore, signalProtocolAddress)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.clipevery.app.AppUI
import com.clipevery.dao.signal.ClipIdentityKey
import com.clipevery.dao.signal.SignalDao
import com.clipevery.dao.sync.SyncRuntimeInfoDao
import com.clipevery.dto.sync.ExchangePreKey
import com.clipevery.dto.sync.RequestTrust
import com.clipevery.dto.sync.RequestTrustSyncInfo
import com.clipevery.dto.sync.SyncInfo
Expand Down Expand Up @@ -97,7 +98,8 @@ fun Routing.syncRouting() {

post("sync/exchangePreKey") {
getAppInstanceId(call).let { appInstanceId ->
val bytes = call.receive<ByteArray>()
val exchangePreKey = call.receive(ExchangePreKey::class)
val bytes = exchangePreKey.data
val signalProtocolAddress = SignalProtocolAddress(appInstanceId, 1)
val identityKey = signalProtocolStore.getIdentity(signalProtocolAddress)
val sessionCipher = SessionCipher(signalProtocolStore, signalProtocolAddress)
Expand Down Expand Up @@ -128,7 +130,7 @@ fun Routing.syncRouting() {

if (Objects.equals("exchange", String(decrypt!!, Charsets.UTF_8))) {
val ciphertextMessage = sessionCipher.encrypt("exchange".toByteArray(Charsets.UTF_8))
successResponse(call, ciphertextMessage.serialize())
successResponse(call, ExchangePreKey(ciphertextMessage.serialize()))
} else {
failResponse(call, StandardErrorCode.SIGNAL_EXCHANGE_FAIL.toErrorCode())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.clipevery.utils

import com.clipevery.dao.sync.HostInfo
import io.ktor.http.URLBuilder
import io.ktor.http.URLProtocol
import io.ktor.http.path

fun buildUrl(urlBuilder: URLBuilder, hostInfo: HostInfo, port: Int, vararg paths: String) {
urlBuilder.protocol = URLProtocol.HTTP
urlBuilder.port = port
urlBuilder.host = hostInfo.hostAddress
urlBuilder.path(*paths)
}
Loading
Loading