Packages

A Source RCON Client implementation

Current section

Files

Jump to
glame src rcon.gleam
Raw

src/rcon.gleam

import gleam/string
import gleam/bit_array
import mug
import packet
import errors
import gleam/option
/// The default timeout used for connecting, reading, and writing to the socket
const default_timeout_ms = 5000
/// Thin wrapper over a TCP socket
pub type Connection {
Connection(sock: mug.Socket, timeout_ms: Int)
}
/// Connects to the TCP socket and auths with the RCON server
pub fn dial(
host: String,
port: Int,
password: String,
connect_timeout_ms: option.Option(Int),
rw_timeout_ms: option.Option(Int),
) -> Result(Connection, errors.Error) {
let sock_result =
mug.new(host, port)
|> mug.timeout(option.unwrap(connect_timeout_ms, default_timeout_ms))
|> mug.connect()
case sock_result {
Ok(sock) -> {
let conn =
Connection(sock, option.unwrap(rw_timeout_ms, default_timeout_ms))
case auth(conn, password) {
Ok(_) -> Ok(conn)
Error(e) -> Error(e)
}
}
Error(e) -> Error(errors.SocketError(e))
}
}
/// Executes a command on the RCON server
pub fn execute(conn: Connection, cmd: String) -> Result(String, errors.Error) {
let cmd_len = string.length(cmd)
case cmd_len {
0 -> Error(errors.BodyEmpty)
_ if cmd_len > 4096 -> Error(errors.BodyTooLarge(cmd_len))
_ -> {
case write(conn, packet.ServerDataExecCommand, 3, cmd) {
Ok(_) -> {
case read(conn) {
Ok(pkt) -> {
case bit_array.to_string(pkt.body) {
Ok(body) -> Ok(body)
Error(_) -> Error(errors.BodyNotUTF8(pkt.body))
}
}
Error(e) -> Error(e)
}
}
Error(e) -> Error(e)
}
}
}
}
/// Authorizes the connection with the RCON server
fn auth(conn: Connection, password: String) -> Result(Nil, errors.Error) {
case write(conn, packet.ServerDataAuth, 2, password) {
Ok(_) -> {
case read(conn) {
Ok(pkt) -> {
case pkt.typ {
0 -> {
// Some servers will send an empty SERVERDATA_RESPONSE_VALUE packet
// immediately followed by an auth response, so we need to discard the first
// packet if necessary
case read(conn) {
Ok(p) -> {
validate_auth_response(p, 2)
}
Error(e) -> Error(e)
}
}
_ -> {
validate_auth_response(pkt, 2)
}
}
}
Error(e) -> Error(e)
}
}
Error(e) -> Error(e)
}
}
/// Makes sure that authentication was successful
fn validate_auth_response(
pkt: packet.Packet,
auth_id: Int,
) -> Result(Nil, errors.Error) {
case pkt.typ {
2 -> {
case pkt.id {
-1 -> Error(errors.AuthFailed)
_ if pkt.id == auth_id -> Ok(Nil)
got -> Error(errors.WrongResponseId(auth_id, got))
}
}
got -> Error(errors.WrongResponseType(2, got))
}
}
/// Sends a packet to the server
fn write(
conn: Connection,
packet_type: packet.PacketType,
packet_id: Int,
cmd: String,
) -> Result(Nil, errors.Error) {
case packet.new(packet_type, packet_id, cmd) {
Ok(pkt) -> {
let bytes = packet.to_bytes(pkt)
case mug.send(conn.sock, bytes) {
Ok(_) -> Ok(Nil)
Error(e) -> Error(errors.SocketError(e))
}
}
Error(e) -> Error(e)
}
}
/// Waits to receive a packet from the server
fn read(conn: Connection) -> Result(packet.Packet, errors.Error) {
case mug.receive(conn.sock, conn.timeout_ms) {
Ok(bytes) -> {
packet.from_bytes(bytes)
}
Error(e) -> Error(errors.SocketError(e))
}
}