Skip to content

Commit

Permalink
🔨 Uniformly use Json format as the data format protocol of the API
Browse files Browse the repository at this point in the history
  • Loading branch information
guiyanakuang committed Feb 9, 2024
1 parent 9719b2f commit 3d40e20
Show file tree
Hide file tree
Showing 15 changed files with 145 additions and 70 deletions.
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

0 comments on commit 3d40e20

Please sign in to comment.