This commit is contained in:
TriMill 2023-06-10 22:35:48 -04:00
parent 6e4d92a030
commit dc47ec918b
16 changed files with 600 additions and 5 deletions

36
Cargo.lock generated
View File

@ -14,6 +14,8 @@ dependencies = [
"matrix-sdk",
"serde",
"serenity",
"strum",
"strum_macros",
"tokio",
"toml 0.7.4",
]
@ -1010,6 +1012,12 @@ dependencies = [
"ahash",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.2.6"
@ -2382,6 +2390,12 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
[[package]]
name = "ryu"
version = "1.0.13"
@ -2650,6 +2664,28 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck",
"proc-macro2 1.0.59",
"quote 1.0.27",
"rustversion",
"syn 1.0.109",
]
[[package]]
name = "subtle"
version = "2.5.0"

View File

@ -8,6 +8,7 @@ readme = "README.md"
[features]
# platforms
irc = ["dep:irc"]
irc-sp = ["tokio/net"]
matrix = ["dep:matrix-sdk"]
discord = ["dep:serenity"]
# tls
@ -16,7 +17,7 @@ tls-rustls = ["irc?/tls-rust", "matrix-sdk?/rustls-tls", "serenity?/rustls_bac
# extra
matrix-e2ee = ["matrix", "matrix-sdk?/e2e-encryption"]
# default
default = ["tls-rustls", "irc", "discord", "matrix", "matrix-e2ee"]
default = ["tls-rustls", "irc", "irc-sp", "discord", "matrix", "matrix-e2ee"]
[dependencies]
async-trait = "0.1"
@ -26,6 +27,8 @@ log = "0.4"
serde = { version = "1.0", features = ["derive", "rc"] }
tokio = { version = "1.28", features = ["rt-multi-thread", "macros", "time", "sync"] }
toml = "0.7"
strum_macros = "0.24"
strum = { version = "0.24", features = ["derive"] }
[dependencies.irc]
optional = true

View File

@ -21,6 +21,7 @@ impl Task for DiscordTask {
async fn start(&self, id: Id, tx: Sender, mut rx: Receiver) -> TaskResult {
let handler = Handler {
links: self.links.clone(),
suffix: self.config.suffix.clone(),
tx,
id,
};
@ -64,7 +65,7 @@ impl DiscordTask {
hook.execute(http, false, |hook| {
hook.username(&msg.author)
.content(&msg.content)
.avatar_url(msg.avatar.as_ref().map(|s| s.as_str()).unwrap_or(""))
.avatar_url(msg.avatar.as_deref().unwrap_or(""))
}).await?;
} else {
channel.say(http, &content).await?;
@ -76,6 +77,7 @@ impl DiscordTask {
struct Handler {
id: Id,
suffix: Arc<str>,
links: Arc<Linkmap<ChannelId>>,
tx: Sender,
}
@ -98,6 +100,7 @@ impl EventHandler for Handler {
origin: (self.id, msg.channel_id.to_string()),
link: link.clone(),
author,
suffix: self.suffix.clone(),
content: msg.content,
avatar,
}).expect("failed to broadcast message");

View File

@ -1,11 +1,17 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use serde::Deserialize;
use serenity::model::prelude::{WebhookId, ChannelId};
fn default_suffix() -> Arc<str> {
"[d]".into()
}
#[derive(Debug, Deserialize)]
pub struct DiscordConfig {
pub token: String,
#[serde(default)]
pub webhooks: HashMap<ChannelId, WebhookId>,
#[serde(default="default_suffix")]
pub suffix: Arc<str>,
}

View File

@ -40,6 +40,7 @@ impl IrcTask {
origin: (id, channel.clone()),
link: link.clone(),
author: author.to_owned(),
suffix: self.config.suffix.clone(),
content: message.clone(),
avatar: None,
})?;

View File

@ -1,7 +1,13 @@
use std::sync::Arc;
use serde::Deserialize;
const fn default_port() -> u16 { 6667 }
const fn default_tls() -> bool { true }
fn default_suffix() -> Arc<str> {
"[i]".into()
}
#[derive(Clone, Debug, Deserialize)]
pub struct IrcConfig {
@ -13,4 +19,6 @@ pub struct IrcConfig {
pub nick: String,
#[serde(default)]
pub alt_nicks: Vec<String>,
#[serde(default="default_suffix")]
pub suffix: Arc<str>,
}

192
src/bridge_irc_sp/bridge.rs Normal file
View File

@ -0,0 +1,192 @@
use std::collections::{HashSet, HashMap};
use async_trait::async_trait;
use log::{debug, warn};
use serde::Deserialize;
use tokio::{net::TcpStream, io::{BufReader, BufWriter, AsyncWrite}, select};
use crate::{supervisor::{Id, TaskResult, Sender, Task, Receiver}, linkmap::Linkmap};
use crate::supervisor::Message as BMessage;
use super::{IrcSpConfig, message::{Message, Command}, network::{MessageReader, MessageWriter}};
#[derive(Debug, Deserialize)]
pub struct IrcSpTask {
config: IrcSpConfig,
links: Linkmap<String>,
}
#[async_trait]
impl Task for IrcSpTask {
async fn start(&self, id: Id, tx: Sender, mut rx: Receiver) -> TaskResult {
let stream = TcpStream::connect((self.config.peer_host.as_ref(), self.config.peer_port)).await?;
let (reader, writer) = stream.into_split();
let mut reader = MessageReader::new(BufReader::new(reader));
let mut ctx = Context {
id,
links: &self.links,
cfg: &self.config,
writer: MessageWriter::new(BufWriter::new(writer)),
tx,
servers: HashSet::from([self.config.server_name.clone()]),
nicks: HashSet::from([self.config.user_nick.clone()]),
local_users: HashSet::from([self.config.user_nick.clone()]),
};
debug!("{id}: identifying");
ctx.writer.write_msg(Message::new(Command::PASS, vec![&ctx.cfg.peer_password, "0210", "abridged|"])).await?;
ctx.writer.write_msg(Message::new(Command::SERVER, vec![&ctx.cfg.server_name, ":Abridged"])).await?;
ctx.writer.flush().await?;
debug!("{id}: checking peer identification");
let passmsg = reader.read_message().await?;
if passmsg.command != Command::PASS {
return Err("Invalid handshake, expected PASS".into())
}
if passmsg.params.first() != Some(&ctx.cfg.password.as_str()) {
return Err("Configured local password does not match password from peer".into())
}
ctx.writer.write_msg(Message::with_prefix(
&ctx.cfg.server_name,
Command::NICK,
vec![&ctx.cfg.user_nick, "0", &ctx.cfg.user_nick, "server", "1", "+", &ctx.cfg.user_nick]
)).await?;
for channel in self.links.iter_channels() {
ctx.writer.write_msg(Message::with_prefix(
&ctx.cfg.user_nick,
Command::JOIN,
vec![channel],
)).await?;
}
ctx.writer.flush().await?;
debug!("{id}: entering event loop");
loop { select! {
msg = reader.read_message() => handle_irc_msg(msg?, &mut ctx).await?,
msg = rx.recv() => handle_bridge_msg(msg?, &mut ctx).await?,
}}
}
}
struct Context<'a, W: AsyncWrite + Unpin> {
id: Id,
links: &'a Linkmap<String>,
cfg: &'a IrcSpConfig,
writer: MessageWriter<W>,
tx: Sender,
servers: HashSet<String>,
nicks: HashSet<String>,
local_users: HashMap<(String, &str), String>,
}
const MALFORMED: &str = "Malformed IRC message";
async fn handle_bridge_msg(msg: BMessage, ctx: &mut Context<'_, impl AsyncWrite + Unpin>) -> TaskResult {
let key = &(msg.author, msg.suffix.as_ref());
let nick = match ctx.local_users.get(&key) {
Some(nick) => nick,
None => {
let nick = msg.author + msg.suffix.as_ref();
ctx.writer.write_msg(Message::with_prefix(
&ctx.cfg.server_name,
Command::NICK,
vec![&nick, "0", &nick, "server", "1", "+", &nick]
)).await?;
ctx.local_users.insert(todo!(), nick.clone());
ctx.nicks.insert(nick.clone());
&nick
}
};
for channel in ctx.links.get_channels(&msg.link) {
if msg.origin.0 == ctx.id && &msg.origin.1 == channel {
continue
}
ctx.writer.write_msg(Message::with_prefix(
&nick,
Command::PRIVMSG,
vec![channel, &msg.content]
)).await?;
ctx.writer.flush().await?;
}
Ok(())
}
async fn handle_irc_msg<'a>(msg: Message<'a>, ctx: &mut Context<'_, impl AsyncWrite + Unpin>) -> TaskResult {
debug!("irc message: {:?}", msg);
match msg.command {
Command::PASS => todo!(),
Command::SERVER => {
let name = *msg.params.first().ok_or(MALFORMED)?;
debug!("linked server {}", name);
ctx.servers.insert(name.to_owned());
}
Command::NICK => {
let prefix = msg.prefix.ok_or(MALFORMED)?;
let name = *msg.params.first().ok_or(MALFORMED)?;
if ctx.nicks.contains(prefix) {
ctx.nicks.remove(prefix);
debug!("updated nick for {} to {}", prefix, name);
} else {
debug!("connecting user {}", name);
}
ctx.nicks.insert(name.to_owned());
},
Command::PING => {
let prefix = msg.prefix.ok_or(MALFORMED)?;
let params = match msg.params.first() {
Some(m) => vec![prefix, m],
None => vec![prefix],
};
ctx.writer.write_msg(Message::with_prefix(&ctx.cfg.server_name, Command::PONG, params)).await?;
ctx.writer.flush().await?;
}
Command::PRIVMSG => {
let channel = msg.params.first().ok_or(MALFORMED)?.to_string();
if let Some(link) = ctx.links.get_link(&channel) {
let author = msg.prefix.ok_or(MALFORMED)?.to_owned();
let content = msg.params.get(1).ok_or(MALFORMED)?.to_string();
ctx.tx.send(BMessage {
origin: (ctx.id, channel.clone()),
link: link.clone(),
author,
suffix: ctx.cfg.suffix.clone(),
content,
avatar: None,
})?;
}
}
Command::ERROR => {
warn!("Error: {:?}", msg.params.first());
}
_ => (),
}
Ok(())
}
/*
+ PASS A_pitowoVsjRgZ 0210 proxy|
+ SERVER c.trimill.xyz :Server Info Text
- :trimill.xyz PASS C_kTNGh3SlfpJ5 0210 proxy|
- :trimill.xyz SERVER trimill.xyz 1 :Server Info Text
- :trimill.xyz NICK trimill 1 trimill server 1 + :trimill
- :trimill.xyz NJOIN #test2 :@trimill
- :trimill.xyz NJOIN #test :@trimill
- :trimill.xyz PING :trimill.xyz
+ :c.trimill.xyz PING :c.trimill.xyz
+ :c.trimill.xyz PONG trimill.xyz :trimill.xyz
- :trimill.xyz PONG c.trimill.xyz :c.trimill.xyz
*/

View File

@ -0,0 +1,33 @@
use std::sync::Arc;
use serde::Deserialize;
const fn default_port() -> u16 { 6667 }
const fn default_nick_len() -> u32 { 9 }
fn default_user_nick() -> String {
"abridged".to_owned()
}
fn default_suffix() -> Arc<str> {
"[i]".into()
}
#[derive(Clone, Debug, Deserialize)]
pub struct IrcSpConfig {
pub server_name: String,
pub peer_host: String,
#[serde(default="default_port")]
pub peer_port: u16,
pub password: String,
pub peer_password: String,
#[serde(default="default_user_nick")]
pub user_nick: String,
#[serde(default="default_nick_len")]
pub max_nick_len: u32,
#[serde(default="default_suffix")]
pub suffix: Arc<str>,
}

View File

@ -0,0 +1,217 @@
use std::{str::{Chars, FromStr}, iter::Peekable, ops::Range, fmt};
use strum_macros::{EnumString, Display};
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumString, Display)]
pub enum Command {
PASS,
SERVER,
NICK,
NJOIN,
PING,
PONG,
MODE,
JOIN,
PRIVMSG,
ERROR,
#[strum(disabled)]
Number(u16),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Message<'a> {
pub prefix: Option<&'a str>,
pub command: Command,
pub params: Vec<&'a str>,
}
impl<'a> Message<'a> {
pub fn new(command: Command, params: Vec<&'a str>) -> Self {
Self {
prefix: None,
command,
params,
}
}
pub fn with_prefix(prefix: &'a str, command: Command, params: Vec<&'a str>) -> Self {
Self {
prefix: Some(prefix),
command,
params,
}
}
}
impl<'a> fmt::Display for Message<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(prefix) = self.prefix {
write!(f, ":{} ", prefix)?;
}
write!(f, "{}", self.command)?;
let paramc = self.params.len();
if paramc > 0 {
for param in &self.params[..(paramc-1)] {
write!(f, " {}", param)?;
}
write!(f, " :{}", &self.params[paramc-1])?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct ParseError {
msg: String,
span: Range<usize>,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}: {}", self.span, self.msg)
}
}
impl std::error::Error for ParseError {}
struct Parser<'a> {
src: &'a str,
chars: Peekable<Chars<'a>>,
pos: usize,
}
impl<'a> Parser<'a> {
fn next(&mut self) -> Option<char> {
self.chars
.next()
.map(|c| {
self.pos += 1;
c
})
}
fn peek(&mut self) -> Option<char> {
self.chars.peek().copied()
}
fn read_word(&mut self) -> &'a str {
let start = self.pos;
while !matches!(self.peek(), Some(' ') | Some('\n') | Some('\r') | None) {
self.next();
}
&self.src[start..self.pos]
}
fn parse(&mut self) -> Result<Message<'a>, ParseError> {
let prefix = if let Some(':') = self.peek() {
self.next();
let p = self.read_word();
match self.peek() {
Some(' ') => { self.next(); },
Some(c) => return Err(ParseError {
msg: format!("expected space, found '{c}'"),
span: self.pos..(self.pos+1),
}),
None => return Err(ParseError {
msg: "expected space, found EOF".to_owned(),
span: self.pos..(self.pos+1)
})
}
Some(p)
} else {
None
};
let command = self.parse_command()?;
let mut params = Vec::new();
while self.peek().is_some() {
params.push(self.parse_param()?);
}
Ok(Message {
prefix,
command,
params,
})
}
fn parse_param(&mut self) -> Result<&'a str, ParseError> {
let Some(' ') = self.peek() else {
let pos = self.pos;
return Err(ParseError {
msg: format!("expected space, found '{}'", self.next().unwrap()),
span: pos..(pos+1),
})
};
self.next();
if let Some(':') = self.peek() {
self.next();
let start = self.pos;
while self.next().is_some() {}
Ok(&self.src[start..])
} else {
let start = self.pos;
while !matches!(self.peek(), Some(' ') | Some('\n') | Some('\r') | None) {
self.next();
}
Ok(&self.src[start..self.pos])
}
}
fn parse_command(&mut self) -> Result<Command, ParseError> {
let start = self.pos;
let name = self.read_word();
if name.len() == 3 {
if let Ok(n) = name.parse::<u16>() {
return Ok(Command::Number(n))
}
}
Command::from_str(name)
.map_err(|_| ParseError {
msg: format!("unknown command: {name}"),
span: start..self.pos
})
}
}
pub fn parse(msg: &str) -> Result<Message, ParseError> {
let mut p = Parser {
src: msg,
chars: msg.chars().peekable(),
pos: 0,
};
p.parse()
}
#[cfg(test)]
mod tests {
use super::{parse, Message, Command, ParseError};
#[test]
fn test() -> Result<(), ParseError> {
assert_eq!(
parse(":example.org PONG irc.example.com :irc.example.com")?,
Message {
prefix: Some("example.org"),
command: Command::PONG,
params: vec!["irc.example.com", "irc.example.com"],
}
);
assert_eq!(
parse("PASS AAAABBBB 0210-IRC+ ngIRCd|26.1:CHLMSXZ PZ")?,
Message {
prefix: None,
command: Command::PASS,
params: vec!["AAAABBBB", "0210-IRC+", "ngIRCd|26.1:CHLMSXZ", "PZ"],
}
);
assert_eq!(
parse(":example.org SERVER example.org 1 :Server Info Text")?,
Message {
prefix: Some("example.org"),
command: Command::SERVER,
params: vec!["example.org", "1", "Server Info Text"],
}
);
Ok(())
}
}

7
src/bridge_irc_sp/mod.rs Normal file
View File

@ -0,0 +1,7 @@
mod config;
mod bridge;
mod network;
mod message;
pub use config::IrcSpConfig;
pub use bridge::IrcSpTask;

View File

@ -0,0 +1,71 @@
use std::{fmt, io};
use tokio::io::{Lines, AsyncBufRead, AsyncBufReadExt, AsyncWrite, AsyncWriteExt};
use log::debug;
use super::message::{Message, parse, ParseError};
#[derive(Debug)]
pub enum ReadMessageError {
String(&'static str),
ParseError(ParseError),
Io(std::io::Error),
}
impl fmt::Display for ReadMessageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReadMessageError::String(e) => write!(f, "{e}"),
ReadMessageError::ParseError(e) => write!(f, "{e}"),
ReadMessageError::Io(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for ReadMessageError {}
pub struct MessageReader<R: AsyncBufRead + Unpin> {
reader: Lines<R>,
buf: String,
}
impl<R: AsyncBufRead + Unpin> MessageReader<R> {
pub fn new(reader: R) -> Self {
Self {
reader: reader.lines(),
buf: String::with_capacity(0),
}
}
pub async fn read_message(&mut self) -> Result<Message, ReadMessageError> {
match self.reader.next_line().await {
Ok(Some(line)) => {
self.buf = line;
parse(&self.buf).map_err(ReadMessageError::ParseError)
}
Ok(None) => Err(ReadMessageError::String("Lost connection to peer server")),
Err(e) => Err(ReadMessageError::Io(e)),
}
}
}
pub struct MessageWriter<W: AsyncWrite + Unpin> {
writer: W,
}
impl<W: AsyncWrite + Unpin> MessageWriter<W> {
pub fn new(writer: W) -> Self {
Self { writer }
}
pub async fn write_msg<'a>(&mut self, msg: Message<'a>) -> Result<(), io::Error> {
let msg = format!("{}\r\n", msg);
debug!("sending message: {:?}", msg);
self.writer.write_all(msg.as_bytes()).await
}
pub async fn flush(&mut self) -> Result<(), io::Error> {
self.writer.flush().await
}
}

View File

@ -22,6 +22,7 @@ pub struct MatrixTask {
struct Context {
id: Id,
tx: Sender,
suffix: Arc<str>,
rooms: Arc<Linkmap<String>>,
}
@ -41,6 +42,7 @@ impl Task for MatrixTask {
client.add_event_handler_context(Arc::new(Context {
id,
tx,
suffix: self.config.suffix.clone(),
rooms: self.links.clone()
}));
client.add_event_handler(handle_matrix_msg);
@ -99,6 +101,7 @@ async fn handle_matrix_msg(ev: SyncRoomMessageEvent, client: Client, room: Room,
origin: (ctx.id, room.room_id().to_string()),
link: link.clone(),
author: sender.name().to_owned(),
suffix: ctx.suffix.clone(),
content: body.to_owned(),
avatar,
})?;

View File

@ -1,8 +1,16 @@
use std::sync::Arc;
use matrix_sdk::ruma::OwnedUserId;
use serde::Deserialize;
fn default_suffix() -> Arc<str> {
"[m]".into()
}
#[derive(Debug, Deserialize)]
pub struct MatrixConfig {
pub user: OwnedUserId,
pub password: String,
#[serde(default="default_suffix")]
pub suffix: Arc<str>
}

View File

@ -7,10 +7,12 @@ use crate::supervisor::Task;
#[derive(Debug, Deserialize)]
#[serde(tag="platform")]
#[serde(rename_all="snake_case")]
#[serde(rename_all="kebab-case")]
pub enum Node {
#[cfg(feature="irc")]
Irc(crate::bridge_irc::IrcTask),
#[cfg(feature="irc-sp")]
IrcSp(crate::bridge_irc_sp::IrcSpTask),
#[cfg(feature="matrix")]
Matrix(crate::bridge_matrix::MatrixTask),
#[cfg(feature="discord")]
@ -22,6 +24,8 @@ impl Node {
match self {
#[cfg(feature="irc")]
Node::Irc(t) => Box::new(t),
#[cfg(feature="irc-sp")]
Node::IrcSp(t) => Box::new(t),
#[cfg(feature="matrix")]
Node::Matrix(t) => Box::new(t),
#[cfg(feature="discord")]

View File

@ -12,6 +12,8 @@ mod config;
#[cfg(feature="irc")]
mod bridge_irc;
#[cfg(feature="irc-sp")]
mod bridge_irc_sp;
#[cfg(feature="matrix")]
mod bridge_matrix;
#[cfg(feature="discord")]

View File

@ -1,4 +1,4 @@
use std::{fmt, error::Error, time::Duration, any::Any, panic::AssertUnwindSafe, collections::HashMap};
use std::{fmt, error::Error, time::Duration, any::Any, panic::AssertUnwindSafe, collections::HashMap, sync::Arc};
use async_trait::async_trait;
use futures::{stream::FuturesUnordered, StreamExt, FutureExt};
@ -36,6 +36,7 @@ pub struct Message {
pub origin: (Id, String),
pub link: Link,
pub author: String,
pub suffix: Arc<str>,
pub content: String,
pub avatar: Option<String>,
}