mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-22 13:46:13 +08:00
New network: framework infrastructure
This commit is contained in:
parent
48564056df
commit
b844efb072
@ -44,6 +44,7 @@ object Versions {
|
|||||||
const val log4j = "2.13.3"
|
const val log4j = "2.13.3"
|
||||||
const val asm = "9.1"
|
const val asm = "9.1"
|
||||||
const val difflib = "1.3.0"
|
const val difflib = "1.3.0"
|
||||||
|
const val netty = "4.1.63.Final"
|
||||||
|
|
||||||
const val junit = "5.4.2"
|
const val junit = "5.4.2"
|
||||||
|
|
||||||
@ -110,4 +111,5 @@ const val `jetbrains-annotations` = "org.jetbrains:annotations:19.0.0"
|
|||||||
|
|
||||||
const val `caller-finder` = "io.github.karlatemp:caller:1.1.1"
|
const val `caller-finder` = "io.github.karlatemp:caller:1.1.1"
|
||||||
|
|
||||||
const val `android-runtime` = "com.google.android:android:${Versions.android}"
|
const val `android-runtime` = "com.google.android:android:${Versions.android}"
|
||||||
|
const val `netty-all` = "io.netty:netty-all:${Versions.netty}"
|
@ -33,6 +33,10 @@ public suspend inline fun <R> runBIO(
|
|||||||
noinline block: () -> R
|
noinline block: () -> R
|
||||||
): R = runInterruptible(context = Dispatchers.IO, block = block)
|
): R = runInterruptible(context = Dispatchers.IO, block = block)
|
||||||
|
|
||||||
|
public suspend inline fun <T, R> T.runBIO(
|
||||||
|
crossinline block: T.() -> R
|
||||||
|
): R = runInterruptible(context = Dispatchers.IO, block = { block() })
|
||||||
|
|
||||||
public inline fun CoroutineScope.launchWithPermit(
|
public inline fun CoroutineScope.launchWithPermit(
|
||||||
semaphore: Semaphore,
|
semaphore: Semaphore,
|
||||||
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
54
mirai-core-utils/src/commonMain/kotlin/ExceptionCollector.kt
Normal file
54
mirai-core-utils/src/commonMain/kotlin/ExceptionCollector.kt
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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.utils
|
||||||
|
|
||||||
|
public class ExceptionCollector {
|
||||||
|
@Volatile
|
||||||
|
private var last: Throwable? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
public fun collect(e: Throwable?) {
|
||||||
|
if (e == null) return
|
||||||
|
val last = last
|
||||||
|
if (last != null) {
|
||||||
|
e.addSuppressed(last)
|
||||||
|
}
|
||||||
|
this.last = e
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias to [collect] to be used inside [withExceptionCollector]
|
||||||
|
*/
|
||||||
|
public fun collectException(e: Throwable?): Unit = collect(e)
|
||||||
|
|
||||||
|
public fun getLast(): Throwable? = last
|
||||||
|
|
||||||
|
@TerminalOperation // to give it a color for a clearer control flow
|
||||||
|
public fun collectThrow(exception: Throwable): Nothing {
|
||||||
|
collect(exception)
|
||||||
|
throw getLast()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
@DslMarker
|
||||||
|
private annotation class TerminalOperation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run with a coverage of `throw`. All thrown exceptions will be caught and rethrown with [ExceptionCollector.collectThrow]
|
||||||
|
*/
|
||||||
|
public inline fun <R> withExceptionCollector(action: ExceptionCollector.() -> R): R {
|
||||||
|
ExceptionCollector().run {
|
||||||
|
try {
|
||||||
|
return action()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
collectThrow(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -72,6 +72,7 @@ kotlin {
|
|||||||
|
|
||||||
api1(`kotlinx-io-jvm`)
|
api1(`kotlinx-io-jvm`)
|
||||||
implementation1(`kotlinx-coroutines-io`)
|
implementation1(`kotlinx-coroutines-io`)
|
||||||
|
implementation(`netty-all`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import net.mamoe.mirai.internal.message.LongMessageInternal
|
|||||||
import net.mamoe.mirai.internal.network.*
|
import net.mamoe.mirai.internal.network.*
|
||||||
import net.mamoe.mirai.internal.network.handler.BdhSessionSyncer
|
import net.mamoe.mirai.internal.network.handler.BdhSessionSyncer
|
||||||
import net.mamoe.mirai.internal.network.handler.QQAndroidBotNetworkHandler
|
import net.mamoe.mirai.internal.network.handler.QQAndroidBotNetworkHandler
|
||||||
|
import net.mamoe.mirai.internal.network.net.protocol.SsoContext
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
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.OutgoingPacketWithRespType
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
|
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
|
||||||
@ -32,7 +33,6 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
|
|||||||
import net.mamoe.mirai.internal.utils.ScheduledJob
|
import net.mamoe.mirai.internal.utils.ScheduledJob
|
||||||
import net.mamoe.mirai.internal.utils.crypto.TEA
|
import net.mamoe.mirai.internal.utils.crypto.TEA
|
||||||
import net.mamoe.mirai.internal.utils.friendCacheFile
|
import net.mamoe.mirai.internal.utils.friendCacheFile
|
||||||
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
|
||||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||||
import net.mamoe.mirai.message.data.ForwardMessage
|
import net.mamoe.mirai.message.data.ForwardMessage
|
||||||
import net.mamoe.mirai.message.data.RichMessage
|
import net.mamoe.mirai.message.data.RichMessage
|
||||||
@ -58,9 +58,9 @@ internal fun QQAndroidBot.createOtherClient(
|
|||||||
|
|
||||||
@Suppress("INVISIBLE_MEMBER", "BooleanLiteralArgument", "OverridingDeprecatedMember")
|
@Suppress("INVISIBLE_MEMBER", "BooleanLiteralArgument", "OverridingDeprecatedMember")
|
||||||
internal class QQAndroidBot constructor(
|
internal class QQAndroidBot constructor(
|
||||||
private val account: BotAccount,
|
internal val account: BotAccount,
|
||||||
configuration: BotConfiguration
|
configuration: BotConfiguration
|
||||||
) : AbstractBot<QQAndroidBotNetworkHandler>(configuration, account.id) {
|
) : AbstractBot<QQAndroidBotNetworkHandler>(configuration, account.id), SsoContext {
|
||||||
val bdhSyncer: BdhSessionSyncer = BdhSessionSyncer(this)
|
val bdhSyncer: BdhSessionSyncer = BdhSessionSyncer(this)
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
@ -101,38 +101,9 @@ internal class QQAndroidBot constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
|
||||||
}
|
|
||||||
|
|
||||||
/////////////////////////// accounts secrets end
|
/////////////////////////// accounts secrets end
|
||||||
|
|
||||||
var client: QQAndroidClient = initClient()
|
override lateinit var client: QQAndroidClient
|
||||||
|
|
||||||
fun initClient(): QQAndroidClient {
|
|
||||||
val device = configuration.deviceInfo?.invoke(this) ?: DeviceInfo.random()
|
|
||||||
client = QQAndroidClient(
|
|
||||||
account,
|
|
||||||
device = device,
|
|
||||||
accountSecrets = loadSecretsFromCacheOrCreate(device)
|
|
||||||
)
|
|
||||||
client._bot = this
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override val bot: QQAndroidBot get() = this
|
override val bot: QQAndroidBot get() = this
|
||||||
|
@ -25,7 +25,7 @@ internal interface AccountSecrets {
|
|||||||
var wLoginSigInfoField: WLoginSigInfo?
|
var wLoginSigInfoField: WLoginSigInfo?
|
||||||
|
|
||||||
val wLoginSigInfoInitialized get() = wLoginSigInfoField != null
|
val wLoginSigInfoInitialized get() = wLoginSigInfoField != null
|
||||||
var wLoginSigInfo
|
var wLoginSigInfo: WLoginSigInfo
|
||||||
get() = wLoginSigInfoField ?: error("wLoginSigInfoField is not yet initialized")
|
get() = wLoginSigInfoField ?: error("wLoginSigInfoField is not yet initialized")
|
||||||
set(value) {
|
set(value) {
|
||||||
wLoginSigInfoField = value
|
wLoginSigInfoField = value
|
||||||
@ -49,12 +49,13 @@ internal interface AccountSecrets {
|
|||||||
val randomKey: ByteArray
|
val randomKey: ByteArray
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("ArrayInDataClass") // for `copy`
|
||||||
@Serializable
|
@Serializable
|
||||||
internal class AccountSecretsImpl(
|
internal data class AccountSecretsImpl(
|
||||||
override var loginExtraData: MutableSet<LoginExtraData>,
|
override var loginExtraData: MutableSet<LoginExtraData>,
|
||||||
override var wLoginSigInfoField: WLoginSigInfo?,
|
override var wLoginSigInfoField: WLoginSigInfo?,
|
||||||
override var G: ByteArray,
|
override var G: ByteArray,
|
||||||
override var dpwd: ByteArray,
|
override var dpwd: ByteArray = get_mpasswd().toByteArray(),
|
||||||
override var randSeed: ByteArray,
|
override var randSeed: ByteArray,
|
||||||
override var ksid: ByteArray,
|
override var ksid: ByteArray,
|
||||||
override var tgtgtKey: ByteArray,
|
override var tgtgtKey: ByteArray,
|
||||||
|
@ -22,6 +22,7 @@ import kotlinx.serialization.Serializable
|
|||||||
import net.mamoe.mirai.data.OnlineStatus
|
import net.mamoe.mirai.data.OnlineStatus
|
||||||
import net.mamoe.mirai.internal.BotAccount
|
import net.mamoe.mirai.internal.BotAccount
|
||||||
import net.mamoe.mirai.internal.QQAndroidBot
|
import net.mamoe.mirai.internal.QQAndroidBot
|
||||||
|
import net.mamoe.mirai.internal.network.net.protocol.LoginSessionAware
|
||||||
import net.mamoe.mirai.internal.network.protocol.SyncingCacheList
|
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.data.jce.FileStoragePushFSSvcList
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
|
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
|
||||||
@ -65,13 +66,15 @@ internal val DefaultServerList: MutableSet<Pair<String, Int>> =
|
|||||||
DOMAINS
|
DOMAINS
|
||||||
Pskey: "openmobile.qq.com"
|
Pskey: "openmobile.qq.com"
|
||||||
*/
|
*/
|
||||||
@PublishedApi
|
/**
|
||||||
|
* holds all the states related to network.
|
||||||
|
*/
|
||||||
internal open class QQAndroidClient(
|
internal open class QQAndroidClient(
|
||||||
val account: BotAccount,
|
val account: BotAccount,
|
||||||
val ecdh: ECDH = ECDH(),
|
override val ecdh: ECDH = ECDH(),
|
||||||
val device: DeviceInfo,
|
val device: DeviceInfo,
|
||||||
accountSecrets: AccountSecrets
|
accountSecrets: AccountSecrets
|
||||||
) : AccountSecrets by accountSecrets {
|
) : AccountSecrets by accountSecrets, LoginSessionAware {
|
||||||
lateinit var _bot: QQAndroidBot
|
lateinit var _bot: QQAndroidBot
|
||||||
val bot: QQAndroidBot get() = _bot
|
val bot: QQAndroidBot get() = _bot
|
||||||
|
|
||||||
@ -124,6 +127,7 @@ internal open class QQAndroidClient(
|
|||||||
return new
|
return new
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: 2021/4/14 investigate whether they can be minimized
|
||||||
private val friendSeq: AtomicInt = atomic(getRandomUnsignedInt())
|
private val friendSeq: AtomicInt = atomic(getRandomUnsignedInt())
|
||||||
internal fun getFriendSeq(): Int = friendSeq.value
|
internal fun getFriendSeq(): Int = friendSeq.value
|
||||||
|
|
||||||
@ -213,8 +217,8 @@ internal open class QQAndroidClient(
|
|||||||
@PublishedApi
|
@PublishedApi
|
||||||
internal val apkId: ByteArray = "com.tencent.mobileqq".toByteArray()
|
internal val apkId: ByteArray = "com.tencent.mobileqq".toByteArray()
|
||||||
|
|
||||||
var outgoingPacketSessionId: ByteArray = 0x02B05B8B.toByteArray()
|
override var outgoingPacketSessionId: ByteArray = 0x02B05B8B.toByteArray()
|
||||||
var loginState = 0
|
override var loginState = 0
|
||||||
|
|
||||||
var t150: Tlv? = null
|
var t150: Tlv? = null
|
||||||
var rollbackSig: ByteArray? = null
|
var rollbackSig: ByteArray? = null
|
||||||
|
@ -67,8 +67,9 @@ internal class LoginExtraData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("ArrayInDataClass") // for `copy`
|
||||||
@Serializable
|
@Serializable
|
||||||
internal class WLoginSigInfo(
|
internal data class WLoginSigInfo(
|
||||||
val uin: Long,
|
val uin: Long,
|
||||||
var encryptA1: ByteArray?, // sigInfo[0]
|
var encryptA1: ByteArray?, // sigInfo[0]
|
||||||
/**
|
/**
|
||||||
|
213
mirai-core/src/commonMain/kotlin/network/net/NetworkHandler.kt
Normal file
213
mirai-core/src/commonMain/kotlin/network/net/NetworkHandler.kt
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import kotlinx.atomicfu.atomic
|
||||||
|
import net.mamoe.mirai.Bot
|
||||||
|
import net.mamoe.mirai.internal.QQAndroidBot
|
||||||
|
import net.mamoe.mirai.internal.network.Packet
|
||||||
|
import net.mamoe.mirai.internal.network.net.NetworkHandler.State
|
||||||
|
import net.mamoe.mirai.internal.network.net.protocol.SsoController
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
||||||
|
import net.mamoe.mirai.utils.BotConfiguration
|
||||||
|
import net.mamoe.mirai.utils.MiraiLogger
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.SocketAddress
|
||||||
|
import java.util.concurrent.CancellationException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable context for [NetworkHandler]
|
||||||
|
*/
|
||||||
|
internal interface NetworkHandlerContext {
|
||||||
|
val bot: QQAndroidBot
|
||||||
|
|
||||||
|
val logger: MiraiLogger
|
||||||
|
val ssoController: SsoController
|
||||||
|
val configuration: BotConfiguration
|
||||||
|
|
||||||
|
fun getNextAddress(): SocketAddress // FIXME: 2021/4/14
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class NetworkHandlerContextImpl(
|
||||||
|
override val bot: QQAndroidBot,
|
||||||
|
override val ssoController: SsoController,
|
||||||
|
) : NetworkHandlerContext {
|
||||||
|
override val configuration: BotConfiguration
|
||||||
|
get() = bot.configuration
|
||||||
|
|
||||||
|
override fun getNextAddress(): SocketAddress {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override val logger: MiraiLogger by lazy { configuration.networkLoggerSupplier(bot) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic interface available to application. Usually wrapped with [SelectorNetworkHandler].
|
||||||
|
*
|
||||||
|
* A [NetworkHandler] holds no reference to [Bot]s.
|
||||||
|
*/
|
||||||
|
internal interface NetworkHandler {
|
||||||
|
val context: NetworkHandlerContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of this handler.
|
||||||
|
*/
|
||||||
|
val state: State
|
||||||
|
|
||||||
|
enum class State {
|
||||||
|
/**
|
||||||
|
* Just created and no connection has been made.
|
||||||
|
*
|
||||||
|
* At this state [resumeConnection] turns state into [CONNECTING] and
|
||||||
|
* establishes a connection to the server and do authentication, for which [sendAndExpect] suspends.
|
||||||
|
*/
|
||||||
|
INITIALIZED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection to server, including the process of authentication.
|
||||||
|
*
|
||||||
|
* At this state [resumeConnection] does nothing. [sendAndExpect] suspends for the result of connection started in [INITIALIZED].
|
||||||
|
*/
|
||||||
|
CONNECTING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Everything is working. [resumeConnection] does nothing. [sendAndExpect] does not suspend for connection reasons.
|
||||||
|
*/
|
||||||
|
OK,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No Internet Connection available or for any other reasons but it is possible to establish a connection again(switching state to [CONNECTING]).
|
||||||
|
*/
|
||||||
|
CONNECTION_LOST,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cannot resume anymore. Both [resumeConnection] and [sendAndExpect] throw a [CancellationException].
|
||||||
|
*
|
||||||
|
* When a handler reached [CLOSED] state, it is finalized and cannot be restored to any other states.
|
||||||
|
*/
|
||||||
|
CLOSED,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to resume the connection. Throws no exception but changes [state]
|
||||||
|
* @see State
|
||||||
|
*/
|
||||||
|
suspend fun resumeConnection()
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends [packet] and expects to receive a response from the server.
|
||||||
|
* @param attempts ranges `1..INFINITY`
|
||||||
|
*/
|
||||||
|
suspend fun sendAndExpect(packet: OutgoingPacket, timeout: Long = 5000, attempts: Int = 2): Packet?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends [packet] and does not expect any response. (Response is still processed but not passed as a return value of this function.)
|
||||||
|
*/
|
||||||
|
suspend fun sendWithoutExpect(packet: OutgoingPacket)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes this handler gracefully and suspends the coroutine for its completion.
|
||||||
|
*/
|
||||||
|
suspend fun close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for a specific [NetworkHandler] implementation.
|
||||||
|
*/
|
||||||
|
internal interface NetworkHandlerFactory<H : NetworkHandler> {
|
||||||
|
fun create(context: NetworkHandlerContext, host: String, port: Int): H =
|
||||||
|
create(context, InetSocketAddress.createUnresolved(host, port))
|
||||||
|
|
||||||
|
fun create(context: NetworkHandlerContext, host: InetAddress, port: Int): H =
|
||||||
|
create(context, InetSocketAddress(host, port))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of [H]. The returning [H] has [NetworkHandler.state] of [State.INITIALIZED]
|
||||||
|
*/
|
||||||
|
fun create(context: NetworkHandlerContext, address: SocketAddress): H
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lazy stateful selector of [NetworkHandler].
|
||||||
|
*
|
||||||
|
* - Calls [factory.create][NetworkHandlerFactory.create] to create [NetworkHandler]s.
|
||||||
|
* - Re-initialize [NetworkHandler] instances if the old one is dead.
|
||||||
|
* - Suspends requests when connection is not available.
|
||||||
|
*
|
||||||
|
* No connection is created until first invocation of [getResumedInstance],
|
||||||
|
* and new connections are created only when calling [getResumedInstance] if the old connection was dead.
|
||||||
|
*/
|
||||||
|
internal abstract class NetworkHandlerSelector<H : NetworkHandler> {
|
||||||
|
/**
|
||||||
|
* Returns an instance immediately without suspension, or `null` if instance not ready.
|
||||||
|
* @see awaitResumeInstance
|
||||||
|
*/
|
||||||
|
abstract fun getResumedInstance(): H?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an alive [NetworkHandler], or suspends the coroutine until the connection has been made again.
|
||||||
|
*/
|
||||||
|
abstract suspend fun awaitResumeInstance(): H
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 2021/4/14 better naming
|
||||||
|
internal abstract class AutoReconnectNetworkHandlerSelector<H : NetworkHandler> : NetworkHandlerSelector<H>() {
|
||||||
|
private val current = atomic<H?>(null)
|
||||||
|
|
||||||
|
protected abstract fun createInstance(): H
|
||||||
|
|
||||||
|
final override fun getResumedInstance(): H? = current.value
|
||||||
|
|
||||||
|
final override tailrec suspend fun awaitResumeInstance(): H {
|
||||||
|
val current = getResumedInstance()
|
||||||
|
return if (current != null) {
|
||||||
|
when (current.state) {
|
||||||
|
State.OK -> current
|
||||||
|
State.CLOSED -> {
|
||||||
|
this.current.compareAndSet(current, null) // invalidate the instance and try again.
|
||||||
|
awaitResumeInstance()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
current.resumeConnection() // try to advance state.
|
||||||
|
awaitResumeInstance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.current.compareAndSet(current, createInstance())
|
||||||
|
awaitResumeInstance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates [NetworkHandler] calls to instance returned by [NetworkHandlerSelector.awaitResumeInstance].
|
||||||
|
*/
|
||||||
|
internal class SelectorNetworkHandler(
|
||||||
|
override val context: NetworkHandlerContext,
|
||||||
|
private val selector: NetworkHandlerSelector<*>
|
||||||
|
) : NetworkHandler {
|
||||||
|
private suspend inline fun instance(): NetworkHandler = selector.awaitResumeInstance()
|
||||||
|
|
||||||
|
override val state: State get() = selector.getResumedInstance()?.state ?: State.INITIALIZED
|
||||||
|
|
||||||
|
override suspend fun resumeConnection() {
|
||||||
|
instance() // the selector will resume connection for us.
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendAndExpect(packet: OutgoingPacket, timeout: Long, attempts: Int) =
|
||||||
|
instance().sendAndExpect(packet, timeout, attempts)
|
||||||
|
|
||||||
|
override suspend fun sendWithoutExpect(packet: OutgoingPacket) = instance().sendWithoutExpect(packet)
|
||||||
|
override suspend fun close() = instance().close()
|
||||||
|
}
|
@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* 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.impl.netty
|
||||||
|
|
||||||
|
import io.netty.bootstrap.Bootstrap
|
||||||
|
import io.netty.buffer.ByteBuf
|
||||||
|
import io.netty.buffer.ByteBufInputStream
|
||||||
|
import io.netty.channel.ChannelHandlerContext
|
||||||
|
import io.netty.channel.ChannelInboundHandlerAdapter
|
||||||
|
import io.netty.channel.ChannelInitializer
|
||||||
|
import io.netty.channel.SimpleChannelInboundHandler
|
||||||
|
import io.netty.channel.nio.NioEventLoopGroup
|
||||||
|
import io.netty.channel.socket.SocketChannel
|
||||||
|
import io.netty.channel.socket.nio.NioSocketChannel
|
||||||
|
import io.netty.handler.codec.LengthFieldBasedFrameDecoder
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.channels.sendBlocking
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.consumeAsFlow
|
||||||
|
import kotlinx.io.core.ByteReadPacket
|
||||||
|
import net.mamoe.mirai.internal.network.net.NetworkHandler
|
||||||
|
import net.mamoe.mirai.internal.network.net.NetworkHandlerContext
|
||||||
|
import net.mamoe.mirai.internal.network.net.protocol.PacketCodec
|
||||||
|
import net.mamoe.mirai.internal.network.net.protocol.RawIncomingPacket
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
||||||
|
import net.mamoe.mirai.utils.childScope
|
||||||
|
import java.net.SocketAddress
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
internal class NettyNetworkHandler(
|
||||||
|
context: NetworkHandlerContext,
|
||||||
|
private val address: SocketAddress,
|
||||||
|
) : NetworkHandlerSupport(context) {
|
||||||
|
override suspend fun close() {
|
||||||
|
super.close()
|
||||||
|
setState(StateClosed())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendPacketImpl(packet: OutgoingPacket) {
|
||||||
|
val state = _state as NettyState
|
||||||
|
state.sendPacketImpl(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// netty conn.
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class RawIncomingPacketCollector(
|
||||||
|
private val decodePipeline: PacketDecodePipeline
|
||||||
|
) : SimpleChannelInboundHandler<RawIncomingPacket>(RawIncomingPacket::class.java) {
|
||||||
|
override fun channelRead0(ctx: ChannelHandlerContext, msg: RawIncomingPacket) {
|
||||||
|
decodePipeline.send(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createConnection(decodePipeline: PacketDecodePipeline): ChannelHandlerContext {
|
||||||
|
val contextResult = CompletableDeferred<ChannelHandlerContext>()
|
||||||
|
|
||||||
|
val eventLoopGroup = NioEventLoopGroup()
|
||||||
|
Bootstrap().group(eventLoopGroup)
|
||||||
|
.channel(NioSocketChannel::class.java)
|
||||||
|
.handler(object : ChannelInitializer<SocketChannel>() {
|
||||||
|
override fun initChannel(ch: SocketChannel) {
|
||||||
|
ch.pipeline().addLast(object : ChannelInboundHandlerAdapter() {
|
||||||
|
override fun channelActive(ctx: ChannelHandlerContext) {
|
||||||
|
contextResult.complete(ctx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 0))
|
||||||
|
.addLast(ByteBufToIncomingPacketDecoder())
|
||||||
|
.addLast(RawIncomingPacketCollector(decodePipeline))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.connect(address).runBIO { await() }
|
||||||
|
// TODO: 2021/4/14 eventLoopGroup 移动到 bot, 并在 bot.close() 时关闭
|
||||||
|
|
||||||
|
return contextResult.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class PacketDecodePipeline(parentContext: CoroutineContext) :
|
||||||
|
CoroutineScope by parentContext.childScope() {
|
||||||
|
private val channel: Channel<RawIncomingPacket> = Channel(Channel.BUFFERED)
|
||||||
|
|
||||||
|
init {
|
||||||
|
launch(CoroutineName("PacketDecodePipeline processor")) {
|
||||||
|
// 'single thread' processor
|
||||||
|
channel.consumeAsFlow().collect { raw ->
|
||||||
|
val result = PacketCodec.processBody(context.bot, raw)
|
||||||
|
if (result == null) {
|
||||||
|
collectUnknownPacket(raw)
|
||||||
|
} else collectReceived(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(raw: RawIncomingPacket) = channel.sendBlocking(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// states
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private abstract inner class NettyState(
|
||||||
|
correspondingState: NetworkHandler.State
|
||||||
|
) : BaseStateImpl(correspondingState) {
|
||||||
|
abstract suspend fun sendPacketImpl(packet: OutgoingPacket)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class StateInitialized : NettyState(NetworkHandler.State.INITIALIZED) {
|
||||||
|
override suspend fun sendPacketImpl(packet: OutgoingPacket) {
|
||||||
|
error("Cannot send packet when connection is not set. (resumeConnection not called.)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun resumeConnection() {
|
||||||
|
setState(StateConnecting(PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class StateConnecting(
|
||||||
|
val decodePipeline: PacketDecodePipeline,
|
||||||
|
) : NettyState(NetworkHandler.State.CONNECTING) {
|
||||||
|
private val connection = async {
|
||||||
|
createConnection(decodePipeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val connectResult = async {
|
||||||
|
val connection = connection.await()
|
||||||
|
context.ssoController.login()
|
||||||
|
setState(StateOK(connection))
|
||||||
|
}.apply {
|
||||||
|
invokeOnCompletion { error ->
|
||||||
|
if (error != null) setState(StateClosed()) // logon failure closes the network handler.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendPacketImpl(packet: OutgoingPacket) {
|
||||||
|
connection.await().writeAndFlush(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun resumeConnection() {
|
||||||
|
connectResult.await() // propagates exceptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class StateOK(
|
||||||
|
private val connection: ChannelHandlerContext
|
||||||
|
) : NettyState(NetworkHandler.State.OK) {
|
||||||
|
override suspend fun sendPacketImpl(packet: OutgoingPacket) {
|
||||||
|
connection.writeAndFlush(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun resumeConnection() {} // noop
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class StateClosed : NettyState(NetworkHandler.State.OK) {
|
||||||
|
override suspend fun sendPacketImpl(packet: OutgoingPacket) = error("NetworkHandler is already closed.")
|
||||||
|
override suspend fun resumeConnection() {} // noop
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initialState(): BaseStateImpl = StateInitialized()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteBuf.toReadPacket(): ByteReadPacket {
|
||||||
|
val buf = this
|
||||||
|
return buildPacket {
|
||||||
|
ByteBufInputStream(buf).withUse { copyTo(outputStream()) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
* 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.impl.netty
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import net.mamoe.mirai.internal.network.Packet
|
||||||
|
import net.mamoe.mirai.internal.network.net.NetworkHandler
|
||||||
|
import net.mamoe.mirai.internal.network.net.NetworkHandlerContext
|
||||||
|
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
|
||||||
|
import net.mamoe.mirai.utils.*
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
|
||||||
|
private val PACKET_DEBUG = systemProp("mirai.debug.packet.logger", true)
|
||||||
|
|
||||||
|
internal abstract class NetworkHandlerSupport(
|
||||||
|
override val context: NetworkHandlerContext,
|
||||||
|
final override val coroutineContext: CoroutineContext = SupervisorJob(),
|
||||||
|
) : NetworkHandler, CoroutineScope by coroutineContext.childScope(SupervisorJob()) {
|
||||||
|
|
||||||
|
protected abstract fun initialState(): BaseStateImpl
|
||||||
|
protected abstract suspend fun sendPacketImpl(packet: OutgoingPacket)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a packet is received.
|
||||||
|
*/
|
||||||
|
protected fun collectReceived(packet: IncomingPacket) {
|
||||||
|
for (listener in packetListeners) {
|
||||||
|
if (!listener.isExpected(packet)) continue
|
||||||
|
if (packetListeners.remove(listener)) {
|
||||||
|
val e = packet.exception
|
||||||
|
if (e != null) {
|
||||||
|
listener.result.completeExceptionally(e)
|
||||||
|
} else {
|
||||||
|
listener.result.complete(packet.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun collectUnknownPacket(raw: RawIncomingPacket) {
|
||||||
|
packetLogger.debug { "Unknown packet: commandName=${raw.commandName}, body=${raw.body.toUHexString()}" }
|
||||||
|
// may add hooks here (to context)
|
||||||
|
}
|
||||||
|
|
||||||
|
final override suspend fun sendAndExpect(packet: OutgoingPacket, timeout: Long, attempts: Int): Packet? {
|
||||||
|
val listener = PacketListener(packet.commandName, packet.sequenceId)
|
||||||
|
packetListeners.add(listener)
|
||||||
|
var exception: Throwable? = null
|
||||||
|
repeat(attempts.coerceAtLeast(1)) {
|
||||||
|
try {
|
||||||
|
sendPacketImpl(packet)
|
||||||
|
try {
|
||||||
|
return withTimeout(timeout) {
|
||||||
|
listener.result.await()
|
||||||
|
}
|
||||||
|
} catch (e: TimeoutCancellationException) {
|
||||||
|
if (exception != null) {
|
||||||
|
e.addSuppressed(exception!!)
|
||||||
|
}
|
||||||
|
exception = e // show last exception
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
packetListeners.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw exception!!
|
||||||
|
}
|
||||||
|
|
||||||
|
final override suspend fun sendWithoutExpect(packet: OutgoingPacket) {
|
||||||
|
sendPacketImpl(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun close() {
|
||||||
|
coroutineContext.job.cancel("NetworkHandler closed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val packetLogger: MiraiLogger by lazy {
|
||||||
|
MiraiLogger.create(context.logger.identity + ".debug").withSwitch(PACKET_DEBUG)
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// await impl
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
protected class PacketListener(
|
||||||
|
val commandName: String,
|
||||||
|
val sequenceId: Int,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Response from server. May complete with [CompletableDeferred.completeExceptionally] for a meaningful stacktrace.
|
||||||
|
*/
|
||||||
|
val result = CompletableDeferred<Packet?>()
|
||||||
|
|
||||||
|
fun isExpected(packet: IncomingPacket): Boolean =
|
||||||
|
this.commandName == packet.commandName && this.sequenceId == packet.sequenceId
|
||||||
|
}
|
||||||
|
|
||||||
|
private val packetListeners = ConcurrentLinkedQueue<PacketListener>()
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// state impl
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A **scoped** state corresponding to [NetworkHandler.State].
|
||||||
|
*
|
||||||
|
* CoroutineScope is cancelled when switched to another state.
|
||||||
|
*/
|
||||||
|
protected abstract inner class BaseStateImpl(
|
||||||
|
val correspondingState: NetworkHandler.State,
|
||||||
|
) : CoroutineScope by CoroutineScope(coroutineContext + SupervisorJob(coroutineContext.job)) {
|
||||||
|
@Throws(Exception::class)
|
||||||
|
abstract suspend fun resumeConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State is *lazy*, initialized only if requested.
|
||||||
|
*/
|
||||||
|
@Suppress("PropertyName")
|
||||||
|
protected var _state: BaseStateImpl by lateinitMutableProperty { initialState() }
|
||||||
|
private set
|
||||||
|
|
||||||
|
final override val state: NetworkHandler.State get() = _state.correspondingState
|
||||||
|
protected fun setState(impl: BaseStateImpl) { // we can add monitor here for debug.
|
||||||
|
val old = _state
|
||||||
|
check(old !== impl) { "Old and new states cannot be the same." }
|
||||||
|
old.cancel()
|
||||||
|
_state = impl
|
||||||
|
}
|
||||||
|
|
||||||
|
final override suspend fun resumeConnection() {
|
||||||
|
_state.resumeConnection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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.impl.netty
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.network.net.NetworkHandlerContext
|
||||||
|
import net.mamoe.mirai.internal.network.net.NetworkHandlerFactory
|
||||||
|
import java.net.SocketAddress
|
||||||
|
|
||||||
|
internal object NettyNetworkHandlerFactory : NetworkHandlerFactory<NettyNetworkHandler> {
|
||||||
|
override fun create(context: NetworkHandlerContext, address: SocketAddress): NettyNetworkHandler {
|
||||||
|
return NettyNetworkHandler(context, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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.WLoginSigInfo
|
||||||
|
import net.mamoe.mirai.internal.utils.crypto.ECDH
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see AccountSecrets
|
||||||
|
*/
|
||||||
|
internal interface LoginSessionAware {
|
||||||
|
var outgoingPacketSessionId: ByteArray
|
||||||
|
|
||||||
|
/**
|
||||||
|
* always 0 for now.
|
||||||
|
*/
|
||||||
|
var loginState: Int
|
||||||
|
val ecdh: ECDH
|
||||||
|
|
||||||
|
// also present in AccountSecrets
|
||||||
|
var wLoginSigInfo: WLoginSigInfo
|
||||||
|
val randomKey: ByteArray
|
||||||
|
}
|
@ -0,0 +1,228 @@
|
|||||||
|
/*
|
||||||
|
* 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 kotlinx.io.core.*
|
||||||
|
import net.mamoe.mirai.internal.QQAndroidBot
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.packet.*
|
||||||
|
import net.mamoe.mirai.internal.utils.crypto.TEA
|
||||||
|
import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey
|
||||||
|
import net.mamoe.mirai.utils.*
|
||||||
|
import kotlin.io.use
|
||||||
|
|
||||||
|
internal object PacketCodec {
|
||||||
|
/**
|
||||||
|
* It's caller's responsibility to close [input]
|
||||||
|
* @param input received from sockets.
|
||||||
|
* @return decoded
|
||||||
|
*/
|
||||||
|
fun decodeRaw(client: LoginSessionAware, input: ByteReadPacket): RawIncomingPacket = input.run {
|
||||||
|
// login
|
||||||
|
val flag1 = readInt()
|
||||||
|
|
||||||
|
PacketLogger.verbose { "开始处理一个包" }
|
||||||
|
|
||||||
|
val flag2 = readByte().toInt()
|
||||||
|
val flag3 = readByte().toInt()
|
||||||
|
check(flag3 == 0) {
|
||||||
|
"Illegal flag3. Expected 0, whereas got $flag3. flag1=$flag1, flag2=$flag2. " +
|
||||||
|
"Remaining=${this.readBytes().toUHexString()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
readString(readInt() - 4)// uinAccount
|
||||||
|
|
||||||
|
ByteArrayPool.useInstance(this.remaining.toInt()) { buffer ->
|
||||||
|
val size = this.readAvailable(buffer)
|
||||||
|
|
||||||
|
when (flag2) {
|
||||||
|
2 -> TEA.decrypt(buffer, DECRYPTER_16_ZERO, size)
|
||||||
|
1 -> TEA.decrypt(buffer, client.wLoginSigInfo.d2Key, size)
|
||||||
|
0 -> buffer
|
||||||
|
else -> error("Unknown flag2=$flag2")
|
||||||
|
}.let { decryptedData ->
|
||||||
|
when (flag1) {
|
||||||
|
0x0A -> parseSsoFrame(client, decryptedData)
|
||||||
|
0x0B -> parseSsoFrame(client, decryptedData) // 这里可能是 uni?? 但测试时候发现结构跟 sso 一样.
|
||||||
|
else -> error("unknown flag1: ${flag1.toByte().toUHexString()}")
|
||||||
|
}
|
||||||
|
}.let { raw ->
|
||||||
|
when (flag2) {
|
||||||
|
0, 1 -> RawIncomingPacket(raw.commandName, raw.sequenceId, raw.body.readBytes())
|
||||||
|
2 -> RawIncomingPacket(
|
||||||
|
raw.commandName,
|
||||||
|
raw.sequenceId,
|
||||||
|
raw.body.withUse { parseOicqResponse(client) })
|
||||||
|
else -> error("Unknown flag2=$flag2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DecodeResult constructor(
|
||||||
|
val commandName: String,
|
||||||
|
val sequenceId: Int,
|
||||||
|
/**
|
||||||
|
* Can be passed to [PacketFactory]
|
||||||
|
*/
|
||||||
|
val body: ByteReadPacket,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun parseSsoFrame(client: LoginSessionAware, bytes: ByteArray): DecodeResult =
|
||||||
|
bytes.toReadPacket().use { input ->
|
||||||
|
val commandName: String
|
||||||
|
val ssoSequenceId: Int
|
||||||
|
val dataCompressed: Int
|
||||||
|
input.readPacketExact(input.readInt() - 4).withUse {
|
||||||
|
ssoSequenceId = readInt()
|
||||||
|
PacketLogger.verbose { "sequenceId = $ssoSequenceId" }
|
||||||
|
|
||||||
|
val returnCode = readInt()
|
||||||
|
check(returnCode == 0) {
|
||||||
|
if (returnCode <= -10000) {
|
||||||
|
// https://github.com/mamoe/mirai/issues/470
|
||||||
|
throw KnownPacketFactories.PacketFactoryIllegalStateException(
|
||||||
|
returnCode,
|
||||||
|
"returnCode = $returnCode"
|
||||||
|
)
|
||||||
|
} else "returnCode = $returnCode"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PacketLogger.isEnabled) {
|
||||||
|
val extraData = readBytes(readInt() - 4)
|
||||||
|
PacketLogger.verbose { "(sso/inner)extraData = ${extraData.toUHexString()}" }
|
||||||
|
} else {
|
||||||
|
discardExact(readInt() - 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
commandName = readString(readInt() - 4)
|
||||||
|
client.outgoingPacketSessionId = readBytes(readInt() - 4)
|
||||||
|
|
||||||
|
dataCompressed = readInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val packet = when (dataCompressed) {
|
||||||
|
0 -> {
|
||||||
|
val size = input.readInt().toLong() and 0xffffffff
|
||||||
|
if (size == input.remaining || size == input.remaining + 4) {
|
||||||
|
input
|
||||||
|
} else {
|
||||||
|
buildPacket {
|
||||||
|
writeInt(size.toInt())
|
||||||
|
writePacket(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
input.discardExact(4)
|
||||||
|
input.useBytes { data, length ->
|
||||||
|
data.unzip(0, length).let {
|
||||||
|
val size = it.toInt()
|
||||||
|
if (size == it.size || size == it.size + 4) {
|
||||||
|
it.toReadPacket(offset = 4)
|
||||||
|
} else {
|
||||||
|
it.toReadPacket()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
8 -> input
|
||||||
|
else -> error("unknown dataCompressed flag: $dataCompressed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// body
|
||||||
|
|
||||||
|
return DecodeResult(commandName, ssoSequenceId, packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteReadPacket.parseOicqResponse(
|
||||||
|
client: LoginSessionAware,
|
||||||
|
): ByteArray {
|
||||||
|
check(readByte().toInt() == 2)
|
||||||
|
this.discardExact(2)
|
||||||
|
this.discardExact(2)
|
||||||
|
this.readUShort()
|
||||||
|
this.readShort()
|
||||||
|
this.readUInt().toLong()
|
||||||
|
val encryptionMethod = this.readUShort().toInt()
|
||||||
|
|
||||||
|
this.discardExact(1)
|
||||||
|
return when (encryptionMethod) {
|
||||||
|
4 -> {
|
||||||
|
val data =
|
||||||
|
TEA.decrypt(
|
||||||
|
this.readBytes(),
|
||||||
|
client.ecdh.keyPair.initialShareKey,
|
||||||
|
length = (this.remaining - 1).toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
val peerShareKey =
|
||||||
|
client.ecdh.calculateShareKeyByPeerPublicKey(readUShortLVByteArray().adjustToPublicKey())
|
||||||
|
TEA.decrypt(data, peerShareKey)
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
// session
|
||||||
|
TEA.decrypt(
|
||||||
|
this.readBytes(),
|
||||||
|
client.wLoginSigInfo.wtSessionTicketKey,
|
||||||
|
length = (this.remaining - 1).toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
0 -> {
|
||||||
|
if (client.loginState == 0) {
|
||||||
|
val size = (this.remaining - 1).toInt()
|
||||||
|
val byteArrayBuffer = this.readBytes(size)
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
TEA.decrypt(byteArrayBuffer, client.ecdh.keyPair.initialShareKey, length = size)
|
||||||
|
}.getOrElse {
|
||||||
|
TEA.decrypt(byteArrayBuffer, client.randomKey, length = size)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TEA.decrypt(this.readBytes(), client.randomKey, length = (this.remaining - 1).toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> error("Illegal encryption method. expected 0 or 4, got $encryptionMethod")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process [RawIncomingPacket] using [IncomingPacketFactory.decode].
|
||||||
|
*
|
||||||
|
* This function wraps exceptions into [IncomingPacket]
|
||||||
|
*/
|
||||||
|
suspend fun processBody(bot: QQAndroidBot, input: RawIncomingPacket): IncomingPacket? {
|
||||||
|
val factory = KnownPacketFactories.findPacketFactory(input.commandName) ?: return null
|
||||||
|
|
||||||
|
return kotlin.runCatching {
|
||||||
|
input.body.toReadPacket().use { body ->
|
||||||
|
when (factory) {
|
||||||
|
is OutgoingPacketFactory -> factory.decode(bot, body)
|
||||||
|
is IncomingPacketFactory -> factory.decode(bot, body, input.sequenceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { packet ->
|
||||||
|
IncomingPacket(input.commandName, input.sequenceId, packet, null)
|
||||||
|
},
|
||||||
|
onFailure = { exception: Throwable ->
|
||||||
|
IncomingPacket(input.commandName, input.sequenceId, null, exception)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal open class RawIncomingPacket constructor(
|
||||||
|
val commandName: String,
|
||||||
|
val sequenceId: Int,
|
||||||
|
/**
|
||||||
|
* Can be passed to [PacketFactory]
|
||||||
|
*/
|
||||||
|
val body: ByteArray,
|
||||||
|
)
|
@ -0,0 +1,228 @@
|
|||||||
|
/*
|
||||||
|
* 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.net.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.BotConfiguration.MiraiProtocol
|
||||||
|
import net.mamoe.mirai.utils.DeviceInfo
|
||||||
|
import net.mamoe.mirai.utils.LoginSolver
|
||||||
|
import net.mamoe.mirai.utils.info
|
||||||
|
import net.mamoe.mirai.utils.withExceptionCollector
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal interface SsoContext {
|
||||||
|
var client: QQAndroidClient
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SsoController(
|
||||||
|
private val ssoContext: SsoContext,
|
||||||
|
private val handler: NetworkHandler,
|
||||||
|
) {
|
||||||
|
@Throws(LoginFailedException::class)
|
||||||
|
suspend fun login() = withExceptionCollector {
|
||||||
|
if (bot.client.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun initClient() {
|
||||||
|
val device = configuration.deviceInfo?.invoke(bot) ?: DeviceInfo.random()
|
||||||
|
ssoContext.client = QQAndroidClient(
|
||||||
|
bot.account,
|
||||||
|
device = device,
|
||||||
|
accountSecrets = loadSecretsFromCacheOrCreate(device)
|
||||||
|
).apply {
|
||||||
|
_bot = 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(bot.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.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -30,6 +30,7 @@ internal class OutgoingPacketWithRespType<R : Packet?> constructor(
|
|||||||
delegate: ByteReadPacket
|
delegate: ByteReadPacket
|
||||||
) : OutgoingPacket(name, commandName, sequenceId, delegate)
|
) : OutgoingPacket(name, commandName, sequenceId, delegate)
|
||||||
|
|
||||||
|
// TODO: 2021/4/12 generalize
|
||||||
internal open class OutgoingPacket constructor(
|
internal open class OutgoingPacket constructor(
|
||||||
name: String?,
|
name: String?,
|
||||||
val commandName: String,
|
val commandName: String,
|
||||||
@ -39,6 +40,30 @@ internal open class OutgoingPacket constructor(
|
|||||||
val name: String = name ?: commandName
|
val name: String = name ?: commandName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class IncomingPacket constructor(
|
||||||
|
val commandName: String,
|
||||||
|
val sequenceId: Int,
|
||||||
|
|
||||||
|
val data: Packet?,
|
||||||
|
/**
|
||||||
|
* If not `null`, [data] is `null`
|
||||||
|
*/
|
||||||
|
val exception: Throwable?, // may complete with exception (thrown by decoders)
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
if (exception != null) require(data == null) { "When exception is not null, data must be null." }
|
||||||
|
if (data != null) require(exception == null) { "When data is not null, exception must be null." }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return if (exception == null) {
|
||||||
|
"IncomingPacket(cmd=$commandName, seq=$sequenceId, SUCCESS, r=$data)"
|
||||||
|
} else {
|
||||||
|
"IncomingPacket(cmd=$commandName, seq=$sequenceId, FAILURE, e=$exception)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal suspend inline fun <E : Packet> OutgoingPacketWithRespType<E>.sendAndExpect(
|
internal suspend inline fun <E : Packet> OutgoingPacketWithRespType<E>.sendAndExpect(
|
||||||
network: QQAndroidBotNetworkHandler,
|
network: QQAndroidBotNetworkHandler,
|
||||||
timeoutMillis: Long = 5000,
|
timeoutMillis: Long = 5000,
|
||||||
|
@ -96,11 +96,14 @@ internal abstract class IncomingPacketFactory<TPacket : Packet?>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmName("decode0")
|
@JvmName("decode0")
|
||||||
private suspend inline fun <P : Packet?> OutgoingPacketFactory<P>.decode(bot: QQAndroidBot, packet: ByteReadPacket): P =
|
internal suspend inline fun <P : Packet?> OutgoingPacketFactory<P>.decode(
|
||||||
|
bot: QQAndroidBot,
|
||||||
|
packet: ByteReadPacket
|
||||||
|
): P =
|
||||||
packet.decode(bot)
|
packet.decode(bot)
|
||||||
|
|
||||||
@JvmName("decode1")
|
@JvmName("decode1")
|
||||||
private suspend inline fun <P : Packet?> IncomingPacketFactory<P>.decode(
|
internal suspend inline fun <P : Packet?> IncomingPacketFactory<P>.decode(
|
||||||
bot: QQAndroidBot,
|
bot: QQAndroidBot,
|
||||||
packet: ByteReadPacket,
|
packet: ByteReadPacket,
|
||||||
sequenceId: Int
|
sequenceId: Int
|
||||||
|
@ -20,7 +20,7 @@ internal object WtLogin2 : WtLoginExt {
|
|||||||
fun SubmitSliderCaptcha(
|
fun SubmitSliderCaptcha(
|
||||||
client: QQAndroidClient,
|
client: QQAndroidClient,
|
||||||
ticket: String
|
ticket: String
|
||||||
): OutgoingPacket = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
||||||
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
|
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
|
||||||
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
|
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
|
||||||
writeShort(2) // subCommand
|
writeShort(2) // subCommand
|
||||||
@ -37,7 +37,7 @@ internal object WtLogin2 : WtLoginExt {
|
|||||||
client: QQAndroidClient,
|
client: QQAndroidClient,
|
||||||
captchaSign: ByteArray,
|
captchaSign: ByteArray,
|
||||||
captchaAnswer: String
|
captchaAnswer: String
|
||||||
): OutgoingPacket = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
||||||
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
|
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
|
||||||
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
|
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
|
||||||
writeShort(2) // subCommand
|
writeShort(2) // subCommand
|
||||||
|
@ -20,7 +20,7 @@ internal object WtLogin9 : WtLoginExt {
|
|||||||
operator fun invoke(
|
operator fun invoke(
|
||||||
client: QQAndroidClient,
|
client: QQAndroidClient,
|
||||||
allowSlider: Boolean
|
allowSlider: Boolean
|
||||||
): OutgoingPacket = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
||||||
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
|
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
|
||||||
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
|
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
|
||||||
writeShort(9) // subCommand
|
writeShort(9) // subCommand
|
||||||
|
14
mirai-core/src/commonTest/kotlin/network/PacketCodecTest.kt
Normal file
14
mirai-core/src/commonTest/kotlin/network/PacketCodecTest.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
internal class PacketCodecTest : AbstractCodecTest()
|
||||||
|
|
||||||
|
internal abstract class AbstractCodecTest
|
60
mirai-core/src/commonTest/kotlin/network/sessionUtils.kt
Normal file
60
mirai-core/src/commonTest/kotlin/network/sessionUtils.kt
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* 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.event.events.BotOnlineEvent
|
||||||
|
import net.mamoe.mirai.internal.QQAndroidBot
|
||||||
|
import net.mamoe.mirai.internal.network.net.protocol.LoginSessionAware
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
|
||||||
|
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
|
||||||
|
import net.mamoe.mirai.utils.debug
|
||||||
|
import net.mamoe.mirai.utils.withUse
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal class TestLoginSessionAware(
|
||||||
|
private val accountSecrets: AccountSecrets,
|
||||||
|
override var outgoingPacketSessionId: ByteArray = byteArrayOf(1, 2, 3, 4),
|
||||||
|
override var loginState: Int = 0,
|
||||||
|
override val ecdh: ECDH = ECDH(),
|
||||||
|
) : LoginSessionAware {
|
||||||
|
override var wLoginSigInfo: WLoginSigInfo by accountSecrets::wLoginSigInfo
|
||||||
|
override val randomKey: ByteArray by accountSecrets::randomKey
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun loadSession(
|
||||||
|
resourceName: String,
|
||||||
|
): AccountSecretsImpl {
|
||||||
|
val bytes = ClassLoader.getSystemResourceAsStream(resourceName)?.withUse { readBytes() }
|
||||||
|
?: error("AccountSecrets resource '$resourceName' not found.")
|
||||||
|
return bytes.loadAs(AccountSecretsImpl.serializer())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* secure to share with others.
|
||||||
|
*/
|
||||||
|
internal fun QQAndroidClient.dumpSessionSafe(): ByteArray {
|
||||||
|
val secrets =
|
||||||
|
AccountSecretsImpl(device, account).copy(
|
||||||
|
wLoginSigInfoField = wLoginSigInfo.copy(
|
||||||
|
tgt = EMPTY_BYTE_ARRAY,
|
||||||
|
encryptA1 = EMPTY_BYTE_ARRAY,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return secrets.toByteArray(AccountSecretsImpl.serializer())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun QQAndroidBot.scheduleSafeSessionDump(outputFile: File) {
|
||||||
|
this.eventChannel.subscribeAlways<BotOnlineEvent> {
|
||||||
|
outputFile.writeBytes(client.dumpSessionSafe())
|
||||||
|
bot.logger.debug { "Dumped safe session to " }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user