commit 3cedeb792e507b57a8a70ca99af110f846a8ee58 Author: Thinkofdeath Date: Mon Sep 7 21:11:00 2015 +0100 Initial commit diff --git a/protocol/src/protocol/mod.rs b/protocol/src/protocol/mod.rs new file mode 100644 index 0000000..8c04af7 --- /dev/null +++ b/protocol/src/protocol/mod.rs @@ -0,0 +1,326 @@ +#![allow(dead_code)] +extern crate byteorder; + +use std::net::TcpStream; +use std::io; +use std::io::{Write, Read}; +use std::convert; +use byteorder::{BigEndian, WriteBytesExt, ReadBytesExt}; + +/// Serializes types into a buffer +macro_rules! serialize_type { + ($dst:expr, $name:expr, u16) => { + $dst.write_u16::($name).unwrap(); + }; + ($dst:expr, $name:expr, i64) => { + $dst.write_i64::($name).unwrap(); + }; + ($dst:expr, $name:expr, VarInt) => { + try!(write_varint($dst, $name)); + }; + ($dst:expr, $name:expr, String) => { + try!(write_varint($dst, $name.len() as i32)); + $dst.extend($name.bytes()); + }; + ($dst:expr, $name:expr, Empty) => { + + }; + ($dst:expr, $name:expr, $ftype:ident) => { + unimplemented!() + }; +} + +/// Deserializes types from a buffer +macro_rules! deserialize_type { + ($src:expr, String) => { + { + let len = read_variant(&mut $src).unwrap(); + let mut ret = String::new(); + (&mut $src).take(len as u64).read_to_string(&mut ret).unwrap(); + ret + } + }; + ($src:expr, i64) => { + $src.read_i64::().unwrap() + }; + ($src:expr, Empty) => { Empty }; + ($src:expr, $ftype:ident) => { + unimplemented!() + }; +} + +/// Helper macro for defining packets +#[macro_export] +macro_rules! state_packets { + ($($state:ident $stateName:ident { + $($dir:ident $dirName:ident { + $($name:ident => $id:expr { + $($field:ident: $field_type:ident),+ + }),* + })+ + })+) => { + use protocol::*; + use std::io; + use std::io::{Read, Write}; + use byteorder::{BigEndian, WriteBytesExt, ReadBytesExt}; + + pub enum Packet { + $( + $( + $( + $name($state::$dir::$name), + )* + )+ + )+ + } + + $( + pub mod $state { + $( + pub mod $dir { + use byteorder::{BigEndian, WriteBytesExt, ReadBytesExt}; + use protocol::*; + use std::io; + use std::io::{Read, Write}; + + $( + pub struct $name { + $(pub $field: $field_type),+, + } + + impl PacketType for $name { + + fn packet_id(&self) -> i32{ $id } + + fn write(self, buf: &mut Vec) -> Result<(), io::Error> { + $( + serialize_type!(buf, self.$field, $field_type); + )+ + + Result::Ok(()) + } + } + )* + } + )+ + } + )+ + + /// Returns the packet for the given state, direction and id after parsing the fields + /// from the buffer. + pub fn packet_by_id(state: State, dir: Direction, id: i32, mut buf: &mut io::Cursor>) -> Option { + match state { + $( + State::$stateName => { + match dir { + $( + Direction::$dirName => { + match id { + $( + $id => Option::Some(Packet::$name($state::$dir::$name { + $($field: deserialize_type!(buf, $field_type)),+, + })), + )* + _ => Option::None + } + } + )+ + } + } + )+ + } + } + } +} + +pub mod packet; + +/// VarInt have a variable size (between 1 and 5 bytes) when encoded based +/// on the size of the number +pub type VarInt = i32; + +/// Encodes a VarInt into the Writer +pub fn write_varint(buf: &mut io::Write, v: VarInt) -> Result<(), io::Error> { + const PART : u32 = 0x7F; + let mut val = v as u32; + loop { + if (val & !PART) == 0 { + try!(buf.write_u8(val as u8)); + return Result::Ok(()); + } + try!(buf.write_u8(((val & PART) | 0x80) as u8)); + val >>= 7; + } +} + +/// Decodes a VarInt from the Reader +pub fn read_variant(buf: &mut io::Read) -> Result { + const PART : u32 = 0x7F; + let mut size = 0; + let mut val = 0u32; + loop { + let b = try!(buf.read_u8()) as u32; + val |= (b & PART) << (size * 7); + size+=1; + if size > 5 { + return Result::Err(io::Error::new(io::ErrorKind::InvalidInput, Error::Err("VarInt too big".to_string()))) + } + if (b & 0x80) == 0 { + break + } + } + + Result::Ok(val as VarInt) +} + +/// Direction is used to define whether packets are going to the +/// server or the client. +pub enum Direction { + Serverbound, + Clientbound +} + +/// The protocol has multiple 'sub-protocols' or states which control which +/// packet an id points to. +#[derive(Clone, Copy)] +pub enum State { + Handshaking, + Play, + Status, + Login +} + +/// Return for any protocol related error. +#[derive(Debug)] +pub enum Error { + Err(String), + IOError(io::Error), +} + +impl convert::From for Error { + fn from(e : io::Error) -> Error { + Error::IOError(e) + } +} + +impl ::std::error::Error for Error { + fn description(&self) -> &str { + match self { + &Error::Err(ref val) => &val[..], + &Error::IOError(ref e) => e.description(), + } + } +} + +impl ::std::fmt::Display for Error { + fn fmt(&self, f : &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + match self { + &Error::Err(ref val) => write!(f, "protocol error: {}", val), + &Error::IOError(ref e) => e.fmt(f) + } + } +} + + +/// Helper for empty structs +pub struct Empty; + +pub struct Conn { + stream: TcpStream, + host: String, + port: u16, + direction: Direction, + state: State, +} + +impl Conn { + pub fn new(target: &str) -> Result{ + // TODO SRV record support + let stream = match TcpStream::connect(target) { + Ok(val) => val, + Err(err) => return Result::Err(Error::IOError(err)) + }; + let parts = target.split(":").collect::>(); + Result::Ok(Conn { + stream: stream, + host: parts[0].to_owned(), + port: parts[1].parse().unwrap(), + direction: Direction::Serverbound, + state: State::Handshaking, + }) + } + + // TODO: compression and encryption + + pub fn write_packet(&mut self, packet: T) -> Result<(), Error> { + let mut buf = Vec::new(); + try!(write_varint(&mut buf, packet.packet_id())); + try!(packet.write(&mut buf)); + try!(write_varint(&mut self.stream, buf.len() as i32)); + try!(self.stream.write_all(&buf.into_boxed_slice())); + + Result::Ok(()) + } + + pub fn read_packet(&mut self) -> Result { + let len = try!(read_variant(&mut self.stream)) as usize; + let mut ibuf = Vec::with_capacity(len); + try!((&mut self.stream).take(len as u64).read_to_end(&mut ibuf)); + + let mut buf = io::Cursor::new(ibuf); + let id = try!(read_variant(&mut buf)); + + let dir = match self.direction { + Direction::Clientbound => Direction::Serverbound, + Direction::Serverbound => Direction::Clientbound, + }; + + let packet = packet::packet_by_id(self.state, dir, id, &mut buf); + + match packet { + Some(val) => { + let pos = buf.position() as usize; + let ibuf = buf.into_inner(); + if ibuf.len() < pos { + return Result::Err(Error::Err(format!("Failed to read all of packet 0x{:X}, had {} bytes left", id, ibuf.len() - pos))) + } + Result::Ok(val) + }, + None => Result::Err(Error::Err("missing packet".to_string())) + } + } +} + +pub trait PacketType { + fn packet_id(&self) -> i32; + + fn write(self, buf: &mut Vec) -> Result<(), io::Error>; +} + +#[test] +fn test() { + let mut c = Conn::new("localhost:25565").unwrap(); + + c.write_packet(packet::handshake::serverbound::Handshake{ + protocol_version: 69, + host: "localhost".to_string(), + port: 25565, + next: 1, + }).unwrap(); + c.state = State::Status; + c.write_packet(packet::status::serverbound::StatusRequest{empty: Empty}).unwrap(); + + match c.read_packet().unwrap() { + packet::Packet::StatusResponse(val) => println!("{}", val.status), + _ => panic!("Wrong packet"), + } + + c.write_packet(packet::status::serverbound::StatusPing{ping: 4433}).unwrap(); + + match c.read_packet().unwrap() { + packet::Packet::StatusPong(val) => println!("{}", val.ping), + _ => panic!("Wrong packet"), + } + + panic!("TODO!"); +} diff --git a/protocol/src/protocol/packet.rs b/protocol/src/protocol/packet.rs new file mode 100644 index 0000000..4819c8a --- /dev/null +++ b/protocol/src/protocol/packet.rs @@ -0,0 +1,96 @@ + +state_packets!( + handshake Handshaking { + serverbound Serverbound { + // Handshake is the first packet sent in the protocol. + // Its used for deciding if the request is a client + // is requesting status information about the server + // (MOTD, players etc) or trying to login to the server. + // + // The host and port fields are not used by the vanilla + // server but are there for virtual server hosting to + // be able to redirect a client to a target server with + // a single address + port. + // + // Some modified servers/proxies use the handshake field + // differently, packing information into the field other + // than the hostname due to the protocol not providing + // any system for custom information to be transfered + // by the client to the server until after login. + Handshake => 0 { + // The protocol version of the connecting client + protocol_version: VarInt, + // The hostname the client connected to + host: String, + // The port the client connected to + port: u16, + // The next protocol state the client wants + next: VarInt + } + } + clientbound Clientbound { + } + } + play Play { + serverbound Serverbound { + } + clientbound Clientbound { + } + } + login Login { + serverbound Serverbound { + } + clientbound Clientbound { + } + } + status Status { + serverbound Serverbound { + // StatusRequest is sent by the client instantly after + // switching to the Status protocol state and is used + // to signal the server to send a StatusResponse to the + // client + StatusRequest => 0 { + empty: Empty + }, + // StatusPing is sent by the client after recieving a + // StatusResponse. The client uses the time from sending + // the ping until the time of recieving a pong to measure + // the latency between the client and the server. + StatusPing => 1 { + ping: i64 + } + } + clientbound Clientbound { + // StatusResponse is sent as a reply to a StatusRequest. + // The Status should contain a json encoded structure with + // version information, a player sample, a description/MOTD + // and optionally a favicon. + // + // The structure is as follows + // { + // "version": { + // "name": "1.8.3", + // "protocol": 47, + // }, + // "players": { + // "max": 20, + // "online": 1, + // "sample": [ + // {"name": "Thinkofdeath", "id": "4566e69f-c907-48ee-8d71-d7ba5aa00d20"} + // ] + // }, + // "description": "Hello world", + // "favicon": "data:image/png;base64," + // } + StatusResponse => 0 { + status: String + }, + // StatusPong is sent as a reply to a StatusPing. + // The Time field should be exactly the same as the + // one sent by the client. + StatusPong => 1 { + ping: i64 + } + } + } +);