[core] Support solving device verification request by SMS:

- close #2190, helps #717
- deprecate `LoginSolver.onSolveUnsafeDeviceLoginVerify`, add `.onSolveDeviceVerification`
This commit is contained in:
Him188 2022-08-26 19:41:09 +08:00
parent 1691172667
commit eb89b6348d
23 changed files with 721 additions and 346 deletions

View File

@ -5691,14 +5691,17 @@ public final class net/mamoe/mirai/network/NoStandardInputForCaptchaException :
}
public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai/network/LoginFailedException {
public synthetic fun <init> (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun getCause ()Ljava/lang/Throwable;
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException {
public fun <init> (Ljava/lang/String;)V
}
public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/LoginFailedException {
public fun <init> (Ljava/lang/String;)V
}
public final class net/mamoe/mirai/network/WrongPasswordException : net/mamoe/mirai/network/LoginFailedException {
}
@ -5934,6 +5937,27 @@ public final class net/mamoe/mirai/utils/DeviceInfoKt {
public static final fun generateDeviceInfoData (Lnet/mamoe/mirai/utils/DeviceInfo;)[B
}
public abstract interface class net/mamoe/mirai/utils/DeviceVerificationRequests {
public abstract fun getFallback ()Lnet/mamoe/mirai/utils/DeviceVerificationRequests$FallbackRequest;
public abstract fun getPreferSms ()Z
public abstract fun getSms ()Lnet/mamoe/mirai/utils/DeviceVerificationRequests$SmsRequest;
}
public abstract interface class net/mamoe/mirai/utils/DeviceVerificationRequests$FallbackRequest {
public abstract fun getUrl ()Ljava/lang/String;
public abstract fun solved ()Lnet/mamoe/mirai/utils/DeviceVerificationResult;
}
public abstract interface class net/mamoe/mirai/utils/DeviceVerificationRequests$SmsRequest {
public abstract fun getCountryCode ()Ljava/lang/String;
public abstract fun getPhoneNumber ()Ljava/lang/String;
public abstract fun requestSms (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun solved (Ljava/lang/String;)Lnet/mamoe/mirai/utils/DeviceVerificationResult;
}
public abstract interface class net/mamoe/mirai/utils/DeviceVerificationResult {
}
public final class net/mamoe/mirai/utils/DirectoryLogger : net/mamoe/mirai/utils/SimpleLogger {
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/io/File;)V
@ -6122,6 +6146,7 @@ public abstract class net/mamoe/mirai/utils/LoginSolver {
public static final field Default Lnet/mamoe/mirai/utils/LoginSolver;
public fun <init> ()V
public fun isSliderCaptchaSupported ()Z
public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun onSolveSliderCaptcha (Lnet/mamoe/mirai/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun onSolveUnsafeDeviceLoginVerify (Lnet/mamoe/mirai/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

View File

@ -5691,14 +5691,17 @@ public final class net/mamoe/mirai/network/NoStandardInputForCaptchaException :
}
public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai/network/LoginFailedException {
public synthetic fun <init> (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun getCause ()Ljava/lang/Throwable;
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException {
public fun <init> (Ljava/lang/String;)V
}
public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/LoginFailedException {
public fun <init> (Ljava/lang/String;)V
}
public final class net/mamoe/mirai/network/WrongPasswordException : net/mamoe/mirai/network/LoginFailedException {
}
@ -5934,6 +5937,27 @@ public final class net/mamoe/mirai/utils/DeviceInfoKt {
public static final fun generateDeviceInfoData (Lnet/mamoe/mirai/utils/DeviceInfo;)[B
}
public abstract interface class net/mamoe/mirai/utils/DeviceVerificationRequests {
public abstract fun getFallback ()Lnet/mamoe/mirai/utils/DeviceVerificationRequests$FallbackRequest;
public abstract fun getPreferSms ()Z
public abstract fun getSms ()Lnet/mamoe/mirai/utils/DeviceVerificationRequests$SmsRequest;
}
public abstract interface class net/mamoe/mirai/utils/DeviceVerificationRequests$FallbackRequest {
public abstract fun getUrl ()Ljava/lang/String;
public abstract fun solved ()Lnet/mamoe/mirai/utils/DeviceVerificationResult;
}
public abstract interface class net/mamoe/mirai/utils/DeviceVerificationRequests$SmsRequest {
public abstract fun getCountryCode ()Ljava/lang/String;
public abstract fun getPhoneNumber ()Ljava/lang/String;
public abstract fun requestSms (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun solved (Ljava/lang/String;)Lnet/mamoe/mirai/utils/DeviceVerificationResult;
}
public abstract interface class net/mamoe/mirai/utils/DeviceVerificationResult {
}
public final class net/mamoe/mirai/utils/DirectoryLogger : net/mamoe/mirai/utils/SimpleLogger {
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/io/File;)V
@ -6122,6 +6146,7 @@ public abstract class net/mamoe/mirai/utils/LoginSolver {
public static final field Default Lnet/mamoe/mirai/utils/LoginSolver;
public fun <init> ()V
public fun isSliderCaptchaSupported ()Z
public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun onSolveSliderCaptcha (Lnet/mamoe/mirai/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun onSolveUnsafeDeviceLoginVerify (Lnet/mamoe/mirai/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
@ -6486,6 +6511,7 @@ public final class net/mamoe/mirai/utils/StandardCharImageLoginSolver : net/mamo
public static final fun createBlocking (Lkotlin/jvm/functions/Function0;)Lnet/mamoe/mirai/utils/StandardCharImageLoginSolver;
public static final fun createBlocking (Lkotlin/jvm/functions/Function0;Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/StandardCharImageLoginSolver;
public fun isSliderCaptchaSupported ()Z
public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun onSolveSliderCaptcha (Lnet/mamoe/mirai/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun onSolveUnsafeDeviceLoginVerify (Lnet/mamoe/mirai/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

View File

@ -10,69 +10,7 @@
package net.mamoe.mirai.utils
import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.LoginSolver.Companion.Default
/**
* 验证码, 设备锁解决器
*
* @see Default
* @see BotConfiguration.loginSolver
*/
public actual abstract class LoginSolver public actual constructor() {
/**
* 处理图片验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String?
/**
* `true` 表示支持滑动验证码, 遇到滑动验证码时 mirai 会请求 [onSolveSliderCaptcha].
* 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败
*/
public actual open val isSliderCaptchaSupported: Boolean
get() = System.getProperty("mirai.slider.captcha.supported") != null
/**
* 处理滑动验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
* @return 验证码解决成功后获得的 ticket.
*/
public actual abstract suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String?
/**
* 处理不安全设备验证.
*
* 返回值保留给将来使用. 目前在处理完成后返回任意内容 (包含 `null`) 均视为处理成功.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止.
*
* @return 任意内容. 返回值保留以供未来更新.
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String?
public actual companion object {
/**
* 当前平台默认的 [LoginSolver]. Android 端没有默认验证码实现, [Default] 总为 `null`.
*/
@JvmField
public actual val Default: LoginSolver? = null
@Suppress("unused")
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
@DeprecatedSinceMirai(hiddenSince = "2.0") // maybe 2.0
public actual fun getDefault(): LoginSolver = Default
?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
}
}
internal actual object PlatformLoginSolverImplementations {
actual val isSliderCaptchaSupported: Boolean by lazy { System.getProperty("mirai.slider.captcha.supported") != null }
actual val default: LoginSolver? get() = null
}

View File

@ -12,7 +12,7 @@
package net.mamoe.mirai.network
import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.MiraiInternalApi
@ -47,8 +47,11 @@ public class NoServerAvailableException @MiraiInternalApi constructor(
/**
* 服务器要求稍后重试
*/
public class RetryLaterException @MiraiInternalApi constructor(override val cause: Throwable? = null) :
LoginFailedException(false, "server requests retrial later")
public class RetryLaterException @MiraiInternalApi constructor(
message: String?,
cause: Throwable? = null,
killBot: Boolean = false
) : LoginFailedException(killBot, message, cause)
/**
* 无标准输入或 Kotlin 不支持此输入.
@ -58,10 +61,10 @@ public class NoStandardInputForCaptchaException @MiraiInternalApi constructor(
) : LoginFailedException(true, "no standard input for captcha")
/**
* 需要短信验证时抛出. mirai 目前还不支持短信验证.
* 需要强制短信验证, 且当前 [LoginSolver] 不支持时抛出.
* @since 2.13
*/
@MiraiExperimentalApi("Will be removed when SMS login is supported")
public class UnsupportedSMSLoginException(message: String?) : LoginFailedException(true, message)
public class UnsupportedSmsLoginException(message: String?) : LoginFailedException(true, message)
/**
* 无法完成滑块验证

View File

@ -7,12 +7,18 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:JvmName("LoginSolver_common")
package net.mamoe.mirai.utils
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.network.RetryLaterException
import net.mamoe.mirai.network.UnsupportedSmsLoginException
import net.mamoe.mirai.utils.LoginSolver.Companion.Default
import kotlin.jvm.JvmField
import kotlin.jvm.JvmName
/**
* 验证码, 设备锁解决器
@ -20,12 +26,18 @@ import kotlin.jvm.JvmField
* @see Default
* @see BotConfiguration.loginSolver
*/
public expect abstract class LoginSolver() {
public abstract class LoginSolver {
/**
* 处理图片验证码.
* 处理图片验证码, 返回图片验证码内容.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* ## 异常类型
*
* 抛出一个 [LoginFailedException] 以正常地终止登录, 并可建议系统进行重连或停止 bot (通过 [LoginFailedException.killBot]).
* 例如抛出 [RetryLaterException] 可让 bot 重新进行一次登录.
*
* 抛出任意其他 [Throwable] 将视为验证码解决器的自身错误.
*
* @throws LoginFailedException
*/
@ -35,28 +47,72 @@ public expect abstract class LoginSolver() {
* `true` 表示支持滑动验证码, 遇到滑动验证码时 mirai 会请求 [onSolveSliderCaptcha].
* 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败
*/
public open val isSliderCaptchaSupported: Boolean
public open val isSliderCaptchaSupported: Boolean get() = PlatformLoginSolverImplementations.isSliderCaptchaSupported
/**
* 处理滑动验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* ## 异常类型
*
* 抛出一个 [LoginFailedException] 以正常地终止登录, 并可建议系统进行重连或停止 bot (通过 [LoginFailedException.killBot]).
* 例如抛出 [RetryLaterException] 可让 bot 重新进行一次登录.
*
* 抛出任意其他 [Throwable] 将视为验证码解决器的自身错误.
*
* @throws LoginFailedException
* @return 验证码解决成功后获得的 ticket.
*/
public abstract suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String?
/**
* 处理设备验证.
*
* ## 异常类型
*
* 抛出一个 [LoginFailedException] 以正常地终止登录, 并可建议系统进行重连或停止 bot (通过 [LoginFailedException.killBot]).
* 例如抛出 [RetryLaterException] 可让 bot 重新进行一次登录.
*
* 抛出任意其他 [Throwable] 将视为验证码解决器的自身错误.
*
* @since 验证结果, 可通过解决 [DeviceVerificationRequests] 获得.
* @throws LoginFailedException
* @since 2.13
*/
public open suspend fun onSolveDeviceVerification(
bot: Bot,
requests: DeviceVerificationRequests,
): DeviceVerificationResult {
requests.fallback?.let { fallback ->
@Suppress("DEPRECATION")
(onSolveUnsafeDeviceLoginVerify(bot, fallback.url))
return fallback.solved()
}
throw UnsupportedSmsLoginException("This login session requires SMS verification, but current LoginSolver($this) does not support it.")
}
/**
* 处理不安全设备验证.
*
* 返回值保留给将来使用. 目前在处理完成后返回任意内容 (包含 `null`) 均视为处理成功.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止.
*
* ## 异常类型
*
* 抛出一个 [LoginFailedException] 以正常地终止登录, 并可建议系统进行重连或停止 bot (通过 [LoginFailedException.killBot]).
* 例如抛出 [RetryLaterException] 可让 bot 重新进行一次登录.
*
* 抛出任意其他 [Throwable] 将视为验证码解决器的自身错误.
*
* @return 任意内容. 返回值保留以供未来更新.
* @throws LoginFailedException
*/
@Deprecated(
"Please use onSolveDeviceVerification instead",
level = DeprecationLevel.WARNING,
replaceWith = ReplaceWith("onSolveDeviceVerification(bot, url, null)")
) // softly
@DeprecatedSinceMirai(warningSince = "2.13") // for hidden
public abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String?
public companion object {
@ -72,11 +128,103 @@ public expect abstract class LoginSolver() {
* @return `SwingSolver` `StandardCharImageLoginSolver` `null`
*/
@JvmField
public val Default: LoginSolver?
public val Default: LoginSolver? = PlatformLoginSolverImplementations.default
@Suppress("unused")
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
public fun getDefault(): LoginSolver
public fun getDefault(): LoginSolver = Default
?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
}
}
internal expect object PlatformLoginSolverImplementations {
val isSliderCaptchaSupported: Boolean
val default: LoginSolver?
}
/**
* 属性 [sms] 为短信验证码验证方式, [fallback] 为其他验证方式.
* 两个属性至少有一个不为 `null`, 在不为 `null` 时表示支持该验证方式. 可任意选用偏好的验证方式.
*
* @since 2.13
*/
@NotStableForInheritance
public interface DeviceVerificationRequests {
/**
* 短信验证码方式. 在不为 `null` 时表示支持该验证方式.
*/
public val sms: SmsRequest?
/**
* 其他验证方式. 在不为 `null` 时表示支持该验证方式.
*/
public val fallback: FallbackRequest?
/**
* 服务器要求使用短信验证码. 此时可能仍可以尝试 [fallback].
*/
public val preferSms: Boolean
/**
* 服务器要求短信验证时提供的账号绑定的手机信息. 使用 [requestSms] 来请求发送验证码
*
* @since 2.13
* @see LoginSolver.onSolveDeviceVerification
*/
@NotStableForInheritance
public interface SmsRequest {
/**
* 手机号归属国家代码, 如中国为 86.
* 在获取失败时会返回 `null`但通常会获取到
*/
public val countryCode: String?
/**
* 手机号码, 部分数字会被隐藏, 示例: `123*******1`.
* 在获取失败时会返回 `null`, 但通常会获取到
*/
public val phoneNumber: String?
/**
* 请求服务器发送短信到验证手机号
*
* @throws RetryLaterException 当请求过于频繁, 服务器拒绝请求时抛出
*/
@JvmBlockingBridge
public suspend fun requestSms()
/**
* 通知此请求已被解决. 获取 [DeviceVerificationResult] 用于返回 [LoginSolver.onSolveDeviceVerification].
*/
public fun solved(code: String): DeviceVerificationResult
}
}
/**
* 其他验证方式.
*
* @since 2.13
* @see LoginSolver.onSolveDeviceVerification
*/
@NotStableForInheritance
public interface FallbackRequest {
/**
* HTTP URL. 可能需要在 QQ 浏览器中打开并人工操作.
*/
public val url: String
/**
* 通知此请求已被解决. 获取 [DeviceVerificationResult] 用于返回 [LoginSolver.onSolveDeviceVerification].
*/
public fun solved(): DeviceVerificationResult
}
}
/**
* 设备验证的验证结果. 请不要自行实现此接口, 而是通过解决 [DeviceVerificationRequests] 中的其中一种验证获得.
*
* @since 2.13
* @see LoginSolver.onSolveDeviceVerification
*/
@NotStableForInheritance
public interface DeviceVerificationResult

View File

@ -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
*/
@ -12,15 +12,14 @@ package net.mamoe.mirai.utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import net.mamoe.mirai.Bot
import net.mamoe.mirai.internal.utils.SeleniumLoginSolver
import net.mamoe.mirai.internal.utils.isSliderCaptchaSupportKind
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.network.NoStandardInputForCaptchaException
import net.mamoe.mirai.utils.LoginSolver.Companion.Default
import net.mamoe.mirai.utils.StandardCharImageLoginSolver.Companion.createBlocking
import java.awt.Image
import java.awt.image.BufferedImage
@ -29,67 +28,10 @@ import javax.imageio.ImageIO
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* 验证码, 设备锁解决器
*
* @see Default
* @see BotConfiguration.loginSolver
*/
public actual abstract class LoginSolver public actual constructor() {
/**
* 处理图片验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String?
/**
* `true` 表示支持滑动验证码, 遇到滑动验证码时 mirai 会请求 [onSolveSliderCaptcha].
* 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败
*/
public actual open val isSliderCaptchaSupported: Boolean
get() = isSliderCaptchaSupportKind ?: true
/**
* 处理滑动验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
* @return 验证码解决成功后获得的 ticket.
*/
public actual abstract suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String?
/**
* 处理不安全设备验证.
*
* 返回值保留给将来使用. 目前在处理完成后返回任意内容 (包含 `null`) 均视为处理成功.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止.
*
* @return 任意内容. 返回值保留以供未来更新.
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String?
public actual companion object {
/**
* 当前平台默认的 [LoginSolver]
*
* 检测策略:
* 1. 检测 `android.util.Log`, 如果存在, 返回 `null`.
* 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 [StandardCharImageLoginSolver]
* 3. 检测 JVM 桌面环境, 若支持, 返回 [SwingSolver]
* 4. 返回 [StandardCharImageLoginSolver]
*
* @return [SwingSolver] [StandardCharImageLoginSolver] `null`
*/
@JvmField
public actual val Default: LoginSolver? = when (WindowHelperJvm.platformKind) {
internal actual object PlatformLoginSolverImplementations {
actual val isSliderCaptchaSupported: Boolean get() = isSliderCaptchaSupportKind ?: true
actual val default: LoginSolver? by lazy {
when (WindowHelperJvm.platformKind) {
WindowHelperJvm.PlatformKind.ANDROID -> null
WindowHelperJvm.PlatformKind.SWING -> {
when (isSliderCaptchaSupportKind) {
@ -99,16 +41,9 @@ public actual abstract class LoginSolver public actual constructor() {
}
WindowHelperJvm.PlatformKind.CLI -> StandardCharImageLoginSolver()
}
@Suppress("unused")
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
public actual fun getDefault(): LoginSolver = Default
?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
}
}
/**
* CLI 环境 [LoginSolver]. 将验证码图片转为字符画并通过 `output` 输出, [input] 获取用户输入.
*
@ -136,8 +71,7 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor(
override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? = loginSolverLock.withLock {
val logger = loggerSupplier(bot)
@Suppress("BlockingMethodInNonBlockingContext")
(withContext(Dispatchers.IO) {
runInterruptible(Dispatchers.IO) {
val tempFile: File = File.createTempFile("tmp", ".png").apply { deleteOnExit() }
tempFile.createNewFile()
logger.info { "[PicCaptcha] 需要图片验证码登录, 验证码为 4 字母" }
@ -165,7 +99,7 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor(
logger.warning("[PicCaptcha] Failed to create char-image. Please see the file.", throwable)
}
}
})
}
logger.info { "[PicCaptcha] 请输入 4 位字母验证码. 若要更换验证码, 请直接回车" }
logger.info { "[PicCaptcha] Please type 4-letter captcha. Press Enter directly to refresh." }
return input().takeUnless { it.isEmpty() || it.length != 4 }.also {
@ -211,13 +145,69 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor(
}
}
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String = loginSolverLock.withLock {
@Suppress("DuplicatedCode")
override suspend fun onSolveDeviceVerification(
bot: Bot, requests: DeviceVerificationRequests
): DeviceVerificationResult {
val logger = loggerSupplier(bot)
logger.info { "[UnsafeLogin] 当前登录环境不安全,服务器要求账户认证。请在 QQ 浏览器打开 $url 并完成验证后输入任意字符。" }
logger.info { "[UnsafeLogin] Account verification required by the server. Please open $url in QQ browser and complete challenge, then type anything here to submit." }
return input().also {
logger.info { "[UnsafeLogin] 正在提交中..." }
logger.info { "[UnsafeLogin] Submitting..." }
requests.sms?.let { req ->
solveSms(logger, req)?.let { return it }
}
requests.fallback?.let { fallback ->
solveFallback(logger, fallback.url)
return fallback.solved()
}
error("User rejected SMS login while fallback login method not available.")
}
private suspend fun solveSms(
logger: MiraiLogger, request: DeviceVerificationRequests.SmsRequest
): DeviceVerificationResult? = loginSolverLock.withLock {
val countryCode = request.countryCode
val phoneNumber = request.phoneNumber
if (countryCode != null && phoneNumber != null) {
logger.info("一条短信验证码将发送到你的手机 (+$countryCode) $phoneNumber. 运营商可能会收取正常短信费用, 是否继续? 输入 yes 继续, 输入其他终止并尝试其他验证方式.")
logger.info(
"A verification code will be send to your phone (+$countryCode) $phoneNumber, which may be charged normally, do you wish to continue? Type yes to continue, type others to cancel and try other methods."
)
} else {
logger.info("一条短信验证码将发送到你的手机 (无法获取到手机号码). 运营商可能会收取正常短信费用, 是否继续? 输入 yes 继续, 输入其他终止并尝试其他验证方式.")
logger.info(
"A verification code will be send to your phone (failed to get phone number), " + "which may be charged normally by your carrier, " + "do you wish to continue? Type yes to continue, type others to cancel and try other methods."
)
}
val answer = input().trim()
return if (answer.equals("yes", ignoreCase = true)) {
logger.info("Attempting SMS verification.")
request.requestSms()
logger.info("Please enter code: ")
val code = input().trim()
logger.info("Continuing with code '$code'.")
request.solved(code)
} else {
logger.info("Cancelled.")
null
}
}
@Deprecated(
"Please use onSolveDeviceVerification instead",
replaceWith = ReplaceWith("onSolveDeviceVerification(bot, url, null)"),
level = DeprecationLevel.WARNING
)
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String =
solveFallback(loggerSupplier(bot), url)
private suspend fun solveFallback(
logger: MiraiLogger, url: String
): String {
return loginSolverLock.withLock {
logger.info { "[UnsafeLogin] 当前登录环境不安全,服务器要求账户认证。请在 QQ 浏览器打开 $url 并完成验证后输入任意字符。" }
logger.info { "[UnsafeLogin] Account verification required by the server. Please open $url in QQ browser and complete challenge, then type anything here to submit." }
input().also {
logger.info { "[UnsafeLogin] 正在提交中..." }
logger.info { "[UnsafeLogin] Submitting..." }
}
}
}
@ -299,4 +289,4 @@ private fun BufferedImage.createCharImg(outputWidth: Int = 100, ignoreRate: Doub
append(line.substring(minXPos, maxXPos)).append("\n")
}
}
}
}

View File

@ -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
*/
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
@ -24,6 +24,8 @@ import java.awt.image.BufferedImage
import java.net.URI
import javax.imageio.ImageIO
import javax.swing.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
@MiraiExperimentalApi
public object SwingSolver : LoginSolver() {
@ -98,8 +100,106 @@ public object SwingSolver : LoginSolver() {
return@coroutineScope solver.openAndWait().takeIf { it.isNotEmpty() }
}
@Suppress("DuplicatedCode")
override suspend fun onSolveDeviceVerification(
bot: Bot,
requests: DeviceVerificationRequests
): DeviceVerificationResult {
requests.sms?.let { req ->
solveSms(bot, req)?.let { return it }
}
requests.fallback?.let { fallback ->
solveFallback(bot, fallback.url)
return fallback.solved()
}
error("User rejected SMS login while fallback login method not available.")
}
@Deprecated(
"Please use onSolveDeviceVerification instead",
replaceWith = ReplaceWith("onSolveDeviceVerification(bot, url, null)"),
level = DeprecationLevel.WARNING
)
public override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String {
val title = "Mirai UnsafeDeviceLoginVerify(${bot.id})"
return solveFallback(bot, url)
}
private suspend fun solveSms(bot: Bot, request: DeviceVerificationRequests.SmsRequest): DeviceVerificationResult? =
coroutineScope {
val smsRequester = object {
var lastRequested = 0L
fun requestSms(parentComponent: Component) = launch {
// oh, so shit code
val diff = (System.currentTimeMillis() - lastRequested).milliseconds
if (diff < 1.minutes) {
parentComponent.createTip(
"""请求过于频繁, 请在 ${1.minutes - diff} 秒后再试""".trimIndent()
).openAndWait()
return@launch
}
lastRequested = System.currentTimeMillis()
kotlin.runCatching {
request.requestSms()
}.fold(
onSuccess = {
parentComponent.createTip(
"""发送验证码成功, 请注意查收. 若未收到, 可在一分钟后重试.""".trimIndent()
).openAndWait()
},
onFailure = {
parentComponent.createTip(
"<html>发送验证码失败.<br/><br/>${
it.stackTraceToString().replace("\n", "<br/>").replace("\r", "")
}"
).openAndWait()
}
)
}
}
val title = "Mirai Device Verification (${bot.id})"
val phoneNumber = request.phoneNumber
val countryCode = request.countryCode
val phoneNumberTip = if (phoneNumber != null && countryCode != null)
"""(+$countryCode) $phoneNumber"""
else "(无法获取到手机号码)"
val code = SwingLoginSolver(
title, "",
arrayOf(
"",
JButton("发送验证码").onClick {
smsRequester.requestSms(this)
},
"",
JLabel("验证码 (输入完成后按回车):")
),
hiddenInput = false,
topComponent = JLabel(
"""
<html>
需要进行短信验证码验证<br>
一条短信验证码将发送到你的手机 $phoneNumberTip<br>
运营商可能会收取正常短信费用<br>
""".trimIndent()
)
).openAndWait().trim().ifEmpty { return@coroutineScope null }
request.solved(code)
}
private fun Component.createTip(tip: String) = SwingLoginSolver(
"提示", "",
arrayOf("", JLabel()),
hiddenInput = true,
topComponent = JLabel(tip),
parentComponent = this,
)
private suspend fun solveFallback(bot: Bot, url: String): String {
val title = "Mirai Device Verification (${bot.id})"
return SwingLoginSolver(
title, "",
arrayOf(
@ -109,12 +209,12 @@ public object SwingSolver : LoginSolver() {
hiddenInput = true,
topComponent = JLabel(
"""
<html>
需要进行账户安全认证<br>
该账户有设备锁/不常用登录地点/不常用设备登录的问题<br>
请在<b>手机 QQ</b> 打开下面链接
成功后请关闭该窗口
""".trimIndent()
<html>
需要进行账户安全认证<br>
该账户有设备锁/不常用登录地点/不常用设备登录的问题<br>
请在<b>手机 QQ</b> 打开下面链接
成功后请关闭该窗口
""".trimIndent()
)
).openAndWait()
}
@ -124,7 +224,6 @@ public object SwingSolver : LoginSolver() {
// 隔离类代码
// 在 jvm 中, 使用 WindowHelperJvm 不会加载 SwingSolverKt
// 不会触发各种 NoDefClassError
@Suppress("DEPRECATION")
internal object WindowHelperJvm {
enum class PlatformKind {
ANDROID,

View File

@ -9,77 +9,7 @@
package net.mamoe.mirai.utils
import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.LoginSolver.Companion.Default
/**
* 验证码, 设备锁解决器
*
* @see Default
* @see BotConfiguration.loginSolver
*/
public actual abstract class LoginSolver actual constructor() {
/**
* 处理图片验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String?
/**
* `true` 表示支持滑动验证码, 遇到滑动验证码时 mirai 会请求 [onSolveSliderCaptcha].
* 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败
*/
public actual open val isSliderCaptchaSupported: Boolean
get() = false
/**
* 处理滑动验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
* @return 验证码解决成功后获得的 ticket.
*/
public actual abstract suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String?
/**
* 处理不安全设备验证.
*
* 返回值保留给将来使用. 目前在处理完成后返回任意内容 (包含 `null`) 均视为处理成功.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止.
*
* @return 任意内容. 返回值保留以供未来更新.
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolveUnsafeDeviceLoginVerify(
bot: Bot,
url: String
): String?
public actual companion object {
/**
* 当前平台默认的 [LoginSolver]
*
* 检测策略:
* 1. 若是 `mirai-core-api-android` `android.util.Log` 存在, 返回 `null`.
* 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 `StandardCharImageLoginSolver`
* 3. 检测 JVM 桌面环境, 若支持, 返回 `SwingSolver`
* 4. 返回 `StandardCharImageLoginSolver`
*
* @return `SwingSolver` `StandardCharImageLoginSolver` `null`
*/
public actual val Default: LoginSolver?
get() = null
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
@Suppress("unused")
public actual fun getDefault(): LoginSolver = Default
?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
}
internal actual object PlatformLoginSolverImplementations {
actual val isSliderCaptchaSupported: Boolean get() = false
actual val default: LoginSolver? get() = null
}

View File

@ -155,6 +155,7 @@ internal open class QQAndroidClient(
var t530: ByteArray? = null
var t528: ByteArray? = null
var t174: ByteArray? = null
/**
* t186

View File

@ -19,14 +19,17 @@ import net.mamoe.mirai.internal.network.component.ComponentKey
import net.mamoe.mirai.internal.network.handler.NetworkHandler
import net.mamoe.mirai.internal.network.handler.selector.NetworkException
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
import net.mamoe.mirai.internal.network.protocol.packet.login.DeviceVerificationResultImpl
import net.mamoe.mirai.internal.network.protocol.packet.login.SmsDeviceVerificationResult
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.internal.network.protocol.packet.login.UrlDeviceVerificationResult
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.internal.network.protocol.packet.login.wtlogin.*
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.network.RetryLaterException
import net.mamoe.mirai.network.UnsupportedSliderCaptchaException
import net.mamoe.mirai.network.WrongPasswordException
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.info
@ -245,9 +248,19 @@ internal class SsoProcessorImpl(
response = WtLogin20(client).sendAndExpect()
}
is LoginPacketResponse.UnsafeLogin -> {
loginSolverNotNull().onSolveUnsafeDeviceLoginVerify(bot, response.url)
response = WtLogin9(client, allowSlider).sendAndExpect()
is LoginPacketResponse.VerificationNeeded -> {
val result = loginSolverNotNull().onSolveDeviceVerification(
bot, response.requests
)
check(result is DeviceVerificationResultImpl)
response = when (result) {
is UrlDeviceVerificationResult -> {
WtLogin9(client, allowSlider).sendAndExpect()
}
is SmsDeviceVerificationResult -> {
WtLogin7(client, result.token, result.code).sendAndExpect()
}
}
}
is Captcha.Picture -> {
@ -290,21 +303,22 @@ internal class SsoProcessorImpl(
is LoginPacketResponse.Error -> {
if (response.message.contains("0x9a")) { //Error(title=登录失败, message=请你稍后重试。(0x9a), errorInfo=)
collectThrow(RetryLaterException(IllegalStateException("Login failed: $response")))
collectThrow(RetryLaterException("Login failed: $response"))
}
val msg = response.toString()
collectThrow(WrongPasswordException(buildString(capacity = msg.length) {
append(msg)
if (msg.contains("当前上网环境异常")) { // Error(title=禁止登录, message=当前上网环境异常,请更换网络环境或在常用设备上登录或稍后再试。, errorInfo=)
append(", tips=若频繁出现, 请尝试开启设备锁")
append(", mirai 提示: 若频繁出现, 请尝试开启设备锁")
}
if (msg.contains("当前登录存在安全风险")) { // Error(title=禁止登录, message=当前上网环境异常,请更换网络环境或在常用设备上登录或稍后再试。, errorInfo=)
append(", mirai 提示: 这可能是尝试登录次数过多导致的, 请等待一段时间后再试")
}
}))
}
is LoginPacketResponse.SMSVerifyCodeNeeded -> {
val message = "SMS required: $response, which isn't yet supported"
logger.error(message)
collectThrow(UnsupportedSMSLoginException(message))
is LoginPacketResponse.SmsRequestSuccess -> {
error("Unexpected response: $response")
}
}
}

View File

@ -27,20 +27,20 @@ import kotlin.random.Random
@kotlin.Suppress("unused")
internal class OutgoingPacketWithRespType<R : Packet?> constructor(
name: String?,
remark: String?,
commandName: String,
sequenceId: Int,
delegate: ByteReadPacket
) : OutgoingPacket(name, commandName, sequenceId, delegate)
) : OutgoingPacket(remark, commandName, sequenceId, delegate)
internal open class OutgoingPacket constructor(
name: String?,
remark: String?,
val commandName: String,
val sequenceId: Int,
delegate: ByteReadPacket
) {
val delegate = delegate.readBytes()
val displayName: String = if (name == null) commandName else "$commandName($name)"
val displayName: String = if (remark == null) commandName else "$commandName($remark)"
}
internal class IncomingPacket private constructor(
@ -78,7 +78,7 @@ internal class IncomingPacket private constructor(
internal inline fun <R : Packet?> OutgoingPacketFactory<R>.buildOutgoingUniPacket(
client: QQAndroidClient,
bodyType: Byte = 1, // 1: PB?
name: String? = this.commandName,
remark: String? = this.commandName,
commandName: String = this.commandName,
key: ByteArray = client.wLoginSigInfo.d2Key,
extraData: ByteReadPacket = BRP_STUB,
@ -86,7 +86,7 @@ internal inline fun <R : Packet?> OutgoingPacketFactory<R>.buildOutgoingUniPacke
body: BytePacketBuilder.(sequenceId: Int) -> Unit
): OutgoingPacketWithRespType<R> {
return OutgoingPacketWithRespType(name, commandName, sequenceId, buildPacket {
return OutgoingPacketWithRespType(remark, commandName, sequenceId, buildPacket {
writeIntLVPacket(lengthOffset = { it + 4 }) {
writeInt(0x0B)
writeByte(bodyType)
@ -173,14 +173,14 @@ internal inline fun <R : Packet?> OutgoingPacketFactory<R>.buildLoginOutgoingPac
client: QQAndroidClient,
bodyType: Byte,
extraData: ByteArray = EMPTY_BYTE_ARRAY,
name: String? = null,
remark: String? = null,
commandName: String = this.commandName,
key: ByteArray = KEY_16_ZEROS,
body: BytePacketBuilder.(sequenceId: Int) -> Unit
): OutgoingPacketWithRespType<R> {
val sequenceId: Int = client.nextSsoSequenceId()
return OutgoingPacketWithRespType(name, commandName, sequenceId, buildPacket {
return OutgoingPacketWithRespType(remark, commandName, sequenceId, buildPacket {
writeIntLVPacket(lengthOffset = { it + 4 }) {
writeInt(0x00_00_00_0A)
writeByte(bodyType)

View File

@ -285,13 +285,14 @@ internal fun BytePacketBuilder.t17a(
}
}
internal fun BytePacketBuilder.t197(
value: ByteArray
) {
internal fun BytePacketBuilder.t197() {
writeShort(0x197)
writeShortLVPacket {
writeFully(value)
}
writeFully(byteArrayOf(0, 1, 0))
}
internal fun BytePacketBuilder.t198() {
writeShort(0x198)
writeFully(byteArrayOf(0, 1, 0))
}
internal fun BytePacketBuilder.t19e(

View File

@ -55,7 +55,7 @@ internal class PbMessageSvc {
return buildOutgoingUniPacket(
client,
name = "PbMsgWithDraw(" +
remark = "PbMsgWithDraw(" +
"group=$groupCode, " +
"seq=${messageSequenceId.joinToString(separator = ",")}, " +
"rand=${messageRandom.joinToString(separator = ",")}" +
@ -98,7 +98,7 @@ internal class PbMessageSvc {
return buildOutgoingUniPacket(
client,
name = "PbMsgWithDraw(" +
remark = "PbMsgWithDraw(" +
"groupTemp=$toUin, " +
"seq=${messageSequenceId.joinToString(separator = ",")}, " +
"rand=${messageRandom.joinToString(separator = ",")}, " +
@ -147,7 +147,7 @@ internal class PbMessageSvc {
return buildOutgoingUniPacket(
client,
name = "PbMsgWithDraw(" +
remark = "PbMsgWithDraw(" +
"friend=$toUin, " +
"seq=${messageSequenceId.joinToString(separator = ",")}, " +
"rand=${messageRandom.joinToString(separator = ",")}, " +

View File

@ -200,7 +200,7 @@ internal class StatSvc {
bodyType = 1,
extraData = client.wLoginSigInfo.d2.data,
key = client.wLoginSigInfo.d2Key,
name = name,
remark = name,
) { sequenceId ->
writeSsoPacket(
client, subAppId = client.subAppId, commandName = commandName,

View File

@ -14,56 +14,98 @@ import io.ktor.utils.io.core.*
import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.AbstractEvent
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.internal.AbstractBot
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.*
import net.mamoe.mirai.internal.network.DebuggingProperties.SHOW_TLV_MAP_ON_LOGIN_SUCCESS
import net.mamoe.mirai.internal.network.handler.logger
import net.mamoe.mirai.internal.network.protocol.packet.*
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin8
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLoginExt
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.analysisTlv0x531
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.orEmpty
import net.mamoe.mirai.internal.utils.crypto.TEA
import net.mamoe.mirai.internal.utils.printStructure
import net.mamoe.mirai.network.RetryLaterException
import net.mamoe.mirai.network.WrongPasswordException
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.structureToString
internal class SmsVerifyInfo(
override val countryCode: String?, // 86
override val phoneNumber: String?, // 123*******1
private val token: ByteArray, // t174
private val bot: AbstractBot,
) : DeviceVerificationRequests.SmsRequest {
override suspend fun requestSms() {
val result = try {
bot.network.sendAndExpect(WtLogin8(bot.client, token))
} catch (e: Exception) {
bot.logger.warning("Exception while requesting SMS.", e)
throw e
}
if (result !is WtLogin.Login.LoginPacketResponse.SmsRequestSuccess) {
bot.logger.warning("Failed requestSms, result = $result")
if (result is WtLogin.Login.LoginPacketResponse.Error) {
when (result.code) {
161, 162 -> {
// 今日操作次数过多,请等待一天后再试。
throw RetryLaterException(
result.message,
null,
killBot = true
)
}
}
}
throw WrongPasswordException("Failed requestSms, result = $result")
}
}
override fun solved(code: String): DeviceVerificationResult {
return SmsDeviceVerificationResult(code, token)
}
override fun toString(): String {
return "SmsVerifyInfo(+$countryCode $phoneNumber)"
}
}
internal class FallbackRequestImpl(override val url: String) : DeviceVerificationRequests.FallbackRequest {
override fun solved(): DeviceVerificationResult {
return UrlDeviceVerificationResult
}
override fun toString(): String {
return "FallbackRequestImpl($url)"
}
}
internal sealed interface DeviceVerificationResultImpl : DeviceVerificationResult
internal object UrlDeviceVerificationResult : DeviceVerificationResultImpl {
override fun toString(): String {
return "UrlVerificationResult"
}
}
internal class SmsDeviceVerificationResult(
val code: String,
val token: ByteArray, // t174
) : DeviceVerificationResultImpl {
override fun toString(): String {
return "SmsVerificationResult(code=$code, token=${token.toUHexString("")})"
}
}
internal class WtLogin {
/**
* OicqRequest
*/
@Suppress("FunctionName")
internal object Login : OutgoingPacketFactory<Login.LoginPacketResponse>("wtlogin.login"), WtLoginExt {
/**
* 提交 SMS
*/
object SubCommand7 {
operator fun invoke(
client: QQAndroidClient
) = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(
client,
client.subAppId,
commandName,
sequenceId = sequenceId,
unknownHex = "01 00 00 00 00 00 00 00 00 00 01 00"
) {
writeOicqRequestPacket(client, commandId = 0x0810) {
writeShort(8) // subCommand
writeShort(6) // count of TLVs, probably ignored by server?
t8(2052)
t104(client.t104)
t116(client.miscBitMap, client.subSigMap)
t174(EMPTY_BYTE_ARRAY)
t17a(9)
t197(byteArrayOf(0.toByte()))
//t401(md5(client.device.guid + "12 34567890123456".toByteArray() + t402))
//t19e(0)//==tlv408
}
}
}
}
/**
* Check SMS Login
*/
@ -104,6 +146,10 @@ internal class WtLogin {
override fun toString(): String = "LoginPacketResponse.Success"
}
class SmsRequestSuccess(override val bot: Bot) : LoginPacketResponse() {
override fun toString(): String = "LoginPacketResponse.SmsRequestSuccess"
}
data class Error(
override val bot: Bot,
val code: Int,
@ -130,31 +176,25 @@ internal class WtLogin {
}
}
data class UnsafeLogin(
class VerificationNeeded(
override val bot: Bot,
val url: String,
) : LoginPacketResponse()
class SMSVerifyCodeNeeded(
override val bot: Bot,
val t402: ByteArray,
val t403: ByteArray,
val message: String?,
val requests: DeviceVerificationRequests,
) : LoginPacketResponse() {
override fun toString(): String {
return "LoginPacketResponse.SMSVerifyCodeNeeded(t402=${t402.toUHexString()}, t403=${t403.toUHexString()})"
}
override fun toString(): String =
"LoginPacketResponse.VerificationNeeded(requests=$requests)"
}
class DeviceLockLogin(
override val bot: Bot,
) : LoginPacketResponse() {
override fun toString(): String = "WtLogin.Login.LoginPacketResponse.DeviceLockLogin"
override fun toString(): String = "LoginPacketResponse.DeviceLockLogin"
}
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): LoginPacketResponse {
val subCommand = readUShort().toInt() // subCommand
val subCommand = readUShort() // subCommand
// println("subCommand=$subCommand")
val type = readUByte()
// println("type=$type")
@ -177,10 +217,23 @@ internal class WtLogin {
// writeFully(tlvMap[0x402] ?: EMPTY_BYTE_ARRAY)
// }.readBytes().md5()
// }
tlvMap[0x402]?.let { t402 ->
bot.client.dpwd = getRandomByteArray(16)
bot.client.t402 = t402
bot.run {
// client.dpwd = getRandomString(16).toByteArray()
client.G = (client.device.guid + client.dpwd + t402).md5()
}
}
return when (type.toInt()) {
0 -> onLoginSuccess(subCommand, tlvMap, bot)
0 -> onLoginSuccess(subCommand.toInt(), tlvMap, bot)
2 -> onSolveLoginCaptcha(tlvMap, bot)
160, 239 /*-96*/ -> onUnsafeDeviceLogin(tlvMap, bot)
160, 239 /*-96*/ -> onVerificationNeeded(subCommand.toShort(), tlvMap, bot)
// 40: blocked
// 161: 今日操作次数过多,请等待一天后再试。 (SMS)
// 162: 可能也是 SMS 太频繁
204 /*-52*/ -> onDevLockLogin(tlvMap, bot)
// 1, 15 -> onErrorMessage(tlvMap) ?: error("Cannot find error message")
else -> {
@ -190,22 +243,65 @@ internal class WtLogin {
}
}
private fun onDevLockLogin(
tlvMap: TlvMap,
bot: QQAndroidBot
): LoginPacketResponse.DeviceLockLogin {
bot.client.t104 = tlvMap.getOrFail(0x104)
bot.run {
// client.dpwd = getRandomString(16).toByteArray()
client.G = (client.device.guid + client.dpwd + tlvMap.getOrFail(0x402)).md5()
}
// println("403 " + tlvMap[0x403]?.toUHexString())
return LoginPacketResponse.DeviceLockLogin(bot)
}
private fun onUnsafeDeviceLogin(tlvMap: TlvMap, bot: QQAndroidBot): LoginPacketResponse.UnsafeLogin {
return LoginPacketResponse.UnsafeLogin(bot, tlvMap.getOrFail(0x204).decodeToString())
private fun onVerificationNeeded(subCommand: Short, tlvMap: TlvMap, bot: QQAndroidBot): LoginPacketResponse {
val t174 = tlvMap[0x174]
val t17b = tlvMap[0x17b]
val message = tlvMap[0x17e]?.decodeToString()
t174?.let {
bot.client.t174 = it
}
if (t174 != null || t17b != null) {
tlvMap[0x104]?.let {
// verify token
bot.client.t104 = it
}
}
tlvMap[0x403]?.let {
bot.client.randSeed = it
}
if (subCommand == WtLogin8.subCommand) {
// response of submit sms
// tlvMap 只有 0x17b 和 0x174
return LoginPacketResponse.SmsRequestSuccess(bot)
}
var countryCode: String? = null
var phoneNumber: String? = null
tlvMap[0x178]?.read {
// phone number
countryCode = readUShortLVString()
phoneNumber = readUShortLVString()
}
val url = tlvMap[0x204]?.decodeToString()
check(url != null || t174 != null) {
"Verification is needed but no method available."
}
return LoginPacketResponse.VerificationNeeded(
bot,
message = message,
requests = object : DeviceVerificationRequests {
override val sms: DeviceVerificationRequests.SmsRequest? =
t174?.let { SmsVerifyInfo(countryCode, phoneNumber, t174, bot) }
override val fallback: DeviceVerificationRequests.FallbackRequest? =
url?.let { FallbackRequestImpl(it) }
override val preferSms: Boolean = tlvMap[0x17b] != null
override fun toString(): String {
return "DeviceVerificationRequests(sms=$sms, preferSms=$preferSms, fallback=$fallback)"
}
},
)
}
private fun onSolveLoginCaptcha(tlvMap: TlvMap, bot: QQAndroidBot): LoginPacketResponse.Captcha {

View File

@ -27,7 +27,9 @@ internal object WtLogin10 : WtLoginExt {
client: QQAndroidClient,
subAppId: Long = 100,
mainSigMap: Int = client.mainSigMap
) = WtLogin.ExchangeEmp.buildLoginOutgoingPacket(client, bodyType = 2, key = ByteArray(16)) { sequenceId ->
) = WtLogin.ExchangeEmp.buildLoginOutgoingPacket(
client, bodyType = 2, key = ByteArray(16), remark = "10:fast-login"
) { sequenceId ->
writeSsoPacket(
client,
client.subAppId,

View File

@ -24,7 +24,9 @@ internal object WtLogin15 : WtLoginExt {
operator fun invoke(
client: QQAndroidClient,
) = WtLogin.ExchangeEmp.buildOutgoingUniPacket(client, bodyType = 2, key = ByteArray(16)) {
) = WtLogin.ExchangeEmp.buildOutgoingUniPacket(
client, bodyType = 2, key = ByteArray(16), remark = "15:refresh-keys"
) {
// writeSsoPacket(client, client.subAppId, WtLogin.ExchangeEmp.commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(
client,

View File

@ -22,7 +22,7 @@ internal object WtLogin2 : WtLoginExt {
fun SubmitSliderCaptcha(
client: QQAndroidClient,
ticket: String
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2, remark = "2:submit-slider") { sequenceId ->
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, commandId = 0x0810) {
writeShort(2) // subCommand
@ -39,7 +39,7 @@ internal object WtLogin2 : WtLoginExt {
client: QQAndroidClient,
captchaSign: ByteArray,
captchaAnswer: String
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2, remark = "2:submit-captcha") { sequenceId ->
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, commandId = 0x0810) {
writeShort(2) // subCommand

View File

@ -20,7 +20,7 @@ import net.mamoe.mirai.internal.network.subSigMap
internal object WtLogin20 : WtLoginExt {
operator fun invoke(
client: QQAndroidClient
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2, remark = "20:dev-lock") { sequenceId ->
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, commandId = 0x0810) {
writeShort(20) // subCommand

View File

@ -0,0 +1,48 @@
/*
* 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.protocol.packet.login.wtlogin
import io.ktor.utils.io.core.*
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.miscBitMap
import net.mamoe.mirai.internal.network.protocol.packet.*
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.internal.network.subAppId
import net.mamoe.mirai.internal.network.subSigMap
import net.mamoe.mirai.utils.DeviceVerificationRequests
/**
* Submit SMS.
* @see DeviceVerificationRequests.SmsRequest.requestSms
*/
internal object WtLogin7 : WtLoginExt {
operator fun invoke(
client: QQAndroidClient,
t174: ByteArray,
code: String
) = WtLogin.Login.buildLoginOutgoingPacket(
client, bodyType = 2, remark = "7:submit-sms"
) { sequenceId ->
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, commandId = 0x0810) {
writeShort(7) // subCommand
writeShort(7) // count of TLVs
t8(2052)
t104(client.t104)
t116(client.miscBitMap, client.subSigMap)
t174(client.t174 ?: t174)
t17c(code.encodeToByteArray())
t401(client.G)
t198()
}
}
}
}

View File

@ -0,0 +1,47 @@
/*
* 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.protocol.packet.login.wtlogin
import io.ktor.utils.io.core.*
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.miscBitMap
import net.mamoe.mirai.internal.network.protocol.packet.*
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.internal.network.subAppId
import net.mamoe.mirai.internal.network.subSigMap
import net.mamoe.mirai.utils.DeviceVerificationRequests
/**
* Request SMS.
* @see DeviceVerificationRequests.SmsRequest.requestSms
*/
internal object WtLogin8 : WtLoginExt {
val subCommand: Short = 8
operator fun invoke(
client: QQAndroidClient,
t174: ByteArray
) = WtLogin.Login.buildLoginOutgoingPacket(
client, bodyType = 2, remark = "8:request-sms"
) { sequenceId ->
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, commandId = 0x0810) {
writeShort(subCommand) // subCommand
writeShort(6) // count of TLVs
t8(2052)
t104(client.t104)
t116(client.miscBitMap, client.subSigMap)
t174(t174)
t17a(9)
t197()
}
}
}
}

View File

@ -20,7 +20,9 @@ internal object WtLogin9 : WtLoginExt {
operator fun invoke(
client: QQAndroidClient,
allowSlider: Boolean
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
) = WtLogin.Login.buildLoginOutgoingPacket(
client, bodyType = 2, remark = "9:password-login"
) { sequenceId ->
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, commandId = 0x0810) {
writeShort(9) // subCommand

View File

@ -38,6 +38,9 @@ internal inline fun WtLoginExt.analysisTlv0x531(
}
}
/**
* @see WtLogin
*/
internal interface WtLoginExt { // so as not to register to global extension
fun onErrorMessage(type: Int, tlvMap: TlvMap, bot: QQAndroidBot): WtLogin.Login.LoginPacketResponse.Error? {