mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-08 01:19:16 +08:00
Updated robot & network structure
This commit is contained in:
parent
c353e8e8f9
commit
e129719d4b
@ -10,7 +10,7 @@ class Group(robot: Robot, number: Long) : Contact(robot, number), Closeable {
|
||||
val members = ContactList<QQ>()
|
||||
|
||||
override fun sendMessage(message: Message) {
|
||||
robot.network.packetSystem.sendGroupMessage(this, message)
|
||||
robot.network.messageHandler.sendGroupMessage(this, message)
|
||||
}
|
||||
|
||||
override fun sendXMLMessage(message: String) {
|
||||
|
@ -12,7 +12,7 @@ import net.mamoe.mirai.message.defaults.At
|
||||
*/
|
||||
class QQ(robot: Robot, number: Long) : Contact(robot, number) {
|
||||
override fun sendMessage(message: Message) {
|
||||
robot.network.packetSystem.sendFriendMessage(this, message)
|
||||
robot.network.messageHandler.sendFriendMessage(this, message)
|
||||
}
|
||||
|
||||
override fun sendXMLMessage(message: String) {
|
||||
|
@ -7,88 +7,86 @@ import java.util.stream.Collectors
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
interface Protocol {
|
||||
companion object {
|
||||
val SERVER_IP: ArrayList<String> = object : ArrayList<String>() {
|
||||
init {
|
||||
add("183.60.56.29")
|
||||
object Protocol {
|
||||
val SERVER_IP: List<String> = object : ArrayList<String>() {
|
||||
init {
|
||||
add("183.60.56.29")
|
||||
|
||||
arrayOf(
|
||||
"sz2.tencent.com",
|
||||
"sz3.tencent.com",
|
||||
"sz4.tencent.com",
|
||||
"sz5.tencent.com",
|
||||
"sz6.tencent.com",
|
||||
"sz8.tencent.com",
|
||||
"sz9.tencent.com"
|
||||
).forEach { this.add(InetAddress.getByName(it).hostAddress) }
|
||||
arrayOf(
|
||||
"sz2.tencent.com",
|
||||
"sz3.tencent.com",
|
||||
"sz4.tencent.com",
|
||||
"sz5.tencent.com",
|
||||
"sz6.tencent.com",
|
||||
"sz8.tencent.com",
|
||||
"sz9.tencent.com"
|
||||
).forEach { this.add(InetAddress.getByName(it).hostAddress) }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
get() = Collections.unmodifiableList(field)
|
||||
|
||||
const val head = "02"
|
||||
const val ver = "37 13"
|
||||
const val fixVer = "03 00 00 00 01 2E 01 00 00 68 52 00 00 00 00"
|
||||
const val tail = "03"
|
||||
/**
|
||||
* _fixVer
|
||||
*/
|
||||
const val fixVer2 = "02 00 00 00 01 01 01 00 00 68 20"
|
||||
/**
|
||||
* 0825data1
|
||||
*/
|
||||
const val constantData0 = "00 18 00 16 00 01 "
|
||||
/**
|
||||
* 0825data2
|
||||
*/
|
||||
const val constantData1 = "00 00 04 53 00 00 00 01 00 00 15 85 "
|
||||
const val key0825 = "A4 F1 91 88 C9 82 14 99 0C 9E 56 55 91 23 C8 3D"
|
||||
const val redirectionKey = "A8 F2 14 5F 58 12 60 AF 07 63 97 D6 76 B2 1A 3B"
|
||||
const val publicKey = "02 6D 28 41 D2 A5 6F D2 FC 3E 2A 1F 03 75 DE 6E 28 8F A8 19 3E 5F 16 49 D3"
|
||||
const val shareKey = "1A E9 7F 7D C9 73 75 98 AC 02 E0 80 5F A9 C6 AF"
|
||||
const val fix0836 = "06 A9 12 97 B7 F8 76 25 AF AF D3 EA B4 C8 BC E7 "
|
||||
|
||||
const val head = "02"
|
||||
const val ver = "37 13"
|
||||
const val fixVer = "03 00 00 00 01 2E 01 00 00 68 52 00 00 00 00"
|
||||
const val tail = "03"
|
||||
/**
|
||||
* _fixVer
|
||||
*/
|
||||
const val fixVer2 = "02 00 00 00 01 01 01 00 00 68 20"
|
||||
/**
|
||||
* 0825data1
|
||||
*/
|
||||
const val constantData0 = "00 18 00 16 00 01 "
|
||||
/**
|
||||
* 0825data2
|
||||
*/
|
||||
const val constantData1 = "00 00 04 53 00 00 00 01 00 00 15 85 "
|
||||
const val key0825 = "A4 F1 91 88 C9 82 14 99 0C 9E 56 55 91 23 C8 3D"
|
||||
const val redirectionKey = "A8 F2 14 5F 58 12 60 AF 07 63 97 D6 76 B2 1A 3B"
|
||||
const val publicKey = "02 6D 28 41 D2 A5 6F D2 FC 3E 2A 1F 03 75 DE 6E 28 8F A8 19 3E 5F 16 49 D3"
|
||||
const val shareKey = "1A E9 7F 7D C9 73 75 98 AC 02 E0 80 5F A9 C6 AF"
|
||||
const val fix0836 = "06 A9 12 97 B7 F8 76 25 AF AF D3 EA B4 C8 BC E7 "
|
||||
const val key00BA = "C1 9C B8 C8 7B 8C 81 BA 9E 9E 7A 89 E1 7A EC 94"
|
||||
const val key00BAFix = "69 20 D1 14 74 F5 B3 93 E4 D5 02 B3 71 1A CD 2A"
|
||||
|
||||
const val key00BA = "C1 9C B8 C8 7B 8C 81 BA 9E 9E 7A 89 E1 7A EC 94"
|
||||
const val key00BAFix = "69 20 D1 14 74 F5 B3 93 E4 D5 02 B3 71 1A CD 2A"
|
||||
const val encryptKey = "“BA 42 FF 01 CF B4 FF D2 12 F0 6E A7 1B 7C B3 08”"
|
||||
|
||||
const val encryptKey = "“BA 42 FF 01 CF B4 FF D2 12 F0 6E A7 1B 7C B3 08”"
|
||||
/**
|
||||
* 0836_622_fix2
|
||||
*/
|
||||
const val passwordSubmissionKey2 = "00 15 00 30 00 01 01 27 9B C7 F5 00 10 65 03 FD 8B 00 00 00 00 00 00 00 00 00 00 00 00 02 90 49 55 33 00 10 15 74 C4 89 85 7A 19 F5 5E A9 C9 A3 5E 8A 5A 9B";
|
||||
/**
|
||||
* 0836_622_fix1
|
||||
*/
|
||||
const val passwordSubmissionKey1 = "03 00 00 00 01 01 01 00 00 68 20 00 00 00 00 00 01 01 03 00 19";
|
||||
/**
|
||||
* fix_0836_1
|
||||
*/
|
||||
const val key0836 = "EF 4A 36 6A 16 A8 E6 3D 2E EA BD 1F 98 C1 3C DA"
|
||||
|
||||
/**
|
||||
* 0836_622_fix2
|
||||
*/
|
||||
const val passwordSubmissionKey2 = "00 15 00 30 00 01 01 27 9B C7 F5 00 10 65 03 FD 8B 00 00 00 00 00 00 00 00 00 00 00 00 02 90 49 55 33 00 10 15 74 C4 89 85 7A 19 F5 5E A9 C9 A3 5E 8A 5A 9B";
|
||||
/**
|
||||
* 0836_622_fix1
|
||||
*/
|
||||
const val passwordSubmissionKey1 = "03 00 00 00 01 01 01 00 00 68 20 00 00 00 00 00 01 01 03 00 19";
|
||||
/**
|
||||
* fix_0836_1
|
||||
*/
|
||||
const val key0836 = "EF 4A 36 6A 16 A8 E6 3D 2E EA BD 1F 98 C1 3C DA"
|
||||
private val hexToByteArrayCacheMap: MutableMap<Int, ByteArray> = mutableMapOf()
|
||||
|
||||
private val hexToByteArrayCacheMap: MutableMap<Int, ByteArray> = mutableMapOf()
|
||||
|
||||
@ExperimentalUnsignedTypes
|
||||
fun hexToBytes(hex: String): ByteArray {
|
||||
hex.hashCode().let { id ->
|
||||
if (hexToByteArrayCacheMap.containsKey(id)) {
|
||||
return hexToByteArrayCacheMap[id]!!.clone()
|
||||
} else {
|
||||
hexToUBytes(hex).toByteArray().let {
|
||||
hexToByteArrayCacheMap[id] = it.clone();
|
||||
return it
|
||||
}
|
||||
@ExperimentalUnsignedTypes
|
||||
fun hexToBytes(hex: String): ByteArray {
|
||||
hex.hashCode().let { id ->
|
||||
if (hexToByteArrayCacheMap.containsKey(id)) {
|
||||
return hexToByteArrayCacheMap[id]!!.clone()
|
||||
} else {
|
||||
hexToUBytes(hex).toByteArray().let {
|
||||
hexToByteArrayCacheMap[id] = it.clone();
|
||||
return it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalUnsignedTypes
|
||||
fun hexToUBytes(hex: String): UByteArray = Arrays
|
||||
.stream(hex.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray())
|
||||
.map { value -> value.trim { it <= ' ' } }
|
||||
.map { s -> s.toUByte(16) }
|
||||
.collect(Collectors.toList()).toUByteArray()
|
||||
|
||||
}
|
||||
|
||||
@ExperimentalUnsignedTypes
|
||||
fun hexToUBytes(hex: String): UByteArray = Arrays
|
||||
.stream(hex.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray())
|
||||
.map { value -> value.trim { it <= ' ' } }
|
||||
.map { s -> s.toUByte(16) }
|
||||
.collect(Collectors.toList()).toUByteArray()
|
||||
|
||||
}
|
||||
|
@ -11,15 +11,15 @@ import net.mamoe.mirai.network.packet.action.ServerSendFriendMessageResponsePack
|
||||
import net.mamoe.mirai.network.packet.action.ServerSendGroupMessageResponsePacket
|
||||
import net.mamoe.mirai.network.packet.login.*
|
||||
import net.mamoe.mirai.task.MiraiThreadPool
|
||||
import net.mamoe.mirai.utils.ClientLoginStatus
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.getGTK
|
||||
import net.mamoe.mirai.utils.lazyEncode
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.io.Closeable
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.*
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* A RobotNetworkHandler is used to connect with Tencent servers.
|
||||
@ -28,329 +28,421 @@ import java.util.concurrent.TimeUnit
|
||||
*/
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")//to simplify code
|
||||
internal class RobotNetworkHandler(private val robot: Robot) : Closeable {
|
||||
private val socketHandler: SocketHandler = SocketHandler()
|
||||
|
||||
private var socket: DatagramSocket = DatagramSocket((15314 + Math.random() * 100).toInt())
|
||||
val debugHandler = DebugHandler()
|
||||
val loginHandler = LoginHandler()
|
||||
val messageHandler = MessageHandler()
|
||||
val actionHandler = ActionHandler()
|
||||
|
||||
private var serverIP: String = ""
|
||||
set(value) {
|
||||
serverAddress = InetSocketAddress(value, 8000)
|
||||
field = value
|
||||
private val packetHandlers: Map<KClass<out PacketHandler>, PacketHandler> = mapOf(
|
||||
DebugHandler::class to debugHandler,
|
||||
LoginHandler::class to loginHandler,
|
||||
MessageHandler::class to messageHandler,
|
||||
ActionHandler::class to actionHandler
|
||||
)
|
||||
|
||||
restartSocket()
|
||||
}
|
||||
|
||||
private lateinit var serverAddress: InetSocketAddress
|
||||
private var closed: Boolean = false
|
||||
|
||||
private lateinit var token00BA: ByteArray //这些数据全部是login用的
|
||||
private lateinit var token0825: ByteArray
|
||||
private var loginTime: Int = 0
|
||||
private lateinit var loginIP: String
|
||||
private var tgtgtKey: ByteArray? = null
|
||||
private var tlv0105: ByteArray
|
||||
/**
|
||||
* 0828_decr_key
|
||||
*/
|
||||
private lateinit var sessionResponseDecryptionKey: ByteArray
|
||||
|
||||
private var verificationCodeSequence: Int = 0//这两个验证码使用
|
||||
private var verificationCodeCache: ByteArray? = null//每次包只发一部分验证码来
|
||||
private var verificationCodeCacheCount: Int = 1//
|
||||
private lateinit var verificationToken: ByteArray
|
||||
|
||||
private lateinit var sessionKey: ByteArray//这两个是登录成功后得到的
|
||||
private lateinit var sKey: String
|
||||
|
||||
/**
|
||||
* Used to access web API(for friends list etc.)
|
||||
* Not async
|
||||
*/
|
||||
private lateinit var cookies: String
|
||||
private var gtk: Int = 0
|
||||
private var ignoreMessage: Boolean = false
|
||||
@ExperimentalUnsignedTypes
|
||||
fun sendPacket(packet: ClientPacket) {
|
||||
socketHandler.sendPacket(packet)
|
||||
}
|
||||
|
||||
private var loginState: LoginState? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (value != null) {
|
||||
loginHook?.invoke(value)
|
||||
override fun close() {
|
||||
this.packetHandlers.values.forEach {
|
||||
it.close()
|
||||
}
|
||||
this.socketHandler.close()
|
||||
}
|
||||
|
||||
|
||||
//private | internal
|
||||
|
||||
internal fun tryLogin(loginHook: ((LoginState) -> Unit)? = null) {
|
||||
val ipQueue: LinkedList<String> = LinkedList(Protocol.SERVER_IP)
|
||||
fun login(): Boolean {
|
||||
val ip = ipQueue.poll()
|
||||
return if (ip != null) {
|
||||
this@RobotNetworkHandler.socketHandler.touch(ip) { state ->
|
||||
if (state == LoginState.UNKNOWN) {
|
||||
login()
|
||||
} else {
|
||||
loginHook?.invoke(state)
|
||||
}
|
||||
}
|
||||
true
|
||||
} else false
|
||||
}
|
||||
login()
|
||||
}
|
||||
|
||||
@ExperimentalUnsignedTypes
|
||||
internal fun onPacketReceived(packet: ServerPacket) {
|
||||
this.packetHandlers.values.forEach {
|
||||
it.onPacketReceived(packet)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private inner class SocketHandler : Closeable {
|
||||
private lateinit var socket: DatagramSocket
|
||||
|
||||
internal var serverIP: String = ""
|
||||
set(value) {
|
||||
serverAddress = InetSocketAddress(value, 8000)
|
||||
field = value
|
||||
|
||||
restartSocket()
|
||||
}
|
||||
|
||||
private var loginHook: ((LoginState) -> Unit)? = null
|
||||
internal var loginState: LoginState? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (value != null && value != LoginState.UNKNOWN) {
|
||||
loginHook?.invoke(value)
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var serverAddress: InetSocketAddress
|
||||
|
||||
private fun restartSocket() {
|
||||
|
||||
socket = DatagramSocket((15314 + Math.random() * 100).toInt())
|
||||
socket.close()
|
||||
socket.connect(this.serverAddress)
|
||||
Thread {
|
||||
while (socket.isConnected) {
|
||||
val packet = DatagramPacket(ByteArray(2048), 2048)
|
||||
kotlin
|
||||
.runCatching { socket.receive(packet) }
|
||||
.onSuccess {
|
||||
MiraiThreadPool.getInstance().submit {
|
||||
try {
|
||||
onPacketReceived(ServerPacket.ofByteArray(packet.data.removeZeroTail()))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
if (it.message == "socket closed") {
|
||||
if (!closed) {
|
||||
restartSocket()
|
||||
}
|
||||
return@Thread
|
||||
}
|
||||
it.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start network and touch the server
|
||||
*/
|
||||
internal fun touch(serverAddress: String, loginHook: ((LoginState) -> Unit)? = null) {
|
||||
socketHandler.serverIP = serverAddress
|
||||
if (loginHook != null) {
|
||||
this.loginHook = loginHook
|
||||
}
|
||||
sendPacket(ClientTouchPacket(robot.account.qqNumber, socketHandler.serverIP))
|
||||
}
|
||||
|
||||
/**
|
||||
* Not async
|
||||
*/
|
||||
@ExperimentalUnsignedTypes
|
||||
internal fun sendPacket(packet: ClientPacket) {
|
||||
try {
|
||||
packet.encode()
|
||||
packet.writeHex(Protocol.tail)
|
||||
|
||||
val data = packet.toByteArray()
|
||||
socket.send(DatagramPacket(data, data.size))
|
||||
MiraiLogger info "Packet sent: $packet"
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private var loginHook: ((LoginState) -> Unit)? = null
|
||||
override fun close() {
|
||||
this.socket.close()
|
||||
this.loginState = null
|
||||
this.loginHook = null
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
tlv0105 = lazyEncode {
|
||||
|
||||
private lateinit var sessionKey: ByteArray
|
||||
|
||||
abstract inner class PacketHandler : Closeable {
|
||||
abstract fun onPacketReceived(packet: ServerPacket)
|
||||
|
||||
override fun close() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kind of [PacketHandler] that prints all packets received in the format of hex byte array.
|
||||
*/
|
||||
inner class DebugHandler : PacketHandler() {
|
||||
override fun onPacketReceived(packet: ServerPacket) {
|
||||
packet.decode()
|
||||
MiraiLogger info "Packet received: $packet"
|
||||
if (packet is ServerEventPacket) {
|
||||
sendPacket(ClientMessageResponsePacket(robot.account.qqNumber, packet.packetId, sessionKey, packet.eventIdentity))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录过程
|
||||
*/
|
||||
inner class LoginHandler : PacketHandler() {
|
||||
private lateinit var token00BA: ByteArray
|
||||
private lateinit var token0825: ByteArray
|
||||
private var loginTime: Int = 0
|
||||
private lateinit var loginIP: String
|
||||
private var tgtgtKey: ByteArray? = null
|
||||
|
||||
private var tlv0105: ByteArray = lazyEncode {
|
||||
it.writeHex("01 05 00 30")
|
||||
it.writeHex("00 01 01 02 00 14 01 01 00 10")
|
||||
it.writeRandom(16)
|
||||
it.writeHex("00 14 01 02 00 10")
|
||||
it.writeRandom(16)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to login to server
|
||||
*/
|
||||
internal fun tryLogin(loginHook: ((LoginState) -> Unit)? = null) {
|
||||
//"14.116.136.106",
|
||||
tryLogin()
|
||||
}
|
||||
/**
|
||||
* 0828_decr_key
|
||||
*/
|
||||
private lateinit var sessionResponseDecryptionKey: ByteArray
|
||||
|
||||
/**
|
||||
* Try to login to server
|
||||
*/
|
||||
private fun tryLogin(serverAddress: String, loginHook: ((LoginState) -> Unit)? = null) {
|
||||
private var verificationCodeSequence: Int = 0//这两个验证码使用
|
||||
private var verificationCodeCache: ByteArray? = null//每次包只发一部分验证码来
|
||||
private var verificationCodeCacheCount: Int = 1//
|
||||
private lateinit var verificationToken: ByteArray
|
||||
|
||||
touch(serverAddress, loginHook)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start network
|
||||
*/
|
||||
private fun touch(serverAddress: String, loginHook: ((LoginState) -> Unit)? = null) {
|
||||
serverIP = serverAddress
|
||||
if (loginHook != null) {
|
||||
this.loginHook = loginHook
|
||||
}
|
||||
this.sendPacket(ClientTouchPacket(this.robot.account.qqNumber, this.serverIP))
|
||||
}
|
||||
private var heartbeatFuture: ScheduledFuture<*>? = null
|
||||
private var sKeyRefresherFuture: ScheduledFuture<*>? = null
|
||||
|
||||
private fun restartSocket() {
|
||||
socket.close()
|
||||
socket = DatagramSocket((15314 + Math.random() * 100).toInt())
|
||||
socket.connect(this.serverAddress)
|
||||
val zeroByte: Byte = 0
|
||||
Thread {
|
||||
while (true) {
|
||||
val dp1 = DatagramPacket(ByteArray(2048), 2048)
|
||||
try {
|
||||
socket.receive(dp1)
|
||||
} catch (e: Exception) {
|
||||
if (e.message == "socket closed") {
|
||||
if (!closed) {
|
||||
restartSocket()
|
||||
}
|
||||
return@Thread
|
||||
override fun onPacketReceived(packet: ServerPacket) {
|
||||
when (packet) {
|
||||
is ServerTouchResponsePacket -> {
|
||||
if (packet.serverIP != null) {//redirection
|
||||
socketHandler.serverIP = packet.serverIP!!
|
||||
//connect(packet.serverIP!!)
|
||||
sendPacket(ClientServerRedirectionPacket(packet.serverIP!!, robot.account.qqNumber))
|
||||
} else {//password submission
|
||||
this.loginIP = packet.loginIP
|
||||
this.loginTime = packet.loginTime
|
||||
this.token0825 = packet.token0825
|
||||
this.tgtgtKey = packet.tgtgtKey
|
||||
sendPacket(ClientPasswordSubmissionPacket(robot.account.qqNumber, robot.account.password, packet.loginTime, packet.loginIP, packet.tgtgtKey, packet.token0825))
|
||||
}
|
||||
}
|
||||
MiraiThreadPool.getInstance().submit {
|
||||
var i = dp1.data.size - 1
|
||||
while (dp1.data[i] == zeroByte) {
|
||||
--i
|
||||
}
|
||||
try {
|
||||
onPacketReceived(ServerPacket.ofByteArray(dp1.data.copyOfRange(0, i + 1)))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
@ExperimentalUnsignedTypes
|
||||
internal fun onPacketReceived(packet: ServerPacket) {
|
||||
packet.decode()
|
||||
MiraiLogger info "Packet received: $packet"
|
||||
if (packet is ServerEventPacket) {
|
||||
sendPacket(ClientMessageResponsePacket(this.robot.account.qqNumber, packet.packetId, this.sessionKey, packet.eventIdentity))
|
||||
}
|
||||
when (packet) {
|
||||
is ServerTouchResponsePacket -> {
|
||||
if (packet.serverIP != null) {//redirection
|
||||
serverIP = packet.serverIP!!
|
||||
//connect(packet.serverIP!!)
|
||||
sendPacket(ClientServerRedirectionPacket(packet.serverIP!!, this.robot.account.qqNumber))
|
||||
} else {//password submission
|
||||
this.loginIP = packet.loginIP
|
||||
this.loginTime = packet.loginTime
|
||||
this.token0825 = packet.token0825
|
||||
this.tgtgtKey = packet.tgtgtKey
|
||||
sendPacket(ClientPasswordSubmissionPacket(this.robot.account.qqNumber, this.robot.account.password, packet.loginTime, packet.loginIP, packet.tgtgtKey, packet.token0825))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerLoginResponseFailedPacket -> {
|
||||
this.loginState = packet.loginState
|
||||
MiraiLogger error "Login failed: " + packet.loginState.toString()
|
||||
return
|
||||
}
|
||||
|
||||
is ServerLoginResponseVerificationCodeInitPacket -> {
|
||||
//[token00BA]来源之一: 验证码
|
||||
this.token00BA = packet.token00BA
|
||||
this.verificationCodeCache = packet.verifyCodePart1
|
||||
|
||||
|
||||
if (packet.unknownBoolean != null && packet.unknownBoolean!!) {
|
||||
this.verificationCodeSequence = 1
|
||||
sendPacket(ClientVerificationCodeTransmissionRequestPacket(1, this.robot.account.qqNumber, this.token0825, this.verificationCodeSequence, this.token00BA))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
is ServerVerificationCodeRepeatPacket -> {//todo 这个名字正确么
|
||||
this.tgtgtKey = packet.tgtgtKeyUpdate
|
||||
this.token00BA = packet.token00BA
|
||||
sendPacket(ClientLoginResendPacket3105(this.robot.account.qqNumber, this.robot.account.password, this.loginTime, this.loginIP, this.tgtgtKey!!, this.token0825, this.token00BA))
|
||||
}
|
||||
|
||||
is ServerVerificationCodeTransmissionPacket -> {
|
||||
this.verificationCodeSequence++
|
||||
this.verificationCodeCache = this.verificationCodeCache!! + packet.verificationCodePartN
|
||||
|
||||
this.verificationToken = packet.verificationToken
|
||||
this.verificationCodeCacheCount++
|
||||
|
||||
this.token00BA = packet.token00BA
|
||||
|
||||
|
||||
//todo 看易语言 count 和 sequence 是怎样变化的
|
||||
|
||||
|
||||
if (packet.transmissionCompleted) {
|
||||
this.verificationCodeCache
|
||||
TODO("验证码好了")
|
||||
} else {
|
||||
sendPacket(ClientVerificationCodeTransmissionRequestPacket(this.verificationCodeCacheCount, this.robot.account.qqNumber, this.token0825, this.verificationCodeSequence, this.token00BA))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerLoginResponseSuccessPacket -> {
|
||||
this.sessionResponseDecryptionKey = packet.sessionResponseDecryptionKey
|
||||
sendPacket(ClientSessionRequestPacket(this.robot.account.qqNumber, this.serverIP, packet.token38, packet.token88, packet.encryptionKey, this.tlv0105))
|
||||
}
|
||||
|
||||
//是ClientPasswordSubmissionPacket之后服务器回复的
|
||||
is ServerLoginResponseResendPacket -> {
|
||||
//if (packet.tokenUnknown != null) {
|
||||
//this.token00BA = packet.token00BA!!
|
||||
//println("token00BA changed!!! to " + token00BA.toUByteArray())
|
||||
//}
|
||||
if (packet.flag == ServerLoginResponseResendPacket.Flag.`08 36 31 03`) {
|
||||
this.tgtgtKey = packet.tgtgtKey
|
||||
sendPacket(ClientLoginResendPacket3104(
|
||||
this.robot.account.qqNumber,
|
||||
this.robot.account.password,
|
||||
this.loginTime,
|
||||
this.loginIP,
|
||||
this.tgtgtKey!!,
|
||||
this.token0825,
|
||||
when (packet.tokenUnknown != null) {
|
||||
true -> packet.tokenUnknown!!
|
||||
false -> this.token00BA
|
||||
},
|
||||
packet._0836_tlv0006_encr
|
||||
))
|
||||
} else {
|
||||
sendPacket(ClientLoginResendPacket3106(
|
||||
this.robot.account.qqNumber,
|
||||
this.robot.account.password,
|
||||
this.loginTime,
|
||||
this.loginIP,
|
||||
this.tgtgtKey!!,
|
||||
this.token0825,
|
||||
when (packet.tokenUnknown != null) {
|
||||
true -> packet.tokenUnknown!!
|
||||
false -> this.token00BA
|
||||
},
|
||||
packet._0836_tlv0006_encr
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
is ServerSessionKeyResponsePacket -> {
|
||||
this.sessionKey = packet.sessionKey
|
||||
MiraiThreadPool.getInstance().scheduleWithFixedDelay({
|
||||
sendPacket(ClientHeartbeatPacket(this.robot.account.qqNumber, this.sessionKey))
|
||||
}, 90000, 90000, TimeUnit.MILLISECONDS)
|
||||
RobotLoginSucceedEvent(robot).broadcast()
|
||||
|
||||
MiraiThreadPool.getInstance().schedule({
|
||||
ignoreMessage = false
|
||||
}, 2, TimeUnit.SECONDS)
|
||||
|
||||
this.tlv0105 = packet.tlv0105
|
||||
sendPacket(ClientChangeOnlineStatusPacket(this.robot.account.qqNumber, this.sessionKey, ClientLoginStatus.ONLINE))
|
||||
}
|
||||
|
||||
is ServerLoginSuccessPacket -> {
|
||||
loginState = LoginState.SUCCEED
|
||||
sendPacket(ClientSKeyRequestPacket(this.robot.account.qqNumber, this.sessionKey))
|
||||
}
|
||||
|
||||
is ServerSKeyResponsePacket -> {
|
||||
this.sKey = packet.sKey
|
||||
this.cookies = "uin=o" + this.robot.account.qqNumber + ";skey=" + this.sKey + ";"
|
||||
|
||||
MiraiThreadPool.getInstance().scheduleWithFixedDelay({
|
||||
sendPacket(ClientSKeyRefreshmentRequestPacket(this.robot.account.qqNumber, this.sessionKey))
|
||||
}, 1800000, 1800000, TimeUnit.MILLISECONDS)
|
||||
|
||||
this.gtk = getGTK(sKey)
|
||||
sendPacket(ClientAccountInfoRequestPacket(this.robot.account.qqNumber, this.sessionKey))
|
||||
}
|
||||
|
||||
is ServerHeartbeatResponsePacket -> {
|
||||
|
||||
}
|
||||
|
||||
is ServerAccountInfoResponsePacket -> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
is ServerFriendMessageEventPacket -> {
|
||||
if (ignoreMessage) {
|
||||
is ServerLoginResponseFailedPacket -> {
|
||||
socketHandler.loginState = packet.loginState
|
||||
MiraiLogger error "Login failed: " + packet.loginState.toString()
|
||||
return
|
||||
}
|
||||
|
||||
FriendMessageEvent(this.robot, this.robot.contacts.getQQ(packet.qq), packet.message)
|
||||
is ServerLoginResponseVerificationCodeInitPacket -> {
|
||||
//[token00BA]来源之一: 验证码
|
||||
this.token00BA = packet.token00BA
|
||||
this.verificationCodeCache = packet.verifyCodePart1
|
||||
|
||||
if (packet.unknownBoolean != null && packet.unknownBoolean!!) {
|
||||
this.verificationCodeSequence = 1
|
||||
sendPacket(ClientVerificationCodeTransmissionRequestPacket(1, robot.account.qqNumber, this.token0825, this.verificationCodeSequence, this.token00BA))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerVerificationCodeRepeatPacket -> {//todo 这个名字正确么
|
||||
this.tgtgtKey = packet.tgtgtKeyUpdate
|
||||
this.token00BA = packet.token00BA
|
||||
sendPacket(ClientLoginResendPacket3105(robot.account.qqNumber, robot.account.password, this.loginTime, this.loginIP, this.tgtgtKey!!, this.token0825, this.token00BA))
|
||||
}
|
||||
|
||||
is ServerVerificationCodeTransmissionPacket -> {
|
||||
this.verificationCodeSequence++
|
||||
this.verificationCodeCache = this.verificationCodeCache!! + packet.verificationCodePartN
|
||||
|
||||
this.verificationToken = packet.verificationToken
|
||||
this.verificationCodeCacheCount++
|
||||
|
||||
this.token00BA = packet.token00BA
|
||||
|
||||
|
||||
//todo 看易语言 count 和 sequence 是怎样变化的
|
||||
|
||||
if (packet.transmissionCompleted) {
|
||||
this.verificationCodeCache
|
||||
TODO("验证码好了")
|
||||
} else {
|
||||
sendPacket(ClientVerificationCodeTransmissionRequestPacket(this.verificationCodeCacheCount, robot.account.qqNumber, this.token0825, this.verificationCodeSequence, this.token00BA))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerLoginResponseSuccessPacket -> {
|
||||
this.sessionResponseDecryptionKey = packet.sessionResponseDecryptionKey
|
||||
sendPacket(ClientSessionRequestPacket(robot.account.qqNumber, socketHandler.serverIP, packet.token38, packet.token88, packet.encryptionKey, this.tlv0105))
|
||||
}
|
||||
|
||||
//是ClientPasswordSubmissionPacket之后服务器回复的
|
||||
is ServerLoginResponseResendPacket -> {
|
||||
//if (packet.tokenUnknown != null) {
|
||||
//this.token00BA = packet.token00BA!!
|
||||
//println("token00BA changed!!! to " + token00BA.toUByteArray())
|
||||
//}
|
||||
if (packet.flag == ServerLoginResponseResendPacket.Flag.`08 36 31 03`) {
|
||||
this.tgtgtKey = packet.tgtgtKey
|
||||
sendPacket(ClientLoginResendPacket3104(
|
||||
robot.account.qqNumber,
|
||||
robot.account.password,
|
||||
this.loginTime,
|
||||
this.loginIP,
|
||||
this.tgtgtKey!!,
|
||||
this.token0825,
|
||||
when (packet.tokenUnknown != null) {
|
||||
true -> packet.tokenUnknown!!
|
||||
false -> this.token00BA
|
||||
},
|
||||
packet._0836_tlv0006_encr
|
||||
))
|
||||
} else {
|
||||
sendPacket(ClientLoginResendPacket3106(
|
||||
robot.account.qqNumber,
|
||||
robot.account.password,
|
||||
this.loginTime,
|
||||
this.loginIP,
|
||||
this.tgtgtKey!!,
|
||||
this.token0825,
|
||||
when (packet.tokenUnknown != null) {
|
||||
true -> packet.tokenUnknown!!
|
||||
false -> this.token00BA
|
||||
},
|
||||
packet._0836_tlv0006_encr
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerSessionKeyResponsePacket -> {
|
||||
sessionKey = packet.sessionKey
|
||||
heartbeatFuture = MiraiThreadPool.getInstance().scheduleWithFixedDelay({
|
||||
sendPacket(ClientHeartbeatPacket(robot.account.qqNumber, sessionKey))
|
||||
}, 90000, 90000, TimeUnit.MILLISECONDS)
|
||||
|
||||
RobotLoginSucceedEvent(robot).broadcast()
|
||||
|
||||
//登录成功后会收到大量上次的消息, 忽略掉
|
||||
MiraiThreadPool.getInstance().schedule({
|
||||
(packetHandlers[MessageHandler::class] as MessageHandler).ignoreMessage = false
|
||||
}, 2, TimeUnit.SECONDS)
|
||||
|
||||
this.tlv0105 = packet.tlv0105
|
||||
sendPacket(ClientChangeOnlineStatusPacket(robot.account.qqNumber, sessionKey, ClientLoginStatus.ONLINE))
|
||||
}
|
||||
|
||||
is ServerLoginSuccessPacket -> {
|
||||
socketHandler.loginState = LoginState.SUCCEED
|
||||
sendPacket(ClientSKeyRequestPacket(robot.account.qqNumber, sessionKey))
|
||||
}
|
||||
|
||||
is ServerSKeyResponsePacket -> {
|
||||
val actionHandler = packetHandlers[ActionHandler::class] as ActionHandler
|
||||
actionHandler.sKey = packet.sKey
|
||||
actionHandler.cookies = "uin=o" + robot.account.qqNumber + ";skey=" + actionHandler.sKey + ";"
|
||||
|
||||
sKeyRefresherFuture = MiraiThreadPool.getInstance().scheduleWithFixedDelay({
|
||||
sendPacket(ClientSKeyRefreshmentRequestPacket(robot.account.qqNumber, sessionKey))
|
||||
}, 1800000, 1800000, TimeUnit.MILLISECONDS)
|
||||
|
||||
actionHandler.gtk = getGTK(actionHandler.sKey)
|
||||
sendPacket(ClientAccountInfoRequestPacket(robot.account.qqNumber, sessionKey))
|
||||
}
|
||||
|
||||
is ServerEventPacket.Raw -> onPacketReceived(packet.distribute())
|
||||
|
||||
is ServerVerificationCodePacket.Encrypted -> onPacketReceived(packet.decrypt())
|
||||
is ServerLoginResponseVerificationCodeInitPacket.Encrypted -> onPacketReceived(packet.decrypt())
|
||||
is ServerLoginResponseResendPacket.Encrypted -> onPacketReceived(packet.decrypt(this.tgtgtKey!!))
|
||||
is ServerLoginResponseSuccessPacket.Encrypted -> onPacketReceived(packet.decrypt(this.tgtgtKey!!))
|
||||
is ServerSessionKeyResponsePacket.Encrypted -> onPacketReceived(packet.decrypt(this.sessionResponseDecryptionKey))
|
||||
is ServerTouchResponsePacket.Encrypted -> onPacketReceived(packet.decrypt())
|
||||
is ServerSKeyResponsePacket.Encrypted -> onPacketReceived(packet.decrypt(sessionKey))
|
||||
is ServerAccountInfoResponsePacket.Encrypted -> onPacketReceived(packet.decrypt(sessionKey))
|
||||
is ServerEventPacket.Raw.Encrypted -> onPacketReceived(packet.decrypt(sessionKey))
|
||||
|
||||
|
||||
is ServerAccountInfoResponsePacket,
|
||||
is ServerHeartbeatResponsePacket,
|
||||
is UnknownServerPacket -> {
|
||||
//ignored
|
||||
}
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ServerGroupMessageEventPacket -> {
|
||||
//todo message chain
|
||||
//GroupMessageEvent(this.robot, this.robot.contacts.getGroupByNumber(packet.groupNumber), this.robot.contacts.getQQ(packet.qq), packet.message)
|
||||
}
|
||||
override fun close() {
|
||||
this.verificationCodeCache = null
|
||||
this.tgtgtKey = null
|
||||
|
||||
is UnknownServerEventPacket -> {
|
||||
//unknown message event
|
||||
}
|
||||
this.heartbeatFuture?.cancel(true)
|
||||
this.sKeyRefresherFuture?.cancel(true)
|
||||
|
||||
is UnknownServerPacket -> {
|
||||
|
||||
}
|
||||
|
||||
is ServerGroupUploadFileEventPacket -> {
|
||||
|
||||
}
|
||||
|
||||
is ServerEventPacket.Raw -> onPacketReceived(packet.distribute())
|
||||
|
||||
is ServerVerificationCodePacket.Encrypted -> onPacketReceived(packet.decrypt())
|
||||
is ServerLoginResponseVerificationCodeInitPacket.Encrypted -> onPacketReceived(packet.decrypt())
|
||||
is ServerLoginResponseResendPacket.Encrypted -> onPacketReceived(packet.decrypt(this.tgtgtKey!!))
|
||||
is ServerLoginResponseSuccessPacket.Encrypted -> onPacketReceived(packet.decrypt(this.tgtgtKey!!))
|
||||
is ServerSessionKeyResponsePacket.Encrypted -> onPacketReceived(packet.decrypt(this.sessionResponseDecryptionKey))
|
||||
is ServerTouchResponsePacket.Encrypted -> onPacketReceived(packet.decrypt())
|
||||
is ServerSKeyResponsePacket.Encrypted -> onPacketReceived(packet.decrypt(this.sessionKey))
|
||||
is ServerAccountInfoResponsePacket.Encrypted -> onPacketReceived(packet.decrypt(this.sessionKey))
|
||||
is ServerEventPacket.Raw.Encrypted -> onPacketReceived(packet.decrypt(this.sessionKey))
|
||||
|
||||
|
||||
is ServerSendFriendMessageResponsePacket,
|
||||
is ServerSendGroupMessageResponsePacket -> {
|
||||
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException(packet.toString())
|
||||
this.heartbeatFuture = null
|
||||
this.sKeyRefresherFuture = null
|
||||
}
|
||||
}
|
||||
|
||||
internal val packetSystem: PacketSystem = PacketSystem()
|
||||
/**
|
||||
* 处理消息事件, 承担消息发送任务.
|
||||
*/
|
||||
inner class MessageHandler : PacketHandler() {
|
||||
internal var ignoreMessage: Boolean = false
|
||||
|
||||
override fun onPacketReceived(packet: ServerPacket) {
|
||||
when (packet) {
|
||||
is ServerGroupUploadFileEventPacket -> {
|
||||
//todo
|
||||
}
|
||||
|
||||
is ServerFriendMessageEventPacket -> {
|
||||
if (ignoreMessage) {
|
||||
return
|
||||
}
|
||||
|
||||
FriendMessageEvent(robot, robot.contacts.getQQ(packet.qq), packet.message)
|
||||
}
|
||||
|
||||
is ServerGroupMessageEventPacket -> {
|
||||
//todo message chain
|
||||
//GroupMessageEvent(this.robot, robot.contacts.getGroupByNumber(packet.groupNumber), robot.contacts.getQQ(packet.qq), packet.message)
|
||||
}
|
||||
|
||||
is UnknownServerEventPacket,
|
||||
is ServerSendFriendMessageResponsePacket,
|
||||
is ServerSendGroupMessageResponsePacket -> {
|
||||
//ignored
|
||||
}
|
||||
else -> {
|
||||
//ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class PacketSystem {
|
||||
fun sendFriendMessage(qq: QQ, message: Message) {
|
||||
TODO()
|
||||
//sendPacket(ClientSendFriendMessagePacket(robot.account.qqNumber, qq.number, sessionKey, message))
|
||||
@ -360,31 +452,23 @@ internal class RobotNetworkHandler(private val robot: Robot) : Closeable {
|
||||
TODO()
|
||||
//sendPacket(ClientSendGroupMessagePacket(group.groupId, robot.account.qqNumber, sessionKey, message))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Not async
|
||||
* 动作: 获取好友列表, 点赞, 踢人等.
|
||||
* 处理动作事件, 承担动作任务.
|
||||
*/
|
||||
@ExperimentalUnsignedTypes
|
||||
fun sendPacket(packet: ClientPacket) {
|
||||
try {
|
||||
packet.encode()
|
||||
packet.writeHex(Protocol.tail)
|
||||
inner class ActionHandler : PacketHandler() {
|
||||
internal lateinit var cookies: String
|
||||
internal lateinit var sKey: String
|
||||
internal var gtk: Int = 0
|
||||
|
||||
override fun onPacketReceived(packet: ServerPacket) {
|
||||
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
val data = packet.toByteArray()
|
||||
socket.send(DatagramPacket(data, data.size))
|
||||
MiraiLogger info "Packet sent: $packet"
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
this.socket.close()
|
||||
this.loginState = null
|
||||
this.loginHook = null
|
||||
this.verificationCodeCache = null
|
||||
this.tgtgtKey = null
|
||||
}
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ class ClientAccountInfoRequestPacket(
|
||||
) : ClientPacket() {
|
||||
override fun encode() {
|
||||
this.writeRandom(2)//part of packet id
|
||||
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(Protocol.fixVer2)
|
||||
this.encryptAndWrite(sessionKey) {
|
||||
|
@ -53,11 +53,13 @@ abstract class ServerPacket(val input: DataInputStream) : Packet {
|
||||
551, 487 -> LoginState.DEVICE_LOCK
|
||||
359 -> LoginState.TAKEN_BACK
|
||||
|
||||
else -> LoginState.UNKNOWN
|
||||
/*
|
||||
//unknown
|
||||
63 -> throw IllegalArgumentException(bytes.size.toString() + " (Unknown error)")
|
||||
351 -> throw IllegalArgumentException(bytes.size.toString() + " (Illegal package data or Unknown error)")//包数据有误
|
||||
|
||||
else -> throw IllegalArgumentException(bytes.size.toString())
|
||||
else -> throw IllegalArgumentException(bytes.size.toString())*/
|
||||
}, stream)
|
||||
}
|
||||
|
||||
@ -112,17 +114,6 @@ abstract class ServerPacket(val input: DataInputStream) : Packet {
|
||||
}
|
||||
|
||||
|
||||
fun DataInputStream.readUntil(byte: Byte): ByteArray {
|
||||
var buff = byteArrayOf()
|
||||
var b: Byte
|
||||
b = readByte()
|
||||
while (b != byte) {
|
||||
buff += b
|
||||
b = readByte()
|
||||
}
|
||||
return buff
|
||||
}
|
||||
|
||||
@ExperimentalUnsignedTypes
|
||||
fun DataInputStream.readIP(): String {
|
||||
var buff = ""
|
||||
|
@ -14,5 +14,7 @@ enum class LoginState {
|
||||
DEVICE_LOCK,//设备锁
|
||||
TAKEN_BACK,//被回收
|
||||
// VERIFICATION_CODE,//需要验证码
|
||||
// SUCCEED,
|
||||
|
||||
|
||||
UNKNOWN,
|
||||
}
|
@ -11,9 +11,8 @@ import java.util.Random;
|
||||
* @author iweiz https://github.com/iweizime/StepChanger/blob/master/app/src/main/java/me/iweizi/stepchanger/qq/Cryptor.java
|
||||
*/
|
||||
public final class TEA {
|
||||
public static final TEA CRYPTOR_SHARE_KEY = new TEA(Protocol.Companion.hexToBytes(Protocol.shareKey));
|
||||
public static final TEA CRYPTOR_0825KEY = new TEA(Protocol.Companion.hexToBytes(Protocol.key0825));
|
||||
public static final TEA CRYPTOR_00BAKEY = new TEA(Protocol.Companion.hexToBytes(Protocol.key00BA));
|
||||
public static final TEA CRYPTOR_SHARE_KEY = new TEA(Protocol.INSTANCE.hexToBytes(Protocol.shareKey));
|
||||
public static final TEA CRYPTOR_0825KEY = new TEA(Protocol.INSTANCE.hexToBytes(Protocol.key0825));
|
||||
|
||||
private static final long UINT32_MASK = 0xffffffffL;
|
||||
private final long[] mKey;
|
||||
|
@ -79,7 +79,7 @@ operator fun File.plus(child: String): File = File(this, child)
|
||||
|
||||
private const val GTK_BASE_VALUE: Int = 5381
|
||||
|
||||
fun getGTK(sKey: String): Int {
|
||||
internal fun getGTK(sKey: String): Int {
|
||||
var value = GTK_BASE_VALUE
|
||||
for (c in sKey.toCharArray()) {
|
||||
value += (value shl 5) + c.toInt()
|
||||
@ -89,7 +89,7 @@ fun getGTK(sKey: String): Int {
|
||||
return value
|
||||
}
|
||||
|
||||
fun getCrc32(key: ByteArray): Int = CRC32().let { it.update(key); it.value.toInt() }
|
||||
internal fun getCrc32(key: ByteArray): Int = CRC32().let { it.update(key); it.value.toInt() }
|
||||
|
||||
|
||||
/**
|
||||
@ -122,3 +122,13 @@ fun Any.getAllDeclaredFields(): List<Field> {
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
private const val ZERO_BYTE: Byte = 0
|
||||
|
||||
fun ByteArray.removeZeroTail(): ByteArray {
|
||||
var i = this.size - 1
|
||||
while (this[i] == ZERO_BYTE) {
|
||||
--i
|
||||
}
|
||||
return this.copyOfRange(0, i + 1)
|
||||
}
|
Loading…
Reference in New Issue
Block a user