Decouple SSO login processor

This commit is contained in:
Him188 2021-04-16 23:25:33 +08:00
parent 997ad1eb63
commit f2b236341a
18 changed files with 450 additions and 357 deletions

View File

@ -108,7 +108,7 @@ internal abstract class AbstractBot constructor(
) {
// Close network to avoid endless reconnection while network is ok
// https://github.com/mamoe/mirai/issues/894
kotlin.runCatching { network.close() }
kotlin.runCatching { network.close(null) }
return@subscribeAlways
}
/*
@ -121,7 +121,7 @@ internal abstract class AbstractBot constructor(
val cause = event.cause
val msg = if (cause == null) "" else " with exception: $cause"
bot.logger.info("Bot is closed manually $msg", cause)
network.close()
network.close(null)
}
is BotOfflineEvent.Force -> {
bot.logger.info { "Connection occupied by another android device: ${event.message}" }
@ -131,7 +131,7 @@ internal abstract class AbstractBot constructor(
bot.logger.info { "Reconnecting..." }
// delay(3000)
} else {
network.close()
network.close(null)
}
}
is BotOfflineEvent.MsfOffline,
@ -200,7 +200,7 @@ internal abstract class AbstractBot constructor(
logger.info { "Bot cancelled" + throwable?.message?.let { ": $it" }.orEmpty() }
kotlin.runCatching {
network.close()
network.close(throwable)
}
offlineListener.cancel(CancellationException("Bot cancelled", throwable))
@ -220,7 +220,7 @@ internal abstract class AbstractBot constructor(
return
}
this.network.close()
this.network.close(cause)
if (supervisorJob.isActive) {
if (cause == null) {

View File

@ -17,11 +17,11 @@ import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.OtherClientInfo
import net.mamoe.mirai.internal.contact.OtherClientImpl
import net.mamoe.mirai.internal.contact.checkIsGroupImpl
import net.mamoe.mirai.internal.contact.uin
import net.mamoe.mirai.internal.network.*
import net.mamoe.mirai.internal.network.handler.*
import net.mamoe.mirai.internal.network.handler.impl.netty.NettyNetworkHandlerFactory
import net.mamoe.mirai.internal.network.net.protocol.SsoContext
import net.mamoe.mirai.internal.network.net.protocol.SsoProcessor
import net.mamoe.mirai.internal.network.net.protocol.SsoProcessorContextImpl
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
@ -53,8 +53,7 @@ internal fun QQAndroidBot.createOtherClient(
internal class QQAndroidBot constructor(
internal val account: BotAccount,
configuration: BotConfiguration
) : AbstractBot(configuration, account.id), SsoContext {
override lateinit var client: QQAndroidClient
) : AbstractBot(configuration, account.id) {
override val bot: QQAndroidBot get() = this
val bdhSyncer: BdhSessionSyncer = BdhSessionSyncer(this)
@ -66,12 +65,16 @@ internal class QQAndroidBot constructor(
// TODO: 2021/4/14 bdhSyncer.loadFromCache() when login
private val ssoProcessor: SsoProcessor by lazy { SsoProcessor(SsoProcessorContextImpl(this)) }
val client get() = ssoProcessor.client
override suspend fun sendLogout() {
network.sendWithoutExpect(StatSvc.Register.offline(client))
}
override fun createNetworkHandler(coroutineContext: CoroutineContext): NetworkHandler {
val context = NetworkHandlerContextImpl(this, this)
val context = NetworkHandlerContextImpl(this, ssoProcessor, configuration.networkLoggerSupplier(this))
return SelectorNetworkHandler(
context,
FactoryKeepAliveNetworkHandlerSelector(NettyNetworkHandlerFactory, serverListNew, context)

View File

@ -20,7 +20,9 @@ import net.mamoe.mirai.utils.md5
import net.mamoe.mirai.utils.toByteArray
import java.util.concurrent.CopyOnWriteArraySet
/**
* Secrets for authentication with server. (login)
*/
internal interface AccountSecrets {
var wLoginSigInfoField: WLoginSigInfo?

View File

@ -0,0 +1,122 @@
/*
* 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
import net.mamoe.mirai.Bot
import net.mamoe.mirai.internal.BotAccount
import net.mamoe.mirai.internal.utils.actualCacheDir
import net.mamoe.mirai.internal.utils.crypto.TEA
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.DeviceInfo
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.info
import java.io.File
/**
* For a [Bot].
*
* @see MemoryAccountSecretsManager
* @see FileCacheAccountSecretsManager
* @see CombinedAccountSecretsManager
*/
internal interface AccountSecretsManager {
fun saveSecrets(account: BotAccount, secrets: AccountSecrets)
fun getSecrets(account: BotAccount): AccountSecrets?
}
internal fun AccountSecretsManager.getSecretsOrCreate(account: BotAccount, device: DeviceInfo): AccountSecrets {
var secrets = getSecrets(account)
if (secrets == null) {
secrets = AccountSecretsImpl(device, account)
saveSecrets(account, secrets)
}
return secrets
}
internal class MemoryAccountSecretsManager : AccountSecretsManager {
@Volatile
private var instance: AccountSecrets? = null
@Synchronized
override fun saveSecrets(account: BotAccount, secrets: AccountSecrets) {
instance = secrets
}
@Synchronized
override fun getSecrets(account: BotAccount): AccountSecrets? = this.instance
}
internal class FileCacheAccountSecretsManager(
val file: File,
val logger: MiraiLogger,
) : AccountSecretsManager {
override fun saveSecrets(account: BotAccount, secrets: AccountSecrets) {
if (secrets.wLoginSigInfoField == null) return
file.writeBytes(
TEA.encrypt(
AccountSecretsImpl(secrets).toByteArray(AccountSecretsImpl.serializer()),
account.passwordMd5
)
)
logger.info { "Saved account secrets to local cache for fast login." }
TEA.encrypt(file.readBytes(), account.passwordMd5).loadAs(AccountSecretsImpl.serializer())
}
override fun getSecrets(account: BotAccount): AccountSecrets? {
return getSecretsImpl(account)
}
private fun getSecretsImpl(account: BotAccount): AccountSecrets? {
if (!file.exists()) return null
val loaded = kotlin.runCatching {
TEA.decrypt(file.readBytes(), account.passwordMd5).loadAs(AccountSecretsImpl.serializer())
}.getOrElse { e ->
logger.error("Failed to load account secrets from local cache. Invalidating cache...", e)
file.delete()
return null
}
logger.info { "Loaded account secrets from local cache." }
return loaded
}
}
internal class CombinedAccountSecretsManager(
private val primary: AccountSecretsManager,
private val alternative: AccountSecretsManager,
) : AccountSecretsManager {
override fun saveSecrets(account: BotAccount, secrets: AccountSecrets) {
primary.saveSecrets(account, secrets)
alternative.saveSecrets(account, secrets)
}
override fun getSecrets(account: BotAccount): AccountSecrets? {
return primary.getSecrets(account) ?: alternative.getSecrets(account)
}
}
/**
* Create a [CombinedAccountSecretsManager] with [MemoryAccountSecretsManager] as primary and [FileCacheAccountSecretsManager] as an alternative.
*/
internal fun BotConfiguration.createAccountsSecretsManager(logger: MiraiLogger): AccountSecretsManager {
return CombinedAccountSecretsManager(
MemoryAccountSecretsManager(),
FileCacheAccountSecretsManager(
actualCacheDir().resolve("account.secrets"),
logger
)
)
}

View File

@ -22,7 +22,7 @@ import kotlinx.serialization.Serializable
import net.mamoe.mirai.data.OnlineStatus
import net.mamoe.mirai.internal.BotAccount
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.net.protocol.LoginSessionAware
import net.mamoe.mirai.internal.network.net.protocol.SsoSession
import net.mamoe.mirai.internal.network.protocol.SyncingCacheList
import net.mamoe.mirai.internal.network.protocol.data.jce.FileStoragePushFSSvcList
import net.mamoe.mirai.internal.network.protocol.packet.Tlv
@ -73,15 +73,13 @@ internal open class QQAndroidClient(
override val ecdh: ECDH = ECDH(),
val device: DeviceInfo,
accountSecrets: AccountSecrets
) : AccountSecrets by accountSecrets, LoginSessionAware {
) : AccountSecrets by accountSecrets, SsoSession {
lateinit var _bot: QQAndroidBot
val bot: QQAndroidBot get() = _bot
internal var strangerSeq: Int = 0
val keys: Map<String, ByteArray> by lazy { allKeys() }
var onlineStatus: OnlineStatus = OnlineStatus.ONLINE

View File

@ -13,10 +13,9 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State
import net.mamoe.mirai.internal.network.net.protocol.SsoContext
import net.mamoe.mirai.internal.network.net.protocol.SsoProcessor
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.MiraiLogger
import java.net.InetAddress
import java.net.InetSocketAddress
@ -27,23 +26,18 @@ import java.util.concurrent.CancellationException
* Immutable context for [NetworkHandler]
*/
internal interface NetworkHandlerContext {
val bot: QQAndroidBot // // TODO: 2021/4/16 this is bad, making it difficult to write unit tests.
val bot: QQAndroidBot
// however migration requires a major change.
val logger: MiraiLogger
val ssoContext: SsoContext
val configuration: BotConfiguration
val ssoProcessor: SsoProcessor
}
internal class NetworkHandlerContextImpl(
override val bot: QQAndroidBot,
override val ssoContext: SsoContext,
) : NetworkHandlerContext {
override val configuration: BotConfiguration
get() = bot.configuration
override val logger: MiraiLogger by lazy { configuration.networkLoggerSupplier(bot) }
}
override val ssoProcessor: SsoProcessor,
override val logger: MiraiLogger,
) : NetworkHandlerContext
/**
* Basic interface available to application. Usually wrapped with [SelectorNetworkHandler].
@ -116,7 +110,7 @@ internal interface NetworkHandler {
/**
* Closes this handler gracefully.
*/
fun close()
fun close(cause: Throwable?)
///////////////////////////////////////////////////////////////////////////
// compatibility

View File

@ -39,8 +39,8 @@ internal class SelectorNetworkHandler(
instance().sendAndExpect(packet, timeout, attempts)
override suspend fun sendWithoutExpect(packet: OutgoingPacket) = instance().sendWithoutExpect(packet)
override fun close() {
selector.getResumedInstance()?.close()
override fun close(cause: Throwable?) {
selector.getResumedInstance()?.close(cause)
}
}

View File

@ -13,6 +13,7 @@ import kotlinx.coroutines.*
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.handler.NetworkHandler
import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext
import net.mamoe.mirai.internal.network.handler.logger
import net.mamoe.mirai.internal.network.net.protocol.RawIncomingPacket
import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacket
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
@ -81,7 +82,8 @@ internal abstract class NetworkHandlerSupport(
sendPacketImpl(packet)
}
override fun close() {
override fun close(cause: Throwable?) {
logger.info { "NetworkHandler closed: $cause" }
coroutineContext.job.cancel("NetworkHandler closed.")
}
@ -138,7 +140,11 @@ internal abstract class NetworkHandlerSupport(
private set
final override val state: NetworkHandler.State get() = _state.correspondingState
protected inline fun setState(crossinline new: () -> BaseStateImpl) = synchronized(this) {
/**
* You may need to call [BaseStateImpl.resumeConnection] since state is lazy.
*/
protected inline fun <S : BaseStateImpl> setState(crossinline new: () -> S): S = synchronized(this) {
// we can add hooks here for debug.
val impl = new()
@ -147,6 +153,7 @@ internal abstract class NetworkHandlerSupport(
check(old !== impl) { "Old and new states cannot be the same." }
old.cancel()
_state = impl
return impl
}
final override suspend fun resumeConnection() {

View File

@ -29,7 +29,7 @@ import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext
import net.mamoe.mirai.internal.network.handler.impl.NetworkHandlerSupport
import net.mamoe.mirai.internal.network.net.protocol.PacketCodec
import net.mamoe.mirai.internal.network.net.protocol.RawIncomingPacket
import net.mamoe.mirai.internal.network.net.protocol.SsoController
import net.mamoe.mirai.internal.network.net.protocol.SsoProcessor
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.utils.childScope
import java.net.SocketAddress
@ -39,11 +39,11 @@ internal class NettyNetworkHandler(
context: NetworkHandlerContext,
private val address: SocketAddress,
) : NetworkHandlerSupport(context) {
override fun close() {
setState { StateClosed(null) }
override fun close(cause: Throwable?) {
setState { StateClosed(cause) }
}
private fun closeSuper() = super.close()
private fun closeSuper(cause: Throwable?) = super.close(cause)
override suspend fun sendPacketImpl(packet: OutgoingPacket) {
val state = _state as NettyState
@ -57,7 +57,7 @@ internal class NettyNetworkHandler(
private inner class ByteBufToIncomingPacketDecoder : SimpleChannelInboundHandler<ByteBuf>(ByteBuf::class.java) {
override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) {
ctx.fireChannelRead(msg.toReadPacket().use { packet ->
PacketCodec.decodeRaw(context.bot.client, packet)
PacketCodec.decodeRaw(context.ssoProcessor.ssoSession, packet)
})
}
}
@ -143,19 +143,24 @@ internal class NettyNetworkHandler(
override suspend fun resumeConnection() {
setState { StateConnecting(PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext)) }
.resumeConnection()
}
}
/**
* 1. Connect to server.
* 2. Perform SSO login with [SsoProcessor]
* 3. If failure, set state to [StateClosed]
* 4. If success, set state to [StateOK]
*/
private inner class StateConnecting(
val decodePipeline: PacketDecodePipeline,
) : NettyState(NetworkHandler.State.CONNECTING) {
private val ssoController = SsoController(context.ssoContext, this@NettyNetworkHandler)
private val connection = async { createConnection(decodePipeline) }
private val connectResult = async {
val connection = connection.await()
ssoController.login()
context.ssoProcessor.login(this@NettyNetworkHandler)
setState { StateOK(connection) }
}.apply {
invokeOnCompletion { error ->
@ -191,6 +196,7 @@ internal class NettyNetworkHandler(
override suspend fun resumeConnection() {
setState { StateConnecting(PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext)) }
.resumeConnection() // the user wil
} // noop
}
@ -198,7 +204,7 @@ internal class NettyNetworkHandler(
val exception: Throwable?
) : NettyState(NetworkHandler.State.OK) {
init {
closeSuper()
closeSuper(exception)
}
override suspend fun sendPacketImpl(packet: OutgoingPacket) = error("NetworkHandler is already closed.")

View File

@ -12,11 +12,6 @@ package net.mamoe.mirai.internal.network
import kotlinx.io.core.ByteReadPacket
import kotlinx.serialization.SerialName
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
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.network.NoServerAvailableException
import net.mamoe.mirai.utils.*
@ -174,65 +169,4 @@ internal open class KeyWithCreationTime(
override fun toString(): String {
return "KeyWithCreationTime(data=${data.toUHexString()}, creationTime=$creationTime)"
}
}
internal suspend inline fun QQAndroidClient.useNextServers(crossinline block: suspend (host: String, port: Int) -> Unit) {
if (bot.serverList.isEmpty()) {
bot.bdhSyncer.loadServerListFromCache()
if (bot.serverList.isEmpty()) {
bot.serverList.addAll(DefaultServerList)
}
}
retryCatchingExceptions(bot.serverList.size, except = LoginFailedException::class) l@{
val pair = bot.serverList[0]
runCatchingExceptions {
block(pair.first, pair.second)
return@l
}.getOrElse {
bot.serverList.remove(pair)
if (it !is LoginFailedException) {
// 不要重复打印.
bot.logger.warning(it)
}
throw it
}
}.getOrElse {
if (it is LoginFailedException) {
throw it
}
bot.serverList.addAll(DefaultServerList)
throw NoServerAvailableException(it)
}
}
@Suppress("RemoveRedundantQualifierName") // bug
internal fun generateTgtgtKey(guid: ByteArray): ByteArray =
(getRandomByteArray(16) + guid).md5()
internal inline fun <R> QQAndroidClient.tryDecryptOrNull(
data: ByteArray,
size: Int = data.size,
mapper: (ByteArray) -> R
): R? {
keys.forEach { (key, value) ->
kotlin.runCatching {
return mapper(TEA.decrypt(data, value, size).also { PacketLogger.verbose { "成功使用 $key 解密" } })
}
}
return null
}
internal fun QQAndroidClient.allKeys() = mapOf(
"16 zero" to ByteArray(16),
"D2 key" to wLoginSigInfo.d2Key,
"wtSessionTicketKey" to wLoginSigInfo.wtSessionTicketKey,
"userStKey" to wLoginSigInfo.userStKey,
"tgtgtKey" to tgtgtKey,
"tgtKey" to wLoginSigInfo.tgtKey,
"deviceToken" to wLoginSigInfo.deviceToken,
"shareKeyCalculatedByConstPubKey" to ecdh.keyPair.initialShareKey
//"t108" to wLoginSigInfo.t1,
//"t10c" to t10c,
//"t163" to t163
)
}

View File

@ -17,13 +17,26 @@ import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey
import net.mamoe.mirai.utils.*
import kotlin.io.use
/**
* Packet decoders.
*
* - Transforms [ByteReadPacket] to [RawIncomingPacket]
*/
internal object PacketCodec {
/**
* 数据包相关的调试输出.
* 它默认是关闭的.
*/
internal val PacketLogger: MiraiLoggerWithSwitch by lazy {
MiraiLogger.create("Packet").withSwitch(false)
}
/**
* It's caller's responsibility to close [input]
* @param input received from sockets.
* @return decoded
*/
fun decodeRaw(client: LoginSessionAware, input: ByteReadPacket): RawIncomingPacket = input.run {
fun decodeRaw(client: SsoSession, input: ByteReadPacket): RawIncomingPacket = input.run {
// login
val flag1 = readInt()
@ -74,7 +87,7 @@ internal object PacketCodec {
val body: ByteReadPacket,
)
private fun parseSsoFrame(client: LoginSessionAware, bytes: ByteArray): DecodeResult =
private fun parseSsoFrame(client: SsoSession, bytes: ByteArray): DecodeResult =
bytes.toReadPacket().use { input ->
val commandName: String
val ssoSequenceId: Int
@ -139,7 +152,7 @@ internal object PacketCodec {
}
private fun ByteReadPacket.parseOicqResponse(
client: LoginSessionAware,
client: SsoSession,
): ByteArray {
check(readByte().toInt() == 2)
this.discardExact(2)
@ -215,7 +228,10 @@ internal object PacketCodec {
}
}
internal open class RawIncomingPacket constructor(
/**
* Represents a packet that has just been decrypted. Subsequent operation is normally passing it to a responsible [PacketFactory] according to [commandName] from [KnownPacketFactories].
*/
internal class RawIncomingPacket constructor(
val commandName: String,
val sequenceId: Int,
/**

View File

@ -1,229 +0,0 @@
/*
* 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.net.protocol
import net.mamoe.mirai.internal.network.AccountSecrets
import net.mamoe.mirai.internal.network.AccountSecretsImpl
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.handler.NetworkHandler
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse.Captcha
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin10
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.sendAndExpect
import net.mamoe.mirai.internal.utils.crypto.TEA
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
import net.mamoe.mirai.network.*
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
import java.io.File
internal interface SsoContext {
var client: QQAndroidClient
val configuration: BotConfiguration
val loginSessionAware: LoginSessionAware get() = client
val accountSecrets: AccountSecrets get() = client
}
internal class SsoController(
private val ssoContext: SsoContext,
private val handler: NetworkHandler,
) {
@Throws(LoginFailedException::class)
suspend fun login() = withExceptionCollector {
if (ssoContext.accountSecrets.wLoginSigInfoInitialized) {
kotlin.runCatching {
fastLogin()
}.onFailure { e ->
collectException(e)
slowLogin()
}
} else {
slowLogin()
}
}
///////////////////////////////////////////////////////////////////////////
// impl
///////////////////////////////////////////////////////////////////////////
private val configuration get() = handler.context.configuration
private val context get() = handler.context
private val bot get() = context.bot
private val logger get() = bot.logger
private val account get() = bot.account
private suspend fun fastLogin() {
val login10 = WtLogin10(bot.client).sendAndExpect(bot)
check(login10 is LoginPacketResponse.Success) { "Fast login failed: $login10" }
}
private fun loginSolverNotNull(): LoginSolver {
fun LoginSolver?.notnull(): LoginSolver {
checkNotNull(this) {
"No LoginSolver found. Please provide by BotConfiguration.loginSolver. " +
"For example use `BotFactory.newBot(...) { loginSolver = yourLoginSolver}` in Kotlin, " +
"use `BotFactory.newBot(..., new BotConfiguration() {{ setLoginSolver(yourLoginSolver) }})` in Java."
}
return this
}
return bot.configuration.loginSolver.notnull()
}
private val sliderSupported get() = bot.configuration.loginSolver?.isSliderCaptchaSupported ?: false
private fun createUnsupportedSliderCaptchaException(allowSlider: Boolean): UnsupportedSliderCaptchaException {
return UnsupportedSliderCaptchaException(
buildString {
append("Mirai 无法完成滑块验证.")
if (allowSlider) {
append(" 使用协议 ")
append(configuration.protocol)
append(" 强制要求滑块验证, 请更换协议后重试.")
}
append(" 另请参阅: https://github.com/project-mirai/mirai-login-solver-selenium")
}
)
}
private suspend fun slowLogin() = withExceptionCollector {
var allowSlider = sliderSupported || bot.configuration.protocol == MiraiProtocol.ANDROID_PHONE
var response: LoginPacketResponse = WtLogin9(bot.client, allowSlider).sendAndExpect()
mainloop@ while (true) {
when (response) {
is LoginPacketResponse.Success -> {
logger.info { "Login successful" }
break@mainloop
}
is LoginPacketResponse.DeviceLockLogin -> {
response = WtLogin20(bot.client).sendAndExpect(bot)
}
is LoginPacketResponse.UnsafeLogin -> {
loginSolverNotNull().onSolveUnsafeDeviceLoginVerify(bot, response.url)
response = WtLogin9(bot.client, allowSlider).sendAndExpect()
}
is Captcha.Picture -> {
var result = loginSolverNotNull().onSolvePicCaptcha(bot, response.data)
if (result == null || result.length != 4) {
//refresh captcha
result = "ABCD"
}
response = WtLogin2.SubmitPictureCaptcha(bot.client, response.sign, result).sendAndExpect()
}
is Captcha.Slider -> {
if (sliderSupported) {
// use solver
val ticket = try {
loginSolverNotNull().onSolveSliderCaptcha(bot, response.url)?.takeIf { it.isNotEmpty() }
} catch (e: LoginFailedException) {
throw e
} catch (error: Throwable) {
if (allowSlider) {
collectException(error)
allowSlider = false
response = WtLogin9(bot.client, allowSlider).sendAndExpect()
continue@mainloop
}
throw error
}
response = if (ticket == null) {
WtLogin9(bot.client, allowSlider).sendAndExpect()
} else {
WtLogin2.SubmitSliderCaptcha(bot.client, ticket).sendAndExpect()
}
} else {
// retry once
if (!allowSlider) throw createUnsupportedSliderCaptchaException(allowSlider)
allowSlider = false
response = WtLogin9(bot.client, allowSlider).sendAndExpect()
}
}
is LoginPacketResponse.Error -> {
if (response.message.contains("0x9a")) { //Error(title=登录失败, message=请你稍后重试。(0x9a), errorInfo=)
throw RetryLaterException().initCause(IllegalStateException("Login failed: $response"))
}
val msg = response.toString()
throw WrongPasswordException(buildString(capacity = msg.length) {
append(msg)
if (msg.contains("当前上网环境异常")) { // Error(title=禁止登录, message=当前上网环境异常,请更换网络环境或在常用设备上登录或稍后再试。, errorInfo=)
append(", tips=若频繁出现, 请尝试开启设备锁")
}
})
}
is LoginPacketResponse.SMSVerifyCodeNeeded -> {
val message = "SMS required: $response, which isn't yet supported"
logger.error(message)
throw UnsupportedSMSLoginException(message)
}
}
}
}
@Suppress("unused") // false positive
internal fun initClient() {
val device = configuration.deviceInfo?.invoke(bot) ?: DeviceInfo.random()
ssoContext.client = QQAndroidClient(
bot.account,
device = device,
accountSecrets = loadSecretsFromCacheOrCreate(device)
).apply {
_bot = this@SsoController.bot
}
}
private suspend inline fun <R : Packet?> OutgoingPacketWithRespType<R>.sendAndExpect(): R = sendAndExpect(bot)
///////////////////////////////////////////////////////////////////////////
// cache
///////////////////////////////////////////////////////////////////////////
// TODO: 2021/4/14 extract a cache service
private val cacheDir: File by lazy {
configuration.workingDir.resolve(ssoContext.configuration.cacheDir).apply { mkdirs() }
}
private val accountSecretsFile: File by lazy {
cacheDir.resolve("account.secrets")
}
private fun loadSecretsFromCacheOrCreate(deviceInfo: DeviceInfo): AccountSecrets {
val loaded = if (configuration.loginCacheEnabled && accountSecretsFile.exists()) {
kotlin.runCatching {
TEA.decrypt(accountSecretsFile.readBytes(), account.passwordMd5).loadAs(AccountSecretsImpl.serializer())
}.getOrElse { e ->
logger.error("Failed to load account secrets from local cache. Invalidating cache...", e)
accountSecretsFile.delete()
null
}
} else null
if (loaded != null) {
logger.info { "Loaded account secrets from local cache." }
return loaded
}
return AccountSecretsImpl(deviceInfo, account) // wLoginSigInfoField is null, no need to save.
}
}

View File

@ -0,0 +1,233 @@
/*
* 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.net.protocol
import net.mamoe.mirai.internal.BotAccount
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.*
import net.mamoe.mirai.internal.network.handler.NetworkHandler
import net.mamoe.mirai.internal.network.handler.impl.netty.NettyNetworkHandler
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse.Captcha
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin10
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.sendAndExpect
import net.mamoe.mirai.network.*
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
/**
* Provides the information needed by the [SsoProcessor].
*/
internal interface SsoProcessorContext {
val bot: QQAndroidBot
val account: BotAccount
val device: DeviceInfo
val protocol: MiraiProtocol
val accountSecretsManager: AccountSecretsManager
val configuration: BotConfiguration
}
internal class SsoProcessorContextImpl(
override val bot: QQAndroidBot
) : SsoProcessorContext {
override val account: BotAccount get() = bot.account
override val device: DeviceInfo = configuration.deviceInfo?.invoke(bot) ?: DeviceInfo.random()
override val protocol: MiraiProtocol get() = configuration.protocol
override val accountSecretsManager: AccountSecretsManager get() = configuration.createAccountsSecretsManager(bot.logger)
override val configuration: BotConfiguration get() = bot.configuration
}
/**
* Strategy that performs the process of single sing-on (SSO). (login)
*
* And allows to retire the [session][ssoSession] after success.
*
* Used by [NettyNetworkHandler.StateConnecting].
*/
internal class SsoProcessor(
private val ssoContext: SsoProcessorContext,
) {
@Volatile
internal var client = createClient(ssoContext.bot)
internal val ssoSession: SsoSession get() = client
/**
* Do login. Throws [LoginFailedException] if failed
*/
@Throws(LoginFailedException::class)
suspend fun login(handler: NetworkHandler) = withExceptionCollector<Unit> {
if (client.wLoginSigInfoInitialized) {
kotlin.runCatching {
FastLoginImpl(handler).doLogin()
}.onFailure { e ->
collectException(e)
SlowLoginImpl(handler).doLogin()
}
} else {
client = createClient(ssoContext.bot)
SlowLoginImpl(handler).doLogin()
}
}
private fun createClient(bot: QQAndroidBot): QQAndroidClient {
val device = ssoContext.device
return QQAndroidClient(
ssoContext.account,
device = device,
accountSecrets = ssoContext.accountSecretsManager.getSecretsOrCreate(ssoContext.account, device)
).apply {
_bot = bot
}
}
// we have exactly two methods----slow and fast.
private abstract inner class LoginStrategy(
val handler: NetworkHandler,
) {
protected val context get() = handler.context
protected val bot get() = context.bot
protected val logger get() = bot.logger
protected suspend fun <R : Packet?> OutgoingPacketWithRespType<R>.sendAndExpect(): R = sendAndExpect(handler)
abstract suspend fun doLogin()
}
private inner class SlowLoginImpl(handler: NetworkHandler) : LoginStrategy(handler) {
private fun loginSolverNotNull(): LoginSolver {
fun LoginSolver?.notnull(): LoginSolver {
checkNotNull(this) {
"No LoginSolver found. Please provide by BotConfiguration.loginSolver. " +
"For example use `BotFactory.newBot(...) { loginSolver = yourLoginSolver}` in Kotlin, " +
"use `BotFactory.newBot(..., new BotConfiguration() {{ setLoginSolver(yourLoginSolver) }})` in Java."
}
return this
}
return bot.configuration.loginSolver.notnull()
}
private val sliderSupported get() = bot.configuration.loginSolver?.isSliderCaptchaSupported ?: false
private fun createUnsupportedSliderCaptchaException(allowSlider: Boolean): UnsupportedSliderCaptchaException {
return UnsupportedSliderCaptchaException(
buildString {
append("Mirai 无法完成滑块验证.")
if (allowSlider) {
append(" 使用协议 ")
append(ssoContext.protocol)
append(" 强制要求滑块验证, 请更换协议后重试.")
}
append(" 另请参阅: https://github.com/project-mirai/mirai-login-solver-selenium")
}
)
}
override suspend fun doLogin() = withExceptionCollector {
var allowSlider = sliderSupported || bot.configuration.protocol == MiraiProtocol.ANDROID_PHONE
var response: LoginPacketResponse = WtLogin9(client, allowSlider).sendAndExpect()
mainloop@ while (true) {
when (response) {
is LoginPacketResponse.Success -> {
logger.info { "Login successful" }
break@mainloop
}
is LoginPacketResponse.DeviceLockLogin -> {
response = WtLogin20(client).sendAndExpect()
}
is LoginPacketResponse.UnsafeLogin -> {
loginSolverNotNull().onSolveUnsafeDeviceLoginVerify(bot, response.url)
response = WtLogin9(client, allowSlider).sendAndExpect()
}
is Captcha.Picture -> {
var result = loginSolverNotNull().onSolvePicCaptcha(bot, response.data)
if (result == null || result.length != 4) {
//refresh captcha
result = "ABCD"
}
response = WtLogin2.SubmitPictureCaptcha(client, response.sign, result).sendAndExpect()
}
is Captcha.Slider -> {
if (sliderSupported) {
// use solver
val ticket = try {
loginSolverNotNull().onSolveSliderCaptcha(bot, response.url)?.takeIf { it.isNotEmpty() }
} catch (e: LoginFailedException) {
throw e
} catch (error: Throwable) {
if (allowSlider) {
collectException(error)
allowSlider = false
response = WtLogin9(client, allowSlider).sendAndExpect()
continue@mainloop
}
throw error
}
response = if (ticket == null) {
WtLogin9(client, allowSlider).sendAndExpect()
} else {
WtLogin2.SubmitSliderCaptcha(client, ticket).sendAndExpect()
}
} else {
// retry once
if (!allowSlider) throw createUnsupportedSliderCaptchaException(allowSlider)
allowSlider = false
response = WtLogin9(client, allowSlider).sendAndExpect()
}
}
is LoginPacketResponse.Error -> {
if (response.message.contains("0x9a")) { //Error(title=登录失败, message=请你稍后重试。(0x9a), errorInfo=)
throw RetryLaterException().initCause(IllegalStateException("Login failed: $response"))
}
val msg = response.toString()
throw WrongPasswordException(buildString(capacity = msg.length) {
append(msg)
if (msg.contains("当前上网环境异常")) { // Error(title=禁止登录, message=当前上网环境异常,请更换网络环境或在常用设备上登录或稍后再试。, errorInfo=)
append(", tips=若频繁出现, 请尝试开启设备锁")
}
})
}
is LoginPacketResponse.SMSVerifyCodeNeeded -> {
val message = "SMS required: $response, which isn't yet supported"
logger.error(message)
throw UnsupportedSMSLoginException(message)
}
}
}
}
}
private inner class FastLoginImpl(handler: NetworkHandler) : LoginStrategy(handler) {
override suspend fun doLogin() {
val login10 = WtLogin10(client).sendAndExpect(handler)
check(login10 is LoginPacketResponse.Success) { "Fast login failed: $login10" }
}
}
}

View File

@ -14,9 +14,11 @@ import net.mamoe.mirai.internal.network.WLoginSigInfo
import net.mamoe.mirai.internal.utils.crypto.ECDH
/**
* Contains secrets for encryption and decryption during a session created by [SsoProcessor] and [PacketCodec].
*
* @see AccountSecrets
*/
internal interface LoginSessionAware {
internal interface SsoSession {
var outgoingPacketSessionId: ByteArray
/**

View File

@ -13,6 +13,7 @@ import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.net.protocol.PacketCodec
import net.mamoe.mirai.internal.network.protocol.packet.chat.*
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.LongConn
@ -26,9 +27,7 @@ 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.summarycard.SummaryCard
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.MiraiLoggerWithSwitch
import net.mamoe.mirai.utils.withSwitch
internal sealed class PacketFactory<TPacket : Packet?> {
/**
@ -112,11 +111,18 @@ internal suspend inline fun <P : Packet?> IncomingPacketFactory<P>.decode(
* 数据包相关的调试输出.
* 它默认是关闭的.
*/
@Deprecated(
"Kept for binary compatibility.",
ReplaceWith("PacketCodec.PacketLogger", "net.mamoe.mirai.internal.network.net.protocol.PacketCodec"),
level = DeprecationLevel.ERROR,
)
@PublishedApi
internal val PacketLogger: MiraiLoggerWithSwitch by lazy {
MiraiLogger.create("Packet").withSwitch(false)
}
internal val PacketLogger: MiraiLoggerWithSwitch
get() = PacketCodec.PacketLogger
/**
* Registered factories.
*/
internal object KnownPacketFactories {
object OutgoingFactories : List<OutgoingPacketFactory<*>> by mutableListOf(
WtLogin.Login,

View File

@ -19,13 +19,13 @@ import net.mamoe.mirai.internal.message.contextualBugReportException
import net.mamoe.mirai.internal.message.toRichTextElems
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.net.protocol.PacketCodec
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgTransmit
import net.mamoe.mirai.internal.network.protocol.data.proto.MultiMsg
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.internal.network.protocol.packet.PacketLogger
import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
import net.mamoe.mirai.internal.utils._miraiContentToString
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
@ -118,7 +118,7 @@ internal class MultiMsg {
val proto: MultiMsg.MultiMsgApplyUpRsp
) : Response() {
override fun toString(): String {
if (PacketLogger.isEnabled) {
if (PacketCodec.PacketLogger.isEnabled) {
return _miraiContentToString()
}
return "MultiMsg.ApplyUp.Response.RequireUpload"

View File

@ -13,9 +13,9 @@ import kotlinx.coroutines.CompletableDeferred
import net.mamoe.mirai.internal.MockBot
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.handler.impl.NetworkHandlerSupport
import net.mamoe.mirai.internal.network.net.protocol.SsoContext
import net.mamoe.mirai.internal.network.net.protocol.SsoProcessor
import net.mamoe.mirai.internal.network.net.protocol.SsoProcessorContextImpl
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.MiraiLogger
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicInteger
@ -24,8 +24,7 @@ import java.util.concurrent.atomic.AtomicInteger
internal class TestNetworkHandlerContext(
override val bot: QQAndroidBot = MockBot(),
override val logger: MiraiLogger = MiraiLogger.create("Test"),
override val ssoContext: SsoContext = bot,
override val configuration: BotConfiguration = bot.configuration
override val ssoProcessor: SsoProcessor = SsoProcessor(SsoProcessorContextImpl(bot)),
) : NetworkHandlerContext
internal open class TestNetworkHandler(

View File

@ -11,7 +11,7 @@ package net.mamoe.mirai.internal.network
import net.mamoe.mirai.event.events.BotOnlineEvent
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.net.protocol.LoginSessionAware
import net.mamoe.mirai.internal.network.net.protocol.SsoSession
import net.mamoe.mirai.internal.utils.crypto.ECDH
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
@ -20,12 +20,12 @@ import net.mamoe.mirai.utils.debug
import net.mamoe.mirai.utils.withUse
import java.io.File
internal class TestLoginSessionAware(
internal class TestSsoSession(
private val accountSecrets: AccountSecrets,
override var outgoingPacketSessionId: ByteArray = byteArrayOf(1, 2, 3, 4),
override var loginState: Int = 0,
override val ecdh: ECDH = ECDH(),
) : LoginSessionAware {
) : SsoSession {
override var wLoginSigInfo: WLoginSigInfo by accountSecrets::wLoginSigInfo
override val randomKey: ByteArray by accountSecrets::randomKey
}
@ -39,7 +39,7 @@ internal fun loadSession(
}
/**
* secure to share with others.
* Secure to share with others. Designed to save real data for tests.
*/
internal fun QQAndroidClient.dumpSessionSafe(): ByteArray {
val secrets =