Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fast login #1154

Merged
merged 9 commits into from
Apr 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
11 changes: 9 additions & 2 deletions mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
Expand Down Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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()

Expand Down Expand Up @@ -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)

Expand All @@ -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) {
Expand All @@ -134,7 +153,6 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
}

channel = PlatformSocket()
bot.initClient()

while (isActive) {
try {
Expand All @@ -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. " +
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -672,16 +755,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" }

Expand Down
14 changes: 13 additions & 1 deletion mirai-core/src/commonMain/kotlin/network/keys.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()})"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ internal object KnownPacketFactories {
WtLogin.ExchangeEmp,
StatSvc.Register,
StatSvc.GetOnlineStatus,
StatSvc.SimpleGet,
StatSvc.GetDevLoginInfo,
MessageSvcPbGetMsg,
MessageSvcPushForceOffline,
Expand Down
19 changes: 19 additions & 0 deletions mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,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(
Expand All @@ -104,7 +127,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)
Expand Down
Loading