mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 22:57:59 -05:00
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:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(®ister_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()))
|
||||
}
|
||||
|
||||
881
bin/darkirc/src/irc/command.rs
Normal file
881
bin/darkirc/src/irc/command.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
205
bin/darkirc/src/irc/rpl.rs
Normal 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 channel’s 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 wasn’t
|
||||
/// 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 wasn’t
|
||||
/// 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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user