From 0a94785bc42e556f36313f3dbb22ea47eb7913e9 Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <janne@kuschku.de>
Date: Sun, 6 Jun 2021 14:07:16 +0200
Subject: [PATCH] Implement new connection handling

---
 client/build.gradle.kts                       |   2 +-
 .../client/session/ClientConnectionHandler.kt |  40 +++
 .../client/session/ClientHandshakeHandler.kt  | 137 ++++++++++
 .../client/session/ClientMagicHandler.kt      |  66 +++++
 .../ClientProxyMessageHandler.kt              |  27 +-
 .../client/{ => session}/ClientRpcHandler.kt  |   2 +-
 .../client/{ => session}/ClientSession.kt     |  41 ++-
 .../{ => session}/ClientSessionState.kt       |   2 +-
 .../client/util/CoroutineKeyedQueue.kt        |  33 +++
 .../libquassel/client/util/CoroutineQueue.kt  |  28 +++
 .../justjanne/libquassel/client/ClientTest.kt |  97 +++++++
 .../libquassel/client/EndToEndTest.kt         | 238 ------------------
 protocol/build.gradle.kts                     |   2 +
 .../protocol/exceptions/HandshakeException.kt |  16 ++
 .../protocol}/io/CoroutineChannel.kt          |   3 +-
 .../protocol}/io/CoroutineChannelState.kt     |   4 +-
 .../protocol}/io/FixedDeflaterOutputStream.kt |   5 +-
 .../protocol}/io/ReadableWrappedChannel.kt    |   2 +-
 .../libquassel/protocol}/io/StreamChannel.kt  |   4 +-
 .../protocol}/io/WritableWrappedChannel.kt    |   2 +-
 .../protocol/session/CommonSyncProxy.kt       |  33 ++-
 .../protocol/session/ConnectionHandler.kt     |  17 ++
 .../libquassel/protocol/session/CoreState.kt  |  20 ++
 ...eMessageHandler.kt => HandshakeHandler.kt} |   8 +-
 .../protocol/session/MessageChannel.kt        | 111 ++++++++
 .../session/MessageChannelReadThread.kt       |  36 +++
 .../protocol/session/ProxyMessageHandler.kt   |   6 +-
 .../libquassel/protocol/session/Session.kt    |   7 +
 .../protocol/syncables/common/IrcChannel.kt   |   1 -
 .../libquassel/protocol/util/log/Logger.kt    |  36 +++
 .../libquassel/protocol/util/x509}/TlsInfo.kt |   2 +-
 .../protocol/util/x509}/X509Helper.kt         |   2 +-
 .../serializers/qt/QCharSerializerTest.kt     |   6 +-
 .../syncables/BufferViewConfigTest.kt         |   2 -
 .../syncables/IgnoreListManagerTest.kt        |   4 -
 .../protocol/syncables/IrcChannelTest.kt      |   9 -
 .../libquassel/protocol/testutil/Random.kt    |   8 +-
 .../mocks/EmptyProxyMessageHandler.kt         |  18 --
 .../protocol/testutil/mocks/EmptySession.kt   |   3 +
 .../testutil/signalProxySerializerTest.kt     |   4 -
 .../protocol/util/collections/MoveTest.kt     |   1 -
 41 files changed, 758 insertions(+), 327 deletions(-)
 create mode 100644 client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientConnectionHandler.kt
 create mode 100644 client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientHandshakeHandler.kt
 create mode 100644 client/src/main/kotlin/de/justjanne/libquassel/client/session/ClientMagicHandler.kt
 rename client/src/main/kotlin/de/justjanne/libquassel/client/{ => session}/ClientProxyMessageHandler.kt (72%)
 rename client/src/main/kotlin/de/justjanne/libquassel/client/{ => session}/ClientRpcHandler.kt (97%)
 rename client/src/main/kotlin/de/justjanne/libquassel/client/{ => session}/ClientSession.kt (77%)
 rename client/src/main/kotlin/de/justjanne/libquassel/client/{ => session}/ClientSessionState.kt (97%)
 create mode 100644 client/src/main/kotlin/de/justjanne/libquassel/client/util/CoroutineKeyedQueue.kt
 create mode 100644 client/src/main/kotlin/de/justjanne/libquassel/client/util/CoroutineQueue.kt
 create mode 100644 client/src/test/kotlin/de/justjanne/libquassel/client/ClientTest.kt
 delete mode 100644 client/src/test/kotlin/de/justjanne/libquassel/client/EndToEndTest.kt
 create mode 100644 protocol/src/main/kotlin/de/justjanne/libquassel/protocol/exceptions/HandshakeException.kt
 rename {client/src/main/kotlin/de/justjanne/libquassel/client => protocol/src/main/kotlin/de/justjanne/libquassel/protocol}/io/CoroutineChannel.kt (95%)
 rename {client/src/main/kotlin/de/justjanne/libquassel/client => protocol/src/main/kotlin/de/justjanne/libquassel/protocol}/io/CoroutineChannelState.kt (80%)
 rename {client/src/main/kotlin/de/justjanne/libquassel/client => protocol/src/main/kotlin/de/justjanne/libquassel/protocol}/io/FixedDeflaterOutputStream.kt (84%)
 rename {client/src/main/kotlin/de/justjanne/libquassel/client => protocol/src/main/kotlin/de/justjanne/libquassel/protocol}/io/ReadableWrappedChannel.kt (98%)
 rename {client/src/main/kotlin/de/justjanne/libquassel/client => protocol/src/main/kotlin/de/justjanne/libquassel/protocol}/io/StreamChannel.kt (96%)
 rename {client/src/main/kotlin/de/justjanne/libquassel/client => protocol/src/main/kotlin/de/justjanne/libquassel/protocol}/io/WritableWrappedChannel.kt (97%)
 create mode 100644 protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/ConnectionHandler.kt
 create mode 100644 protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/CoreState.kt
 rename protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/{HandshakeMessageHandler.kt => HandshakeHandler.kt} (85%)
 create mode 100644 protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/MessageChannel.kt
 create mode 100644 protocol/src/main/kotlin/de/justjanne/libquassel/protocol/session/MessageChannelReadThread.kt
 create mode 100644 protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/log/Logger.kt
 rename {client/src/main/kotlin/de/justjanne/libquassel/client/util => protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/x509}/TlsInfo.kt (97%)
 rename {client/src/main/kotlin/de/justjanne/libquassel/client/util => protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/x509}/X509Helper.kt (93%)
 delete mode 100644 protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/mocks/EmptyProxyMessageHandler.kt

diff --git a/client/build.gradle.kts b/client/build.gradle.kts
index 60fff23..97528d7 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 0000000..a8fc8c5
--- /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 0000000..511a4f7
--- /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 0000000..31f78d9
--- /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 eb43d04..c444b84 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 1a16c05..c2e5257 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 07ee170..0d6d847 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 179ee40..9d29f8f 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 0000000..ae4a2c9
--- /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 0000000..c97b76d
--- /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 0000000..b9b1ea7
--- /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 7daae93..0000000
--- 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 0b0c873..d3bb98a 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 0000000..2aacfc6
--- /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 62ca790..9ac7289 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 6721f76..2f6539e 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 c50b3ad..ab531db 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 a94f37c..860e9e5 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 bfdc5fc..02c3785 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 88122e0..552340a 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 7a3d804..de1d9a6 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 0000000..23376b0
--- /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 0000000..c1c988e
--- /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 331f138..79c2780 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 0000000..9bb1fed
--- /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 0000000..b2efcf1
--- /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 1264ec4..8b2141a 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 d04f5de..056a86b 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 6fc9c0e..fa2dd52 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 0000000..58a9c8d
--- /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 7a31626..69002ca 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 91c5af1..ef63e28 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 1057ade..559ed79 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 bf8ce11..fbdce22 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 2ede1b7..7d6b122 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 76c03f6..94e4d2e 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 f76348b..5bc159b 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 9df6699..0000000
--- 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 13a7edf..aa52533 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 361ba39..815f8bd 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 84e203a..0e63985 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)
-- 
GitLab