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)