From 7bb788a2cf4c39c6bf6bdff0b473d0c1e0b636e5 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sat, 2 Apr 2022 13:16:01 +0100 Subject: [PATCH] Relogin when receiving returnCode <= -10000, fix #1961 --- .../kotlin/network/components/PacketCodec.kt | 100 +++++++++++++----- .../handler/selector/NetworkException.kt | 12 ++- .../network/impl/netty/NettyNetworkHandler.kt | 40 +++++-- 3 files changed, 113 insertions(+), 39 deletions(-) diff --git a/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt b/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt index 50e39cc06..bb1316823 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt @@ -1,10 +1,10 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 Mamoe Technologies and contributors. * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. * - * https://github.com/mamoe/mirai/blob/master/LICENSE + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ package net.mamoe.mirai.internal.network.components @@ -14,6 +14,8 @@ import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.network.QQAndroidClient import net.mamoe.mirai.internal.network.component.ComponentKey import net.mamoe.mirai.internal.network.components.PacketCodec.Companion.PacketLogger +import net.mamoe.mirai.internal.network.components.PacketCodecException.Kind.* +import net.mamoe.mirai.internal.network.handler.selector.NetworkException import net.mamoe.mirai.internal.network.protocol.packet.* import net.mamoe.mirai.internal.utils.crypto.TEA import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey @@ -28,10 +30,14 @@ import kotlin.io.use */ internal interface PacketCodec { /** - * It's caller's responsibility to close [input] + * It's caller's responsibility to close [input]. + * + * @throws PacketCodecException normal, known errors + * @throws Exception unexpected errors * @param input received from sockets. * @return decoded */ + @Throws(PacketCodecException::class) fun decodeRaw(client: SsoSession, input: ByteReadPacket): RawIncomingPacket /** @@ -50,17 +56,39 @@ internal interface PacketCodec { } } +/** + * Wraps an exception thrown by [PacketCodec.decodeRaw], which is not a [PacketCodecException] (meaning unexpected). + */ internal data class ExceptionInPacketCodecException( override val cause: Throwable, ) : IllegalStateException("Exception in PacketCodec.", cause) -internal class OicqDecodingException( - val targetException: Throwable -) : RuntimeException( - null, targetException, - true, // enableSuppression - false, // writableStackTrace -) { +/** + * Thrown by [PacketCodec.decodeRaw], representing an excepted error. + */ +internal class PacketCodecException( + val targetException: Throwable, + val kind: Kind, +) : NetworkException(recoverable = true, cause = targetException) { + constructor(message: String, kind: Kind) : this(IllegalStateException(message), kind) + + enum class Kind { + /** + * 会触发重连 + */ + SESSION_EXPIRED, + + /** + * 只记录日志 + */ + PROTOCOL_UPDATED, + + /** + * 只记录日志 + */ + OTHER, + } + override fun getStackTrace(): Array { return targetException.stackTrace } @@ -76,9 +104,12 @@ internal class PacketCodecImpl : PacketCodec { val flag2 = readByte().toInt() val flag3 = readByte().toInt() - check(flag3 == 0) { - "Illegal flag3. Expected 0, whereas got $flag3. flag1=$flag1, flag2=$flag2. " + - "Remaining=${this.readBytes().toUHexString()}" + if (flag3 != 0) { + throw PacketCodecException( + "Illegal flag3. Expected 0, whereas got $flag3. flag1=$flag1, flag2=$flag2. " + + "Remaining=${this.readBytes().toUHexString()}", + kind = PROTOCOL_UPDATED + ) } readString(readInt() - 4)// uinAccount @@ -90,12 +121,15 @@ internal class PacketCodecImpl : PacketCodec { 2 -> TEA.decrypt(buffer, DECRYPTER_16_ZERO, size) 1 -> TEA.decrypt(buffer, client.wLoginSigInfo.d2Key, size) 0 -> buffer - else -> error("Unknown flag2=$flag2") + else -> throw PacketCodecException("Unknown flag2=$flag2", PROTOCOL_UPDATED) }.let { decryptedData -> when (flag1) { 0x0A -> parseSsoFrame(client, decryptedData) 0x0B -> parseSsoFrame(client, decryptedData) // 这里可能是 uni?? 但测试时候发现结构跟 sso 一样. - else -> error("unknown flag1: ${flag1.toByte().toUHexString()}") + else -> throw PacketCodecException( + "unknown flag1: ${flag1.toByte().toUHexString()}", + PROTOCOL_UPDATED + ) } }.let { raw -> when (flag2) { @@ -107,11 +141,11 @@ internal class PacketCodecImpl : PacketCodec { try { parseOicqResponse(client) } catch (e: Throwable) { - throw OicqDecodingException(e) + throw PacketCodecException(e, PacketCodecException.Kind.OTHER) } } ) - else -> error("Unknown flag2=$flag2") + else -> error("unreachable") } } } @@ -136,11 +170,27 @@ internal class PacketCodecImpl : PacketCodec { PacketLogger.verbose { "sequenceId = $ssoSequenceId" } val returnCode = readInt() - check(returnCode == 0) { + if (returnCode != 0) { if (returnCode <= -10000) { - // https://github.com/mamoe/mirai/issues/470 - error("returnCode = $returnCode") - } else "returnCode = $returnCode" + // #470: -10008, 例如在手机QQ强制下线机器人 + // #1957: -10106, 未知原因, 但会导致收不到消息 + + throw PacketCodecException( + "Received packet returnCode = $returnCode, which may mean session expired.", + SESSION_EXPIRED + ) + + // 备注: 之后该异常将会导致 NetworkHandler close, 然后由 selector 触发重连. + // 重连时会在 net.mamoe.mirai.internal.network.components.SsoProcessorImpl.login 进行 FastLogin. + // 不确定在这种情况下执行 FastLogin 是否正确. 若有问题, 考虑强制执行 SlowLogin (by invalidating session). + } else { + throw PacketCodecException( + "Received unknown packet returnCode = $returnCode, ignoring. Please report to https://github.com/mamoe/mirai/issues/new/choose if you see anything abnormal", + OTHER + ) + + // 备注: OTHER 不会触发重连, 只会记录日志. + } } if (PacketLogger.isEnabled) { @@ -182,7 +232,7 @@ internal class PacketCodecImpl : PacketCodec { } } 8 -> input - else -> error("unknown dataCompressed flag: $dataCompressed") + else -> throw PacketCodecException("Unknown dataCompressed flag: $dataCompressed", PROTOCOL_UPDATED) } // body @@ -221,7 +271,7 @@ internal class PacketCodecImpl : PacketCodec { TEA.decrypt(data, peerShareKey) } 3 -> { - val size = (this.remaining - 1).toInt(); + val size = (this.remaining - 1).toInt() // session TEA.decrypt( this.readBytes(), diff --git a/mirai-core/src/commonMain/kotlin/network/handler/selector/NetworkException.kt b/mirai-core/src/commonMain/kotlin/network/handler/selector/NetworkException.kt index f28bfe846..0b07d444f 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/selector/NetworkException.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/selector/NetworkException.kt @@ -1,10 +1,10 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 Mamoe Technologies and contributors. * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. * - * https://github.com/mamoe/mirai/blob/master/LICENSE + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ package net.mamoe.mirai.internal.network.handler.selector @@ -19,6 +19,10 @@ internal abstract class NetworkException : Exception { this.recoverable = recoverable } + constructor(recoverable: Boolean, cause: Throwable?) : super(cause) { + this.recoverable = recoverable + } + constructor(message: String, recoverable: Boolean) : super(message) { this.recoverable = recoverable } diff --git a/mirai-core/src/commonMain/kotlin/network/impl/netty/NettyNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/network/impl/netty/NettyNetworkHandler.kt index 87cac335e..050e84e2a 100644 --- a/mirai-core/src/commonMain/kotlin/network/impl/netty/NettyNetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/impl/netty/NettyNetworkHandler.kt @@ -1,10 +1,10 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 Mamoe Technologies and contributors. * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. * - * https://github.com/mamoe/mirai/blob/master/LICENSE + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ package net.mamoe.mirai.internal.network.impl.netty @@ -22,6 +22,8 @@ import net.mamoe.mirai.internal.network.components.* import net.mamoe.mirai.internal.network.handler.NetworkHandler.State import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext import net.mamoe.mirai.internal.network.handler.NetworkHandlerSupport +import net.mamoe.mirai.internal.network.handler.selector.NetworkException +import net.mamoe.mirai.internal.network.handler.selector.NetworkHandlerSelector import net.mamoe.mirai.internal.network.handler.state.StateObserver import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.utils.* @@ -52,14 +54,27 @@ internal open class NettyNetworkHandler( // exception handling /////////////////////////////////////////////////////////////////////////// protected open fun handleExceptionInDecoding(error: Throwable) { - if (error is OicqDecodingException) { - if (error.targetException is EOFException) return + fun passToExceptionHandler() { + // Typically, just log the exception + coroutineContext[CoroutineExceptionHandler]!!.handleException( + coroutineContext, + ExceptionInPacketCodecException(error.unwrap()) + ) } - coroutineContext[CoroutineExceptionHandler]!!.handleException( - coroutineContext, - ExceptionInPacketCodecException(error.unwrap()) - ) + if (error is PacketCodecException) { + if (error.targetException is EOFException) return + when (error.kind) { + PacketCodecException.Kind.SESSION_EXPIRED -> { + setState { StateClosed(error) } + return + } + PacketCodecException.Kind.PROTOCOL_UPDATED -> passToExceptionHandler() + PacketCodecException.Kind.OTHER -> passToExceptionHandler() + } + } + + passToExceptionHandler() } protected open fun handlePipelineException(ctx: ChannelHandlerContext, error: Throwable) { @@ -370,6 +385,11 @@ internal open class NettyNetworkHandler( override fun toString(): String = "StateOK" } + /** + * 这会永久关闭这个 [NettyNetworkHandler], 但通常 bot 会使用 [NetworkHandlerSelector], selector 会创建新的 [NettyNetworkHandler] 来恢复连接. + * + * 备注: selector 会恢复连接, 当且仅当 [exception] 类型是 [NetworkException] 且 [NetworkException.recoverable] 为 `true`. + */ protected inner class StateClosed( val exception: Throwable?, ) : NettyState(State.CLOSED) {