From 5ca8ef9e99ff57e347f54c53b495c86b591b3694 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Wed, 27 Jan 2021 03:05:28 +0000 Subject: [PATCH] Implement server -> client responses & initial connection song-and-dance --- Cargo.lock | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ titanirc-codec/src/lib.rs | 2 +- titanirc-codec/src/wire.rs | 18 ++++++++++++++++++ titanirc-server/Cargo.toml | 3 ++- titanirc-server/src/server.rs | 11 +++++++---- titanirc-server/src/session.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- titanirc-types/Cargo.toml | 4 +++- titanirc-types/src/lib.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- titanirc-types/src/primitives.rs | 264 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------- titanirc-types/src/replies.rs | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 743 insertions(+), 54 deletions(-) create mode 100644 titanirc-types/src/replies.rs diff --git a/Cargo.lock b/Cargo.lock index f206ece..5ea1955 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,17 @@ dependencies = [ ] [[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] name = "autocfg" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -147,6 +158,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "clap" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "crossbeam-channel" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -174,6 +217,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993a608597367c6377b258c25d7120740f00ed23a2252b729b1932dd7866f908" [[package]] +name = "derive_more" +version = "0.99.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "displaydoc" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -273,6 +327,12 @@ dependencies = [ ] [[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] name = "heck" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -313,6 +373,16 @@ dependencies = [ ] [[package]] +name = "indexmap" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] name = "instant" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -476,6 +546,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" [[package]] +name = "os_str_bytes" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" + +[[package]] name = "parking_lot" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -531,6 +607,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] name = "proc-macro2" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -667,6 +767,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] name = "syn" version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -684,6 +790,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e" [[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" +dependencies = [ + "unicode-width", +] + +[[package]] name = "thiserror" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -735,6 +859,7 @@ dependencies = [ "actix", "actix-rt", "async-stream", + "clap", "displaydoc", "thiserror", "titanirc-codec", @@ -747,6 +872,8 @@ dependencies = [ name = "titanirc-types" version = "0.1.0" dependencies = [ + "bytes", + "derive_more", "nom", "paste", ] @@ -866,6 +993,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" [[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -884,6 +1017,12 @@ dependencies = [ ] [[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] name = "version_check" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -918,6 +1057,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/titanirc-codec/src/lib.rs b/titanirc-codec/src/lib.rs index bb3d677..206da83 100644 --- a/titanirc-codec/src/lib.rs +++ b/titanirc-codec/src/lib.rs @@ -3,4 +3,4 @@ mod wire; -pub use crate::wire::Decoder; +pub use crate::wire::{Decoder, Encoder}; diff --git a/titanirc-codec/src/wire.rs b/titanirc-codec/src/wire.rs index eb9548e..9b9cee5 100644 --- a/titanirc-codec/src/wire.rs +++ b/titanirc-codec/src/wire.rs @@ -27,6 +27,8 @@ impl FrameDecoder for Decoder { let bytes = src.copy_to_bytes(length + 1); + eprintln!("{:?}", std::str::from_utf8(&bytes[..bytes.len() - 2])); + match Command::parse(&bytes[..bytes.len() - 2]) { Ok(Some(msg)) => Ok(Some(msg)), Ok(None) => Err(std::io::Error::new( @@ -41,6 +43,22 @@ impl FrameDecoder for Decoder { } } +pub struct Encoder; + +impl tokio_util::codec::Encoder for Encoder { + type Error = std::io::Error; + + fn encode( + &mut self, + item: titanirc_types::ServerMessage, + dst: &mut BytesMut, + ) -> Result<(), Self::Error> { + item.write("my.cool.server", "jordan", dst); + dst.extend_from_slice(b"\r\n"); + Ok(()) + } +} + fn find_crlf(src: &mut BytesMut) -> Option { let mut iter = src.iter().enumerate(); diff --git a/titanirc-server/Cargo.toml b/titanirc-server/Cargo.toml index f0ca783..8cb9be8 100644 --- a/titanirc-server/Cargo.toml +++ b/titanirc-server/Cargo.toml @@ -16,4 +16,5 @@ 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 +displaydoc = "0.1" +clap = "3.0.0-beta.2" \ No newline at end of file diff --git a/titanirc-server/src/server.rs b/titanirc-server/src/server.rs index 9740800..e0af39d 100644 --- a/titanirc-server/src/server.rs +++ b/titanirc-server/src/server.rs @@ -2,7 +2,7 @@ use crate::session::Session; use std::net::SocketAddr; -use actix::prelude::*; +use actix::{io::FramedWrite, prelude::*}; use tokio::net::TcpStream; use tokio_util::codec::FramedRead; @@ -19,13 +19,16 @@ pub struct Connection(pub TcpStream, pub SocketAddr); impl Handler for Server { type Result = (); - fn handle(&mut self, Connection(stream, remote): Connection, _: &mut Self::Context) { + fn handle(&mut self, Connection(stream, remote): Connection, _ctx: &mut Self::Context) { println!("Accepted connection from {}", remote); Session::create(move |ctx| { - let (read, _write) = tokio::io::split(stream); + let (read, write) = tokio::io::split(stream); Session::add_stream(FramedRead::new(read, titanirc_codec::Decoder), ctx); - Session {} + Session { + writer: FramedWrite::new(write, titanirc_codec::Encoder, ctx), + last_active: std::time::Instant::now(), + } }); } } diff --git a/titanirc-server/src/session.rs b/titanirc-server/src/session.rs index 8482c46..f87cd9f 100644 --- a/titanirc-server/src/session.rs +++ b/titanirc-server/src/session.rs @@ -1,16 +1,88 @@ -use actix::prelude::*; +use actix::{ + io::{FramedWrite, WriteHandler}, + prelude::*, +}; +use std::time::{Duration, Instant}; use titanirc_types::Command; +use tokio::{io::WriteHalf, net::TcpStream}; -pub struct Session {} +pub struct Session { + pub writer: + FramedWrite, titanirc_codec::Encoder>, + pub last_active: Instant, +} + +fn schedule_ping(ctx: &mut ::Context) { + ctx.run_later(Duration::from_secs(30), |act, ctx| { + if Instant::now().duration_since(act.last_active) > Duration::from_secs(240) { + // send `QUIT :Ping timeout: 120 seconds` & `ERROR :Closing Link: {ip} (Ping timeout: 120 seconds)` + eprintln!("ping timeout"); + ctx.stop(); + } + + act.writer.write(titanirc_types::ServerMessage::Ping); + schedule_ping(ctx); + }); +} impl Actor for Session { type Context = Context; + + fn started(&mut self, ctx: &mut Self::Context) { + schedule_ping(ctx); + } } +impl WriteHandler for Session {} + impl StreamHandler> for Session { /// This is main event loop for client requests fn handle(&mut self, cmd: Result, _ctx: &mut Self::Context) { + self.last_active = Instant::now(); + match cmd { + Ok(Command::Nick(_v)) => { + self.writer.write(titanirc_types::Reply::RplWelcome.into()); + self.writer.write(titanirc_types::Reply::RplYourHost.into()); + self.writer.write(titanirc_types::Reply::RplCreated.into()); + self.writer.write(titanirc_types::Reply::RplMyInfo.into()); + self.writer.write(titanirc_types::Reply::RplISupport.into()); + } + Ok(Command::Mode(titanirc_types::ModeCommand { mode, .. })) => self + .writer + .write(titanirc_types::Reply::RplUmodeIs(mode).into()), + Ok(Command::Motd(_)) => { + self.writer.write( + titanirc_types::Reply::RplMotdStart(titanirc_types::ServerName( + "my.test.server".to_string(), + )) + .into(), + ); + self.writer.write( + titanirc_types::Reply::RplMotd(titanirc_types::FreeText( + "Hello, welcome to this server!".to_string(), + )) + .into(), + ); + self.writer.write( + titanirc_types::Reply::RplMotd(titanirc_types::FreeText( + "it's very cool!".to_string(), + )) + .into(), + ); + self.writer + .write(titanirc_types::Reply::RplEndOfMotd.into()); + } + Ok(Command::Version(_)) => self.writer.write( + titanirc_types::Reply::RplVersion( + clap::crate_version!().to_string(), + "release".to_string(), + titanirc_types::ServerName("my.test.server".to_string()), + titanirc_types::FreeText("https://github.com/MITBorg/titanirc".to_string()), + ) + .into(), + ), + Ok(Command::Pong(_)) => {} Ok(cmd) => println!("cmd: {:?}", cmd), Err(e) => eprintln!("error decoding: {}", e), } diff --git a/titanirc-types/Cargo.toml b/titanirc-types/Cargo.toml index 73a11a8..562d290 100644 --- a/titanirc-types/Cargo.toml +++ b/titanirc-types/Cargo.toml @@ -8,4 +8,6 @@ edition = "2018" [dependencies] paste = "1.0" -nom = "6.1" \ No newline at end of file +nom = "6.1" +derive_more = "0.99" +bytes = "1.0" \ No newline at end of file diff --git a/titanirc-types/src/lib.rs b/titanirc-types/src/lib.rs index ec6c621..81e6b3c 100644 --- a/titanirc-types/src/lib.rs +++ b/titanirc-types/src/lib.rs @@ -2,8 +2,21 @@ #![allow(clippy::missing_errors_doc)] mod primitives; +mod replies; pub use crate::primitives::*; +pub use crate::replies::{Reply, ServerMessage}; + +use nom::{ + bytes::complete::{tag, take_till}, + error::Error as NomError, +}; + +fn parse_optional_source(input: &[u8]) -> nom::IResult<&[u8], &[u8]> { + let (rest, _) = tag(":")(input)?; + let (rest, _) = take_till(|c| c == b' ')(rest)?; + tag(" ")(rest) +} macro_rules! define_commands { ( @@ -22,20 +35,36 @@ macro_rules! define_commands { $(const [<$name _BYTES>]: &[u8] = stringify!($name).as_bytes();)* impl Command { - pub fn parse(input: &[u8]) -> Result, nom::Err>> { - let (rest, kind) = nom::bytes::complete::take_till(|c| c == b' ')(input)?; + pub fn parse(input: &[u8]) -> Result, nom::Err>> { + // skip the optional source at the start of the message + let rest = if let Ok((rest, _)) = parse_optional_source(input) { + rest + } else { + input + }; + + let (rest, kind) = take_till(|c| c == b' ')(rest)?; - match kind { + // fix this shit + match std::str::from_utf8(kind).unwrap().to_uppercase().as_bytes() { $([<$name _BYTES>] => Ok(Some(Self::[<$name:camel>]([<$name:camel Command>]::parse(rest)?)))),*, _ => Ok(None) } } } + impl std::fmt::Display for Command { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + $(Self::[<$name:camel>](cmd) => cmd.fmt(fmt)),* + } + } + } + $( #[derive(Debug)] pub struct [<$name:camel Command>] { - $($([<$param:snake>]: $param),*),* + $($(pub [<$param:snake>]: $param),*),* } impl [<$name:camel Command>] { @@ -43,7 +72,7 @@ macro_rules! define_commands { pub fn parse(rest: &[u8]) -> Result>> { $( $( - let (rest, _) = nom::bytes::complete::tag(" ")(rest)?; + let (rest, _) = tag(" ")(rest)?; let (rest, [<$param:snake>]) = $param::parse(rest)?; )* )* @@ -53,6 +82,21 @@ macro_rules! define_commands { }) } } + + impl std::fmt::Display for [<$name:camel Command>] { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt.write_str(stringify!($name))?; + + $( + $( + fmt.write_str(" ")?; + self.[<$param:snake>].fmt(fmt)?; + )* + )* + + Ok(()) + } + } )* } }; @@ -62,17 +106,21 @@ define_commands! { USER(Username, HostName, ServerName, RealName), NICK(Nick), + MOTD, VERSION, HELP, USERS, TIME, + PONG(ServerName), + PING(ServerName), LIST, + MODE(Nick, Mode), WHOIS(Nick), USERHOST(Nick), USERIP(Nick), JOIN(Channel), - PRIVMSG(Receiver, Message), + PRIVMSG(Receiver, FreeText), } #[cfg(test)] @@ -90,8 +138,24 @@ mod tests { Command::parse(b"PRIVMSG foo :baz"), Ok(Some(Command::Privmsg(super::PrivmsgCommand { receiver: super::Receiver::User(super::Nick(nick)), - message: super::Message(msg), - }))) if nick == "foo" && msg == ":baz" + free_text: super::primitives::FreeText(msg), + }))) if nick == "foo" && msg == "baz" + )) + } + + #[test] + fn parse_privmsg_opt_source() { + eprintln!( + "{:?}", + Command::parse(b":some-fake-source!dude@nice PRIVMSG foo :baz") + ); + + assert!(matches!( + Command::parse(b":some-fake-source!dude@nice PRIVMSG foo :baz"), + Ok(Some(Command::Privmsg(super::PrivmsgCommand { + receiver: super::Receiver::User(super::Nick(nick)), + free_text: super::primitives::FreeText(msg), + }))) if nick == "foo" && msg == "baz" )) } } diff --git a/titanirc-types/src/primitives.rs b/titanirc-types/src/primitives.rs index ed3dcb9..7803339 100644 --- a/titanirc-types/src/primitives.rs +++ b/titanirc-types/src/primitives.rs @@ -1,4 +1,14 @@ -use nom::IResult; +use derive_more::{Deref, From}; +use nom::{ + bytes::complete::{tag, take_till}, + combinator::{iterator, map_res}, + sequence::terminated, + IResult, +}; + +pub trait ValidatingParser { + fn validate(bytes: &[u8]) -> bool; +} pub trait PrimitiveParser { fn parse(bytes: &[u8]) -> IResult<&[u8], Self> @@ -6,58 +16,250 @@ pub trait PrimitiveParser { Self: Sized; } -macro_rules! standard_string_parser { +macro_rules! noop_validator { + ($name:ty) => { + impl ValidatingParser for $name { + fn validate(_: &[u8]) -> bool { + true + } + } + }; +} + +macro_rules! free_text_primitive { ($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)?; + let (rest, _) = tag(b":")(bytes)?; + Ok((&[], Self(std::str::from_utf8(rest).unwrap().to_string()))) + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } + } + }; +} + +macro_rules! space_terminated_primitive { + ($name:ty) => { + impl PrimitiveParser for $name { + fn parse(bytes: &[u8]) -> IResult<&[u8], Self> { + let (rest, val) = map_res(take_till(|c| c == b' '), std::str::from_utf8)(bytes)?; + + if !::validate(val.as_bytes()) { + return Err(nom::Err::Failure(nom::error::Error::new( + bytes, + nom::error::ErrorKind::Verify, + ))); + } // TODO: don't clone Ok((rest, Self(val.to_string()))) } } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } + } }; } -#[derive(Debug)] -pub struct Username(pub(crate) String); -standard_string_parser!(Username); +macro_rules! space_delimited_display { + ($name:ty) => { + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // once the first iteration is complete, we'll start adding spaces before + // each nickname. + let mut space = false; -#[derive(Debug)] -pub struct HostName(pub(crate) String); -standard_string_parser!(HostName); + for value in &self.0 { + if space { + f.write_str(" ")?; + } else { + space = true; + } -#[derive(Debug)] -pub struct ServerName(pub(crate) String); -standard_string_parser!(ServerName); + value.fmt(f)?; + } -#[derive(Debug)] -pub struct RealName(pub(crate) String); -standard_string_parser!(RealName); + Ok(()) + } + } + }; +} + +pub struct Letter; + +impl ValidatingParser for Letter { + fn validate(bytes: &[u8]) -> bool { + bytes + .iter() + .all(|c| (b'a'..=b'z').contains(c) || (b'A'..=b'Z').contains(c)) + } +} + +pub struct Number; + +impl ValidatingParser for Number { + fn validate(bytes: &[u8]) -> bool { + bytes.iter().all(|c| (b'0'..=b'9').contains(c)) + } +} + +pub struct Special; + +impl ValidatingParser for Special { + fn validate(bytes: &[u8]) -> bool { + const ALLOWED: &[u8] = &[b'-', b'[', b']', b'\\', b'`', b'^', b'{', b'}']; + + bytes.iter().all(|c| ALLOWED.contains(c)) + } +} + +#[derive(Debug, Deref, From)] +pub struct Username(pub String); +space_terminated_primitive!(Username); +noop_validator!(Username); + +#[derive(Debug, Deref, From)] +pub struct Mode(pub String); +space_terminated_primitive!(Mode); +noop_validator!(Mode); + +#[derive(Debug, Deref, From)] +pub struct HostName(pub String); +space_terminated_primitive!(HostName); +noop_validator!(HostName); + +#[derive(Debug, Deref, From)] +pub struct ServerName(pub String); +space_terminated_primitive!(ServerName); +noop_validator!(ServerName); + +#[derive(Debug, Deref, From)] +pub struct RealName(pub String); +space_terminated_primitive!(RealName); +noop_validator!(RealName); + +#[derive(Debug, Deref, From)] +pub struct Nick(pub String); +space_terminated_primitive!(Nick); + +// TODO: i feel like this would be better suited as a nom chomper to stop +// iterating over the string twice unnecessarily +impl ValidatingParser for Nick { + fn validate(bytes: &[u8]) -> bool { + if bytes.is_empty() { + return false; + } + + if !Letter::validate(&[bytes[0]]) { + return false; + } + + bytes[1..] + .iter() + .all(|c| Letter::validate(&[*c]) || Number::validate(&[*c]) || Special::validate(&[*c])) + } +} + +#[derive(Debug, Deref, From)] +pub struct Channel(pub String); +space_terminated_primitive!(Channel); +noop_validator!(Channel); + +#[derive(Debug, Deref, From)] +pub struct FreeText(pub String); +free_text_primitive!(FreeText); +noop_validator!(FreeText); + +#[derive(Debug, Deref, From)] +pub struct Nicks(pub Vec); +space_delimited_display!(Nicks); + +impl PrimitiveParser for Nicks { + fn parse(bytes: &[u8]) -> IResult<&[u8], Self> { + let mut it = iterator(bytes, terminated(take_till(|c| c == b' '), tag(b" "))); + + let parsed = it + .map(|v| Nick(std::str::from_utf8(v).unwrap().to_string())) + .collect(); + + it.finish() + .map(move |(remaining, _)| (remaining, Self(parsed))) + } +} #[derive(Debug)] -pub struct Nick(pub(crate) String); -standard_string_parser!(Nick); +pub struct RightsPrefixedNick(pub Rights, pub Nick); + +impl std::fmt::Display for RightsPrefixedNick { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f)?; + self.1.fmt(f) + } +} + +#[derive(Debug, Deref, From)] +pub struct RightsPrefixedNicks(pub Vec); +space_delimited_display!(RightsPrefixedNicks); #[derive(Debug)] -pub struct Channel(pub(crate) String); -standard_string_parser!(Channel); +pub struct RightsPrefixedChannel(pub Rights, pub Nick); + +impl std::fmt::Display for RightsPrefixedChannel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f)?; + self.1.fmt(f) + } +} + +#[derive(Debug, Deref, From)] +pub struct RightsPrefixedChannels(pub Vec); +space_delimited_display!(RightsPrefixedChannels); #[derive(Debug)] +pub enum Rights { + Op, + Voice, +} + +impl std::fmt::Display for Rights { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Op => "@", + Self::Voice => "+", + }) + } +} + +#[derive(Debug, From)] pub enum Receiver { User(Nick), Channel(Channel), } +impl std::ops::Deref for Receiver { + type Target = String; + + fn deref(&self) -> &Self::Target { + match self { + Self::User(nick) => &*nick, + Self::Channel(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) + if let Ok((_, _)) = nom::bytes::complete::tag::<_, _, nom::error::Error<&[u8]>>("#")(bytes) { - let (rest, channel) = Channel::parse(rest)?; + let (rest, channel) = Channel::parse(bytes)?; Ok((rest, Self::Channel(channel))) } else { let (rest, nick) = Nick::parse(bytes)?; @@ -65,15 +267,3 @@ impl PrimitiveParser for Receiver { } } } - -#[derive(Debug)] -pub struct Message(pub(crate) 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))) - } -} diff --git a/titanirc-types/src/replies.rs b/titanirc-types/src/replies.rs new file mode 100644 index 0000000..e89296f --- /dev/null +++ b/titanirc-types/src/replies.rs @@ -0,0 +1,191 @@ +use crate::{primitives::*, Command}; +use std::fmt::Write; + +#[derive(Debug)] +pub enum Source { + User(Nick), // change Nick to whatever type nick!user@netmask is.. + Server, +} + +#[derive(Debug, derive_more::From)] +pub enum ServerMessage { + Reply(Reply), + Command(Source, Command), // change Nick to whatever type nick!user@netmask is.. + Ping, + Pong, +} + +impl ServerMessage { + pub fn write(self, server_name: &str, client_username: &str, dst: &mut bytes::BytesMut) { + match self { + Self::Reply(reply) => write!( + dst, + ":{} {} {} {}", + server_name, + reply.code(), + client_username, + reply, + ), + Self::Ping => write!(dst, "PING :{}", server_name), + Self::Pong => write!(dst, "PONG :{}", server_name), + Self::Command(source, command) => { + let source = match &source { + Source::User(nick) => nick.as_str(), + Source::Server => server_name, + }; + write!(dst, ":{} {}", source, command) + } + } + .unwrap() + } +} + +macro_rules! define_replies { + ( + $( + $name:ident$(($($arg:ty),*))? = $num:expr $(=> $msg:expr)? + ),* $(,)? + ) => { + #[derive(Debug)] + #[allow(clippy::pub_enum_variant_names)] + pub enum Reply { + $( + $name$(($($arg),*))*, + )* + } + + impl std::fmt::Display for Reply { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + paste::paste! { + match self { + $(Self::$name$(($([<$arg:snake>]),*))* => write!(fmt, concat!("", $($msg),*) $(, $([<$arg:snake>]),*)*)),* + } + } + } + } + + impl Reply { + #[must_use] + pub fn code(&self) -> &'static str { + paste::paste! { + match self { + $(Self::$name$(($([<_ $arg:snake>]),*))* => stringify!($num)),* + } + } + } + } + }; +} + +type Target = String; +type CommandName = String; +type Mask = String; +type Banid = String; +type ConfigFile = String; +type Hopcount = String; +type ServerInfo = String; +type HG = String; +type Debuglevel = String; +type ModeParams = String; +type Version = String; +type AmtVisible = String; +type Integer = String; +type File = String; +type FileOp = String; +type Char = String; + +// TODO: fix these +type UserHost = String; + +define_replies! { + RplWelcome = 001 => ":Welcome to the network jordan!jordan@proper.sick.kid", + RplYourHost = 002 => ":Your host is a sick kid", + RplCreated = 003 => ":This server was created at some point", + RplMyInfo = 004 => ":my.test.server 0.0.1 DOQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI", + RplISupport = 005 => "D :are supported by this server", + + RplUmodeIs(Mode) = 221 => "{}", + + ErrNoSuchNick(Nick) = 401 => "{} :No such nick/channel", + ErrNoSuchServer(ServerName) = 402 => "{} :No such server", + ErrNoSuchChannel(Channel) = 403 => "{} :No such channel", + ErrCannotSendToChan(Channel) = 404 => "{} :Cannot send to channel", + ErrTooManyChannels(Channel) = 405 => "{} :You have joined too many channels", + ErrWasNoSuchNick(Nick) = 406 => "{} :There was no such nickname", + ErrTooManyTargets(Target) = 407 => "{} :Duplicate recipients. No message delivered", + ErrNoOrigin = 409 => ":No origin specified", + ErrNoRecipient(CommandName) = 411 => ":No recipient given ({})", + ErrNoTextToSend = 412 => ":No text to send", + ErrNoTopLevel(Mask) = 413 => "{} :No toplevel domain specified", + ErrWildTopLevel(Mask) = 414 => "{} :Wildcard in toplevel domain", + ErrUnknownCommand(CommandName) = 421 => "{} :Unknown command", + ErrNoMotd = 422 => ":MOTD File is missing", + ErrNoAdminInfo(ServerName) = 423 => "{} :No administrative info available", + ErrFileError(FileOp, File) = 424 => ":File error doing {} on {}", + ErrNoNickGiven = 431 => ":No nickname given", + ErrErroneusNick(Nick) = 432 => "{} :Erroneus nickname", + ErrNickInUse(Nick) = 433 => "{} :Nick is already in use", + ErrNickCollision(Nick) = 436 => "{} :Nick collision KILL", + ErrUserNotInChannel(Nick, Channel) = 441 => "{} {} :They aren't on that channel", + ErrNotOnChannel(Channel) = 442 => "{} :You're not on that channel", + ErrUserOnChannel(Username, Channel) = 443 => "{} {} :is already on channel", + ErrNoLogin(Username) = 444 => "{} :User not logged in", + ErrSummonDisabled = 445 => ":SUMMON has been disabled", + ErrUsersDisabled = 446 => ":USERS has been disabled", + ErrNotRegistered = 451 => ":You have not registered", + ErrNeedMoreParams(CommandName) = 461 => "{} :Not enough parameters", + ErrAlreadyRegistered = 462 => ":You may not reregister", + ErrNoPermForHost = 463 => ":Your host isn't among the privileged", + ErrPasswdMismatch = 464 => ":Password incorrect", + ErrYoureBannedCreep = 465 => ":You are banned from this server", + ErrKeySet(Channel) = 467 => "{} :Channel key already set", + ErrChannelIsFull(Channel) = 471 => "{} :Cannot join channel (+l)", + ErrUnknownMode(Char) = 472 => "{} :is unknown mode char to me", + ErrInviteOnlyChan(Channel) = 473 => "{} :Cannot join channel (+i)", + ErrBannedFromChan(Channel) = 474 => "{} :Cannot join channel (+b)", + ErrBadChannelKey(Channel) = 475 => "{} :Cannot join channel (+k)", + ErrNoPrivileges = 481 => ":Permission Denied- You're not an IRC operator", + ErrChanOPrivsNeeded(Channel) = 482 => "{} :You're not channel operator", + ErrCantKillServer = 483 => ":You cant kill a server!", + ErrNoOperHost = 491 => ":No O-lines for your host", + ErrUmodeUnknownFlag = 501 => ":Unknown MODE flag", + ErrUsersDontMatch = 502 => ":Cant change mode for other users", + RplNone = 300, + RplUserHost(UserHost) = 302 => "{}", + RplIson(Nicks) = 303 => "{}", + RplAway(Nick, FreeText) = 301 => "{} :{}", + RplUnaway = 305 => ":You are no longer marked as being away", + RplNowAway = 306 => ":You have been marked as being away", + RplWhoisUser(Nick, Username, HostName, RealName) = 311 => "{} {} {} * :{}", + RplWhoisServer(Nick, ServerName, ServerInfo) = 312 => "{} {} :{}", + RplWhoisOperator(Nick) = 313 => "{} :is an IRC operator", + RplWhoisIdle(Nick, Integer) = 317 => "{} {} :seconds idle", + RplEndOfWhois(Nick) = 318 => "{} :End of /WHOIS list", + RplWhoisChannels(Nick, RightsPrefixedChannels) = 319 => "{} :{}", // todo + RplWhoWasUser(Nick, Username, HostName, RealName) = 314 => "{} {} {} * :{}", + RplEndOfWhoWas(Nick) = 369 => "{} :End of WHOWAS", + RplListStart = 321 => "Channel :Users RealName", + RplList(Channel, AmtVisible, FreeText) = 322 => "{} {} :{}", + RplListEnd = 323 => ":End of /LIST", + RplChannelModeIs(Channel, Mode, ModeParams) = 324 => "{} {} {}", + RplNoTopic(Channel) = 331 => "{} :No topic is set", + RplTopic(Channel, FreeText) = 332 => "{} :{}", + RplInviting(Channel, Nick) = 341 => "{} {}", + RplVersion(Version, Debuglevel, ServerName, FreeText) = 351 => "{}.{} {} :{}", + RplWhoReply(Channel, Username, HostName, ServerName, Nick, HG, Hopcount, RealName) = 352 => "{} {} {} {} {} {}[*][@|+] :{} {}", + RplEndOfWho(Target) = 315 => "{} :End of /WHO list", + RplNamReply(Channel, RightsPrefixedNicks) = 353 => "{} :{}", + RplEndOfNames(Channel) = 366 => "{} :End of /NAMES list", + RplLinks(Mask, ServerName, Hopcount, ServerInfo) = 364 => "{} {} :{} {}", + RplEndOfLinks(Mask) = 365 => "{} :End of /LINKS list", + RplBanList(Channel, Banid) = 367 => "{} {}", + RPLEndOfBanList(Channel) = 368 => "{} :End of channel ban list", + RplInfo(String) = 371 => ":{}", + RplEndOfInfo = 374 => ":End of /INFO list", + RplMotdStart(ServerName) = 375 => ":- {} Message of the day -", + RplMotd(FreeText) = 372 => ":- {}", + RplEndOfMotd = 376 => ":End of /MOTD command", + RplYoureOper = 381 => ":You are now an IRC operator", + RplRehashing(ConfigFile) = 382 => "{} :Rehashing", + RplTime = 391, +} -- libgit2 1.7.2