Added documents
This commit is contained in:
30 files changed
+1423
-132
No files matched your search
@@ -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
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
```
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -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实现
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
```
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
File renamed without changes.
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
-10
@@ -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,7 +1,6 @@
|
||||
[versions]
|
||||
kotlin = "2.4.10"
|
||||
kotlinx-io = "0.9.1"
|
||||
ktor-network = "3.5.2"
|
||||
coroutines-test = "1.11.0"
|
||||
kotlinx-coroutines = "1.11.0"
|
||||
ktor-core = "3.5.2"
|
||||
@@ -10,7 +9,7 @@ netty = "4.2.17.Final"
|
||||
|
||||
[libraries]
|
||||
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 = { 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" }
|
||||
|
||||
@@ -11,9 +11,9 @@ import cn.rtast.libmc.network.SocketContext
|
||||
import cn.rtast.libmc.network.SocketEngine
|
||||
|
||||
public data class ProtocolContext(
|
||||
val rsaEncryptor: RSA1024Encryptor,
|
||||
val sha1Hasher: Sha1Hasher,
|
||||
val cipherFactory: (sharedKey: ByteArray) -> NetworkCipher,
|
||||
val rsaEncryptor: RSA1024Encryptor?,
|
||||
val sha1Hasher: Sha1Hasher?,
|
||||
val cipherFactory: ((sharedKey: ByteArray) -> NetworkCipher)?,
|
||||
val authProvider: AuthenticationProvider?,
|
||||
override val engine: SocketEngine,
|
||||
) : SocketContext()
|
||||
@@ -27,12 +27,22 @@ public class ProtocolContextBuilder(private val onlineMode: Boolean) {
|
||||
|
||||
public fun build(): ProtocolContext =
|
||||
ProtocolContext(
|
||||
rsaEncryptor = if (::rsaEncryptor.isInitialized) rsaEncryptor else error("rsaEncryptor is required"),
|
||||
sha1Hasher = if (::sha1Hasher.isInitialized) sha1Hasher else error("sha1Hasher is required"),
|
||||
cipherFactory = if (::cipherFactory.isInitialized) cipherFactory else error("cipherFactory is required"),
|
||||
rsaEncryptor = if (onlineMode) {
|
||||
if (::rsaEncryptor.isInitialized) rsaEncryptor else error("rsaEncryptor is required in online mode")
|
||||
} 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) {
|
||||
if (::authProvider.isInitialized) authProvider else error("authProvider is required in online mode")
|
||||
} else if (::authProvider.isInitialized) authProvider else null,
|
||||
|
||||
engine = if (::socketEngine.isInitialized) socketEngine else error("SocketEngine is not configured")
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
public val httpClient: HttpClient = HttpClient()
|
||||
private val httpClient: HttpClient = HttpClient()
|
||||
|
||||
public val DefaultProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
rsaEncryptor = RSA1024Encryptor { key, data -> rsaEncrypt(key, data) }
|
||||
|
||||
+4
-8
@@ -29,27 +29,23 @@ public class MinecraftClient internal constructor(
|
||||
internal val accessToken: String?,
|
||||
parentJob: Job?,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
protocolContext: ProtocolContext,
|
||||
internal val protocolContext: ProtocolContext,
|
||||
) : PacketEventDispatcher(), CoroutineScope {
|
||||
internal val rsa1024Encryptor = protocolContext.rsaEncryptor
|
||||
internal val serverIdHasher = protocolContext.sha1Hasher
|
||||
internal val authProvider = protocolContext.authProvider
|
||||
internal val stateMachine = ClientStateMachine()
|
||||
|
||||
public val networkChannel: NetworkChannel = NetworkChannel(
|
||||
host, port, stateMachine,
|
||||
protocolContext.cipherFactory,
|
||||
this, protocolContext
|
||||
)
|
||||
|
||||
private val internalPacketDispatcher = InternalPacketDispatcher(this, authProvider)
|
||||
private val internalPacketDispatcher = InternalPacketDispatcher(this)
|
||||
private val clientJob = SupervisorJob(parentJob)
|
||||
private var listenJob: Job? = null
|
||||
|
||||
public val isOnlineMode: Boolean get() = accessToken != null
|
||||
public val isOnlineMode: Boolean = accessToken != null
|
||||
public val transactionManager: TransactionIdManager = TransactionIdManager()
|
||||
|
||||
public suspend fun connect(protocolVersion: Int = 776) {
|
||||
public suspend fun connect(protocolVersion: Int = CURRENT_MINECRAFT_PROTOCOL_VERSION) {
|
||||
networkChannel.connect()
|
||||
startListening()
|
||||
networkChannel.sendPacket(
|
||||
|
||||
+10
@@ -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
-9
@@ -7,7 +7,6 @@
|
||||
|
||||
package cn.rtast.libmc.protocol.event
|
||||
|
||||
import cn.rtast.libmc.crypto.AuthenticationProvider
|
||||
import cn.rtast.libmc.packet.MinecraftPacket
|
||||
import cn.rtast.libmc.protocol.client.MinecraftClient
|
||||
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.
|
||||
* Only including `Handshake`, `Login` and `Configuration` State
|
||||
*/
|
||||
public class InternalPacketDispatcher(
|
||||
private val client: MinecraftClient,
|
||||
private val authProvider: AuthenticationProvider?,
|
||||
) {
|
||||
public class InternalPacketDispatcher(private val client: MinecraftClient) {
|
||||
public suspend fun handleIncomingPackets(packet: MinecraftPacket) {
|
||||
when (packet) {
|
||||
// login
|
||||
@@ -50,16 +46,18 @@ public class InternalPacketDispatcher(
|
||||
is ClientboundHelloPacket -> {
|
||||
val sharedSecret = generateRandom16Bytes()
|
||||
if (client.isOnlineMode) {
|
||||
val serverHash = client.serverIdHasher.hash(packet.serverId, sharedSecret, packet.publicKey)
|
||||
authProvider!!.joinServer(
|
||||
val serverHash = client.protocolContext.sha1Hasher!!
|
||||
.hash(packet.serverId, sharedSecret, packet.publicKey)
|
||||
client.protocolContext.authProvider!!.joinServer(
|
||||
"https://sessionserver.mojang.com/session/minecraft/join",
|
||||
client.accessToken!!,
|
||||
client.uuid.toString().replace("-", ""),
|
||||
serverHash
|
||||
)
|
||||
}
|
||||
val encryptedSecret = client.rsa1024Encryptor.encrypt(packet.publicKey, sharedSecret)
|
||||
val encryptedVerifyToken = client.rsa1024Encryptor.encrypt(packet.publicKey, packet.verifyToken)
|
||||
val encryptedSecret = client.protocolContext.rsaEncryptor!!.encrypt(packet.publicKey, sharedSecret)
|
||||
val encryptedVerifyToken =
|
||||
client.protocolContext.rsaEncryptor!!.encrypt(packet.publicKey, packet.verifyToken)
|
||||
client.networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken))
|
||||
client.networkChannel.session.enableEncryption(sharedSecret)
|
||||
}
|
||||
|
||||
+1
-2
@@ -26,11 +26,10 @@ public class NetworkChannel internal constructor(
|
||||
host: String,
|
||||
port: Int,
|
||||
private val stateMachine: ClientStateMachine,
|
||||
cipherProvider: (ByteArray) -> NetworkCipher,
|
||||
private val dispatcher: PacketEventDispatcher,
|
||||
protocolContext: ProtocolContext,
|
||||
) {
|
||||
internal val session: NetworkSession = NetworkSession(host, port, cipherProvider, protocolContext)
|
||||
internal val session: NetworkSession = NetworkSession(host, port, protocolContext)
|
||||
|
||||
@Volatile
|
||||
private var threshold = -1
|
||||
|
||||
+1
-3
@@ -6,7 +6,6 @@
|
||||
|
||||
package cn.rtast.libmc.protocol.network
|
||||
|
||||
import cn.rtast.libmc.crypto.NetworkCipher
|
||||
import cn.rtast.libmc.crypto.ProtocolContext
|
||||
import cn.rtast.libmc.network.RawSocket
|
||||
import cn.rtast.libmc.network.ReadChannel
|
||||
@@ -16,7 +15,6 @@ import cn.rtast.libmc.primitives.readVarInt
|
||||
public class NetworkSession internal constructor(
|
||||
private val host: String,
|
||||
private val port: Int,
|
||||
private var cipherProvider: (ByteArray) -> NetworkCipher,
|
||||
private val context: ProtocolContext,
|
||||
) {
|
||||
private var socket: RawSocket? = null
|
||||
@@ -38,7 +36,7 @@ public class NetworkSession internal constructor(
|
||||
public fun enableEncryption(sharedKey: ByteArray) {
|
||||
val currentRead = requireNotNull(readChannel)
|
||||
val currentWrite = requireNotNull(writeChannel)
|
||||
val cipher = cipherProvider(sharedKey)
|
||||
val cipher = context.cipherFactory!!.invoke(sharedKey)
|
||||
this.readChannel = CipherReadChannel(currentRead, cipher)
|
||||
this.writeChannel = CipherWriteChannel(currentWrite, cipher)
|
||||
}
|
||||
|
||||
+4
@@ -10,6 +10,10 @@ package cn.rtast.libmc.protocol.util
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Client-side managed transaction id manager,
|
||||
* managed an auto-increment transaction id
|
||||
*/
|
||||
public class TransactionIdManager internal constructor() {
|
||||
private var queryTransactionCounter: 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 org.junit.Test
|
||||
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.time.Clock
|
||||
import kotlin.uuid.Uuid
|
||||
@@ -31,43 +26,14 @@ class TestClientTestInJvm {
|
||||
val accessToken = File("src/jvmTest/resources/accessToken.txt").readText()
|
||||
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
|
||||
fun `test client`() {
|
||||
val cli = createMinecraftClient(
|
||||
"127.0.0.1",
|
||||
25565,
|
||||
"RTAkland",
|
||||
// generateOfflineUuid("RTAkland"),
|
||||
"127.0.0.1", 25565, "RTAkland",
|
||||
Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
|
||||
// null,
|
||||
accessToken,
|
||||
context = DefaultProtocolContext
|
||||
)
|
||||
// cli.onPacket<ClientboundSystemChatMessagePacket> { println(it) }
|
||||
// cli.onPacket<ClientboundLoginSuccessPacket> { println(it) }
|
||||
cli.on { packet, direction -> println("$direction -> $packet") }
|
||||
cli.launch { cli.connect() }
|
||||
while (true) {
|
||||
@@ -77,11 +43,8 @@ class TestClientTestInJvm {
|
||||
@Test
|
||||
fun `test client offline mode`() {
|
||||
val cli = createMinecraftClient(
|
||||
"127.0.0.1",
|
||||
25566,
|
||||
"11",
|
||||
generateOfflineUuid("11"),
|
||||
null,
|
||||
"127.0.0.1", 25566, "11",
|
||||
generateOfflineUuid("11"), null,
|
||||
context = DefaultProtocolContext.withCustom {
|
||||
socketEngine = KtorNetworkEngine()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user