Fix packet parsing

This commit is contained in:
Him188 2020-01-11 15:30:19 +08:00
parent 1e6a41ca7c
commit d7f67e5159
16 changed files with 244 additions and 73 deletions

View File

@ -1,6 +1,7 @@
package net.mamoe.mirai.qqandroid.network
import kotlinx.coroutines.*
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.use
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.broadcast
@ -36,6 +37,27 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
LoginPacket.SubCommand9(bot.client).sendAndExpect<LoginPacket.LoginPacketResponse>()
}
internal fun launchPacketProcessor(rawInput: ByteReadPacket): Job {
return launch(CoroutineName("Incoming Packet handler")) {
rawInput.debugPrint("Received").use { input ->
if (input.remaining == 0L) {
bot.logger.error("Empty packet received. Consider if bad packet was sent.")
return@launch
}
KnownPacketFactories.parseIncomingPacket(bot, input) { packet: Packet, packetId: PacketId, sequenceId: Int ->
if (PacketReceivedEvent(packet).broadcast().cancelled) {
return@parseIncomingPacket
}
packetListeners.forEach { listener ->
if (listener.filter(packetId, sequenceId) && packetListeners.remove(listener)) {
listener.complete(packet)
}
}
}
}
}
}
private suspend fun processReceive() {
while (channel.isOpen) {
val rawInput = try {
@ -52,25 +74,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
bot.logger.error("Caught unexpected exceptions", e)
continue
}
launch(CoroutineName("Incoming Packet handler")) {
rawInput.debugPrint("Received").use { input ->
if (input.remaining == 0L) {
bot.logger.error("Empty packet received. Consider if bad packet was sent.")
return@launch
}
KnownPacketFactories.parseIncomingPacket(bot, input) { packet: Packet, packetId: PacketId, sequenceId: Int ->
if (PacketReceivedEvent(packet).broadcast().cancelled) {
return@parseIncomingPacket
}
packetListeners.forEach { listener ->
if (listener.filter(packetId, sequenceId) && packetListeners.remove(listener)) {
listener.complete(packet)
}
}
}
}
}
launchPacketProcessor(rawInput)
}
}
@ -96,9 +100,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
fun filter(packetId: PacketId, sequenceId: Int) = this.packetId == packetId && this.sequenceId == sequenceId
}
override suspend fun awaitDisconnection() {
supervisor.join()
}
override suspend fun awaitDisconnection() = supervisor.join()
override fun dispose(cause: Throwable?) {
println("Closed")

View File

@ -6,6 +6,7 @@ import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.toByteArray
import net.mamoe.mirai.BotAccount
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.protocol.packet.Tlv
import net.mamoe.mirai.qqandroid.utils.Context
import net.mamoe.mirai.qqandroid.utils.DeviceInfo
import net.mamoe.mirai.qqandroid.utils.NetworkType
@ -13,6 +14,7 @@ import net.mamoe.mirai.qqandroid.utils.SystemDeviceInfo
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.cryptor.ECDH
import net.mamoe.mirai.utils.cryptor.contentToString
import net.mamoe.mirai.utils.io.hexToBytes
import net.mamoe.mirai.utils.unsafeWeakRef
@ -40,6 +42,11 @@ internal open class QQAndroidClient(
val device: DeviceInfo = SystemDeviceInfo(context),
bot: QQAndroidBot
) {
@UseExperimental(MiraiInternalAPI::class)
override fun toString(): String { // net.mamoe.mirai.utils.cryptor.ProtoKt.contentToString
return "QQAndroidClient(account=$account, ecdh=$ecdh, device=$device, tgtgtKey=${tgtgtKey.contentToString()}, randomKey=${randomKey.contentToString()}, miscBitMap=$miscBitMap, mainSigMap=$mainSigMap, subSigMap=$subSigMap, _ssoSequenceId=$_ssoSequenceId, openAppId=$openAppId, apkVersionName=${apkVersionName.contentToString()}, loginState=$loginState, appClientVersion=$appClientVersion, networkType=$networkType, apkSignatureMd5=${apkSignatureMd5.contentToString()}, protocolVersion=$protocolVersion, apkId=${apkId.contentToString()}, t150=${t150?.contentToString()}, rollbackSig=${rollbackSig?.contentToString()}, ipFromT149=${ipFromT149?.contentToString()}, timeDifference=$timeDifference, uin=$uin, t530=${t530?.contentToString()}, t528=${t528?.contentToString()}, ksid='$ksid', pwdFlag=$pwdFlag, loginExtraData=$loginExtraData, wFastLoginInfo=$wFastLoginInfo, reserveUinInfo=$reserveUinInfo, wLoginSigInfo=$wLoginSigInfo, tlv113=${tlv113?.contentToString()}, qrPushSig=${qrPushSig.contentToString()}, mainDisplayName='$mainDisplayName')"
}
val context by context.unsafeWeakRef()
val bot: QQAndroidBot by bot.unsafeWeakRef()
@ -81,7 +88,7 @@ internal open class QQAndroidClient(
*/
var t150: ByteArray? = null
var t150: Tlv? = null
var rollbackSig: ByteArray? = null
var ipFromT149: ByteArray? = null
/**
@ -100,7 +107,7 @@ internal open class QQAndroidClient(
/**
* t108 时更新
*/
var ksid: String = "|454001228437590|A8.2.0.27f6ea96"
var ksid: ByteArray = "|454001228437590|A8.2.0.27f6ea96".toByteArray()
/**
* t186
*/
@ -110,19 +117,23 @@ internal open class QQAndroidClient(
*/
var loginExtraData: LoginExtraData? = null
lateinit var wFastLoginInfo: WFastLoginInfo
lateinit var reserveUinInfo: ReserveUinInfo
var reserveUinInfo: ReserveUinInfo? = null
var wLoginSigInfo: WLoginSigInfo? = null
var tlv113: ByteArray? = null
lateinit var qrPushSig: ByteArray
lateinit var mainDisplayName: String
lateinit var mainDisplayName: ByteArray
}
class ReserveUinInfo(
val imgType: ByteArray,
val imgFormat: ByteArray,
val imgUrl: ByteArray
)
) {
override fun toString(): String {
return "ReserveUinInfo(imgType=${imgType.contentToString()}, imgFormat=${imgFormat.contentToString()}, imgUrl=${imgUrl.contentToString()})"
}
}
class WFastLoginInfo(
val outA1: ByteReadPacket,
@ -130,7 +141,11 @@ class WFastLoginInfo(
var iconUrl: String = "",
var profileUrl: String = "",
var userJson: String = ""
)
) {
override fun toString(): String {
return "WFastLoginInfo(outA1=$outA1, adUrl='$adUrl', iconUrl='$iconUrl', profileUrl='$profileUrl', userJson='$userJson')"
}
}
class WLoginSimpleInfo(
val uin: Long, // uin
@ -142,14 +157,22 @@ class WLoginSimpleInfo(
val imgFormat: ByteArray,
val imgUrl: ByteArray,
val mainDisplayName: ByteArray
)
) {
override fun toString(): String {
return "WLoginSimpleInfo(uin=$uin, face=$face, age=$age, gender=$gender, nick='$nick', imgType=${imgType.contentToString()}, imgFormat=${imgFormat.contentToString()}, imgUrl=${imgUrl.contentToString()}, mainDisplayName=${mainDisplayName.contentToString()})"
}
}
class LoginExtraData(
val uin: Long,
val ip: ByteArray,
val time: Int,
val version: Int
)
) {
override fun toString(): String {
return "LoginExtraData(uin=$uin, ip=${ip.contentToString()}, time=$time, version=$version)"
}
}
class WLoginSigInfo(
val uin: Long,
@ -193,7 +216,11 @@ class WLoginSigInfo(
val wtSessionTicket: WtSessionTicket,
val wtSessionTicketKey: ByteArray,
val deviceToken: ByteArray
)
) {
override fun toString(): String {
return "WLoginSigInfo(uin=$uin, encryptA1=${encryptA1.contentToString()}, noPicSig=${noPicSig.contentToString()}, G=${G.contentToString()}, dpwd=${dpwd.contentToString()}, randSeed=${randSeed.contentToString()}, simpleInfo=$simpleInfo, appPri=$appPri, a2ExpiryTime=$a2ExpiryTime, loginBitmap=$loginBitmap, tgt=${tgt.contentToString()}, a2CreationTime=$a2CreationTime, tgtKey=${tgtKey.contentToString()}, userStSig=$userStSig, userStKey=${userStKey.contentToString()}, userStWebSig=$userStWebSig, userA5=$userA5, userA8=$userA8, lsKey=$lsKey, sKey=$sKey, userSig64=$userSig64, openId=${openId.contentToString()}, openKey=$openKey, vKey=$vKey, accessToken=$accessToken, d2=$d2, d2Key=${d2Key.contentToString()}, sid=$sid, aqSig=$aqSig, psKey=$psKey, superKey=${superKey.contentToString()}, payToken=${payToken.contentToString()}, pf=${pf.contentToString()}, pfKey=${pfKey.contentToString()}, da2=${da2.contentToString()}, wtSessionTicket=$wtSessionTicket, wtSessionTicketKey=${wtSessionTicketKey.contentToString()}, deviceToken=${deviceToken.contentToString()})"
}
}
class UserStSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class LSKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)

View File

@ -144,8 +144,8 @@ private inline fun BytePacketBuilder.writeLoginSsoPacket(
writeInt(4)
client.ksid.let {
writeShort((it.length + 2).toShort())
writeStringUtf8(it)
writeShort((it.size + 2).toShort())
writeFully(it)
}
writeInt(4)

View File

@ -48,6 +48,7 @@ private val DECRYPTER_16_ZERO = ByteArray(16)
internal typealias PacketConsumer = suspend (packet: Packet, packetId: PacketId, ssoSequenceId: Int) -> Unit
@UseExperimental(ExperimentalUnsignedTypes::class)
internal object KnownPacketFactories : List<PacketFactory<*, *>> by mutableListOf(
LoginPacket
) {
@ -60,7 +61,11 @@ internal object KnownPacketFactories : List<PacketFactory<*, *>> by mutableListO
suspend fun parseIncomingPacket(bot: QQAndroidBot, rawInput: ByteReadPacket, consumer: PacketConsumer) =
rawInput.debugPrintIfFail("Incoming packet") {
require(remaining < Int.MAX_VALUE) { "rawInput is too long" }
val expectedLength = readInt() - 4
val expectedLength = readUInt().toInt() - 4
if (expectedLength > 16e7) {
bot.logger.warning("Detect incomplete packet, ignoring.")
return@debugPrintIfFail
}
check(remaining.toInt() == expectedLength) { "Invalid packet length. Expected $expectedLength, got ${rawInput.remaining} Probably packets merged? " }
// login
when (val flag1 = readInt()) {

View File

@ -10,6 +10,11 @@ import net.mamoe.mirai.utils.io.*
import net.mamoe.mirai.utils.md5
import kotlin.random.Random
/**
* 显式表示一个 [ByteArray] 是一个 tlv body
*/
inline class Tlv(val value: ByteArray)
inline class LoginType(
val value: Int
) {

View File

@ -3,6 +3,7 @@ package net.mamoe.mirai.qqandroid.utils
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoBuf
import net.mamoe.mirai.utils.cryptor.contentToString
import net.mamoe.mirai.utils.unsafeWeakRef
abstract class DeviceInfo(
@ -75,9 +76,15 @@ abstract class DeviceInfo(
)
}
override fun toString(): String { // net.mamoe.mirai.utils.cryptor.ProtoKt.contentToString
return "DeviceInfo(display=${display.contentToString()}, product=${product.contentToString()}, device=${device.contentToString()}, board=${board.contentToString()}, brand=${brand.contentToString()}, model=${model.contentToString()}, bootloader=${bootloader.contentToString()}, fingerprint=${fingerprint.contentToString()}, bootId=${bootId.contentToString()}, procVersion=${procVersion.contentToString()}, baseBand=${baseBand.contentToString()}, version=$version, simInfo=${simInfo.contentToString()}, osType=${osType.contentToString()}, macAddress=${macAddress.contentToString()}, wifiBSSID=${wifiBSSID?.contentToString()}, wifiSSID=${wifiSSID?.contentToString()}, imsiMd5=${imsiMd5.contentToString()}, imei='$imei', ipAddress='$ipAddress', androidId=${androidId.contentToString()}, apn=${apn.contentToString()})"
}
interface Version {
val incremental: ByteArray
val release: ByteArray
val codename: ByteArray
}
}

View File

@ -45,4 +45,8 @@ actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray {
return calculateShareKey(keyPair.privateKey, peerPublicKey)
}
actual override fun toString(): String {
return "ECDH(keyPair=$keyPair)"
}
}

View File

@ -0,0 +1,18 @@
package net.mamoe.mirai.utils.cryptor
actual fun Any.contentToStringReflectively(prefix: String): String {
val newPrefix = prefix + ProtoMap.indent
return (this::class.simpleName ?: "<UnnamedClass>") + "#" + this::class.hashCode() + " {\n" +
this::class.java.fields.toMutableSet().apply { addAll(this::class.java.declaredFields) }.asSequence().filterNot { it.name.contains("$") || it.name == "Companion" || it.isSynthetic }
.joinToStringPrefixed(
prefix = newPrefix
) {
it.isAccessible = true
it.name + "=" + kotlin.runCatching {
val value = it.get(this)
if (value == this) "<this>"
else value.contentToString(newPrefix)
}.getOrElse { "<!>" }
} + "\n$prefix}"
}

View File

@ -93,7 +93,9 @@ abstract class Bot : CoroutineScope {
abstract val network: BotNetworkHandler
/**
* 登录
* 登录.
*
* 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.login]
*
* @throws LoginFailedException
*/

View File

@ -28,6 +28,8 @@ expect class ECDH(keyPair: ECDHKeyPair) {
fun generateKeyPair(): ECDHKeyPair
fun calculateShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray
}
override fun toString(): String
}
@Suppress("FunctionName")

View File

@ -96,7 +96,7 @@ fun protoFieldNumber(number: UInt): Int = number.toInt().ushr(3)
class ProtoMap(map: MutableMap<ProtoFieldId, Any>) : MutableMap<ProtoFieldId, Any> by map {
companion object {
@JvmStatic
val indent: String = " "
internal val indent: String = " "
}
override fun toString(): String {
@ -117,7 +117,12 @@ class ProtoMap(map: MutableMap<ProtoFieldId, Any>) : MutableMap<ProtoFieldId, An
}*/
}
internal fun Any.contentToString(prefix: String = ""): String = when (this) {
fun <T> Sequence<T>.joinToStringPrefixed(prefix: String, transform: (T) -> CharSequence): String {
return this.joinToString(prefix = "$prefix${ProtoMap.indent}", separator = "\n$prefix${ProtoMap.indent}", transform = transform)
}
fun Any?.contentToString(prefix: String = ""): String = when (this) {
is Unit -> "Unit"
is UInt -> "0x" + this.toUHexString("") + "($this)"
is UByte -> "0x" + this.toUHexString() + "($this)"
is UShort -> "0x" + this.toUHexString("") + "($this)"
@ -131,12 +136,38 @@ internal fun Any.contentToString(prefix: String = ""): String = when (this) {
is Boolean -> if (this) "true" else "false"
is ByteArray -> this.toUHexString()// + " (${this.encodeToString()})"
is ByteArray -> {
if (this.size == 0) "<Empty ByteArray>"
else this.toUHexString()// + " (${this.encodeToString()})"
}
is UByteArray -> {
if (this.size == 0) "<Empty UByteArray>"
else this.toUHexString()// + " (${this.encodeToString()})"
}
is ProtoMap -> "ProtoMap(size=$size){\n" + this.toStringPrefixed("$prefix${ProtoMap.indent}${ProtoMap.indent}") + "\n$prefix${ProtoMap.indent}}"
else -> this.toString()
is Collection<*> -> this.joinToString(prefix = "[", postfix = "]") { it.contentToString() }
is Map<*, *> -> this.entries.joinToString(prefix = "{", postfix = "}") { it.key.contentToString() + "=" + it.value.contentToString() }
else -> {
if (this == null) "null"
else if (this::class.isData) this.toString()
else {
if (this::class.qualifiedName?.startsWith("net.mamoe.mirai.") == true) {
this.contentToStringReflectively(prefix + ProtoMap.indent)
} else this.toString()
/*
(this::class.simpleName ?: "<UnnamedClass>") + "#" + this::class.hashCode() + "{\n" +
this::class.members.asSequence().filterIsInstance<KProperty<*>>().filter { !it.isSuspend && it.visibility == KVisibility.PUBLIC }
.joinToStringPrefixed(
prefix = ProtoMap.indent
) { it.name + "=" + kotlin.runCatching { it.call(it).contentToString(ProtoMap.indent) }.getOrElse { "<!>" } }
*/
}
}
}
expect fun Any.contentToStringReflectively(prefix: String = ""): String
fun ByteReadPacket.readProtoMap(length: Long = this.remaining): ProtoMap {
val map = ProtoMap(mutableMapOf())

View File

@ -9,6 +9,8 @@ import net.mamoe.mirai.contact.GroupId
import net.mamoe.mirai.contact.GroupInternalId
import net.mamoe.mirai.contact.groupId
import net.mamoe.mirai.contact.groupInternalId
import net.mamoe.mirai.utils.assertUnreachable
import net.mamoe.mirai.utils.cryptor.contentToString
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
@ -76,14 +78,14 @@ fun Input.readTLVMap(tagSize: Int = 2): MutableMap<Int, ByteArray> = readTLVMap(
@Suppress("DuplicatedCode")
fun Input.readTLVMap(expectingEOF: Boolean = true, tagSize: Int): MutableMap<Int, ByteArray> {
val map = mutableMapOf<Int, ByteArray>()
var type: Int = 0
var key = 0
while (inline {
try {
type = when (tagSize) {
1 -> readByte().toInt()
2 -> readShort().toInt()
4 -> readInt()
key = when (tagSize) {
1 -> readUByte().toInt()
2 -> readUShort().toInt()
4 -> readUInt().toInt()
else -> error("Unsupported tag size: $tagSize")
}
} catch (e: Exception) { // java.nio.BufferUnderflowException is not a EOFException...
@ -92,22 +94,33 @@ fun Input.readTLVMap(expectingEOF: Boolean = true, tagSize: Int): MutableMap<Int
}
throw e
}
type
key
}.toUByte() != UByte.MAX_VALUE) {
check(!map.containsKey(type)) {
"Count not readTLVMap: duplicated key 0x${type.toUInt().toUHexString("")}. " +
"map=$map" +
", duplicating value=${this.readUShortLVByteArray().toUHexString()}" +
", remaining=" + if (expectingEOF) this.readBytes().toUHexString() else "[Not expecting EOF]"
}
try {
map[type] = this.readUShortLVByteArray()
} catch (e: Exception) { // BufferUnderflowException, java.io.EOFException
if (expectingEOF) {
return map
if (map.containsKey(key)) {
DebugLogger.error(
@Suppress("IMPLICIT_CAST_TO_ANY")
"""
Error readTLVMap:
duplicated key ${when (tagSize) {
1 -> key.toByte()
2 -> key.toShort()
4 -> key
else -> assertUnreachable()
}.contentToString()}
map=${map.contentToString()}
duplicating value=${this.readUShortLVByteArray().toUHexString()}
""".trimIndent()
)
} else {
try {
map[key] = this.readUShortLVByteArray()
} catch (e: Exception) { // BufferUnderflowException, java.io.EOFException
// if (expectingEOF) {
// return map
// }
throw e
}
throw e
}
}
return map

View File

@ -51,13 +51,13 @@ fun UShort.toByteArray(): ByteArray = with(toUInt()) {
fun Short.toUHexString(separator: String = " "): String = this.toUShort().toUHexString(separator)
fun UShort.toUHexString(separator: String = " "): String =
(this.toInt().shr(8).toUShort() and 255u).toByte().toUHexString() + separator + (this and 255u).toByte().toUHexString()
this.toInt().shr(8).toUShort().toUByte().toUHexString() + separator + this.toUByte().toUHexString()
fun ULong.toUHexString(separator: String = " "): String =
this.toLong().toUHexString(separator)
fun Long.toUHexString(separator: String = " "): String =
this.ushr(32).toUInt().toUHexString(separator) + separator + this.ushr(32).toUInt().toUHexString(separator)
this.ushr(32).toUInt().toUHexString(separator) + separator + this.toUInt().toUHexString(separator)
/**
* 255 -> 00 FF

View File

@ -51,4 +51,8 @@ actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray {
return calculateShareKey(keyPair.privateKey, peerPublicKey)
}
actual override fun toString(): String {
return "ECDH(keyPair=$keyPair)"
}
}

View File

@ -0,0 +1,31 @@
package net.mamoe.mirai.utils.cryptor
import java.lang.reflect.Field
import kotlin.reflect.full.allSuperclasses
actual fun Any.contentToStringReflectively(prefix: String): String {
val newPrefix = prefix + ProtoMap.indent
return (this::class.simpleName ?: "<UnnamedClass>") + "#" + this::class.hashCode() + " {\n" +
this.allFieldsFromSuperClassesMatching { it.packageName.startsWith("net.mamoe.mirai") }
.distinctBy { it.name }
.filterNot { it.name.contains("$") || it.name == "Companion" || it.isSynthetic || it.name == "serialVersionUID" }
.joinToStringPrefixed(
prefix = newPrefix
) {
it.trySetAccessible()
it.name + "=" + kotlin.runCatching {
val value = it.get(this)
if (value == this) "<this>"
else value.contentToString(newPrefix)
}.getOrElse { "<!>" }
} + "\n$prefix}"
}
internal fun Any.allFieldsFromSuperClassesMatching(classFilter: (Class<out Any>) -> Boolean): Sequence<Field> {
return (this::class.java.takeIf(classFilter)?.declaredFields?.asSequence() ?: sequenceOf<Field>()) + this::class.allSuperclasses
.asSequence()
.map { it.java }
.filter(classFilter)
.flatMap { it.declaredFields.asSequence() }
}