Fast login (#1154)

* wtlogin10

* Fast login Packet Implement (#1125)

* Correct group syncing logic again, Fix #1120

* Implement fast login packet, thanks to MiraiGo

* Delete duplicated tlv

* Refresh Token when exchanging and solve connection dropping issue (#1128)

* Correct group syncing logic again, Fix #1120

* Implement fast login packet, thanks to MiraiGo

* Delete duplicated tlv

* Schedule token exchanging every 10 minutes, solve connection dropping issue

* Refresh Token when exchanging, and correct token expire time

* Remove useless params for doFastLogin

* Fix missed register and tgt update (#1131)

* Correct group syncing logic again, Fix #1120

* Implement fast login packet, thanks to MiraiGo

* Delete duplicated tlv

* Schedule token exchanging every 10 minutes, solve connection dropping issue

* Refresh Token when exchanging, and correct token expire time

* Remove useless params for doFastLogin

* Fix missed register and tgt update

* Add login lock

* Add login lock

* Remove key refresh

* Remove heartbeat period override

* Login: Update tlv and solve constant connection dropping issue (#1150)

* Correct group syncing logic again, Fix #1120

* Implement fast login packet, thanks to MiraiGo

* Delete duplicated tlv

* Schedule token exchanging every 10 minutes, solve connection dropping issue

* Refresh Token when exchanging, and correct token expire time

* Remove useless params for doFastLogin

* Fix missed register and tgt update

* Update tlv, add tlv11d and tlv11a decoding

* Add stat heartbeat, solve constant connection dropping issue

* Update apidump for new configuration

* Add comment for statHeartbeatPeriodMillis

* Change old naming

* Add since version

Co-authored-by: Him188 <Him188@mamoe.net>

Co-authored-by: sandtechnology <20417547+sandtechnology@users.noreply.github.com>
This commit is contained in:
Him188 2021-04-03 22:31:14 +08:00 committed by GitHub
parent f6fd4de14b
commit ea1f43b9c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 437 additions and 69 deletions

View File

@ -5518,6 +5518,7 @@ public class net/mamoe/mirai/utils/BotConfiguration {
public final fun getProtocol ()Lnet/mamoe/mirai/utils/BotConfiguration$MiraiProtocol;
public final fun getReconnectPeriodMillis ()J
public final fun getReconnectionRetryTimes ()I
public final fun getStatHeartbeatPeriodMillis ()J
public final fun getWorkingDir ()Ljava/io/File;
public final synthetic fun inheritCoroutineContext (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun isConvertLineSeparator ()Z
@ -5560,6 +5561,7 @@ public class net/mamoe/mirai/utils/BotConfiguration {
public final fun setProtocol (Lnet/mamoe/mirai/utils/BotConfiguration$MiraiProtocol;)V
public final fun setReconnectPeriodMillis (J)V
public final fun setReconnectionRetryTimes (I)V
public final fun setStatHeartbeatPeriodMillis (J)V
public final fun setWorkingDir (Ljava/io/File;)V
}

View File

@ -5518,6 +5518,7 @@ public class net/mamoe/mirai/utils/BotConfiguration {
public final fun getProtocol ()Lnet/mamoe/mirai/utils/BotConfiguration$MiraiProtocol;
public final fun getReconnectPeriodMillis ()J
public final fun getReconnectionRetryTimes ()I
public final fun getStatHeartbeatPeriodMillis ()J
public final fun getWorkingDir ()Ljava/io/File;
public final synthetic fun inheritCoroutineContext (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun isConvertLineSeparator ()Z
@ -5560,6 +5561,7 @@ public class net/mamoe/mirai/utils/BotConfiguration {
public final fun setProtocol (Lnet/mamoe/mirai/utils/BotConfiguration$MiraiProtocol;)V
public final fun setReconnectPeriodMillis (J)V
public final fun setReconnectionRetryTimes (I)V
public final fun setStatHeartbeatPeriodMillis (J)V
public final fun setWorkingDir (Ljava/io/File;)V
}

View File

@ -142,9 +142,16 @@ public open class BotConfiguration { // open for Java
// Connection
///////////////////////////////////////////////////////////////////////////
/** 心跳周期. 过长会导致被服务器断开连接. */
/** 连接心跳周期. 过长会导致被服务器断开连接. */
public var heartbeatPeriodMillis: Long = 60.secondsToMillis
/**
* 状态心跳包周期. 过长会导致掉线.
* 该值会在登录时根据服务器下发的配置自动进行更新.
* @since 2.6
*/
public var statHeartbeatPeriodMillis: Long = 300.secondsToMillis
/**
* 每次心跳时等待结果的时间.
* 一旦心跳超时, 整个网络服务将会重启 (将消耗约 1s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
@ -545,4 +552,4 @@ internal val deviceInfoStub: (Bot) -> DeviceInfo = {
MiraiLogger.TopLevel.warning("未指定设备信息, 已使用随机设备信息. 请查看 BotConfiguration.deviceInfo 以获取更多信息.")
MiraiLogger.TopLevel.warning("Device info isn't specified. Please refer to BotConfiguration.deviceInfo for more information")
DeviceInfo.random()
}
}

View File

@ -35,10 +35,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.ConfigPushSvc
import net.mamoe.mirai.internal.network.protocol.packet.login.Heartbeat
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin15
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin2
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin20
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin9
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.*
import net.mamoe.mirai.internal.utils.*
import net.mamoe.mirai.network.*
import net.mamoe.mirai.utils.*
@ -63,6 +60,7 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
private var _packetReceiverJob: Job? = null
private var heartbeatJob: Job? = null
private var statHeartbeatJob: Job? = null
private val packetReceiveLock: Mutex = Mutex()
@ -96,6 +94,25 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
}.also { _packetReceiverJob = it }
}
private fun startStatHeartbeatJobOrKill(cancelCause: CancellationException? = null): Job {
statHeartbeatJob?.cancel(cancelCause)
return this@QQAndroidBotNetworkHandler.launch(CoroutineName("statHeartbeatJob")) statHeartbeatJob@{
while (this.isActive) {
delay(bot.configuration.statHeartbeatPeriodMillis)
val failException = doStatHeartbeat()
if (failException != null) {
delay(bot.configuration.firstReconnectDelayMillis)
bot.launch {
BotOfflineEvent.Dropped(bot, failException).broadcast()
}
return@statHeartbeatJob
}
}
}.also { statHeartbeatJob = it }
}
private fun startHeartbeatJobOrKill(cancelCause: CancellationException? = null): Job {
heartbeatJob?.cancel(cancelCause)
@ -121,6 +138,8 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
override suspend fun closeEverythingAndRelogin(host: String, port: Int, cause: Throwable?, step: Int) {
heartbeatJob?.cancel(CancellationException("relogin", cause))
heartbeatJob?.join()
statHeartbeatJob?.cancel(CancellationException("relogin", cause))
statHeartbeatJob?.join()
_packetReceiverJob?.cancel(CancellationException("relogin", cause))
_packetReceiverJob?.join()
if (::channel.isInitialized) {
@ -134,7 +153,6 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
}
channel = PlatformSocket()
bot.initClient()
while (isActive) {
try {
@ -156,9 +174,81 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
}
}
}
logger.info { "Connected to server $host:$port" }
if (bot.client.wLoginSigInfoInitialized) {
// do fast login
} else {
bot.initClient()
}
startPacketReceiverJobOrKill(CancellationException("relogin", cause))
if (bot.client.wLoginSigInfoInitialized) {
// do fast login
kotlin.runCatching {
doFastLogin()
}.onFailure {
bot.initClient()
doSlowLogin(host, port, cause, step)
}
} else {
doSlowLogin(host, port, cause, step)
}
// println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
registerClientOnline()
startStatHeartbeatJobOrKill()
startHeartbeatJobOrKill()
bot.eventChannel.subscribeOnce<BotOnlineEvent>(this.coroutineContext) {
val bot = (bot as QQAndroidBot)
if (bot.firstLoginSucceed && bot.client.wLoginSigInfoInitialized) {
launch {
while (isActive) {
bot.client.wLoginSigInfo.vKey.run {
//由过期时间最短的且不会被skey更换更新的vkey计算重新登录的时间
val delay = (expireTime - creationTime).seconds - 5.minutes
logger.info { "Scheduled refresh login session in ${delay.toHumanReadableString()}." }
delay(delay)
}
runCatching {
doFastLogin()
registerClientOnline()
}.onFailure {
logger.warning("Failed to refresh login session.", it)
}
}
}
launch {
while (isActive) {
bot.client.wLoginSigInfo.sKey.run {
val delay = (expireTime - creationTime).seconds - 5.minutes
logger.info { "Scheduled key refresh in ${delay.toHumanReadableString()}." }
delay(delay)
}
runCatching {
refreshKeys()
}.onFailure {
logger.error("Failed to refresh key.", it)
}
}
}
}
}
}
private val fastLoginOrSendPacketLock = Mutex()
private suspend fun doFastLogin(): Boolean {
fastLoginOrSendPacketLock.withLock {
val login10 = WtLogin10(bot.client).sendAndExpect(ignoreLock = true)
return login10 is WtLogin.Login.LoginPacketResponse.Success
}
}
private suspend fun doSlowLogin(host: String, port: Int, cause: Throwable?, step: Int) {
fun LoginSolver?.notnull(): LoginSolver {
checkNotNull(this) {
"No LoginSolver found. Please provide by BotConfiguration.loginSolver. " +
@ -263,24 +353,6 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
}
}
// println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
registerClientOnline()
startHeartbeatJobOrKill()
launch {
while (isActive) {
bot.client.wLoginSigInfo.sKey.run {
val delay = (expireTime - creationTime).seconds - 5.minutes
logger.info { "Scheduled key refresh in ${delay.toHumanReadableString()}." }
delay(delay)
}
runCatching {
refreshKeys()
}.onFailure {
logger.error("Failed to refresh key.", it)
}
}
}
}
suspend fun refreshKeys() {
@ -429,6 +501,17 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
logger.info { "Syncing friend message history: Success." }
}
private suspend fun doStatHeartbeat(): Throwable? {
return retryCatching(2) {
StatSvc.SimpleGet(bot.client)
.sendAndExpect<StatSvc.SimpleGet.Response>(
timeoutMillis = bot.configuration.heartbeatTimeoutMillis,
retry = 2
)
return null
}.exceptionOrNull()
}
private suspend fun doHeartBeat(): Throwable? {
return retryCatching(2) {
Heartbeat.Alive(bot.client)
@ -675,16 +758,27 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
suspend inline fun <E : Packet> OutgoingPacketWithRespType<E>.sendAndExpect(
timeoutMillis: Long = 5000,
retry: Int = 2
retry: Int = 2,
ignoreLock: Boolean = false,
): E {
return (this as OutgoingPacket).sendAndExpect(timeoutMillis, retry)
return (this as OutgoingPacket).sendAndExpect(timeoutMillis, retry, ignoreLock)
}
/**
* 发送一个包, 挂起协程直到接收到指定的返回包或超时
*/
@Suppress("UNCHECKED_CAST")
suspend fun <E : Packet> OutgoingPacket.sendAndExpect(timeoutMillis: Long = 5000, retry: Int = 2): E {
suspend fun <E : Packet> OutgoingPacket.sendAndExpect(
timeoutMillis: Long = 5000,
retry: Int = 2,
ignoreLock: Boolean = false
): E {
return if (!ignoreLock) fastLoginOrSendPacketLock.withLock {
sendAndExpectImpl(timeoutMillis, retry)
} else sendAndExpectImpl(timeoutMillis, retry)
}
private suspend fun <E : Packet> OutgoingPacket.sendAndExpectImpl(timeoutMillis: Long, retry: Int): E {
require(timeoutMillis > 100) { "timeoutMillis must > 100" }
require(retry in 0..10) { "retry must in 0..10" }

View File

@ -13,6 +13,7 @@ import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.Input
import kotlinx.io.core.readBytes
import kotlinx.io.core.readUShort
import kotlinx.serialization.Serializable
import net.mamoe.mirai.internal.network.getRandomByteArray
import net.mamoe.mirai.internal.network.protocol.packet.PacketLogger
import net.mamoe.mirai.internal.utils.crypto.TEA
@ -111,8 +112,19 @@ internal class WLoginSigInfo(
// val pt4Token: ByteArray,
var wtSessionTicket: WtSessionTicket,
var wtSessionTicketKey: ByteArray,
var deviceToken: ByteArray
var deviceToken: ByteArray,
var encryptedDownloadSession: EncryptedDownloadSession? = null
) {
//图片加密下载
//是否加密从bigdatachannel处得知
@Serializable
internal class EncryptedDownloadSession(
val appId: Long,//1600000226L
val stKey: ByteArray,
val stSig: ByteArray
)
override fun toString(): String {
return "WLoginSigInfo(uin=$uin, encryptA1=${encryptA1?.toUHexString()}, noPicSig=${noPicSig?.toUHexString()}, simpleInfo=$simpleInfo, appPri=$appPri, a2ExpiryTime=$a2ExpiryTime, loginBitmap=$loginBitmap, tgt=${tgt.toUHexString()}, a2CreationTime=$a2CreationTime, tgtKey=${tgtKey.toUHexString()}, userStSig=$userStSig, userStKey=${userStKey.toUHexString()}, userStWebSig=$userStWebSig, userA5=$userA5, userA8=$userA8, lsKey=$lsKey, sKey=$sKey, userSig64=$userSig64, openId=${openId.toUHexString()}, openKey=$openKey, vKey=$vKey, accessToken=$accessToken, d2=$d2, d2Key=${d2Key.toUHexString()}, sid=$sid, aqSig=$aqSig, psKey=$psKeyMap, superKey=${superKey.toUHexString()}, payToken=${payToken.toUHexString()}, pf=${pf.toUHexString()}, pfKey=${pfKey.toUHexString()}, da2=${da2.toUHexString()}, wtSessionTicket=$wtSessionTicket, wtSessionTicketKey=${wtSessionTicketKey.toUHexString()}, deviceToken=${deviceToken.toUHexString()})"
}

View File

@ -127,6 +127,7 @@ internal object KnownPacketFactories {
WtLogin.ExchangeEmp,
StatSvc.Register,
StatSvc.GetOnlineStatus,
StatSvc.SimpleGet,
StatSvc.GetDevLoginInfo,
MessageSvcPbGetMsg,
MessageSvcPushForceOffline,

View File

@ -222,6 +222,16 @@ internal fun BytePacketBuilder.t100(
} shouldEqualsTo 22
}
internal fun BytePacketBuilder.t10a(
tgt: ByteArray,
) {
writeShort(0x10a)
writeShortLVPacket {
writeFully(tgt)
}
}
internal fun BytePacketBuilder.t107(
picType: Int,
capType: Int = 0,
@ -326,6 +336,15 @@ internal fun BytePacketBuilder.t142(
}
}
internal fun BytePacketBuilder.t143(
d2: ByteArray
) {
writeShort(0x143)
writeShortLVPacket {
writeFully(d2)
}
}
internal fun BytePacketBuilder.t112(
nonNumberUin: ByteArray
) {

View File

@ -143,7 +143,7 @@ internal class ConfigPushSvc {
serverListPush.mobileSSOServerList
}
bot.logger.info { "Server list: ${pushServerList.joinToString()}." }
bot.network.logger.info { "Server list: ${pushServerList.joinToString()}." }
if (pushServerList.isNotEmpty()) {
bot.serverList.clear()

View File

@ -95,6 +95,29 @@ internal class StatSvc {
}
}
internal object SimpleGet : OutgoingPacketFactory<SimpleGet.Response>("StatSvc.SimpleGet") {
internal object Response : Packet {
override fun toString(): String = "Response(SimpleGet.Response)"
}
operator fun invoke(
client: QQAndroidClient
): OutgoingPacket = buildLoginOutgoingPacket(
client,
bodyType = 1,
extraData = client.wLoginSigInfo.d2.data,
key = client.wLoginSigInfo.d2Key
) {
writeSsoPacket(client, client.subAppId, commandName, sequenceId = it) {
}
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
return Response
}
}
internal object Register : OutgoingPacketFactory<Register.Response>("StatSvc.register") {
internal class Response(
@ -106,7 +129,7 @@ internal class StatSvc {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
val packet = readUniPacket(SvcRespRegister.serializer())
packet.iHelloInterval.let {
bot.configuration.heartbeatPeriodMillis = it.times(1000).toLong()
bot.configuration.statHeartbeatPeriodMillis = it.times(1000).toLong()
}
return Response(packet)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,89 @@
/*
* Copyright 2019-2021 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.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.packet.*
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.internal.utils.GuidSource
import net.mamoe.mirai.internal.utils.MacOrAndroidIdChangeFlag
import net.mamoe.mirai.internal.utils.guidFlag
import net.mamoe.mirai.utils.generateDeviceInfoData
import net.mamoe.mirai.utils.md5
import net.mamoe.mirai.utils.toReadPacket
internal object WtLogin10 : WtLoginExt {
const val appId: Long = 16L
operator fun invoke(
client: QQAndroidClient,
) = WtLogin.ExchangeEmp.buildLoginOutgoingPacket(client, bodyType = 2, key = ByteArray(16)) { sequenceId ->
writeSsoPacket(
client,
client.subAppId,
WtLogin.ExchangeEmp.commandName,
extraData = client.wLoginSigInfo.tgt.toReadPacket(),
sequenceId = sequenceId
) {
writeOicqRequestPacket(
client,
EncryptMethodECDH(client.ecdh),
0x0810
) {
writeShort(11) // subCommand
writeShort(17)
t100(appId, 100, client.appClientVersion, client.ssoVersion, client.mainSigMap)
t10a(client.wLoginSigInfo.tgt)
t116(client.miscBitMap, client.subSigMap)
t108(client.ksid)
t144(
androidId = client.device.androidId,
androidDevInfo = client.device.generateDeviceInfoData(),
osType = client.device.osType,
osVersion = client.device.version.release,
networkType = client.networkType,
simInfo = client.device.simInfo,
unknown = byteArrayOf(),
apn = client.device.apn,
isGuidFromFileNull = false,
isGuidAvailable = true,
isGuidChanged = false,
guidFlag = guidFlag(GuidSource.FROM_STORAGE, MacOrAndroidIdChangeFlag(0)),
buildModel = client.device.model,
guid = client.device.guid,
buildBrand = client.device.brand,
tgtgtKey = client.wLoginSigInfo.d2Key.md5()
)
//t112(client.account.phoneNumber.encodeToByteArray())
t143(client.wLoginSigInfo.d2.data)
t142(client.apkId)
t154(sequenceId)
t18(appId, uin = client.uin)
t141(client.device.simInfo, client.networkType, client.device.apn)
t8(2052)
//t511()
t147(appId, client.apkVersionName, client.apkSignatureMd5)
t177(client.buildTime, client.sdkVersion)
t187(client.device.macAddress)
t188(client.device.androidId)
t194(client.device.imsiMd5)
t511(
listOf(
"tenpay.com", "openmobile.qq.com", "docs.qq.com", "connect.qq.com",
"qzone.qq.com", "vip.qq.com", "qun.qq.com", "game.qq.com", "qqweb.qq.com",
"office.qq.com", "ti.qq.com", "mail.qq.com", "qzone.com", "mma.qq.com"
)
)
//t544()
}
}
}
}

View File

@ -12,6 +12,7 @@ package net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin
import kotlinx.io.core.*
import net.mamoe.mirai.internal.network.LoginExtraData
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.WLoginSigInfo
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.internal.network.protocol.packet.Tlv
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
@ -40,14 +41,14 @@ internal inline fun WtLoginExt.analysisTlv0x531(
internal interface WtLoginExt { // so as not to register to global extension
fun onErrorMessage(tlvMap: TlvMap): WtLogin.Login.LoginPacketResponse.Error? {
fun onErrorMessage(type: Int, tlvMap: TlvMap): WtLogin.Login.LoginPacketResponse.Error? {
return tlvMap[0x149]?.read {
discardExact(2) //type
val title: String = readUShortLVString()
val content: String = readUShortLVString()
val otherInfo: String = readUShortLVString()
WtLogin.Login.LoginPacketResponse.Error(title, content, otherInfo)
WtLogin.Login.LoginPacketResponse.Error(type, title, content, otherInfo)
} ?: tlvMap[0x146]?.read {
discardExact(2) // ver
discardExact(2) // code
@ -56,7 +57,7 @@ internal interface WtLoginExt { // so as not to register to global extension
val message = readUShortLVString()
val errorInfo = readUShortLVString()
WtLogin.Login.LoginPacketResponse.Error(title, message, errorInfo)
WtLogin.Login.LoginPacketResponse.Error(type, title, message, errorInfo)
}
}
@ -116,6 +117,23 @@ internal interface WtLoginExt { // so as not to register to global extension
}
}
/**
* Encrypt sig and key for pic downloading
*/
fun QQAndroidClient.analysisTlv11d(t11d: ByteArray): WLoginSigInfo.EncryptedDownloadSession = t11d.read {
val appid = readInt().toLong().and(4294967295L)
val stKey = ByteArray(16)
readAvailable(stKey)
val stSigLength = readUShort().toInt()
val stSig = ByteArray(stSigLength)
readAvailable(stSig)
WLoginSigInfo.EncryptedDownloadSession(
appid,
stKey,
stSig
)
}
/**
* pwd flag
*/