diff --git a/.gitea/workflows/test-pr.yml b/.gitea/workflows/test-pr.yml new file mode 100644 index 0000000..18dca5b --- /dev/null +++ b/.gitea/workflows/test-pr.yml @@ -0,0 +1,19 @@ +name: check-and-test +on: [push, pull_request] +jobs: + check-and-test: + runs-on: ubuntu-latest + steps: + - name: git checkout repository + uses: actions/checkout@v4 + - name: setup rust + uses: https://github.com/actions-rs/toolchain@v1 + - name: cargo check + uses: https://github.com/actions-rs/cargo@v1 + with: + command: check + - name: test + uses: https://github.com/actions-rs/cargo@v1 + with: + command: test + args: --workspace -- --skip projections::irc diff --git a/Cargo.lock b/Cargo.lock index ad25c33..2469e90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,54 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -161,6 +209,52 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "const-oid" version = "0.9.5" @@ -754,15 +848,17 @@ version = "0.0.1-dev" dependencies = [ "anyhow", "assert_matches", + "clap", "derive_more", "figment", "futures-util", "http-body-util", "hyper 1.0.0-rc.3", - "lazy_static", - "nom", + "mgmt-api", "nonempty", "prometheus", + "proto-irc", + "proto-xmpp", "quick-xml", "regex", "reqwest", @@ -846,6 +942,13 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "mgmt-api" +version = "0.0.1-dev" +dependencies = [ + "serde", +] + [[package]] name = "mime" version = "0.3.17" @@ -1151,6 +1254,30 @@ dependencies = [ "thiserror", ] +[[package]] +name = "proto-irc" +version = "0.0.1-dev" +dependencies = [ + "anyhow", + "assert_matches", + "futures-util", + "nom", + "nonempty", + "tokio", +] + +[[package]] +name = "proto-xmpp" +version = "0.0.1-dev" +dependencies = [ + "anyhow", + "derive_more", + "lazy_static", + "quick-xml", + "regex", + "tokio", +] + [[package]] name = "quick-xml" version = "0.30.0" @@ -1770,6 +1897,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.5.0" @@ -2080,6 +2213,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index 7b855e3..4c90804 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,33 +1,59 @@ +[workspace] +members = [ + ".", + "crates/proto-irc", + "crates/proto-xmpp", + "crates/mgmt-api", +] + +[workspace.package] +version = "0.0.1-dev" + +[workspace.dependencies] +nom = "7.1.3" +assert_matches = "1.5.0" +tokio = { version = "1.24.1", features = ["full"] } # async runtime +futures-util = "0.3.25" +anyhow = "1.0.68" # error utils +nonempty = "0.8.1" +quick-xml = { version = "0.30.0", features = ["async-tokio"] } +lazy_static = "1.4.0" +regex = "1.7.1" +derive_more = "0.99.17" +clap = { version = "4.4.4", features = ["derive"] } +serde = { version = "1.0.152", features = ["rc", "serde_derive"] } + [package] name = "lavina" -version = "0.0.1-dev" +version.workspace = true edition = "2021" publish = false [dependencies] -anyhow = "1.0.68" # error utils +anyhow.workspace = true figment = { version = "0.10.8", features = ["env", "toml"] } # configuration files hyper = { version = "1.0.0-rc.3,<1.0.0-rc.4", features = ["server", "http1"] } # http server http-body-util = "0.1.0-rc.3" -serde = { version = "1.0.152", features = ["rc", "serde_derive"] } +serde.workspace = true serde_json = "1.0.93" -tokio = { version = "1.24.1", features = ["full"] } # async runtime +tokio.workspace = true tracing = "0.1.37" # logging & tracing api tracing-subscriber = "0.3.16" -futures-util = "0.3.25" +futures-util.workspace = true prometheus = { version = "0.13.3", default-features = false } -regex = "1.7.1" -lazy_static = "1.4.0" -nom = "7.1.3" -nonempty = "0.8.1" +nonempty.workspace = true tokio-rustls = "0.24.1" rustls-pemfile = "1.0.2" -quick-xml = { version = "0.30.0", features = ["async-tokio"] } -derive_more = "0.99.17" +quick-xml.workspace = true +derive_more.workspace = true uuid = { version = "1.3.0", features = ["v4"] } sqlx = { version = "0.7.0-alpha.2", features = ["sqlite", "migrate"] } +proto-irc = { path = "crates/proto-irc" } +proto-xmpp = { path = "crates/proto-xmpp" } +mgmt-api = { path = "crates/mgmt-api" } +clap.workspace = true [dev-dependencies] -assert_matches = "1.5.0" +assert_matches.workspace = true regex = "1.7.1" reqwest = { version = "0.11", default-features = false } diff --git a/crates/mgmt-api/Cargo.toml b/crates/mgmt-api/Cargo.toml new file mode 100644 index 0000000..030f3cf --- /dev/null +++ b/crates/mgmt-api/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "mgmt-api" +edition = "2021" +version.workspace = true +publish = false + +[dependencies] +serde.workspace = true diff --git a/crates/mgmt-api/src/lib.rs b/crates/mgmt-api/src/lib.rs new file mode 100644 index 0000000..cfe5b69 --- /dev/null +++ b/crates/mgmt-api/src/lib.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct ErrorResponse<'a> { + pub code: &'a str, + pub message: &'a str, +} + +#[derive(Serialize, Deserialize)] +pub struct CreatePlayerRequest<'a> { + pub name: &'a str, +} + +#[derive(Serialize, Deserialize)] +pub struct ChangePasswordRequest<'a> { + pub player_name: &'a str, + pub password: &'a str, +} + +pub mod paths { + pub const CREATE_PLAYER: &'static str = "/mgmt/create_player"; + pub const SET_PASSWORD: &'static str = "/mgmt/set_password"; +} + +pub mod errors { + pub const INVALID_PATH: &'static str = "invalid_path"; + pub const MALFORMED_REQUEST: &'static str = "malformed_request"; + pub const PLAYER_NOT_FOUND: &'static str = "player_not_found"; +} diff --git a/crates/proto-irc/Cargo.toml b/crates/proto-irc/Cargo.toml new file mode 100644 index 0000000..4a5286e --- /dev/null +++ b/crates/proto-irc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "proto-irc" +edition = "2021" +version.workspace = true +publish = false + +[dependencies] +nom.workspace = true +nonempty.workspace = true +tokio.workspace = true +futures-util.workspace = true +anyhow.workspace = true + +[dev-dependencies] +assert_matches.workspace = true diff --git a/src/protos/irc/client.rs b/crates/proto-irc/src/client.rs similarity index 100% rename from src/protos/irc/client.rs rename to crates/proto-irc/src/client.rs diff --git a/src/protos/irc/mod.rs b/crates/proto-irc/src/lib.rs similarity index 98% rename from src/protos/irc/mod.rs rename to crates/proto-irc/src/lib.rs index 0997a79..cd1f64f 100644 --- a/src/protos/irc/mod.rs +++ b/crates/proto-irc/src/lib.rs @@ -1,10 +1,13 @@ //! Client-to-Server IRC protocol. pub mod client; +mod prelude; pub mod server; +#[cfg(test)] +mod testkit; pub mod user; -use std::io::Result; use crate::prelude::Str; +use std::io::Result; use nom::{ branch::alt, @@ -101,7 +104,7 @@ mod test { use assert_matches::*; use super::*; - use crate::util::testkit::*; + use crate::testkit::*; #[test] fn test_chan_global() { diff --git a/crates/proto-irc/src/prelude.rs b/crates/proto-irc/src/prelude.rs new file mode 100644 index 0000000..04c4d62 --- /dev/null +++ b/crates/proto-irc/src/prelude.rs @@ -0,0 +1,3 @@ +use std::sync::Arc; + +pub type Str = Arc; diff --git a/src/protos/irc/server.rs b/crates/proto-irc/src/server.rs similarity index 98% rename from src/protos/irc/server.rs rename to crates/proto-irc/src/server.rs index 194c756..1bead65 100644 --- a/src/protos/irc/server.rs +++ b/crates/proto-irc/src/server.rs @@ -3,7 +3,7 @@ use tokio::io::AsyncWrite; use tokio::io::AsyncWriteExt; use super::*; -use crate::protos::irc::user::PrefixedNick; +use crate::user::PrefixedNick; /// Server-to-client message. #[derive(Clone, Debug, PartialEq, Eq)] @@ -287,7 +287,9 @@ impl ServerMessageBody { chan.write_async(writer).await?; writer.write_all(b" :").await?; for member in members { - writer.write_all(member.prefix.to_string().as_bytes()).await?; + writer + .write_all(member.prefix.to_string().as_bytes()) + .await?; writer.write_all(member.nick.as_bytes()).await?; writer.write_all(b" ").await?; } @@ -387,7 +389,7 @@ mod test { use assert_matches::*; use super::*; - use crate::util::testkit::*; + use crate::testkit::*; #[test] fn test_server_message_notice() { diff --git a/crates/proto-irc/src/testkit.rs b/crates/proto-irc/src/testkit.rs new file mode 100644 index 0000000..57949d0 --- /dev/null +++ b/crates/proto-irc/src/testkit.rs @@ -0,0 +1,16 @@ +use std::future::Future; +use std::task::{Context, Poll}; + +use futures_util::task::noop_waker_ref; +use tokio::pin; + +pub fn sync_future(future: impl Future) -> anyhow::Result { + let waker = noop_waker_ref(); + let mut context = Context::from_waker(waker); + pin!(future); + if let Poll::Ready(a) = future.poll(&mut context) { + Ok(a) + } else { + Err(anyhow::Error::msg("Future has suspended")) + } +} diff --git a/src/protos/irc/user.rs b/crates/proto-irc/src/user.rs similarity index 67% rename from src/protos/irc/user.rs rename to crates/proto-irc/src/user.rs index e426812..3998fc2 100644 --- a/src/protos/irc/user.rs +++ b/crates/proto-irc/src/user.rs @@ -1,6 +1,5 @@ use super::*; use std::fmt; -use crate::core::player::PlayerId; #[derive(Clone, Debug, PartialEq, Eq)] pub enum Prefix { @@ -23,10 +22,9 @@ pub struct PrefixedNick { impl PrefixedNick { pub fn from_str(nick: Str) -> PrefixedNick { - PrefixedNick { prefix: Prefix::Empty, nick } + PrefixedNick { + prefix: Prefix::Empty, + nick, + } } - - pub fn from_player_id(id: PlayerId) -> PrefixedNick { - PrefixedNick { prefix: Prefix::Empty, nick: id.into_inner() } - } -} \ No newline at end of file +} diff --git a/crates/proto-xmpp/Cargo.toml b/crates/proto-xmpp/Cargo.toml new file mode 100644 index 0000000..d8b28f9 --- /dev/null +++ b/crates/proto-xmpp/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "proto-xmpp" +edition = "2021" +version.workspace = true + +[dependencies] +quick-xml.workspace = true +lazy_static.workspace = true +regex.workspace = true +anyhow.workspace = true +tokio.workspace = true +derive_more.workspace = true diff --git a/src/protos/xmpp/bind.rs b/crates/proto-xmpp/src/bind.rs similarity index 84% rename from src/protos/xmpp/bind.rs rename to crates/proto-xmpp/src/bind.rs index ebabff6..8d9a4cf 100644 --- a/src/protos/xmpp/bind.rs +++ b/crates/proto-xmpp/src/bind.rs @@ -1,11 +1,11 @@ use std::fmt::Display; -use nom::AsBytes; +use anyhow::{anyhow, Result}; use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use quick_xml::name::{Namespace, ResolveResult}; use crate::prelude::*; -use crate::util::xml::*; +use crate::xml::*; pub const XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-bind"; @@ -49,7 +49,7 @@ impl Jid { } let m = RE .captures(i) - .ok_or(ffail!("Incorrectly format jid: {i}"))?; + .ok_or(anyhow!("Incorrectly format jid: {i}"))?; let name = m.get(2).map(|name| Name(name.as_str().into())); let server = m.get(3).unwrap(); @@ -88,18 +88,18 @@ impl FromXml for BindRequest { fn parse() -> Self::P { |(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result { - let mut resource: Option> = None; + let mut resource: Option = None; let Event::Start(bytes) = event else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); }; if bytes.name().0 != BindRequest::NAME.as_bytes() { - return Err(ffail!("Unexpected XML tag: {:?}", bytes.name())); + return Err(anyhow!("Unexpected XML tag: {:?}", bytes.name())); } let ResolveResult::Bound(Namespace(ns)) = namespace else { - return Err(ffail!("No namespace provided")); + return Err(anyhow!("No namespace provided")); }; if ns != XMLNS.as_bytes() { - return Err(ffail!("Incorrect namespace")); + return Err(anyhow!("Incorrect namespace")); } loop { let (namespace, event) = yield; @@ -107,31 +107,27 @@ impl FromXml for BindRequest { Event::Start(bytes) if bytes.name().0 == b"resource" => { let (namespace, event) = yield; let Event::Text(text) = event else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); }; - resource = Some(text.as_bytes().into()); + resource = Some(std::str::from_utf8(&*text)?.into()); let (namespace, event) = yield; let Event::End(bytes) = event else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); }; if bytes.name().0 != b"resource" { - return Err(ffail!("Unexpected XML tag: {:?}", bytes.name())); + return Err(anyhow!("Unexpected XML tag: {:?}", bytes.name())); } } Event::End(bytes) if bytes.name().0 == BindRequest::NAME.as_bytes() => { break; } - _ => return Err(ffail!("Unexpected XML event: {event:?}")), + _ => return Err(anyhow!("Unexpected XML event: {event:?}")), } } let Some(resource) = resource else { - return Err(ffail!("No resource was provided")); + return Err(anyhow!("No resource was provided")); }; - let resource = match std::str::from_utf8(resource.as_bytes()) { - Ok(e) => e.to_string(), - Err(err) => return Err(err.into()), - }; - Ok(BindRequest(Resource(resource.into()))) + Ok(BindRequest(Resource(resource))) } } } diff --git a/src/protos/xmpp/client.rs b/crates/proto-xmpp/src/client.rs similarity index 99% rename from src/protos/xmpp/client.rs rename to crates/proto-xmpp/src/client.rs index 606e03c..2a6697f 100644 --- a/src/protos/xmpp/client.rs +++ b/crates/proto-xmpp/src/client.rs @@ -3,8 +3,11 @@ use quick_xml::events::attributes::Attribute; use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use quick_xml::name::{QName, ResolveResult}; +use anyhow::{Result, anyhow as ffail}; + + use crate::prelude::*; -use crate::util::xml::*; +use crate::xml::*; use super::bind::Jid; @@ -308,9 +311,9 @@ impl Parser for IqParser { } IqParserInner::Final(state) => { if let Event::End(ref bytes) = event { - let id = fail_fast!(state.id.ok_or_else(|| fail("No id provided"))); - let r#type = fail_fast!(state.r#type.ok_or_else(|| fail("No type provided"))); - let body = fail_fast!(state.body.ok_or_else(|| fail("No body provided"))); + let id = fail_fast!(state.id.ok_or_else(|| ffail!("No id provided"))); + let r#type = fail_fast!(state.r#type.ok_or_else(|| ffail!("No type provided"))); + let body = fail_fast!(state.body.ok_or_else(|| ffail!("No body provided"))); Continuation::Final(Ok(Iq { from: state.from, id, @@ -579,7 +582,7 @@ impl ToXml for Presence { #[cfg(test)] mod tests { - use crate::protos::xmpp::bind::{BindRequest, Name, Resource, Server}; + use crate::bind::{BindRequest, Name, Resource, Server}; use super::*; use quick_xml::NsReader; diff --git a/src/protos/xmpp/disco.rs b/crates/proto-xmpp/src/disco.rs similarity index 99% rename from src/protos/xmpp/disco.rs rename to crates/proto-xmpp/src/disco.rs index a15e610..973242d 100644 --- a/src/protos/xmpp/disco.rs +++ b/crates/proto-xmpp/src/disco.rs @@ -2,8 +2,8 @@ use quick_xml::events::attributes::Attribute; use quick_xml::events::{BytesEnd, BytesStart, Event}; use quick_xml::name::{QName, ResolveResult}; -use crate::prelude::*; -use crate::util::xml::*; +use anyhow::{Result, anyhow as ffail}; +use crate::xml::*; use super::bind::Jid; diff --git a/src/protos/xmpp/mod.rs b/crates/proto-xmpp/src/lib.rs similarity index 78% rename from src/protos/xmpp/mod.rs rename to crates/proto-xmpp/src/lib.rs index 0f99af7..ee85319 100644 --- a/src/protos/xmpp/mod.rs +++ b/crates/proto-xmpp/src/lib.rs @@ -1,3 +1,10 @@ +#![feature( + generators, + generator_trait, + type_alias_impl_trait, + impl_trait_in_assoc_type +)] + pub mod bind; pub mod client; pub mod disco; @@ -8,6 +15,8 @@ pub mod session; pub mod stanzaerror; pub mod stream; pub mod tls; +mod prelude; +pub mod xml; // Implemented as a macro instead of a fn due to borrowck limitations macro_rules! skip_text { @@ -25,4 +34,4 @@ macro_rules! skip_text { }; } -pub(super) use skip_text; +pub(crate) use skip_text; diff --git a/src/protos/xmpp/muc/mod.rs b/crates/proto-xmpp/src/muc/mod.rs similarity index 86% rename from src/protos/xmpp/muc/mod.rs rename to crates/proto-xmpp/src/muc/mod.rs index 8822fe3..250d50d 100644 --- a/src/protos/xmpp/muc/mod.rs +++ b/crates/proto-xmpp/src/muc/mod.rs @@ -1,8 +1,8 @@ use quick_xml::events::Event; use quick_xml::name::ResolveResult; -use crate::prelude::*; -use crate::util::xml::*; +use anyhow::{anyhow, Result}; +use crate::xml::*; pub const XMLNS: &'static str = "http://jabber.org/protocol/muc"; @@ -22,7 +22,7 @@ impl FromXml for History { let (bytes, end) = match event { Event::Start(bytes) => (bytes, false), Event::Empty(bytes) => (bytes, true), - _ => return Err(ffail!("Unexpected XML event: {event:?}")), + _ => return Err(anyhow!("Unexpected XML event: {event:?}")), }; for attr in bytes.attributes() { let attr = attr?; @@ -51,7 +51,7 @@ impl FromXml for History { let (namespace, event) = yield; let Event::End(bytes) = event else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); }; Ok(history) } @@ -73,15 +73,15 @@ impl FromXml for Password { fn parse() -> Self::P { |(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result { let Event::Start(bytes) = event else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); }; let (namespace, event) = yield; let Event::Text(bytes) = event else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); }; let s = std::str::from_utf8(bytes)?.to_string(); let Event::End(bytes) = event else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); }; Ok(Password(s)) } @@ -109,7 +109,7 @@ impl FromXml for X { let (bytes, end) = match event { Event::Start(bytes) => (bytes, false), Event::Empty(bytes) => (bytes, true), - _ => return Err(ffail!("Unexpected XML event: {event:?}")), + _ => return Err(anyhow!("Unexpected XML event: {event:?}")), }; if end { return Ok(res); @@ -121,7 +121,7 @@ impl FromXml for X { Event::Start(bytes) => bytes, Event::Empty(bytes) => bytes, Event::End(_) => break, - _ => return Err(ffail!("Unexpected XML event: {event:?}")), + _ => return Err(anyhow!("Unexpected XML event: {event:?}")), }; if bytes.name().0 == Password::NAME.as_bytes() { let password = delegate_parsing!(Password, namespace, event)?; @@ -130,7 +130,7 @@ impl FromXml for X { let history = delegate_parsing!(History, namespace, event)?; res.history = Some(history); } else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); } } diff --git a/crates/proto-xmpp/src/prelude.rs b/crates/proto-xmpp/src/prelude.rs new file mode 100644 index 0000000..04c4d62 --- /dev/null +++ b/crates/proto-xmpp/src/prelude.rs @@ -0,0 +1,3 @@ +use std::sync::Arc; + +pub type Str = Arc; diff --git a/src/protos/xmpp/roster.rs b/crates/proto-xmpp/src/roster.rs similarity index 96% rename from src/protos/xmpp/roster.rs rename to crates/proto-xmpp/src/roster.rs index 4990bc1..e153843 100644 --- a/src/protos/xmpp/roster.rs +++ b/crates/proto-xmpp/src/roster.rs @@ -1,7 +1,7 @@ use quick_xml::events::{BytesStart, Event}; -use crate::prelude::*; -use crate::util::xml::*; +use crate::xml::*; +use anyhow::{anyhow as ffail, Result}; pub const XMLNS: &'static str = "jabber:iq:roster"; diff --git a/src/protos/xmpp/sasl.rs b/crates/proto-xmpp/src/sasl.rs similarity index 83% rename from src/protos/xmpp/sasl.rs rename to crates/proto-xmpp/src/sasl.rs index e78f605..7068e7a 100644 --- a/src/protos/xmpp/sasl.rs +++ b/crates/proto-xmpp/src/sasl.rs @@ -7,7 +7,7 @@ use quick_xml::{ use tokio::io::{AsyncBufRead, AsyncWrite}; use super::skip_text; -use crate::prelude::*; +use anyhow::{anyhow, Result}; pub enum Mechanism { Plain, @@ -22,7 +22,7 @@ impl Mechanism { pub fn from_str(input: &[u8]) -> Result { match input { b"PLAIN" => Ok(Mechanism::Plain), - _ => Err(fail(format!("unknown auth mechanism: {input:?}").as_str())), + _ => Err(anyhow!("unknown auth mechanism: {input:?}")), } } } @@ -48,20 +48,20 @@ impl Auth { if let Some(mechanism) = mechanism { Mechanism::from_str(mechanism.borrow())? } else { - return Err(fail("expected mechanism attribute in ")); + return Err(anyhow!("expected mechanism attribute in ")); } } else { - return Err(fail("expected start of ")); + return Err(anyhow!("expected start of ")); }; let body = if let Event::Text(text) = reader.read_event_into_async(buf).await? { text.into_inner().into_owned() } else { - return Err(fail("expected text body in ")); + return Err(anyhow!("expected text body in ")); }; if let Event::End(_) = reader.read_event_into_async(buf).await? { //TODO } else { - return Err(fail("expected end of ")); + return Err(anyhow!("expected end of ")); }; Ok(Auth { mechanism, body }) diff --git a/src/protos/xmpp/session.rs b/crates/proto-xmpp/src/session.rs similarity index 86% rename from src/protos/xmpp/session.rs rename to crates/proto-xmpp/src/session.rs index 9f0104b..ba1ab43 100644 --- a/src/protos/xmpp/session.rs +++ b/crates/proto-xmpp/src/session.rs @@ -1,7 +1,7 @@ use quick_xml::events::{BytesStart, Event}; -use crate::prelude::*; -use crate::util::xml::*; +use crate::xml::*; +use anyhow::{anyhow, Result}; pub const XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-session"; @@ -29,11 +29,11 @@ impl Parser for SessionParser { Continuation::Continue(SessionParser(SessionParserInner::InSession)) } Event::Empty(_) => Continuation::Final(Ok(Session)), - _ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))), + _ => Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))), }, SessionParserInner::InSession => match event { Event::End(_) => Continuation::Final(Ok(Session)), - _ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))), + _ => Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))), }, } } diff --git a/src/protos/xmpp/stanzaerror.rs b/crates/proto-xmpp/src/stanzaerror.rs similarity index 100% rename from src/protos/xmpp/stanzaerror.rs rename to crates/proto-xmpp/src/stanzaerror.rs diff --git a/src/protos/xmpp/stream.rs b/crates/proto-xmpp/src/stream.rs similarity index 98% rename from src/protos/xmpp/stream.rs rename to crates/proto-xmpp/src/stream.rs index 1f133a3..4d11039 100644 --- a/src/protos/xmpp/stream.rs +++ b/crates/proto-xmpp/src/stream.rs @@ -5,8 +5,9 @@ use quick_xml::{NsReader, Writer}; use tokio::io::{AsyncBufRead, AsyncWrite}; use super::skip_text; -use crate::prelude::*; -use crate::util::xml::ToXml; + +use anyhow::{anyhow, Result}; +use crate::xml::ToXml; pub static XMLNS: &'static str = "http://etherx.jabber.org/streams"; pub static PREFIX: &'static str = "stream"; @@ -63,7 +64,6 @@ impl ClientStreamStart { version: version.unwrap(), }) } else { - log::error!("WAT: {incoming:?}"); Err(panic!()) } } diff --git a/src/protos/xmpp/tls.rs b/crates/proto-xmpp/src/tls.rs similarity index 91% rename from src/protos/xmpp/tls.rs rename to crates/proto-xmpp/src/tls.rs index 6fdf4ad..a5a9e0d 100644 --- a/src/protos/xmpp/tls.rs +++ b/crates/proto-xmpp/src/tls.rs @@ -5,7 +5,8 @@ use quick_xml::{NsReader, Writer}; use tokio::io::{AsyncBufRead, AsyncWrite}; use super::skip_text; -use crate::prelude::*; + +use anyhow::{anyhow, Result}; pub static XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-tls"; @@ -21,7 +22,7 @@ impl StartTLS { return Ok(StartTLS); } } - Err(ffail!("XML tag starttls expected, received: {incoming:?}")) + Err(anyhow!("XML tag starttls expected, received: {incoming:?}")) } } diff --git a/src/util/xml/ignore.rs b/crates/proto-xmpp/src/xml/ignore.rs similarity index 100% rename from src/util/xml/ignore.rs rename to crates/proto-xmpp/src/xml/ignore.rs diff --git a/src/util/xml.rs b/crates/proto-xmpp/src/xml/mod.rs similarity index 96% rename from src/util/xml.rs rename to crates/proto-xmpp/src/xml/mod.rs index 9ad847e..771bef4 100644 --- a/src/util/xml.rs +++ b/crates/proto-xmpp/src/xml/mod.rs @@ -4,7 +4,7 @@ use std::pin::Pin; use quick_xml::events::Event; use quick_xml::name::ResolveResult; -use crate::prelude::Result; +use anyhow::{anyhow, Result}; mod ignore; pub use ignore::Ignore; @@ -72,6 +72,7 @@ macro_rules! fail_fast { }; } +#[macro_export] macro_rules! delegate_parsing { ($parser: ty, $namespace: expr, $event: expr) => {{ let mut parser = <$parser as FromXml>::parse().consume($namespace, $event); @@ -88,6 +89,7 @@ macro_rules! delegate_parsing { }}; } +#[macro_export] macro_rules! match_parser { ($name: expr, $ns: expr, $event: expr; $subtype: ty, $fin: block) => { if $name.0 == <$subtype as FromXmlTag>::NAME.as_bytes() && $ns == ResolveResult::Bound(Namespace(<$subtype as FromXmlTag>::NS.as_bytes())) { @@ -105,6 +107,6 @@ macro_rules! match_parser { }; } -pub(crate) use delegate_parsing; +pub use delegate_parsing; pub(crate) use fail_fast; -pub(crate) use match_parser; +pub use match_parser; diff --git a/dist/alpine3.18.Dockerfile b/dist/alpine3.18.Dockerfile index 3a4a5d4..facdfb0 100644 --- a/dist/alpine3.18.Dockerfile +++ b/dist/alpine3.18.Dockerfile @@ -8,4 +8,4 @@ FROM alpine:3.18@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4 COPY --from=bld target/release/lavina /usr/bin/lavina VOLUME ["/etc/lavina/", "/var/lib/lavina/"] -ENTRYPOINT ["lavina"] +ENTRYPOINT ["lavina", "--config", "/etc/lavina/config.toml"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..866c756 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 120 \ No newline at end of file diff --git a/src/core/repo/mod.rs b/src/core/repo/mod.rs index 10b601a..57371ed 100644 --- a/src/core/repo/mod.rs +++ b/src/core/repo/mod.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use serde::Deserialize; use sqlx::sqlite::SqliteConnectOptions; -use sqlx::{ConnectOptions, Connection, FromRow, SqliteConnection}; +use sqlx::{ConnectOptions, Connection, FromRow, Sqlite, SqliteConnection, Transaction}; use tokio::sync::Mutex; use crate::prelude::*; @@ -102,6 +102,50 @@ impl Storage { res.close().await?; Ok(()) } + + pub async fn create_user(&mut self, name: &str) -> Result<()> { + let query = sqlx::query( + "insert into users(name) + values (?);", + ) + .bind(name); + let mut executor = self.conn.lock().await; + query.execute(&mut *executor).await?; + + Ok(()) + } + + pub async fn set_password<'a>(&'a mut self, name: &'a str, pwd: &'a str) -> Result> { + async fn inner(txn: &mut Transaction<'_, Sqlite>, name: &str, pwd: &str) -> Result> { + let id: Option<(u32,)> = sqlx::query_as("select * from users where name = ? limit 1;") + .bind(name) + .fetch_optional(&mut **txn) + .await?; + let Some((id,)) = id else { + return Ok(None); + }; + sqlx::query("insert or replace into challenges_plain_password(user_id, password) values (?, ?);") + .bind(id) + .bind(pwd) + .execute(&mut **txn) + .await?; + Ok(Some(())) + } + + let mut executor = self.conn.lock().await; + let mut tx = executor.begin().await?; + let res = inner(&mut tx, name, pwd).await; + match res { + Ok(e) => { + tx.commit().await?; + Ok(e) + } + Err(e) => { + tx.rollback().await?; + Err(e) + } + } + } } #[derive(FromRow)] diff --git a/src/main.rs b/src/main.rs index cfc3955..ddaab27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,11 +8,12 @@ mod core; mod prelude; mod projections; -mod protos; mod util; use std::future::Future; +use std::path::Path; +use clap::Parser; use figment::providers::Format; use figment::{providers::Toml, Figment}; use prometheus::Registry as MetricsRegistry; @@ -31,11 +32,15 @@ struct ServerConfig { storage: core::repo::StorageConfig, } +#[derive(Parser)] +struct CliArgs { + #[arg(long)] + config: Box, +} + fn load_config() -> Result { - // TODO get config path as a cmd line arg - let raw_config = Figment::new() - .merge(Toml::file("config.toml")) - .merge(Toml::file("/etc/lavina/config.toml")); + let args = CliArgs::parse(); + let raw_config = Figment::from(Toml::file(args.config)); let config: ServerConfig = raw_config.extract()?; Ok(config) } @@ -59,7 +64,7 @@ async fn main() -> Result<()> { let rooms = RoomRegistry::new(&mut metrics, storage.clone())?; let mut players = PlayerRegistry::empty(rooms.clone(), &mut metrics)?; let telemetry_terminator = - util::telemetry::launch(telemetry_config, metrics.clone(), rooms.clone()).await?; + util::telemetry::launch(telemetry_config, metrics.clone(), rooms.clone(), storage.clone()).await?; let irc = projections::irc::launch(irc_config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await?; let xmpp = projections::xmpp::launch(xmpp_config, players.clone(), rooms.clone(), metrics.clone()).await?; tracing::info!("Started"); diff --git a/src/projections/irc/mod.rs b/src/projections/irc/mod.rs index 43e580b..a98394b 100644 --- a/src/projections/irc/mod.rs +++ b/src/projections/irc/mod.rs @@ -5,6 +5,7 @@ use futures_util::future::join_all; use nonempty::nonempty; use nonempty::NonEmpty; use prometheus::{IntCounter, IntGauge, Registry as MetricsRegistry}; + use serde::Deserialize; use tokio::io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter}; use tokio::net::tcp::{ReadHalf, WriteHalf}; @@ -15,10 +16,10 @@ use crate::core::player::*; use crate::core::repo::Storage; use crate::core::room::{RoomId, RoomInfo, RoomRegistry}; use crate::prelude::*; -use crate::protos::irc::client::{client_message, ClientMessage}; -use crate::protos::irc::server::{AwayStatus, ServerMessage, ServerMessageBody}; -use crate::protos::irc::{Chan, Recipient}; -use crate::protos::irc::user::PrefixedNick; +use proto_irc::client::{client_message, ClientMessage}; +use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody}; +use proto_irc::{Chan, Recipient}; +use proto_irc::user::{Prefix, PrefixedNick}; use crate::util::Terminator; #[cfg(test)] @@ -51,6 +52,7 @@ async fn handle_socket( termination: Deferred<()>, // TODO use it to stop the connection gracefully mut storage: Storage, ) -> Result<()> { + log::info!("Received an IRC connection from {socket_addr}"); let (reader, writer) = stream.split(); let mut reader: BufReader = BufReader::new(reader); let mut writer = BufWriter::new(writer); @@ -73,10 +75,13 @@ async fn handle_socket( match registered_user { Ok(user) => { + log::debug!("User registered"); handle_registered_socket(config, players, rooms, &mut reader, &mut writer, user) .await?; } - Err(_) => {} + Err(_) => { + log::debug!("Registration failed"); + } } stream.shutdown().await?; @@ -669,7 +674,7 @@ async fn produce_on_join_cmd_messages( } .write_async(writer) .await?; - let prefixed_members: Vec = room_info.members.iter().map(|member| PrefixedNick::from_player_id(member.clone())).collect(); + let prefixed_members: Vec = room_info.members.iter().map(|member| PrefixedNick::from_str(member.clone().into_inner())).collect(); let non_empty_members: NonEmpty = NonEmpty::from_vec(prefixed_members).unwrap_or(nonempty![PrefixedNick::from_str(user.nickname.clone())]); ServerMessage { diff --git a/src/projections/xmpp/mod.rs b/src/projections/xmpp/mod.rs index a02ceca..db19455 100644 --- a/src/projections/xmpp/mod.rs +++ b/src/projections/xmpp/mod.rs @@ -22,14 +22,13 @@ use tokio_rustls::TlsAcceptor; use crate::core::player::{PlayerConnection, PlayerId, PlayerRegistry}; use crate::core::room::{RoomId, RoomRegistry}; use crate::prelude::*; -use crate::protos::xmpp; -use crate::protos::xmpp::bind::{BindResponse, Jid, Name, Resource, Server}; -use crate::protos::xmpp::client::{Iq, Message, MessageType, Presence}; -use crate::protos::xmpp::disco::*; -use crate::protos::xmpp::roster::RosterQuery; -use crate::protos::xmpp::session::Session; -use crate::protos::xmpp::stream::*; -use crate::util::xml::{Continuation, FromXml, Parser, ToXml}; +use proto_xmpp::bind::{BindResponse, Jid, Name, Resource, Server}; +use proto_xmpp::client::{Iq, Message, MessageType, Presence}; +use proto_xmpp::disco::*; +use proto_xmpp::roster::RosterQuery; +use proto_xmpp::session::Session; +use proto_xmpp::stream::*; +use proto_xmpp::xml::{Continuation, FromXml, Parser, ToXml}; use crate::util::Terminator; use self::proto::{ClientPacket, IqClientBody}; @@ -149,7 +148,7 @@ async fn handle_socket( rooms: RoomRegistry, termination: Deferred<()>, // TODO use it to stop the connection gracefully ) -> Result<()> { - log::debug!("Received an XMPP connection from {socket_addr}"); + log::info!("Received an XMPP connection from {socket_addr}"); let mut reader_buf = vec![]; let (reader, writer) = stream.split(); let mut buf_reader = BufReader::new(reader); @@ -172,6 +171,7 @@ async fn handle_socket( let mut xml_writer = Writer::new(b); let authenticated = socket_auth(&mut xml_reader, &mut xml_writer, &mut reader_buf).await?; + log::debug!("User authenticated"); let mut connection = players .connect_to_player(authenticated.player_id.clone()) .await; @@ -196,7 +196,7 @@ async fn socket_force_tls( writer: &mut (impl AsyncWrite + Unpin), reader_buf: &mut Vec, ) -> Result<()> { - use crate::protos::xmpp::tls::*; + use proto_xmpp::tls::*; let xml_reader = &mut NsReader::from_reader(reader); let xml_writer = &mut Writer::new(writer); read_xml_header(xml_reader, reader_buf).await?; @@ -253,8 +253,8 @@ async fn socket_auth( .await?; xml_writer.get_mut().flush().await?; - let _ = xmpp::sasl::Auth::parse(xml_reader, reader_buf).await?; - xmpp::sasl::Success.write_xml(xml_writer).await?; + let _ = proto_xmpp::sasl::Auth::parse(xml_reader, reader_buf).await?; + proto_xmpp::sasl::Success.write_xml(xml_writer).await?; let name: Str = "darova".into(); Ok(Authenticated { @@ -314,7 +314,6 @@ async fn socket_final( match parser.consume(ns, &event) { Continuation::Final(res) => { let res = res?; - dbg!(&res); let stop = handle_packet(&mut events, res, authenticated, user_handle, rooms).await?; for i in &events { xml_writer.write_event_async(i).await?; @@ -346,7 +345,7 @@ async fn socket_final( resource: Some(Resource(author_id.into_inner().into())), }), id: None, - r#type: xmpp::client::MessageType::Groupchat, + r#type: proto_xmpp::client::MessageType::Groupchat, lang: None, subject: None, body: body.into(), @@ -414,7 +413,7 @@ async fn handle_packet( resource: Some(user.xmpp_muc_name.clone()), }), id: m.id, - r#type: xmpp::client::MessageType::Groupchat, + r#type: proto_xmpp::client::MessageType::Groupchat, lang: None, subject: None, body: m.body.clone(), @@ -485,7 +484,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: None, id: iq.id, to: None, - r#type: xmpp::client::IqType::Result, + r#type: proto_xmpp::client::IqType::Result, body: BindResponse(Jid { name: Some(Name("darova".into())), server: Server("localhost".into()), @@ -499,7 +498,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: None, id: iq.id, to: None, - r#type: xmpp::client::IqType::Result, + r#type: proto_xmpp::client::IqType::Result, body: Session, }; req.serialize(output); @@ -509,7 +508,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: None, id: iq.id, to: None, - r#type: xmpp::client::IqType::Result, + r#type: proto_xmpp::client::IqType::Result, body: RosterQuery, }; req.serialize(output); @@ -520,7 +519,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: iq.to, id: iq.id, to: None, - r#type: xmpp::client::IqType::Result, + r#type: proto_xmpp::client::IqType::Result, body: response, }; req.serialize(output); @@ -531,7 +530,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: iq.to, id: iq.id, to: None, - r#type: xmpp::client::IqType::Result, + r#type: proto_xmpp::client::IqType::Result, body: response, }; req.serialize(output); @@ -541,7 +540,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: None, id: iq.id, to: None, - r#type: xmpp::client::IqType::Error, + r#type: proto_xmpp::client::IqType::Error, body: (), }; req.serialize(output); diff --git a/src/projections/xmpp/proto.rs b/src/projections/xmpp/proto.rs index 4667dd1..5fd54d1 100644 --- a/src/projections/xmpp/proto.rs +++ b/src/projections/xmpp/proto.rs @@ -2,12 +2,12 @@ use derive_more::From; use quick_xml::events::Event; use quick_xml::name::{Namespace, ResolveResult}; -use crate::protos::xmpp::bind::BindRequest; -use crate::protos::xmpp::client::{Iq, Message, Presence}; -use crate::protos::xmpp::disco::{InfoQuery, ItemQuery}; -use crate::protos::xmpp::roster::RosterQuery; -use crate::protos::xmpp::session::Session; -use crate::util::xml::*; +use proto_xmpp::bind::BindRequest; +use proto_xmpp::client::{Iq, Message, Presence}; +use proto_xmpp::disco::{InfoQuery, ItemQuery}; +use proto_xmpp::roster::RosterQuery; +use proto_xmpp::session::Session; +use proto_xmpp::xml::*; use crate::prelude::*; diff --git a/src/protos/mod.rs b/src/protos/mod.rs deleted file mode 100644 index 25f58e2..0000000 --- a/src/protos/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Definitions of wire protocols to be used in implementations of projections. -pub mod irc; -pub mod xmpp; diff --git a/src/util/http.rs b/src/util/http.rs deleted file mode 100644 index 1f79e32..0000000 --- a/src/util/http.rs +++ /dev/null @@ -1,10 +0,0 @@ -use std::convert::Infallible; - -use http_body_util::Full; -use hyper::{body::Bytes, Response, StatusCode}; - -pub fn not_found() -> std::result::Result>, Infallible> { - let mut response = Response::new(Full::new(Bytes::from("404"))); - *response.status_mut() = StatusCode::NOT_FOUND; - Ok(response) -} diff --git a/src/util/mod.rs b/src/util/mod.rs index 0593dbd..fb2f952 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,11 +1,9 @@ use crate::prelude::*; -pub mod http; pub mod table; pub mod telemetry; #[cfg(test)] pub mod testkit; -pub mod xml; pub struct Terminator { signal: Promise<()>, diff --git a/src/util/telemetry.rs b/src/util/telemetry.rs index 26387dd..3bb5ec9 100644 --- a/src/util/telemetry.rs +++ b/src/util/telemetry.rs @@ -6,18 +6,18 @@ use http_body_util::{BodyExt, Full}; use hyper::body::Bytes; use hyper::server::conn::http1; use hyper::service::service_fn; -use hyper::{Method, Request, Response}; +use hyper::{Method, Request, Response, StatusCode}; use prometheus::{Encoder, Registry as MetricsRegistry, TextEncoder}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; +use crate::core::repo::Storage; use crate::core::room::RoomRegistry; use crate::prelude::*; - -use crate::util::http::*; use crate::util::Terminator; -type BoxBody = http_body_util::combinators::BoxBody; +use mgmt_api::*; + type HttpResult = std::result::Result; #[derive(Deserialize, Debug)] @@ -29,11 +29,12 @@ pub async fn launch( config: ServerConfig, metrics: MetricsRegistry, rooms: RoomRegistry, + storage: Storage, ) -> Result { log::info!("Starting the telemetry service"); let listener = TcpListener::bind(config.listen_on).await?; log::debug!("Listener started"); - let terminator = Terminator::spawn(|rx| main_loop(listener, metrics, rooms, rx.map(|_| ()))); + let terminator = Terminator::spawn(|rx| main_loop(listener, metrics, rooms, storage, rx.map(|_| ()))); Ok(terminator) } @@ -41,6 +42,7 @@ async fn main_loop( listener: TcpListener, metrics: MetricsRegistry, rooms: RoomRegistry, + storage: Storage, termination: impl Future, ) -> Result<()> { pin!(termination); @@ -52,10 +54,12 @@ async fn main_loop( let (stream, _) = result?; let metrics = metrics.clone(); let rooms = rooms.clone(); + let storage = storage.clone(); tokio::task::spawn(async move { let registry = metrics.clone(); let rooms = rooms.clone(); - let server = http1::Builder::new().serve_connection(stream, service_fn(move |r| route(registry.clone(), rooms.clone(), r))); + let storage = storage.clone(); + let server = http1::Builder::new().serve_connection(stream, service_fn(move |r| route(registry.clone(), rooms.clone(), storage.clone(), r))); if let Err(err) = server.await { tracing::error!("Error serving connection: {:?}", err); } @@ -70,27 +74,124 @@ async fn main_loop( async fn route( registry: MetricsRegistry, rooms: RoomRegistry, + storage: Storage, request: Request, -) -> std::result::Result, Infallible> { - match (request.method(), request.uri().path()) { - (&Method::GET, "/metrics") => Ok(endpoint_metrics(registry)?.map(BodyExt::boxed)), - (&Method::GET, "/rooms") => Ok(endpoint_rooms(rooms).await?.map(BodyExt::boxed)), - _ => Ok(not_found()?.map(BodyExt::boxed)), +) -> HttpResult>> { + let res = match (request.method(), request.uri().path()) { + (&Method::GET, "/metrics") => endpoint_metrics(registry), + (&Method::GET, "/rooms") => endpoint_rooms(rooms).await, + (&Method::POST, paths::CREATE_PLAYER) => endpoint_create_player(request, storage).await.or5xx(), + (&Method::POST, paths::SET_PASSWORD) => endpoint_set_password(request, storage).await.or5xx(), + _ => not_found(), + }; + Ok(res) +} + +fn endpoint_metrics(registry: MetricsRegistry) -> Response> { + let mf = registry.gather(); + let mut buffer = vec![]; + TextEncoder.encode(&mf, &mut buffer).expect("write to vec cannot fail"); + Response::new(Full::new(Bytes::from(buffer))) +} + +async fn endpoint_rooms(rooms: RoomRegistry) -> Response> { + // TODO introduce management API types independent from core-domain types + // TODO remove `Serialize` implementations from all core-domain types + let room_list = rooms.get_all_rooms().await.to_body(); + Response::new(room_list) +} + +async fn endpoint_create_player( + request: Request, + mut storage: Storage, +) -> Result>> { + let str = request.collect().await?.to_bytes(); + let Ok(res) = serde_json::from_slice::(&str[..]) else { + let payload = ErrorResponse { + code: errors::MALFORMED_REQUEST, + message: "The request payload contains incorrect JSON value", + } + .to_body(); + let mut response = Response::new(payload); + *response.status_mut() = StatusCode::BAD_REQUEST; + return Ok(response); + }; + storage.create_user(&res.name).await?; + log::info!("Player {} created", res.name); + let mut response = Response::new(Full::::default()); + *response.status_mut() = StatusCode::CREATED; + Ok(response) +} + +async fn endpoint_set_password( + request: Request, + mut storage: Storage, +) -> Result>> { + let str = request.collect().await?.to_bytes(); + let Ok(res) = serde_json::from_slice::(&str[..]) else { + let payload = ErrorResponse { + code: errors::MALFORMED_REQUEST, + message: "The request payload contains incorrect JSON value", + } + .to_body(); + let mut response = Response::new(payload); + *response.status_mut() = StatusCode::BAD_REQUEST; + return Ok(response); + }; + let Some(_) = storage.set_password(&res.player_name, &res.password).await? else { + let payload = ErrorResponse { + code: errors::PLAYER_NOT_FOUND, + message: "No such player exists", + } + .to_body(); + let mut response = Response::new(payload); + *response.status_mut() = StatusCode::UNPROCESSABLE_ENTITY; + return Ok(response); + }; + log::info!("Password changed for player {}", res.player_name); + let mut response = Response::new(Full::::default()); + *response.status_mut() = StatusCode::NO_CONTENT; + Ok(response) +} + +pub fn not_found() -> Response> { + let payload = ErrorResponse { + code: errors::INVALID_PATH, + message: "The path does not exist", + } + .to_body(); + + let mut response = Response::new(payload); + *response.status_mut() = StatusCode::NOT_FOUND; + response +} + +trait Or5xx { + fn or5xx(self) -> Response>; +} +impl Or5xx for Result>> { + fn or5xx(self) -> Response> { + match self { + Ok(e) => e, + Err(e) => { + let mut response = Response::new(Full::new(e.to_string().into())); + *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + response + } + } } } -fn endpoint_metrics(registry: MetricsRegistry) -> HttpResult>> { - let mf = registry.gather(); - let mut buffer = vec![]; - TextEncoder - .encode(&mf, &mut buffer) - .expect("write to vec cannot fail"); - Ok(Response::new(Full::new(Bytes::from(buffer)))) +trait ToBody { + fn to_body(&self) -> Full; } - -async fn endpoint_rooms(rooms: RoomRegistry) -> HttpResult>> { - let room_list = rooms.get_all_rooms().await; - let mut buffer = vec![]; - serde_json::to_writer(&mut buffer, &room_list).expect("unexpected fail when writing to vec"); - Ok(Response::new(Full::new(Bytes::from(buffer)))) +impl ToBody for T +where + T: Serialize, +{ + fn to_body(&self) -> Full { + let mut buffer = vec![]; + serde_json::to_writer(&mut buffer, self).expect("unexpected fail when writing to vec"); + Full::new(Bytes::from(buffer)) + } }