mirror of
https://github.com/mamoe/mirai.git
synced 2025-04-14 23:20:49 +08:00
Android SMS login - Incomplete
This commit is contained in:
parent
767b945a10
commit
d8cd6625e5
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network
mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/network
mirai-core/src
commonMain/kotlin/net.mamoe.mirai/utils
jvmMain/kotlin/net/mamoe/mirai/utils
@ -33,37 +33,53 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
|
||||
launch(CoroutineName("Incoming Packet Receiver")) { processReceive() }
|
||||
|
||||
bot.logger.info("Trying login")
|
||||
when (val response: LoginPacket.LoginPacketResponse = LoginPacket.SubCommand9(bot.client).sendAndExpect()) {
|
||||
is UnsafeLogin -> {
|
||||
bot.logger.info("Login unsuccessful, device auth is needed")
|
||||
bot.logger.info("登陆失败, 原因为非常用设备登陆")
|
||||
bot.logger.info("Open the following URL in QQ browser and complete the verification")
|
||||
bot.logger.info("将下面这个链接在QQ浏览器中打开并完成认证后尝试再次登陆")
|
||||
bot.logger.info(response.url)
|
||||
return
|
||||
}
|
||||
var response: LoginPacket.LoginPacketResponse = LoginPacket.SubCommand9(bot.client).sendAndExpect()
|
||||
mainloop@ while (true) {
|
||||
when (response) {
|
||||
is UnsafeLogin -> {
|
||||
bot.logger.info("Login unsuccessful, device auth is needed")
|
||||
bot.logger.info("登陆失败, 原因为非常用设备登陆")
|
||||
bot.logger.info("Open the following URL in QQ browser and complete the verification")
|
||||
bot.logger.info("将下面这个链接在QQ浏览器中打开并完成认证后尝试再次登陆")
|
||||
bot.logger.info(response.url)
|
||||
return
|
||||
}
|
||||
|
||||
is Captcha -> when (response) {
|
||||
is Captcha.Picture -> {
|
||||
bot.logger.info("需要图片验证码")
|
||||
var result = bot.configuration.captchaSolver.invoke(bot, response.data)
|
||||
if (result === null || result.length != 4) {
|
||||
//refresh captcha
|
||||
result = "ABCD"
|
||||
is Captcha -> when (response) {
|
||||
is Captcha.Picture -> {
|
||||
bot.logger.info("需要图片验证码")
|
||||
var result = bot.configuration.loginSolver.onSolvePicCaptcha(bot, response.data)
|
||||
if (result === null || result.length != 4) {
|
||||
//refresh captcha
|
||||
result = "ABCD"
|
||||
}
|
||||
bot.logger.info("提交验证码")
|
||||
response = LoginPacket.SubCommand2(bot.client, response.sign, result).sendAndExpect()
|
||||
continue@mainloop
|
||||
}
|
||||
is Captcha.Slider -> {
|
||||
bot.logger.info("需要滑动验证码")
|
||||
TODO("滑动验证码")
|
||||
}
|
||||
bot.logger.info("提交验证码")
|
||||
val captchaResponse: LoginPacket.LoginPacketResponse =
|
||||
LoginPacket.SubCommand2(bot.client, response.sign, result).sendAndExpect()
|
||||
}
|
||||
is Captcha.Slider -> {
|
||||
bot.logger.info("需要滑动验证码")
|
||||
|
||||
is Error -> error(response.toString())
|
||||
|
||||
is SMSVerifyCodeNeeded -> {
|
||||
val result = bot.configuration.loginSolver.onGetPhoneNumber()
|
||||
response = LoginPacket.SubCommand7(
|
||||
bot.client,
|
||||
response.t174,
|
||||
response.t402,
|
||||
result
|
||||
).sendAndExpect()
|
||||
continue@mainloop
|
||||
}
|
||||
}
|
||||
|
||||
is Error -> error(response.toString())
|
||||
|
||||
is Success -> {
|
||||
bot.logger.info("Login successful")
|
||||
is Success -> {
|
||||
bot.logger.info("Login successful")
|
||||
break@mainloop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,6 +142,7 @@ fun BytePacketBuilder.t116(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun BytePacketBuilder.t100(
|
||||
appId: Long = 16,
|
||||
subAppId: Long = 537062845,
|
||||
@ -193,6 +194,44 @@ fun BytePacketBuilder.t104(
|
||||
}
|
||||
}
|
||||
|
||||
fun BytePacketBuilder.t174(
|
||||
t174Data: ByteArray
|
||||
) {
|
||||
writeShort(0x174)
|
||||
writeShortLVPacket {
|
||||
writeFully(t174Data)
|
||||
}
|
||||
}
|
||||
|
||||
fun BytePacketBuilder.t19e(
|
||||
value: Int = 0
|
||||
) {
|
||||
writeShort(0x19e)
|
||||
writeShortLVPacket {
|
||||
writeShort(1)
|
||||
writeByte(value.toByte())
|
||||
}
|
||||
}
|
||||
|
||||
fun BytePacketBuilder.t17c(
|
||||
t17cData: ByteArray
|
||||
) {
|
||||
writeShort(0x17c)
|
||||
writeShortLVPacket {
|
||||
writeShort(t17cData.size.toShort())
|
||||
writeFully(t17cData)
|
||||
}
|
||||
}
|
||||
|
||||
fun BytePacketBuilder.t401(
|
||||
t401Data: ByteArray
|
||||
) {
|
||||
writeShort(0x401)
|
||||
writeShortLVPacket {
|
||||
writeFully(t401Data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param apkId application.getPackageName().getBytes()
|
||||
*/
|
||||
|
@ -11,12 +11,9 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.*
|
||||
import net.mamoe.mirai.qqandroid.utils.GuidSource
|
||||
import net.mamoe.mirai.qqandroid.utils.MacOrAndroidIdChangeFlag
|
||||
import net.mamoe.mirai.qqandroid.utils.guidFlag
|
||||
import net.mamoe.mirai.utils.MiraiDebugAPI
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.cryptor.contentToString
|
||||
import net.mamoe.mirai.utils.cryptor.decryptBy
|
||||
import net.mamoe.mirai.utils.currentTimeMillis
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.utils.io.discardExact
|
||||
|
||||
@ -49,6 +46,33 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt
|
||||
}
|
||||
}
|
||||
|
||||
object SubCommand7 {
|
||||
private const val appId = 16L
|
||||
private const val subAppId = 537062845L
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
operator fun invoke(
|
||||
client: QQAndroidClient,
|
||||
t174: ByteArray,
|
||||
t402: ByteArray,
|
||||
phoneNumber: String
|
||||
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
||||
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
|
||||
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) {
|
||||
writeShort(7) // subCommand
|
||||
writeShort(7) // count of TLVs, probably ignored by server?TODO
|
||||
t8(2052)
|
||||
t104(client.t104)
|
||||
t116(150470524, 66560)
|
||||
t174(t174)
|
||||
t17c(phoneNumber.toByteArray())
|
||||
t401(md5(client.device.guid + "1234567890123456".toByteArray() + t402))
|
||||
t19e(0)//==tlv408
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SubCommand9 {
|
||||
private const val appId = 16L
|
||||
private const val subAppId = 537062845L
|
||||
@ -225,7 +249,7 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt
|
||||
|
||||
class UnsafeLogin(val url: String) : LoginPacketResponse()
|
||||
|
||||
class DeviceLockLogin() : LoginPacketResponse()
|
||||
class SMSVerifyCodeNeeded(val t174: ByteArray, val t402: ByteArray) : LoginPacketResponse()
|
||||
}
|
||||
|
||||
@InternalAPI
|
||||
@ -251,17 +275,18 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt
|
||||
1, 15 -> onErrorMessage(tlvMap)
|
||||
2 -> onSolveLoginCaptcha(tlvMap, bot)
|
||||
-96 -> onUnsafeDeviceLogin(tlvMap, bot)
|
||||
-52 -> onDeviceLockLogin(tlvMap, bot)
|
||||
-52 -> onSMSVerifyNeeded(tlvMap, bot)
|
||||
else -> error("unknown login result type: $type")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun onDeviceLockLogin(tlvMap: Map<Int, ByteArray>, bot: QQAndroidBot): LoginPacketResponse.DeviceLockLogin {
|
||||
println(tlvMap[0x104]!!.toUHexString())
|
||||
println(tlvMap[0x402]!!.toUHexString())
|
||||
println(tlvMap[0x403]!!.toUHexString())
|
||||
return LoginPacketResponse.DeviceLockLogin();
|
||||
private fun onSMSVerifyNeeded(
|
||||
tlvMap: Map<Int, ByteArray>,
|
||||
bot: QQAndroidBot
|
||||
): LoginPacketResponse.SMSVerifyCodeNeeded {
|
||||
bot.client.t104 = tlvMap[0x104]!!
|
||||
return LoginPacketResponse.SMSVerifyCodeNeeded(tlvMap[0x174] ?: EMPTY_BYTE_ARRAY, tlvMap[0x402]!!)
|
||||
}
|
||||
|
||||
private fun onUnsafeDeviceLogin(tlvMap: Map<Int, ByteArray>, bot: QQAndroidBot): LoginPacketResponse.UnsafeLogin {
|
||||
|
@ -389,7 +389,7 @@ internal class TIMPCBotNetworkHandler internal constructor(coroutineContext: Cor
|
||||
close()
|
||||
return
|
||||
}
|
||||
val code = configuration.captchaSolver(bot, captchaCache!!)
|
||||
val code = configuration.loginSolver(bot, captchaCache!!)
|
||||
|
||||
this.captchaCache = null
|
||||
if (code == null || code.length != 4) {
|
||||
|
@ -7,16 +7,21 @@ import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.jvm.JvmStatic
|
||||
|
||||
/**
|
||||
* 验证码处理器. 需挂起(阻塞)直到处理完成验证码.
|
||||
*
|
||||
* 返回长度为 4 的验证码. 为空则刷新验证码
|
||||
*/
|
||||
typealias CaptchaSolver = suspend Bot.(IoBuffer) -> String?
|
||||
abstract class LoginSolver {
|
||||
abstract suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String?
|
||||
|
||||
abstract suspend fun onSolveSliderCaptcha(bot: Bot, data: IoBuffer): String?
|
||||
|
||||
abstract suspend fun onGetPhoneNumber(): String
|
||||
|
||||
abstract suspend fun onGetSMSVerifyCode(): String
|
||||
}
|
||||
|
||||
/**
|
||||
* 在各平台实现的默认的验证码处理器.
|
||||
*/
|
||||
expect var DefaultCaptchaSolver: CaptchaSolver
|
||||
expect var defaultLoginSolver: LoginSolver
|
||||
|
||||
/**
|
||||
* 网络和连接配置
|
||||
@ -70,7 +75,7 @@ class BotConfiguration {
|
||||
/**
|
||||
* 验证码处理器
|
||||
*/
|
||||
var captchaSolver: CaptchaSolver = DefaultCaptchaSolver
|
||||
var loginSolver: LoginSolver = defaultLoginSolver
|
||||
/**
|
||||
* 登录完成后几秒会收到好友消息的历史记录,
|
||||
* 这些历史记录不会触发事件.
|
||||
|
@ -10,7 +10,9 @@ import kotlinx.coroutines.io.reader
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.io.core.IoBuffer
|
||||
import kotlinx.io.core.use
|
||||
import net.mamoe.mirai.Bot
|
||||
import java.awt.Image
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
@ -23,31 +25,68 @@ import kotlin.coroutines.CoroutineContext
|
||||
*
|
||||
* 可被修改, 除覆盖配置外全局生效.
|
||||
*/
|
||||
actual var DefaultCaptchaSolver: CaptchaSolver = {
|
||||
captchaLock.withLock {
|
||||
val tempFile: File = createTempFile(suffix = ".png").apply { deleteOnExit() }
|
||||
withContext(Dispatchers.IO) {
|
||||
tempFile.createNewFile()
|
||||
MiraiLogger.info("需要验证码登录, 验证码为 4 字母")
|
||||
try {
|
||||
tempFile.writeChannel().use { writeFully(it) }
|
||||
MiraiLogger.info("将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
MiraiLogger.info("无法写出验证码文件(${e.message}), 请尝试查看以上字符图片")
|
||||
}
|
||||
actual var defaultLoginSolver: LoginSolver = DefaultLoginSolver()
|
||||
|
||||
tempFile.inputStream().use {
|
||||
val img = ImageIO.read(it)
|
||||
if (img == null) {
|
||||
MiraiLogger.info("无法创建字符图片. 请查看文件")
|
||||
} else {
|
||||
MiraiLogger.info(img.createCharImg())
|
||||
|
||||
class DefaultLoginSolver(): LoginSolver(){
|
||||
override suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String? {
|
||||
loginSolverLock.withLock {
|
||||
val tempFile: File = createTempFile(suffix = ".png").apply { deleteOnExit() }
|
||||
withContext(Dispatchers.IO) {
|
||||
tempFile.createNewFile()
|
||||
MiraiLogger.info("需要验证码登录, 验证码为 4 字母")
|
||||
try {
|
||||
tempFile.writeChannel().use { writeFully(data) }
|
||||
MiraiLogger.info("将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
MiraiLogger.info("无法写出验证码文件(${e.message}), 请尝试查看以上字符图片")
|
||||
}
|
||||
|
||||
tempFile.inputStream().use {
|
||||
val img = ImageIO.read(it)
|
||||
if (img == null) {
|
||||
MiraiLogger.info("无法创建字符图片. 请查看文件")
|
||||
} else {
|
||||
MiraiLogger.info(img.createCharImg())
|
||||
}
|
||||
}
|
||||
}
|
||||
MiraiLogger.info("请输入 4 位字母验证码. 若要更换验证码, 请直接回车")
|
||||
return readLine()?.takeUnless { it.isEmpty() || it.length != 4 }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onSolveSliderCaptcha(bot: Bot, data: IoBuffer): String? {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override suspend fun onGetPhoneNumber(): String {
|
||||
loginSolverLock.withLock {
|
||||
while (true){
|
||||
MiraiLogger.info("请输入你的手机号码")
|
||||
val var0 = readLine()
|
||||
if(var0!==null && var0.length > 10){
|
||||
return var0;
|
||||
}
|
||||
}
|
||||
}
|
||||
MiraiLogger.info("请输入 4 位字母验证码. 若要更换验证码, 请直接回车")
|
||||
readLine()?.takeUnless { it.isEmpty() || it.length != 4 }
|
||||
return "";
|
||||
}
|
||||
|
||||
override suspend fun onGetSMSVerifyCode(): String {
|
||||
loginSolverLock.withLock {
|
||||
while (true){
|
||||
MiraiLogger.info("请输入你刚刚收到的手机验证码[6位数字]")
|
||||
val var0 = readLine()
|
||||
if(var0!==null && var0.length == 6){
|
||||
return var0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Copied from Ktor CIO
|
||||
@ -62,7 +101,7 @@ private fun File.writeChannel(
|
||||
}.channel
|
||||
|
||||
|
||||
private val captchaLock = Mutex()
|
||||
private val loginSolverLock = Mutex()
|
||||
|
||||
/**
|
||||
* @author NaturalHG
|
||||
|
Loading…
Reference in New Issue
Block a user