separate client and wrap as lib

This commit is contained in:
Raptorox 2026-07-27 22:26:20 +02:00
parent d3a7cebc13
commit b7669181a1
No known key found for this signature in database
GPG key ID: 8B3556FC3ED1F6D8
3 changed files with 114 additions and 110 deletions

110
src/client.rs Normal file
View file

@ -0,0 +1,110 @@
use std::{io::{Read, Write}, net::{TcpStream, ToSocketAddrs}};
#[derive(Clone, Copy, PartialEq)]
enum PacketType {
Login = 3,
Command = 2,
Response = 0
}
impl PacketType {
fn to_le_bytes(&self) -> [u8; 4] {
(*self as i32).to_le_bytes()
}
}
impl TryFrom<i32> for PacketType {
type Error = ();
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
x if x == Self::Login as i32 => Ok(Self::Login),
x if x == Self::Command as i32 => Ok(Self::Command),
x if x == Self::Response as i32 => Ok(Self::Response),
_ => Err(())
}
}
}
pub struct RconClient {
stream: TcpStream,
next_id: i32
}
impl RconClient {
fn read_i32_le(&mut self) -> i32 {
let mut buf = [0u8; 4];
self.stream.read_exact(&mut buf).unwrap();
i32::from_le_bytes(buf)
}
fn recv_packet(&mut self) -> (i32, PacketType, String) {
let length = self.read_i32_le();
if length < 10 {
panic!("response length < 10");
}
let id = self.read_i32_le();
let ptype = PacketType::try_from(self.read_i32_le()).unwrap();
let payload_len = (length - 10) as usize;
let mut payload_bytes = vec![0u8; payload_len];
self.stream.read_exact(&mut payload_bytes).unwrap();
let mut nulls = [0u8; 2];
self.stream.read_exact(&mut nulls).unwrap();
let payload = String::from_utf8_lossy(&payload_bytes).into_owned();
(id, ptype, payload)
}
fn send_packet(&mut self, ptype: PacketType, payload: &str) -> i32 {
let id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);
let payload_bytes = payload.as_bytes();
let length = (payload_bytes.len() + 10) as i32;
let mut packet = Vec::with_capacity(4 + length as usize);
packet.extend_from_slice(&length.to_le_bytes());
packet.extend_from_slice(&id.to_le_bytes());
packet.extend_from_slice(&ptype.to_le_bytes());
packet.extend_from_slice(&payload_bytes);
packet.push(0); // terminate payload
packet.push(0); // padding
self.stream.write_all(&packet).unwrap();
id
}
fn authenticate(&mut self, password: &str) {
self.send_packet(PacketType::Login, password);
let (auth_id, _, _) = self.recv_packet();
if auth_id == -1 {
panic!("auth failed");
}
}
pub fn connect(addr: impl ToSocketAddrs, password: &str) -> Self {
let stream = TcpStream::connect(addr).unwrap();
let mut client = RconClient { stream, next_id: 1 };
client.authenticate(password);
client
}
pub fn command(&mut self, cmd: &str) -> String {
let id = self.send_packet(PacketType::Command, cmd);
let (resp_id, resp_type, payload) = self.recv_packet();
if resp_type != PacketType::Response {
panic!("invalid response")
}
if resp_id != id {
panic!("invalid response")
}
payload
}
}

2
src/lib.rs Normal file
View file

@ -0,0 +1,2 @@
mod client;
pub use client::RconClient;

View file

@ -1,6 +1,7 @@
use std::{io::{Read, Write}, net::{TcpStream, ToSocketAddrs}};
use clap::Parser;
use mc_rcon::RconClient;
#[derive(Parser, Debug)]
struct Args {
#[arg(short, long)]
@ -13,115 +14,6 @@ struct Args {
cmd: String
}
#[derive(Clone, Copy, PartialEq)]
enum PacketType {
Login = 3,
Command = 2,
Response = 0
}
impl PacketType {
fn to_le_bytes(&self) -> [u8; 4] {
(*self as i32).to_le_bytes()
}
}
impl TryFrom<i32> for PacketType {
type Error = ();
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
x if x == Self::Login as i32 => Ok(Self::Login),
x if x == Self::Command as i32 => Ok(Self::Command),
x if x == Self::Response as i32 => Ok(Self::Response),
_ => Err(())
}
}
}
struct RconClient {
stream: TcpStream,
next_id: i32
}
impl RconClient {
fn read_i32_le(&mut self) -> i32 {
let mut buf = [0u8; 4];
self.stream.read_exact(&mut buf).unwrap();
i32::from_le_bytes(buf)
}
fn recv_packet(&mut self) -> (i32, PacketType, String) {
let length = self.read_i32_le();
if length < 10 {
panic!("response length < 10");
}
let id = self.read_i32_le();
let ptype = PacketType::try_from(self.read_i32_le()).unwrap();
let payload_len = (length - 10) as usize;
let mut payload_bytes = vec![0u8; payload_len];
self.stream.read_exact(&mut payload_bytes).unwrap();
let mut nulls = [0u8; 2];
self.stream.read_exact(&mut nulls).unwrap();
let payload = String::from_utf8_lossy(&payload_bytes).into_owned();
(id, ptype, payload)
}
fn send_packet(&mut self, ptype: PacketType, payload: &str) -> i32 {
let id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);
let payload_bytes = payload.as_bytes();
let length = (payload_bytes.len() + 10) as i32;
let mut packet = Vec::with_capacity(4 + length as usize);
packet.extend_from_slice(&length.to_le_bytes());
packet.extend_from_slice(&id.to_le_bytes());
packet.extend_from_slice(&ptype.to_le_bytes());
packet.extend_from_slice(&payload_bytes);
packet.push(0); // terminate payload
packet.push(0); // padding
self.stream.write_all(&packet).unwrap();
id
}
fn authenticate(&mut self, password: &str) {
self.send_packet(PacketType::Login, password);
let (auth_id, _, _) = self.recv_packet();
if auth_id == -1 {
panic!("auth failed");
}
}
fn connect(addr: impl ToSocketAddrs, password: &str) -> Self {
let stream = TcpStream::connect(addr).unwrap();
let mut client = RconClient { stream, next_id: 1 };
client.authenticate(password);
client
}
fn command(&mut self, cmd: &str) -> String {
let id = self.send_packet(PacketType::Command, cmd);
let (resp_id, resp_type, payload) = self.recv_packet();
if resp_type != PacketType::Response {
panic!("invalid response")
}
if resp_id != id {
panic!("invalid response")
}
payload
}
}
fn main() {
let args = Args::parse();