mucking around with sockets and rcon

This commit is contained in:
Adam 2021-10-15 23:43:42 +02:00
parent 17b4fc3b9a
commit c3ac3bd34a
12 changed files with 613 additions and 4 deletions

View file

@ -0,0 +1,15 @@
plugins {
id("dev.adamko.factoriowebmap.archetype.kotlin-jvm")
}
//group = "${rootProject.group}.server"
//version = rootProject.version
//
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
implementation("io.ktor:ktor-network:1.6.4")
}

View file

@ -0,0 +1,124 @@
package dev.adamko.ktrcon
import java.io.*
import java.net.Socket
import java.net.SocketException
import java.nio.BufferUnderflowException
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.channels.AsynchronousSocketChannel
import kotlin.coroutines.coroutineContext
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
import kotlin.random.nextUInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.yield
class KtRconClient(
private val host: String,
private val port: Int,
private val password: Password,
) : AutoCloseable {
private val rconClientScope = CoroutineScope(SupervisorJob())
private val socket: Socket = Socket(host, port)
init {
runBlocking {
send(PacketType.AUTH_REQUEST, password())
}
require(socket.isConnected) { "Socket must connect" }
println("socket.isConnected: ${socket.isConnected}")
// val j = rconClientScope.launch {
runCatching {
val reader = socket.getInputStream()
.bufferedReader()
.lineSequence()
.asFlow()
.onEach {
println("socket response: [$it]")
}
.launchIn(rconClientScope)
// while (socket.isConnected) {
// println("response: ${input.read().toByte().toInt().toChar()}")
println("is listener active? ${reader.isActive}")
reader.invokeOnCompletion {
println("cancelled input stream job")
}
}
// }
// yield()
// }
// }
}
suspend fun send(type: PacketType, body: String) = supervisorScope {
val p = Packet(Random.nextUInt(), type, body)
println("Sending packet: $p - totalSize: ${p.totalSize}")
println("p.size: ${p.size}")
println("p.id: ${p.id}")
println("p.type.value: ${p.type.value}")
// println("p.bodyBytes: ${p.bodyBytes} / ${p.body}")
// println("p.terminator: ${p.terminator} / ${p.terminator.decodeToString()}")
// println("p.byteArray: ${p.toByteArray()}")
val bb = p.toByteBuffer2()
// ByteBuffer.wrap(p.toByteArray())
// bb.order(ByteOrder.LITTLE_ENDIAN)
// val bb = ByteBuffer.allocate(p.totalSize * 2).apply {
// order(ByteOrder.LITTLE_ENDIAN)
// putInt(p.size)
// putInt(p.id.toInt())
// putInt(p.type.value)
// put(p.bodyBytes)
// put(p.terminator)
// }
runCatching {
socket.getOutputStream().use { os ->
os.write(bb.array())
os.flush()
}
}
// socket.channel.finishConnect()
// socket.getInputStream().use { input ->
// println("running inputddd")
//// while (true) {
// println("response: ${input.readAllBytes().decodeToString()}")
//
//// yield()
//// }
// }
}
override fun close() {
rconClientScope.cancel("Closing ${this::class}")
socket.close()
}
}

View file

@ -0,0 +1,103 @@
package dev.adamko.ktrcon
import java.io.*
import java.net.InetSocketAddress
import java.net.Socket
import java.net.SocketException
import java.nio.BufferUnderflowException
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.channels.AsynchronousSocketChannel
import java.nio.channels.CompletionHandler
import java.util.concurrent.TimeUnit
import kotlin.coroutines.coroutineContext
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
import kotlin.random.nextUInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.yield
class KtRconClient2(
private val host: String,
private val port: Int,
private val password: Password,
) : AutoCloseable {
private val rconClientScope = CoroutineScope(SupervisorJob())
private val client: AsynchronousSocketChannel by lazy {
println("Initialising client")
val client = AsynchronousSocketChannel.open()
val address = InetSocketAddress(host, port)
val result = client.connect(address)
result.get() //.get(2, TimeUnit.SECONDS)
println("connected to async - $address")
require(client.isOpen) { "Require client is open" }
client
}
init {
}
suspend fun send(type: PacketType, body: String) = supervisorScope {
val p = Packet(Random.nextUInt(1u..100u), type, body)
println("Sending packet: $p - totalSize: ${p.totalSize}")
println("p.size: ${p.size}")
println("p.totalSize: ${p.totalSize}")
println("p.id: ${p.id}")
println("p.type.value: ${p.type.value}")
// println("p.bodyBytes: ${p.bodyBytes} / ${p.body}")
// println("p.terminator: ${p.terminator} / ${p.terminator.decodeToString()}")
// println("p.byteArray: ${p.toByteArray()}")
// println("p.byteArray: ${p.toByteBuffer().array()}")
runCatching {
val bb = p.toByteBuffer2()
// ByteBuffer.wrap(p.toByteArray())
// bb.order(ByteOrder.LITTLE_ENDIAN)
println("writing bb to client")
bb.compact()
val future = client.write(bb)
delay(2000)
val writeResult = future.get() //.get(2, TimeUnit.SECONDS)
delay(2000)
println("write result: $writeResult")
val response = ByteBuffer.allocate(Int.SIZE_BYTES)
val responseStatus = runCatching {
client.read(response).get() //.get(2, TimeUnit.SECONDS)
}
println("responseStatus: $responseStatus")
println("response from server: ${response.array().decodeToString()}")
delay(2000)
}
}
override fun close() {
rconClientScope.cancel("Closing ${this::class}")
client.shutdownInput()
client.close()
}
}

View file

@ -0,0 +1,130 @@
package dev.adamko.ktrcon
import io.ktor.network.selector.ActorSelectorManager
import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel
import io.ktor.util.decodeString
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.ByteWriteChannel
import io.ktor.utils.io.core.isNotEmpty
import io.ktor.utils.io.core.readBytes
import io.ktor.utils.io.readRemaining
import io.ktor.utils.io.readUTF8Line
import io.ktor.utils.io.writeIntLittleEndian
import io.ktor.utils.io.writeStringUtf8
import java.io.*
import java.net.InetSocketAddress
import java.net.Socket
import java.net.SocketException
import java.nio.BufferUnderflowException
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.channels.AsynchronousSocketChannel
import java.nio.channels.CompletionHandler
import java.util.concurrent.TimeUnit
import kotlin.coroutines.coroutineContext
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
import kotlin.random.nextUInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.yield
class KtRconClient3(
private val host: String,
private val port: Int,
private val password: Password,
) : AutoCloseable {
private val rconClientJob = SupervisorJob()
private val rconClientScope = CoroutineScope(rconClientJob)
private val input: ByteReadChannel
private val output: ByteWriteChannel
init {
val socket = runBlocking {
val selector = ActorSelectorManager(rconClientJob)
aSocket(selector)
.tcp()
.connect(InetSocketAddress(host, port))
}
input = socket.openReadChannel()
output = socket.openWriteChannel()
// runBlocking {
// output.writeStringUtf8("test")
// output.flush()
// delay(1000)
// println("availableForRead: ${input.availableForRead}")
//
// }
rconClientScope.launch {
while (!input.isClosedForRead) {
val packet = input.readRemaining(1)
while (packet.isNotEmpty) {
val bytes = packet.readBytes()
println(
"received ${bytes.size} bytes: ${bytes.joinToString { "$it" }} - ${bytes.decodeToString()}"
)
yield()
}
yield()
}
}
}
suspend fun send(type: PacketType, body: String) = supervisorScope {
val p = Packet(Random.nextUInt(1u..100u), type, body)
println("Sending packet: $p - totalSize: ${p.totalSize}")
println("p.size: ${p.size}")
println("p.totalSize: ${p.totalSize}")
println("p.id: ${p.id}")
println("p.type.value: ${p.type.value}")
// println("p.bodyBytes: ${p.bodyBytes} / ${p.body}")
// println("p.terminator: ${p.terminator} / ${p.terminator.decodeToString()}")
// println("p.byteArray: ${p.toByteArray()}")
println("p.byteArray: ${p.toByteBuffer2().array().joinToString { it.toString() }}")
val bb = p.toByteBuffer2()
println("bb: ${bb.position()} - ${bb.toString()}")
bb.array().forEach {
print("-$it")
output.writeByte(it)
}
// output.writeFully(bb)
// output.writeFully(bb)
output.flush()
println("finished writing")
// if (type == PacketType.EXEC_COMMAND) {
// println("awaiting response from server")
// input.read(0) {
// println("server response: ${it.array()}")
// }
//// val response = input.readUTF8Line()
//// println("Server said $response")
// }
}
override fun close() {
rconClientJob.complete()
rconClientScope.cancel("Closing ${this::class}")
}
}

View file

@ -0,0 +1,152 @@
package dev.adamko.ktrcon
import io.ktor.utils.io.bits.reverseByteOrder
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* @param[size] The packet size field is a 32-bit little endian integer, representing the length of the
* request in bytes. Note that the packet size field itself is not included when determining the
* size of the packet, so the value of this field is always 4 less than the packet's actual
* length.
*
* The minimum possible value for packet size is 10. The maximum possible value of packet size
* is 4096. If the response is too large to fit into one packet, it will be split and sent as
* multiple packets.
*
* @param[id] The packet id field is a 32-bit little endian integer chosen by the client for each
* request. It may be set to any positive integer. When the server responds to the request, the
* response packet will have the same packet id as the original request (unless it is a failed
* [PacketType.AUTH_RESPONSE] packet.) It need not be unique, but if a unique packet id is
* assigned, it can be used to match incoming responses to their corresponding requests.
*
* @param[type] See [PacketType]
*
* @param[body] The packet body field is a null-terminated string encoded in ASCII (i.e. ASCIIZ).
* Depending on the packet type, it may contain either the RCON password for the server, the command
* to be executed, or the server's response to a request.
*/
data class Packet(
val id: UInt,
val type: PacketType,
val bodyRaw: String,
) {
private val bodyTerminator: Byte = NULL_CHAR_BYTE
private val body: ByteArray = bodyRaw.toByteArray(Charsets.UTF_8)
// val bodyBytes = body.toByteArray(Charsets.UTF_8)
/** The size of this packet (note that the size itself is excluded from the calculation.) */
val size: Int = run {
val idSize = 1 * Int.SIZE_BYTES
val typeSize = 1 * Int.SIZE_BYTES
val bodySize = body.size * Byte.SIZE_BYTES
val bodyTerminatorSize = 1 * Byte.SIZE_BYTES
val packetTerminatorSize = 1 * Byte.SIZE_BYTES
val total = idSize + typeSize + bodySize + bodyTerminatorSize + packetTerminatorSize
println(
"packetSize: $total = $idSize + $typeSize + $bodySize + $bodyTerminatorSize + $packetTerminatorSize"
)
total
}
val totalSize: Int = size + Int.SIZE_BYTES
init {
println("totalSize: $totalSize")
println("body: ${body.joinToString { it.toString() }}")
}
// fun toByteArray(): ByteArray {
// val array = buildList {
// add(this@Packet.size.toByte())
// add(id.toByte())
// add(type.value.toByte())
// addAll(body.toByteArray(Charsets.UTF_8).asIterable())
// add(bodyTerminator)
// add(RCON_PACKET_TERMINATOR)
// }.toByteArray()
//
// println(array.joinToString { it.toInt().toChar().toString().padStart(3, ' ') })
// println(array.joinToString { it.toInt().toString().padStart(3, ' ') })
// println(array.joinToString { it.toUInt().toString().padStart(3, ' ') })
// println(array.joinToString { it.toString().padStart(3, ' ') })
//
//// require(array.size == totalSize) { "${array.size} == $totalSize" }
// return array
// }
fun toByteBuffer2(): ByteBuffer {
return ByteBuffer
.allocate(totalSize)
.order(ByteOrder.LITTLE_ENDIAN)
// size
.putInt(size)
// ID
.putInt(id.toInt())
// type
.putInt(type.value)
// body
.put(body)
// body terminator
.put(bodyTerminator)
// packet terminator
.put(RCON_PACKET_TERMINATOR)
// .asReadOnlyBuffer()
}
// fun toByteBuffer(): ByteBuffer {
//
//
// val bodyBA = buildList {
// add(id.toByte())
// add(type.value.toByte())
// addAll(bodyBytes.asIterable())
// add(bodyTerminator)
// add(RCON_PACKET_TERMINATOR)
// }.toByteArray()
////
////
//// val body = ByteBuffer.allocate(totalSize + 15).apply {
//// order(ByteOrder.LITTLE_ENDIAN)
////// putInt(this@Packet.size)
//// putInt(this@Packet.id.toInt())
//// putInt(this@Packet.type.value)
//// put(this@Packet.bodyBytes)
//// put(this@Packet.terminator)
//// }
//
// val bodySize = bodyBA.size //* Byte.SIZE_BYTES
// println("bodySize: $bodySize")
//
// val bb = ByteBuffer.allocate(bodySize + Int.SIZE_BYTES).apply {
// order(ByteOrder.LITTLE_ENDIAN)
// putInt(bodySize)
//// put(body.array())
// put(bodyBA)
// }
//
// require(
// bb.array().size == totalSize) { "${bb.array().size} == $totalSize ($size + ${Int.SIZE_BYTES})" }
//
// return bb
// }
companion object {
/** [Char.MIN_VALUE] = null char */
const val NULL_CHAR_BYTE = Char.MIN_VALUE.code.toByte()
/**
* The end of a Source RCON Protocol packet is marked by an empty ASCIIZ string -
* essentially just 8 bits of 0. In languages that do not null-terminate strings automatically,
* you can simply write 16 bits of 0 immediately after the packet body (8 to terminate the
* packet body and 8 for the empty string afterwards).
*/
const val RCON_PACKET_TERMINATOR = NULL_CHAR_BYTE
}
}

View file

@ -0,0 +1,31 @@
package dev.adamko.ktrcon
/**
* The packet type field is a 32-bit little endian integer, which indicates the purpose of the
* packet. Its value will always be either 0, 2, or 3, depending on which of the request/response
* types the packet represents.
*/
enum class PacketType(
val value: Int,
val descriptor: String,
) {
/**
* Typically, the first packet sent by the client will be a [AUTH_RESPONSE] packet, which is
* used to authenticate the connection with the server.
*
* The value of the packet's fields are as follows:
*
* * **ID** - any positive integer, chosen by the client
* (will be mirrored back in the server's response)
* * **Type** - 3
* * **Body** - the RCON password of the server (if this matches the server's *rcon_password*
* cvar, the auth will succeed)
*
* If the rcon_password cvar is not set, or if it is set to empty string, all
* [AUTH_RESPONSE] requests will be refused.
*/
AUTH_REQUEST(3, "SERVERDATA_AUTH"),
AUTH_RESPONSE(2, "SERVERDATA_AUTH_RESPONSE"),
EXEC_COMMAND(2, "SERVERDATA_EXECCOMMAND"),
RESPONSE_VALUE(0, "SERVERDATA_RESPONSE_VALUE"),
}

View file

@ -0,0 +1,16 @@
package dev.adamko.ktrcon
class Password constructor(password: String) {
private val password: ByteArray = password.encodeToByteArray()
private val hashcode: Int = password.hashCode()
operator fun invoke() = password.decodeToString()
fun access(accessor: (password: String) -> Unit) {
accessor(password.decodeToString())
}
override fun equals(other: Any?) = (other as? Password)?.hashcode == this.hashcode
override fun hashCode() = hashcode
}

View file

@ -0,0 +1,25 @@
package dev.adamko.ktrcon
import kotlin.time.Duration
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
fun main(): Unit = runBlocking {
KtRconClient3(
"localhost",
27715,
Password("coh9oongoojahX4")
).use { client ->
// client.send(PacketType.AUTH_REQUEST, "passwrd")
client.send(PacketType.AUTH_REQUEST, "coh9oongoojahX4")
delay(Duration.seconds(10))
client.send(PacketType.EXEC_COMMAND, "/help")
delay(Duration.seconds(10))
}
}