Commit d8cd6625 authored by jiahua.liu's avatar jiahua.liu

Android SMS login - Incomplete

parent 767b945a
...@@ -33,37 +33,53 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -33,37 +33,53 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
launch(CoroutineName("Incoming Packet Receiver")) { processReceive() } launch(CoroutineName("Incoming Packet Receiver")) { processReceive() }
bot.logger.info("Trying login") bot.logger.info("Trying login")
when (val response: LoginPacket.LoginPacketResponse = LoginPacket.SubCommand9(bot.client).sendAndExpect()) { var response: LoginPacket.LoginPacketResponse = LoginPacket.SubCommand9(bot.client).sendAndExpect()
is UnsafeLogin -> { mainloop@ while (true) {
bot.logger.info("Login unsuccessful, device auth is needed") when (response) {
bot.logger.info("登陆失败, 原因为非常用设备登陆") is UnsafeLogin -> {
bot.logger.info("Open the following URL in QQ browser and complete the verification") bot.logger.info("Login unsuccessful, device auth is needed")
bot.logger.info("将下面这个链接在QQ浏览器中打开并完成认证后尝试再次登陆") bot.logger.info("登陆失败, 原因为非常用设备登陆")
bot.logger.info(response.url) bot.logger.info("Open the following URL in QQ browser and complete the verification")
return bot.logger.info("将下面这个链接在QQ浏览器中打开并完成认证后尝试再次登陆")
} bot.logger.info(response.url)
return
}
is Captcha -> when (response) { is Captcha -> when (response) {
is Captcha.Picture -> { is Captcha.Picture -> {
bot.logger.info("需要图片验证码") bot.logger.info("需要图片验证码")
var result = bot.configuration.captchaSolver.invoke(bot, response.data) var result = bot.configuration.loginSolver.onSolvePicCaptcha(bot, response.data)
if (result === null || result.length != 4) { if (result === null || result.length != 4) {
//refresh captcha //refresh captcha
result = "ABCD" 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 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 Success -> { is Success -> {
bot.logger.info("Login successful") bot.logger.info("Login successful")
break@mainloop
}
} }
} }
......
...@@ -142,6 +142,7 @@ fun BytePacketBuilder.t116( ...@@ -142,6 +142,7 @@ fun BytePacketBuilder.t116(
} }
} }
fun BytePacketBuilder.t100( fun BytePacketBuilder.t100(
appId: Long = 16, appId: Long = 16,
subAppId: Long = 537062845, subAppId: Long = 537062845,
...@@ -193,6 +194,44 @@ fun BytePacketBuilder.t104( ...@@ -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() * @param apkId application.getPackageName().getBytes()
*/ */
......
...@@ -11,12 +11,9 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.* ...@@ -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.GuidSource
import net.mamoe.mirai.qqandroid.utils.MacOrAndroidIdChangeFlag import net.mamoe.mirai.qqandroid.utils.MacOrAndroidIdChangeFlag
import net.mamoe.mirai.qqandroid.utils.guidFlag import net.mamoe.mirai.qqandroid.utils.guidFlag
import net.mamoe.mirai.utils.MiraiDebugAPI import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.cryptor.contentToString import net.mamoe.mirai.utils.cryptor.contentToString
import net.mamoe.mirai.utils.cryptor.decryptBy 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.*
import net.mamoe.mirai.utils.io.discardExact import net.mamoe.mirai.utils.io.discardExact
...@@ -49,6 +46,33 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt ...@@ -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 { object SubCommand9 {
private const val appId = 16L private const val appId = 16L
private const val subAppId = 537062845L private const val subAppId = 537062845L
...@@ -225,7 +249,7 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt ...@@ -225,7 +249,7 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt
class UnsafeLogin(val url: String) : LoginPacketResponse() class UnsafeLogin(val url: String) : LoginPacketResponse()
class DeviceLockLogin() : LoginPacketResponse() class SMSVerifyCodeNeeded(val t174: ByteArray, val t402: ByteArray) : LoginPacketResponse()
} }
@InternalAPI @InternalAPI
...@@ -251,17 +275,18 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt ...@@ -251,17 +275,18 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt
1, 15 -> onErrorMessage(tlvMap) 1, 15 -> onErrorMessage(tlvMap)
2 -> onSolveLoginCaptcha(tlvMap, bot) 2 -> onSolveLoginCaptcha(tlvMap, bot)
-96 -> onUnsafeDeviceLogin(tlvMap, bot) -96 -> onUnsafeDeviceLogin(tlvMap, bot)
-52 -> onDeviceLockLogin(tlvMap, bot) -52 -> onSMSVerifyNeeded(tlvMap, bot)
else -> error("unknown login result type: $type") else -> error("unknown login result type: $type")
} }
} }
private fun onDeviceLockLogin(tlvMap: Map<Int, ByteArray>, bot: QQAndroidBot): LoginPacketResponse.DeviceLockLogin { private fun onSMSVerifyNeeded(
println(tlvMap[0x104]!!.toUHexString()) tlvMap: Map<Int, ByteArray>,
println(tlvMap[0x402]!!.toUHexString()) bot: QQAndroidBot
println(tlvMap[0x403]!!.toUHexString()) ): LoginPacketResponse.SMSVerifyCodeNeeded {
return LoginPacketResponse.DeviceLockLogin(); 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 { private fun onUnsafeDeviceLogin(tlvMap: Map<Int, ByteArray>, bot: QQAndroidBot): LoginPacketResponse.UnsafeLogin {
......
...@@ -389,7 +389,7 @@ internal class TIMPCBotNetworkHandler internal constructor(coroutineContext: Cor ...@@ -389,7 +389,7 @@ internal class TIMPCBotNetworkHandler internal constructor(coroutineContext: Cor
close() close()
return return
} }
val code = configuration.captchaSolver(bot, captchaCache!!) val code = configuration.loginSolver(bot, captchaCache!!)
this.captchaCache = null this.captchaCache = null
if (code == null || code.length != 4) { if (code == null || code.length != 4) {
......
...@@ -7,16 +7,21 @@ import kotlin.coroutines.EmptyCoroutineContext ...@@ -7,16 +7,21 @@ import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmStatic 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 { ...@@ -70,7 +75,7 @@ class BotConfiguration {
/** /**
* 验证码处理器 * 验证码处理器
*/ */
var captchaSolver: CaptchaSolver = DefaultCaptchaSolver var loginSolver: LoginSolver = defaultLoginSolver
/** /**
* 登录完成后几秒会收到好友消息的历史记录, * 登录完成后几秒会收到好友消息的历史记录,
* 这些历史记录不会触发事件. * 这些历史记录不会触发事件.
......
...@@ -10,7 +10,9 @@ import kotlinx.coroutines.io.reader ...@@ -10,7 +10,9 @@ import kotlinx.coroutines.io.reader
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.io.core.IoBuffer
import kotlinx.io.core.use import kotlinx.io.core.use
import net.mamoe.mirai.Bot
import java.awt.Image import java.awt.Image
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.io.File import java.io.File
...@@ -23,31 +25,68 @@ import kotlin.coroutines.CoroutineContext ...@@ -23,31 +25,68 @@ import kotlin.coroutines.CoroutineContext
* *
* 可被修改, 除覆盖配置外全局生效. * 可被修改, 除覆盖配置外全局生效.
*/ */
actual var DefaultCaptchaSolver: CaptchaSolver = { actual var defaultLoginSolver: LoginSolver = DefaultLoginSolver()
captchaLock.withLock {
val tempFile: File = createTempFile(suffix = ".png").apply { deleteOnExit() }
withContext(Dispatchers.IO) { class DefaultLoginSolver(): LoginSolver(){
tempFile.createNewFile() override suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String? {
MiraiLogger.info("需要验证码登录, 验证码为 4 字母") loginSolverLock.withLock {
try { val tempFile: File = createTempFile(suffix = ".png").apply { deleteOnExit() }
tempFile.writeChannel().use { writeFully(it) } withContext(Dispatchers.IO) {
MiraiLogger.info("将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}") tempFile.createNewFile()
} catch (e: Exception) { MiraiLogger.info("需要验证码登录, 验证码为 4 字母")
MiraiLogger.info("无法写出验证码文件(${e.message}), 请尝试查看以上字符图片") 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 }
}
}
tempFile.inputStream().use { override suspend fun onSolveSliderCaptcha(bot: Bot, data: IoBuffer): String? {
val img = ImageIO.read(it) TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
if (img == null) { }
MiraiLogger.info("无法创建字符图片. 请查看文件")
} else { override suspend fun onGetPhoneNumber(): String {
MiraiLogger.info(img.createCharImg()) loginSolverLock.withLock {
while (true){
MiraiLogger.info("请输入你的手机号码")
val var0 = readLine()
if(var0!==null && var0.length > 10){
return var0;
} }
} }
} }
MiraiLogger.info("请输入 4 位字母验证码. 若要更换验证码, 请直接回车") return "";
readLine()?.takeUnless { it.isEmpty() || it.length != 4 }
} }
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 // Copied from Ktor CIO
...@@ -62,7 +101,7 @@ private fun File.writeChannel( ...@@ -62,7 +101,7 @@ private fun File.writeChannel(
}.channel }.channel
private val captchaLock = Mutex() private val loginSolverLock = Mutex()
/** /**
* @author NaturalHG * @author NaturalHG
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment