diff --git a/libquassel-client/src/main/kotlin/de/justjanne/libquassel/client/io/FixedDeflaterOutputStream.kt b/libquassel-client/src/main/kotlin/de/justjanne/libquassel/client/io/FixedDeflaterOutputStream.kt index 56eafab68b0b8eca17119c8e3f71d52bf22cd773..ff09c58f00e094fddf106b97abfab3121e15380a 100644 --- a/libquassel-client/src/main/kotlin/de/justjanne/libquassel/client/io/FixedDeflaterOutputStream.kt +++ b/libquassel-client/src/main/kotlin/de/justjanne/libquassel/client/io/FixedDeflaterOutputStream.kt @@ -22,6 +22,10 @@ package de.justjanne.libquassel.client.io import java.io.OutputStream import java.util.zip.DeflaterOutputStream +/** + * Wrapper class around a [DeflaterOutputStream] correctly handling closing of + * the current stream + */ class FixedDeflaterOutputStream( stream: OutputStream ) : DeflaterOutputStream(stream, true) { diff --git a/libquassel-client/src/main/kotlin/de/justjanne/libquassel/client/util/TlsInfo.kt b/libquassel-client/src/main/kotlin/de/justjanne/libquassel/client/util/TlsInfo.kt index db0f048ea2daa90648ad33ebaebdbde526ed6759..f5f46baa3e7306b45e6b60b53d326bc3c6778ab0 100644 --- a/libquassel-client/src/main/kotlin/de/justjanne/libquassel/client/util/TlsInfo.kt +++ b/libquassel-client/src/main/kotlin/de/justjanne/libquassel/client/util/TlsInfo.kt @@ -23,10 +23,26 @@ import java.security.cert.Certificate import java.security.cert.X509Certificate import javax.net.ssl.SSLSession +/** + * Model representing the metadata of a negotiated TLS session + */ data class TlsInfo( + /** + * Name of the TLS protocol, e.g. TLSv1.3 + */ val protocol: String, + /** + * Negotiated cipher suite + */ val cipherSuite: String, + /** + * Negotiated key exchange mechanism if applicable + * Deprecated in TLSv1.3 + */ val keyExchangeMechanism: String?, + /** + * Peer certificate chain + */ val certificateChain: List<X509Certificate>, ) { @@ -54,6 +70,9 @@ data class TlsInfo( } } + /** + * Obtain the TLS metadata of an existing [SSLSession] + */ fun ofSession(session: SSLSession): TlsInfo? { val (cipherSuite, keyExchangeMechanism) = parseCipherSuite( session.protocol, diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/serializers/handshake/ClientInitAckSerializer.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/serializers/handshake/ClientInitAckSerializer.kt index 5aa439aeb1e71e908aaac17d4b75f4cd6f0c307d..f8655c6f74de3e445b3ab384121302a9761eeeae 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/serializers/handshake/ClientInitAckSerializer.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/serializers/handshake/ClientInitAckSerializer.kt @@ -49,10 +49,10 @@ object ClientInitAckSerializer : HandshakeSerializer<HandshakeMessage.ClientInit ) override fun deserialize(data: QVariantMap) = HandshakeMessage.ClientInitAck( - coreFeatures = LegacyFeature.of(data["CoreFeatures"].into<UInt>()), + coreConfigured = data["Configured"].into(), backendInfo = data["StorageBackends"].into(emptyList()), authenticatorInfo = data["Authenticators"].into(emptyList()), - coreConfigured = data["Configured"].into(), + coreFeatures = LegacyFeature.of(data["CoreFeatures"].into<UInt>()), featureList = data["FeatureList"].into<QStringList>(emptyList()) .filterNotNull() .map(::QuasselFeatureName), diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/serializers/qt/StringSerializer.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/serializers/qt/StringSerializer.kt index 4174161a3dbfb18e60b9de81daacada938dcb001..4de6d627213e9f91ae9c808663cd5d052567737d 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/serializers/qt/StringSerializer.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/serializers/qt/StringSerializer.kt @@ -71,6 +71,9 @@ abstract class StringSerializer( return result } + /** + * Serialize a string, without length prefix, as a byte size + */ fun serializeRaw(data: String?): ByteBuffer { val result = encoder().encode(data) if (nullLimited) { @@ -82,6 +85,9 @@ abstract class StringSerializer( return result } + /** + * Deserialize a string from a given byte slice + */ fun deserializeRaw(data: ByteBuffer): String { if (nullLimited) { data.limit(removeNullBytes(data.limit())) diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/types/HandshakeMessage.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/types/HandshakeMessage.kt index 7126e635c5a84b6cd0d3c3393bd30688936893e5..447e6a299f1a637bcd9f100bc123683b2d5afaee 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/types/HandshakeMessage.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/types/HandshakeMessage.kt @@ -24,63 +24,186 @@ import de.justjanne.libquassel.protocol.features.QuasselFeatureName import de.justjanne.libquassel.protocol.variant.QVariantList import de.justjanne.libquassel.protocol.variant.QVariantMap +/** + * Model classes for messages exchanged during handshake + */ sealed class HandshakeMessage { + /** + * Client registration message containing metadata about the connecting client + * Core should respond with either [ClientInitAck] or [ClientInitReject] + */ data class ClientInit( + /** + * Human readable (HTML formatted) version of the client + */ val clientVersion: String?, + /** + * Build timestamp of the client + */ val buildDate: String?, + /** + * Set of legacy features supported + */ val clientFeatures: LegacyFeatures, + /** + * List of supported features. If a feature can also be represented as + * legacy feature, it is included in both. + */ val featureList: List<QuasselFeatureName> ) : HandshakeMessage() + /** + * Message representing a successful client registration attempt + * Contains metadata about the core + * Client should proceed with either [ClientLogin] or [CoreSetupData] + */ data class ClientInitAck( - val coreFeatures: LegacyFeatures, + /** + * Whether or not the core needs to be configured via the quassel protocol + */ val coreConfigured: Boolean?, + /** + * If the core needs to be configured, this contains metadata about the + * supported storage backends + */ val backendInfo: QVariantList, + /** + * If the core needs to be configured, this contains metadata about the + * supported authentication backends + */ val authenticatorInfo: QVariantList, + /** + * Set of legacy features supported + */ + val coreFeatures: LegacyFeatures, + /** + * List of supported features. If a feature can also be represented as + * legacy feature, it is included in both. + */ val featureList: List<QuasselFeatureName> ) : HandshakeMessage() + /** + * Message representing a failed client registration attempt + * Client should abort the connection + */ data class ClientInitReject( + /**Username + * HTML-formatted error message + */ val errorString: String? ) : HandshakeMessage() + /** + * Client login message containing authentication data + * Core should respond with either [ClientLoginAck] or [ClientLoginReject] + */ data class ClientLogin( + /** + * Username of the core account + */ val user: String?, + /** + * Password of the core account + */ val password: String? ) : HandshakeMessage() + /** + * Message representing a successful client login attempt + * Client will receive [SessionInit] immediately afterwards + */ object ClientLoginAck : HandshakeMessage() { override fun toString(): String { return "ClientLoginAck" } } + /** + * Message representing a failed client login attempt + * Client should retry with different [ClientLogin] or abort the connection + */ data class ClientLoginReject( + /** + * HTML-formatted error message + */ val errorString: String? ) : HandshakeMessage() + /** + * Message representing a successful core configuration attempt + * Client should proceed with [ClientLogin] + */ object CoreSetupAck : HandshakeMessage() { override fun toString(): String { return "CoreSetupAck" } } + /** + * Core configuration message containing initial configuration properties + * Configuration has to happen before login + * Core should respond with either [CoreSetupAck] or [CoreSetupReject] + */ data class CoreSetupData( + /** + * Username of a new core account to be created + */ val adminUser: String?, + /** + * Password of a new core account to be created + */ val adminPassword: String?, + /** + * Chosen storage backend id + */ val backend: String?, + /** + * Storage backend configuration data + */ val setupData: QVariantMap?, + /** + * Chosen authenticator backend id + */ val authenticator: String?, + /** + * Authenticator backend configuration data + */ val authSetupData: QVariantMap? ) : HandshakeMessage() + /** + * Message representing a failed core configuration attempt + * Client should retry with different [CoreSetupData] or abort the connection + */ data class CoreSetupReject( + /** + * HTML-formatted error message + */ val errorString: String? ) : HandshakeMessage() + /** + * Initial session data for the client + */ data class SessionInit( + /** + * List of Identity sync objects existing at the current time + * Identity objects created or modified after [SessionInit] will be defined + * via sync updates and RPC identity creation messages + */ val identities: QVariantList?, + /** + * List of existing buffers at the current time + * Buffers created or deleted after [SessionInit] will be defined via RPC + * messages + */ val bufferInfos: QVariantList?, + /** + * List of Ids of Network sync objects existing at the current time + * Network objects created or modified after [SessionInit] will be defined + * via sync updates and RPC identity creation messages + */ val networkIds: QVariantList? ) : HandshakeMessage() } diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/types/TimeSpec.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/types/TimeSpec.kt index 6d9bcf37e3c997c62b9530ab347a87e6ddc57a21..2b06efdb197a577ec064c9ca3589ff15e556ae7f 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/types/TimeSpec.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/types/TimeSpec.kt @@ -19,15 +19,50 @@ package de.justjanne.libquassel.protocol.types -enum class TimeSpec(val value: Byte) { +/** + * Zoned definition for timestamps + */ +enum class TimeSpec( + /** + * Underlying representation + */ + val value: Byte +) { + /** + * Unknown zone data + * Should be treated like [LocalStandard] + */ LocalUnknown(-1), + + /** + * Local zone data + * Should be serialized as local time without DST + */ LocalStandard(0), + + /** + * Local zone data + * Should be serialized as local time with DST, if applicable + */ LocalDST(1), + + /** + * Universal Time Coordinated + * Should be treated as a zone offset of 0 + */ UTC(2), + + /** + * Time with specified offset in seconds + */ OffsetFromUTC(3); companion object { private val map = values().associateBy(TimeSpec::value) + + /** + * Obtain a zone specification by its underlying representation + */ fun of(type: Byte) = map[type] } } diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/variant/QtType.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/variant/QtType.kt index c99fc1a1fd5137cc1867f5d7f192c7845b827f49..95169e39e0bbfe6a9302dcacbdd0bd9a231aaf1b 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/variant/QtType.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/variant/QtType.kt @@ -19,33 +19,154 @@ package de.justjanne.libquassel.protocol.variant -enum class QtType(val id: kotlin.Int) { +/** + * Supported qt types for serialization + */ +enum class QtType( + /** + * Underlying representation + */ + val id: kotlin.Int +) { + /** + * Void, no data at all + */ Void(0), + + /** + * 8-bit boolean, 0 is false, everything else is true + * See [kotlin.Boolean] + */ Bool(1), + + /** + * 8-bit signed integer + * See [kotlin.Byte] + */ Char(131), + + /** + * 8-bit unsigned integer + * See [kotlin.UByte] + */ UChar(134), + + /** + * 16-bit signed integer + * See [kotlin.Short] + */ Short(130), + + /** + * 16-bit unsigned integer + * See [kotlin.UShort] + */ UShort(133), + + /** + * 32-bit signed integer + * See [kotlin.Int] + */ Int(2), + + /** + * 32-bit unsigned integer + * See [kotlin.UInt] + */ UInt(3), + + /** + * 64-bit signed integer + * See [kotlin.Long] + */ Long(129), + + /** + * 64-bit unsigned integer + * See [kotlin.ULong] + */ ULong(132), + + /** + * 32-bit IEEE 754 float + * See [kotlin.Float] + */ Float(135), + + /** + * 64-bit IEEE 754 float + * See [kotlin.Double] + */ Double(6), + + /** + * Date in the gregorian calender as julian date + * See [org.threeten.bp.LocalDate] + */ QDate(14), + + /** + * Relative time in milliseconds since midnight + * See [org.threeten.bp.LocalTime] + */ QTime(15), + + /** + * Timestamp composed out of QDate, QTime and a zone specifier + */ QDateTime(16), + + /** + * 16-bit unicode character + * See [kotlin.Char] + */ QChar(7), + + /** + * UTF-16 big endian string + * See [kotlin.String] + */ QString(10), + + /** + * Length prefixes list of UTF-16 big endian strings + */ QStringList(11), + + /** + * Length prefixed slice of binary data + * See [java.nio.ByteBuffer] + */ QByteArray(12), + + /** + * Typed box + * See [QVariant] + */ QVariant(138), + + /** + * Length prefixed map of [QString] to [QVariant] + */ QVariantMap(8), + + /** + * Length prefixed list of [QVariant] + */ QVariantList(9), + + /** + * Custom data with a special (de-)serializer + * See [QuasselType] + */ UserType(127); companion object { private val values = values().associateBy(QtType::id) + + /** + * Obtain a QtType by its underlying representation + */ fun of(id: kotlin.Int): QtType? = values[id] } } diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/variant/QuasselType.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/variant/QuasselType.kt index b1636181e610160c7631d8ba1da3114d1776dc60..fb20daf67b99acba90bc3fec5311aa6552668005 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/variant/QuasselType.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/variant/QuasselType.kt @@ -19,28 +19,107 @@ package de.justjanne.libquassel.protocol.variant +/** + * Supported quassel types for serialization + */ enum class QuasselType( + /** + * Standardized name of the type + */ val typeName: String, + /** + * Actual underlying serialization type + */ val qtType: QtType = QtType.UserType, ) { + /** + * Type for [de.justjanne.libquassel.protocol.types.BufferId] + */ BufferId("BufferId"), + + /** + * Type for [de.justjanne.libquassel.protocol.types.BufferInfo] + */ BufferInfo("BufferInfo"), + + /** + * Type for [de.justjanne.libquassel.protocol.types.DccIpDetectionMode] + */ DccConfigIpDetectionMode("DccConfig::IpDetectionMode"), + + /** + * Type for [de.justjanne.libquassel.protocol.types.DccPortSelectionMode] + */ DccConfigPortSelectionMode("DccConfig::PortSelectionMode"), + + /** + * Type for IrcUser objects + * Serialized as [QVariantMap] + */ IrcUser("IrcUser"), + /** + * Type for IrcChannel objects + * Serialized as [QVariantMap] + */ IrcChannel("IrcChannel"), + /** + * Type for Identity objects + * Serialized as [QVariantMap] + */ Identity("Identity"), + /** + * Type for [de.justjanne.libquassel.protocol.types.IdentityId] + */ IdentityId("IdentityId"), + + /** + * Type for [de.justjanne.libquassel.protocol.types.Message] + */ Message("Message"), + + /** + * Type for [de.justjanne.libquassel.protocol.types.MsgId] + */ MsgId("MsgId"), + + /** + * Type for [de.justjanne.libquassel.protocol.types.NetworkId] + */ NetworkId("NetworkId"), + /** + * Type for NetworkInfo objects + * Serialized as [QVariantMap] + */ NetworkInfo("NetworkInfo"), + /** + * Type for NetworkServer objects + * Serialized as [QVariantMap] + */ NetworkServer("Network::Server"), + + /** + * Type for [java.net.InetAddress] + */ QHostAddress("QHostAddress"), + + /** + * Serialization type for PeerPtr. + * + * This type is only used as placeholder for values that should only be used + * internally. During serialization it is serialized as a zero-valued 64-bit + * unsigned integer, during deserialization it is replaced by an object + * representing the RPC interface of the remote peer. + * + * This is used in the core to return responses to the same client that made + * a request. + */ PeerPtr("PeerPtr"); companion object { private val values = values().associateBy(QuasselType::typeName) + /** + * Obtain a QtType by its standardized name + */ fun of(typeName: String?): QuasselType? = values[typeName] } }