Kaynağa Gözat

darkirc: Preliminary services implementation

parazyd 2 yıl önce
ebeveyn
işleme
1c163c546b

+ 22 - 6
bin/darkirc/src/irc/client.rs

@@ -41,7 +41,7 @@ use smol::{
 
 use super::{
     server::{IrcServer, MAX_NICK_LEN},
-    Privmsg, SERVER_NAME,
+    NickServ, Privmsg, SERVER_NAME,
 };
 
 const PENALTY_LIMIT: usize = 5;
@@ -56,6 +56,8 @@ pub enum ReplyType {
     Pong(String),
     /// CAP reply
     Cap(String),
+    /// NOTICE reply (from, to, what)
+    Notice((String, String, String)),
 }
 
 /// Stateful IRC client handler, used for each client connection
@@ -77,9 +79,9 @@ pub struct Client {
     /// Registration pause marker
     pub reg_paused: AtomicBool,
     /// Client username
-    pub username: RwLock<String>,
+    pub username: Arc<RwLock<String>>,
     /// Client nickname
-    pub nickname: RwLock<String>,
+    pub nickname: Arc<RwLock<String>>,
     /// Client realname
     pub realname: RwLock<String>,
     /// Client caps
@@ -87,6 +89,8 @@ pub struct Client {
     /// Set of seen messages for the user
     /// TODO: It grows indefinitely, needs to be pruned.
     pub seen: OnceCell<sled::Tree>,
+    /// NickServ instance
+    pub nickserv: Arc<NickServ>,
 }
 
 impl Client {
@@ -98,8 +102,11 @@ impl Client {
     ) -> Result<Self> {
         let caps = HashMap::from([("no-history".to_string(), false)]);
 
+        let username = Arc::new(RwLock::new(String::from("*")));
+        let nickname = Arc::new(RwLock::new(String::from("*")));
+
         Ok(Self {
-            server,
+            server: server.clone(),
             incoming,
             addr,
             last_sent: RwLock::new(NULL_ID),
@@ -107,11 +114,12 @@ impl Client {
             penalty: AtomicUsize::new(0),
             registered: AtomicBool::new(false),
             reg_paused: AtomicBool::new(false),
-            username: RwLock::new(String::from("*")),
-            nickname: RwLock::new(String::from("*")),
+            username: username.clone(),
+            nickname: nickname.clone(),
             realname: RwLock::new(String::from("*")),
             caps: RwLock::new(caps),
             seen: OnceCell::new(),
+            nickserv: Arc::new(NickServ::new(username.clone(), nickname.clone(), server.clone())),
         })
     }
 
@@ -215,6 +223,11 @@ impl Client {
                         }
                     };
 
+                    // We should skip any attempts to contact services from the network.
+                    if ["nickserv", "chanserv"].contains(&privmsg.nick.to_lowercase().as_str()) {
+                        continue
+                    }
+
                     // If successful, potentially decrypt it:
                     self.server.try_decrypt(&mut privmsg).await;
 
@@ -265,6 +278,9 @@ impl Client {
             ReplyType::Client((nick, msg)) => format!(":{}!~anon@darkirc {}", nick, msg),
             ReplyType::Pong(origin) => format!(":{} PONG :{}", SERVER_NAME, origin),
             ReplyType::Cap(msg) => format!(":{} {}", SERVER_NAME, msg),
+            ReplyType::Notice((src, dst, msg)) => {
+                format!(":{}!~anon@darkirc NOTICE {} :{}", src, dst, msg)
+            }
         };
 
         debug!("[{}] <-- {}", self.addr, r);

+ 7 - 1
bin/darkirc/src/irc/command.rs

@@ -670,7 +670,8 @@ impl Client {
             ))])
         }
 
-        // We only send a client reply if the message is for ourself.
+        // We only send a client reply if the message is for ourself or if
+        // we're trying to communicate with IRC services.
         // Anything else is rendered by the IRC client and not supposed
         // to be echoed by the IRC serer.
         if target == nick {
@@ -680,6 +681,11 @@ impl Client {
             ))])
         }
 
+        // Handle queries to NickServ
+        if target.to_lowercase().as_str() == "nickserv" {
+            return self.nickserv.handle_query(message.strip_prefix(':').unwrap()).await
+        }
+
         // If it's a DM and we don't have an encryption key, we will
         // refuse to send it. Send ERR_NORECIPIENT to the client.
         if !target.starts_with('#') && !self.server.contacts.read().await.contains_key(target) {

+ 4 - 0
bin/darkirc/src/irc/mod.rs

@@ -30,6 +30,10 @@ pub(crate) mod server;
 /// IRC command handler
 pub(crate) mod command;
 
+/// Services implementations
+pub(crate) mod services;
+pub(crate) use services::nickserv::NickServ;
+
 /// IRC numerics and server replies
 pub(crate) mod rpl;
 

+ 20 - 0
bin/darkirc/src/irc/services/mod.rs

@@ -0,0 +1,20 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/// NickServ implementation, used for account management
+pub mod nickserv;

+ 90 - 0
bin/darkirc/src/irc/services/nickserv.rs

@@ -0,0 +1,90 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use darkfi::Result;
+use smol::lock::RwLock;
+
+use super::super::{client::ReplyType, rpl::*};
+use crate::IrcServer;
+
+const NICKSERV_USAGE: &str = r#"***** NickServ Help ***** 
+
+NickServ allows a client to perform account management on DarkIRC.
+
+The following commands are available:
+
+  REGISTER      Register an account
+
+For more information on a NickServ command, type:
+/msg NickServ HELP <command>
+
+***** End of Help *****
+"#;
+
+/// NickServ implementation used for IRC account management
+pub struct NickServ {
+    /// Client username
+    pub username: Arc<RwLock<String>>,
+    /// Client nickname
+    pub nickname: Arc<RwLock<String>>,
+    /// Pointer to parent `IrcServer`
+    pub server: Arc<IrcServer>,
+}
+
+impl NickServ {
+    /// Instantiate a new `NickServ` for a client.
+    /// This is called from `Client::new()`
+    pub fn new(
+        username: Arc<RwLock<String>>,
+        nickname: Arc<RwLock<String>>,
+        server: Arc<IrcServer>,
+    ) -> Self {
+        Self { username, nickname, server }
+    }
+
+    /// Handle a `NickServ` query. This is the main command handler.
+    /// Called from `command::handle_cmd_privmsg`.
+    pub async fn handle_query(&self, query: &str) -> Result<Vec<ReplyType>> {
+        let nick = self.nickname.read().await.to_string();
+        let mut tokens = query.split_ascii_whitespace();
+
+        let Some(command) = tokens.next() else {
+            return Ok(vec![ReplyType::Server((
+                ERR_NOTEXTTOSEND,
+                format!("{} :No text to send", nick),
+            ))])
+        };
+
+        match command.to_uppercase().as_str() {
+            "HELP" => self.reply_help(&nick).await,
+            _x => todo!(),
+        }
+    }
+
+    /// Reply to the HELP command
+    pub async fn reply_help(&self, nick: &str) -> Result<Vec<ReplyType>> {
+        let replies = NICKSERV_USAGE
+            .lines()
+            .map(|x| ReplyType::Notice(("NickServ".to_string(), nick.to_string(), x.to_string())))
+            .collect();
+
+        Ok(replies)
+    }
+}