diff --git a/Cargo.lock b/Cargo.lock index e76d49f..d773d48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -284,6 +284,7 @@ dependencies = [ "anyhow", "argon2", "base64", + "fedserv-api", "hmac", "pbkdf2", "prost", @@ -302,6 +303,10 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "fedserv-api" +version = "0.0.1" + [[package]] name = "find-msvc-tools" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 0e25f26..5446bd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["api"] + [package] name = "fedserv" version = "0.0.1" @@ -5,6 +8,7 @@ edition = "2021" description = "Federated IRC services daemon (protocol-agnostic core, InspIRCd link first)" [dependencies] +fedserv-api = { path = "api" } tokio = { version = "1", features = ["net", "io-util", "rt-multi-thread", "macros", "time", "sync", "process"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 0000000..ea99e23 --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "fedserv-api" +version = "0.0.1" +edition = "2021" +description = "Stable module SDK: the traits and vocabulary a service or protocol module builds against." + +[dependencies] diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 0000000..3eed88c --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,225 @@ +//! The module SDK. A service or protocol module depends only on this crate: +//! it carries the traits a module implements and the normalized vocabulary the +//! engine speaks, with no storage or runtime dependencies of its own. + +// --------------------------------------------------------------------------- +// Protocol vocabulary +// --------------------------------------------------------------------------- + +// Normalized inbound facts, translated from the uplink's raw lines. +#[derive(Debug, Clone)] +pub enum NetEvent { + Registered, + EndBurst, + Ping { token: String, from: Option }, + Privmsg { from: String, to: String, text: String }, + UserConnect { uid: String, nick: String, host: String }, + NickChange { uid: String, nick: String }, + // A channel was created or bursted (an FJOIN). Subsequent single joins arrive + // as IJOIN and are not surfaced. + ChannelCreate { channel: String }, + // A user joined a channel (an FJOIN member or an IJOIN), for auto-op. `op` is + // whether they hold channel-operator status at that point (an FJOIN prefix). + Join { uid: String, channel: String, op: bool }, + // A user left a channel (PART) or was removed (KICK), for membership tracking. + Part { uid: String, channel: String }, + // A user's channel-operator status changed (FMODE +o/-o), for live op tracking. + ChannelOp { channel: String, uid: String, op: bool }, + // A channel's modes changed (FMODE), for enforcing mode locks. Our own + // changes are filtered out by the protocol layer. + ChannelModeChange { channel: String, modes: String }, + // A channel's key (+k/-k) changed, tracked so GETKEY can report it. + ChannelKey { channel: String, key: Option }, + Quit { uid: String }, + // An ircd relaying an IRCv3 account-registration request to us as the authority. + AccountRequest { reqid: String, origin: String, kind: String, account: String, p2: String, p3: String }, + // An ircd relaying a SASL exchange step to us (the SASL agent). mode = H/S/C/D. + Sasl { client: String, agent: String, mode: String, data: Vec }, + Unknown { line: String }, +} + +// Normalized outbound intents the engine wants performed on the network. +#[derive(Debug, Clone)] +pub enum NetAction { + Burst, + EndBurst, + Pong { token: String, from: Option }, + IntroduceUser { uid: String, nick: String, ident: String, host: String, gecos: String }, + Privmsg { from: String, to: String, text: String }, + Notice { from: String, to: String, text: String }, + AccountResponse { reqid: String, kind: String, account: String, status: String, code: String, message: String }, + // A SASL exchange step back to the ircd, sourced from our SASL agent. mode = C/D. + Sasl { agent: String, client: String, mode: String, data: Vec }, + // Publish network state to the uplink: target "*" is server-global (e.g. the + // advertised SASL mechanism list), otherwise a user uid (e.g. their account). + Metadata { target: String, key: String, value: String }, + // Force a user's nick (SVSNICK), e.g. renaming to a guest nick on logout. + // The protocol stamps the new nick's timestamp. + ForceNick { uid: String, nick: String }, + // Set channel modes from services, e.g. +r on a registered channel. `from` is + // the pseudoclient uid to source it from (empty = the services server). The + // protocol stamps a timestamp the ircd will accept. + ChannelMode { from: String, channel: String, modes: String }, + // Kick a user from a channel, sourced from pseudoclient `from`. + Kick { from: String, channel: String, uid: String, reason: String }, + // Set a channel's topic, sourced from pseudoclient `from`. + Topic { from: String, channel: String, topic: String }, + // Invite a user to a channel, sourced from pseudoclient `from`. + Invite { from: String, uid: String, channel: String }, + Raw(String), + // Internal only, never serialized to the wire: a registration whose password + // still needs its (expensive) key derivation. The link layer runs the + // derivation off-thread, then calls Engine::complete_register. + DeferRegister { account: String, password: String, email: Option, reply: RegReply }, + // Internal only: a password change awaiting the same off-thread derivation. + // The link layer derives, then calls Engine::complete_password_change. + DeferPassword { account: String, password: String, agent: String, uid: String }, + // Internal only: send an email (plaintext + optional HTML). The link layer + // pipes it to the configured mail command off-thread; never serialized. + SendEmail { to: String, subject: String, text: String, html: Option }, +} + +// How to answer a registration once its credentials have been derived. +#[derive(Debug, Clone)] +pub enum RegReply { + // IRCv3 account-registration relay: answer the requesting ircd. + Relay { reqid: String, kind: String }, + // NickServ REGISTER: NOTICE the requesting user, logging them in on success. + NickServ { agent: String, uid: String, nick: String }, +} + +// The ircd link layer. The engine only ever sees NetEvent / NetAction; raw +// server-to-server lines live entirely behind a Protocol impl, so a new ircd is +// one new module and the engine is untouched. +pub trait Protocol: Send { + /// Lines to send immediately on connect (auth / capability negotiation). + fn handshake(&mut self) -> Vec; + /// One raw inbound line -> zero or more normalized events. + fn parse(&mut self, line: &str) -> Vec; + /// One normalized action -> the raw line(s) that realise it. + fn serialize(&mut self, action: &NetAction) -> Vec; + /// Our own server id, used as the source prefix for server-sourced lines. + fn sid(&self) -> &str; +} + +// --------------------------------------------------------------------------- +// Service vocabulary +// --------------------------------------------------------------------------- + +// Who sent the command, resolved by the engine (UID + current nick + the +// account they are identified to, if any). +pub struct Sender<'a> { + pub uid: &'a str, + pub nick: &'a str, + pub account: Option<&'a str>, +} + +// The intent sink a service writes to. A service never mutates the network or +// the store itself; it pushes normalized actions the engine drains and applies. +#[derive(Default)] +pub struct ServiceCtx { + pub actions: Vec, +} + +impl ServiceCtx { + pub fn notice(&mut self, from: &str, to: &str, text: impl Into) { + self.actions.push(NetAction::Notice { + from: from.to_string(), + to: to.to_string(), + text: text.into(), + }); + } + + // Hand a registration to the engine to finish: its password derivation runs + // off the reactor, then the engine commits it and answers `reply`. + pub fn defer_register(&mut self, account: impl Into, password: impl Into, email: Option, reply: RegReply) { + self.actions.push(NetAction::DeferRegister { + account: account.into(), + password: password.into(), + email, + reply, + }); + } + + // Send an email (the link layer pipes it to the configured mail command). + pub fn send_email(&mut self, to: impl Into, subject: impl Into, text: impl Into, html: Option) { + self.actions.push(NetAction::SendEmail { to: to.into(), subject: subject.into(), text: text.into(), html }); + } + + // Hand a password change to the engine to finish: its derivation runs off the + // reactor, then the engine commits it and notices `uid`, sourced from `agent`. + pub fn defer_password(&mut self, account: impl Into, password: impl Into, agent: impl Into, uid: impl Into) { + self.actions.push(NetAction::DeferPassword { + account: account.into(), + password: password.into(), + agent: agent.into(), + uid: uid.into(), + }); + } + + // Log a user into an account: sets the accountname the ircd turns into + // RPL_LOGGEDIN (900) and exposes to account-tag / WHOX, the same login the + // SASL agent applies. Used after a successful REGISTER / IDENTIFY. + pub fn login(&mut self, uid: &str, account: &str) { + self.actions.push(NetAction::Metadata { + target: uid.to_string(), + key: "accountname".to_string(), + value: account.to_string(), + }); + } + + // Log a user out: clearing the accountname the ircd turns into RPL_LOGGEDOUT + // (901) and drops from account-tag / WHOX. The inverse of `login`. + pub fn logout(&mut self, uid: &str) { + self.actions.push(NetAction::Metadata { + target: uid.to_string(), + key: "accountname".to_string(), + value: String::new(), + }); + } + + // Force a user's nick (SVSNICK), e.g. to a guest nick after logout. + pub fn force_nick(&mut self, uid: &str, nick: &str) { + self.actions.push(NetAction::ForceNick { + uid: uid.to_string(), + nick: nick.to_string(), + }); + } + + // Set channel modes, sourced from pseudoclient `from` (e.g. ChanServ). + pub fn channel_mode(&mut self, from: &str, channel: &str, modes: &str) { + self.actions.push(NetAction::ChannelMode { + from: from.to_string(), + channel: channel.to_string(), + modes: modes.to_string(), + }); + } + + // Kick a user, sourced from pseudoclient `from`. + pub fn kick(&mut self, from: &str, channel: &str, uid: &str, reason: &str) { + self.actions.push(NetAction::Kick { + from: from.to_string(), + channel: channel.to_string(), + uid: uid.to_string(), + reason: reason.to_string(), + }); + } + + // Set a channel's topic, sourced from pseudoclient `from`. + pub fn topic(&mut self, from: &str, channel: &str, topic: &str) { + self.actions.push(NetAction::Topic { + from: from.to_string(), + channel: channel.to_string(), + topic: topic.to_string(), + }); + } + + // Invite a user to a channel, sourced from pseudoclient `from`. + pub fn invite(&mut self, from: &str, uid: &str, channel: &str) { + self.actions.push(NetAction::Invite { + from: from.to_string(), + uid: uid.to_string(), + channel: channel.to_string(), + }); + } +} diff --git a/modules/protocol/mod.rs b/modules/protocol/mod.rs index 900a171..c6ac0d9 100644 --- a/modules/protocol/mod.rs +++ b/modules/protocol/mod.rs @@ -1,97 +1,6 @@ -// Protocol abstraction. The engine only ever sees NetEvent / NetAction; raw -// server-to-server lines live entirely behind a Protocol impl. Adding another -// ircd later means one new module, engine untouched. +// Protocol layer. The normalized vocabulary (NetEvent / NetAction / RegReply) +// and the Protocol trait live in the fedserv-api SDK crate; re-exported here so +// the engine and the ircd modules keep referring to them as `crate::proto::*`. pub mod inspircd; -// Normalized inbound facts, translated from the uplink's raw lines. -#[derive(Debug, Clone)] -pub enum NetEvent { - Registered, - EndBurst, - Ping { token: String, from: Option }, - Privmsg { from: String, to: String, text: String }, - UserConnect { uid: String, nick: String, host: String }, - NickChange { uid: String, nick: String }, - // A channel was created or bursted (InspIRCd FJOIN). Subsequent single joins - // arrive as IJOIN and are not surfaced. - ChannelCreate { channel: String }, - // A user joined a channel (an FJOIN member or an IJOIN), for auto-op. `op` is - // whether they hold channel-operator status at that point (an FJOIN prefix). - Join { uid: String, channel: String, op: bool }, - // A user left a channel (PART) or was removed (KICK), for membership tracking. - Part { uid: String, channel: String }, - // A user's channel-operator status changed (FMODE +o/-o), for live op tracking. - ChannelOp { channel: String, uid: String, op: bool }, - // A channel's modes changed (FMODE), for enforcing mode locks. Our own - // changes are filtered out by the protocol layer. - ChannelModeChange { channel: String, modes: String }, - // A channel's key (+k/-k) changed, tracked so GETKEY can report it. - ChannelKey { channel: String, key: Option }, - Quit { uid: String }, - // An ircd relaying an IRCv3 account-registration request to us as the authority. - AccountRequest { reqid: String, origin: String, kind: String, account: String, p2: String, p3: String }, - // An ircd relaying a SASL exchange step to us (the SASL agent). mode = H/S/C/D. - Sasl { client: String, agent: String, mode: String, data: Vec }, - Unknown { line: String }, -} - -// Normalized outbound intents the engine wants performed on the network. -#[derive(Debug, Clone)] -pub enum NetAction { - Burst, - EndBurst, - Pong { token: String, from: Option }, - IntroduceUser { uid: String, nick: String, ident: String, host: String, gecos: String }, - Privmsg { from: String, to: String, text: String }, - Notice { from: String, to: String, text: String }, - AccountResponse { reqid: String, kind: String, account: String, status: String, code: String, message: String }, - // A SASL exchange step back to the ircd, sourced from our SASL agent. mode = C/D. - Sasl { agent: String, client: String, mode: String, data: Vec }, - // Publish network state to the uplink: target "*" is server-global (e.g. the - // advertised SASL mechanism list), otherwise a user uid (e.g. their account). - Metadata { target: String, key: String, value: String }, - // Force a user's nick (SVSNICK), e.g. renaming to a guest nick on logout. - // The protocol stamps the new nick's timestamp. - ForceNick { uid: String, nick: String }, - // Set channel modes from services, e.g. +r on a registered channel. `from` is - // the pseudoclient uid to source it from (empty = the services server). The - // protocol stamps a timestamp the ircd will accept. - ChannelMode { from: String, channel: String, modes: String }, - // Kick a user from a channel, sourced from pseudoclient `from`. - Kick { from: String, channel: String, uid: String, reason: String }, - // Set a channel's topic, sourced from pseudoclient `from`. - Topic { from: String, channel: String, topic: String }, - // Invite a user to a channel, sourced from pseudoclient `from`. - Invite { from: String, uid: String, channel: String }, - Raw(String), - // Internal only, never serialized to the wire: a registration whose password - // still needs its (expensive) key derivation. The link layer runs the - // derivation off-thread, then calls Engine::complete_register. - DeferRegister { account: String, password: String, email: Option, reply: RegReply }, - // Internal only: a password change awaiting the same off-thread derivation. - // The link layer derives, then calls Engine::complete_password_change. - DeferPassword { account: String, password: String, agent: String, uid: String }, - // Internal only: send an email (plaintext + optional HTML). The link layer - // pipes it to the configured mail command off-thread; never serialized. - SendEmail { to: String, subject: String, text: String, html: Option }, -} - -// How to answer a registration once its credentials have been derived. -#[derive(Debug, Clone)] -pub enum RegReply { - // IRCv3 account-registration relay: answer the requesting ircd. - Relay { reqid: String, kind: String }, - // NickServ REGISTER: NOTICE the requesting user, logging them in on success. - NickServ { agent: String, uid: String, nick: String }, -} - -pub trait Protocol: Send { - /// Lines to send immediately on connect (auth / capability negotiation). - fn handshake(&mut self) -> Vec; - /// One raw inbound line -> zero or more normalized events. - fn parse(&mut self, line: &str) -> Vec; - /// One normalized action -> the raw line(s) that realise it. - fn serialize(&mut self, action: &NetAction) -> Vec; - /// Our own server id, used as the source prefix for server-sourced lines. - fn sid(&self) -> &str; -} +pub use fedserv_api::{NetAction, NetEvent, Protocol, RegReply}; diff --git a/src/engine/service.rs b/src/engine/service.rs index f448768..7b61907 100644 --- a/src/engine/service.rs +++ b/src/engine/service.rs @@ -1,14 +1,10 @@ use crate::engine::db::Db; use crate::engine::state::Network; -use crate::proto::{NetAction, RegReply}; -// Who sent the command, resolved by the engine (UID + current nick + the -// account they are identified to, if any). -pub struct Sender<'a> { - pub uid: &'a str, - pub nick: &'a str, - pub account: Option<&'a str>, -} +// Sender + ServiceCtx (the command context a module receives) live in the +// fedserv-api SDK crate; re-exported so modules keep using +// `crate::engine::service::{Sender, ServiceCtx}`. +pub use fedserv_api::{Sender, ServiceCtx}; // A pseudo-client (NickServ, ChanServ, ...). Introduced at burst, receives the // commands users message it, reads/writes the account store, and pushes actions. @@ -31,111 +27,3 @@ pub trait Service: Send { } fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &mut Db); } - -#[derive(Default)] -pub struct ServiceCtx { - pub actions: Vec, -} - -impl ServiceCtx { - pub fn notice(&mut self, from: &str, to: &str, text: impl Into) { - self.actions.push(NetAction::Notice { - from: from.to_string(), - to: to.to_string(), - text: text.into(), - }); - } - - // Hand a registration to the engine to finish: its password derivation runs - // off the reactor, then the engine commits it and answers `reply`. - pub fn defer_register(&mut self, account: impl Into, password: impl Into, email: Option, reply: RegReply) { - self.actions.push(NetAction::DeferRegister { - account: account.into(), - password: password.into(), - email, - reply, - }); - } - - // Send an email (the link layer pipes it to the configured mail command). - pub fn send_email(&mut self, to: impl Into, subject: impl Into, text: impl Into, html: Option) { - self.actions.push(NetAction::SendEmail { to: to.into(), subject: subject.into(), text: text.into(), html }); - } - - // Hand a password change to the engine to finish: its derivation runs off the - // reactor, then the engine commits it and notices `uid`, sourced from `agent`. - pub fn defer_password(&mut self, account: impl Into, password: impl Into, agent: impl Into, uid: impl Into) { - self.actions.push(NetAction::DeferPassword { - account: account.into(), - password: password.into(), - agent: agent.into(), - uid: uid.into(), - }); - } - - // Log a user into an account: sets the accountname the ircd turns into - // RPL_LOGGEDIN (900) and exposes to account-tag / WHOX, the same login the - // SASL agent applies. Used after a successful REGISTER / IDENTIFY. - pub fn login(&mut self, uid: &str, account: &str) { - self.actions.push(NetAction::Metadata { - target: uid.to_string(), - key: "accountname".to_string(), - value: account.to_string(), - }); - } - - // Log a user out: clearing the accountname the ircd turns into RPL_LOGGEDOUT - // (901) and drops from account-tag / WHOX. The inverse of `login`. - pub fn logout(&mut self, uid: &str) { - self.actions.push(NetAction::Metadata { - target: uid.to_string(), - key: "accountname".to_string(), - value: String::new(), - }); - } - - // Force a user's nick (SVSNICK), e.g. to a guest nick after logout. - pub fn force_nick(&mut self, uid: &str, nick: &str) { - self.actions.push(NetAction::ForceNick { - uid: uid.to_string(), - nick: nick.to_string(), - }); - } - - // Set channel modes, sourced from pseudoclient `from` (e.g. ChanServ). - pub fn channel_mode(&mut self, from: &str, channel: &str, modes: &str) { - self.actions.push(NetAction::ChannelMode { - from: from.to_string(), - channel: channel.to_string(), - modes: modes.to_string(), - }); - } - - // Kick a user, sourced from pseudoclient `from`. - pub fn kick(&mut self, from: &str, channel: &str, uid: &str, reason: &str) { - self.actions.push(NetAction::Kick { - from: from.to_string(), - channel: channel.to_string(), - uid: uid.to_string(), - reason: reason.to_string(), - }); - } - - // Set a channel's topic, sourced from pseudoclient `from`. - pub fn topic(&mut self, from: &str, channel: &str, topic: &str) { - self.actions.push(NetAction::Topic { - from: from.to_string(), - channel: channel.to_string(), - topic: topic.to_string(), - }); - } - - // Invite a user to a channel, sourced from pseudoclient `from`. - pub fn invite(&mut self, from: &str, uid: &str, channel: &str) { - self.actions.push(NetAction::Invite { - from: from.to_string(), - uid: uid.to_string(), - channel: channel.to_string(), - }); - } -}