commonMain.network.components.SsoProcessor.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mirai-core-jvm Show documentation
Show all versions of mirai-core-jvm Show documentation
Mirai Protocol implementation for QQ Android
/*
* 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.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.internal.network.components
import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.WLoginSigInfo
import net.mamoe.mirai.internal.network.component.ComponentKey
import net.mamoe.mirai.internal.network.handler.NetworkHandler
import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandler
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
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.network.*
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.info
import net.mamoe.mirai.utils.withExceptionCollector
/**
* Handles login, and acts also as a mediator of [BotInitProcessor]
*/
internal interface SsoProcessor {
val client: QQAndroidClient
val ssoSession: SsoSession
val firstLoginResult: AtomicRef // null means just initialized
val firstLoginSucceed: Boolean get() = firstLoginResult.value?.success ?: false
val registerResp: StatSvc.Register.Response?
/**
* Do login. Throws [LoginFailedException] if failed
*/
@Throws(LoginFailedException::class)
suspend fun login(handler: NetworkHandler)
suspend fun logout(handler: NetworkHandler)
suspend fun sendRegister(handler: NetworkHandler): StatSvc.Register.Response
companion object : ComponentKey
}
internal enum class FirstLoginResult(
val success: Boolean,
val canRecoverOnFirstLogin: Boolean,
) {
PASSED(true, true),
CHANGE_SERVER(false, true), // by ConfigPush
OTHER_FAILURE(false, false),
}
/**
* Contains secrets for encryption and decryption during a session created by [SsoProcessor] and [PacketCodec].
*
* @see AccountSecrets
*/
internal interface SsoSession {
var outgoingPacketSessionId: ByteArray
/**
* always 0 for now.
*/
var loginState: Int
// also present in AccountSecrets
var wLoginSigInfo: WLoginSigInfo
val randomKey: ByteArray
}
/**
* 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 SsoProcessorImpl(
val ssoContext: SsoProcessorContext,
) : SsoProcessor {
///////////////////////////////////////////////////////////////////////////
// public
///////////////////////////////////////////////////////////////////////////
override val firstLoginResult: AtomicRef = atomic(null)
@Volatile
override var registerResp: StatSvc.Register.Response? = null
override var client
get() = ssoContext.bot.components[BotClientHolder].client
set(value) {
ssoContext.bot.components[BotClientHolder].client = value
}
override val ssoSession: SsoSession get() = client
private val components get() = ssoContext.bot.components
/**
* Do login. Throws [LoginFailedException] if failed
*/
@Throws(LoginFailedException::class)
override suspend fun login(handler: NetworkHandler) = withExceptionCollector {
components[BdhSessionSyncer].loadServerListFromCache()
try {
if (client.wLoginSigInfoInitialized) {
ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyECDH()
kotlin.runCatching {
FastLoginImpl(handler).doLogin()
}.onFailure { e ->
collectException(e)
SlowLoginImpl(handler).doLogin()
}
} else {
client = createClient(ssoContext.bot)
ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyECDH()
SlowLoginImpl(handler).doLogin()
}
} catch (e: Exception) {
// Failed to log in, invalidate secrets.
ssoContext.bot.components[AccountSecretsManager].invalidate()
throw e
}
components[AccountSecretsManager].saveSecrets(ssoContext.account, AccountSecretsImpl(client))
registerClientOnline(handler)
ssoContext.bot.logger.info { "Login successful." }
}
override suspend fun sendRegister(handler: NetworkHandler): StatSvc.Register.Response {
return registerClientOnline(handler).also { registerResp = it }
}
private suspend fun registerClientOnline(handler: NetworkHandler): StatSvc.Register.Response {
return handler.sendAndExpect(StatSvc.Register.online(client)).also {
registerResp = it
}
}
override suspend fun logout(handler: NetworkHandler) {
handler.sendWithoutExpect(StatSvc.Register.offline(client))
}
private fun createClient(bot: QQAndroidBot): QQAndroidClient {
val device = ssoContext.device
return QQAndroidClient(
ssoContext.account,
device = device,
accountSecrets = bot.components[AccountSecretsManager].getSecretsOrCreate(ssoContext.account, device)
).apply {
_bot = bot
}
}
///////////////////////////////////////////////////////////////////////////
// login
///////////////////////////////////////////////////////////////////////////
// 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 OutgoingPacketWithRespType.sendAndExpect(): R =
handler.sendAndExpect(this)
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) {
collectThrow(e)
} catch (error: Throwable) {
if (allowSlider) {
collectException(error)
allowSlider = false
response = WtLogin9(client, allowSlider).sendAndExpect()
continue@mainloop
}
collectThrow(error)
}
response = if (ticket == null) {
WtLogin9(client, allowSlider).sendAndExpect()
} else {
WtLogin2.SubmitSliderCaptcha(client, ticket).sendAndExpect()
}
} else {
// retry once
if (!allowSlider) collectThrow(createUnsupportedSliderCaptchaException(allowSlider))
allowSlider = false
response = WtLogin9(client, allowSlider).sendAndExpect()
}
}
is LoginPacketResponse.Error -> {
if (response.message.contains("0x9a")) { //Error(title=登录失败, message=请你稍后重试。(0x9a), errorInfo=)
collectThrow(RetryLaterException().initCause(IllegalStateException("Login failed: $response")))
}
val msg = response.toString()
collectThrow(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)
collectThrow(UnsupportedSMSLoginException(message))
}
}
}
}
}
private inner class FastLoginImpl(handler: NetworkHandler) : LoginStrategy(handler) {
override suspend fun doLogin() {
val login10 = handler.sendAndExpect(WtLogin10(client))
check(login10 is LoginPacketResponse.Success) { "Fast login failed: $login10" }
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy