diff --git a/client/build.gradle.kts b/client/build.gradle.kts index 60fff23a085ddededab1d4c83a13106df4512ef4..97528d7a56f8ef3e73c589a71067f9358bcfd19f 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -17,5 +17,5 @@ dependencies { val testcontainersCiVersion: String by project testImplementation("de.justjanne", "testcontainers-ci", testcontainersCiVersion) val sl4jVersion: String by project - testImplementation("org.slf4j", "slf4j-simple", sl4jVersion) + implementation("org.slf4j", "slf4j-simple", sl4jVersion) } diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientConnectionHandler.kt b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientConnectionHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8fc8c52174901462c073def69dcfcc95f054ae4 --- /dev/null +++ b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientConnectionHandler.kt @@ -0,0 +1,40 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.client.session + +import de.justjanne.libquassel.client.util.CoroutineQueue +import de.justjanne.libquassel.protocol.models.HandshakeMessage +import de.justjanne.libquassel.protocol.models.SignalProxyMessage +import de.justjanne.libquassel.protocol.session.ConnectionHandler +import de.justjanne.libquassel.protocol.session.MessageChannel + +abstract class ClientConnectionHandler : ConnectionHandler { + protected var channel: MessageChannel? = null + private val readyQueue = CoroutineQueue() + override suspend fun init(channel: MessageChannel): Boolean { + this.channel = channel + readyQueue.resume() + return false + } + + suspend fun emit(message: SignalProxyMessage) { + if (channel == null) { + readyQueue.wait() + } + channel?.emit(message) + } + + suspend fun emit(message: HandshakeMessage) { + if (channel == null) { + readyQueue.wait() + } + channel?.emit(message) + } +} diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientHandshakeHandler.kt b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientHandshakeHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..511a4f7fb03833b945b22280ae4c9efe291c9f83 --- /dev/null +++ b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientHandshakeHandler.kt @@ -0,0 +1,137 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.client.session + +import de.justjanne.libquassel.client.util.CoroutineKeyedQueue +import de.justjanne.libquassel.protocol.exceptions.HandshakeException +import de.justjanne.libquassel.protocol.features.FeatureSet +import de.justjanne.libquassel.protocol.models.HandshakeMessage +import de.justjanne.libquassel.protocol.serializers.HandshakeMessageSerializer +import de.justjanne.libquassel.protocol.session.CoreState +import de.justjanne.libquassel.protocol.session.HandshakeHandler +import de.justjanne.libquassel.protocol.session.MessageChannelReadThread +import de.justjanne.libquassel.protocol.session.Session +import de.justjanne.libquassel.protocol.util.log.trace +import de.justjanne.libquassel.protocol.variant.QVariantMap +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer + +class ClientHandshakeHandler( + val session: Session +) : HandshakeHandler, ClientConnectionHandler() { + private val messageQueue = CoroutineKeyedQueue<Class<out HandshakeMessage>, HandshakeMessage>() + + override suspend fun read(buffer: ByteBuffer): Boolean { + return dispatch(HandshakeMessageSerializer.deserialize(buffer, channel!!.negotiatedFeatures)) + } + + private fun dispatch(message: HandshakeMessage): Boolean { + logger.trace { "Read handshake message $message" } + messageQueue.resume(message.javaClass, message) + if (message is HandshakeMessage.SessionInit) { + session.init(message.identities, message.bufferInfos, message.networkIds) + return true + } else { + return false + } + } + + override suspend fun init( + clientVersion: String, + buildDate: String, + featureSet: FeatureSet + ): CoreState { + emit( + HandshakeMessage.ClientInit( + clientVersion, + buildDate, + featureSet + ) + ) + when ( + val response = messageQueue.wait( + HandshakeMessage.ClientInitAck::class.java, + HandshakeMessage.ClientInitReject::class.java + ) + ) { + is HandshakeMessage.ClientInitReject -> + throw HandshakeException.InitException(response.errorString ?: "Unknown Error") + is HandshakeMessage.ClientInitAck -> + if (response.coreConfigured == null) { + throw HandshakeException.InitException("Unknown Error") + } else if (response.coreConfigured == true) { + return CoreState.Configured + } else { + return CoreState.Unconfigured( + response.backendInfo, + response.authenticatorInfo + ) + } + else -> throw HandshakeException.InitException("Unknown Error") + } + } + + override suspend fun login(username: String, password: String) { + emit( + HandshakeMessage.ClientLogin( + username, + password + ) + ) + when ( + val response = messageQueue.wait( + HandshakeMessage.ClientLoginAck::class.java, + HandshakeMessage.ClientLoginReject::class.java + ) + ) { + is HandshakeMessage.ClientLoginReject -> + throw HandshakeException.LoginException(response.errorString ?: "Unknown Error") + is HandshakeMessage.ClientLoginAck -> + return + else -> throw HandshakeException.LoginException("Unknown Error") + } + } + + override suspend fun configureCore( + adminUsername: String, + adminPassword: String, + backend: String, + backendConfiguration: QVariantMap, + authenticator: String, + authenticatorConfiguration: QVariantMap + ) { + emit( + HandshakeMessage.CoreSetupData( + adminUsername, + adminPassword, + backend, + backendConfiguration, + authenticator, + authenticatorConfiguration + ) + ) + when ( + val response = messageQueue.wait( + HandshakeMessage.CoreSetupAck::class.java, + HandshakeMessage.CoreSetupReject::class.java + ) + ) { + is HandshakeMessage.CoreSetupReject -> + throw HandshakeException.SetupException(response.errorString ?: "Unknown Error") + is HandshakeMessage.CoreSetupAck -> + return + else -> throw HandshakeException.SetupException("Unknown Error") + } + } + + companion object { + private val logger = LoggerFactory.getLogger(MessageChannelReadThread::class.java) + } +} diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientMagicHandler.kt b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientMagicHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..31f78d92681e195f33f64210ea51522e9b3b4acc --- /dev/null +++ b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientMagicHandler.kt @@ -0,0 +1,66 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.client.session + +import de.justjanne.libquassel.protocol.connection.ClientHeader +import de.justjanne.libquassel.protocol.connection.ClientHeaderSerializer +import de.justjanne.libquassel.protocol.connection.CoreHeaderSerializer +import de.justjanne.libquassel.protocol.connection.ProtocolFeature +import de.justjanne.libquassel.protocol.connection.ProtocolFeatures +import de.justjanne.libquassel.protocol.connection.ProtocolMeta +import de.justjanne.libquassel.protocol.features.FeatureSet +import de.justjanne.libquassel.protocol.session.ConnectionHandler +import de.justjanne.libquassel.protocol.session.MessageChannel +import de.justjanne.libquassel.protocol.util.log.trace +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer +import javax.net.ssl.SSLContext + +class ClientMagicHandler( + private val protocolFeatures: ProtocolFeatures, + private val protocols: List<ProtocolMeta>, + private val sslContext: SSLContext +) : ConnectionHandler { + private val connectionFeatureSet = FeatureSet.none() + + override suspend fun init(channel: MessageChannel): Boolean { + val header = ClientHeader( + features = protocolFeatures, + versions = protocols + ) + logger.trace { "Writing client header $header" } + channel.emit(sizePrefix = false) { + ClientHeaderSerializer.serialize( + it, + header, + connectionFeatureSet + ) + } + + val handshakeBuffer = ByteBuffer.allocateDirect(4) + channel.channel.read(handshakeBuffer) + handshakeBuffer.flip() + val protocol = CoreHeaderSerializer.deserialize(handshakeBuffer, connectionFeatureSet) + logger.trace { "Read server header $protocol" } + if (protocol.features.contains(ProtocolFeature.TLS)) { + channel.channel.enableTLS(sslContext) + } + if (protocol.features.contains(ProtocolFeature.Compression)) { + channel.channel.enableCompression() + } + return true + } + + override suspend fun read(buffer: ByteBuffer) = true + + companion object { + private val logger = LoggerFactory.getLogger(ClientMagicHandler::class.java) + } +} diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/ClientProxyMessageHandler.kt b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientProxyMessageHandler.kt similarity index 72% rename from client/src/main/kotlin/de/justjanne/libquassel/client/ClientProxyMessageHandler.kt rename to client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientProxyMessageHandler.kt index eb43d04608a6587011083327ca41a9e0b8c715ef..c444b84c97d7c0b454ec048137c383178514122c 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/ClientProxyMessageHandler.kt +++ b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientProxyMessageHandler.kt @@ -7,27 +7,34 @@ * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client +package de.justjanne.libquassel.client.session import de.justjanne.libquassel.annotations.ProtocolSide import de.justjanne.libquassel.protocol.exceptions.RpcInvocationFailedException import de.justjanne.libquassel.protocol.models.SignalProxyMessage +import de.justjanne.libquassel.protocol.serializers.SignalProxyMessageSerializer import de.justjanne.libquassel.protocol.session.ProxyMessageHandler import de.justjanne.libquassel.protocol.syncables.HeartBeatHandler import de.justjanne.libquassel.protocol.syncables.ObjectRepository import de.justjanne.libquassel.protocol.syncables.common.RpcHandler import de.justjanne.libquassel.protocol.syncables.invoker.Invokers +import de.justjanne.libquassel.protocol.util.log.trace +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer class ClientProxyMessageHandler( - val heartBeatHandler: HeartBeatHandler, - val objectRepository: ObjectRepository, - val rpcHandler: RpcHandler -) : ProxyMessageHandler { - override fun emit(message: SignalProxyMessage) { - TODO("Not Implemented") + private val heartBeatHandler: HeartBeatHandler, + private val objectRepository: ObjectRepository, + private val rpcHandler: RpcHandler +) : ProxyMessageHandler, ClientConnectionHandler() { + + override suspend fun read(buffer: ByteBuffer): Boolean { + dispatch(SignalProxyMessageSerializer.deserialize(buffer, channel!!.negotiatedFeatures)) + return false } - override fun dispatch(message: SignalProxyMessage) { + override suspend fun dispatch(message: SignalProxyMessage) { + logger.trace { "Read signal proxy message $message" } when (message) { is SignalProxyMessage.HeartBeat -> emit(SignalProxyMessage.HeartBeatReply(message.timestamp)) is SignalProxyMessage.HeartBeatReply -> heartBeatHandler.recomputeLatency(message.timestamp, force = true) @@ -52,4 +59,8 @@ class ClientProxyMessageHandler( } } } + + companion object { + private val logger = LoggerFactory.getLogger(ClientProxyMessageHandler::class.java) + } } diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/ClientRpcHandler.kt b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientRpcHandler.kt similarity index 97% rename from client/src/main/kotlin/de/justjanne/libquassel/client/ClientRpcHandler.kt rename to client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientRpcHandler.kt index 1a16c05721c3d257d064416dcd5b7cdd4186cf79..c2e5257e1eacac940c722ce0a214f4af7dbc23de 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/ClientRpcHandler.kt +++ b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientRpcHandler.kt @@ -7,7 +7,7 @@ * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client +package de.justjanne.libquassel.client.session import de.justjanne.libquassel.protocol.models.Message import de.justjanne.libquassel.protocol.models.StatusMessage diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/ClientSession.kt b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientSession.kt similarity index 77% rename from client/src/main/kotlin/de/justjanne/libquassel/client/ClientSession.kt rename to client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientSession.kt index 07ee170da88b6077b86849eda3d85041adc914d3..0d6d8473696f50b33ffd8e0a3e86a645cfcf42fd 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/ClientSession.kt +++ b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientSession.kt @@ -6,14 +6,19 @@ * v. 2.0. If a copy of the MPL was not distributed with this file, You can * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client +package de.justjanne.libquassel.client.session import de.justjanne.libquassel.annotations.ProtocolSide -import de.justjanne.libquassel.client.io.CoroutineChannel +import de.justjanne.libquassel.protocol.connection.ProtocolFeatures +import de.justjanne.libquassel.protocol.connection.ProtocolMeta +import de.justjanne.libquassel.protocol.io.CoroutineChannel +import de.justjanne.libquassel.protocol.models.BufferInfo import de.justjanne.libquassel.protocol.models.ids.IdentityId import de.justjanne.libquassel.protocol.models.ids.NetworkId import de.justjanne.libquassel.protocol.serializers.qt.StringSerializerUtf8 import de.justjanne.libquassel.protocol.session.CommonSyncProxy +import de.justjanne.libquassel.protocol.session.MessageChannel +import de.justjanne.libquassel.protocol.session.MessageChannelReadThread import de.justjanne.libquassel.protocol.session.Session import de.justjanne.libquassel.protocol.syncables.HeartBeatHandler import de.justjanne.libquassel.protocol.syncables.ObjectRepository @@ -30,29 +35,55 @@ import de.justjanne.libquassel.protocol.syncables.common.IrcListHelper import de.justjanne.libquassel.protocol.syncables.common.Network import de.justjanne.libquassel.protocol.syncables.common.NetworkConfig import de.justjanne.libquassel.protocol.syncables.state.NetworkState +import de.justjanne.libquassel.protocol.util.log.info import de.justjanne.libquassel.protocol.util.update import de.justjanne.libquassel.protocol.variant.QVariantMap import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import org.slf4j.LoggerFactory +import javax.net.ssl.SSLContext class ClientSession( - private val connection: CoroutineChannel + connection: CoroutineChannel, + protocolFeatures: ProtocolFeatures, + protocols: List<ProtocolMeta>, + sslContext: SSLContext ) : Session { override val side = ProtocolSide.CLIENT private val rpcHandler = ClientRpcHandler(this) private val heartBeatHandler = HeartBeatHandler() override val objectRepository = ObjectRepository() + val handshakeHandler = ClientHandshakeHandler(this) private val proxyMessageHandler = ClientProxyMessageHandler( heartBeatHandler, objectRepository, rpcHandler ) + private val magicHandler = ClientMagicHandler(protocolFeatures, protocols, sslContext) override val proxy = CommonSyncProxy( ProtocolSide.CLIENT, objectRepository, proxyMessageHandler ) + private val messageChannel = MessageChannel(connection) + + init { + messageChannel.register(magicHandler) + messageChannel.register(handshakeHandler) + messageChannel.register(proxyMessageHandler) + MessageChannelReadThread(messageChannel).start() + } + + override fun init( + identities: List<QVariantMap>, + bufferInfos: List<BufferInfo>, + networkIds: List<NetworkId> + ) { + logger.info { + "Client session initialized: networks = $networkIds, buffers = $bufferInfos, identities = $identities" + } + } override fun network(id: NetworkId) = state().networks[id] override fun addNetwork(id: NetworkId) { @@ -141,4 +172,8 @@ class ClientSession( networkConfig = NetworkConfig(this) ) ) + + companion object { + private val logger = LoggerFactory.getLogger(ClientSession::class.java) + } } diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/ClientSessionState.kt b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientSessionState.kt similarity index 97% rename from client/src/main/kotlin/de/justjanne/libquassel/client/ClientSessionState.kt rename to client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientSessionState.kt index 179ee40dafc52b1f025cd6ee2b615ef7bfe814c4..9d29f8f0610dc9541b5a5fb8e837ad39736802b3 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/ClientSessionState.kt +++ b/client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientSessionState.kt @@ -7,7 +7,7 @@ * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client +package de.justjanne.libquassel.client.session import de.justjanne.libquassel.protocol.models.ids.IdentityId import de.justjanne.libquassel.protocol.models.ids.NetworkId diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/util/CoroutineKeyedQueue.kt b/client/src/main/kotlin/de/justjanne/libquassel/client/util/CoroutineKeyedQueue.kt new file mode 100644 index 0000000000000000000000000000000000000000..ae4a2c99eff0389348e9148e1fba34617492e31d --- /dev/null +++ b/client/src/main/kotlin/de/justjanne/libquassel/client/util/CoroutineKeyedQueue.kt @@ -0,0 +1,33 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.client.util + +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class CoroutineKeyedQueue<Key, Value> { + private val waiting = mutableMapOf<Key, MutableList<Continuation<Value>>>() + suspend fun wait(vararg keys: Key): Value = suspendCoroutine { + for (key in keys) { + waiting.getOrPut(key, ::mutableListOf).add(it) + } + } + + fun resume(key: Key, value: Value) { + val continuations = waiting[key].orEmpty().distinct() + for (continuation in continuations) { + continuation.resume(value) + } + for (it in waiting.keys) { + waiting[it]?.removeAll(continuations) + } + } +} diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/util/CoroutineQueue.kt b/client/src/main/kotlin/de/justjanne/libquassel/client/util/CoroutineQueue.kt new file mode 100644 index 0000000000000000000000000000000000000000..c97b76d6920b4a5e05db7a68a8b545b1e3bac70d --- /dev/null +++ b/client/src/main/kotlin/de/justjanne/libquassel/client/util/CoroutineQueue.kt @@ -0,0 +1,28 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.client.util + +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class CoroutineQueue { + private val waiting = mutableListOf<Continuation<Unit>>() + suspend fun wait(): Unit = suspendCoroutine { + waiting.add(it) + } + + suspend fun resume() { + for (continuation in waiting) { + continuation.resume(Unit) + } + waiting.clear() + } +} diff --git a/client/src/test/kotlin/de/justjanne/libquassel/client/ClientTest.kt b/client/src/test/kotlin/de/justjanne/libquassel/client/ClientTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b9b1ea7b4d0ccf73e81a8e19767282725c9d154e --- /dev/null +++ b/client/src/test/kotlin/de/justjanne/libquassel/client/ClientTest.kt @@ -0,0 +1,97 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.client + +import de.justjanne.libquassel.client.session.ClientSession +import de.justjanne.libquassel.client.testutil.QuasselCoreContainer +import de.justjanne.libquassel.client.testutil.TestX509TrustManager +import de.justjanne.libquassel.protocol.connection.ProtocolFeature +import de.justjanne.libquassel.protocol.connection.ProtocolMeta +import de.justjanne.libquassel.protocol.connection.ProtocolVersion +import de.justjanne.libquassel.protocol.exceptions.HandshakeException +import de.justjanne.libquassel.protocol.features.FeatureSet +import de.justjanne.libquassel.protocol.io.CoroutineChannel +import de.justjanne.libquassel.protocol.session.CoreState +import de.justjanne.testcontainersci.api.providedContainer +import de.justjanne.testcontainersci.extension.CiContainers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.net.InetSocketAddress +import javax.net.ssl.SSLContext +import kotlin.test.assertTrue + +@ExperimentalCoroutinesApi +@CiContainers +class ClientTest { + private val quassel = providedContainer("QUASSEL_CONTAINER") { + QuasselCoreContainer() + } + + private val sslContext = SSLContext.getInstance("TLSv1.3").apply { + init(null, arrayOf(TestX509TrustManager), null) + } + + private val channel = CoroutineChannel() + + private val username = "AzureDiamond" + private val password = "hunter2" + + @Test + fun testConnect(): Unit = runBlocking { + channel.connect( + InetSocketAddress( + quassel.address, + quassel.getMappedPort(4242) + ) + ) + + val session = ClientSession( + channel, ProtocolFeature.all, + listOf( + ProtocolMeta( + ProtocolVersion.Datastream, + 0x0000u + ) + ), + sslContext + ) + val coreState: CoreState = session.handshakeHandler.init( + "Quasseltest v0.1", + "2021-06-06", + FeatureSet.all() + ) + assertTrue(coreState is CoreState.Unconfigured) + assertThrows<HandshakeException.SetupException> { + session.handshakeHandler.configureCore( + username, + password, + "MongoDB", + emptyMap(), + "OAuth2", + emptyMap() + ) + } + session.handshakeHandler.configureCore( + username, + password, + "SQLite", + emptyMap(), + "Database", + emptyMap() + ) + assertThrows<HandshakeException.LoginException> { + session.handshakeHandler.login("acidburn", "ineverweardresses") + } + session.handshakeHandler.login(username, password) + channel.close() + } +} diff --git a/client/src/test/kotlin/de/justjanne/libquassel/client/EndToEndTest.kt b/client/src/test/kotlin/de/justjanne/libquassel/client/EndToEndTest.kt deleted file mode 100644 index 7daae9372f3bd03c197f588db452952dc0475a66..0000000000000000000000000000000000000000 --- a/client/src/test/kotlin/de/justjanne/libquassel/client/EndToEndTest.kt +++ /dev/null @@ -1,238 +0,0 @@ -/* - * libquassel - * Copyright (c) 2021 Janne Mareike Koschinski - * - * This Source Code Form is subject to the terms of the Mozilla Public License, - * v. 2.0. If a copy of the MPL was not distributed with this file, You can - * obtain one at https://mozilla.org/MPL/2.0/. - */ - -package de.justjanne.libquassel.client - -import de.justjanne.bitflags.of -import de.justjanne.libquassel.client.io.CoroutineChannel -import de.justjanne.libquassel.client.testutil.QuasselCoreContainer -import de.justjanne.libquassel.client.testutil.TestX509TrustManager -import de.justjanne.libquassel.protocol.connection.ClientHeader -import de.justjanne.libquassel.protocol.connection.ClientHeaderSerializer -import de.justjanne.libquassel.protocol.connection.CoreHeaderSerializer -import de.justjanne.libquassel.protocol.connection.ProtocolFeature -import de.justjanne.libquassel.protocol.connection.ProtocolMeta -import de.justjanne.libquassel.protocol.connection.ProtocolVersion -import de.justjanne.libquassel.protocol.features.FeatureSet -import de.justjanne.libquassel.protocol.io.ChainedByteBuffer -import de.justjanne.libquassel.protocol.models.HandshakeMessage -import de.justjanne.libquassel.protocol.serializers.HandshakeMessageSerializer -import de.justjanne.libquassel.protocol.serializers.handshake.ClientLoginSerializer -import de.justjanne.libquassel.protocol.serializers.qt.HandshakeMapSerializer -import de.justjanne.libquassel.protocol.serializers.qt.IntSerializer -import de.justjanne.testcontainersci.api.providedContainer -import de.justjanne.testcontainersci.extension.CiContainers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import java.net.InetSocketAddress -import java.nio.ByteBuffer -import javax.net.ssl.SSLContext - -@ExperimentalCoroutinesApi -@CiContainers -class EndToEndTest { - private val quassel = providedContainer("QUASSEL_CONTAINER") { - QuasselCoreContainer() - } - - private val sslContext = SSLContext.getInstance("TLSv1.3").apply { - init(null, arrayOf(TestX509TrustManager), null) - } - - private val connectionFeatureSet = FeatureSet.all() - private val sizeBuffer = ByteBuffer.allocateDirect(4) - private val sendBuffer = ChainedByteBuffer(direct = true) - private val channel = CoroutineChannel() - - private val username = "AzureDiamond" - private val password = "hunter2" - - @Test - fun testConnect(): Unit = runBlocking { - channel.connect( - InetSocketAddress( - quassel.address, - quassel.getMappedPort(4242) - ) - ) - - println("Writing protocol") - write(sizePrefix = false) { - ClientHeaderSerializer.serialize( - it, - ClientHeader( - features = ProtocolFeature.of( - ProtocolFeature.Compression, - ProtocolFeature.TLS - ), - versions = listOf( - ProtocolMeta( - ProtocolVersion.Datastream, - 0x0000u, - ), - ) - ), - connectionFeatureSet - ) - } - - println("Reading protocol") - read(4) { - val protocol = CoreHeaderSerializer.deserialize(it, connectionFeatureSet) - assertEquals( - ProtocolFeature.of( - ProtocolFeature.TLS, - ProtocolFeature.Compression - ), - protocol.features - ) - println("Negotiated protocol $protocol") - if (protocol.features.contains(ProtocolFeature.TLS)) { - channel.enableTLS(sslContext) - } - if (protocol.features.contains(ProtocolFeature.Compression)) { - channel.enableCompression() - } - } - println("Writing clientInit") - write { - HandshakeMessageSerializer.serialize( - it, - HandshakeMessage.ClientInit( - clientVersion = "Quasseldroid test", - buildDate = "Never", - featureSet = connectionFeatureSet - ), - connectionFeatureSet - ) - } - println("Reading clientInit response") - read { - println(HandshakeMessageSerializer.deserialize(it, connectionFeatureSet)) - } - println("Writing invalid core init") - write { - HandshakeMessageSerializer.serialize( - it, - HandshakeMessage.CoreSetupData( - adminUser = username, - adminPassword = password, - backend = "MongoDB", - setupData = emptyMap(), - authenticator = "OAuth2", - authSetupData = emptyMap(), - ), - connectionFeatureSet - ) - } - println("Reading invalid clientInit response") - read { - assertEquals( - HandshakeMessage.CoreSetupReject("Could not setup storage!"), - HandshakeMessageSerializer.deserialize(it, connectionFeatureSet) - ) - } - println("Writing valid core init") - write { - HandshakeMessageSerializer.serialize( - it, - HandshakeMessage.CoreSetupData( - adminUser = username, - adminPassword = password, - backend = "SQLite", - setupData = emptyMap(), - authenticator = "Database", - authSetupData = emptyMap(), - ), - connectionFeatureSet - ) - } - println("Reading valid clientInit response") - read { - println(HandshakeMessageSerializer.deserialize(it, connectionFeatureSet)) - } - println("Writing invalid clientLogin") - write { - HandshakeMapSerializer.serialize( - it, - ClientLoginSerializer.serialize( - HandshakeMessage.ClientLogin( - user = "acidburn", - password = "ineverweardresses" - ) - ), - connectionFeatureSet - ) - } - println("Reading invalid clientLogin response") - read { - assertEquals( - HandshakeMessage.ClientLoginReject( - "<b>Invalid username or password!</b><br>" + - "The username/password combination you supplied could not be found in the database." - ), - HandshakeMessageSerializer.deserialize(it, connectionFeatureSet) - ) - } - println("Writing valid clientLogin") - write { - HandshakeMessageSerializer.serialize( - it, - HandshakeMessage.ClientLogin( - user = username, - password = password - ), - connectionFeatureSet - ) - } - println("Reading valid clientLogin response") - read { - println(HandshakeMessageSerializer.deserialize(it, connectionFeatureSet)) - } - println("Reading valid session init") - read { - println(HandshakeMessageSerializer.deserialize(it, connectionFeatureSet)) - } - } - - private suspend fun readAmount(amount: Int? = null): Int { - if (amount != null) return amount - - sizeBuffer.clear() - channel.read(sizeBuffer) - sizeBuffer.flip() - val size = IntSerializer.deserialize(sizeBuffer, connectionFeatureSet) - sizeBuffer.clear() - return size - } - - private suspend fun write(sizePrefix: Boolean = true, f: suspend (ChainedByteBuffer) -> Unit) { - f(sendBuffer) - if (sizePrefix) { - sizeBuffer.clear() - sizeBuffer.putInt(sendBuffer.size) - sizeBuffer.flip() - channel.write(sizeBuffer) - sizeBuffer.clear() - } - channel.write(sendBuffer) - channel.flush() - sendBuffer.clear() - } - - private suspend fun <T> read(amount: Int? = null, f: suspend (ByteBuffer) -> T): T { - val amount1 = readAmount(amount) - val messageBuffer = ByteBuffer.allocateDirect(minOf(amount1, 65 * 1024 * 1024)) - channel.read(messageBuffer) - messageBuffer.flip() - return f(messageBuffer) - } -} diff --git a/protocol/build.gradle.kts b/protocol/build.gradle.kts index 0b0c873a0ec6ae8683861264c2843c28c273c6dc..d3bb98aa8b750ec298b8b0e09980bbe6f23a050c 100644 --- a/protocol/build.gradle.kts +++ b/protocol/build.gradle.kts @@ -20,6 +20,8 @@ dependencies { api("de.justjanne", "kotlin-bitflags", kotlinBitflagsVersion) val bouncyCastleVersion: String by project implementation("org.bouncycastle", "bcpkix-jdk15on", bouncyCastleVersion) + val sl4jVersion: String by project + implementation("org.slf4j", "slf4j-simple", sl4jVersion) api(project(":annotations")) ksp(project(":generator")) } diff --git a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/exceptions/HandshakeException.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/exceptions/HandshakeException.kt new file mode 100644 index 0000000000000000000000000000000000000000..2aacfc6db0a7576dafdfd95bd4920ef7a4d12cfe --- /dev/null +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/exceptions/HandshakeException.kt @@ -0,0 +1,16 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.exceptions + +sealed class HandshakeException(message: String) : Exception(message) { + class InitException(message: String) : HandshakeException(message) + class SetupException(message: String) : HandshakeException(message) + class LoginException(message: String) : HandshakeException(message) +} diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/io/CoroutineChannel.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/CoroutineChannel.kt similarity index 95% rename from client/src/main/kotlin/de/justjanne/libquassel/client/io/CoroutineChannel.kt rename to protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/CoroutineChannel.kt index 62ca790d076a380fcd860ff9afa18f05c1efd2c2..9ac728996772ad3d79f83d6c612a03d78448ab92 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/io/CoroutineChannel.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/CoroutineChannel.kt @@ -7,9 +7,8 @@ * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client.io +package de.justjanne.libquassel.protocol.io -import de.justjanne.libquassel.protocol.io.ChainedByteBuffer import de.justjanne.libquassel.protocol.util.update import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asCoroutineDispatcher diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/io/CoroutineChannelState.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/CoroutineChannelState.kt similarity index 80% rename from client/src/main/kotlin/de/justjanne/libquassel/client/io/CoroutineChannelState.kt rename to protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/CoroutineChannelState.kt index 6721f76a1a5714dc1c0ccf57b21ecc63004192c7..2f6539ea27027a2ab831148804046d45d4041e04 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/io/CoroutineChannelState.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/CoroutineChannelState.kt @@ -7,9 +7,9 @@ * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client.io +package de.justjanne.libquassel.protocol.io -import de.justjanne.libquassel.client.util.TlsInfo +import de.justjanne.libquassel.protocol.util.x509.TlsInfo data class CoroutineChannelState( val tlsInfo: TlsInfo? = null, diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/io/FixedDeflaterOutputStream.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/FixedDeflaterOutputStream.kt similarity index 84% rename from client/src/main/kotlin/de/justjanne/libquassel/client/io/FixedDeflaterOutputStream.kt rename to protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/FixedDeflaterOutputStream.kt index c50b3ad87b7186bfc540f3dc6aba31d8283d33d3..ab531db62204825199b7e5fbf0d036dfe870497f 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/io/FixedDeflaterOutputStream.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/FixedDeflaterOutputStream.kt @@ -7,9 +7,10 @@ * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client.io +package de.justjanne.libquassel.protocol.io import java.io.OutputStream +import java.net.SocketException import java.util.zip.DeflaterOutputStream /** @@ -25,6 +26,8 @@ class FixedDeflaterOutputStream( override fun close() { try { super.close() + } catch (e: SocketException) { + // ignored } finally { def.end() } diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/io/ReadableWrappedChannel.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/ReadableWrappedChannel.kt similarity index 98% rename from client/src/main/kotlin/de/justjanne/libquassel/client/io/ReadableWrappedChannel.kt rename to protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/ReadableWrappedChannel.kt index a94f37c1f743b0163d7daec682ecf8f531bb222c..860e9e53cd49260437df2c0bab2e89862ca35f79 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/io/ReadableWrappedChannel.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/ReadableWrappedChannel.kt @@ -7,7 +7,7 @@ * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client.io +package de.justjanne.libquassel.protocol.io import java.io.InputStream import java.nio.ByteBuffer diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/io/StreamChannel.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/StreamChannel.kt similarity index 96% rename from client/src/main/kotlin/de/justjanne/libquassel/client/io/StreamChannel.kt rename to protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/StreamChannel.kt index bfdc5fce7a12b518dc686edc296bd3a66eff587d..02c3785ce8cbec04c53d14d0bcca6192ada10d9e 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/io/StreamChannel.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/StreamChannel.kt @@ -7,9 +7,9 @@ * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client.io +package de.justjanne.libquassel.protocol.io -import de.justjanne.libquassel.client.util.TlsInfo +import de.justjanne.libquassel.protocol.util.x509.TlsInfo import java.io.Flushable import java.io.InputStream import java.io.OutputStream diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/io/WritableWrappedChannel.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/WritableWrappedChannel.kt similarity index 97% rename from client/src/main/kotlin/de/justjanne/libquassel/client/io/WritableWrappedChannel.kt rename to protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/WritableWrappedChannel.kt index 88122e096f34b7a5c6b1a6f7185f453ac3abd758..552340a2feebb50979b1ecb250e941fdd3723cb2 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/io/WritableWrappedChannel.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/io/WritableWrappedChannel.kt @@ -7,7 +7,7 @@ * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client.io +package de.justjanne.libquassel.protocol.io import java.io.OutputStream import java.nio.ByteBuffer diff --git a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/CommonSyncProxy.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/CommonSyncProxy.kt index 7a3d8041120f4e5bfb778cd382bfbeaf7d12ea68..de1d9a6bdd66a3b064f3afcd4328e8d49d788feb 100644 --- a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/CommonSyncProxy.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/CommonSyncProxy.kt @@ -14,6 +14,7 @@ import de.justjanne.libquassel.protocol.models.SignalProxyMessage import de.justjanne.libquassel.protocol.syncables.ObjectRepository import de.justjanne.libquassel.protocol.syncables.SyncableStub import de.justjanne.libquassel.protocol.variant.QVariantList +import kotlinx.coroutines.runBlocking class CommonSyncProxy( private val protocolSide: ProtocolSide, @@ -22,7 +23,9 @@ class CommonSyncProxy( ) : SyncProxy { override fun synchronize(syncable: SyncableStub) { if (objectRepository.add(syncable)) { - proxyMessageHandler.dispatch(SignalProxyMessage.InitRequest(syncable.className, syncable.objectName)) + runBlocking { + proxyMessageHandler.dispatch(SignalProxyMessage.InitRequest(syncable.className, syncable.objectName)) + } } } @@ -38,14 +41,16 @@ class CommonSyncProxy( arguments: QVariantList ) { if (target != protocolSide) { - proxyMessageHandler.emit( - SignalProxyMessage.Sync( - className, - objectName, - function, - arguments + runBlocking { + proxyMessageHandler.emit( + SignalProxyMessage.Sync( + className, + objectName, + function, + arguments + ) ) - ) + } } } @@ -55,12 +60,14 @@ class CommonSyncProxy( arguments: QVariantList ) { if (target != protocolSide) { - proxyMessageHandler.emit( - SignalProxyMessage.Rpc( - function, - arguments + runBlocking { + proxyMessageHandler.emit( + SignalProxyMessage.Rpc( + function, + arguments + ) ) - ) + } } } } diff --git a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/ConnectionHandler.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/ConnectionHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..23376b0df0fa930b3f2647591948dae16f5e67ff --- /dev/null +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/ConnectionHandler.kt @@ -0,0 +1,17 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.session + +import java.nio.ByteBuffer + +interface ConnectionHandler { + suspend fun init(channel: MessageChannel): Boolean + suspend fun read(buffer: ByteBuffer): Boolean +} diff --git a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/CoreState.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/CoreState.kt new file mode 100644 index 0000000000000000000000000000000000000000..c1c988e5ddfb1e31c835f31c1b5b62ad9fefe9e7 --- /dev/null +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/CoreState.kt @@ -0,0 +1,20 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.session + +import de.justjanne.libquassel.protocol.models.setup.BackendInfo + +sealed class CoreState { + object Configured : CoreState() + data class Unconfigured( + val databases: List<BackendInfo>, + val authenticators: List<BackendInfo> + ) : CoreState() +} diff --git a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/HandshakeMessageHandler.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/HandshakeHandler.kt similarity index 85% rename from protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/HandshakeMessageHandler.kt rename to protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/HandshakeHandler.kt index 331f13868a8f6e6979dcdf72012c38836535649e..79c278060b7b7361b6c84a8dc10310457d86fbb1 100644 --- a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/HandshakeMessageHandler.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/HandshakeHandler.kt @@ -9,13 +9,15 @@ package de.justjanne.libquassel.protocol.session +import de.justjanne.libquassel.protocol.exceptions.HandshakeException import de.justjanne.libquassel.protocol.features.FeatureSet import de.justjanne.libquassel.protocol.variant.QVariantMap -interface HandshakeMessageHandler { +interface HandshakeHandler : ConnectionHandler { /** * Register client and start connection */ + @Throws(HandshakeException.InitException::class) suspend fun init( /** * Human readable (HTML formatted) version of the client @@ -29,11 +31,12 @@ interface HandshakeMessageHandler { * Enabled client features for this connection */ featureSet: FeatureSet - ) + ): CoreState /** * Login to core with authentication data */ + @Throws(HandshakeException.LoginException::class) suspend fun login( /** * Username of the core account @@ -48,6 +51,7 @@ interface HandshakeMessageHandler { /** * Configure core for the first time */ + @Throws(HandshakeException.SetupException::class) suspend fun configureCore( /** * Username of a new core account to be created diff --git a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/MessageChannel.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/MessageChannel.kt new file mode 100644 index 0000000000000000000000000000000000000000..9bb1fed779642198a747bdf5a9ee25e9c5fde23e --- /dev/null +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/MessageChannel.kt @@ -0,0 +1,111 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.session + +import de.justjanne.libquassel.protocol.features.FeatureSet +import de.justjanne.libquassel.protocol.io.ChainedByteBuffer +import de.justjanne.libquassel.protocol.io.CoroutineChannel +import de.justjanne.libquassel.protocol.models.HandshakeMessage +import de.justjanne.libquassel.protocol.models.SignalProxyMessage +import de.justjanne.libquassel.protocol.serializers.HandshakeMessageSerializer +import de.justjanne.libquassel.protocol.serializers.SignalProxyMessageSerializer +import de.justjanne.libquassel.protocol.util.log.trace +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer + +class MessageChannel( + val channel: CoroutineChannel +) { + var negotiatedFeatures = FeatureSet.none() + + private var handlers = mutableListOf<ConnectionHandler>() + fun register(handler: ConnectionHandler) { + handlers.add(handler) + } + + private val sendBuffer = ThreadLocal.withInitial(::ChainedByteBuffer) + private val sizeBuffer = ThreadLocal.withInitial { ByteBuffer.allocateDirect(4) } + suspend fun init() { + setupHandlers() + } + + private suspend fun readAmount(): Int { + val sizeBuffer = sizeBuffer.get() + + sizeBuffer.clear() + channel.read(sizeBuffer) + sizeBuffer.flip() + val size = sizeBuffer.int + sizeBuffer.clear() + return size + } + + suspend fun read() { + val amount = readAmount() + val messageBuffer = ByteBuffer.allocateDirect(minOf(amount, 65 * 1024 * 1024)) + channel.read(messageBuffer) + messageBuffer.flip() + dispatch(messageBuffer) + } + + private suspend fun setupHandlers() { + while (true) { + val handler = handlers.firstOrNull() + logger.trace { "Setting up handler $handler" } + if (handler?.init(this) != true) { + break + } + logger.trace { "Handler $handler is done" } + handlers.removeFirst() + } + if (handlers.isEmpty()) { + logger.trace { "All handlers done" } + channel.close() + } + } + + private suspend fun dispatch(message: ByteBuffer) { + if (handlers.first().read(message)) { + handlers.removeFirst() + setupHandlers() + } + } + + suspend fun emit(message: HandshakeMessage) = emit { + logger.trace { "Writing handshake message $message" } + HandshakeMessageSerializer.serialize(it, message, negotiatedFeatures) + } + + suspend fun emit(message: SignalProxyMessage) = emit { + logger.trace { "Writing signal proxy message $message" } + SignalProxyMessageSerializer.serialize(it, message, negotiatedFeatures) + } + + suspend fun emit(sizePrefix: Boolean = true, f: (ChainedByteBuffer) -> Unit) { + val sendBuffer = sendBuffer.get() + val sizeBuffer = sizeBuffer.get() + + f(sendBuffer) + if (sizePrefix) { + sizeBuffer.clear() + sizeBuffer.putInt(sendBuffer.size) + sizeBuffer.flip() + channel.write(sizeBuffer) + sizeBuffer.clear() + } + channel.write(sendBuffer) + channel.flush() + sendBuffer.clear() + } + + companion object { + private val logger = LoggerFactory.getLogger(MessageChannel::class.java) + } +} diff --git a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/MessageChannelReadThread.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/MessageChannelReadThread.kt new file mode 100644 index 0000000000000000000000000000000000000000..b2efcf139b48da8ab7c1ec66134e6cc8ec06b83f --- /dev/null +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/MessageChannelReadThread.kt @@ -0,0 +1,36 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.session + +import de.justjanne.libquassel.protocol.util.log.info +import kotlinx.coroutines.runBlocking +import org.slf4j.LoggerFactory +import java.nio.channels.ClosedChannelException + +class MessageChannelReadThread( + val channel: MessageChannel +) : Thread("Message Channel Read Thread") { + override fun run() { + runBlocking { + try { + channel.init() + while (channel.channel.state().connected) { + channel.read() + } + } catch (e: ClosedChannelException) { + logger.info { "Channel closed" } + } + } + } + + companion object { + private val logger = LoggerFactory.getLogger(MessageChannelReadThread::class.java) + } +} diff --git a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/ProxyMessageHandler.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/ProxyMessageHandler.kt index 1264ec408b9afa7a2cc53ef9dae920db98f88971..8b2141a65f117e7ab2ea46ac7e5f0e6c2cf59512 100644 --- a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/ProxyMessageHandler.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/ProxyMessageHandler.kt @@ -11,7 +11,7 @@ package de.justjanne.libquassel.protocol.session import de.justjanne.libquassel.protocol.models.SignalProxyMessage -interface ProxyMessageHandler { - fun emit(message: SignalProxyMessage) - fun dispatch(message: SignalProxyMessage) +interface ProxyMessageHandler : ConnectionHandler { + suspend fun emit(message: SignalProxyMessage) + suspend fun dispatch(message: SignalProxyMessage) } diff --git a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/Session.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/Session.kt index d04f5de184d72830b29f4515f307a72fe27d3c65..056a86b1e3a3afc2bc61ada0bc060f22a6b678b0 100644 --- a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/Session.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/Session.kt @@ -10,6 +10,7 @@ package de.justjanne.libquassel.protocol.session import de.justjanne.libquassel.annotations.ProtocolSide +import de.justjanne.libquassel.protocol.models.BufferInfo import de.justjanne.libquassel.protocol.models.ids.IdentityId import de.justjanne.libquassel.protocol.models.ids.NetworkId import de.justjanne.libquassel.protocol.syncables.ObjectRepository @@ -32,6 +33,12 @@ interface Session { val proxy: SyncProxy val objectRepository: ObjectRepository + fun init( + identities: List<QVariantMap>, + bufferInfos: List<BufferInfo>, + networkIds: List<NetworkId> + ) + fun network(id: NetworkId): Network? fun addNetwork(id: NetworkId) fun removeNetwork(id: NetworkId) diff --git a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/common/IrcChannel.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/common/IrcChannel.kt index 6fc9c0edf2896b16fdf56ff71ee1ec620a8b5dc3..fa2dd5215b7910488584f03a7638bec56f24c501 100644 --- a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/common/IrcChannel.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/common/IrcChannel.kt @@ -238,7 +238,6 @@ open class IrcChannel( override fun removeChannelMode(mode: Char, value: String?) { val network = session?.network(network()) - println(network?.channelModeType(mode)) state.update { copy( channelModes = channelModes.run { diff --git a/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/log/Logger.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/log/Logger.kt new file mode 100644 index 0000000000000000000000000000000000000000..58a9c8dcea561f5848b599ced78a0497b8af09f4 --- /dev/null +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/log/Logger.kt @@ -0,0 +1,36 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.util.log + +import org.slf4j.Logger + +fun Logger.trace(f: () -> String) { + if (isTraceEnabled) { + trace(f()) + } +} + +fun Logger.debug(f: () -> String) { + if (isDebugEnabled) { + debug(f()) + } +} + +fun Logger.info(f: () -> String) { + if (isInfoEnabled) { + info(f()) + } +} + +fun Logger.warn(f: () -> String) { + if (isWarnEnabled) { + info(f()) + } +} diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/util/TlsInfo.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/x509/TlsInfo.kt similarity index 97% rename from client/src/main/kotlin/de/justjanne/libquassel/client/util/TlsInfo.kt rename to protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/x509/TlsInfo.kt index 7a316267ac2b449d7b3fb3e4a57fd693d813990a..69002ca74512f511e8bdd5d458d1525480649136 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/util/TlsInfo.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/x509/TlsInfo.kt @@ -7,7 +7,7 @@ * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client.util +package de.justjanne.libquassel.protocol.util.x509 import java.security.cert.Certificate import java.security.cert.X509Certificate diff --git a/client/src/main/kotlin/de/justjanne/libquassel/client/util/X509Helper.kt b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/x509/X509Helper.kt similarity index 93% rename from client/src/main/kotlin/de/justjanne/libquassel/client/util/X509Helper.kt rename to protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/x509/X509Helper.kt index 91c5af182067e1920b4c1c6a1b5c86531ecd1403..ef63e28d2d7b9c674816eac2ade173cb37956637 100644 --- a/client/src/main/kotlin/de/justjanne/libquassel/client/util/X509Helper.kt +++ b/protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/x509/X509Helper.kt @@ -7,7 +7,7 @@ * obtain one at https://mozilla.org/MPL/2.0/. */ -package de.justjanne.libquassel.client.util +package de.justjanne.libquassel.protocol.util.x509 import java.io.ByteArrayInputStream import java.security.cert.Certificate diff --git a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/serializers/qt/QCharSerializerTest.kt b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/serializers/qt/QCharSerializerTest.kt index 1057ade6c0a8c00d51ded718944f2ceedd1df896..559ed796f0ec60ff8d1d3a2b71b0b20f6b3047c3 100644 --- a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/serializers/qt/QCharSerializerTest.kt +++ b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/serializers/qt/QCharSerializerTest.kt @@ -63,17 +63,17 @@ class QCharSerializerTest { for (value in 'a'..'z') primitiveSerializerTest( QCharSerializer, value, - byteBufferOf(0, value.toByte()) + byteBufferOf(0, value.code.toByte()) ) for (value in 'A'..'Z') primitiveSerializerTest( QCharSerializer, value, - byteBufferOf(0, value.toByte()) + byteBufferOf(0, value.code.toByte()) ) for (value in '0'..'9') primitiveSerializerTest( QCharSerializer, value, - byteBufferOf(0, value.toByte()) + byteBufferOf(0, value.code.toByte()) ) } diff --git a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewConfigTest.kt b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewConfigTest.kt index bf8ce11e9437ee2258bbdc043e720386a9ab4a5b..fbdce229fde7cfdaa02bb53816738fb6047e5ca2 100644 --- a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewConfigTest.kt +++ b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewConfigTest.kt @@ -299,9 +299,7 @@ class BufferViewConfigTest { val hiddenSize = value.hiddenBuffers().size val removedSize = value.removedBuffers().size val buffer = value.buffers().first() - println(value.buffers()) value.moveBuffer(buffer, 3) - println(value.buffers()) assertEquals(3, value.buffers().indexOf(buffer)) assertEquals(bufferSize, value.buffers().size) assertEquals(hiddenSize, value.hiddenBuffers().size) diff --git a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/IgnoreListManagerTest.kt b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/IgnoreListManagerTest.kt index 2ede1b74273803cc9b358f99471723485c82ed57..7d6b1220e304547ba8eb52c0e63f2ccd2cd77f7d 100644 --- a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/IgnoreListManagerTest.kt +++ b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/IgnoreListManagerTest.kt @@ -269,10 +269,6 @@ class IgnoreListManagerTest { assertFalse(value.isEmpty()) for (rule in value.state().rules) { - println(rule) - val index = value.state().indexOf(rule.ignoreRule) - println(index) - println(value.state().rules[index]) value.removeIgnoreListItem(rule.ignoreRule) } assertTrue(value.isEmpty()) diff --git a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannelTest.kt b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannelTest.kt index 76c03f6ca6b293f7bda058aa7659cec1c3863438..94e4d2ee760b30ac1acbcdd1cb46fb613b35d943 100644 --- a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannelTest.kt +++ b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannelTest.kt @@ -300,10 +300,6 @@ class IrcChannelTest { ) session.networks.add(network) val channel = network.state().ircChannels.values.first() - println(network.supports()) - println(network.channelModes()) - println(network.session) - println(channel.session) assertEquals(emptyMap<Char, Set<String>>(), channel.state().channelModes.a) assertEquals(emptyMap<Char, String>(), channel.state().channelModes.b) @@ -390,7 +386,6 @@ class IrcChannelTest { ) session.networks.add(network) val channel = network.state().ircChannels.values.first() - println(network.channelModes()) assertEquals(expected.a, channel.state().channelModes.a) assertEquals(expected.b, channel.state().channelModes.b) @@ -441,7 +436,6 @@ class IrcChannelTest { ) session.networks.add(network) val channel = network.state().ircChannels.values.first() - println(network.channelModes()) assertEquals(expected.a, channel.state().channelModes.a) assertEquals(expected.b, channel.state().channelModes.b) @@ -532,7 +526,6 @@ class IrcChannelTest { ) session.networks.add(network) val channel = network.state().ircChannels.values.first() - println(network.channelModes()) assertEquals(expected.a, channel.state().channelModes.a) assertEquals(expected.b, channel.state().channelModes.b) @@ -601,7 +594,6 @@ class IrcChannelTest { ) session.networks.add(network) val channel = network.state().ircChannels.values.first() - println(network.channelModes()) assertEquals(expected.a, channel.state().channelModes.a) assertEquals(expected.b, channel.state().channelModes.b) @@ -678,7 +670,6 @@ class IrcChannelTest { ) session.networks.add(network) val channel = network.state().ircChannels.values.first() - println(network.channelModes()) assertEquals(expected.a, channel.state().channelModes.a) assertEquals(expected.b, channel.state().channelModes.b) diff --git a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/Random.kt b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/Random.kt index f76348bb8329b616e7c346f36a3440e4bb47436b..5bc159be1bdd8a73209b0b45e35c945271799fd9 100644 --- a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/Random.kt +++ b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/Random.kt @@ -66,18 +66,18 @@ fun Random.nextNetwork(networkId: NetworkId) = NetworkState( ircUsers = List(nextInt(20)) { IrcUser(state = nextIrcUser(networkId)) }.associateBy(IrcUser::nick).mapKeys { (key) -> - key.toLowerCase(Locale.ROOT) + key.lowercase(Locale.ROOT) }, ircChannels = List(nextInt(20)) { IrcChannel(state = nextIrcChannel(networkId)) }.associateBy(IrcChannel::name).mapKeys { (key) -> - key.toLowerCase(Locale.ROOT) + key.lowercase(Locale.ROOT) }, supports = List(nextInt(20)) { - nextString().toUpperCase(Locale.ROOT) to nextString() + nextString().uppercase(Locale.ROOT) to nextString() }.toMap(), caps = List(nextInt(20)) { - nextString().toLowerCase(Locale.ROOT) to nextString() + nextString().lowercase(Locale.ROOT) to nextString() }.toMap(), capsEnabled = List(nextInt(20)) { nextString() diff --git a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/mocks/EmptyProxyMessageHandler.kt b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/mocks/EmptyProxyMessageHandler.kt deleted file mode 100644 index 9df669994c098708b244581a4c0164e119157777..0000000000000000000000000000000000000000 --- a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/mocks/EmptyProxyMessageHandler.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * libquassel - * Copyright (c) 2021 Janne Mareike Koschinski - * - * This Source Code Form is subject to the terms of the Mozilla Public License, - * v. 2.0. If a copy of the MPL was not distributed with this file, You can - * obtain one at https://mozilla.org/MPL/2.0/. - */ - -package de.justjanne.libquassel.protocol.testutil.mocks - -import de.justjanne.libquassel.protocol.models.SignalProxyMessage -import de.justjanne.libquassel.protocol.session.ProxyMessageHandler - -open class EmptyProxyMessageHandler : ProxyMessageHandler { - override fun emit(message: SignalProxyMessage) = Unit - override fun dispatch(message: SignalProxyMessage) = Unit -} diff --git a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/mocks/EmptySession.kt b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/mocks/EmptySession.kt index 13a7edf8ebe23889922310d62926c3f587ec56e7..aa52533f18574315896cbd6a32d82802c6eed4e9 100644 --- a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/mocks/EmptySession.kt +++ b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/mocks/EmptySession.kt @@ -10,6 +10,7 @@ package de.justjanne.libquassel.protocol.testutil.mocks import de.justjanne.libquassel.annotations.ProtocolSide +import de.justjanne.libquassel.protocol.models.BufferInfo import de.justjanne.libquassel.protocol.models.ids.IdentityId import de.justjanne.libquassel.protocol.models.ids.NetworkId import de.justjanne.libquassel.protocol.session.Session @@ -33,6 +34,8 @@ open class EmptySession : Session { final override val objectRepository = ObjectRepository() override val proxy = EmptySyncProxy() + override fun init(identities: List<QVariantMap>, bufferInfos: List<BufferInfo>, networkIds: List<NetworkId>) = Unit + override fun network(id: NetworkId): Network? = null override fun addNetwork(id: NetworkId) = Unit override fun removeNetwork(id: NetworkId) = Unit diff --git a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/signalProxySerializerTest.kt b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/signalProxySerializerTest.kt index 361ba39db55b3d0dc5e666670dc80bef4464f16f..815f8bd95924d1a8ef661183e090f1ee4b6e54a5 100644 --- a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/signalProxySerializerTest.kt +++ b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/signalProxySerializerTest.kt @@ -9,7 +9,6 @@ package de.justjanne.libquassel.protocol.testutil import de.justjanne.libquassel.protocol.features.FeatureSet -import de.justjanne.libquassel.protocol.io.contentToString import de.justjanne.libquassel.protocol.io.useChainedByteBuffer import de.justjanne.libquassel.protocol.models.SignalProxyMessage import de.justjanne.libquassel.protocol.serializers.SignalProxyMessageSerializer @@ -50,9 +49,6 @@ inline fun <reified T : SignalProxyMessage> signalProxySerializerTest( val after = SignalProxyMessageSerializer.deserialize( useChainedByteBuffer { SignalProxyMessageSerializer.serialize(it, value, featureSet) - if (encoded == null) { - println(it.toBuffer().contentToString()) - } }, featureSet ) diff --git a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/collections/MoveTest.kt b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/collections/MoveTest.kt index 84e203a80fc5c025edf49c217fbf4d6a2b31caf4..0e63985f76f22e690759f5bff2b1955fede37bf5 100644 --- a/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/collections/MoveTest.kt +++ b/protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/collections/MoveTest.kt @@ -54,7 +54,6 @@ class MoveTest { @Test fun movesCorrectly() { - val data = listOf(1, 2, 3, 4, 5, 7).shuffled() assertEquals( listOf('a', 'c', 'd', 'e', 'b', 'f'), listOf('a', 'b', 'c', 'd', 'e', 'f').move('b', 4)