darkirc: Code refactor

This is a big rewrite of the DarkIRC code with an improved and more
robust architecture. We use proper client-server architecture without
having to rely on internal channels.

* The server now performs more correctly (although not fully) per the RFC.

* Removed obsolete code and made lots of performance and readability
  improvements.

* The network now propagates events over the Event Graph (DAG).

* Clients are more robust and stateful and have better separation.
This commit is contained in:
parazyd
2023-09-16 17:27:28 +02:00
parent 3561205b2d
commit 4da846bf03
16 changed files with 2052 additions and 1869 deletions

10
Cargo.lock generated
View File

@@ -1742,14 +1742,14 @@ version = "0.4.1"
dependencies = [
"async-rustls",
"async-trait",
"blake3",
"bs58",
"chrono",
"clap 4.4.1",
"crypto_box",
"darkfi",
"darkfi-serial",
"easy-parallel",
"futures",
"libc",
"log",
"rand 0.8.5",
"rustls-pemfile",
@@ -1757,10 +1757,10 @@ dependencies = [
"signal-hook",
"signal-hook-async-std",
"simplelog",
"sled",
"smol",
"structopt",
"structopt-toml",
"tinyjson",
"toml 0.7.6",
"url",
]
@@ -3320,9 +3320,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "libc"
version = "0.2.147"
version = "0.2.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
[[package]]
name = "libloading"

View File

@@ -138,7 +138,8 @@ prettytable-rs = "0.10.0"
# -----BEGIN LIBRARY FEATURES-----
[features]
async-daemonize = []
async-daemonize = ["system"]
async-serial = ["darkfi-serial/async"]
async-sdk = [

View File

@@ -9,8 +9,12 @@ homepage = "https://dark.fi"
repository = "https://github.com/darkrenaissance/darkfi"
[dependencies]
darkfi = {path = "../../", features = ["bs58", "toml", "async-daemonize", "event-graph", "rpc"]}
darkfi-serial = {path = "../../src/serial"}
darkfi = {path = "../../", features = ["async-daemonize", "event-graph", "net", "util", "system", "rpc"]}
darkfi-serial = {path = "../../src/serial", features = ["async"]}
libc = "0.2.148"
# Event Graph DB
sled = "0.34.7"
# TLS
async-rustls = "0.4.0"
@@ -19,18 +23,16 @@ futures = "0.3.28"
rustls-pemfile = "1.0.3"
# Crypto
blake3 = "1.4.1"
crypto_box = {version = "0.9.1", features = ["std", "chacha20"]}
rand = "0.8.5"
# Misc
clap = {version = "4.4.1", features = ["derive"]}
chrono = "0.4.26"
log = "0.4.20"
url = "2.4.1"
# Encoding and parsing
bs58 = "0.5.0"
tinyjson= "2.5.1"
toml = "0.7.6"
# Daemon

View File

@@ -1,11 +1,4 @@
# darkirc
darkirc
=======
see [Darkfi Book](https://darkrenaissance.github.io/darkfi/misc/darkirc/darkirc.html) for the installation guide.
## Services
To operate with `darkirc` using IRC clients, we can implement special
namespaces which we are then able to query and use that as the
client's interactive communication with the server/daemon:
* `nickserv` - Account management
[Installation Guide](https://darkrenaissance.github.io/darkfi/misc/darkirc/darkirc.html)

View File

@@ -51,6 +51,9 @@ seeds = [
"tcp+tls://lilith1.dark.fi:5262",
]
## Manual peers to connect to
#peers = []
# Whitelisted transports for outbound connections
allowed_transports = ["tcp+tls"]
@@ -106,7 +109,8 @@ topic = "/b/"
## You can generate a keypair with: darkirc --gen-keypair
## and replace the secret key below with the generated one.
## **You should never share this secret key with anyone**
#[secret_key."AKfyoKxnHb8smqP2zt9BVvXkcN7pm9GnqqyuYRmxmWtR"]
#[crypto]
#dm_chacha_secret = "AKfyoKxnHb8smqP2zt9BVvXkcN7pm9GnqqyuYRmxmWtR"
## This is where you put other people's public keys. The format is:
## [contact."nickname"]. "nickname" can be anything you want.
@@ -114,7 +118,7 @@ topic = "/b/"
##
## Example (set as many as you want):
#[contact."satoshi"]
#public_key = "C9vC6HNDfGQofWCapZfQK5MkV1JR8Cct839RDUCqbDGK"
#dm_chacha_public = "C9vC6HNDfGQofWCapZfQK5MkV1JR8Cct839RDUCqbDGK"
#
#[contact."anon"]
#public_key = "7iTddcopP2pkvszFjbFUr7MwTcMSKZkYP6zUan22pxfX"
#dm_chacha_public = "7iTddcopP2pkvszFjbFUr7MwTcMSKZkYP6zUan22pxfX"

View File

@@ -16,168 +16,47 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{collections::HashMap, fmt};
use crypto_box::{
aead::{Aead, AeadCore},
ChaChaBox,
};
use rand::rngs::OsRng;
use crate::{
privmsg::PrivMsgEvent,
settings::{ChannelInfo, ContactInfo, MAXIMUM_LENGTH_OF_NICK_CHAN_CNT},
};
#[derive(serde::Serialize)]
pub struct KeyPair {
pub public: String,
pub secret: String,
}
impl fmt::Display for KeyPair {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Public key: {}\nSecret key: {}", self.public, self.secret)
}
}
/// The format we're using is nonce+ciphertext, where nonce is 24 bytes.
fn try_decrypt(salt_box: &ChaChaBox, ciphertext: &str) -> Option<String> {
let bytes = match bs58::decode(ciphertext).into_vec() {
Ok(v) => v,
Err(_) => return None,
};
if bytes.len() < 25 {
return None
}
// Try extracting the nonce
let nonce = match bytes[0..24].try_into() {
Ok(v) => v,
Err(_) => return None,
};
// Take the remaining ciphertext
let message = &bytes[24..];
// Try decrypting the message
match salt_box.decrypt(nonce, message) {
Ok(v) => Some(String::from_utf8_lossy(&v).to_string()),
Err(_) => None,
}
}
/// The format we're using is nonce+ciphertext, where nonce is 24 bytes.
/// Encrypt given data using the given `ChaChaBox`.
/// Returns base58-encoded string of the ciphertext.
/// Panics if encryption fails.
///
/// The encryption format we're using with `ChaChaBox` is `nonce||ciphertext`,
/// where nonce is 24 bytes large, and the remaining data should be the ciphertext.
pub fn encrypt(salt_box: &ChaChaBox, plaintext: &[u8]) -> String {
// Generate the nonce
let nonce = ChaChaBox::generate_nonce(&mut OsRng);
// Encrypt
let mut ciphertext = salt_box.encrypt(&nonce, plaintext).unwrap();
let mut concat = vec![];
// Concatenate
let mut concat = Vec::with_capacity(24 + ciphertext.len());
concat.append(&mut nonce.as_slice().to_vec());
concat.append(&mut ciphertext);
// Encode
bs58::encode(concat).into_string()
}
pub fn decrypt_target(
contact: &mut String,
privmsg: &mut PrivMsgEvent,
configured_chans: &HashMap<String, ChannelInfo>,
configured_contacts: &HashMap<String, ContactInfo>,
) {
for chan_name in configured_chans.keys() {
let chan_info = configured_chans.get(chan_name).unwrap();
if let Some(salt_box) = &chan_info.salt_box {
let decrypted_target = try_decrypt(salt_box, &privmsg.target);
if decrypted_target.is_none() {
continue
}
let target =
String::from_utf8_lossy(&unpad(decrypted_target.unwrap().into())).to_string();
if *chan_name == target {
privmsg.target = target;
return
}
}
/// Attempt to decrypt given ciphertext using the given `ChaChaBox`.
/// Returns a lossy utf-8 string on success, and `None` on failure.
///
/// The encryption format we're using with `ChaChaBox` is `nonce||ciphertext`,
/// where nonce is 24 bytes large, and the remaining data should be the ciphertext.
pub fn try_decrypt(salt_box: &ChaChaBox, ciphertext: &[u8]) -> Option<String> {
// Make sure we have enough bytes to work with
if ciphertext.len() < 25 {
return None
}
for cnt_name in configured_contacts.keys() {
let cnt_info = configured_contacts.get(cnt_name).unwrap();
if let Some(salt_box) = &cnt_info.salt_box {
let decrypted_target = try_decrypt(salt_box, &privmsg.target);
if decrypted_target.is_none() {
continue
}
let target =
String::from_utf8_lossy(&unpad(decrypted_target.unwrap().into())).to_string();
privmsg.target = target;
*contact = cnt_name.into();
return
}
}
}
/// Decrypt PrivMsg nickname and message
pub fn decrypt_privmsg(salt_box: &ChaChaBox, privmsg: &mut PrivMsgEvent) {
let decrypted_nick = try_decrypt(salt_box, &privmsg.nick);
let decrypted_msg = try_decrypt(salt_box, &privmsg.msg);
if decrypted_nick.is_none() && decrypted_msg.is_none() {
return
}
privmsg.nick = String::from_utf8_lossy(&unpad(decrypted_nick.unwrap().into())).to_string();
privmsg.msg = decrypted_msg.unwrap();
}
/// Encrypt PrivMsg
pub fn encrypt_privmsg(salt_box: &ChaChaBox, privmsg: &mut PrivMsgEvent) {
privmsg.nick = encrypt(salt_box, &pad(privmsg.nick.clone().into()));
privmsg.target = encrypt(salt_box, &pad(privmsg.target.clone().into()));
privmsg.msg = encrypt(salt_box, privmsg.msg.as_bytes());
}
fn pad(data: Vec<u8>) -> Vec<u8> {
if data.len() == MAXIMUM_LENGTH_OF_NICK_CHAN_CNT {
return data
}
assert!(data.len() < MAXIMUM_LENGTH_OF_NICK_CHAN_CNT);
let padding = vec![0u8; MAXIMUM_LENGTH_OF_NICK_CHAN_CNT - data.len()];
let mut data = data;
data.extend_from_slice(&padding);
data
}
fn unpad(data: Vec<u8>) -> Vec<u8> {
assert!(data.len() == MAXIMUM_LENGTH_OF_NICK_CHAN_CNT);
match data.iter().position(|&x| x == 0u8) {
Some(idx) => data[..idx].to_vec(),
None => data,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pad_unpad() {
let nick = String::from("terry-davis");
let padded = pad(nick.clone().into());
assert!(padded.len() == 32);
assert_eq!(nick, String::from_utf8_lossy(&unpad(padded)));
let nick = String::from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
let padded = pad(nick.clone().into());
assert_eq!(nick, String::from_utf8_lossy(&padded));
assert_eq!(nick, String::from_utf8_lossy(&unpad(padded)));
match salt_box.decrypt((&ciphertext[0..24]).into(), &ciphertext[24..]) {
Ok(v) => Some(String::from_utf8_lossy(&v).into()),
Err(_) => None,
}
}

View File

@@ -16,636 +16,321 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{collections::HashSet, sync::Arc};
use std::{
collections::{HashMap, HashSet},
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc,
},
};
use darkfi::{
event_graph::{model::Event, EventMsg},
event_graph2::{proto::EventPut, Event, NULL_ID},
system::Subscription,
Error, Result,
};
use darkfi_serial::{deserialize_async_partial, serialize_async};
use futures::FutureExt;
use log::{debug, error, info, warn};
use log::{debug, error, warn};
use smol::{
io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadHalf, WriteHalf},
io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader},
lock::Mutex,
net::SocketAddr,
prelude::{AsyncRead, AsyncWrite},
};
use crate::{
crypto::saltbox::{decrypt_privmsg, decrypt_target, encrypt_privmsg},
settings,
settings::{Nick, UserMode, RPL},
ChannelInfo, PrivMsgEvent,
};
use super::{server::IrcServer, Privmsg, SERVER_NAME};
use super::{ClientSubMsg, IrcConfig, NotifierMsg};
const PENALTY_LIMIT: usize = 5;
pub struct IrcClient<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> {
// network stream
write_stream: WriteHalf<C>,
read_stream: BufReader<ReadHalf<C>>,
pub address: SocketAddr,
// irc config
irc_config: IrcConfig,
/// Joined channels, mapped here for better SIGHUP UX.
channels_joined: HashSet<String>,
server_notifier: smol::channel::Sender<(NotifierMsg, usize)>,
subscription: Subscription<ClientSubMsg>,
missed_events: Arc<Mutex<Vec<Event<PrivMsgEvent>>>>,
/// Reply types, we can either send server replies, or client replies.
pub enum ReplyType {
/// Server reply, we have to use numerics
Server((u16, String)),
/// Client reply, message from someone to some{one,where}
Client((String, String)),
/// Pong reply, we just use server origin
Pong(String),
/// CAP reply
Cap(String),
}
impl<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> IrcClient<C> {
pub fn new(
write_stream: WriteHalf<C>,
read_stream: BufReader<ReadHalf<C>>,
address: SocketAddr,
irc_config: IrcConfig,
server_notifier: smol::channel::Sender<(NotifierMsg, usize)>,
subscription: Subscription<ClientSubMsg>,
missed_events: Arc<Mutex<Vec<Event<PrivMsgEvent>>>>,
) -> Self {
Self {
write_stream,
read_stream,
address,
irc_config,
channels_joined: HashSet::new(),
subscription,
server_notifier,
missed_events,
}
/// Stateful IRC client, used for each client connection
pub struct Client {
/// Pointer to parent `IrcServer`
pub server: Arc<IrcServer>,
/// Subscription for incoming events
pub incoming: Subscription<Event>,
/// Client socket addr
pub addr: SocketAddr,
/// ID of the last sent event
pub last_sent: Mutex<blake3::Hash>,
/// Active (joined) channels for this client
pub channels: Mutex<HashSet<String>>,
/// Penalty counter, when limit is reached, disconnect client
pub penalty: AtomicUsize,
/// Registration marker
pub registered: AtomicBool,
/// Registration pause marker
pub reg_paused: AtomicBool,
/// Client username
pub username: Mutex<String>,
/// Client nickname
pub nickname: Mutex<String>,
/// Client realname
pub realname: Mutex<String>,
/// Client caps
pub caps: Mutex<HashMap<String, bool>>,
}
impl Client {
/// Instantiate a new Client.
pub async fn new(
server: Arc<IrcServer>,
incoming: Subscription<Event>,
addr: SocketAddr,
) -> Result<Self> {
let caps = HashMap::from([("no-history".to_string(), false)]);
Ok(Self {
server,
incoming,
addr,
last_sent: Mutex::new(NULL_ID),
channels: Mutex::new(HashSet::new()),
penalty: AtomicUsize::new(0),
registered: AtomicBool::new(false),
reg_paused: AtomicBool::new(false),
username: Mutex::new(String::from("*")),
nickname: Mutex::new(String::from("*")),
realname: Mutex::new(String::from("*")),
caps: Mutex::new(caps),
})
}
/// Start listening for messages came from View or irc client
pub async fn listen(&mut self) {
loop {
let mut line = String::new();
/// This function handles a single IRC client. We listen to messages from the
/// IRC client and relay them to the network, and we also get notified of
/// incoming messages and relay them to the IRC client. The notifications come
/// from events being inserted into the Event Graph.
pub async fn multiplex_connection<S>(&self, stream: S) -> Result<()>
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let (reader, mut writer) = io::split(stream);
let mut reader = BufReader::new(reader);
// Our buffer for the client line
let mut line = String::new();
loop {
futures::select! {
// Process msg from View or other client connnected to the same irc server
msg = self.subscription.receive().fuse() => {
match msg {
ClientSubMsg::Privmsg(m) => {
if let Err(e) = self.process_msg(&m).await {
error!("[CLIENT {}] Process msg: {}", self.address, e);
break
// Process message from the IRC client
r = reader.read_line(&mut line).fuse() => {
// If something failed during reading, we disconnect.
if let Err(e) = r {
error!("[IRC CLIENT] Read failed for {}: {}", self.addr, e);
self.incoming.unsubscribe().await;
return Err(Error::ChannelStopped)
}
// If the penalty limit is reached, disconnect the client.
if self.penalty.load(SeqCst) == PENALTY_LIMIT {
self.incoming.unsubscribe().await;
return Err(Error::ChannelStopped)
}
// We'll be strict here and disconnect the client
// in case line processing failed in any way.
match self.process_client_line(&line, &mut writer).await {
// If we got an event back, we should broadcast it.
// This means we add it to our DAG, and the DAG will
// handle the rest of the propagation.
Ok(Some(event)) => {
// Update the last sent event.
*self.last_sent.lock().await = event.id();
// If it fails for some reason, for now, we just note it
// and pass.
if let Err(e) = self.server.darkirc.event_graph.dag_insert(event.clone()).await {
error!("[IRC CLIENT] Failed inserting new event to DAG: {}", e);
} else {
// Otherwise, broadcast it
self.server.darkirc.p2p.broadcast(&EventPut(event)).await;
}
}
ClientSubMsg::Config(c) => self.update_config(c).await,
// If we got nothing, we just pass.
Ok(None) => {}
// If we got an error, we disconnect the client.
Err(e) => {
self.incoming.unsubscribe().await;
return Err(e)
}
}
// Clear the line buffer
line = String::new();
continue
}
// Process msg from IRC client
err = self.read_stream.read_line(&mut line).fuse() => {
if let Err(e) = err {
error!("[CLIENT {}] Read line error: {}", self.address, e);
break
// Process message from the network. These should only be PRIVMSG.
r = self.incoming.receive().fuse() => {
// We will skip this if it's our own message.
if *self.last_sent.lock().await == r.id() {
continue
}
if let Err(e) = self.process_line(line).await {
error!("[CLIENT {}] Process line failed: {}", self.address, e);
break
// Try to deserialize the `Event`'s content into a `Privmsg`
let mut privmsg: Privmsg = match deserialize_async_partial(r.content()).await {
Ok((v, _)) => v,
Err(e) => {
error!("[IRC CLIENT] Failed deserializing incoming Privmsg event: {}", e);
continue
}
};
// If successful, potentially decrypt it:
self.server.try_decrypt(&mut privmsg).await;
// If we have this channel, or it's a DM to our nickname, forward
// it to the client.
let have_channel = self.channels.lock().await.contains(&privmsg.channel);
let msg_for_self = *self.nickname.lock().await == privmsg.channel;
if have_channel || msg_for_self {
// Add the nickname to the list of nicks on the channel
(*self.server.channels.lock().await).get_mut(&privmsg.channel)
.unwrap().nicks.insert(privmsg.nick.clone());
// Format the message
let msg = format!("PRIVMSG {} :{}", privmsg.channel, privmsg.msg);
// Send it to the client
let reply = ReplyType::Client((privmsg.nick, msg));
if let Err(e) = self.reply(&mut writer, &reply).await {
error!("[IRC CLIENT] Failed writing PRIVMSG to client: {}", e);
continue
}
}
}
}
}
warn!("[CLIENT {}] Close connection", self.address);
self.subscription.unsubscribe().await;
}
pub async fn update_config(&mut self, new_config: IrcConfig) {
info!("[CLIENT {}] Updating config...", self.address);
/// Send a reply to the IRC client. Matches on the reply type.
async fn reply<W>(&self, writer: &mut W, reply: &ReplyType) -> Result<()>
where
W: AsyncWrite + Unpin,
{
let r = match reply {
ReplyType::Server((rpl, msg)) => format!(":{} {:03} {}", SERVER_NAME, rpl, msg),
ReplyType::Client((nick, msg)) => format!(":{}!~anon@darkirc {}", nick, msg),
ReplyType::Pong(origin) => format!(":{} PONG :{}", SERVER_NAME, origin),
ReplyType::Cap(msg) => format!(":{} {}", SERVER_NAME, msg),
};
let old_config = self.irc_config.clone();
let mut _chans_to_replay = HashSet::new();
let mut _contacts_to_replay = HashSet::new();
debug!("[{}] <-- {}", self.addr, r);
for (name, new_info) in new_config.channels.iter() {
let Some(old_info) = old_config.channels.get(name) else {
// New channel wasn't in old config, replay it
_chans_to_replay.insert(name);
continue
};
// TODO: Maybe if the salt_box changed, replay it, although
// kinda hard to do since there's no Eq/PartialEq on there,
// and we probably shouldn't keep the secret key laying around.
// We got some secret key for this channel, replay it
if old_info.salt_box.is_none() && new_info.salt_box.is_some() {
_chans_to_replay.insert(name);
continue
}
}
for (name, _new_info) in new_config.contacts.iter() {
let Some(_old_info) = old_config.contacts.get(name) else {
// New contact wasn't in old config, replay it
_contacts_to_replay.insert(name);
continue
};
}
self.irc_config.channels.extend(new_config.channels);
self.irc_config.contacts.extend(new_config.contacts);
self.irc_config.pass = new_config.pass;
if self.on_receive_join(self.irc_config.channels.keys().cloned().collect()).await.is_err() {
warn!("Error joining updated channels");
} else {
info!("[CLIENT {}] Config updated", self.address);
}
}
pub async fn process_msg(&mut self, msg: &PrivMsgEvent) -> Result<()> {
debug!("[CLIENT {}] msg from View: {:?}", self.address, msg.to_string());
let mut msg = msg.clone();
let mut contact = String::new();
decrypt_target(
&mut contact,
&mut msg,
&self.irc_config.channels,
&self.irc_config.contacts,
);
// The message is a channel message
if msg.target.starts_with('#') {
// Try to potentially decrypt the incoming message.
if !self.irc_config.channels.contains_key(&msg.target) {
return Ok(())
}
// Skip everything if we're not joined in the channel
if !self.channels_joined.contains(&msg.target) {
return Ok(())
}
let chan_info = self.irc_config.channels.get_mut(&msg.target).unwrap();
// We use this flag to mark if the message has been encrypted or not.
// Depending on it, we set a specific usermode to the nickname in the
// channel so in UI we can tell who is sending encrypted messages.
let mut encrypted = false;
if let Some(salt_box) = &chan_info.salt_box {
decrypt_privmsg(salt_box, &mut msg);
encrypted = true;
debug!("[P2P] Decrypted received message: {:?}", msg);
}
// Add the nickname to the channel's names
let mut nick: Nick = msg.nick.clone().into();
let _mode_change = if chan_info.names.contains(&nick) {
let mut n = chan_info.names.get(&nick).unwrap().clone();
let mode_change = if encrypted {
n.set_mode(UserMode::Voice)
} else {
n.unset_mode(UserMode::Voice)
};
chan_info.names.insert(n);
mode_change
} else {
let mode_change = if encrypted { nick.set_mode(UserMode::Voice) } else { None };
chan_info.names.insert(nick);
mode_change
};
self.reply(&msg.to_string()).await?;
return Ok(())
}
// The message is not a channel message, handle accordingly.
if self.irc_config.is_cap_end && self.irc_config.is_nick_init {
if !self.irc_config.contacts.contains_key(&contact) {
return Ok(())
}
let contact_info = self.irc_config.contacts.get(&contact).unwrap();
if let Some(salt_box) = &contact_info.salt_box {
decrypt_privmsg(salt_box, &mut msg);
// This is for /query
msg.nick = contact;
debug!("[P2P] Decrypted received message: {:?}", msg);
}
self.reply(&msg.to_string()).await?;
return Ok(())
}
writer.write(r.as_bytes()).await?;
writer.write(b"\r\n").await?;
writer.flush().await?;
Ok(())
}
pub async fn process_line(&mut self, line: String) -> Result<()> {
let irc_msg = match clean_input_line(line) {
Ok(msg) => msg,
Err(e) => {
warn!("[CLIENT {}] Connection error: {}", self.address, e);
return Err(Error::ChannelStopped)
/// Handle the incoming line given sent by the IRC client
async fn process_client_line<W>(&self, line: &str, writer: &mut W) -> Result<Option<Event>>
where
W: AsyncWrite + Unpin,
{
if line.is_empty() || line == "\n" || line == "\r\n" {
return Err(Error::ParseFailed("Line is empty"))
}
let mut line = line.to_string();
// Remove CRLF
if &line[(line.len() - 2)..] == "\r\n" {
line.pop();
line.pop();
} else if &line[(line.len() - 1)..] == "\n" {
line.pop();
} else {
return Err(Error::ParseFailed("Line doesn't end with CR/LF"))
}
// Parse the line
let mut tokens = line.split_ascii_whitespace();
// Commands can begin with :garbage, but we will reject clients
// doing that for now to keep the protocol simple and focused.
let cmd = tokens.next().ok_or(Error::ParseFailed("Invalid command line"))?;
let args = line.replacen(cmd, "", 1);
let cmd = cmd.to_uppercase();
debug!("[{}] --> {}{}", self.addr, cmd, args);
// Handle the command. These implementations are in `command.rs`.
let replies: Vec<ReplyType> = match cmd.as_str() {
"ADMIN" => self.handle_cmd_admin(&args).await?,
"CAP" => self.handle_cmd_cap(&args).await?,
"INFO" => self.handle_cmd_info(&args).await?,
"JOIN" => self.handle_cmd_join(&args).await?,
"LIST" => self.handle_cmd_list(&args).await?,
"MODE" => self.handle_cmd_mode(&args).await?,
"MOTD" => self.handle_cmd_motd(&args).await?,
"NAMES" => self.handle_cmd_names(&args).await?,
"NICK" => self.handle_cmd_nick(&args).await?,
"PART" => self.handle_cmd_part(&args).await?,
"PING" => self.handle_cmd_ping(&args).await?,
"PRIVMSG" => self.handle_cmd_privmsg(&args).await?,
"REHASH" => self.handle_cmd_rehash(&args).await?,
"TOPIC" => self.handle_cmd_topic(&args).await?,
"USER" => self.handle_cmd_user(&args).await?,
"VERSION" => self.handle_cmd_version(&args).await?,
"QUIT" => return Err(Error::ChannelStopped),
_ => {
warn!("[IRC CLIENT] Unimplemented \"{}\" command", cmd);
vec![]
}
};
debug!("[CLIENT {}] Process msg: {}", self.address, irc_msg);
if let Err(e) = self.update(irc_msg).await {
warn!("[CLIENT {}] Connection error: {}", self.address, e);
return Err(Error::ChannelStopped)
}
Ok(())
}
async fn update(&mut self, line: String) -> Result<()> {
if line.len() > settings::MAXIMUM_LENGTH_OF_MESSAGE {
return Err(Error::MalformedPacket)
// Depending on the reply type, we send according messages.
for reply in replies.iter() {
self.reply(writer, reply).await?;
}
let (command, value) = parse_line(&line)?;
let (command, value) = (command.as_str(), value.as_str());
// If the command was a PRIVMSG the client sent, we need to encrypt it and
// create an Event to broadcast and return it from this function. So let's try.
// We also do not allow sending unencrypted DMs. In that case, we send a notice
// to the client to inform them that the feature is not enabled.
match command {
"PASS" => self.on_receive_pass(value).await?,
"USER" => self.on_receive_user().await?,
"NAMES" => self.on_receive_names(value.split(',').map(String::from).collect()).await?,
"NICK" => self.on_receive_nick(value).await?,
"JOIN" => self.on_receive_join(value.split(',').map(String::from).collect()).await?,
"PART" => self.on_receive_part(value.split(',').map(String::from).collect()).await?,
"TOPIC" => self.on_receive_topic(&line, value).await?,
"PING" => self.on_ping(value).await?,
"PRIVMSG" => self.on_receive_privmsg(&line, value).await?,
"CAP" => self.on_receive_cap(&line, &value.to_uppercase()).await?,
"QUIT" => self.on_quit()?,
_ => warn!("[CLIENT {}] Unimplemented `{}` command", self.address, command),
}
self.register().await?;
Ok(())
}
async fn register(&mut self) -> Result<()> {
if !self.irc_config.is_pass_init && self.irc_config.pass.is_empty() {
self.irc_config.is_pass_init = true
}
if !self.irc_config.is_registered &&
self.irc_config.is_cap_end &&
self.irc_config.is_nick_init &&
self.irc_config.is_user_init
{
debug!("Initializing peer connection");
let register_reply =
format!(":darkfi 001 {} :Let there be dark\r\n", self.irc_config.nick);
self.reply(&register_reply).await?;
self.irc_config.is_registered = true;
// join all channels
self.on_receive_join(self.irc_config.auto_channels.clone()).await?;
self.on_receive_join(self.irc_config.channels.keys().cloned().collect()).await?;
}
Ok(())
}
async fn reply(&mut self, message: &str) -> Result<()> {
self.write_stream.write_all(message.as_bytes()).await?;
debug!("Sent {}", message.trim_end());
Ok(())
}
fn on_quit(&self) -> Result<()> {
// Close the connection
Err(Error::NetworkServiceStopped)
}
async fn on_receive_user(&mut self) -> Result<()> {
// We can stuff any extra things like public keys in here.
// Ignore it for now.
if self.irc_config.is_pass_init {
self.irc_config.is_user_init = true;
} else {
// Close the connection
warn!("[CLIENT {}] Password is required", self.address);
return self.on_quit()
}
Ok(())
}
async fn on_receive_pass(&mut self, pass: &str) -> Result<()> {
if self.irc_config.pass == pass {
self.irc_config.is_pass_init = true
} else {
// Close the connection
warn!("[CLIENT {}] Password is not correct!", self.address);
return self.on_quit()
}
Ok(())
}
async fn on_receive_nick(&mut self, nickname: &str) -> Result<()> {
if nickname.len() >= settings::MAXIMUM_LENGTH_OF_NICK_CHAN_CNT {
return Ok(())
}
self.irc_config.is_nick_init = true;
let old_nick = std::mem::replace(&mut self.irc_config.nick, nickname.to_string());
let nick_reply = format!(":{}!anon@dark.fi NICK {}\r\n", old_nick, self.irc_config.nick);
self.reply(&nick_reply).await
}
async fn on_receive_part(&mut self, channels: Vec<String>) -> Result<()> {
for chan in channels.iter() {
let part_reply = format!(":{}!anon@dark.fi PART {}\r\n", self.irc_config.nick, chan);
self.reply(&part_reply).await?;
self.channels_joined.remove(chan);
}
Ok(())
}
async fn on_receive_topic(&mut self, line: &str, channel: &str) -> Result<()> {
if let Some(substr_idx) = line.find(':') {
// Client is setting the topic
if substr_idx >= line.len() {
return Err(Error::MalformedPacket)
}
let topic = &line[substr_idx + 1..];
let chan_info = self.irc_config.channels.get_mut(channel).unwrap();
chan_info.topic = Some(topic.to_string());
let topic_reply =
format!(":{}!anon@dark.fi TOPIC {} :{}\r\n", self.irc_config.nick, channel, topic);
self.reply(&topic_reply).await?;
} else {
// Client is asking or the topic
let chan_info = self.irc_config.channels.get(channel).unwrap();
let topic_reply = if let Some(topic) = &chan_info.topic {
format!("{} {} {} :{}\r\n", RPL::Topic as u32, self.irc_config.nick, channel, topic)
} else {
const TOPIC: &str = "No topic is set";
format!(
"{} {} {} :{}\r\n",
RPL::NoTopic as u32,
self.irc_config.nick,
channel,
TOPIC
)
// NOTE: This is not the most performant way to do this, probably not even
// TODO: the best place to do it. Patches welcome. It's also a bit fragile
// since we assume that `handle_cmd_privmsg()` won't return any replies.
if cmd.as_str() == "PRIVMSG" && replies.is_empty() {
let channel = args.split_ascii_whitespace().next().unwrap().to_string();
let msg_offset = args.find(':').unwrap() + 1;
let (_, msg) = args.split_at(msg_offset);
let mut privmsg = Privmsg {
channel,
nick: self.nickname.lock().await.to_string(),
msg: msg.to_string(),
};
self.reply(&topic_reply).await?;
}
Ok(())
}
async fn on_ping(&mut self, value: &str) -> Result<()> {
let pong = format!("PONG {}\r\n", value);
self.reply(&pong).await
}
// Encrypt the Privmsg if an encryption method is available.
self.server.try_encrypt(&mut privmsg).await;
async fn on_receive_cap(&mut self, line: &str, subcommand: &str) -> Result<()> {
self.irc_config.is_cap_end = false;
// Build a DAG event and return it.
let event = Event::new(
serialize_async(&privmsg).await,
self.server.darkirc.event_graph.clone(),
)
.await;
let caps_keys: Vec<String> = self.irc_config.caps.keys().cloned().collect();
match subcommand {
"LS" => {
let cap_ls_reply = format!(
":{}!anon@dark.fi CAP * LS :{}\r\n",
self.irc_config.nick,
caps_keys.join(" ")
);
self.reply(&cap_ls_reply).await?;
}
"REQ" => {
let substr_idx = line.find(':').ok_or(Error::MalformedPacket)?;
if substr_idx >= line.len() {
return Err(Error::MalformedPacket)
}
let cap: Vec<&str> = line[substr_idx + 1..].split(' ').collect();
let mut ack_list = vec![];
let mut nak_list = vec![];
for c in cap {
if self.irc_config.caps.contains_key(c) {
self.irc_config.caps.insert(c.to_string(), true);
ack_list.push(c);
} else {
nak_list.push(c);
}
}
let cap_ack_reply = format!(
":{}!anon@dark.fi CAP * ACK :{}\r\n",
self.irc_config.nick,
ack_list.join(" ")
);
let cap_nak_reply = format!(
":{}!anon@dark.fi CAP * NAK :{}\r\n",
self.irc_config.nick,
nak_list.join(" ")
);
self.reply(&cap_ack_reply).await?;
self.reply(&cap_nak_reply).await?;
}
"LIST" => {
let enabled_caps: Vec<String> = self
.irc_config
.caps
.clone()
.into_iter()
.filter(|(_, v)| *v)
.map(|(k, _)| k)
.collect();
let cap_list_reply = format!(
":{}!anon@dark.fi CAP * LIST :{}\r\n",
self.irc_config.nick,
enabled_caps.join(" ")
);
self.reply(&cap_list_reply).await?;
}
"END" => {
self.irc_config.is_cap_end = true;
}
_ => {}
}
Ok(())
}
async fn on_receive_names(&mut self, channels: Vec<String>) -> Result<()> {
for chan in channels.iter() {
if !chan.starts_with('#') {
continue
}
if self.irc_config.channels.contains_key(chan) {
let chan_info = self.irc_config.channels.get(chan).unwrap();
if chan_info.names.is_empty() {
return Ok(())
}
let names_reply = format!(
":{}!anon@dark.fi {} = {} : {}\r\n",
self.irc_config.nick,
RPL::NameReply as u32,
chan,
chan_info.names()
);
self.reply(&names_reply).await?;
let end_of_names = format!(
":DarkFi {:03} {} {} :End of NAMES list\r\n",
RPL::EndOfNames as u32,
self.irc_config.nick,
chan
);
self.reply(&end_of_names).await?;
}
}
Ok(())
}
async fn on_receive_privmsg(&mut self, line: &str, target: &str) -> Result<()> {
let substr_idx = line.find(':').ok_or(Error::MalformedPacket)?;
if substr_idx >= line.len() {
return Err(Error::MalformedPacket)
return Ok(Some(event))
}
let message = line[substr_idx + 1..].to_string();
debug!("[CLIENT {}] (Plain) PRIVMSG {} :{}", self.address, target, message,);
let mut privmsg = PrivMsgEvent::new();
privmsg.nick = self.irc_config.nick.clone();
privmsg.target = target.to_string();
privmsg.msg = message.clone();
if target.starts_with('#') {
if !self.irc_config.channels.contains_key(target) {
return Ok(())
}
if !self.channels_joined.contains(target) {
return Ok(())
}
let channel_info = self.irc_config.channels.get(target).unwrap();
if let Some(salt_box) = &channel_info.salt_box {
encrypt_privmsg(salt_box, &mut privmsg);
debug!("[CLIENT {}] (Encrypted) PRIVMSG: {:?}", self.address, privmsg);
}
} else {
if !self.irc_config.contacts.contains_key(target) {
return Ok(())
}
let contact_info = self.irc_config.contacts.get(target).unwrap();
if let Some(salt_box) = &contact_info.salt_box {
encrypt_privmsg(salt_box, &mut privmsg);
debug!("[CLIENT {}] (Encrypted) PRIVMSG: {:?}", self.address, privmsg);
}
}
self.server_notifier
.send((NotifierMsg::Privmsg(privmsg), self.subscription.get_id()))
.await?;
Ok(())
}
async fn on_receive_join(&mut self, channels: Vec<String>) -> Result<()> {
for chan in channels.iter() {
if !chan.starts_with('#') {
continue
}
if !self.irc_config.channels.contains_key(chan) {
let mut chan_info = ChannelInfo::new()?;
chan_info.topic = Some("n/a".to_string());
self.irc_config.channels.insert(chan.to_string(), chan_info);
}
if !self.channels_joined.insert(chan.to_string()) {
return Ok(())
}
let chan_info = self.irc_config.channels.get_mut(chan).unwrap();
let topic =
if let Some(topic) = chan_info.topic.clone() { topic } else { "n/a".to_string() };
chan_info.topic = Some(topic.to_string());
{
let j = format!(":{}!anon@dark.fi JOIN {}\r\n", self.irc_config.nick, chan);
let t = format!(":DarkFi TOPIC {} :{}\r\n", chan, topic);
self.reply(&j).await?;
self.reply(&t).await?;
}
}
if *self.irc_config.caps.get("no-history").unwrap() {
return Ok(())
}
// Process missed messages if any (sorted by event's timestamp)
let mut hash_vec = self.missed_events.lock().await.clone();
hash_vec.sort_by(|a, b| a.timestamp.0.cmp(&b.timestamp.0));
for event in hash_vec {
if let Err(e) = self.process_msg(&event.action).await {
error!("[CLIENT {}] Process msg: {}", self.address, e);
continue
}
}
Ok(())
Ok(None)
}
}
//
// Helper functions
//
fn clean_input_line(mut line: String) -> Result<String> {
if line.is_empty() {
return Err(Error::ChannelStopped)
}
if line == "\n" || line == "\r\n" {
return Err(Error::ChannelStopped)
}
if &line[(line.len() - 2)..] == "\r\n" {
// Remove CRLF
line.pop();
line.pop();
} else if &line[(line.len() - 1)..] == "\n" {
line.pop();
} else {
return Err(Error::ChannelStopped)
}
Ok(line.clone())
}
fn parse_line(line: &str) -> Result<(String, String)> {
let mut tokens = line.split_ascii_whitespace();
// Commands can begin with :garbage but we will reject clients doing
// that for now to keep the protocol simple and focused.
let command = tokens.next().ok_or(Error::MalformedPacket)?.to_uppercase();
let value = tokens.next().ok_or(Error::MalformedPacket)?;
Ok((command, value.to_owned()))
}

View File

@@ -0,0 +1,881 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2023 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/>.
*/
//! IRC command implemenatations
//!
//! These try to follow the RFCs, modified in order for our P2P stack.
//! Copied from https://simple.wikipedia.org/wiki/List_of_Internet_Relay_Chat_commands
//!
//! Unimplemented commands:
//! * `AWAY`
//! * `CONNECT`
//! * `DIE`
//! * `ERROR`
//! * `INVITE`
//! * `ISON`
//! * `KICK`
//! * `KILL`
//! * `NOTICE`
//! * `OPER`
//! * `PASS`
//! * `RESTART`
//! * `SERVICE`
//! * `SERVLIST`
//! * `SERVER`
//! * `SQUERY`
//! * `SQUIT`
//! * `SUMMON`
//! * `TRACE`
//! * `USERHOST`
//! * `WALLOPS`
//! * `WHO`
//! * `WHOIS`
//! * `WHOWAS`
//!
//! Some of the above commands could actually be implemented and could
//! work in respect to the P2P network.
use std::{collections::HashSet, sync::atomic::Ordering::SeqCst};
use darkfi::Result;
use log::{error, info};
use super::{
client::{Client, ReplyType},
rpl::*,
IrcChannel, SERVER_NAME,
};
impl Client {
/// `ADMIN [<server>]`
///
/// Asks the server for information about the administrator of the server.
pub async fn handle_cmd_admin(&self, _args: &str) -> Result<Vec<ReplyType>> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.lock().await.to_string();
let replies = vec![
ReplyType::Server((
RPL_ADMINME,
format!("{} {} :Administrative info", nick, SERVER_NAME),
)),
ReplyType::Server((RPL_ADMINLOC1, format!("{} :", nick))),
ReplyType::Server((RPL_ADMINLOC2, format!("{} :", nick))),
ReplyType::Server((RPL_ADMINEMAIL, format!("{} :anon@darkirc", nick))),
];
Ok(replies)
}
/// `CAP <args>`
pub async fn handle_cmd_cap(&self, args: &str) -> Result<Vec<ReplyType>> {
let mut tokens = args.split_ascii_whitespace();
let Some(subcommand) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} CAP :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
};
let caps_keys: Vec<String> = self.caps.lock().await.keys().cloned().collect();
let nick = self.nickname.lock().await.to_string();
match subcommand.to_uppercase().as_str() {
"LS" => {
/*
let Some(_version) = tokens.next() else {
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} CAP :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
};
*/
self.reg_paused.store(true, SeqCst);
return Ok(vec![ReplyType::Cap(format!("CAP * LS :{}", caps_keys.join(" ")))])
}
"REQ" => {
let Some(substr_idx) = args.find(':') else {
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} CAP :{}", nick, INVALID_SYNTAX),
))])
};
if substr_idx >= args.len() {
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} CAP :{}", nick, INVALID_SYNTAX),
))])
}
let cap_reqs: Vec<&str> = args[substr_idx + 1..].split(' ').collect();
let mut ack_list = vec![];
let mut nak_list = vec![];
let mut available_caps = self.caps.lock().await;
for cap in cap_reqs {
if available_caps.contains_key(cap) {
available_caps.insert(cap.to_string(), true);
ack_list.push(cap);
} else {
nak_list.push(cap);
}
}
let mut replies = vec![];
if !ack_list.is_empty() {
replies.push(ReplyType::Cap(format!(
"CAP {} ACK :{}",
nick,
ack_list.join(" ")
)));
}
if !nak_list.is_empty() {
replies.push(ReplyType::Cap(format!(
"CAP {} NAK :{}",
nick,
nak_list.join(" ")
)));
}
return Ok(replies)
}
"LIST" => {
let enabled_caps: Vec<String> = self
.caps
.lock()
.await
.clone()
.into_iter()
.filter(|(_, v)| *v)
.map(|(k, _)| k)
.collect();
return Ok(vec![ReplyType::Cap(format!(
"CAP {} LIST :{}",
nick,
enabled_caps.join(" ")
))])
}
"END" => {
// At CAP END, if we have USER and NICK, we can welcome them.
self.reg_paused.store(false, SeqCst);
if self.registered.load(SeqCst) {
return Ok(self.welcome().await)
}
return Ok(vec![])
}
_ => {}
}
self.penalty.fetch_add(1, SeqCst);
Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} CAP :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
}
/// `INFO [<target>]`
///
/// Gives information about the `<target>` server, or the current server if
/// `<target>` is not used. The information includes the server's version,
/// when it was compiled, the patch level, when it was started, and any
/// other information which might be relevant.
pub async fn handle_cmd_info(&self, _args: &str) -> Result<Vec<ReplyType>> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.lock().await.clone();
let replies = vec![
ReplyType::Server((
RPL_INFO,
format!("{} :DarkIRC {}", nick, env!("CARGO_PKG_VERSION")),
)),
ReplyType::Server((RPL_ENDOFINFO, format!("{} :End of INFO list", nick))),
];
Ok(replies)
}
/// `JOIN <channels> [<keys>]`
///
/// Makes the client join the channels in the list `<channels>`.
/// Passwords can be used in the list `<keys>`. If the channels do not
/// exist, they will be created.
pub async fn handle_cmd_join(&self, args: &str) -> Result<Vec<ReplyType>> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
// Client's (already) active channels
let mut active_channels = self.channels.lock().await;
// Here we'll hold valid channel names.
let mut channels = HashSet::new();
// Let's scan through our channels. For now we'll only support
// channel names starting with a single '#' character.
let tokens = args.split_ascii_whitespace();
for channel in tokens {
if !channel.starts_with('#') {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} JOIN :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
}
if !active_channels.contains(channel) {
channels.insert(channel.to_string());
}
}
// We need at least one channel.
if channels.is_empty() {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} JOIN :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
}
// Create new channels for this client and construct replies.
let mut server_channels = self.server.channels.lock().await;
let mut replies = vec![];
let nick = self.nickname.lock().await.to_string();
for channel in channels {
// Insert the channel name into the set of client's active channels
active_channels.insert(channel.clone());
// Create or update the channel on the server side.
if let Some(server_chan) = server_channels.get_mut(&channel) {
server_chan.nicks.insert(nick.clone());
} else {
let chan = IrcChannel {
topic: String::new(),
nicks: HashSet::from([nick.clone()]),
saltbox: None,
};
server_channels.insert(channel.clone(), chan);
}
// Create the replies
replies.push(ReplyType::Client((nick.clone(), format!("JOIN :{}", channel))));
replies.push(ReplyType::Server((
RPL_NAMREPLY,
format!("{} = {} :{}", nick, channel, nick),
)));
replies.push(ReplyType::Server((
RPL_ENDOFNAMES,
format!("{} {} :End of NAMES list", nick, channel),
)));
if let Some(chan) = server_channels.get(&channel) {
if !chan.topic.is_empty() {
replies.push(ReplyType::Client((
nick.clone(),
format!("TOPIC {} :{}", channel, chan.topic),
)));
}
}
}
Ok(replies)
}
/// `LIST [<channels> [<server>]]`
///
/// List all channels on the server. If the list `<channels>` is given, it
/// will return the channel topics. If <server> is given, the command will
/// be sent to `<server>` for evaluation.
pub async fn handle_cmd_list(&self, _args: &str) -> Result<Vec<ReplyType>> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.lock().await.to_string();
let mut list = vec![];
for (name, channel) in self.server.channels.lock().await.iter() {
list.push(format!("{} {} {} :{}", nick, name, channel.nicks.len(), channel.topic));
}
let mut replies = vec![];
replies.push(ReplyType::Server((RPL_LISTSTART, format!("{} Channel :Users Name", nick))));
for chan in list {
replies.push(ReplyType::Server((RPL_LIST, chan)));
}
replies.push(ReplyType::Server((RPL_LISTEND, format!("{} :End of /LIST", nick))));
Ok(replies)
}
/// `MODE <nickname> <flags>`
/// `MODE <channel> <flags>`
///
/// The MODE command has two uses. It can be used to set both user and
/// channel modes.
pub async fn handle_cmd_mode(&self, args: &str) -> Result<Vec<ReplyType>> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let mut tokens = args.split_ascii_whitespace();
let Some(target) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} MODE :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
};
let nick = self.nickname.lock().await.to_string();
if target == nick {
return Ok(vec![ReplyType::Server((RPL_UMODEIS, format!("{} +", nick)))])
}
if !target.starts_with('#') {
return Ok(vec![ReplyType::Server((
ERR_USERSDONTMATCH,
format!("{} :Can't set/get mode for other users", nick),
))])
}
if !self.server.channels.lock().await.contains_key(target) {
return Ok(vec![ReplyType::Server((
ERR_NOSUCHNICK,
format!("{} {} :No such nick or channel name", nick, target),
))])
}
Ok(vec![ReplyType::Server((RPL_CHANNELMODEIS, format!("{} {} +", nick, target)))])
}
/// `MOTD [<server>]`
///
/// Returns the message of the day on <server> or the current server if
/// it is not stated.
pub async fn handle_cmd_motd(&self, _args: &str) -> Result<Vec<ReplyType>> {
let nick = self.nickname.read().await.to_string();
Ok(vec![
ReplyType::Server((
RPL_MOTDSTART,
format!("{} :- {} message of the day", nick, SERVER_NAME),
)),
ReplyType::Server((RPL_MOTD, format!("{} :Let there be dark!", nick))),
ReplyType::Server((RPL_ENDOFMOTD, format!("{} :End of /MOTD command.", nick))),
])
}
/// `NAMES [<channel>]`
///
/// Returns a list of who is on the list of <channel>, by channel name.
/// If <channel> is not used, all users are shown. They are grouped by
/// channel name with all users who are not on a channel being shown as
/// part of channel "*".
pub async fn handle_cmd_names(&self, args: &str) -> Result<Vec<ReplyType>> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.lock().await.to_string();
let mut tokens = args.split_ascii_whitespace();
let mut replies = vec![];
// If a channel was requested, reply only with that one.
// Otherwise, return info for all known channels.
if let Some(req_chan) = tokens.next() {
if let Some(chan) = self.server.channels.lock().await.get(req_chan) {
let nicks: Vec<String> = chan.nicks.iter().cloned().collect();
replies.push(ReplyType::Server((
RPL_NAMREPLY,
format!("{} = {} :{}", nick, req_chan, nicks.join(" ")),
)));
}
replies.push(ReplyType::Server((
RPL_ENDOFNAMES,
format!("{} {} :End of NAMES list", nick, req_chan),
)));
Ok(replies)
} else {
for (name, chan) in self.server.channels.lock().await.iter() {
let nicks: Vec<String> = chan.nicks.iter().cloned().collect();
replies.push(ReplyType::Server((
RPL_NAMREPLY,
format!("{} = {} :{}", nick, name, nicks.join(" ")),
)));
}
replies.push(ReplyType::Server((
RPL_ENDOFNAMES,
format!("{} * :End of NAMES list", nick),
)));
Ok(replies)
}
}
/// `NICK <nickname>`
///
/// Allows a client to change their IRC nickname.
pub async fn handle_cmd_nick(&self, args: &str) -> Result<Vec<ReplyType>> {
// Parse the line
let mut tokens = args.split_ascii_whitespace();
// Reference the current nickname
let old_nick = self.nickname.lock().await.to_string();
let Some(nickname) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} NICK :{}", old_nick, INVALID_SYNTAX),
))])
};
// Forbid disallowed characters.
// The next() call is done to check for ASCII whitespace in the nick.
if tokens.next().is_some() || nickname.starts_with(':') || nickname.starts_with('#') {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_ERRONEOUSNICKNAME,
format!("{} {} :Erroneous nickname", old_nick, nickname),
))])
}
// Set the new nickname
*self.nickname.lock().await = nickname.to_string();
// If the username is set, we can complete the registration
if *self.username.lock().await != "*" && !self.registered.load(SeqCst) {
self.registered.store(true, SeqCst);
if self.reg_paused.load(SeqCst) {
return Ok(vec![])
} else {
return Ok(self.welcome().await)
}
}
// If we were registered, we send a client reply about it.
if self.registered.load(SeqCst) {
Ok(vec![ReplyType::Client((old_nick, format!("NICK :{}", self.nickname.lock().await)))])
} else {
// Otherwise, we don't reply.
Ok(vec![])
}
}
/// `PART <channel>`
///
/// Causes a user to leave the channel <channel>.
pub async fn handle_cmd_part(&self, args: &str) -> Result<Vec<ReplyType>> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let mut tokens = args.split_ascii_whitespace();
let Some(channel) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} PART :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
};
if !channel.starts_with('#') {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} PART :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
}
let mut active_channels = self.channels.lock().await;
if !active_channels.contains(channel) {
return Ok(vec![ReplyType::Server((
ERR_NOSUCHCHANNEL,
format!("{} {} :No such channel", self.nickname.lock().await, channel),
))])
}
// Remove the channel from the client's channel list
active_channels.remove(channel);
let replies = vec![ReplyType::Client((
self.nickname.lock().await.to_string(),
format!("PART {} :Bye", channel),
))];
Ok(replies)
}
/// `PING <server1>`
///
/// Tests a connection. A PING message results in a PONG reply.
pub async fn handle_cmd_ping(&self, args: &str) -> Result<Vec<ReplyType>> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let mut tokens = args.split_ascii_whitespace();
let Some(origin) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOORIGIN,
format!("{} :No origin specified", self.nickname.lock().await),
))])
};
Ok(vec![ReplyType::Pong(origin.to_string())])
}
/// `PRIVMSG <msgtarget> <message>`
///
/// Sends <message> to <msgtarget>. The target is usually a user or
/// a channel.
pub async fn handle_cmd_privmsg(&self, args: &str) -> Result<Vec<ReplyType>> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let mut tokens = args.split_ascii_whitespace();
let Some(target) = tokens.next() else {
return Ok(vec![ReplyType::Server((
ERR_NORECIPIENT,
format!("{} :No recipient given (PRIVMSG)", self.nickname.lock().await),
))])
};
let Some(message) = tokens.next() else {
return Ok(vec![ReplyType::Server((
ERR_NOTEXTTOSEND,
format!("{} :No text to send", self.nickname.lock().await),
))])
};
if !message.starts_with(':') {
return Ok(vec![ReplyType::Server((
ERR_NOTEXTTOSEND,
format!("{} :No text to send", self.nickname.lock().await),
))])
}
// We only send a client reply if the message is for ourself.
// Anything else is rendered by the IRC client and not supposed
// to be echoed by the IRC serer.
if target == *self.nickname.lock().await {
return Ok(vec![ReplyType::Client((
target.to_string(),
format!("PRIVMSG {} {}", target, message),
))])
}
// 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.lock().await.contains_key(target) {
return Ok(vec![ReplyType::Server((
ERR_NOSUCHNICK,
format!("{} :{}", self.nickname.lock().await, target),
))])
}
Ok(vec![])
}
/// `REHASH`
///
/// Causes the server to re-read and re-process its configuration file(s).
pub async fn handle_cmd_rehash(&self, _args: &str) -> Result<Vec<ReplyType>> {
info!("Attempting to rehash server...");
if let Err(e) = self.server.rehash().await {
error!("Failed to rehash server: {}", e);
}
Ok(vec![])
}
/// `TOPIC <channel> [<topic>]`
///
/// Used to get the channel topic on <channel>. If <topic> is given, it
/// sets the channel topic to <topic>.
pub async fn handle_cmd_topic(&self, args: &str) -> Result<Vec<ReplyType>> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let mut tokens = args.split_ascii_whitespace();
let Some(channel) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} TOPIC :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
};
if !self.server.channels.lock().await.contains_key(channel) {
return Ok(vec![ReplyType::Server((
ERR_NOSUCHCHANNEL,
format!("{} {} :No such channel", self.nickname.lock().await, channel),
))])
}
// If there's a topic, we'll set it, otherwise return the set topic.
let Some(topic) = tokens.next() else {
let topic = self.server.channels.lock().await.get(channel).unwrap().topic.clone();
if topic.is_empty() {
return Ok(vec![ReplyType::Server((
RPL_NOTOPIC,
format!("{} {} :No topic is set", self.nickname.lock().await, channel),
))])
} else {
return Ok(vec![ReplyType::Server((
RPL_TOPIC,
format!("{} {} :{}", self.nickname.lock().await, channel, topic),
))])
}
};
// Set the new topic
self.server.channels.lock().await.get_mut(channel).unwrap().topic =
topic.strip_prefix(':').unwrap().to_string();
// Send reply
let replies = vec![ReplyType::Client((
self.nickname.lock().await.to_string(),
format!("TOPIC {} {}", channel, topic),
))];
Ok(replies)
}
/// `USER <user> <mode> <unused> <realname>`
///
/// This command is used at the beginning of a connection to specify the
/// username, hostname, real name, and the initial user modes of the
/// connecting client. <realname> may contain spaces, and thus must be
/// prefixed with a colon.
pub async fn handle_cmd_user(&self, args: &str) -> Result<Vec<ReplyType>> {
if self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_ALREADYREGISTERED,
format!("{} :{}", self.nickname.lock().await, ALREADY_REGISTERED),
))])
}
// Parse the line
let mut tokens = args.split_ascii_whitespace();
let Some(username) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} USER :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
};
// Mode syntax is currently ignored, but should be part of the command
let Some(_mode) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} USER :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
};
// Next token is unused per RFC, but should be part of the command
let Some(_unused) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} USER :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
};
// The final token should be realname and should start with a colon
let Some(realname) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} USER :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
};
if !realname.starts_with(':') {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} USER :{}", self.nickname.lock().await, INVALID_SYNTAX),
))])
}
*self.username.lock().await = username.to_string();
*self.realname.lock().await = realname.to_string();
// If the nickname is set, we can complete the registration
if *self.nickname.lock().await != "*" {
self.registered.store(true, SeqCst);
if self.reg_paused.load(SeqCst) {
return Ok(vec![])
} else {
return Ok(self.welcome().await)
}
}
// Otherwise, we don't have to reply.
Ok(vec![])
}
/// `VERSION`
///
/// Returns the version of the server.
pub async fn handle_cmd_version(&self, _args: &str) -> Result<Vec<ReplyType>> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let replies = vec![ReplyType::Server((
RPL_VERSION,
format!(
"{} {} {} :Let there be dark!",
self.nickname.lock().await,
env!("CARGO_PKG_VERSION"),
SERVER_NAME
),
))];
Ok(replies)
}
/// Internal function that constructs the welcome message.
async fn welcome(&self) -> Vec<ReplyType> {
let nick = self.nickname.lock().await.to_string();
let mut replies = vec![
ReplyType::Server((RPL_WELCOME, format!("{} :{}", nick, WELCOME))),
ReplyType::Server((
RPL_YOURHOST,
format!(
"{} :Your host is irc.dark.fi, running version {}",
nick,
env!("CARGO_PKG_VERSION")
),
)),
];
// Append the MOTD
replies.append(&mut self.handle_cmd_motd("").await.unwrap());
// If we have any configured autojoin channels, let's join the user
// and set their topics, if any.
let mut config_chans = self.server.channels.lock().await;
for channel in self.server.autojoin.lock().await.iter() {
replies.push(ReplyType::Client((nick.clone(), format!("JOIN :{}", channel))));
replies.push(ReplyType::Server((
RPL_NAMREPLY,
format!("{} = {} :{}", nick, channel, nick),
)));
replies.push(ReplyType::Server((
RPL_ENDOFNAMES,
format!("{} {} :End of NAMES list", nick, channel),
)));
if let Some(chan) = config_chans.get_mut(channel) {
if !chan.topic.is_empty() {
replies.push(ReplyType::Client((
nick.clone(),
format!("TOPIC {} :{}", channel, chan.topic),
)));
}
// Insert the client into the channel nicklist
chan.nicks.insert(nick.clone());
}
}
replies
}
}

View File

@@ -16,83 +16,44 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::collections::HashMap;
use std::{collections::HashSet, sync::Arc};
use darkfi::{util::path::get_config_path, Result};
use crypto_box::ChaChaBox;
use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
use crate::{
settings::{
parse_configured_channels, parse_configured_contacts, Args, ChannelInfo, ContactInfo,
CONFIG_FILE, MAXIMUM_LENGTH_OF_NICK_CHAN_CNT,
},
PrivMsgEvent,
};
/// IRC client state
pub(crate) mod client;
mod client;
pub use client::IrcClient;
/// IRC server implementation
pub(crate) mod server;
mod server;
pub use server::IrcServer;
/// IRC command handler
pub(crate) mod command;
#[derive(Clone)]
pub struct IrcConfig {
// init bool
pub is_nick_init: bool,
pub is_user_init: bool,
pub is_registered: bool,
pub is_cap_end: bool,
pub is_pass_init: bool,
/// IRC numerics and server replies
pub(crate) mod rpl;
// user config
/// Hardcoded server name
const SERVER_NAME: &str = "irc.dark.fi";
/// IRC PRIVMSG
#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
pub struct Privmsg {
pub channel: String,
pub nick: String,
pub pass: String,
pub caps: HashMap<String, bool>,
// channels and contacts
pub auto_channels: Vec<String>,
pub channels: HashMap<String, ChannelInfo>,
pub contacts: HashMap<String, ContactInfo>,
}
impl IrcConfig {
pub fn new(settings: &Args) -> Result<Self> {
let pass = settings.password.as_ref().unwrap_or(&String::new()).clone();
let mut auto_channels = settings.autojoin.clone();
auto_channels.retain(|chan| chan.len() <= MAXIMUM_LENGTH_OF_NICK_CHAN_CNT);
// Pick up channel settings from the TOML configuration
let cfg_path = get_config_path(settings.config.clone(), CONFIG_FILE)?;
let toml_contents = std::fs::read_to_string(cfg_path)?;
let channels = parse_configured_channels(&toml_contents)?;
let contacts = parse_configured_contacts(&toml_contents)?;
let mut caps = HashMap::new();
caps.insert("no-history".to_string(), false);
Ok(Self {
is_nick_init: false,
is_user_init: false,
is_registered: false,
is_cap_end: true,
is_pass_init: false,
nick: "anon".to_string(),
pass,
auto_channels,
channels,
contacts,
caps,
})
}
pub msg: String,
}
/// IRC channel definition
#[derive(Clone)]
pub enum ClientSubMsg {
Privmsg(PrivMsgEvent),
Config(IrcConfig),
pub struct IrcChannel {
pub topic: String,
pub nicks: HashSet<String>,
pub saltbox: Option<Arc<ChaChaBox>>,
}
/// IRC contact definition
#[derive(Clone)]
pub enum NotifierMsg {
Privmsg(PrivMsgEvent),
UpdateConfig,
pub struct IrcContact {
pub saltbox: Option<Arc<ChaChaBox>>,
}

205
bin/darkirc/src/irc/rpl.rs Normal file
View File

@@ -0,0 +1,205 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2023 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/>.
*/
#![allow(clippy::zero_prefixed_literal)]
/// The welcome message sent upon successful registration
pub const WELCOME: &str = "Welcome to the DarkIRC network";
/// The message sent to the client when they are not registered
pub const NOT_REGISTERED: &str = "You have not registered";
/// The message sent to the client when they are already registered
pub const ALREADY_REGISTERED: &str = "You may not reregister";
/// The message sent to the client when command params could not parse
pub const INVALID_SYNTAX: &str = "Syntax error";
/// `<client> :Welcome to the DarkIRC network`
///
/// The first message sent after client registration.
pub const RPL_WELCOME: u16 = 001;
/// `<client> :Your host is <servername>, running version <version`
///
/// Part of the post-registration greeting.
pub const RPL_YOURHOST: u16 = 002;
/// `<client> <user modes>`
///
/// Sent to a client to inform that client of their currently-set user modes.
pub const RPL_UMODEIS: u16 = 221;
/// `<client> [<server>] :Administrative info`
///
/// Sent as a reply to an ADMIN command, this numeric establishes the
/// name of the server whose administrative info is being provided.
pub const RPL_ADMINME: u16 = 256;
/// `<client> :<info>`
///
/// Sent as a reply to an ADMIN command. <info> is a string intended to
/// provide information about the location of the server.
pub const RPL_ADMINLOC1: u16 = 257;
/// `<client> :<info>`
///
/// Sent as a reply to an ADMIN command. <info> is a string intended to
/// provide information about whoever runs the server.
pub const RPL_ADMINLOC2: u16 = 258;
/// `<client> :<info>`
///
/// Sent as a reply to an ADMIN command. <info> MUST contain the email
/// address to contact the administrator(s) of the server.
pub const RPL_ADMINEMAIL: u16 = 259;
/// `<client> Channel :Users Name`
///
/// Sent as a reply to the LIST command, this numeric marks the start
/// of a channel list.
pub const RPL_LISTSTART: u16 = 321;
/// `<client> <channel> <client count> :<topic>`
///
/// Sent as a reply to the LIST command, this numeric sends information
/// about a channel to the client. <channel> is the name of the channel.
/// <client count> is an integer indicating how many clients are joined
/// to that channel. <topic> is the channels topic.
pub const RPL_LIST: u16 = 322;
/// <client> :End of /LIST
///
/// Sent as a reply to the LIST command, this numeric indicates the end
/// of a LIST response.
pub const RPL_LISTEND: u16 = 323;
/// `<client> <channel> <modestring> <mode arguments>...`
///
/// Sent to a client to inform them of the currently-set modes of a channel.
/// <channel> is the name of the channel.
pub const RPL_CHANNELMODEIS: u16 = 324;
/// `<client> <channel> :No topic is set`
///
/// Sent to a client when joining a channel to inform them that the channel
/// with the name <channel> does not have any topic set.
pub const RPL_NOTOPIC: u16 = 331;
/// `<client> <channel> :<topic>`
///
/// Sent to a client when joining the <channel> to inform them of the
/// current topic of the channel.
pub const RPL_TOPIC: u16 = 332;
/// `<client> <version> <server> :<comments>`
///
/// Sent as a reply to the VERSION command.
pub const RPL_VERSION: u16 = 351;
/// `<client> <symbol> <channel> :[prefix]<nick>{ [prefix]<nick>}`
///
/// Sent as a reply to the NAMES command
pub const RPL_NAMREPLY: u16 = 353;
/// `<client> <channel> :End of /NAMES list`
///
/// Sent as a reply to the NAMES command
pub const RPL_ENDOFNAMES: u16 = 366;
/// `<client> :<string>`
///
/// Sent as the reply to the INFO command.
pub const RPL_INFO: u16 = 371;
/// `<client> :End of INFO list`
///
/// Indicates the end of an INFO response.
pub const RPL_ENDOFINFO: u16 = 374;
/// `<client> :- <server> Message of the day -`
///
/// Indicates the start of the Message of the Day to the client.
pub const RPL_MOTDSTART: u16 = 375;
/// `<client> :<line of the motd>`
///
/// When sending the Message of the Day to the client, servers reply
/// with each line of the MOTD as this numeric.
pub const RPL_MOTD: u16 = 372;
/// `<client> :End of /MOTD command.`
///
/// Indicates the end of the Message of the Day to the client.
pub const RPL_ENDOFMOTD: u16 = 376;
/// `<client> <nickname> :No such nick/channel`
///
/// Indicates that no client can be found for the supplied nickname.
pub const ERR_NOSUCHNICK: u16 = 401;
/// `<client> <channel> :No such channel`
///
/// Indicates that no channel can be found for the supplied channel name.
pub const ERR_NOSUCHCHANNEL: u16 = 403;
/// `<client> :No origin specified`
///
/// Indicates a PING or PONG message missing the originator parameter
/// which is required by old IRC servers. Nowadays, this may be used by
/// some servers when the PING <token> is empty.
pub const ERR_NOORIGIN: u16 = 409;
/// `<client> :No recipient given (<command>)`
///
/// Returned by the PRIVMSG command to indicate the message wasnt
/// delivered because there was no recipient given.
pub const ERR_NORECIPIENT: u16 = 411;
/// `<client> :No text to send`
///
/// Returned by the PRIVMSG command to indicate the message wasnt
/// delivered because there was no text to send.
pub const ERR_NOTEXTTOSEND: u16 = 412;
/// <client> <nick> :Erroneus nickname
///
/// Returned when a NICK command cannot be successfully completed as
/// the desired nickname contains characters that are disallowed by the server.
pub const ERR_ERRONEOUSNICKNAME: u16 = 432;
/// `<client> :You have not registered`
///
/// Returned when a client command cannot be parsed because they are
/// not registered.
pub const ERR_NOTREGISTERED: u16 = 451;
/// `<client> <command> :Not enough parameters`
///
/// Returned when a client command cannot be parsed because not enough
/// parameters were supplied.
pub const ERR_NEEDMOREPARAMS: u16 = 461;
/// `<client> :You may not reregister`
///
/// Returned when a client tries to change a detail that can only be
/// set during registration.
pub const ERR_ALREADYREGISTERED: u16 = 462;
/// `<client> :Cant change mode for other users`
///
/// Indicates that a MODE command affecting a user failed because they
/// were trying to set or view modes for other users.
pub const ERR_USERSDONTMATCH: u16 = 502;

View File

@@ -16,324 +16,326 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{fs::File, sync::Arc};
use std::{collections::HashMap, fs::File, io::BufReader, path::PathBuf, sync::Arc};
use async_rustls::{rustls, TlsAcceptor};
use log::{error, info};
use smol::{
io::{self, AsyncRead, AsyncWrite, BufReader},
lock::Mutex,
net::{SocketAddr, TcpListener},
};
use darkfi::{
event_graph::{
model::{Event, EventId, ModelPtr},
protocol_event::{Seen, SeenPtr},
view::ViewPtr,
},
net::P2pPtr,
system::{StoppableTask, SubscriberPtr},
util::{path::expand_path, time::Timestamp},
event_graph2::Event,
system::{StoppableTask, StoppableTaskPtr, Subscription},
util::path::expand_path,
Error, Result,
};
use log::{debug, error, info};
use smol::{
fs,
lock::Mutex,
net::{SocketAddr, TcpListener},
prelude::{AsyncRead, AsyncWrite},
Executor,
};
use url::Url;
use super::{ClientSubMsg, IrcClient, IrcConfig, NotifierMsg};
use crate::{settings::Args, PrivMsgEvent};
mod nickserv;
use nickserv::NickServ;
const NICK_NICKSERV: &str = "nickserv";
use super::{client::Client, IrcChannel, IrcContact, Privmsg};
use crate::{
crypto::saltbox,
settings::{parse_autojoin_channels, parse_configured_channels, parse_configured_contacts},
DarkIrc,
};
/// IRC server instance
pub struct IrcServer {
settings: Args,
p2p: P2pPtr,
model: ModelPtr<PrivMsgEvent>,
view: ViewPtr<PrivMsgEvent>,
clients_subscriptions: SubscriberPtr<ClientSubMsg>,
seen: SeenPtr<EventId>,
missed_events: Arc<Mutex<Vec<Event<PrivMsgEvent>>>>,
/// nickserv service
pub nickserv: NickServ,
/// DarkIrc instance
pub darkirc: Arc<DarkIrc>,
/// Path to the darkirc config file
config_path: PathBuf,
/// TCP listener
listener: TcpListener,
/// TLS acceptor
acceptor: Option<TlsAcceptor>,
/// Configured autojoin channels
pub autojoin: Mutex<Vec<String>>,
/// Configured IRC channels
pub channels: Mutex<HashMap<String, IrcChannel>>,
/// Configured IRC contacts
pub contacts: Mutex<HashMap<String, IrcContact>>,
/// Active client connections
clients: Mutex<HashMap<u16, StoppableTaskPtr>>,
}
impl IrcServer {
/// Instantiate a new IRC server. This function will try to bind a TCP socket,
/// and optionally load a TLS certificate and key. To start the listening loop,
/// call `IrcServer::listen()`.
pub async fn new(
settings: Args,
p2p: P2pPtr,
model: ModelPtr<PrivMsgEvent>,
view: ViewPtr<PrivMsgEvent>,
clients_subscriptions: SubscriberPtr<ClientSubMsg>,
) -> Result<Self> {
let seen = Seen::new();
let missed_events = Arc::new(Mutex::new(vec![]));
Ok(Self {
settings,
p2p,
model,
view,
clients_subscriptions,
seen,
missed_events,
nickserv: NickServ::default(),
})
}
pub async fn start(&self, executor: Arc<smol::Executor<'_>>) -> Result<()> {
let (msg_notifier, msg_recv) = smol::channel::unbounded();
// Listen to msgs from clients
StoppableTask::new().start(
Self::listen_to_msgs(
self.p2p.clone(),
self.model.clone(),
self.seen.clone(),
msg_recv,
self.missed_events.clone(),
self.clients_subscriptions.clone(),
),
|res| async {
match res {
Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
Err(e) => error!(target: "darkirc::irc::server::start", "Failed starting listen to msgs: {}", e),
}
},
Error::DetachedTaskStopped,
executor.clone(),
);
// Listen to msgs from View
StoppableTask::new().start(
Self::listen_to_view(
self.view.clone(),
self.seen.clone(),
self.missed_events.clone(),
self.clients_subscriptions.clone(),
),
|res| async {
match res {
Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
Err(e) => error!(target: "darkirc::irc::server::start", "Failed starting listen to view: {}", e),
}
},
Error::DetachedTaskStopped,
executor.clone(),
);
// Start listening for new connections
self.listen(msg_notifier, executor).await?;
Ok(())
}
async fn listen_to_view(
view: ViewPtr<PrivMsgEvent>,
seen: SeenPtr<EventId>,
missed_events: Arc<Mutex<Vec<Event<PrivMsgEvent>>>>,
clients_subscriptions: SubscriberPtr<ClientSubMsg>,
) -> Result<()> {
loop {
let event = view.lock().await.process().await?;
if !seen.push(&event.hash()).await {
continue
}
missed_events.lock().await.push(event.clone());
let msg = event.action.clone();
clients_subscriptions.notify(ClientSubMsg::Privmsg(msg)).await;
darkirc: Arc<DarkIrc>,
listen: Url,
tls_cert: Option<String>,
tls_secret: Option<String>,
config_path: PathBuf,
) -> Result<Arc<Self>> {
let scheme = listen.scheme();
if scheme != "tcp" && scheme != "tcp+tls" {
error!("IRC server supports listening only on tcp:// or tcp+tls://");
return Err(Error::BindFailed(listen.to_string()))
}
}
/// Start listening to msgs from irc clients
pub async fn listen_to_msgs(
p2p: P2pPtr,
model: ModelPtr<PrivMsgEvent>,
seen: SeenPtr<EventId>,
recv: smol::channel::Receiver<(NotifierMsg, usize)>,
missed_events: Arc<Mutex<Vec<Event<PrivMsgEvent>>>>,
clients_subscriptions: SubscriberPtr<ClientSubMsg>,
) -> Result<()> {
loop {
let (msg, subscription_id) = recv.recv().await?;
match msg {
NotifierMsg::Privmsg(msg) => {
// First check if we're communicating with any services.
// If not, then we proceed with behaving like it's a normal
// message.
// TODO: This needs to be protected from adversaries doing
// remote execution.
#[allow(clippy::single_match)]
match msg.target.to_lowercase().as_str() {
NICK_NICKSERV => {
//self.nickserv.act(msg);
continue
}
_ => {} // pass
}
let event = Event {
previous_event_hash: model.lock().await.get_head_hash()?,
action: msg.clone(),
timestamp: Timestamp::current_time(),
};
// Since this will be added to the View directly, other clients connected to irc
// server must get informed about this new msg
clients_subscriptions
.notify_with_exclude(ClientSubMsg::Privmsg(msg), &[subscription_id])
.await;
if !seen.push(&event.hash()).await {
continue
}
missed_events.lock().await.push(event.clone());
p2p.broadcast(&event).await;
}
NotifierMsg::UpdateConfig => {
//
// load and parse the new settings from configuration file and pass it to all
// irc clients
//
// let new_config = IrcConfig::new()?;
// clients_subscriptions.notify(ClientSubMsg::Config(new_config)).await;
}
}
if scheme == "tcp+tls" && (tls_cert.is_none() || tls_secret.is_none()) {
error!("You must provide a TLS certificate and key if you want a TLS server");
return Err(Error::BindFailed(listen.to_string()))
}
}
/// Start listening to new connections from irc clients
pub async fn listen(
&self,
notifier: smol::channel::Sender<(NotifierMsg, usize)>,
executor: Arc<smol::Executor<'_>>,
) -> Result<()> {
let (listener, acceptor) = self.setup_listener().await?;
info!("[IRC SERVER] listening on {}", self.settings.irc_listen);
loop {
let (stream, peer_addr) = match listener.accept().await {
Ok((s, a)) => (s, a),
Err(e) => {
error!("[IRC SERVER] Failed accepting new connections: {}", e);
continue
}
};
let result = if let Some(acceptor) = acceptor.clone() {
// TLS connection
let stream = match acceptor.accept(stream).await {
Ok(s) => s,
Err(e) => {
error!("[IRC SERVER] Failed accepting TLS connection: {}", e);
continue
}
};
self.process_connection(stream, peer_addr, notifier.clone(), executor.clone()).await
} else {
// TCP connection
self.process_connection(stream, peer_addr, notifier.clone(), executor.clone()).await
};
if let Err(e) = result {
error!("[IRC SERVER] Failed processing connection {}: {}", peer_addr, e);
continue
};
info!("[IRC SERVER] Accept new connection: {}", peer_addr);
}
}
/// On every new connection create new IrcClient
async fn process_connection<C: AsyncRead + AsyncWrite + Send + Unpin + 'static>(
&self,
stream: C,
peer_addr: SocketAddr,
notifier: smol::channel::Sender<(NotifierMsg, usize)>,
executor: Arc<smol::Executor<'_>>,
) -> Result<()> {
let (reader, writer) = io::split(stream);
let reader = BufReader::new(reader);
// Subscription for the new client
let client_subscription = self.clients_subscriptions.clone().subscribe().await;
// new irc configuration
let irc_config = IrcConfig::new(&self.settings)?;
// New irc client
let mut client = IrcClient::new(
writer,
reader,
peer_addr,
irc_config,
notifier,
client_subscription,
self.missed_events.clone(),
);
// Start listening and detach
StoppableTask::new().start(
// Weird hack to prevent lifetimes hell
async move {client.listen().await; Ok(())},
|res| async {
match res {
Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
Err(e) => error!(target: "darkirc::irc::server::process_connection", "Failed starting client listen: {}", e),
}
},
Error::DetachedTaskStopped,
executor,
);
Ok(())
}
/// Setup a listener for irc server
async fn setup_listener(&self) -> Result<(TcpListener, Option<TlsAcceptor>)> {
let listenaddr = self.settings.irc_listen.socket_addrs(|| None)?[0];
let listener = TcpListener::bind(listenaddr).await?;
let acceptor = match self.settings.irc_listen.scheme() {
// Bind listener
let listen_addr = listen.socket_addrs(|| None)?[0];
let listener = TcpListener::bind(listen_addr).await?;
let acceptor = match scheme {
"tcp+tls" => {
// openssl genpkey -algorithm ED25519 > example.com.key
// openssl req -new -out example.com.csr -key example.com.key
// openssl x509 -req -days 700 -in example.com.csr -signkey example.com.key -out example.com.crt
if self.settings.irc_tls_secret.is_none() || self.settings.irc_tls_cert.is_none() {
error!("[IRC SERVER] To listen using TLS, please set irc_tls_secret and irc_tls_cert in your config file.");
return Err(Error::KeypairPathNotFound)
}
let file =
File::open(expand_path(self.settings.irc_tls_secret.as_ref().unwrap())?)?;
let mut reader = std::io::BufReader::new(file);
// openssl x509 -req -in example.com.csr -signkey example.com.key -out example.com.crt
let f = File::open(expand_path(tls_secret.as_ref().unwrap())?)?;
let mut reader = BufReader::new(f);
let secret = &rustls_pemfile::pkcs8_private_keys(&mut reader)?[0];
let secret = rustls::PrivateKey(secret.clone());
let file = File::open(expand_path(self.settings.irc_tls_cert.as_ref().unwrap())?)?;
let mut reader = std::io::BufReader::new(file);
let certificate = &rustls_pemfile::certs(&mut reader)?[0];
let certificate = rustls::Certificate(certificate.clone());
let f = File::open(expand_path(tls_cert.as_ref().unwrap())?)?;
let mut reader = BufReader::new(f);
let cert = &rustls_pemfile::certs(&mut reader)?[0];
let cert = rustls::Certificate(cert.clone());
let config = rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(vec![certificate], secret)?;
.with_single_cert(vec![cert], secret)?;
let acceptor = TlsAcceptor::from(Arc::new(config));
Some(acceptor)
}
_ => None,
};
Ok((listener, acceptor))
let self_ = Arc::new(Self {
darkirc,
config_path,
listener,
acceptor,
autojoin: Mutex::new(Vec::new()),
channels: Mutex::new(HashMap::new()),
contacts: Mutex::new(HashMap::new()),
clients: Mutex::new(HashMap::new()),
});
// Load any channel/contact configuration.
self_.rehash().await?;
Ok(self_)
}
/// Reload the darkirc configuration file and reconfigure channels and contacts.
pub async fn rehash(&self) -> Result<()> {
let contents = fs::read_to_string(&self.config_path).await?;
let contents = match toml::from_str(&contents) {
Ok(v) => v,
Err(e) => {
error!("Failed parsing TOML config: {}", e);
return Err(Error::ParseFailed("Failed parsing TOML config"))
}
};
// Parse autojoin channels
let autojoin = parse_autojoin_channels(&contents)?;
// Parse configured channels
let channels = parse_configured_channels(&contents)?;
// Parse configured contacts
let contacts = parse_configured_contacts(&contents)?;
// FIXME: This will remove clients' joined channels. They need to stay.
// Only if everything is fine, replace.
*self.autojoin.lock().await = autojoin;
*self.channels.lock().await = channels;
*self.contacts.lock().await = contacts;
Ok(())
}
/// Start accepting new IRC connections.
pub async fn listen(self: Arc<Self>, ex: Arc<Executor<'_>>) -> Result<()> {
loop {
let (stream, peer_addr) = match self.listener.accept().await {
Ok((s, a)) => (s, a),
// As per usual accept(2) recommendations
Err(e) if e.raw_os_error().is_some() => match e.raw_os_error().unwrap() {
libc::EAGAIN | libc::ECONNABORTED | libc::EPROTO | libc::EINTR => continue,
_ => {
error!("[IRC SERVER] Failed accepting connection: {}", e);
return Err(e.into())
}
},
Err(e) => {
error!("[IRC SERVER] Failed accepting new connection: {}", e);
continue
}
};
match &self.acceptor {
// Expecting encrypted TLS connection
Some(acceptor) => {
let stream = match acceptor.accept(stream).await {
Ok(s) => s,
Err(e) => {
error!("[IRC SERVER] Failed accepting new TLS connection: {}", e);
continue
}
};
// Subscribe to incoming events and set up the connection.
let incoming = self.darkirc.event_graph.event_sub.clone().subscribe().await;
if let Err(e) = self
.clone()
.process_connection(stream, peer_addr, incoming, ex.clone())
.await
{
error!("[IRC SERVER] Failed processing new connection: {}", e);
continue
};
}
// Expecting plain TCP connection
None => {
// Subscribe to incoming events and set up the connection.
let incoming = self.darkirc.event_graph.event_sub.clone().subscribe().await;
if let Err(e) = self
.clone()
.process_connection(stream, peer_addr, incoming, ex.clone())
.await
{
error!("[IRC SERVER] Failed processing new connection: {}", e);
continue
};
}
}
info!("[IRC SERVER] Accepted new client connection at: {}", peer_addr);
}
}
/// IRC client connection process.
/// Sets up multiplexing between the server and client.
/// Detaches the connection as a `StoppableTask`.
async fn process_connection<C: AsyncRead + AsyncWrite + Send + Unpin + 'static>(
self: Arc<Self>,
stream: C,
peer_addr: SocketAddr,
incoming: Subscription<Event>,
ex: Arc<Executor<'_>>,
) -> Result<()> {
let port = peer_addr.port();
let client = Client::new(self.clone(), incoming, peer_addr).await?;
let conn_task = StoppableTask::new();
self.clients.lock().await.insert(port, conn_task.clone());
conn_task.clone().start(
async move { client.multiplex_connection(stream).await },
move |_| async move {
info!("[IRC SERVER] Disconnected client from {}", peer_addr);
self.clone().clients.lock().await.remove(&port);
},
Error::ChannelStopped,
ex,
);
Ok(())
}
/// Try encrypting a given `Privmsg` if there is such a channel/contact.
pub async fn try_encrypt(&self, privmsg: &mut Privmsg) {
if let Some((name, channel)) = self.channels.lock().await.get_key_value(&privmsg.channel) {
if let Some(saltbox) = &channel.saltbox {
privmsg.channel = saltbox::encrypt(saltbox, privmsg.channel.as_bytes());
privmsg.nick = saltbox::encrypt(saltbox, privmsg.nick.as_bytes());
privmsg.msg = saltbox::encrypt(saltbox, privmsg.msg.as_bytes());
debug!("Successfully encrypted message for {}", name);
return
}
};
if let Some((name, contact)) = self.contacts.lock().await.get_key_value(&privmsg.channel) {
if let Some(saltbox) = &contact.saltbox {
privmsg.channel = saltbox::encrypt(saltbox, privmsg.channel.as_bytes());
privmsg.nick = saltbox::encrypt(saltbox, privmsg.nick.as_bytes());
privmsg.msg = saltbox::encrypt(saltbox, privmsg.msg.as_bytes());
debug!("Successfully encrypted message for {}", name);
}
};
}
/// Try decrypting a given potentially encrypted `Privmsg` object.
pub async fn try_decrypt(&self, privmsg: &mut Privmsg) {
// If all fields have base58, then we can consider decrypting.
let channel_ciphertext = match bs58::decode(&privmsg.channel).into_vec() {
Ok(v) => v,
Err(_) => return,
};
let nick_ciphertext = match bs58::decode(&privmsg.nick).into_vec() {
Ok(v) => v,
Err(_) => return,
};
let msg_ciphertext = match bs58::decode(&privmsg.msg).into_vec() {
Ok(v) => v,
Err(_) => return,
};
// Now go through all 3 ciphertexts. We'll use intermediate buffers
// for decryption, and only if all passes, we will return a modified
// (i.e. decrypted) privmsg, otherwise we return the original.
for (name, channel) in self.channels.lock().await.iter() {
if let Some(saltbox) = &channel.saltbox {
let Some(channel_dec) = saltbox::try_decrypt(saltbox, &channel_ciphertext) else {
continue
};
let Some(nick_dec) = saltbox::try_decrypt(saltbox, &nick_ciphertext) else {
continue
};
let Some(msg_dec) = saltbox::try_decrypt(saltbox, &msg_ciphertext) else {
continue
};
privmsg.channel = channel_dec;
privmsg.nick = nick_dec;
privmsg.msg = msg_dec;
debug!("Successfuly decrypted message for {}", name);
return
}
}
for (name, contact) in self.contacts.lock().await.iter() {
if let Some(saltbox) = &contact.saltbox {
let Some(channel_dec) = saltbox::try_decrypt(saltbox, &channel_ciphertext) else {
continue
};
let Some(nick_dec) = saltbox::try_decrypt(saltbox, &nick_ciphertext) else {
continue
};
let Some(msg_dec) = saltbox::try_decrypt(saltbox, &msg_ciphertext) else {
continue
};
privmsg.channel = channel_dec;
privmsg.nick = nick_dec;
privmsg.msg = msg_dec;
debug!("Successfully decrypted message from {}", name);
return
}
}
}
}

View File

@@ -1,97 +0,0 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2023 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::collections::BTreeMap;
use crate::PrivMsgEvent;
#[derive(Debug, Clone, Default)]
pub struct NickServ {
_db: BTreeMap<Vec<u8>, Vec<u8>>,
}
impl NickServ {
fn usage() -> Vec<String> {
let r = vec![
"***** nickserv help *****",
"",
"nickserv allows clients to 'register' an account. An account",
"registration is necessary to be able to join and send messages",
"to the p2p network.",
"",
"The following commands are available:",
"",
" CREATE Create a new account",
" LIST List available accounts",
" REGISTER Register a new account",
" IDENTIFY Identify and pick an account to use",
"",
"***** end of help *****",
];
r.iter().map(|x| x.to_string()).collect()
}
// Here because we might consider returning the actual full protocol PRIVMSG
// in a vec. So we can use the result of this and feed it directly to the
// client as messages. Dunno if necessary, just a thought.
fn reply(msg: String) -> Vec<String> {
vec![msg]
}
/// Parse an incoming nickserv message
pub fn act(&mut self, ev: PrivMsgEvent) -> Result<Vec<String>, Vec<String>> {
assert_eq!(ev.target.to_lowercase().as_str(), super::NICK_NICKSERV);
let parts: Vec<String> = ev.msg.split(' ').map(|x| x.to_string()).collect();
match parts[0].to_uppercase().as_str() {
"CREATE" => self.create(),
"LIST" => self.list(),
"REGISTER" => self.register(),
"IDENTIFY" => self.identify(),
"HELP" => Ok(Self::usage()),
c => Err(vec![format!("Invalid command {}", c), "Type HELP to get help".to_string()]),
}
}
/// Create a new account
fn create(&mut self) -> Result<Vec<String>, Vec<String>> {
Ok(Self::reply("Account created successfully.".to_string()))
}
/// List available accounts
fn list(&self) -> Result<Vec<String>, Vec<String>> {
Ok(Self::reply("Accounts: ...".to_string()))
}
/// Register a created but unregistered account
fn register(&mut self) -> Result<Vec<String>, Vec<String>> {
Ok(Self::reply("Account created successfully.".to_string()))
}
/// Pick an account to use
fn identify(&mut self) -> Result<Vec<String>, Vec<String>> {
Ok(Self::reply("Using account 0.".to_string()))
}
}

View File

@@ -15,205 +15,177 @@
* 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::{
collections::{HashMap, HashSet},
sync::Arc,
};
use chrono::{Duration, Utc};
use irc::ClientSubMsg;
use log::{debug, error, info};
use rand::rngs::OsRng;
use smol::{fs::create_dir_all, lock::Mutex, stream::StreamExt};
use structopt_toml::StructOptToml;
use tinyjson::JsonValue;
use std::{collections::HashSet, sync::Arc};
use darkfi::{
async_daemonize,
event_graph::{
events_queue::EventsQueue,
model::{Model, ModelPtr},
protocol_event::{ProtocolEvent, Seen},
view::View,
},
net,
async_daemonize, cli_desc,
event_graph2::{proto::ProtocolEventGraph, EventGraph, EventGraphPtr},
net::{settings::SettingsOpt, P2p, P2pPtr, SESSION_ALL},
rpc::{
jsonrpc::JsonSubscriber,
server::{listen_and_serve, RequestHandler},
},
system::{sleep, StoppableTask, Subscriber, SubscriberPtr},
util::{file::save_json_file, path::expand_path, time::Timestamp},
system::{StoppableTask, StoppableTaskPtr},
util::path::{expand_path, get_config_path},
Error, Result,
};
use log::{debug, error, info};
use rand::rngs::OsRng;
use smol::{fs, lock::Mutex, stream::StreamExt, Executor};
use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
use url::Url;
pub mod crypto;
pub mod irc;
pub mod privmsg;
pub mod rpc;
pub mod settings;
const CONFIG_FILE: &str = "darkirc_config.toml";
const CONFIG_FILE_CONTENTS: &str = include_str!("../darkirc_config.toml");
use crate::{
crypto::saltbox::KeyPair,
irc::{IrcConfig, IrcServer},
privmsg::PrivMsgEvent,
rpc::JsonRpcInterface,
settings::{Args, ChannelInfo, CONFIG_FILE, CONFIG_FILE_CONTENTS},
};
/// IRC server and client handler implementation
mod irc;
use irc::server::IrcServer;
async fn parse_signals(
sighup_sub: SubscriberPtr<Args>,
client_sub: SubscriberPtr<ClientSubMsg>,
) -> Result<()> {
debug!("Started signal parsing handler");
let subscription = sighup_sub.subscribe().await;
loop {
let args = subscription.receive().await;
let new_config = IrcConfig::new(&args)?;
client_sub.notify(ClientSubMsg::Config(new_config)).await;
}
/// Cryptography utilities
mod crypto;
/// JSON-RPC methods
mod rpc;
/// Settings utilities
mod settings;
#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
#[serde(default)]
#[structopt(name = "darkirc", about = cli_desc!())]
struct Args {
#[structopt(short, parse(from_occurrences))]
/// Increase verbosity (-vvv supported)
verbose: u8,
#[structopt(short, long)]
/// Configuration file to use
config: Option<String>,
#[structopt(long)]
/// Set log file output
log: Option<String>,
#[structopt(long, default_value = "tcp://127.0.0.1:26660")]
/// RPC server listen address
rpc_listen: Url,
#[structopt(long, default_value = "tcp://127.0.0.1:6667")]
/// IRC server listen address
irc_listen: Url,
/// Optional TLS certificate file path if `irc_listen` uses TLS
irc_tls_cert: Option<String>,
/// Optional TLS certificate key file path if `irc_listen` uses TLS
irc_tls_secret: Option<String>,
#[structopt(short, long, default_value = "~/.local/darkfi/darkirc_db")]
/// Datastore (DB) path
datastore: String,
/// Generate a new NaCl keypair and exit
#[structopt(long)]
gen_chacha_keypair: bool,
/// Generate a new encrypted channel NaCl secret and exit
#[structopt(long)]
gen_channel_secret: bool,
/// Recover NaCl public key from a secret key
#[structopt(long)]
get_chacha_pubkey: Option<String>,
/// P2P network settings
#[structopt(flatten)]
net: SettingsOpt,
}
// Removes events older than one week ,then sleeps untill next midnight
async fn remove_old_events(model: ModelPtr<PrivMsgEvent>) -> Result<()> {
loop {
let now = Utc::now();
pub struct DarkIrc {
/// P2P network pointer
p2p: P2pPtr,
/// Event Graph instance
event_graph: EventGraphPtr,
/// JSON-RPC connection tracker
rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
/// dnet JSON-RPC subscriber
dnet_sub: JsonSubscriber,
}
// clocks are valid, safe to unwrap
let next_midnight = (now + Duration::days(1)).date_naive().and_hms_opt(0, 0, 0).unwrap();
let duration = next_midnight.signed_duration_since(now.naive_utc()).to_std().unwrap();
let week_old_datetime =
(now - Duration::weeks(1)).date_naive().and_hms_opt(0, 0, 0).unwrap();
let timestamp = week_old_datetime.timestamp() as u64;
model.lock().await.remove_old_events(Timestamp(timestamp))?;
info!("Removing old events");
sleep(duration.as_secs() + 1).await;
impl DarkIrc {
fn new(p2p: P2pPtr, event_graph: EventGraphPtr, dnet_sub: JsonSubscriber) -> Self {
Self { p2p, event_graph, rpc_connections: Mutex::new(HashSet::new()), dnet_sub }
}
}
async_daemonize!(realmain);
async fn realmain(settings: Args, executor: Arc<smol::Executor<'static>>) -> Result<()> {
let datastore_path = expand_path(&settings.datastore)?;
// mkdir datastore_path if not exists
create_dir_all(datastore_path.clone()).await?;
// Signal handling for config reload and graceful termination.
let (signals_handler, signals_task) = SignalHandler::new(executor.clone())?;
let client_sub = Subscriber::new();
executor.spawn(parse_signals(signals_handler.sighup_sub.clone(), client_sub.clone())).detach();
////////////////////
// Generate new keypair and exit
////////////////////
if settings.gen_keypair {
let secret_key = crypto_box::SecretKey::generate(&mut OsRng);
let public_key = secret_key.public_key();
let secret = bs58::encode(secret_key.to_bytes()).into_string();
let public = bs58::encode(public_key.as_bytes()).into_string();
let kp = KeyPair { secret, public };
if settings.output.is_some() {
let datastore = expand_path(&settings.output.unwrap())?;
let kp_enc = JsonValue::Object(HashMap::from([
("public".to_string(), JsonValue::String(kp.public)),
("secret".to_string(), JsonValue::String(kp.secret)),
]));
save_json_file(&datastore, &kp_enc, false)?;
} else {
println!("Generated keypair:\n{}", kp);
}
async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
if args.gen_chacha_keypair {
let secret = crypto_box::SecretKey::generate(&mut OsRng);
let public = secret.public_key();
let secret = bs58::encode(secret.to_bytes()).into_string();
let public = bs58::encode(public.to_bytes()).into_string();
println!("Place this in your config file:\n");
println!("[crypto]");
println!("#dm_chacha_public = \"{}\"", public);
println!("dm_chacha_secret = \"{}\"", secret);
return Ok(())
}
if settings.secret.is_some() {
let secret = settings.secret.clone().unwrap();
let bytes: [u8; 32] = bs58::decode(secret).into_vec()?.try_into().unwrap();
let secret = crypto_box::SecretKey::from(bytes);
let pubkey = secret.public_key();
let pub_encoded = bs58::encode(pubkey.as_bytes()).into_string();
if settings.output.is_some() {
let datastore = expand_path(&settings.output.unwrap())?;
save_json_file(&datastore, &JsonValue::String(pub_encoded), false)?;
} else {
println!("Public key recovered: {}", pub_encoded);
}
if args.gen_channel_secret {
let secret = crypto_box::SecretKey::generate(&mut OsRng);
let secret = bs58::encode(secret.to_bytes()).into_string();
println!("Place this in your config file:\n");
println!("[channel.\"#yourchannelname\"]");
println!("secret = \"{}\"", secret);
return Ok(())
}
if settings.gen_secret {
let secret_key = crypto_box::SecretKey::generate(&mut OsRng);
let encoded = bs58::encode(secret_key.to_bytes());
println!("{}", encoded.into_string());
if let Some(chacha_secret) = args.get_chacha_pubkey {
let bytes = match bs58::decode(chacha_secret).into_vec() {
Ok(v) => v,
Err(e) => {
println!("Error: {}", e);
return Err(Error::ParseFailed("Secret key parsing failed"))
}
};
if bytes.len() != 32 {
return Err(Error::ParseFailed("Decoded base58 is not 32 bytes long"))
}
let secret: [u8; 32] = bytes.try_into().unwrap();
let secret = crypto_box::SecretKey::from(secret);
println!("{}", bs58::encode(secret.public_key().to_bytes()).into_string());
return Ok(())
}
////////////////////
// Initialize the base structures
////////////////////
let events_queue = EventsQueue::<PrivMsgEvent>::new();
let model = Arc::new(Mutex::new(Model::new(events_queue.clone())));
let view = Arc::new(Mutex::new(View::new(events_queue.clone())));
let model_clone = model.clone();
let model_clone2 = model.clone();
info!("Initializing DarkIRC node");
{
// Temporarly load model and check if the loaded head is not
// older than one week (already removed from other node's tree)
let now = Utc::now();
// Create datastore path if not there already.
let datastore = expand_path(&args.datastore)?;
fs::create_dir_all(&datastore).await?;
let now_datetime = (now - Duration::weeks(1)).date_naive().and_hms_opt(0, 0, 0).unwrap();
let timestamp = Timestamp(now_datetime.timestamp() as u64);
info!("Instantiating event DAG");
let sled_db = sled::open(datastore)?;
let p2p = P2p::new(args.net.into(), ex.clone()).await;
let event_graph =
EventGraph::new(p2p.clone(), sled_db.clone(), "darkirc_dag", 1, ex.clone()).await?;
let mut loaded_model = Model::new(events_queue.clone());
loaded_model.load_tree(&datastore_path)?;
if loaded_model
.get_event(&loaded_model.get_head_hash()?)
.is_some_and(|event| event.timestamp >= timestamp)
{
model.lock().await.load_tree(&datastore_path)?;
}
}
////////////////////
// P2p setup
////////////////////
// Buffers
let seen_event = Seen::new();
let seen_inv = Seen::new();
// Check the version
let net_settings = settings.net.clone();
// New p2p
let p2p = net::P2p::new(net_settings.into(), executor.clone()).await;
// Register the protocol_event
info!("Registering EventGraph P2P protocol");
let event_graph_ = Arc::clone(&event_graph);
let registry = p2p.protocol_registry();
registry
.register(net::SESSION_ALL, move |channel, p2p| {
let seen_event = seen_event.clone();
let seen_inv = seen_inv.clone();
let model = model.clone();
async move { ProtocolEvent::init(channel, p2p, model, seen_event, seen_inv).await }
.register(SESSION_ALL, move |channel, _| {
let event_graph_ = event_graph_.clone();
async move { ProtocolEventGraph::init(event_graph_, channel).await.unwrap() }
})
.await;
// ==============
// p2p dnet setup
// ==============
info!(target: "darkirc", "Starting dnet subs task");
let json_sub = JsonSubscriber::new("dnet.subscribe_events");
let json_sub_ = json_sub.clone();
info!("Starting dnet subs task");
let dnet_sub = JsonSubscriber::new("dnet.subscribe_events");
let dnet_sub_ = dnet_sub.clone();
let p2p_ = p2p.clone();
let dnet_task = StoppableTask::new();
dnet_task.clone().start(
@@ -222,117 +194,103 @@ async fn realmain(settings: Args, executor: Arc<smol::Executor<'static>>) -> Res
loop {
let event = dnet_sub.receive().await;
debug!("Got dnet event: {:?}", event);
json_sub_.notify(vec![event.into()]).await;
dnet_sub_.notify(vec![event.into()]).await;
}
},
|res| async {
match res {
Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
Err(e) => {
error!(target: "darkirc", "Failed starting remove old events task: {}", e)
}
Err(e) => panic!("{}", e),
}
},
Error::DetachedTaskStopped,
executor.clone(),
ex.clone(),
);
////////////////////
// RPC interface setup
////////////////////
let rpc_listen_addr = settings.rpc_listen.clone();
info!(target: "darkirc", "Starting JSON-RPC server on {}", rpc_listen_addr);
let rpc_interface = Arc::new(JsonRpcInterface {
addr: rpc_listen_addr.clone(),
p2p: p2p.clone(),
dnet_sub: json_sub,
rpc_connections: Mutex::new(HashSet::new()),
});
info!("Starting JSON-RPC server");
let darkirc = Arc::new(DarkIrc::new(p2p.clone(), event_graph.clone(), dnet_sub));
let darkirc_ = Arc::clone(&darkirc);
let rpc_task = StoppableTask::new();
let rpc_interface_ = rpc_interface.clone();
rpc_task.clone().start(
listen_and_serve(rpc_listen_addr, rpc_interface, None, executor.clone()),
listen_and_serve(args.rpc_listen, darkirc.clone(), None, ex.clone()),
|res| async move {
match res {
Ok(()) | Err(Error::RpcServerStopped) => rpc_interface_.stop_connections().await,
Err(e) => error!(target: "darkirc", "Failed starting JSON-RPC server: {}", e),
Ok(()) | Err(Error::RpcServerStopped) => darkirc_.stop_connections().await,
Err(e) => error!("Failed stopping JSON-RPC server: {}", e),
}
},
Error::RpcServerStopped,
executor.clone(),
ex.clone(),
);
////////////////////
// Start P2P network
////////////////////
info!(target: "darkirc", "Starting P2P network");
p2p.clone().start().await?;
////////////////////
// IRC server
////////////////////
info!(target: "darkirc", "Starting IRC server");
info!("Starting IRC server");
let config_path = get_config_path(args.config, CONFIG_FILE)?;
let irc_server = IrcServer::new(
settings.clone(),
p2p.clone(),
model_clone.clone(),
view.clone(),
client_sub,
darkirc.clone(),
args.irc_listen,
args.irc_tls_cert,
args.irc_tls_secret,
config_path,
)
.await?;
let irc_server_task = StoppableTask::new();
let executor_ = executor.clone();
irc_server_task.clone().start(
// Weird hack to prevent lifetimes hell
async move { irc_server.start(executor_).await },
|res| async {
let irc_task = StoppableTask::new();
let ex_ = ex.clone();
irc_task.clone().start(
irc_server.clone().listen(ex_),
|res| async move {
match res {
Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
Err(e) => error!(target: "darkirc", "Failed starting IRC server: {}", e),
Ok(()) | Err(Error::DetachedTaskStopped) => { /* TODO: */ }
Err(e) => error!("Failed stopping IRC server: {}", e),
}
},
Error::DetachedTaskStopped,
executor.clone(),
ex.clone(),
);
// Reset root task
info!(target: "darkirc", "Starting remove old events task");
let remove_old_events_task = StoppableTask::new();
remove_old_events_task.clone().start(
remove_old_events(model_clone2),
|res| async {
match res {
Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
Err(e) => {
error!(target: "darkirc", "Failed starting remove old events task: {}", e)
info!("Starting P2P network");
p2p.clone().start().await?;
/*
// We'll attempt to sync 5 times
for i in 1..=6 {
info!("Syncing event DAG (attempt #{})", i);
match event_graph.dag_sync().await {
Ok(()) => break,
Err(e) => {
if i == 6 {
error!("Failed syncing DAG. Exiting.");
p2p.stop().await;
return Err(Error::DagSyncFailed)
} else {
// TODO: Maybe at this point we should prune or something?
// TODO: Or maybe just tell the user to delete the DAG from FS.
error!("Failed syncing DAG ({}), retrying in 10s...", e);
sleep(10).await;
}
}
},
Error::DetachedTaskStopped,
executor,
);
}
}
*/
// Wait for termination signal
// Signal handling for graceful termination.
let (signals_handler, signals_task) = SignalHandler::new(ex)?;
signals_handler.wait_termination(signals_task).await?;
info!("Caught termination signal, cleaning up and exiting...");
model_clone.lock().await.save_tree(&datastore_path)?;
info!(target: "darkirc", "Stopping dnet subs task...");
dnet_task.stop().await;
info!(target: "darkirc", "Stopping JSON-RPC server...");
rpc_task.stop().await;
info!(target: "darkirc", "Stopping P2P network");
info!("Stopping P2P network");
p2p.stop().await;
info!(target: "darkirc", "Stopping IRC server...");
irc_server_task.stop().await;
info!("Stopping JSON-RPC server");
rpc_task.stop().await;
dnet_task.stop().await;
info!(target: "darkirc", "Stopping remove old events task...");
remove_old_events_task.stop().await;
info!("Stopping IRC server");
irc_task.stop().await;
info!("Flushing sled");
sled_db.flush_async().await?;
info!("Shut down successfully");
Ok(())
}

View File

@@ -1,43 +0,0 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2023 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 darkfi::event_graph::EventMsg;
use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
#[derive(SerialEncodable, SerialDecodable, Clone, Debug)]
pub struct PrivMsgEvent {
pub nick: String,
pub msg: String,
pub target: String,
}
impl std::string::ToString for PrivMsgEvent {
fn to_string(&self) -> String {
format!(":{}!anon@dark.fi PRIVMSG {} :{}\r\n", self.nick, self.target, self.msg)
}
}
impl EventMsg for PrivMsgEvent {
fn new() -> Self {
Self {
nick: "root".to_string(),
msg: "Let there be dark".to_string(),
target: "root".to_string(),
}
}
}

View File

@@ -15,35 +15,26 @@
* 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::collections::HashSet;
use async_trait::async_trait;
use log::debug;
use smol::lock::{Mutex, MutexGuard};
use tinyjson::JsonValue;
use url::Url;
use darkfi::{
net,
net::P2pPtr,
rpc::{
jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult, JsonSubscriber},
jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult},
p2p_method::HandlerP2p,
server::RequestHandler,
util::JsonValue,
},
system::StoppableTaskPtr,
};
use log::debug;
use smol::lock::MutexGuard;
pub struct JsonRpcInterface {
pub addr: Url,
pub p2p: net::P2pPtr,
pub dnet_sub: JsonSubscriber,
/// JSON-RPC connection tracker
pub rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
}
use super::DarkIrc;
#[async_trait]
impl RequestHandler for JsonRpcInterface {
impl RequestHandler for DarkIrc {
async fn handle_request(&self, req: JsonRequest) -> JsonResult {
debug!(target: "darkirc::rpc", "--> {}", req.stringify().unwrap());
@@ -51,7 +42,7 @@ impl RequestHandler for JsonRpcInterface {
"ping" => self.pong(req.id, req.params).await,
"dnet.switch" => self.dnet_switch(req.id, req.params).await,
"dnet.subscribe_events" => self.dnet_subscribe_events(req.id, req.params).await,
// TODO: make this optional
// TODO: Make this optional
"p2p.get_info" => self.p2p_get_info(req.id, req.params).await,
_ => JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
}
@@ -62,7 +53,7 @@ impl RequestHandler for JsonRpcInterface {
}
}
impl JsonRpcInterface {
impl DarkIrc {
// RPCAPI:
// Activate or deactivate dnet in the P2P stack.
// By sending `true`, dnet will be activated, and by sending `false` dnet
@@ -104,8 +95,8 @@ impl JsonRpcInterface {
}
}
impl HandlerP2p for JsonRpcInterface {
fn p2p(&self) -> net::P2pPtr {
impl HandlerP2p for DarkIrc {
fn p2p(&self) -> P2pPtr {
self.p2p.clone()
}
}

View File

@@ -21,226 +21,125 @@ use std::{
sync::Arc,
};
use crypto_box::ChaChaBox;
use log::{info, warn};
use serde::{self, Deserialize};
use structopt::StructOpt;
use structopt_toml::StructOptToml;
use toml::Value;
use url::Url;
use darkfi::{Error::ParseFailed, Result};
use log::info;
use darkfi::{net::settings::SettingsOpt, Result};
use crate::irc::{IrcChannel, IrcContact};
// Location for config file
pub const CONFIG_FILE: &str = "darkirc_config.toml";
pub const CONFIG_FILE_CONTENTS: &str = include_str!("../darkirc_config.toml");
// Msg config
pub const MAXIMUM_LENGTH_OF_MESSAGE: usize = 1024;
pub const MAXIMUM_LENGTH_OF_NICK_CHAN_CNT: usize = 32;
// IRC Client
pub enum RPL {
NoTopic = 331,
Topic = 332,
NameReply = 353,
EndOfNames = 366,
}
/// darkirc cli
#[derive(Clone, Deserialize, StructOpt, StructOptToml)]
#[serde(default)]
#[structopt(name = "darkirc")]
pub struct Args {
/// Sets a custom config file
#[structopt(long)]
pub config: Option<String>,
/// Sets Datastore Path
#[structopt(long, default_value = "~/.local/darkfi/darkirc")]
pub datastore: String,
/// JSON-RPC listen URL
#[structopt(long = "rpc", default_value = "tcp://127.0.0.1:26660")]
pub rpc_listen: Url,
/// IRC listen URL
#[structopt(long = "irc", default_value = "tcp://127.0.0.1:6667")]
pub irc_listen: Url,
/// Optional TLS certificate file path if `irc_listen` uses TLS
pub irc_tls_cert: Option<String>,
/// Optional TLS certificate key file path if `irc_listen` uses TLS
pub irc_tls_secret: Option<String>,
/// Generate a new NaCl keypair and exit
#[structopt(long)]
pub gen_keypair: bool,
/// Generate a new NaCl secret for an encrypted channel and exit
#[structopt(long)]
pub gen_secret: bool,
/// Recover public key from secret key
#[structopt(long = "recover_pubkey")]
pub secret: Option<String>,
/// Path to save keypair in
#[structopt(short)]
pub output: Option<String>,
/// Autojoin channels
#[structopt(long)]
pub autojoin: Vec<String>,
/// Password
#[structopt(long)]
pub password: Option<String>,
/// Network settings
#[structopt(flatten)]
pub net: SettingsOpt,
#[structopt(short, long)]
/// Set log file to ouput into
pub log: Option<String>,
/// Increase verbosity
#[structopt(short, parse(from_occurrences))]
pub verbose: u8,
}
/// This struct holds information about preconfigured contacts.
/// In the TOML configuration file, we can configure contacts as such:
/// Parse configured autojoin channels from a TOML map.
///
/// ```toml
/// [contact."nick"]
/// pubkey = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
/// autojoin = ["#dev", "#memes"]
/// ```
#[derive(Clone)]
pub struct ContactInfo {
/// Optional NaCl box for the channel, used for {en,de}cryption.
pub salt_box: Option<Arc<ChaChaBox>>,
}
pub fn parse_autojoin_channels(data: &toml::Value) -> Result<Vec<String>> {
let mut ret = vec![];
impl ContactInfo {
pub fn new() -> Result<Self> {
Ok(Self { salt_box: None })
}
}
let Some(autojoin) = data.get("autojoin") else { return Ok(ret) };
let Some(autojoin) = autojoin.as_array() else {
return Err(ParseFailed("autojoin not an array"))
};
/// Defined user modes
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
pub enum UserMode {
None,
Op,
Voice,
HalfOp,
Admin,
Owner,
}
for item in autojoin {
let Some(channel) = item.as_str() else {
return Err(ParseFailed("autojoin channel not a string"))
};
impl std::fmt::Display for UserMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
match self {
Self::None => write!(f, ""),
Self::Op => write!(f, "@"),
Self::Voice => write!(f, "+"),
Self::HalfOp => write!(f, "%"),
Self::Admin => write!(f, "&"),
Self::Owner => write!(f, "~"),
}
}
}
/// This struct holds info about a specific nickname within a channel.
/// We usually use it to implement modes.
#[derive(Debug, Clone, Eq)]
pub struct Nick {
name: String,
mode: UserMode,
}
impl Nick {
pub fn new(name: String) -> Self {
Self { name, mode: UserMode::None }
}
pub fn set_mode(&mut self, mode: UserMode) -> Option<String> {
if self.mode == mode {
return None
if !channel.starts_with('#') {
return Err(ParseFailed("autojoin channel not a valid channel"))
}
self.mode = mode;
Some(format!("+{}", mode))
}
pub fn unset_mode(&mut self, mode: UserMode) -> Option<String> {
if self.mode != mode {
return None
if ret.contains(&channel.to_string()) {
return Err(ParseFailed("Duplicate autojoin channel found"))
}
self.mode = mode;
Some(format!("-{}", mode))
ret.push(channel.to_string());
}
Ok(ret)
}
impl PartialEq for Nick {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl From<String> for Nick {
fn from(name: String) -> Self {
Self { name, mode: UserMode::None }
}
}
impl std::hash::Hash for Nick {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
state.write(&self.name.clone().into_bytes());
}
}
impl std::fmt::Display for Nick {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{}{}", self.mode, self.name)
}
}
/// This struct holds information about preconfigured channels.
/// In the TOML configuration file, we can configure channels as such:
/// Parse a DM secret key from a TOML map.
///
/// ```toml
/// [channel."#dev"]
/// secret = "GvH4kno3kUu6dqPrZ8zjMhqxTUDZ2ev16EdprZiZJgj1"
/// topic = "DarkFi Development Channel"
/// [crypto]
/// dm_chacha_secret = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
/// ```
/// Having a secret will enable a NaCl box that is able to encrypt and
/// decrypt messages in this channel using this set shared secret.
/// The secret should be shared OOB, via a secure channel.
/// Having a topic set is useful if one wants to have a topic in the
/// configured channel. It is not shared with others, but it is useful
/// for personal reference.
#[derive(Default, Clone)]
pub struct ChannelInfo {
/// Optional topic for the channel
pub topic: Option<String>,
/// Optional NaCl box for the channel, used for {en,de}cryption.
pub salt_box: Option<Arc<ChaChaBox>>,
/// All nicknames which are visible on the channel
pub names: HashSet<Nick>,
fn parse_dm_chacha_secret(data: &toml::Value) -> Result<Option<crypto_box::SecretKey>> {
let Some(table) = data.as_table() else { return Err(ParseFailed("TOML not a map")) };
let Some(crypto) = table.get("crypto") else { return Ok(None) };
let Some(crypto) = crypto.as_table() else { return Err(ParseFailed("`crypto` not a map")) };
if !crypto.contains_key("dm_chacha_secret") {
return Ok(None)
}
let Some(secret_str) = crypto["dm_chacha_secret"].as_str() else {
return Err(ParseFailed("dm_chacha_secret not a string"))
};
let Ok(secret_bytes) = bs58::decode(secret_str).into_vec() else {
return Err(ParseFailed("dm_chacha_secret not valid base58"))
};
if secret_bytes.len() != 32 {
return Err(ParseFailed("dm_chacha_secret not 32 bytes long"))
}
let secret_bytes: [u8; 32] = secret_bytes.try_into().unwrap();
Ok(Some(crypto_box::SecretKey::from(secret_bytes)))
}
impl ChannelInfo {
pub fn new() -> Result<Self> {
Ok(Self { topic: None, salt_box: None, names: HashSet::new() })
/// Parse configured contacts from a TOML map.
///
/// ```toml
/// [contact."anon"]
/// dm_chacha_public = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
/// ```
pub fn parse_configured_contacts(data: &toml::Value) -> Result<HashMap<String, IrcContact>> {
let mut ret = HashMap::new();
let Some(table) = data.as_table() else { return Err(ParseFailed("TOML not a map")) };
let Some(contacts) = table.get("contact") else { return Ok(ret) };
let Some(contacts) = contacts.as_table() else {
return Err(ParseFailed("`contact` not a map"))
};
let Some(secret) = parse_dm_chacha_secret(data)? else {
return Err(ParseFailed("Did not find a valid chacha secret"))
};
for (name, items) in contacts {
let Some(public_str) = items.get("dm_chacha_public") else {
return Err(ParseFailed("Invalid contact configuration"))
};
let Some(public_str) = public_str.as_str() else {
return Err(ParseFailed("Invalid contact configuration"))
};
let Ok(public_bytes) = bs58::decode(public_str).into_vec() else {
return Err(ParseFailed("Invalid base58 for contact pubkey"))
};
if public_bytes.len() != 32 {
return Err(ParseFailed("Invalid contact pubkey (not 32 bytes)"))
}
let public_bytes: [u8; 32] = public_bytes.try_into().unwrap();
let public = crypto_box::PublicKey::from(public_bytes);
let saltbox = Some(Arc::new(crypto_box::ChaChaBox::new(&public, &secret)));
if ret.contains_key(name) {
return Err(ParseFailed("Duplicate contact found"))
}
info!("Instantiated ChaChaBox for contact \"{}\"", name);
ret.insert(name.to_string(), IrcContact { saltbox });
}
pub fn names(&self) -> String {
self.names.iter().map(|n| n.to_string()).collect::<Vec<String>>().join(" ")
}
Ok(ret)
}
/// Parse a TOML string for any configured channels and return
@@ -251,186 +150,48 @@ impl ChannelInfo {
/// secret = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
/// topic = "Dank Memes"
/// ```
pub fn parse_configured_channels(data: &str) -> Result<HashMap<String, ChannelInfo>> {
pub fn parse_configured_channels(data: &toml::Value) -> Result<HashMap<String, IrcChannel>> {
let mut ret = HashMap::new();
let map = match toml::from_str(data)? {
Value::Table(m) => m,
_ => return Ok(ret),
};
let Some(table) = data.as_table() else { return Err(ParseFailed("TOML not a map")) };
let Some(chans) = table.get("channel") else { return Ok(ret) };
let Some(chans) = chans.as_table() else { return Err(ParseFailed("`channel` not a map")) };
if !map.contains_key("channel") {
return Ok(ret)
}
for (name, items) in chans {
let mut chan = IrcChannel { topic: String::new(), nicks: HashSet::new(), saltbox: None };
if !map["channel"].is_table() {
return Ok(ret)
}
for chan in map["channel"].as_table().unwrap() {
if chan.0.len() > MAXIMUM_LENGTH_OF_NICK_CHAN_CNT {
warn!("Channel name is too long, skipping...");
continue
}
info!("Found configuration for channel {}", chan.0);
let mut channel_info = ChannelInfo::new()?;
if chan.1.as_table().unwrap().contains_key("topic") {
let topic = chan.1["topic"].as_str().unwrap().to_string();
info!("Found topic for channel {}: {}", chan.0, topic);
channel_info.topic = Some(topic);
}
if chan.1.as_table().unwrap().contains_key("secret") {
// Build the NaCl box
if let Some(s) = chan.1["secret"].as_str() {
let salt_box = salt_box_from_shared_secret(s)?;
channel_info.salt_box = Some(Arc::new(salt_box));
info!("Instantiated NaCl box for channel {}", chan.0);
if let Some(topic) = items.get("topic") {
if let Some(topic) = topic.as_str() {
info!("Found configured topic for {}: {}", name, topic);
chan.topic = topic.to_string();
} else {
return Err(ParseFailed("Channel topic not a string"))
}
}
ret.insert(chan.0.to_string(), channel_info);
}
if let Some(secret) = items.get("secret") {
if let Some(secret) = secret.as_str() {
let Ok(secret_bytes) = bs58::decode(secret).into_vec() else {
return Err(ParseFailed("Channel secret not valid base58"))
};
Ok(ret)
}
/// Parse a TOML string for any configured contact list and return
/// a map containing said configurations.
///
/// ```toml
/// [contact."nick"]
/// public_key = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
/// ```
pub fn parse_configured_contacts(data: &str) -> Result<HashMap<String, ContactInfo>> {
let mut ret = HashMap::new();
let map = match toml::from_str(data) {
Ok(Value::Table(m)) => m,
_ => {
warn!("Invalid TOML string passed as argument to parse_configured_contacts()");
return Ok(ret)
}
};
if !map.contains_key("contact") {
return Ok(ret)
}
if !map["contact"].is_table() {
warn!("TOML configuration contains a \"contact\" field, but it is not a table.");
return Ok(ret)
}
let contacts = map["contact"].as_table().unwrap();
// Our secret key for NaCl boxes.
let found_secret = match parse_secret_key(data) {
Ok(v) => v,
Err(_) => {
info!("Did not find secret key in config, skipping contact configuration.");
return Ok(ret)
}
};
let bytes: [u8; 32] = match bs58::decode(found_secret).into_vec() {
Ok(v) => {
if v.len() != 32 {
warn!("Decoded base58 secret key string is not 32 bytes");
warn!("Skipping private contact configuration");
return Ok(ret)
}
v.try_into().unwrap()
}
Err(e) => {
warn!("Failed to decode base58 secret key from string: {}", e);
warn!("Skipping private contact configuration");
return Ok(ret)
}
};
let secret = crypto_box::SecretKey::from(bytes);
for cnt in contacts {
if cnt.0.len() > MAXIMUM_LENGTH_OF_NICK_CHAN_CNT {
warn!("Contact name is too long, skipping...");
continue
}
info!("Found configuration for contact {}", cnt.0);
let mut contact_info = ContactInfo::new()?;
if !cnt.1.is_table() {
warn!("Config for contact {} isn't a TOML table", cnt.0);
continue
}
let table = cnt.1.as_table().unwrap();
if table.is_empty() {
warn!("Configuration for contact {} is empty.", cnt.0);
continue
}
// Build the NaCl box
if !table.contains_key("public_key") || !table["public_key"].is_str() {
warn!("Contact {} doesn't have `public_key` set or is not a valid string.", cnt.0);
continue
}
let pub_str = table["public_key"].as_str().unwrap();
let bytes: [u8; 32] = match bs58::decode(pub_str).into_vec() {
Ok(v) => {
if v.len() != 32 {
warn!("Decoded base58 string is not 32 bytes");
continue
if secret_bytes.len() != 32 {
return Err(ParseFailed("Channel secret not 32 bytes long"))
}
v.try_into().unwrap()
let secret_bytes: [u8; 32] = secret_bytes.try_into().unwrap();
let secret = crypto_box::SecretKey::from(secret_bytes);
let public = secret.public_key();
chan.saltbox = Some(Arc::new(crypto_box::ChaChaBox::new(&public, &secret)));
info!("Configured NaCl box for channel {}", name);
} else {
return Err(ParseFailed("Channel secret not a string"))
}
Err(e) => {
warn!("Failed to decode base58 pubkey from string: {}", e);
continue
}
};
}
let public = crypto_box::PublicKey::from(bytes);
contact_info.salt_box = Some(Arc::new(ChaChaBox::new(&public, &secret)));
ret.insert(cnt.0.to_string(), contact_info);
info!("Instantiated NaCl box for contact \"{}\"", cnt.0);
info!("Configured channel {}", name);
ret.insert(name.to_string(), chan);
}
Ok(ret)
}
fn salt_box_from_shared_secret(s: &str) -> Result<ChaChaBox> {
let bytes: [u8; 32] = bs58::decode(s).into_vec()?.try_into().unwrap();
let secret = crypto_box::SecretKey::from(bytes);
let public = secret.public_key();
Ok(ChaChaBox::new(&public, &secret))
}
fn parse_secret_key(data: &str) -> Result<String> {
let mut sk = String::new();
let map = match toml::from_str(data)? {
Value::Table(m) => m,
_ => return Ok(sk),
};
if !map.contains_key("secret_key") {
return Ok(sk)
}
if !map["secret_key"].is_table() {
return Ok(sk)
}
let secret_keys = map["secret_key"].as_table().unwrap();
for key in secret_keys {
sk = key.0.into();
}
info!("Found secret key in config, noted it down.");
Ok(sk)
}