From c3eb52e5dd4980c6cba7abf05761c5bcb4d3fcb1 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Tue, 26 Jan 2021 00:57:46 +0000 Subject: [PATCH] Parse commands from over the wire --- Cargo.lock | 19 +++++++++++++++++++ Cargo.toml | 3 ++- titanirc-codec/Cargo.toml | 5 ++++- titanirc-codec/src/lib.rs | 10 +++------- titanirc-codec/src/parser.rs | 1 - titanirc-codec/src/wire.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ titanirc-server/Cargo.toml | 4 ++++ titanirc-server/src/main.rs | 1 + titanirc-server/src/server.rs | 11 ++++++++++- titanirc-server/src/session.rs | 18 ++++++++++++++++++ titanirc-types/Cargo.toml | 11 +++++++++++ titanirc-types/src/lib.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ titanirc-types/src/primitives.rs | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 288 insertions(+), 11 deletions(-) delete mode 100644 titanirc-codec/src/parser.rs create mode 100644 titanirc-codec/src/wire.rs create mode 100644 titanirc-server/src/session.rs create mode 100644 titanirc-types/Cargo.toml create mode 100644 titanirc-types/src/lib.rs create mode 100644 titanirc-types/src/primitives.rs diff --git a/Cargo.lock b/Cargo.lock index 15d540c..f206ece 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,6 +501,12 @@ dependencies = [ ] [[package]] +name = "paste" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1" + +[[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -716,7 +722,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" name = "titanirc-codec" version = "0.1.0" dependencies = [ + "bytes", "nom", + "titanirc-types", "tokio-util", ] @@ -729,7 +737,18 @@ dependencies = [ "async-stream", "displaydoc", "thiserror", + "titanirc-codec", + "titanirc-types", "tokio", + "tokio-util", +] + +[[package]] +name = "titanirc-types" +version = "0.1.0" +dependencies = [ + "nom", + "paste", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e6e7812..2d4e71e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ "titanirc-server", - "titanirc-codec" + "titanirc-codec", + "titanirc-types" ] diff --git a/titanirc-codec/Cargo.toml b/titanirc-codec/Cargo.toml index b3b4dfc..fbeb8a8 100644 --- a/titanirc-codec/Cargo.toml +++ b/titanirc-codec/Cargo.toml @@ -7,5 +7,8 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +titanirc-types = { path = "../titanirc-types" } + nom = "6.1" -tokio-util = { version = "0.6", features = ["codec"] } \ No newline at end of file +tokio-util = { version = "0.6", features = ["codec"] } +bytes = "1.0" \ No newline at end of file diff --git a/titanirc-codec/src/lib.rs b/titanirc-codec/src/lib.rs index 31e1bb2..0ea1a5c 100644 --- a/titanirc-codec/src/lib.rs +++ b/titanirc-codec/src/lib.rs @@ -1,7 +1,3 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} +mod wire; + +pub use crate::wire::Decoder; diff --git a/titanirc-codec/src/parser.rs b/titanirc-codec/src/parser.rs deleted file mode 100644 index 8b13789..0000000 --- a/titanirc-codec/src/parser.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/titanirc-codec/src/wire.rs b/titanirc-codec/src/wire.rs new file mode 100644 index 0000000..ea36387 --- /dev/null +++ b/titanirc-codec/src/wire.rs @@ -0,0 +1,57 @@ +use bytes::{Buf, BytesMut}; +use titanirc_types::Command; +use tokio_util::codec::Decoder as FrameDecoder; + +pub const MAX_LENGTH: usize = 1024; + +pub struct Decoder; + +impl FrameDecoder for Decoder { + type Item = Command; + type Error = std::io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let length = match find_crlf(src) { + Some(len) => len, + None => { + if src.len() > MAX_LENGTH { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Frame of length {} is too large.", src.len()), + )); + } + + // tell Framed we need more bytes + return Ok(None); + } + }; + + let bytes = src.copy_to_bytes(length + 1); + + match Command::parse(&bytes[..bytes.len() - 2]) { + Ok(Some(msg)) => Ok(Some(msg)), + Ok(None) => Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Unknown command", + )), + Err(err) => Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + err.to_string(), + )), + } + } +} + +fn find_crlf(src: &mut BytesMut) -> Option { + let mut iter = src.iter().enumerate(); + + while let Some((_, byte)) = iter.next() { + if byte == &b'\r' { + if let Some((pos, &b'\n')) = iter.next() { + return Some(pos); + } + } + } + + None +} diff --git a/titanirc-server/Cargo.toml b/titanirc-server/Cargo.toml index 1dc83ff..f0ca783 100644 --- a/titanirc-server/Cargo.toml +++ b/titanirc-server/Cargo.toml @@ -7,9 +7,13 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +titanirc-codec = { path = "../titanirc-codec" } +titanirc-types = { path = "../titanirc-types" } + actix = { git = "https://github.com/actix/actix", rev = "fdaa5d50e25ffc892f5c1c6fcc51097796debecf" } # tokio 1.0 support not yet released actix-rt = "2.0.0-beta.1" tokio = { version = "1.1", features = ["net", "signal"] } +tokio-util = "0.6" async-stream = "0.3" thiserror = "1" displaydoc = "0.1" \ No newline at end of file diff --git a/titanirc-server/src/main.rs b/titanirc-server/src/main.rs index eb7e78b..05d9419 100644 --- a/titanirc-server/src/main.rs +++ b/titanirc-server/src/main.rs @@ -1,5 +1,6 @@ mod error; mod server; +mod session; use crate::{ error::Result, diff --git a/titanirc-server/src/server.rs b/titanirc-server/src/server.rs index 56ed689..dda6d22 100644 --- a/titanirc-server/src/server.rs +++ b/titanirc-server/src/server.rs @@ -1,7 +1,10 @@ +use crate::session::Session; + use std::net::SocketAddr; use actix::prelude::*; use tokio::net::TcpStream; +use tokio_util::codec::FramedRead; pub struct Server {} @@ -16,7 +19,13 @@ pub struct Connection(pub TcpStream, pub SocketAddr); impl Handler for Server { type Result = (); - fn handle(&mut self, Connection(_stream, remote): Connection, _: &mut Context) { + fn handle(&mut self, Connection(stream, remote): Connection, _: &mut Self::Context) { println!("Accepted connection from {}", remote); + + Session::create(move |ctx| { + let (read, write) = tokio::io::split(stream); + Session::add_stream(FramedRead::new(read, titanirc_codec::Decoder), ctx); + Session {} + }); } } diff --git a/titanirc-server/src/session.rs b/titanirc-server/src/session.rs new file mode 100644 index 0000000..c74bd3a --- /dev/null +++ b/titanirc-server/src/session.rs @@ -0,0 +1,18 @@ +use actix::prelude::*; +use titanirc_types::Command; + +pub struct Session {} + +impl Actor for Session { + type Context = Context; +} + +impl StreamHandler> for Session { + /// This is main event loop for client requests + fn handle(&mut self, cmd: Result, ctx: &mut Self::Context) { + match cmd { + Ok(cmd) => println!("cmd: {:?}", cmd), + Err(e) => eprintln!("error decoding: {}", e), + } + } +} diff --git a/titanirc-types/Cargo.toml b/titanirc-types/Cargo.toml new file mode 100644 index 0000000..73a11a8 --- /dev/null +++ b/titanirc-types/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "titanirc-types" +version = "0.1.0" +authors = ["Jordan Doyle "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +paste = "1.0" +nom = "6.1" \ No newline at end of file diff --git a/titanirc-types/src/lib.rs b/titanirc-types/src/lib.rs new file mode 100644 index 0000000..e439c45 --- /dev/null +++ b/titanirc-types/src/lib.rs @@ -0,0 +1,74 @@ +mod primitives; + +pub use crate::primitives::*; + +macro_rules! define_commands { + ( + $( + $name:ident$(( + $($param:ty),* + ))? + ),* $(,)? + ) => { + paste::paste! { + #[derive(Debug)] + pub enum Command { + $([<$name:camel>]([<$name:camel Command>])),* + } + + impl Command { + pub fn parse(input: &[u8]) -> Result, nom::Err>> { + let (rest, kind) = nom::bytes::complete::take_till(|c| c == b' ')(input)?; + + $(const [<$name _BYTES>]: &[u8] = stringify!($name).as_bytes();)* + + eprintln!("kind: {:?}", std::str::from_utf8(kind)); + + match kind { + $([<$name _BYTES>] => Ok(Some(Self::[<$name:camel>]([<$name:camel Command>]::parse(rest)?)))),*, + _ => Ok(None) + } + } + } + + $( + #[derive(Debug)] + pub struct [<$name:camel Command>] { + $($([<$param:snake>]: $param),*),* + } + + impl [<$name:camel Command>] { + pub fn parse(rest: &[u8]) -> Result>> { + $( + $( + let (rest, _) = nom::bytes::complete::tag(" ")(rest)?; + let (rest, [<$param:snake>]) = $param::parse(rest)?; + )* + )* + + Ok(Self { + $($([<$param:snake>]),*),* + }) + } + } + )* + } + }; +} + +define_commands! { + USER(Username, HostName, ServerName, RealName), + NICK(Nick), + + VERSION, + HELP, + USERS, + TIME, + LIST, + WHOIS(Nick), + USERHOST(Nick), + USERIP(Nick), + JOIN(Channel), + + PRIVMSG(Receiver, Message), +} diff --git a/titanirc-types/src/primitives.rs b/titanirc-types/src/primitives.rs new file mode 100644 index 0000000..3ba32d5 --- /dev/null +++ b/titanirc-types/src/primitives.rs @@ -0,0 +1,85 @@ +use nom::IResult; + +pub trait PrimitiveParser { + fn parse(bytes: &[u8]) -> IResult<&[u8], Self> + where + Self: Sized; +} + +macro_rules! standard_string_parser { + ($name:ty) => { + impl PrimitiveParser for $name { + fn parse(bytes: &[u8]) -> IResult<&[u8], Self> { + let (rest, val) = nom::combinator::map_res( + nom::bytes::complete::take_till(|c| c == b' '), + std::str::from_utf8, + )(bytes)?; + + // TODO: don't clone + Ok((rest, Self(val.to_string()))) + } + } + }; +} + +#[derive(Debug)] +pub struct Username(String); + +standard_string_parser!(Username); + +#[derive(Debug)] +pub struct HostName(String); + +standard_string_parser!(HostName); + +#[derive(Debug)] +pub struct ServerName(String); + +standard_string_parser!(ServerName); + +#[derive(Debug)] +pub struct RealName(String); + +standard_string_parser!(RealName); + +#[derive(Debug)] +pub struct Nick(String); + +standard_string_parser!(Nick); + +#[derive(Debug)] +pub struct Channel(String); + +standard_string_parser!(Channel); + +#[derive(Debug)] +pub enum Receiver { + User(Nick), + Channel(Channel), +} + +impl PrimitiveParser for Receiver { + fn parse(bytes: &[u8]) -> IResult<&[u8], Self> { + if let Ok((rest, _)) = + nom::bytes::complete::tag::<_, _, nom::error::Error<&[u8]>>("#")(bytes) + { + let (rest, channel) = Channel::parse(rest)?; + Ok((rest, Self::Channel(channel))) + } else { + let (rest, nick) = Nick::parse(bytes)?; + Ok((rest, Self::User(nick))) + } + } +} + +#[derive(Debug)] +pub struct Message(String); + +impl PrimitiveParser for Message { + fn parse(bytes: &[u8]) -> IResult<&[u8], Self> { + // TODO: don't clone, don't panic + let val = std::str::from_utf8(bytes).expect("utf-8").to_string(); + + Ok((b"", Self(val))) + } +} -- libgit2 1.7.2