relaymsg: RELAYMSG + draft/relaymsg cap for bridge-style spoofed-nick channel messages
This commit is contained in:
parent
71d6e7441b
commit
0d5717c585
5 changed files with 112 additions and 0 deletions
|
|
@ -120,6 +120,12 @@ amu_target = both
|
||||||
# --- chanlog (m_chanlog): mirror the oper server-notice stream into a channel so
|
# --- chanlog (m_chanlog): mirror the oper server-notice stream into a channel so
|
||||||
# staff can watch it in a normal window. Set the channel (create/keep it opped):
|
# staff can watch it in a normal window. Set the channel (create/keep it opped):
|
||||||
# chanlog = #snotices
|
# chanlog = #snotices
|
||||||
|
# --- relaymsg (m_relaymsg / draft/relaymsg): a member whose client negotiated the
|
||||||
|
# capability can /RELAYMSG <#chan> <nick> <text> to speak under a spoofed relay
|
||||||
|
# nick (for bridges). The nick must contain a separator and not collide.
|
||||||
|
# relaymsg_separators = /
|
||||||
|
# relaymsg_ident = relay
|
||||||
|
# relaymsg_host = relay.example.com # default: the server name
|
||||||
# --- helpmode (m_helpmode): no config — adds oper-settable user mode +h (helpop),
|
# --- helpmode (m_helpmode): no config — adds oper-settable user mode +h (helpop),
|
||||||
# which shows "is available for help" in the user's WHOIS.
|
# which shows "is available for help" in the user's WHOIS.
|
||||||
# --- globops (m_globops): no config — adds the oper command /GLOBOPS <message>,
|
# --- globops (m_globops): no config — adds the oper command /GLOBOPS <message>,
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ pub mod profilelink;
|
||||||
pub mod randquote;
|
pub mod randquote;
|
||||||
pub mod realnameban;
|
pub mod realnameban;
|
||||||
pub mod recaptcha;
|
pub mod recaptcha;
|
||||||
|
pub mod relaymsg;
|
||||||
pub mod reputation;
|
pub mod reputation;
|
||||||
pub mod restrictchans;
|
pub mod restrictchans;
|
||||||
pub mod restrictcommands;
|
pub mod restrictcommands;
|
||||||
|
|
@ -131,5 +132,6 @@ pub fn module_commands() -> Vec<Box<dyn Command>> {
|
||||||
.chain(dccallow::commands())
|
.chain(dccallow::commands())
|
||||||
.chain(geoip::commands())
|
.chain(geoip::commands())
|
||||||
.chain(globops::commands())
|
.chain(globops::commands())
|
||||||
|
.chain(relaymsg::commands())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
99
src/modules/relaymsg.rs
Normal file
99
src/modules/relaymsg.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
//! relaymsg — `RELAYMSG <channel> <nick> <text>` (IRCv3 `draft/relaymsg`): a member
|
||||||
|
//! whose client negotiated the capability sends a channel message under a spoofed
|
||||||
|
//! "relay" nick (e.g. `discord/alice`), for stateless bridges. The message is
|
||||||
|
//! tagged `@draft/relaymsg=<sender>` so clients can attribute it. The spoofed nick
|
||||||
|
//! must contain a configured separator and must not collide with a real nick.
|
||||||
|
//!
|
||||||
|
//! Config: `relaymsg_separators` (default `/`), `relaymsg_ident` (default `relay`),
|
||||||
|
//! `relaymsg_host` (default = server name). Reference: InspIRCd's `m_relaymsg`.
|
||||||
|
//! (Local delivery; cross-server ENCAP relay is not propagated.) Original native Rust.
|
||||||
|
|
||||||
|
use crate::command::{CmdResult, Command};
|
||||||
|
use crate::numeric::{ERR_BADRELAYNICK, ERR_CANNOTSENDTOCHAN, ERR_NOPRIVILEGES, ERR_NOSUCHCHANNEL};
|
||||||
|
use crate::server::Server;
|
||||||
|
use crate::Uid;
|
||||||
|
|
||||||
|
/// Characters never allowed in a spoofed relay nick (core IRC syntax).
|
||||||
|
const FORBIDDEN: &str = "!+%@&#$:'\"?*,.";
|
||||||
|
|
||||||
|
pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
|
vec![Box::new(RelayMsg)]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RelayMsg;
|
||||||
|
impl Command for RelayMsg {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"RELAYMSG"
|
||||||
|
}
|
||||||
|
fn min_params(&self) -> usize {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||||
|
let (chan, nick, text) = (¶ms[0], ¶ms[1], ¶ms[2]);
|
||||||
|
let bad = |s: &mut Server, msg: &str| {
|
||||||
|
s.numeric(uid, ERR_BADRELAYNICK, &format!("{nick} :{msg}"));
|
||||||
|
CmdResult::Fail
|
||||||
|
};
|
||||||
|
|
||||||
|
if !s.users.get(&uid).map(|u| u.caps.relaymsg).unwrap_or(false) {
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
ERR_NOPRIVILEGES,
|
||||||
|
":You must enable the draft/relaymsg capability to use RELAYMSG",
|
||||||
|
);
|
||||||
|
return CmdResult::Fail;
|
||||||
|
}
|
||||||
|
let key = chan.to_ascii_lowercase();
|
||||||
|
if !s.channels.contains_key(&key) {
|
||||||
|
s.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{chan} :No such channel"));
|
||||||
|
return CmdResult::Fail;
|
||||||
|
}
|
||||||
|
if !s.is_member(uid, &key) {
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
ERR_CANNOTSENDTOCHAN,
|
||||||
|
&format!("{chan} :You must be in the channel to use RELAYMSG"),
|
||||||
|
);
|
||||||
|
return CmdResult::Fail;
|
||||||
|
}
|
||||||
|
if s.find_nick(nick).is_some() {
|
||||||
|
return bad(s, "RELAYMSG spoofed nick is already in use");
|
||||||
|
}
|
||||||
|
if nick.chars().any(|c| FORBIDDEN.contains(c)) {
|
||||||
|
return bad(s, "Invalid characters in spoofed nick");
|
||||||
|
}
|
||||||
|
let seps = s
|
||||||
|
.conf("relaymsg_separators")
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.unwrap_or("/")
|
||||||
|
.to_string();
|
||||||
|
if !nick.chars().any(|c| seps.contains(c)) {
|
||||||
|
return bad(
|
||||||
|
s,
|
||||||
|
&format!("Spoofed nick must include one of these separators: {seps}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the fake source and relay it to every member (sender included, so
|
||||||
|
// their own client sees the @draft/relaymsg echo)
|
||||||
|
let ident = s.conf("relaymsg_ident").filter(|v| !v.is_empty()).unwrap_or("relay").to_string();
|
||||||
|
let host = s
|
||||||
|
.conf("relaymsg_host")
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.unwrap_or(s.name.as_str())
|
||||||
|
.to_string();
|
||||||
|
let sender = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default();
|
||||||
|
let body = format!(":{nick}!{ident}@{host} PRIVMSG {chan} :{text}");
|
||||||
|
let ctags = format!("draft/relaymsg={sender}");
|
||||||
|
let msgid = s.next_msgid();
|
||||||
|
let members: Vec<Uid> = s
|
||||||
|
.channels
|
||||||
|
.get(&key)
|
||||||
|
.map(|c| c.members.keys().copied().collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
for m in members {
|
||||||
|
s.send_tagged(m, uid, &ctags, &msgid, &body);
|
||||||
|
}
|
||||||
|
CmdResult::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -119,6 +119,7 @@ pub const RPL_LINKS: u16 = 364;
|
||||||
pub const RPL_ENDOFLINKS: u16 = 365;
|
pub const RPL_ENDOFLINKS: u16 = 365;
|
||||||
|
|
||||||
pub const ERR_NOSUCHNICK: u16 = 401;
|
pub const ERR_NOSUCHNICK: u16 = 401;
|
||||||
|
pub const ERR_BADRELAYNICK: u16 = 573; // RELAYMSG: bad/taken spoofed nick
|
||||||
pub const ERR_NOSUCHCHANNEL: u16 = 403;
|
pub const ERR_NOSUCHCHANNEL: u16 = 403;
|
||||||
pub const ERR_CANNOTSENDTOCHAN: u16 = 404;
|
pub const ERR_CANNOTSENDTOCHAN: u16 = 404;
|
||||||
pub const ERR_NORECIPIENT: u16 = 411;
|
pub const ERR_NORECIPIENT: u16 = 411;
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ pub const SUPPORTED_CAPS: &[&str] = &[
|
||||||
"draft/json-log",
|
"draft/json-log",
|
||||||
"draft/extended-isupport",
|
"draft/extended-isupport",
|
||||||
"reverse.im/filehost",
|
"reverse.im/filehost",
|
||||||
|
"draft/relaymsg",
|
||||||
"cap-notify",
|
"cap-notify",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -155,6 +156,7 @@ pub struct Caps {
|
||||||
pub json_log: bool, // draft/json-log — structured JSON tag on server notices
|
pub json_log: bool, // draft/json-log — structured JSON tag on server notices
|
||||||
pub ext_isupport: bool, // draft/extended-isupport — ISUPPORT command + batched 005
|
pub ext_isupport: bool, // draft/extended-isupport — ISUPPORT command + batched 005
|
||||||
pub filehost: bool, // reverse.im/filehost — knows the file-host extension
|
pub filehost: bool, // reverse.im/filehost — knows the file-host extension
|
||||||
|
pub relaymsg: bool, // draft/relaymsg — may use RELAYMSG (bridge relaying)
|
||||||
pub cap_notify: bool,
|
pub cap_notify: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,6 +224,7 @@ impl Caps {
|
||||||
"draft/json-log" => self.json_log,
|
"draft/json-log" => self.json_log,
|
||||||
"draft/extended-isupport" => self.ext_isupport,
|
"draft/extended-isupport" => self.ext_isupport,
|
||||||
"reverse.im/filehost" => self.filehost,
|
"reverse.im/filehost" => self.filehost,
|
||||||
|
"draft/relaymsg" => self.relaymsg,
|
||||||
"cap-notify" => self.cap_notify,
|
"cap-notify" => self.cap_notify,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
|
|
@ -256,6 +259,7 @@ impl Caps {
|
||||||
"draft/json-log" => &mut self.json_log,
|
"draft/json-log" => &mut self.json_log,
|
||||||
"draft/extended-isupport" => &mut self.ext_isupport,
|
"draft/extended-isupport" => &mut self.ext_isupport,
|
||||||
"reverse.im/filehost" => &mut self.filehost,
|
"reverse.im/filehost" => &mut self.filehost,
|
||||||
|
"draft/relaymsg" => &mut self.relaymsg,
|
||||||
"cap-notify" => &mut self.cap_notify,
|
"cap-notify" => &mut self.cap_notify,
|
||||||
_ => return false,
|
_ => return false,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue