From 48850d738966cc7a2d4fd8bd133972ffa8939bfc Mon Sep 17 00:00:00 2001 From: Him188 Date: Mon, 6 Jan 2020 21:29:57 +0800 Subject: [PATCH] Pass compilation --- .../qqandroid/event/PacketReceivedEvent.kt | 10 ++ .../network/QQAndroidBotNetworkHandler.kt | 92 +++++++++------ .../protocol/packet/OutgoingPacketAndroid.kt | 51 +++++---- .../network/protocol/packet/PacketFactory.kt | 108 +++++++++++++++++- .../protocol/packet/login/LoginPacket.kt | 33 +++--- .../net/mamoe/mirai/qqandroid/utils/ECDH.kt | 22 ---- .../handler/DataPacketSocketAdapter.kt | 2 + .../packet/login/PasswordSubmission.kt | 4 +- .../mamoe/mirai/utils/cryptor/ECDHAndroid.kt | 2 +- .../net.mamoe.mirai/utils/cryptor/ECDH.kt | 4 +- .../net/mamoe/mirai/utils/cryptor/ECDHJvm.kt | 2 +- 11 files changed, 217 insertions(+), 113 deletions(-) create mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/event/PacketReceivedEvent.kt delete mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/ECDH.kt diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/event/PacketReceivedEvent.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/event/PacketReceivedEvent.kt new file mode 100644 index 000000000..6230d509c --- /dev/null +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/event/PacketReceivedEvent.kt @@ -0,0 +1,10 @@ +package net.mamoe.mirai.qqandroid.event + +import net.mamoe.mirai.data.Packet +import net.mamoe.mirai.event.Cancellable +import net.mamoe.mirai.event.Event + +/** + * 接收到数据包 + */ +class PacketReceivedEvent(val packet: Packet) : Event(), Cancellable \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt index 45d6d7227..74a7770d6 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt @@ -1,40 +1,41 @@ package net.mamoe.mirai.qqandroid.network import kotlinx.coroutines.* -import kotlinx.io.core.* -import kotlinx.io.pool.useInstance +import kotlinx.io.core.use +import net.mamoe.mirai.data.Packet +import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.network.BotNetworkHandler import net.mamoe.mirai.qqandroid.QQAndroidBot +import net.mamoe.mirai.qqandroid.event.PacketReceivedEvent +import net.mamoe.mirai.qqandroid.network.protocol.packet.KnownPacketFactories +import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.qqandroid.network.protocol.packet.login.LoginPacket -import net.mamoe.mirai.utils.io.* +import net.mamoe.mirai.qqandroid.network.protocol.packet.login.PacketId +import net.mamoe.mirai.utils.LockFreeLinkedList +import net.mamoe.mirai.utils.MiraiInternalAPI +import net.mamoe.mirai.utils.io.ClosedChannelException +import net.mamoe.mirai.utils.io.PlatformDatagramChannel +import net.mamoe.mirai.utils.io.ReadPacketInternalException +import net.mamoe.mirai.utils.io.debugPrint import kotlin.coroutines.CoroutineContext +@UseExperimental(MiraiInternalAPI::class) internal class QQAndroidBotNetworkHandler(override val bot: QQAndroidBot) : BotNetworkHandler() { override val supervisor: CompletableJob = SupervisorJob(bot.coroutineContext[Job]) private val channel: PlatformDatagramChannel = PlatformDatagramChannel("wtlogin.qq.com", 8000) override suspend fun login() { - launch { processReceive() } + launch(CoroutineName("Incoming Packet Receiver")) { processReceive() } - val buffer = IoBuffer.Pool.borrow() - buffer.writePacket(LoginPacket(bot.client).delegate) - val shouldBeSent = buffer.readRemaining - check(channel.send(buffer) == shouldBeSent) { - "Buffer is not entirely sent. " + - "Required sent length=$shouldBeSent, but after channel.send, " + - "buffer remains ${buffer.readBytes().toUHexString()}" - } - buffer.release(IoBuffer.Pool) + LoginPacket(bot.client).sendAndExpect() println("Login sent") } - private suspend fun processReceive() { + private suspend inline fun processReceive() { while (channel.isOpen) { - val buffer = IoBuffer.Pool.borrow() - - try { - channel.read(buffer)// JVM: withContext(IO) + val rawInput = try { + channel.read() } catch (e: ClosedChannelException) { dispose() return @@ -46,35 +47,50 @@ internal class QQAndroidBotNetworkHandler(override val bot: QQAndroidBot) : BotN } catch (e: Throwable) { bot.logger.error("Caught unexpected exceptions", e) continue - } finally { - if (!buffer.canRead() || buffer.readRemaining == 0) {//size==0 - //bot.logger.debug("processReceive: Buffer cannot be read") - buffer.release(IoBuffer.Pool) - continue - }// sometimes exceptions are thrown without this `if` clause } - //buffer.resetForRead() - launch(CoroutineName("handleServerPacket")) { - // `.use`: Ensure that the packet is consumed **totally** - // so that all the buffers are released - ByteArrayPool.useInstance { - val length = buffer.readRemaining - 1 - buffer.readFully(it, 0, length) - buffer.resetForWrite() - buffer.writeFully(it, 0, length) + launch(CoroutineName("Incoming Packet handler")) { + try { + rawInput.debugPrint("Received") + } catch (e: Exception) { + bot.logger.error(e) } - ByteReadPacket(buffer, IoBuffer.Pool).use { input -> - try { - input.debugPrint("Received") - } catch (e: Exception) { - bot.logger.error(e) + } + + rawInput.use { + KnownPacketFactories.parseIncomingPacket(bot, rawInput) { packet: Packet, packetId: PacketId, sequenceId: Int -> + if (PacketReceivedEvent(packet).broadcast().cancelled) { + return + } + packetListeners.forEach { listener -> + if (listener.filter(packetId, sequenceId) && packetListeners.remove(listener)) { + listener.complete(packet) + } } } } } } + suspend fun OutgoingPacket.sendAndExpect(): E { + val handler = PacketListener(packetId = packetId, sequenceId = sequenceId) + packetListeners.addLast(handler) + check(channel.send(delegate)) { packetListeners.remove(handler); "Cannot send packet" } + @Suppress("UNCHECKED_CAST") + return handler.await() as E + } + + @PublishedApi + internal val packetListeners = LockFreeLinkedList() + + @PublishedApi + internal inner class PacketListener( + val packetId: PacketId, + val sequenceId: Int + ) : CompletableDeferred by CompletableDeferred(supervisor) { + fun filter(packetId: PacketId, sequenceId: Int) = this.packetId == packetId && this.sequenceId == sequenceId + } + override suspend fun awaitDisconnection() { while (true) { delay(100) diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt index 526be7187..f2857ff9a 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt @@ -7,9 +7,9 @@ import kotlinx.io.core.buildPacket import kotlinx.io.core.writeFully import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.qqandroid.network.protocol.packet.login.PacketId -import net.mamoe.mirai.qqandroid.utils.ECDH import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.cryptor.DecrypterByteArray +import net.mamoe.mirai.utils.cryptor.ECDH import net.mamoe.mirai.utils.cryptor.encryptAndWrite import net.mamoe.mirai.utils.cryptor.encryptBy import net.mamoe.mirai.utils.io.* @@ -21,7 +21,12 @@ import net.mamoe.mirai.utils.io.* internal class OutgoingPacket constructor( name: String?, val packetId: PacketId, - val sequenceId: Short, + val sequenceId: Int, + // TODO: 2020/1/6 这个 sequenceId 设计有问题. + // 02 03 包里面的那个应该并不是 sequenceId. + // 它应该是固定的 0x001. + // 应该在这里填入 SSO 的 sequenceId. + // 同时考虑修改名称. 这可能不应该叫做 SSO. 它在全程都有 val delegate: ByteReadPacket ) { val name: String by lazy { @@ -52,7 +57,7 @@ internal inline fun PacketFactory<*, *>.buildLoginOutgoingPacket( extraData: ByteArray = EMPTY_BYTE_ARRAY, name: String? = null, id: PacketId = this.id, - sequenceId: Short = PacketFactory.atomicNextSequenceId(), + sequenceId: Int = PacketFactory.atomicNextSequenceId(), body: BytePacketBuilder.() -> Unit ): OutgoingPacket = OutgoingPacket(name, id, sequenceId, buildPacket { writeIntLVPacket(lengthOffset = { it + 4 }) { @@ -69,11 +74,6 @@ internal inline fun PacketFactory<*, *>.buildLoginOutgoingPacket( } }) -internal class CommandId(val stringValue: String, val id: Int) { - override fun toString(): String = stringValue -} - - private val BRP_STUB = ByteReadPacket(EMPTY_BYTE_ARRAY) /** @@ -85,7 +85,7 @@ private val BRP_STUB = ByteReadPacket(EMPTY_BYTE_ARRAY) internal inline fun BytePacketBuilder.writeLoginSsoPacket( client: QQAndroidClient, subAppId: Long, - commandId: CommandId, + packetId: PacketId, extraData: ByteReadPacket = BRP_STUB, body: BytePacketBuilder.(ssoSequenceId: Int) -> Unit ) { @@ -102,21 +102,25 @@ internal inline fun BytePacketBuilder.writeLoginSsoPacket( writeInt((extraData.remaining + 4).toInt()) writePacket(extraData) } - writeInt(commandId.stringValue.length + 4) - writeStringUtf8(commandId.stringValue) + packetId.commandName.let { + writeInt(it.length + 4) + writeStringUtf8(it) + } writeInt(4 + 4) writeInt(45112203) // 02 B0 5B 8B - val imei = client.device.imei - writeInt(imei.length + 4) - writeStringUtf8(imei) + client.device.imei.let { + writeInt(it.length + 4) + writeStringUtf8(it) + } writeInt(4) - val ksid = client.device.ksid - writeShort((ksid.length + 2).toShort()) - writeStringUtf8(ksid) + client.device.ksid.let { + writeInt(it.length + 4) + writeStringUtf8(it) + } writeInt(4) } @@ -226,10 +230,10 @@ internal interface EncryptMethodECDH : EncryptMethod { override fun BytePacketBuilder.encryptAndWrite(body: ByteReadPacket) = ecdh.run { writeByte(1) // const writeByte(1) // const - writeFully(privateKey) + writeFully(keyPair.privateKey.getEncoded()) writeShort(258) // const - writeShortLVByteArray(publicKey) - body.encryptBy(shareKey) { encrypted -> writeFully(encrypted) } + writeShortLVByteArray(keyPair.publicKey.getEncoded().drop(23).toByteArray().also { check(it.size == 49) { "Bad publicKey generated. Expected size=49, got${it.size}" } }) + body.encryptBy(keyPair.shareKey) { encrypted -> writeFully(encrypted) } } } @@ -256,8 +260,7 @@ internal interface EncryptMethodECDH : EncryptMethod { internal inline fun BytePacketBuilder.writeRequestPacket( client: QQAndroidClient, encryptMethod: EncryptMethod, - commandId: CommandId, - sequenceId: Short = PacketFactory.atomicNextSequenceId(), + packetId: PacketId, bodyBlock: BytePacketBuilder.() -> Unit ) { val body = encryptMethod.run { @@ -268,8 +271,8 @@ internal inline fun BytePacketBuilder.writeRequestPacket( writeByte(0x02) // head writeShort((27 + 2 + body.remaining).toShort()) // orthodox algorithm writeShort(client.protocolVersion) - writeShort(sequenceId) - writeShort(commandId.id.toShort()) + writeShort(1) + writeShort(packetId.commandId.toShort()) writeQQ(client.account.id) writeByte(3) // originally const writeByte(encryptMethod.id.toByte()) diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt index 2414f42dd..e820725bd 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt @@ -3,13 +3,21 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet import kotlinx.atomicfu.AtomicInt import kotlinx.atomicfu.atomic import kotlinx.io.core.ByteReadPacket +import kotlinx.io.core.Closeable +import kotlinx.io.core.discardExact +import kotlinx.io.core.readBytes import net.mamoe.mirai.data.Packet -import net.mamoe.mirai.network.BotNetworkHandler +import net.mamoe.mirai.qqandroid.QQAndroidBot import net.mamoe.mirai.qqandroid.network.protocol.packet.login.NullPacketId +import net.mamoe.mirai.qqandroid.network.protocol.packet.login.NullPacketId.commandName import net.mamoe.mirai.qqandroid.network.protocol.packet.login.PacketId -import net.mamoe.mirai.utils.LockFreeLinkedList import net.mamoe.mirai.utils.cryptor.Decrypter import net.mamoe.mirai.utils.cryptor.DecrypterType +import net.mamoe.mirai.utils.cryptor.adjustToPublicKey +import net.mamoe.mirai.utils.cryptor.decryptBy +import net.mamoe.mirai.utils.io.* +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract /** * 一种数据包的处理工厂. 它可以解密解码服务器发来的这个包, 也可以编码加密要发送给服务器的这个包 @@ -32,21 +40,109 @@ internal abstract class PacketFactory Short.MAX_VALUE.toInt() * 2) { sequenceId.value = 0 return atomicNextSequenceId() } - return id.toShort() + // return id.toShort() } } } -internal class KnownPacketFactories : LockFreeLinkedList>() { +private val DECRYPTER_16_ZERO = ByteArray(16) + +internal typealias PacketConsumer = (packet: Packet, packetId: PacketId, ssoSequenceId: Int) -> Unit + +internal object KnownPacketFactories : List> by mutableListOf() { + + fun findPacketFactory(commandName: String): PacketFactory<*, *> = this.first { it.id.commandName == commandName } + + fun findPacketFactory(commandId: Int): PacketFactory<*, *> = this.first { it.id.commandName == commandName } + + suspend inline fun parseIncomingPacket(bot: QQAndroidBot, rawInput: ByteReadPacket, consumer: PacketConsumer) = + rawInput.debugPrintIfFail("Incoming packet") { + require(rawInput.remaining < Int.MAX_VALUE) { "rawInput is too long" } + val expectedLength = readInt() - 4 + check(rawInput.remaining.toInt() == expectedLength) { "Invalid packet length. Expected $expectedLength, got ${rawInput.remaining} Probably packets merged? " } + // login + when (val flag1 = readInt()) { + 0x0A -> when (val flag2 = readByte().toInt()) { + 0x02 -> { + val flag3 = readByte().toInt() + check(flag3 == 0) { "Illegal flag3. Expected 0, got $flag3" } + + discardExact(readInt() - 4) // uinAccount + + parseLoginSsoPacket(bot, decryptBy(DECRYPTER_16_ZERO), consumer) + } + else -> error("Illegal flag2. Expected 0x02, got $flag2") + } + else -> error("Illegal flag1. Expected 0x0A or 0x0B, got $flag1") + } + } + + @UseExperimental(ExperimentalUnsignedTypes::class) + private suspend inline fun parseLoginSsoPacket(bot: QQAndroidBot, rawInput: ByteReadPacket, consumer: PacketConsumer) = + rawInput.debugPrintIfFail("Login sso packet") { + val commandName: String + val ssoSequenceId: Int + readIoBuffer(readInt() - 4).withUse { + ssoSequenceId = readInt() + check(readInt() == 0) + val loginExtraData = readIoBuffer(readInt() - 4) + + commandName = readString(readInt() - 4) + val unknown = readBytes(readInt() - 4) + if (unknown.toInt() != 0x02B05B8B) DebugLogger.debug("got new unknown: $unknown") + + check(readInt() == 0) + } + + val packetFactory = findPacketFactory(commandName) + + val qq: Long + val subCommandId: Int + readIoBuffer(readInt() - 4).withUse { + check(readByte().toInt() == 2) + discardExact(2) // 27 + 2 + body.size + discardExact(2) // const, =8001 + readShort() // commandId + readShort() // innerSequenceId + qq = readInt().toLong() + + discardExact(1) // const = 0 + val packet = when (val encryptionMethod = readByte().toInt()) { + 4 -> { // peer public key, ECDH + packetFactory.run { + bot.client.ecdh.calculateShareKeyByPeerPublicKey(readUShortLVByteArray().adjustToPublicKey()).read { + decode(bot) + } + } + } + else -> error("Illegal encryption method. expected 4, got $encryptionMethod") + } + + consumer(packet, packetFactory.id, ssoSequenceId) + } + } +} + +@UseExperimental(ExperimentalContracts::class) +internal inline fun I.withUse(block: I.() -> R): R { + contract { + callsInPlace(block, kotlin.contracts.InvocationKind.EXACTLY_ONCE) + } + return try { + block(this) + } finally { + close() + } } \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt index 76adf2c98..1af4409bf 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt @@ -5,7 +5,7 @@ import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.buildPacket import kotlinx.io.core.readBytes import net.mamoe.mirai.data.Packet -import net.mamoe.mirai.network.BotNetworkHandler +import net.mamoe.mirai.qqandroid.QQAndroidBot import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.qqandroid.network.protocol.packet.* import net.mamoe.mirai.qqandroid.utils.GuidSource @@ -21,7 +21,7 @@ class LoginPacketDecrypter(override val value: ByteArray) : DecrypterByteArray { internal object LoginPacket : PacketFactory(LoginPacketDecrypter) { init { - this._id = PacketId(CommandId("wtlogin.login", 0x0810), 9) + this._id = PacketId(commandId = 0x0810, commandName = "wtlogin.login", subCommandId = 9) } operator fun invoke( @@ -30,8 +30,8 @@ internal object LoginPacket : PacketFactory - writeRequestPacket(client, EncryptMethodECDH135(client.ecdh), _id.commandId) { + writeLoginSsoPacket(client, subAppId, id) { ssoSequenceId -> + writeRequestPacket(client, EncryptMethodECDH135(client.ecdh), id) { writeShort(9) // subCommand writeShort(LoginType.PASSWORD.value.toShort()) @@ -163,30 +163,27 @@ internal object LoginPacket : PacketFactory= 16) { @@ -263,7 +263,7 @@ internal object SubmitPasswordPacket : PacketFactory