Added documents

This commit is contained in:
2026-09-08 11:13:37 +08:00
parent f82d1a1d8f
commit 08392f92b8
30 files changed
+1423 -132

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
# Assemble context API
```kotlin
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
rsaEncryptor = RSA1024Encryptor { key, data -> ... }
sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... }
cipherFactory = { key -> ... }
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... }
socketEngine = MyCustomTCPSocketImpl()
}
```
> If using `DefaultProtocolContext` from `libmc-protocol-context` as the default context implementation,
> please refer to [Implementing TCPSocket](Impl-TCP-Socket.md)
# Modify exists context API implementation
```kotlin
fun main() {
val myNewProtocolContext = DefaultProtocolContext.withCustom {
rsaEncryptor = RSA1024Encryptor { key, data -> ... }
sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... }
cipherFactory = { key -> ... }
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... }
socketEngine = MyCustomTCPSocketImpl()
}
}
```
> Call `.withCustom` on a `ProtocolContextBuilder` to modify exists context implementation
+82
View File
@@ -0,0 +1,82 @@
# Creating a Client
```kotlin
public fun main() = runBlocking {
val client = createMinecraftClient(
"127.0.0.1", 25566, "MyBot",
generateOfflineUuid("MyBot"),
accessToken = null,
context = DefaultProtocolContext.withCustom {
socketEngine = KtorNetworkEngine()
}
)
client.launch { client.connect() }
while (true) {
delay(5.seconds)
}
}
```
> In the example code above, a `MinecraftClient` is created. This client will connect to an offline server at
> `127.0.0.1:25565` using `MyBot` as the player name, and replaces the underlying TCP Socket
> implementation with a `ktor-network` based TCP Socket. (For details on how to create a SocketEngine, please refer
> to [Implementing TCP Socket](Impl-TCP-Socket.md). For details on how to
> create a Context, please refer to [Required APIs](README.md#required-apis))
> MinecraftClient implements CoroutineScope, and calling `client.connect()` will execute the connection on a background
> thread. Blocking thread to prevent the application from exiting
## Connecting to an Online-Mode Server
```kotlin
private val accessToken = "eyJraWQiOiIw..."
public fun main() = runBlocking {
val client = createMinecraftClient(
// Other parameters
username = "RTAkland",
uuid = Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
accessToken = accessToken,
// Other parameters
)
}
```
> In the example code above, the client will connect to the server using `RTAkland` as the player name.
# Get an AccessToken
Open [minecraft.net](https://minecraft.net), log in, press `F12`, and run the following code in the Console:
```javascript
console.log(`; ${document.cookie}`.split('; bearer_token=').pop().split(';').shift())
```
> Note: AccessTokens are valid for 24 hours
# Listening for Packets
```kotlin
// Listen for specific received packets (must start with Clientbound)
client.onPacket<ClientboundLoginSuccessPacket> {
println(it)
}
// Listen for all received packets
client.on { packet, direction ->
println("$direction ->$packet")
}
// Listen for outgoing packets (must start with Serverbound)
cli.onSent<ServerboundPongConfigurationPacket> {
println(it)
}
```
# Sending Packets
```kotlin
// Only packets starting with Serverbound can be sent, otherwise an UnsupportedOperationException will be thrown
client.networkChannel.sendPacket(
ServerboundChatCommandPacket(command = "say Hello from libmc")
)
```
+67
View File
@@ -0,0 +1,67 @@
# Before start
`AES-128-CFB8`, `RSA1024` and `SHA1` has been implemented in `libmc-protocol-context` module, it uses
`cryptography-kotlin`(and its platform based provider)
> All impl example below are based on Java's built-in security & cryptography API
# AES-128-CFB8
```kotlin
class JavaAesCipher(sharedKey: ByteArray) : NetworkCipher {
private val encryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply {
init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey))
}
private val decryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply {
init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey))
}
override fun encrypt(buffer: ByteArray, offset: Int, length: Int) {
encryptCipher.update(buffer, offset, length, buffer, offset)
}
override fun decrypt(buffer: ByteArray, offset: Int, length: Int) {
decryptCipher.update(buffer, offset, length, buffer, offset)
}
}
```
# RSA1024
```kotlin
private fun encrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray {
val keySpec = X509EncodedKeySpec(publicKeyBytes)
val keyFactory = KeyFactory.getInstance("RSA")
val publicKey = keyFactory.generatePublic(keySpec)
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
return cipher.doFinal(data)
}
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
rsaEncryptor = RSA1024Encryptor { key: ByteArray, data: ByteArray -> encrypt(key, data) }
}
```
# SHA1
```kotlin
private fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String {
val serverIdBytes: ByteArray = serverId.encodeToByteArray()
for (b in serverIdBytes) require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" }
val data = ByteArray(serverIdBytes.size + secretKey.size + publicKey.size)
System.arraycopy(serverIdBytes, 0, data, 0, serverIdBytes.size)
System.arraycopy(secretKey, 0, data, serverIdBytes.size, secretKey.size)
System.arraycopy(publicKey, 0, data, serverIdBytes.size + secretKey.size, publicKey.size)
val digest = MessageDigest.getInstance("SHA-1")
val hash = digest.digest(data)
return BigInteger(hash).toString(16)
}
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
sha1Hasher = Sha1Hasher { serverId: String, secretKey: ByteArray, publicKey: ByteArray ->
minecraftServerIdHash(serverId, secretKey, publicKey)
}
}
```
+41
View File
@@ -0,0 +1,41 @@
# Ktor based http client
```kotlin
private val httpClient = HttpClient()
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
val status = httpClient.post(url) {
headers { header("Content-Type", "application/json") }
setBody("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}")
}.status
require(status == HttpStatusCode.NoContent)
}
}
```
# HttpURLConnection based http client
```kotlin
private fun sendJoinRequest(url: String, accessToken: String, uuid: String, serverIdHash: String): Boolean {
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
setRequestProperty("Content-Type", "application/json")
doOutput = true
connectTimeout = 5000
readTimeout = 5000
}
val jsonPayload = """{"accessToken":"$accessToken","selectedProfile":"$uuid","serverId":"$serverIdHash"}"""
connection.outputStream.use { os -> os.write(jsonPayload.toByteArray(Charsets.UTF_8)) }
val responseCode = connection.responseCode
connection.disconnect()
return responseCode == HttpURLConnection.HTTP_NO_CONTENT
}
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
val success = sendJoinRequest(url, accessToken, uuid, serverIdHash)
require(success) { "Failed to authenticate with Mojang session server" }
}
}
```
+329
View File
@@ -0,0 +1,329 @@
# Before started
If the TCP Socket library is non-suspending, **DO NOT** wrap I/O read and write operations with `withContext`.
It will cause frequent thread context switching during network I/O, leading to a degradation in application
performance
# Netty based TCP Socket implementation
```kotlin
dependencies {
implementation("io.netty:netty-transport:4.2.17.Final")
implementation("io.netty:netty-buffer:4.2.17.Final")
}
```
<details>
<summary>Click to expand code</summary>
```kotlin
import cn.rtast.libmc.network.RawSocket
import cn.rtast.libmc.network.ReadChannel
import cn.rtast.libmc.network.SocketEngine
import cn.rtast.libmc.network.WriteChannel
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import io.netty.channel.*
import io.netty.channel.nio.NioIoHandler
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import io.netty.channel.Channel as NettyChannel
import kotlinx.coroutines.channels.Channel as KotlinChannel
public class NettyNetworkSocketEngine : SocketEngine {
override fun create(host: String, port: Int): RawSocket = NettyNetworkSocket(host, port)
private class NettyNetworkSocket(private val host: String, private val port: Int) : RawSocket {
private val workerGroup: EventLoopGroup = MultiThreadIoEventLoopGroup(0, NioIoHandler.newFactory())
private lateinit var channel: NettyChannel
private val inboundQueue = KotlinChannel<ByteArray>(KotlinChannel.UNLIMITED)
private lateinit var readChannel: NettyReadChannel
private lateinit var writeChannel: NettyWriteChannel
override suspend fun connect() {
val bootstrap = Bootstrap()
.group(workerGroup)
.channel(NioSocketChannel::class.java)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.handler(object : ChannelInitializer<SocketChannel>() {
override fun initChannel(ch: SocketChannel) {
ch.pipeline().addLast(object : ChannelInboundHandlerAdapter() {
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is ByteBuf) {
try {
val bytes = ByteArray(msg.readableBytes())
msg.readBytes(bytes)
inboundQueue.trySend(bytes)
} finally {
msg.release()
}
}
}
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
inboundQueue.close(cause)
ctx.close()
}
override fun channelInactive(ctx: ChannelHandlerContext) {
inboundQueue.close()
super.channelInactive(ctx)
}
})
}
})
channel = withContext(Dispatchers.IO) {
bootstrap.connect(host, port).suspendAwait()
}
readChannel = NettyReadChannel(inboundQueue)
writeChannel = NettyWriteChannel(channel)
}
override fun openReadChannel(): ReadChannel = readChannel
override fun openWriteChannel(): WriteChannel = writeChannel
override fun close() {
if (::channel.isInitialized && channel.isOpen) {
channel.close()
}
inboundQueue.close()
workerGroup.shutdownGracefully()
}
}
private class NettyReadChannel(private val inboundQueue: KotlinChannel<ByteArray>) : ReadChannel {
private var currentChunk: ByteArray? = null
private var chunkOffset = 0
private suspend fun fetchNextChunk() {
val next = inboundQueue.receiveCatching().getOrNull()
?: throw IllegalStateException("Socket/Channel closed while reading")
currentChunk = next
chunkOffset = 0
}
override suspend fun readByte(): Byte {
while (currentChunk == null || chunkOffset >= currentChunk!!.size) {
fetchNextChunk()
}
val chunk = currentChunk!!
return chunk[chunkOffset++]
}
override suspend fun readBytes(length: Int): ByteArray {
val result = ByteArray(length)
readFully(result, 0, length)
return result
}
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
var written = start
while (written < end) {
while (currentChunk == null || chunkOffset >= currentChunk!!.size) {
fetchNextChunk()
}
val chunk = currentChunk!!
val available = chunk.size - chunkOffset
val toCopy = minOf(available, end - written)
chunk.copyInto(
out,
destinationOffset = written,
startIndex = chunkOffset,
endIndex = chunkOffset + toCopy
)
chunkOffset += toCopy
written += toCopy
}
}
}
private class NettyWriteChannel(private val channel: NettyChannel) : WriteChannel {
private val writeMutex = Mutex()
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
val length = endIndex - startIndex
if (length <= 0) return
val nettyBuf = Unpooled.copiedBuffer(value, startIndex, length)
writeMutex.withLock {
withContext(Dispatchers.IO) {
channel.writeAndFlush(nettyBuf).suspendAwait()
}
}
}
override suspend fun flush() {
writeMutex.withLock {
withContext(Dispatchers.IO) {
channel.flush()
}
}
}
}
}
private suspend inline fun ChannelFuture.suspendAwait(): NettyChannel = suspendCancellableCoroutine { cont ->
if (isDone) {
if (isSuccess) cont.resume(channel())
else cont.resumeWithException(cause() ?: RuntimeException("Netty operation failed"))
return@suspendCancellableCoroutine
}
addListener { future ->
if (future.isSuccess) {
cont.resume(channel())
} else {
cont.resumeWithException(future.cause() ?: RuntimeException("Netty operation failed"))
}
}
cont.invokeOnCancellation { cancel(false) }
}
```
> Also copy the imports
</details>
# ktor-network based TCP Socket implementation
```kotlin
dependencies {
implementation("io.ktor:ktor-network:3.5.2")
}
```
<details>
<summary>Click to expand</summary>
```kotlin
public class KtorNetworkEngine : SocketEngine {
override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port)
}
public class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket {
private val sm = SelectorManager(Dispatchers.IO)
private lateinit var socket: Socket
override suspend fun connect(): Unit = run { socket = aSocket(sm).tcp().connect(host, port) }
override fun openReadChannel(): ReadChannel = KtorReadChannel(socket.openReadChannel())
override fun openWriteChannel(): WriteChannel = KtorWriteChannel(socket.openWriteChannel())
override fun close() {
socket.close()
sm.close()
}
}
public class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel {
override suspend fun readByte(): Byte = readChannel.readByte()
override suspend fun readBytes(length: Int): ByteArray = readChannel.readByteArray(length)
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit =
readChannel.readFully(out, start, end)
}
public class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel {
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
writeChannel.writeFully(value, startIndex, endIndex)
}
override suspend fun flush(): Unit = writeChannel.flush()
}
```
</details>
# Java built-in Socket based implementation
<details>
<summary>Click to expand code</summary>
```kotlin
class JavaSocketEngine : SocketEngine {
override fun create(host: String, port: Int): RawSocket = JavaNetworkSocket(host, port)
}
class JavaNetworkSocket(
private val host: String,
private val port: Int,
private val connectTimeoutMs: Int = 10000,
) : RawSocket {
private lateinit var socket: Socket
private lateinit var readChannel: JavaReadChannel
private lateinit var writeChannel: JavaWriteChannel
override suspend fun connect() {
val s = Socket()
s.tcpNoDelay = true
s.connect(InetSocketAddress(host, port), connectTimeoutMs)
socket = s
readChannel = JavaReadChannel(s.getInputStream())
writeChannel = JavaWriteChannel(s.getOutputStream())
}
override fun openReadChannel(): ReadChannel = readChannel
override fun openWriteChannel(): WriteChannel = writeChannel
override fun close() {
if (::socket.isInitialized && !socket.isClosed) {
runCatching { socket.close() }
}
}
}
class JavaReadChannel(private val inputStream: InputStream) : ReadChannel {
override suspend fun readByte(): Byte {
val b = inputStream.read()
if (b == -1) throw IllegalStateException("Socket stream reached EOF while reading byte")
return b.toByte()
}
override suspend fun readBytes(length: Int): ByteArray {
val buffer = ByteArray(length)
readFullyInternal(buffer, 0, length)
return buffer
}
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
readFullyInternal(out, start, end - start)
}
private fun readFullyInternal(out: ByteArray, offset: Int, length: Int) {
var bytesRead = 0
while (bytesRead < length) {
val count = inputStream.read(out, offset + bytesRead, length - bytesRead)
if (count == -1) {
throw IllegalStateException("Socket stream closed unexpectedly (read $bytesRead of $length bytes)")
}
bytesRead += count
}
}
}
class JavaWriteChannel(private val outputStream: OutputStream) : WriteChannel {
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
outputStream.write(value, startIndex, endIndex - startIndex)
}
override suspend fun flush() {
outputStream.flush()
}
}
```
</details>
+29
View File
@@ -0,0 +1,29 @@
# libmc
A lightweight, modern Minecraft client protocol library designed for Kotlin Native & JVM. The core protocol library
module relies on the following dependencies:
- Standard Library (`kotlin-stdlib`)
- I/O (`kotlinx-io`) - Efficiently wraps each packet into a Buffer
- Coroutines (`kotlinx-coroutines`) - Enables high-performance asynchronous operations
## Required APIs
| Module Name | Required | Notes |
|:-------------|:------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| TCP Socket | Yes | The `protocol` module does not have a built-in TCP Socket implementation. [Implement TCP Socket](Impl-TCP-Socket.md) |
| AES-128-CFB8 | Conditional | Required only when logging into an `online-mode` server to encrypt/decrypt traffic. [Implement AES-128-CFB8](Impl-Crypto.md#AES-128-CFB8) |
| RSA 1024 | Conditional | Required only when logging into an `online-mode` server to encrypt the shared secret with the server's public key. [Implement RSA 1024](Impl-Crypto.md#RSA1024) |
| SHA1 | Conditional | Required only when logging into an `online-mode` server to compute the Server ID hash. [Implement SHA1](Impl-Crypto.md#SHA1) |
| HTTP Client | Conditional | Required only when logging into an `online-mode` server to send join request to mojang's session server. [Implement HTTP Client](Impl-HTTP-Client.md) |
# Get started
> `libmc-protocol` is current under development. It only supports the latest Minecraft version
> (Current supported Minecraft version: `26.2`, Protocol Version: `776`)
[Start using libmc-protocol](Get-started.md)
# Assemble all context APIs
[Assemble context APIs](Assemble-context.md)
-16
View File
@@ -1,16 +0,0 @@
* **AES-128-CFB8** (`NetworkCipher`): Handles in-place network packet encryption and decryption.
* **SHA-1** (`Sha1Hasher`): Computes the Minecraft server ID hash for online-mode authentication.
* **RSA-1024** (`RSA1024Encryptor`): Encrypts the shared secret and verify token during the login handshake.
* **Auth Join Request** (`AuthenticationProvider`): Sends the HTTP POST request to Mojang's session server
(`https://sessionserver.mojang.com/session/minecraft/join`).
[Use libmc-protocol-encrypt](https://repo.rtast.cn/packages/-/cn.rtast.libmc:protocol-encrypt)
```kotlin
fun main() {
val client = createMinecraftClient(
// other parameter
crypto = DefaultProtocolContext
)
}
```
+31
View File
@@ -0,0 +1,31 @@
# 组合上下文API
```kotlin
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
rsaEncryptor = RSA1024Encryptor { key, data -> ... }
sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... }
cipherFactory = { key -> ... }
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... }
socketEngine = MyCustomTCPSocketImpl()
}
```
> 如果使用`libmc-protocol-context`中的`DefaultProtocolContext`作为默认上下文实现,
> 请参阅[实现 TCPSocket](Impl-TCP-Socket-zh.md)
# 修改默认实现
```kotlin
fun main() {
val myNewProtocolContext = DefaultProtocolContext.withCustom {
rsaEncryptor = RSA1024Encryptor { key, data -> ... }
sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... }
cipherFactory = { key -> ... }
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... }
socketEngine = MyCustomTCPSocketImpl()
}
}
```
> 使用`.withCustom`来修改已有的上下文API实现
+79
View File
@@ -0,0 +1,79 @@
# 创建客户端
```kotlin
public fun main() = runBlocking {
val client = createMinecraftClient(
"127.0.0.1", 25566, "MyBot",
generateOfflineUuid("MyBot"),
accessToken = null,
context = DefaultProtocolContext.withCustom {
socketEngine = KtorNetworkEngine()
}
)
client.launch { client.connect() }
while (true) {
delay(5.seconds)
}
}
```
> 在上面的示例代码中, 创建了一个`MinecraftClient`, 这个客户端将会使用`MyBot`作为玩家名称连接到`127.0.0.1:25565`的`离线`
> 服务器, 并且将底层TCP Socket实现替换为了基于`ktor-network`实现的TCP Socket.
> (有关如何创建`SocketEngine`请参阅[实现TCP Socket](Impl-TCP-Socket-zh.md),
> 有关如何创建Context请参阅[需要实现的API](README-zh.md#需要实现的api))
> `MinecraftClient`实现了`CoroutineContext`, 使用`client.connect()`后将会切换到后台线程执行, 添加阻塞线程代码以免程序直接退出.
## 连接到在线服务器
```kotlin
private val accessToken = "eyJraWQiOiIw..."
public fun main() = runBlocking {
val client = createMinecraftClient(
// 其他参数
username = "RTAkland",
uuid = Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
accessToken = accessToken,
// 其他参数
)
}
```
> 上面的示例代码中, 将会使用`RTAkland`作为玩家名称连接到服务器
### 快速获取AccessToken
打开[minecraft.net](https://minecraft.net)并登录后按下F12, 在Console内输入
```javascript
console.log(`; ${document.cookie}`.split('; bearer_token=').pop().split(';').shift())
```
> AccessToken的有效期为24小时
# 监听数据包
```kotlin
// 监听接收到的指定类型的数据包, 必须以Clientbound开头
client.onPacket<ClientboundLoginSuccessPacket> {
println(it)
}
// 监听所有接收到的数据包
client.on { packet, direction ->
println("$direction -> $packet")
}
// 监听被发送出去的数据包, 必须以Serverbound开头
cli.onSent<ServerboundPongConfigurationPacket> {
println(it)
}
```
# 发送数据包
```kotlin
// 必须以Serverbound开头的数据包才可以被发送, 否则将会抛出UnsupportedOperationException异常
client.networkChannel.sendPacket(
ServerboundChatCommandPacket(command = "say Hello from libmc")
)
```
+67
View File
@@ -0,0 +1,67 @@
# Before start
`libmc-protocol-context` 模块中默认实现了 `AES-128-CFB8`, `RSA1024` 以及 `SHA1`,
三种加密/签名算法都基于`cryptography-kotlin`及其对应平台的provider
> 下面给出的示例代码均基于Java内置的密码学API
# AES-128-CFB8
```kotlin
class JavaAesCipher(sharedKey: ByteArray) : NetworkCipher {
private val encryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply {
init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey))
}
private val decryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply {
init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey))
}
override fun encrypt(buffer: ByteArray, offset: Int, length: Int) {
encryptCipher.update(buffer, offset, length, buffer, offset)
}
override fun decrypt(buffer: ByteArray, offset: Int, length: Int) {
decryptCipher.update(buffer, offset, length, buffer, offset)
}
}
```
# RSA1024
```kotlin
private fun encrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray {
val keySpec = X509EncodedKeySpec(publicKeyBytes)
val keyFactory = KeyFactory.getInstance("RSA")
val publicKey = keyFactory.generatePublic(keySpec)
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
return cipher.doFinal(data)
}
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
rsaEncryptor = RSA1024Encryptor { key: ByteArray, data: ByteArray -> encrypt(key, data) }
}
```
# SHA1
```kotlin
private fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String {
val serverIdBytes: ByteArray = serverId.encodeToByteArray()
for (b in serverIdBytes) require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" }
val data = ByteArray(serverIdBytes.size + secretKey.size + publicKey.size)
System.arraycopy(serverIdBytes, 0, data, 0, serverIdBytes.size)
System.arraycopy(secretKey, 0, data, serverIdBytes.size, secretKey.size)
System.arraycopy(publicKey, 0, data, serverIdBytes.size + secretKey.size, publicKey.size)
val digest = MessageDigest.getInstance("SHA-1")
val hash = digest.digest(data)
return BigInteger(hash).toString(16)
}
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
sha1Hasher = Sha1Hasher { serverId: String, secretKey: ByteArray, publicKey: ByteArray ->
minecraftServerIdHash(serverId, secretKey, publicKey)
}
}
```
+45
View File
@@ -0,0 +1,45 @@
# 前言
`libmc-protocol-context` 中提供了默认HTTP客户端实现, 如果使用了此模块则不需要手动配置HTTP客户端认证部分
# 基于Ktor的HTTP客户端
```kotlin
private val httpClient = HttpClient()
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
val status = httpClient.post(url) {
headers { header("Content-Type", "application/json") }
setBody("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}")
}.status
require(status == HttpStatusCode.NoContent)
}
}
```
# 基于Java8 HttpURLConnection的HTTP客户端
```kotlin
private fun sendJoinRequest(url: String, accessToken: String, uuid: String, serverIdHash: String): Boolean {
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
setRequestProperty("Content-Type", "application/json")
doOutput = true
connectTimeout = 5000
readTimeout = 5000
}
val jsonPayload = """{"accessToken":"$accessToken","selectedProfile":"$uuid","serverId":"$serverIdHash"}"""
connection.outputStream.use { os -> os.write(jsonPayload.toByteArray(Charsets.UTF_8)) }
val responseCode = connection.responseCode
connection.disconnect()
return responseCode == HttpURLConnection.HTTP_NO_CONTENT
}
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
val success = sendJoinRequest(url, accessToken, uuid, serverIdHash)
require(success) { "Failed to authenticate with Mojang session server" }
}
}
```
+327
View File
@@ -0,0 +1,327 @@
# 前言
如果底层TCP Socket库是非挂起的, 那么**不要**将IO读写操作使用`withContext`包裹,
这会导致在网络IO中频繁的切换线程导致程序性能**下降**.
# 基于Netty的TCP Socket实现
```kotlin
dependencies {
implementation("io.netty:netty-transport:4.2.17.Final")
implementation("io.netty:netty-buffer:4.2.17.Final")
}
```
<details>
<summary>点击展开代码</summary>
```kotlin
import cn.rtast.libmc.network.RawSocket
import cn.rtast.libmc.network.ReadChannel
import cn.rtast.libmc.network.SocketEngine
import cn.rtast.libmc.network.WriteChannel
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import io.netty.channel.*
import io.netty.channel.nio.NioIoHandler
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import io.netty.channel.Channel as NettyChannel
import kotlinx.coroutines.channels.Channel as KotlinChannel
public class NettyNetworkSocketEngine : SocketEngine {
override fun create(host: String, port: Int): RawSocket = NettyNetworkSocket(host, port)
private class NettyNetworkSocket(private val host: String, private val port: Int) : RawSocket {
private val workerGroup: EventLoopGroup = MultiThreadIoEventLoopGroup(0, NioIoHandler.newFactory())
private lateinit var channel: NettyChannel
private val inboundQueue = KotlinChannel<ByteArray>(KotlinChannel.UNLIMITED)
private lateinit var readChannel: NettyReadChannel
private lateinit var writeChannel: NettyWriteChannel
override suspend fun connect() {
val bootstrap = Bootstrap()
.group(workerGroup)
.channel(NioSocketChannel::class.java)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.handler(object : ChannelInitializer<SocketChannel>() {
override fun initChannel(ch: SocketChannel) {
ch.pipeline().addLast(object : ChannelInboundHandlerAdapter() {
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is ByteBuf) {
try {
val bytes = ByteArray(msg.readableBytes())
msg.readBytes(bytes)
inboundQueue.trySend(bytes)
} finally {
msg.release()
}
}
}
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
inboundQueue.close(cause)
ctx.close()
}
override fun channelInactive(ctx: ChannelHandlerContext) {
inboundQueue.close()
super.channelInactive(ctx)
}
})
}
})
channel = withContext(Dispatchers.IO) {
bootstrap.connect(host, port).suspendAwait()
}
readChannel = NettyReadChannel(inboundQueue)
writeChannel = NettyWriteChannel(channel)
}
override fun openReadChannel(): ReadChannel = readChannel
override fun openWriteChannel(): WriteChannel = writeChannel
override fun close() {
if (::channel.isInitialized && channel.isOpen) {
channel.close()
}
inboundQueue.close()
workerGroup.shutdownGracefully()
}
}
private class NettyReadChannel(private val inboundQueue: KotlinChannel<ByteArray>) : ReadChannel {
private var currentChunk: ByteArray? = null
private var chunkOffset = 0
private suspend fun fetchNextChunk() {
val next = inboundQueue.receiveCatching().getOrNull()
?: throw IllegalStateException("Socket/Channel closed while reading")
currentChunk = next
chunkOffset = 0
}
override suspend fun readByte(): Byte {
while (currentChunk == null || chunkOffset >= currentChunk!!.size) {
fetchNextChunk()
}
val chunk = currentChunk!!
return chunk[chunkOffset++]
}
override suspend fun readBytes(length: Int): ByteArray {
val result = ByteArray(length)
readFully(result, 0, length)
return result
}
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
var written = start
while (written < end) {
while (currentChunk == null || chunkOffset >= currentChunk!!.size) {
fetchNextChunk()
}
val chunk = currentChunk!!
val available = chunk.size - chunkOffset
val toCopy = minOf(available, end - written)
chunk.copyInto(
out,
destinationOffset = written,
startIndex = chunkOffset,
endIndex = chunkOffset + toCopy
)
chunkOffset += toCopy
written += toCopy
}
}
}
private class NettyWriteChannel(private val channel: NettyChannel) : WriteChannel {
private val writeMutex = Mutex()
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
val length = endIndex - startIndex
if (length <= 0) return
val nettyBuf = Unpooled.copiedBuffer(value, startIndex, length)
writeMutex.withLock {
withContext(Dispatchers.IO) {
channel.writeAndFlush(nettyBuf).suspendAwait()
}
}
}
override suspend fun flush() {
writeMutex.withLock {
withContext(Dispatchers.IO) {
channel.flush()
}
}
}
}
}
private suspend inline fun ChannelFuture.suspendAwait(): NettyChannel = suspendCancellableCoroutine { cont ->
if (isDone) {
if (isSuccess) cont.resume(channel())
else cont.resumeWithException(cause() ?: RuntimeException("Netty operation failed"))
return@suspendCancellableCoroutine
}
addListener { future ->
if (future.isSuccess) {
cont.resume(channel())
} else {
cont.resumeWithException(future.cause() ?: RuntimeException("Netty operation failed"))
}
}
cont.invokeOnCancellation { cancel(false) }
}
```
> 请一并复制import部分的代码
</details>
# 基于ktor-network的TCP Socket实现
```kotlin
dependencies {
implementation("io.ktor:ktor-network:3.5.2")
}
```
<details>
<summary>点击展开代码</summary>
```kotlin
public class KtorNetworkEngine : SocketEngine {
override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port)
}
public class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket {
private val sm = SelectorManager(Dispatchers.IO)
private lateinit var socket: Socket
override suspend fun connect(): Unit = run { socket = aSocket(sm).tcp().connect(host, port) }
override fun openReadChannel(): ReadChannel = KtorReadChannel(socket.openReadChannel())
override fun openWriteChannel(): WriteChannel = KtorWriteChannel(socket.openWriteChannel())
override fun close() {
socket.close()
sm.close()
}
}
public class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel {
override suspend fun readByte(): Byte = readChannel.readByte()
override suspend fun readBytes(length: Int): ByteArray = readChannel.readByteArray(length)
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit =
readChannel.readFully(out, start, end)
}
public class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel {
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
writeChannel.writeFully(value, startIndex, endIndex)
}
override suspend fun flush(): Unit = writeChannel.flush()
}
```
</details>
# 基于Java内置Socket的TCP Socket实现
<details>
<summary>点击展开代码</summary>
```kotlin
class JavaSocketEngine : SocketEngine {
override fun create(host: String, port: Int): RawSocket = JavaNetworkSocket(host, port)
}
class JavaNetworkSocket(
private val host: String,
private val port: Int,
private val connectTimeoutMs: Int = 10000,
) : RawSocket {
private lateinit var socket: Socket
private lateinit var readChannel: JavaReadChannel
private lateinit var writeChannel: JavaWriteChannel
override suspend fun connect() {
val s = Socket()
s.tcpNoDelay = true
s.connect(InetSocketAddress(host, port), connectTimeoutMs)
socket = s
readChannel = JavaReadChannel(s.getInputStream())
writeChannel = JavaWriteChannel(s.getOutputStream())
}
override fun openReadChannel(): ReadChannel = readChannel
override fun openWriteChannel(): WriteChannel = writeChannel
override fun close() {
if (::socket.isInitialized && !socket.isClosed) {
runCatching { socket.close() }
}
}
}
class JavaReadChannel(private val inputStream: InputStream) : ReadChannel {
override suspend fun readByte(): Byte {
val b = inputStream.read()
if (b == -1) throw IllegalStateException("Socket stream reached EOF while reading byte")
return b.toByte()
}
override suspend fun readBytes(length: Int): ByteArray {
val buffer = ByteArray(length)
readFullyInternal(buffer, 0, length)
return buffer
}
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
readFullyInternal(out, start, end - start)
}
private fun readFullyInternal(out: ByteArray, offset: Int, length: Int) {
var bytesRead = 0
while (bytesRead < length) {
val count = inputStream.read(out, offset + bytesRead, length - bytesRead)
if (count == -1) {
throw IllegalStateException("Socket stream closed unexpectedly (read $bytesRead of $length bytes)")
}
bytesRead += count
}
}
}
class JavaWriteChannel(private val outputStream: OutputStream) : WriteChannel {
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
outputStream.write(value, startIndex, endIndex - startIndex)
}
override suspend fun flush() {
outputStream.flush()
}
}
```
</details>
+28
View File
@@ -0,0 +1,28 @@
# libmc
一个轻量级适用于Kotlin Native & Jvm的现代Minecraft客户端协议库, 核心协议库模块使用了以下依赖
- 标准库 (`kotlin-stdlib`)
- IO (`kotlinx-io`) - 高效的将每个数据包封装为一个Buffer
- 协程库 (`kotlinx-coroutines`) - 实现高性能异步操作
## 需要实现的API
| 名称 | 是否必须实现 | 描述 |
|:-------------|:-------------|:-----------------------------------------------------------------------------------------------------------------------------------------------|
| TCP Socket | 是 | `protocol` 模块没有内置TCP Socket实现. [实现 TCP Socket](Impl-TCP-Socket-zh.md) |
| AES-128-CFB8 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于加密/解密流量. [实现AES-128-CFB8](Impl-Crypto-zh.md#AES-128-CFB8) |
| RSA 1024 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于对服务器下发的公钥进行签名. [实现RSA 1024](Impl-Crypto-zh.md#RSA1024) |
| SHA1 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于计算服务器的ServerId. [实现SHA1](Impl-Crypto-zh.md#SHA1) |
| HTTP 客户端 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于向Mojang的Session服务器发送加入服务器的请求 [实现HTTP 客户端](Impl-HTTP-Client-zh.md) |
# 开始使用
> `libmc-protocol`目前正在开发中, 也许存在着某些未被发现的问题.
> 它仅支持最新的Minecraft版本 (目前支持的游戏版本: `26.2`, 协议版本: `776`)
[开始使用libmc-protocol](Get-started-zh.md)
# 将实现的API组合起来
[将所有上下文API组合](Assemble-context-zh.md)
@@ -0,0 +1,89 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/7
*/
package engines
import cn.rtast.libmc.network.RawSocket
import cn.rtast.libmc.network.ReadChannel
import cn.rtast.libmc.network.SocketEngine
import cn.rtast.libmc.network.WriteChannel
import java.io.InputStream
import java.io.OutputStream
import java.net.InetSocketAddress
import java.net.Socket
class JavaSocketEngine : SocketEngine {
override fun create(host: String, port: Int): RawSocket = JavaNetworkSocket(host, port)
}
class JavaNetworkSocket(
private val host: String,
private val port: Int,
private val connectTimeoutMs: Int = 10000,
) : RawSocket {
private lateinit var socket: Socket
private lateinit var readChannel: JavaReadChannel
private lateinit var writeChannel: JavaWriteChannel
override suspend fun connect() {
val s = Socket()
s.tcpNoDelay = true
s.connect(InetSocketAddress(host, port), connectTimeoutMs)
socket = s
readChannel = JavaReadChannel(s.getInputStream())
writeChannel = JavaWriteChannel(s.getOutputStream())
}
override fun openReadChannel(): ReadChannel = readChannel
override fun openWriteChannel(): WriteChannel = writeChannel
override fun close() {
if (::socket.isInitialized && !socket.isClosed) {
runCatching { socket.close() }
}
}
}
class JavaReadChannel(private val inputStream: InputStream) : ReadChannel {
override suspend fun readByte(): Byte {
val b = inputStream.read()
if (b == -1) throw IllegalStateException("Socket stream reached EOF while reading byte")
return b.toByte()
}
override suspend fun readBytes(length: Int): ByteArray {
val buffer = ByteArray(length)
readFullyInternal(buffer, 0, length)
return buffer
}
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
readFullyInternal(out, start, end - start)
}
private fun readFullyInternal(out: ByteArray, offset: Int, length: Int) {
var bytesRead = 0
while (bytesRead < length) {
val count = inputStream.read(out, offset + bytesRead, length - bytesRead)
if (count == -1) {
throw IllegalStateException("Socket stream closed unexpectedly (read $bytesRead of $length bytes)")
}
bytesRead += count
}
}
}
class JavaWriteChannel(private val outputStream: OutputStream) : WriteChannel {
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
outputStream.write(value, startIndex, endIndex - startIndex)
}
override suspend fun flush() {
outputStream.flush()
}
}
@@ -0,0 +1,41 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package engines
public class KtorNetworkEngine : SocketEngine {
override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port)
}
public class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket {
private val sm = SelectorManager(Dispatchers.IO)
private lateinit var socket: Socket
override suspend fun connect(): Unit = run { socket = aSocket(sm).tcp().connect(host, port) }
override fun openReadChannel(): ReadChannel = KtorReadChannel(socket.openReadChannel())
override fun openWriteChannel(): WriteChannel = KtorWriteChannel(socket.openWriteChannel())
override fun close() {
socket.close()
sm.close()
}
}
public class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel {
override suspend fun readByte(): Byte = readChannel.readByte()
override suspend fun readBytes(length: Int): ByteArray = readChannel.readByteArray(length)
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit =
readChannel.readFully(out, start, end)
}
public class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel {
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
writeChannel.writeFully(value, startIndex, endIndex)
}
override suspend fun flush(): Unit = writeChannel.flush()
}
@@ -1,19 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
kotlin {
explicitApi()
withSourcesJar()
linuxX64()
linuxArm64()
macosArm64()
mingwX64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets {
jvmMain.dependencies {
api(project(":protocol"))
api(libs.ktor.network)
}
}
}
@@ -1,10 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package engines
@@ -1,16 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
kotlin {
explicitApi()
withSourcesJar()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets {
jvmMain.dependencies {
api(project(":protocol"))
api(libs.netty.handler)
api(libs.netty.buffer)
}
}
}
+1 -2
View File
@@ -1,7 +1,6 @@
[versions] [versions]
kotlin = "2.4.10" kotlin = "2.4.10"
kotlinx-io = "0.9.1" kotlinx-io = "0.9.1"
ktor-network = "3.5.2"
coroutines-test = "1.11.0" coroutines-test = "1.11.0"
kotlinx-coroutines = "1.11.0" kotlinx-coroutines = "1.11.0"
ktor-core = "3.5.2" ktor-core = "3.5.2"
@@ -10,7 +9,7 @@ netty = "4.2.17.Final"
[libraries] [libraries]
kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" } kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor-network" } ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor-core" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines-test" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines-test" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
cryptography-provider-optimal = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "cryptography-core" } cryptography-provider-optimal = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "cryptography-core" }
@@ -11,9 +11,9 @@ import cn.rtast.libmc.network.SocketContext
import cn.rtast.libmc.network.SocketEngine import cn.rtast.libmc.network.SocketEngine
public data class ProtocolContext( public data class ProtocolContext(
val rsaEncryptor: RSA1024Encryptor, val rsaEncryptor: RSA1024Encryptor?,
val sha1Hasher: Sha1Hasher, val sha1Hasher: Sha1Hasher?,
val cipherFactory: (sharedKey: ByteArray) -> NetworkCipher, val cipherFactory: ((sharedKey: ByteArray) -> NetworkCipher)?,
val authProvider: AuthenticationProvider?, val authProvider: AuthenticationProvider?,
override val engine: SocketEngine, override val engine: SocketEngine,
) : SocketContext() ) : SocketContext()
@@ -27,12 +27,22 @@ public class ProtocolContextBuilder(private val onlineMode: Boolean) {
public fun build(): ProtocolContext = public fun build(): ProtocolContext =
ProtocolContext( ProtocolContext(
rsaEncryptor = if (::rsaEncryptor.isInitialized) rsaEncryptor else error("rsaEncryptor is required"), rsaEncryptor = if (onlineMode) {
sha1Hasher = if (::sha1Hasher.isInitialized) sha1Hasher else error("sha1Hasher is required"), if (::rsaEncryptor.isInitialized) rsaEncryptor else error("rsaEncryptor is required in online mode")
cipherFactory = if (::cipherFactory.isInitialized) cipherFactory else error("cipherFactory is required"), } else if (::rsaEncryptor.isInitialized) rsaEncryptor else null,
sha1Hasher = if (onlineMode) {
if (::sha1Hasher.isInitialized) sha1Hasher else error("sha1Hasher is required in online mode")
} else if (::sha1Hasher.isInitialized) sha1Hasher else null,
cipherFactory = if (onlineMode) {
if (::cipherFactory.isInitialized) cipherFactory else error("cipherFactory is required in online mode")
} else if (::cipherFactory.isInitialized) cipherFactory else null,
authProvider = if (onlineMode) { authProvider = if (onlineMode) {
if (::authProvider.isInitialized) authProvider else error("authProvider is required in online mode") if (::authProvider.isInitialized) authProvider else error("authProvider is required in online mode")
} else if (::authProvider.isInitialized) authProvider else null, } else if (::authProvider.isInitialized) authProvider else null,
engine = if (::socketEngine.isInitialized) socketEngine else error("SocketEngine is not configured") engine = if (::socketEngine.isInitialized) socketEngine else error("SocketEngine is not configured")
) )
} }
@@ -15,7 +15,7 @@ import io.ktor.client.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.http.* import io.ktor.http.*
public val httpClient: HttpClient = HttpClient() private val httpClient: HttpClient = HttpClient()
public val DefaultProtocolContext: ProtocolContextBuilder.() -> Unit = { public val DefaultProtocolContext: ProtocolContextBuilder.() -> Unit = {
rsaEncryptor = RSA1024Encryptor { key, data -> rsaEncrypt(key, data) } rsaEncryptor = RSA1024Encryptor { key, data -> rsaEncrypt(key, data) }
@@ -29,27 +29,23 @@ public class MinecraftClient internal constructor(
internal val accessToken: String?, internal val accessToken: String?,
parentJob: Job?, parentJob: Job?,
private val ioDispatcher: CoroutineDispatcher, private val ioDispatcher: CoroutineDispatcher,
protocolContext: ProtocolContext, internal val protocolContext: ProtocolContext,
) : PacketEventDispatcher(), CoroutineScope { ) : PacketEventDispatcher(), CoroutineScope {
internal val rsa1024Encryptor = protocolContext.rsaEncryptor
internal val serverIdHasher = protocolContext.sha1Hasher
internal val authProvider = protocolContext.authProvider
internal val stateMachine = ClientStateMachine() internal val stateMachine = ClientStateMachine()
public val networkChannel: NetworkChannel = NetworkChannel( public val networkChannel: NetworkChannel = NetworkChannel(
host, port, stateMachine, host, port, stateMachine,
protocolContext.cipherFactory,
this, protocolContext this, protocolContext
) )
private val internalPacketDispatcher = InternalPacketDispatcher(this, authProvider) private val internalPacketDispatcher = InternalPacketDispatcher(this)
private val clientJob = SupervisorJob(parentJob) private val clientJob = SupervisorJob(parentJob)
private var listenJob: Job? = null private var listenJob: Job? = null
public val isOnlineMode: Boolean get() = accessToken != null public val isOnlineMode: Boolean = accessToken != null
public val transactionManager: TransactionIdManager = TransactionIdManager() public val transactionManager: TransactionIdManager = TransactionIdManager()
public suspend fun connect(protocolVersion: Int = 776) { public suspend fun connect(protocolVersion: Int = CURRENT_MINECRAFT_PROTOCOL_VERSION) {
networkChannel.connect() networkChannel.connect()
startListening() startListening()
networkChannel.sendPacket( networkChannel.sendPacket(
@@ -0,0 +1,10 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package cn.rtast.libmc.protocol.client
internal const val CURRENT_MINECRAFT_PROTOCOL_VERSION: Int = 776
@@ -7,7 +7,6 @@
package cn.rtast.libmc.protocol.event package cn.rtast.libmc.protocol.event
import cn.rtast.libmc.crypto.AuthenticationProvider
import cn.rtast.libmc.packet.MinecraftPacket import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.protocol.client.MinecraftClient import cn.rtast.libmc.protocol.client.MinecraftClient
import cn.rtast.libmc.protocol.packet.configuration.clientbound.* import cn.rtast.libmc.protocol.packet.configuration.clientbound.*
@@ -33,10 +32,7 @@ import cn.rtast.libmc.protocol.util.generateRandom16Bytes
* Auto respond packets the server needed. * Auto respond packets the server needed.
* Only including `Handshake`, `Login` and `Configuration` State * Only including `Handshake`, `Login` and `Configuration` State
*/ */
public class InternalPacketDispatcher( public class InternalPacketDispatcher(private val client: MinecraftClient) {
private val client: MinecraftClient,
private val authProvider: AuthenticationProvider?,
) {
public suspend fun handleIncomingPackets(packet: MinecraftPacket) { public suspend fun handleIncomingPackets(packet: MinecraftPacket) {
when (packet) { when (packet) {
// login // login
@@ -50,16 +46,18 @@ public class InternalPacketDispatcher(
is ClientboundHelloPacket -> { is ClientboundHelloPacket -> {
val sharedSecret = generateRandom16Bytes() val sharedSecret = generateRandom16Bytes()
if (client.isOnlineMode) { if (client.isOnlineMode) {
val serverHash = client.serverIdHasher.hash(packet.serverId, sharedSecret, packet.publicKey) val serverHash = client.protocolContext.sha1Hasher!!
authProvider!!.joinServer( .hash(packet.serverId, sharedSecret, packet.publicKey)
client.protocolContext.authProvider!!.joinServer(
"https://sessionserver.mojang.com/session/minecraft/join", "https://sessionserver.mojang.com/session/minecraft/join",
client.accessToken!!, client.accessToken!!,
client.uuid.toString().replace("-", ""), client.uuid.toString().replace("-", ""),
serverHash serverHash
) )
} }
val encryptedSecret = client.rsa1024Encryptor.encrypt(packet.publicKey, sharedSecret) val encryptedSecret = client.protocolContext.rsaEncryptor!!.encrypt(packet.publicKey, sharedSecret)
val encryptedVerifyToken = client.rsa1024Encryptor.encrypt(packet.publicKey, packet.verifyToken) val encryptedVerifyToken =
client.protocolContext.rsaEncryptor!!.encrypt(packet.publicKey, packet.verifyToken)
client.networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken)) client.networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken))
client.networkChannel.session.enableEncryption(sharedSecret) client.networkChannel.session.enableEncryption(sharedSecret)
} }
@@ -26,11 +26,10 @@ public class NetworkChannel internal constructor(
host: String, host: String,
port: Int, port: Int,
private val stateMachine: ClientStateMachine, private val stateMachine: ClientStateMachine,
cipherProvider: (ByteArray) -> NetworkCipher,
private val dispatcher: PacketEventDispatcher, private val dispatcher: PacketEventDispatcher,
protocolContext: ProtocolContext, protocolContext: ProtocolContext,
) { ) {
internal val session: NetworkSession = NetworkSession(host, port, cipherProvider, protocolContext) internal val session: NetworkSession = NetworkSession(host, port, protocolContext)
@Volatile @Volatile
private var threshold = -1 private var threshold = -1
@@ -6,7 +6,6 @@
package cn.rtast.libmc.protocol.network package cn.rtast.libmc.protocol.network
import cn.rtast.libmc.crypto.NetworkCipher
import cn.rtast.libmc.crypto.ProtocolContext import cn.rtast.libmc.crypto.ProtocolContext
import cn.rtast.libmc.network.RawSocket import cn.rtast.libmc.network.RawSocket
import cn.rtast.libmc.network.ReadChannel import cn.rtast.libmc.network.ReadChannel
@@ -16,7 +15,6 @@ import cn.rtast.libmc.primitives.readVarInt
public class NetworkSession internal constructor( public class NetworkSession internal constructor(
private val host: String, private val host: String,
private val port: Int, private val port: Int,
private var cipherProvider: (ByteArray) -> NetworkCipher,
private val context: ProtocolContext, private val context: ProtocolContext,
) { ) {
private var socket: RawSocket? = null private var socket: RawSocket? = null
@@ -38,7 +36,7 @@ public class NetworkSession internal constructor(
public fun enableEncryption(sharedKey: ByteArray) { public fun enableEncryption(sharedKey: ByteArray) {
val currentRead = requireNotNull(readChannel) val currentRead = requireNotNull(readChannel)
val currentWrite = requireNotNull(writeChannel) val currentWrite = requireNotNull(writeChannel)
val cipher = cipherProvider(sharedKey) val cipher = context.cipherFactory!!.invoke(sharedKey)
this.readChannel = CipherReadChannel(currentRead, cipher) this.readChannel = CipherReadChannel(currentRead, cipher)
this.writeChannel = CipherWriteChannel(currentWrite, cipher) this.writeChannel = CipherWriteChannel(currentWrite, cipher)
} }
@@ -10,6 +10,10 @@ package cn.rtast.libmc.protocol.util
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
/**
* Client-side managed transaction id manager,
* managed an auto-increment transaction id
*/
public class TransactionIdManager internal constructor() { public class TransactionIdManager internal constructor() {
private var queryTransactionCounter: Int = 1 private var queryTransactionCounter: Int = 1
private var commandSuggestionTransactionCounter: Int = 1 private var commandSuggestionTransactionCounter: Int = 1
@@ -0,0 +1,89 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/7
*/
package test
import cn.rtast.libmc.network.RawSocket
import cn.rtast.libmc.network.ReadChannel
import cn.rtast.libmc.network.SocketEngine
import cn.rtast.libmc.network.WriteChannel
import java.io.InputStream
import java.io.OutputStream
import java.net.InetSocketAddress
import java.net.Socket
class JavaSocketEngine : SocketEngine {
override fun create(host: String, port: Int): RawSocket = JavaNetworkSocket(host, port)
}
class JavaNetworkSocket(
private val host: String,
private val port: Int,
private val connectTimeoutMs: Int = 10000,
) : RawSocket {
private lateinit var socket: Socket
private lateinit var readChannel: JavaReadChannel
private lateinit var writeChannel: JavaWriteChannel
override suspend fun connect() {
val s = Socket()
s.tcpNoDelay = true
s.connect(InetSocketAddress(host, port), connectTimeoutMs)
socket = s
readChannel = JavaReadChannel(s.getInputStream())
writeChannel = JavaWriteChannel(s.getOutputStream())
}
override fun openReadChannel(): ReadChannel = readChannel
override fun openWriteChannel(): WriteChannel = writeChannel
override fun close() {
if (::socket.isInitialized && !socket.isClosed) {
runCatching { socket.close() }
}
}
}
class JavaReadChannel(private val inputStream: InputStream) : ReadChannel {
override suspend fun readByte(): Byte {
val b = inputStream.read()
if (b == -1) throw IllegalStateException("Socket stream reached EOF while reading byte")
return b.toByte()
}
override suspend fun readBytes(length: Int): ByteArray {
val buffer = ByteArray(length)
readFullyInternal(buffer, 0, length)
return buffer
}
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
readFullyInternal(out, start, end - start)
}
private fun readFullyInternal(out: ByteArray, offset: Int, length: Int) {
var bytesRead = 0
while (bytesRead < length) {
val count = inputStream.read(out, offset + bytesRead, length - bytesRead)
if (count == -1) {
throw IllegalStateException("Socket stream closed unexpectedly (read $bytesRead of $length bytes)")
}
bytesRead += count
}
}
}
class JavaWriteChannel(private val outputStream: OutputStream) : WriteChannel {
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
outputStream.write(value, startIndex, endIndex - startIndex)
}
override suspend fun flush() {
outputStream.flush()
}
}
@@ -17,11 +17,6 @@ import cn.rtast.libmc.protocol.util.generateOfflineUuid
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.math.BigInteger
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher
import kotlin.random.Random import kotlin.random.Random
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
@@ -31,43 +26,14 @@ class TestClientTestInJvm {
val accessToken = File("src/jvmTest/resources/accessToken.txt").readText() val accessToken = File("src/jvmTest/resources/accessToken.txt").readText()
private val chatTracker = ClientChatTracker() private val chatTracker = ClientChatTracker()
fun encrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray {
val keySpec = X509EncodedKeySpec(publicKeyBytes)
val keyFactory = KeyFactory.getInstance("RSA")
val publicKey = keyFactory.generatePublic(keySpec)
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
return cipher.doFinal(data)
}
fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String {
val serverIdBytes: ByteArray = serverId.encodeToByteArray()
for (b in serverIdBytes) {
require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" }
}
val data = ByteArray(serverIdBytes.size + secretKey.size + publicKey.size)
System.arraycopy(serverIdBytes, 0, data, 0, serverIdBytes.size)
System.arraycopy(secretKey, 0, data, serverIdBytes.size, secretKey.size)
System.arraycopy(publicKey, 0, data, serverIdBytes.size + secretKey.size, publicKey.size)
val digest = MessageDigest.getInstance("SHA-1")
val hash = digest.digest(data)
return BigInteger(hash).toString(16)
}
@Test @Test
fun `test client`() { fun `test client`() {
val cli = createMinecraftClient( val cli = createMinecraftClient(
"127.0.0.1", "127.0.0.1", 25565, "RTAkland",
25565,
"RTAkland",
// generateOfflineUuid("RTAkland"),
Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"), Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
// null,
accessToken, accessToken,
context = DefaultProtocolContext context = DefaultProtocolContext
) )
// cli.onPacket<ClientboundSystemChatMessagePacket> { println(it) }
// cli.onPacket<ClientboundLoginSuccessPacket> { println(it) }
cli.on { packet, direction -> println("$direction -> $packet") } cli.on { packet, direction -> println("$direction -> $packet") }
cli.launch { cli.connect() } cli.launch { cli.connect() }
while (true) { while (true) {
@@ -77,11 +43,8 @@ class TestClientTestInJvm {
@Test @Test
fun `test client offline mode`() { fun `test client offline mode`() {
val cli = createMinecraftClient( val cli = createMinecraftClient(
"127.0.0.1", "127.0.0.1", 25566, "11",
25566, generateOfflineUuid("11"), null,
"11",
generateOfflineUuid("11"),
null,
context = DefaultProtocolContext.withCustom { context = DefaultProtocolContext.withCustom {
socketEngine = KtorNetworkEngine() socketEngine = KtorNetworkEngine()
} }